Gitlab CI でマージリクエストのマージ結果でパイプラインを実行する

Gitlab は マージリクエストに対してそれがマージされた結果を元にパイプラインを実行できます。

便利そうですけど、これは gitlab.com なら Silver 以上、セルフホスティングなら Premium 以上じゃないと使えないようなので、Gitlab CE でも似たようなことをやる方法。

要するにパイプラインの実行時にマージしてしまえばいいので次のような感じに。

image: gitコマンドが使えるイメージ

stages:
  - build

.merge_result: &merge_result >
  [ "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-}" != "" ]
  && git checkout "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
  && git merge --squash -v -
  && git diff --stat --staged

build:
  stage: build
  rules:
    - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"
  script:
    - *merge_result
    - いろいろ

[ "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-}" != "" ] のような条件を付けているのは同じジョブがマージリクエスト以外から実行されたとき用です(この例だと rules$CI_MERGE_REQUEST_TARGET_BRANCH_NAME を見ているのでマージリクエスト以外から実行されることはありませんが)。

その後の git checkoutgit merge で実際にマージリクエストのターゲットブランチをチェックアウトして、ソースブランチをマージしています。--squash なのでマージはされるもののマージコミットは作成されず、ワークツリーとインデックスに書き込まれるのみです。

マージコミットを作成するようにしても良いと思いますが、その場合は git config user.name とか git config user.email とかも必要です。

最後に git diff --stat --staged で、このマージでどんなことが行われたかをざっくり表示しています。

さいごに

Gitlab でマージリクエストでパイプラインを実行できると知ってから結構使ってましたが、てっきりマージ結果で実行されるのがデフォだと思ってました。

例えばマージリクエストのパイプラインでデプロイを実行していると、素のままだとマージ前のコードがデプロイされてしまうので要注意です。

この方法でとりあえず対応はできますが、Gitlab の WebUI からはそれがわからないし、ジョブを手動で再実行したときにターゲットブランチが進んでしまっていると同じワーキングツリーが再現できなかったり、いろいろ問題もありそうなのであきらめるかどうしても必要なら素直に Premium にするか考えたほうが良いかも。

PHP版の Power Assert もどき「Phpower」

js には Power Assert という便利なパッケージがあります。私自身は js でテストをほとんど書いたことがないので使ったこと無いのですが、例えば次のようなアサーションが、

assert(ary.indexOf(zero) === two)

次のように assert の各部位の値が表示されるようになります(README より)。

  assert(ary.indexOf(zero) === two)
         |   |       |     |   |
         |   |       |     |   2
         |   -1      0     false
         [1,2,3]

  [number] two
  => 2
  [number] ary.indexOf(zero)
  => -1

通常のアサーションライブラリでは大量のアサーション API があり、わかりやすい出力を得るためには API を使い分ける必要があります。Power Assert ならだいたい assert ひとつで上記のようなわかりやすい出力が得られるため、大量の API を覚える必要が無いメリットがあります。

これはソースコードの AST を書き換えて assert を拡張することで実現されているのですが、今日日の PHP ならソースコードを解析して AST を得るのは容易なので、同じようなものを PHP で作ってみました。

Phpower

Composer でインストールできます。重要な点として PHPUnit も Composer でインストールする必要があります。

composer require --dev phpunit/phpunit ngyuki/phpower:dev-master

例えば次のようなテストコードが、

<?php
namespace Test;

use PHPUnit\Framework\TestCase;

class SomeTest extends TestCase
{
    public function test()
    {
        $ary = [1,2,3];
        $zero = 0;
        $two = 2;
        assert(array_search($zero, $ary, true) === $two);
    }
}

次のように出力されます。

Assertion failed array_search($zero,$ary,true) === $two -> false
# $zero -> 0
# $ary -> [
#           1,
#           2,
#           3,
#         ]
# array_search($zero,$ary,true) -> false
# $two -> 2

Power Assert のように罫線を引きたいところですが簡単ではなさそうなので上のような出力になっています。値の表示には symfony/var-dumper を使っています。配列が要素ごとに改行されるので縦幅がちょっと冗長な気もしますね。

いくつか制限もあります。今判っている限りでは次のような制限があります。

phpunit.phar は未サポート

PHPUnit はテストコードを読み込むときに PHPUnit\Util\FileLoader というクラスを使っています。PHPUnit がこのクラスをロードする前に同名のクラスを先に定義することで PHPUnit\Util\FileLoader の実装を差し替えて、テストコードが読み込まれるときにコードの書き換えを行っています。

PHPUnit\Util\FileLoader の差し替え版の読み込みは composer.jsonautoload.files で行うようにしているので vendor/autoload.php が読まれた時点で差し替えが行われます。Composer で入れた PHPUnit であれば vendor/bin/phpunit の中で一番最初に vendor/autoload.php が読まれるのでこれは有効に働きます。

しかし、phpunit.phar の場合、phpunit.phar のスタブコードで phpunit.phar に含まれるコードが一括で読まれています。そこに PHPUnit\Util\FileLoader も含まれているため差し替えることができず、テストコードを書き換えることができません。

ので、phpunit.phar で実行した場合は assert はただの assert として機能します。

リファレンス引数の関数呼び出し

リファレンス引数を持つ関数呼び出しが含まれると元の assert と互換性がなくなります。

例えば次のようなコードです。

function f(&$r)
{
    return ++$r;
}
$a = 0;
assert(f($a)); // Notice: Only variables should be passed by reference
assert($a === 1);

これは assert の中身を次のように書き換えているためです(簡略化しています、実際のものとは異なります)。

_expr(_cap(f(_cap($a))))

f(_cap($a)) のように、リファレンス引数であるはずの f 関数の引数が _cap 関数の戻り値になるため Only variables should be passed by reference となり、関数に $a の参照が渡らなくなります。

さいごに

assert だけで式の中の途中経過まですべて表示されるのは便利だと思う反面、PHPUnit の assertSame などに配列を渡せば expected と actual の diff が表示されるので、どこが間違っているか直ぐに把握できて便利です。

Phpower だと差分にはならないし、また、情報量も無駄に多くなりがちなので、どこが間違っているかをぱっと見で把握するのはなかなか困難です。

ので、アサーションを全部置き換えるようなことはできず、結局使い分けは必要でしょう。もっとも、まだ作ったばかりでどの程度使えそうか自分でも判らないので、とりあえずどこかのプロジェクトに突っ込んでみて使用感を見つつ改善などしていこうと思います。

docker-compose で PostgreSQL のストリーミングレプリケーションをサクッと試す

PostgreSQL のレプリケーションははるか昔に pgpool (pgpool-II ではなく) でレプリケーションさせて以来まったく触ってなかったので、組み込みのストリーミングレプリケーションをサクッと試してみました。

他にも色々パラメータを調整すべきものがあるらいしのだけど、とりあえずそれっぽく動いているところまで。

docker-compose で MySQL レプリケーション環境をサクッと用意する でやったように /docker-entrypoint-initdb.d/*.sh を使って docker-compose up だけでできるとよいかと思ったのだけど面倒だったので泥臭く構築。

docker-compose.yml は次の通り。

version: '3.7'

services:

  master:
    image: postgres:11-alpine
    ports:
      - 15432:5432
    environment:
      POSTGRES_DB: test
    volumes:
      - master:/var/lib/postgresql/data

  slave:
    image: postgres:11-alpine
    ports:
      - 25432:5432
    environment:
      POSTGRES_DB: test
    volumes:
      - slave:/var/lib/postgresql/data

volumes:
  master:
  slave:

次のように構築&動作確認。

# master のデータベースディレクトリを初期化
# リッスン開始した風のログが表示されたところで Ctrl+C で止める
docker-compose run --rm master

# pg_hba.conf を見てみる
docker-compose run --rm master cat /var/lib/postgresql/data/pg_hba.conf

# `host all all all trust` があるのだけどこれだけではレプリケーションはできないものなの?(よくわからない)
# ので追記
echo "host replication all all trust" | docker-compose run -T --rm master tee -a /var/lib/postgresql/data/pg_hba.conf

# master を開始する
docker-compose up -d master

# レプリケーションユーザーの作成
echo 'CREATE ROLE repl_user LOGIN REPLICATION' | psql -h127.0.0.1 -p15432 -Upostgres

# slave で master から pg_basebackup してデータディレクトリを初期化
docker-compose run --rm slave pg_basebackup -h master -D /var/lib/postgresql/data -R --progress -U repl_user

# slave も開始
docker-compose up -d

# master に接続してレプリケーションの状態を見たりテーブルを作ってデータを入れたりする
psql -h127.0.0.1 -p15432 -Upostgres test
  SELECT * FROM pg_stat_replication;
  create table t (id int not null primary key, name text not null);
  insert into t values (1, 'ore');
  insert into t values (2, 'are');
  insert into t values (3, 'sore');

# slave に接続してレプリケーションされたデータを見たりデータを入れようとしてみる
psql -h127.0.0.1 -p25432 -Upostgres test
  select * from t;
  insert into t values (4, 'dore');
  #=> ERROR:  cannot execute INSERT in a read-only transaction

参考

「入力した項目だけで検索」に IS NOT FALSE を使うなら被検索列が NULL にならないか注意

わりとよくある、複数の項目に検索条件が入力できて、入力した項目だけを検索条件として使用し、未入力の項目は検索条件には使わない、というフォーム、クエリビルダのようなものを使わないなら IS NOT FALSE を使うと便利です。

SELECT * FROM t WHERE (
    a = :a AND b = :b AND c = :c
) IS NOT FALSE

NULL と比較すると UNKNOWN となり、UNKNOWN AND TRUEUNKNOWNUNKNOWN AND FALSEFALSE、というSQLの3論理値の特徴を利用しています。

  • a = :ab = :bTRUEc = :cUNKNOWN なら TRUE AND TRUE AND UNKNOWNUNKNOWN なので IS NOT FALSETRUE
  • a = :aTRUEb = :bFALSEc = :cUNKNOWN なら、TRUE AND FALSE AND UNKNOWNFALSE なので IS NOT FALSEFALSE

ただこれは被検索列の abcNOT NULL の場合のみ期待した結果になります。もし被検索列が NULL だと、例えば aNULL だった場合、:a がどんな値だったとしても結果は a = :aUNKNOWN になるため :a になにを入力してもヒットします。

具体例 v1

例えばあるテーブルに日付の FROM と TO が記録されていて、入力した FROM と TO と期間が重なるレコードを検索する、ただし FROM や TO が未入力なら条件として使わない、というクエリは IS NOT FALSE で次のように書けます。

SELECT * FROM t WHERE (
        date_from <= :date_to
    AND date_to   >= :date_from
) IS NOT FALSE

がしかし、もし t.date_fromNULL だったとすると :date_to になにを入力しても検索にヒットしてしまいます。

ん? t.date_fromNULL なら大抵の場合は開始日が無期限というか無限の過去みたいな扱いだろうので、何を入力してもヒットするのは正しいのでは。

というわけでこれは例が悪かったです。むしろ IS NOT FALSE を使えば簡単になる良い例でした。

具体例 v2

例えば、カテゴリとかで整数型な ID でカテゴリテーブルと関連させていて、カテゴリ未設定のときに NULL が入るようにしているとき。

SELECT * FROM t WHERE (
        date_from <= :date_to
    AND date_to   >= :date_from
    AND category   = :category
) IS NOT FALSE

検索条件に「カテゴリ=フルーツ」とか入れて 検索して、カテゴリが未設定の行が検索結果に表示されると変ですね。

どう書くのがキレイでしょうかね。

/* IS NOT FALSE の外に出して IS NULL を付ける */
SELECT * FROM t WHERE (
        date_from <= :date_to
    AND date_to   >= :date_from
) IS NOT FALSE
AND (category = :category OR :category IS NULL);
/* IS NOT FALSE の内側に置くなら <=> を使う */
SELECT * FROM t WHERE (
        date_from <=  :date_to
    AND date_to   >=  :date_from
    AND (category <=> :category OR :category IS NULL)
) IS NOT FALSE
/* IFNULL でもいいかも */
SELECT * FROM t WHERE (
        date_from <= :date_to
    AND date_to   >= :date_from
    AND category <=> IFNULL(:category, category)
) IS NOT FALSE

さいごに

要するに「入力した項目だけで検索」で IS NOT FALSE を使うときに被検索列が NULL になる可能性があるならその NULL が検索でどのように扱うのかを留意する必要があるということですね。

あと、↑のどの書き方でもインデックスはまあまともに使われないと思うので、実は NULL が来ることはないのだけどコピペで IS NOT FALSE にしてしまってパフォーマンス出ない、ということにならないように注意する必要もあります。

Ansible AWX を素振りしたメモ

Ansible AWX を素振りしたメモ。WebUI から Ansible を実行して実行結果を記録・閲覧したり、役割ベースでアクセスコントロールしたり、スケジュールで自動で Ansible を実行したり、メールや Slack などで通知したり、などなど、いろいろできます。

Ansible AWX はエンタープライブ向けの Ansible Tower に対する、無償のアップストリーム版という位置付けです。

AWX と Red Hat Ansible Tower の比較

インストール

下記を参考に適当な CentOS 7 のホストの Docker 環境にセットアップします。

https://github.com/ansible/awx/blob/devel/INSTALL.md

インストーラーとして Ansible を使うので Ansible を入れます。

yum install -y epel-release
yum install -y ansible

AWX のソースをダウンロードして展開します(欲しいのは installer/ などのごく一部なんですけどね)。

wget https://github.com/ansible/awx/archive/9.1.0.tar.gz
tar xvzf 9.1.0.tar.gz
cd awx-9.1.0/installer/

プロジェクトのディレクトリを -e project_data_dir=/var/lib/awx/projects のように指定しつつプレイブックを実行します(インベントリファイルを書き換えても OK です)。 このディレクトリに Git などの SCM からチェックアウトしたソースが保存されます。未指定だと AWX のコンテナのローカルに保存されるため、コンテナを再起動すると消えてしまします。 AWX の管理データは PostgreSQL に保存されており、チェックアウトしたソースが消えたとしても再チェックアウトすれば良いだけではありますが。なのでデフォでは未指定になっているのでしょうか。

ansible-playbook -i inventory install.yml -e project_data_dir=/var/lib/awx/projects

プレイブックを実行すると次のようなエラーでコケました。

# TASK [local_docker : Start the containers]
fatal: [localhost]: FAILED! => changed=false
  msg: 'Failed to import the required Python library (Docker SDK for Python: docker (Python >= 2.7) or docker-py (Python 2.6)) on ore-no-awx-server''s Python /bin/python. Please read module documentation and install in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter, for example via `pip install docker` or `pip install docker-py` (Python 2.6). The error was: No module named requests.exceptions'

Python 2.7 以上なら docker モジュールが、2.6 なら docker-py が必要とのことです。ホストは CentOS 7 だったので Python 2.7 なのですが、docker-py なら yum でさくっと入りそうでしたが docker の方は pip を使う必要がありそうでした。

見た感じ、単に ~/.awx/awxcompose/ の中で docker-compose でサービスを開始しようとしているだけだったので、次のように手動で開始させました。

cd ~/.awx/awxcompose/
docker-compose up -d

ホストの 80 ポート HTTP で開くとログイン画面になります。admin / password でログインできます。

ジョブを実行してみる

下記を参考に「組織」「認証情報」「プロジェクト」「インベントリ」「ジョブテンプレート」を作成し、Ansible のジョブが実行できることを確認しました。

https://dev.classmethod.jp/server-side/ansible/ansible-awx_rk-20180225/

次のように AWX の画面上で実行結果などが閲覧できます。

f:id:ngyuki:20200125154953p:plain

以下は補足的なメモ。

「組織」が「プロジェクト」を作成する前に必要でした。プロジェクトは組織の中に作る必要があるのですが、AWX をセットアップ直後は組織が空だったので適当なものを作りました。

「プロジェクト」で指定する Git リポジトリを SSH でチェックアウトするために「認証情報」を登録しました。まず、手元で SSH 秘密鍵を作成し、Gitlab リポジトリのデプロイキーに公開鍵を登録します。秘密鍵は AWX の「認証情報」に登録します。認証タイプは「ソースコントロール」です。ユーザー名は指定せず SSH 秘密鍵のみ入力します。

「インベントリー」はホストを手入力で作成するのではなく、プロジェクトの Git リポジトリの中のインベントリファイルを指定しました。 インベントリで「ソース」を選択、ソースの種類に「ソース:プロジェクト」をします。 なぜか「インベントリーファイル」のドロップダウンの中身が空だったため、手打ちする必要がありました。

インベントリファイルには次のように ansible_connection: local なものを用意しました。認証情報を用意するのがめんどくさかったためです。普通に SSH するならそれ用の SSH 秘密鍵も認証情報に認証タイプ「マシン」で登録する必要があります。

all:
  hosts:
    localhost:
      ansible_connection: local

さいごに

Ansible の実行環境が開発者のローカルだと微妙な環境の差が問題になることがあります。Gitlab CI のパイプラインで実行することでその問題を避けることはできますが、Gitlab CI でやるなら Ansible AWX でやっても良いかもしれません。もしくは Gitlab CI のパイプラインから REST API で Ansible AWX を叩く構成でもありかも。

ただ、プロジェクト・インベントリ・テンプレートなどの設定が基本的に UI からポチポチなのが煩雑に感じます。API でも登録できるだろうとは思いますが。

ソースリポジトリだけ設定すればソースのルートの特定ファイル(.ansible-awx.yml とか)から自動的にインベントリやジョブテンプレート・ワークフローテンプレートなどの情報を拾ってきて、ジョブの実行や結果の閲覧のみ UI からポチる、ぐらいのほうが扱いやすいように感じます。

また、そもそものことろ Ansible での構成管理は(アプリのデプロイと比べて)そこまで頻繁には変更しないので、そのために Ansible AWX を設けるのもちょっと無駄な気もしないでもないです。ただアプリのデプロイをいま Ansible でやっているので、それを Ansible AWX でやるように置き換えるのはありかも。

Ansible の synchronize で不必要に changed になるのを防止+α

Ansible で大量のファイルをコピーしたいときは copy よりも synchronize の方が早いですが、

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/

これだけだと rsync-a が指定された状態になるので不必要に changed になることがあります。

タイムスタンプ

rsync -a はタイムスタンプも同期されるため、ファイルのタイムスタンプが異なるだけで changed になります。Git はファイルのタイムスタンプは維持しないので、ソースをチェックアウトし直しただけで changed になってしまいます。

タイムスタンプを同期しないよう times: no にしつつ、比較でタイムスタンプが無視されるように checksum: yes を付けます。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no

UID/GID

rsync -a はリモート側が root で実行されていると(become: yes なり remote_user: root なり)ファイルの UID/GID も同期されるため、異なるホストで異なる UID/GID でチェックアウトしたソースだと changed になります。そのような運用は避けて Ansible を実行するための専用ホスト&アカウントを設ければ解決ではあるのですが、そうは言っても手元で実行したいこともあるでしょう。

そもそも Ansible で synchronize するときにローカルのファイルの UID/GID とターゲットホストのファイルの UID/GID を一致させたいということはあまりないでしょう(ターゲットが localhost だったり、後述のように delegate_to を使うなら別ですけど)。

ので、owner: nogroup: no を付けて UID/GID が同期されないようにします。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        owner: no
        group: no

もしくは、rsync_opts--chown=USER:GROUP でユーザー・グループを指定します。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        rsync_opts:
          - --chown=nobody:nobody

パーミッション

rsync -a はファイルのパーミッションも同期されます。Git は実行属性だけは維持されますがそれ以外の属性は維持されないので、チェックアウト時の umask が異なると changed になります。

ので、perms: no を付けてパーミッションが同期されないようにします。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        owner: no
        group: no
        perms: no

もしくは、rsync_opts--chmod=CHMOD で指定します。

ファイルとディレクトリで固定の値を指定するなら次のように。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=D755,F644

ファイルに実行属性付きのスクリプトなどが混在しているのなら次のように。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=u=rwX,go=rX

むしろ archive: no にして必要なものだけ指定するほうが良いかもしれません。最低限なら次のように。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        archive: no
        recursive: yes
        checksum: yes

UID/GID やパーミッションを同期させるなら次のように。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        archive: no
        recursive: yes
        checksum: yes
        owner: yes
        group: yes
        perms: yes
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=u=rwX,go=rX

copy モジュールは mode とか owner とか group とかで指定しない限り同期されないわけなので、synchronize でも同期したいものだけ指定するほうがわかりやすいです。

use_ssh_args

本題とは関係ないのですが、synchronize には use_ssh_args というオプションがあり、これを指定すると下記などで指定された ssh のコマンドライン引数が rsyncssh にも付与されます。

  • ansible.cfg[ssh_connection] セクションの ssh_args
  • 変数 ansible_ssh_common_args
  • 変数 ansible_ssh_extra_args

Bastion ホストを中継するために ansible_ssh_common_args などで -J bastion などとしているときは use_ssh_args: yes にしておかないとこの引数が追加されないため失敗します。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        archive: no
        recursive: yes
        checksum: yes
        owner: yes
        group: yes
        perms: yes
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=u=rwX,go=rX
        use_ssh_args: yes

また、ドキュメントには無いのですが ssh_args で直接コマンドライン引数を指定できます。

https://github.com/ansible/ansible/blob/v2.9.2/lib/ansible/modules/files/synchronize.py#L407

- hosts: all
  become: yes
  tasks:
    - synchronize:
        src: many-files/
        dest: /tmp/many-files/
        archive: no
        recursive: yes
        checksum: yes
        owner: yes
        group: yes
        perms: yes
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=u=rwX,go=rX
        ssh_args: -C -J bastion

use_ssh_args: yes と併用すると use_ssh_args: yes が優先されます。というか use_ssh_args: yes によってモジュールの ssh_args オプションが設定される形なので ssh_args を直接は使わないほうが良いです。

https://github.com/ansible/ansible/blob/v2.9.2/lib/ansible/plugins/action/synchronize.py#L384-L390

接続の共通は使用されない

本題とは関係ないのですが、synchronize では ssh のコマンドラインに -S none が固定で追加されるため ControlMaster による接続の共有は使用されず、毎回新規に接続されます。

https://github.com/ansible/ansible/blob/v2.9.2/lib/ansible/modules/files/synchronize.py#L523 ssh_cmd = [module.get_bin_path('ssh', required=True), '-S', 'none']

これは下記の問題に対応するために追加されたようです。

ControlPersist sockets conflict with synchronize and rsync commands in playbook · Issue #8473 · ansible/ansible

プレイブックを実行する sshrsyncssh が競合するため? ControlMaster の接続の共有は1本の接続を複数の ssh で共有できるので大丈夫そうですけど・・なにが問題なのかは良くわかりませんでした。

delegate_to

本題とは関係ないのですが、synchronize モジュールは delegate_to を指定したときの動きが他のモジュールと異なります。

synchronize モジュールは、要するにローカルホスト上でローカルファイルをソースとしてターゲットホストを宛先に rsync するものですが、delegate_to を指定すると rsync を実行するローカルホストを別のホストに変更することができます。

例えば、copy だと delegate_to はコピーする先のターゲットホストになりますが、synchronize だと delegate_to はコピーする元のソースホストになります。つまり、delegate_to で指定したホストから本来のターゲットホストへの rsync になります。

- hosts: are
  become: yes
  tasks:
    - synchronize:
        src: /tmp/many-files/
        dest: /tmp/many-files/
      delegate_to: ore

この例では ore から arersync されます。もちろん ore から are への ssh を通す必要があります。ansible.cfgssh_connection.ssh_args-A を追加するなどしてエージェントフォワーディングを有効にするなどが必要です。

パッシブチェックのアラートを freshness で自動で解除する

snmptrap などによる通知を Nagios のパッシブチェックで受けるとき、アラートになったあと手動で解除するまでずっとアラート状態のままになります。解除(OK)もパッシブで受けるようになっていれば別ですけど・・・

なんとなくずっとアラート状態のままなのも気持ち悪いので Freshness チェックを用いて一定時間後に自動で解除されるようにしてみます。

define service{
    use                     generic-service
    host_name               hoge-server
    service_description     Passive check

    # パッシブチェックのためのオマジナイ
    active_checks_enabled   0
    passive_checks_enabled  1
    is_volatile             1
    max_check_attempts      1

    # 通知は1回だけ&復旧は通知しない(お好みで)
    notification_interval   0
    notification_options    w,u,c

    # 3600 秒でアラートを解除する
    check_freshness         1
    freshness_threshold     3600
    check_command           check_dummy!0 "Passive check cleared automatically over 60 minutes"
}

# ↑で使っている check_dummy の定義
define command {
    command_name    check_dummy
    command_line    /usr/lib64/nagios/plugins/check_dummy $ARG1$
}

アラート状態になったあと、1時間何もなければ自動的にアラートが解除されます。