KVM の 1 台の仮想化ホストの中の複数のゲスト間でネットワーク分断を発生させる

とあるシステムの検証用の環境で 1 台のホストの中で複数のゲストでクラスタリングしており、そのノード間でネットワーク分断を発生させたかったときのメモ。

LANケーブルを抜こうにも抜くべきケーブルがありません。

ebtables

ぱっと思いついたのがこの方法、KVM + libvirt で仮想化しているならホストで ebtables が有効になっていると思うので、

lsmod | grep ebtables
# ebtables

まずゲストの I/F を調べて、

virsh domiflist sv01
# vnet1
virsh domiflist sv02
# vnet2

ebtables でこれらの通信を DROP します。

ebtables -A FORWARD -j DROP -i vnet1 -o vnet2

戻すときは↑で追加したもの以外にルールがないことを確認のうえで、

ebtables-save

-F でルールを削除します。

ebtables -F FORWARD

もし↑で追加したもの以外にルールがあるなら -D でルール番号を指定して削除する必要があります。

brctl

試していないですけど、brctl でゲストの仮想 I/F をブリッジから引っこ抜いても OK だと思います。

btcrl delif br0 vnet1

次のように戻します。

btcrl addif br0 vnet1

この方法はそのゲストの I/F が完全に死ぬのでそのゲストに ssh とかも繋がらなくなってしまうのが難点です。 (virsh console とかでならつながる)

tcpkill

試していないですけど、tcpkill があれば tcpdump と同じような記法でパケットを Reject させられるようです。

CentOS 7 なら dsniff パッケージを入れると一緒に入ります。

yum install -y dsniff

仮想化のホストではなくゲスト上で次のように分断させたい通信内容を tcpdump のパターンと同じように記述します。

tcpkill -i any host 192.0.2.123

dsniff には他にも ARP スプーフィングなどの面白げなツールが含まれているので、とりあえずいれておくと良い一品かもしれません。

KVM ホストの NetworkManager 管理の DNS の設定を変える

とある KVM ホストの NetworkManager 管理の DNS の設定を変えようとしたときのこと。

cat /etc/resolv.conf | grep nameserver
# 古い dns サーバになってるぜ

nmcli con modify br0 ipv4.dns 192.0.2.100
# 変更するぜ

nmcli con up br0
# 変更を反映するぜ

cat /etc/resolv.conf
# 変更が反映されているぜ

おしまい

.

.

.

とはなりませんでした。この KVM ホストの中のゲストのネットワークが全死しました。

KVM でゲストの I/F をブリッジで構成する場合(つまりホストの I/F を共有する構成)、次のようにホストのブリッジ br* に仮想I/F vnet* がぶら下がるようになります。

brctl show
# bridge name     bridge id               STP enabled     interfaces
# br0             8000.1234567890ab       no              eth0
#                                                         vnet0
#                                                         vnet1
#                                                         vnet2

この「br0vnet* がぶら下がっている」という構成は NetworkManager の管理外なので nmcli con up br0 などとすると。

brctl show
# bridge name     bridge id               STP enabled     interfaces
# br0             8000.1234567890ab       no              eth0

のように全部外れてしまいます。

対応策 v1

手でもとに戻せば OK です。

btctl addif br0 vnet0
btctl addif br0 vnet1
btctl addif br0 vnet2

昔なにかのときに実際のこの方法で復旧させたことがあるのでたぶん大丈夫です、たぶん。

対応策 v2

ゲストを再起動(停止→開始)させれば戻ります(reboot だとダメだったかも)

virsh shutdown guest0
virsh shutdown guest1
virsh shutdown guest2

virsh list --all

virsh start guest0
virsh start guest1
virsh start guest2

対応策 v3

もっと良い方法があるはず・・・

要するに nmcli con up br0 だと NetworkManager 管理外のブリッジの構成が外れてしまうことが問題なので、別の方法で dns の変更を反映させられればよいのですが、試しに適当な環境で試行錯誤しました。

# ブリッジを作る
nmcli con add type bridge ifname br0 con-name br0 stp no

# ブリッジの設定
nmcli con mod br0 \
    ipv4.method manual \
    ipv4.addresses 192.168.0.123/23 \
    ipv4.gateway 192.168.0.1 \
    ipv4.dns 192.168.0.2 \
    ipv6.method ignore

# ブリッジを有効にする
nmcli con down eth0
nmcli con up br0

# NetworkManager の管理外で eth0 をブリッジに入れる
brctl addif br0 eth0

# 試しに `nmcli con up br0` してみる
nmcli con up br0

# ブリッジから eth0 が外れてます
brctl show
# bridge name     bridge id               STP enabled     interfaces
# br0             8000.000000000000       no

# 元に戻します
brctl addif br0 eth0
brctl show
# bridge name     bridge id               STP enabled     interfaces
# br0             8000.1234567890ab       no              eth0

# dns を変更します
nmcli con mod br0 ipv4.dns 192.168.0.3

# NetworkManager をリスタートします
systemctl restart NetworkManager

# 新しい dns に・・反映されてるぜ
cat /etc/resolv.conf | grep nameserver
# nameserver 192.168.0.3

# ブリッジの構成は・・変わってないぜ
brctl show
# bridge name     bridge id               STP enabled     interfaces
# br0             8000.1234567890ab       no              eth0

というわけで単に NetworkManager をリスタートするのが正解なのかもしれません。今までなんとなく考えなしに nmcli con mod した後は nmcli con up で反映させてました・・・

対応策 v4

そもそものところ resolv.conf を書き換えたいだけなのに変なリスクを犯すのもどうかと思うので NetworkManager が resolv.conf を書き換えないようにしてしまえば良い気がする。

/etc/NetworkManager/NetworkManager.conf

[main]
plugins=ifcfg-rh
dns=none

Gitlab Runner で分散キャッシュや --docker-volumes を使ってジョブ間のキャッシュを共有する

Gitlab Runner で concurrent が 1 より大きい場合、パイプラインを 1 つの Runner で実行するよう構成したとしてもジョブ間でキャッシュが共有されないことがあります。

例えば concurrent = 4 な Runner で次のパイプラインを実行します。

image: alpine

stages:
  - stage1
  - stage2

1A:
  stage: stage1
  tags: [ore]
  cache:
    key: {}
  script:
    - echo ok

1B:
  stage: stage1
  tags: [ore]
  cache:
    key: hoge
    paths:
      - cache.txt
  script:
    - touch cache.txt
    - cat cache.txt
    - echo "$CI_JOB_NAME" > cache.txt

2A:
  stage: stage2
  tags: [ore]
  cache:
    key: hoge
    paths:
      - cache.txt
  script:
    - touch cache.txt
    - cat cache.txt
    - echo "$CI_JOB_NAME" > cache.txt

1B と 2A は stage が異なる& key: hoge でキャッシュキーを固定値にしているのでキャッシュが共有されそうですが、実際には共有されません。

concurrent = 4 の場合、Runner が 1 つしか登録されていなくても実際には下記の 4 つの Runner が存在するのと同じような状態になっているためです。

  • runner-XXXXXXXX-project-N-concurrent-0
  • runner-XXXXXXXX-project-N-concurrent-1
  • runner-XXXXXXXX-project-N-concurrent-2
  • runner-XXXXXXXX-project-N-concurrent-3

ジョブ開始時の runner-XXXXXXXX-project-N-concurrent-0 via ... のようなメッセージでどの Runner で実行されているかわかります。

キャッシュはこのそれぞれの Runner ごとに保持されます。そのため、↑のパイプラインだと次のように Runner とジョブが対応して実行されるので、

  • runner-XXXXXXXX-project-N-concurrent-0: 1A -> 2A
  • runner-XXXXXXXX-project-N-concurrent-1: 1B

1A と 2A ならキャッシュが共有されますが、1B とは共有されません。

.gitlab-ci.yml 上で 1A と 1B の位置を入れ替えたり、あるいは同じ Runner で実行されている別のジョブがあってたまたま 1B と 2A が同じ runner-XXXXXXXX-project-N-concurrent-? で実行されれば共有されることもあります。

Docker executor のキャッシュの保存先

ジョブが実行されるコンテナは --cache-dir で指定されたコンテナ内のパスにキャッシュのアーカイブを保存します。これはデフォルトでは /cache です。

ジョブの実行時、--docker-volumes で指定されたディレクトリがジョブを実行するコンテナにマウントされます。これは <host-path>:<path> の形式と <path> の形式で指定することができて、<host-path>:<path> の形式ならホストのディレクトリがコンテナに bind マウントされます。

<path> の形式の場合、--docker-cache-dir が指定されていれば、そのディレクトリの中の runner-<short-token>-project-<id>-concurrent-<job-id>/<unique-id> のようなサブディレクトリをコンテナにマウントします。--docker-cache-dir が指定されていなければ runner-<short-token>-project-<id>-concurrent-<job-id>-cache-<unique-id> のような名前のデータボリュームコンテナを作成して、そのボリュームをコンテナにマウントします。ただし --docker-disable-cache が指定されていると --docker-cache-dir が指定されていてもデータボリュームコンテナが作成され、かつ、そのデータボリュームコンテナはジョブの終了時に自動で削除されます。

<unique-id> の部分はコンテナのパス(<path>)に基づくハッシュ値です。

要約すると・・・

  • ジョブのコンテナは --cache-dir のパスにアーカイブを保存する
    • デフォルトは /cache
  • --docker-volumes が・・
    • <host-path>:<path> の形式なら・・
      • ホストのディレクトリをジョブのコンテナにマウントする
    • <path> の形式なら・・
      • --docker-disable-cache が指定されていれは・・
        • データボリュームコンテナを作成してボリュームをジョブのコンテナにマウントする
        • ジョブの終了時に自動で削除される
      • --docker-cache-dir が指定されていれば・・
        • そのディレクトリのサブディレクトリをジョブのコンテナにマウントする
      • --docker-cache-dir が未指定なら
        • データボリュームコンテナを作成してボリュームをジョブのコンテナにマウントする
    • デフォルトは /cache

デフォルトの動きは次のようになります。

  • ジョブのコンテナは /cache にアーカイブを保存する
  • ジョブの開始時にデータボリュームコンテナを作成して /cache にマウントする

Docker executor でジョブ間でキャッシュを共有

--docker-volumes でホストのディレクトリを /cache にマウントすれば runner-<short-token>-project-<id>-concurrent-<job-id>/<unique-id> のようなサブディレクトリは作成されないため(サブディレクトリが作成されるのは <path> の形式のときだけだから)、前述のような異なる Runner の concurrent になってもジョブ間でキャッシュが共有されるようになります。

gitlab-runner register \
  --non-interactive \
  --url "$url" \
  --registration-token "$token" \
  --name 'ore-no-runner' \
  --executor 'docker' \
  --run-untagged \
  --tag-list 'ore' \
  --docker-image 'alpine:latest' \
  --docker-volumes '/srv/gitlab-runner/cache:/cache'

ただ、並列に実行される複数のジョブが同じアーカイブを読み書きしようとするだろうので、壊れたアーカイブが作成されたりしてしまいそうです、たぶんこれはやめておいたほうが良いでしょう。

Docker executor のビルドディレクトリ

--docker-disable-cache--docker-cache-dir の指定はソースがチェックアウトされるディレクトリにも関係します。これらの指定に基づいてソースファイルをチェックアウトするためのデータボリュームやホストのディレクトリのマウントが行われます(cache という名のついたパラメータなのに build ディレクトリに関係するのわかりにくい・・・)。

--build-dir でコンテナのどこにマウントするかを指定できます(デフォルトは /builds)。このディレクトリに /builds/<namespace>/<project-name> のようにサブディレクトリを掘ってチェックアウトされます。

つまり、<--docker-cache-dir>/runner-<short-token>-project-<id>-concurrent-<job-id>/<unique-id>/ のようなホストのディレクトリ、または、runner-<short-token>-project-<id>-concurrent-<job-id>-cache-<unique-id> のようなデータボリュームが、ジョブのコンテナの /builds/<namespace>/<project-name> にマウントされます。

なお、/cache とは異なり、--docker-volumes でホストのディレクトリを /builds にマウントしている場合は /builds/<short-token>/<concurrent-id>/<namespace>/<project-name> のようなサブディレクトリが掘られてそこにチェックアウトされるようになります。

分散キャッシュ

--docker-volumes で変なことをやらなくても、分散キャッシュを使えばジョブ間でキャッシュを共有させられます。

S3 や Google Cloud Storage を使えば楽そうですけど minio でホストさせても OK です。

# minio のコンテナを開始
docker run --detach \
  --name minio \
  --hostname minio \
  --restart always \
  --publish 9005:9000 \
  --volume /srv/minio/root/.minio:/root/.minio \
  --volume /srv/minio/export:/export \
  minio/minio:latest server /export

# バケット用のディレクトリを作成
sudo mkdir /srv/minio/export/runner

# ホストの IP  アドレスをメモる
hostname -i

# アクセスキーとシークレットキーをメモる
docker exec minio cat /export/.minio.sys/config/config.json | grep Key

# Runner を登録
gitlab-runner register \
  --non-interactive \
  --url "$url" \
  --registration-token "$token" \
  --request-concurrency 4 \
  --name 'ore-no-runner' \
  --executor 'docker' \
  --run-untagged \
  --tag-list 'ore' \
  --docker-image 'alpine:latest' \
  --docker-cache-dir '/srv/gitlab-runner/cache' \
  --cache-type 's3' \
  # minio の IP アドレスとポート番号
  --cache-s3-server-address '192.0.2.123:9005' \
  # アクセスキーとシークレットキー
  --cache-s3-access-key 'abc...' \
  --cache-s3-secret-key 'abc...' \
  # バケット名
  --cache-s3-bucket-name 'runner' \
  --cache-s3-insecure true

Gitlab Runner も Docker で実行して minio と同じネットワークに入れれば --publish 9005:9000 などせずに --cache-s3-server-address 'minio:9000' で大丈夫かと思いきやそんなことはありません(最初そうしようとしてあれー?と思いました)。

minio にはジョブとして実行されるコンテナの中からアクセスできる必要があり、ジョブのコンテナを minio と同じネットワークにはできないので、minio で --publish 9005:9000 のようにポートを晒して --cache-s3-server-address '192.0.2.123:9005' のようにホストのアドレス・ポートを指定する必要があります。

参考

NAT の内側からリモートのサーバを使って PhpStorm+Xdebug+Docker で開発するメモ

例えば AWS の EC2 インスタンスに Docker Engine を入れて、その Docker を用いて PhpStorm+Xdebug で開発するメモ。

なお、PC 側は Windows 10 で WSL です。

Docker Remote API

EC2 インスタンス上の Docker で Remote API を有効にします。以下のいずれでもお好みで良いと思います。

  • TLS 有効で 0.0.0.0
    • -H 0.0.0.0:2376 --tlsverify --tlscacert=/etc/docker/ca.pem --tlscert=/etc/docker/cert.pem --tlskey=/etc/docker/key.pem
    • 証明書の管理がめんどくさくないなら
  • TLS 無効で 0.0.0.0 でセキュリティグループでソースアドレスで制限する
    • -H 0.0.0.0:2375
    • PC 側が固定IPで同じNATの内側に信頼できる端末しか無いなら
  • TLS 無効で 127.0.0.1
    • -H 127.0.0.1:2375
    • ssh でローカルポートを転送して Docker Remote API にアクセスする

今回は ssh でのポートフォワード前提になっているので、3番目の方法を用います。

sshd_config

ssh で -R で xdebug のポートをリモート→ローカルに転送する必要があるのですが、素のままだとリモートでのリッスンポートがループバックデバイスのみリッスンするので、コンテナ内の xdebug から転送しているポートにアクセスできません。

リモートの sshd_configGatewayPorts yes にすれば 0.0.0.0 でリッスンするので、コンテナ内からでも転送しているポートにアクセスできるようになります。

SSH でポートの転送

次のようにリモートサーバに ssh してポートを転送します。

ssh ore-no-server -D 1080 -R 9000:localhost:9000 -L 2375:localhost:2375 -N

-R 9000:localhost:9000 は Xdebug 用です。リモートサーバの 9000 ポートをローカルの 9000 ポートに転送します。

-L 2375:localhost:2375 は Docker Remote API 用です。ローカルの 2375 ポートをリモートの 2375 ポートに転送します。ローカルで Docker Desktop を実行しているなどで 2375 ポートが使えないなら -L 12375:localhost:2375 などと適当にポート番号は変えると良いです。

-D 1080 は Docker で Expose しているポートへブラウザからアクセスするための設定です。

.envrc

direnv の .envrc などで Docker のための環境変数を設定します。

export DOCKER_HOST=tcp://localhost:2375
export DOCKER_TLS_VERIFY=0

これでローカルの dockerdocker-compose が SSH で転送されたポートを経由してリモートで実行されるようになります。

PhpStorm 設定

Build, Execution, Deployment > Docker

Docker サーバを追加します。TCP sockettcp://localhost:2375 で OK です。

Path mappings はデフォルトの /c/Users -> C:\Users が無難だと思います。

Languages & Frameworks > PHP

CLI Interpreter でインタプリタを Docker Compose で上で追加したサーバを指定して追加します。その他の設定はプロジェクトの構成にあわせて指定します。

さらに AddtionalConfiguration optionsxdebug.remote_host でコンテナからみたリモートサーバの IP アドレスを指定します。コンテナに入って ip route show で表示されるデフォルトゲートウェイを指定すれば OK です(Docker Desktop なら host.docker.internal で良いですけど Linux だとそうもいかない)。

Languages & Frameworks > PHP > Test Framework

PHPUnit by Remote Interpreter で上で追加したインタプリタを選択して追加します。その他の設定はプロジェクトの構成にあわせて指定します。

ファイル同期

ローカルからリモートへファイルを同期させる必要があります。

バージョンを揃えたりがやや面倒ですが これ でも使用した unison が便利です。

docker run --rm -v /usr/local/bin:/x eugenmayer/unison:2.51.2.1 sh -c 'cp /usr/local/bin/unison* /x'

次のように同期を実行します。

unison . "ssh://ore-no-server/$PWD" -ignore 'Name .git' -ignore 'Name .idea' -auto -batch -repeat watch

ブラウザのプロキシ設定

ブラウザのプロキシ設定で SOCKS プロキシを localhost:1080 で設定します。 いちいち設定を切り替えるのは面倒だろので、簡単に切り替えられるアドオンを使うか、どこかに次のような proxy.pac を置いてブラウザに読ませると楽です。

function FindProxyForURL(url, host) {

    if (localHostOrDomainIs(host, "ore-no-server")) {
        return "SOCKS5 127.0.0.1:1080";
    }

    return "DIRECT";
}

ブラウザで http://ore-no-server:9876 のように Docker で Expose しているポートにアクセスできるようになります(セキュリティグループでポートを開ける必要はありません)。

さいごに

特に変わったことはしていないけど sshd_configGatewayPorts yes が必要というところにちょっと嵌ったのでメモ。

unison で vendor や node_modules まで同期されるのはやりすぎかもしれないので適宜設定は調整すると良いかもしれない。手元では↓みたいになってました。

unison . "ssh://root@ore-no-server/$(pwd -P)" \
  -ignore 'Name .git' \
  -ignore 'Name .idea' \
  -ignore 'Name public' \
  -ignore 'Name build' \
  -ignore 'Name storage' \
  -ignore 'Name vendor' \
  -ignore 'Name node_modules' \
  -auto -batch -repeat watch

ポートフォワードも -D 1080 ではなく Expose してるポートを個別に -L で転送しても良いかと思います。手元では↓みたいになってました。これなら http://localhost:9876 でアクセスできるので Docker Desktop と使用感が近くなります。

ssh ore-no-server -N -D 1080 \
  -R 9000:localhost:9000 \
  -L 3000:localhost:3000 \
  -L 9876:localhost:9876 \
  -L 12376:localhost:2376 \
  -L 3306:localhost:1330

修正したファイルが unison で同期されるまでに若干の待ちがあるのが欠点です。

Nagios で Pacemaker のクラスタの状態を監視する

とある Pacemaker で HA クラスタにしてるシステムで Pacemaker のクラスタの状態を監視したときのメモ。

Pacemaker から次のような方法でパッシブに Nagios に通知すれば良さそうですけど・・・

パッシブチェックのための処理を監視対象のサーバに仕込むのはけっこう面倒なので Nagios からのアクティブチェックで監視できるようにします。

次のようなものを監視したいです。

  • ノードが online 以外になっている(offline とか standby)
  • リソースがコケてフェイルカウントがあがってる
  • リソースを移動させたときのロケーション制約が残ったまま

これらの情報は crm_mon コマンドで取得できるので、Nagios から nrpe 経由で crm_mon コマンドを実行してその内容を元に結果を出力する Nagios プラグインを作ります。

check_pacemaker.sh

#!/bin/bash

tmp=$(mktemp /tmp/check_pacemaker.XXXXXX)
trap 'rm -f $tmp' EXIT

sudo crm_mon -1NDfn -L > "$tmp"

output=$(
  cat "$tmp" | grep '^Node' | grep -v 'online$'

  cat "$tmp" | sed -n -e '/^Failed Actions:/,/^$/p'

  cat "$tmp" | sed -n -e '/^Migration Summary:/,/^$/p' | grep 'fail-count' >/dev/null &&\
  cat "$tmp" | sed -n -e '/^Migration Summary:/,/^$/p'

  cat "$tmp" | sed -n -e '/^Negative Location Constraints:/,/^$/p' | tail -n +2 | grep . >/dev/null &&\
  cat "$tmp" | sed -n -e '/^Negative Location Constraints:/,/^$/p'
)

if [ -n "$output" ]; then
  echo "$output" | tr '|' ' '
  exit 1
fi

sudo crm_mon -s

crm_mon の出力を一時ファイルに書いていますがよく考えたら変数に入れるだけで十分でした。

sudo するために以下のような sudoers も必要です。secure ログへの出力がうざかったので nrpe では syslog も無効にしています。

Defaults:nrpe !syslog
nrpe ALL=(root) NOPASSWD: /usr/sbin/crm_mon -1NDfn -L, /usr/sbin/crm_mon -s

次のような通知が発生します。

正常なとき

[OK] CLUSTER OK: 2 nodes online, 3 resources configured

ノードが standby のとき

[WARNING] Node db02: standby

ノードが offline のとき

[WARNING] Node db02: OFFLINE

リソースにロケーション制約がついているとき

[WARNING] Negative Location Constraints:
 cli-ban-mysql-on-db01  prevents mysql from running on db01

リソースがコケたとき

[WARNING] Failed Actions:
* mysql_monitor_20000 on db01 'not running' (7): call=77, status=complete, exitreason='',
    last-rc-change='Thu Oct 24 09:55:25 2019', queued=0ms, exec=0ms

Migration Summary:
* Node db01:
   mysql: migration-threshold=1000000 fail-count=1 last-failure='Thu Oct 24 09:55:25 2019'
* Node db02:

リソースがコケたときの通知がめちゃくちゃ冗長な気がするのと、Nagios のプラグインの出力は1行目と2行目以降で意味に違いがあったりパフォーマンスデータがパイプで区切られる仕様だったりするはずなのだけど・・・

とりあえず動いているので良しとします。

これを次のように nrpe の設定ファイルに仕込んで、

command[check_pacemaker]=/usr/lib64/nagios/plugins/check_pacemaker.sh $ARG1$

Nagios から次のように監視します。

define command{
    command_name    check_nrpe
    command_line    $USER1$/check_nrpe -H $HOSTADDRESS$ -c $ARG1$ -a "$ARG2$"
}

define service{
    use                   generic-service
    host_name             db-active
    service_description   Pacemaker
    check_command         check_nrpe!check_pacemaker
}

さいごに

ぐぐると次のようなものも見つかりました。

あと pcs-snmp というものもあって SNMP で crm_mon と同じような情報が得られるようです。

簡易的にフェイルオーバーの発生を nagios で通知する

とある Keepalived や Pacemaker で仮想IP(VIP)を用いたフェイルオーバーによる冗長化を行っているシステムで、フェイルオーバーが発生したことを nagios で簡易的に監視したときのメモ。

Keepalived の notify スクリプトや、Pacemaker の crm_mon --external-agent とかで nsca で Nagios の Passive check に通知すれば良いとは思うのですが、そのためにスクリプトを仕込んだりテストしたりとかまでやる意義があまりなかったので、Active check だけでもっと簡易的に実装しました。

要するに VIP を持っている実サーバに変化があればフェイルオーバーしたとみなせます。

監視対象のサーバには snmpd が動いていたので snmpget でホスト名が得られました。ので VIP に対して snmpget でホスト名を取得し、前回の Active check 実行時から変化があればフェイルオーバーが発生したとみなして通知します。

前回の Active check の結果との比較は、SERVICEOUTPUT マクロに前回の Active check の実行時のチェックコマンドの出力が入るので、ここにホスト名を出力するようにして、その内容を比較します。

監視用のスクリプトは次のようになります。これを Nagios の /usr/lib64/nagios/plugins/ とかに置いておきます。

#!/bin/bash

set -eu -o pipefail

trap 'exit 3' ERR

# $1 には監視対象のIPアドレスを入れる
target=$1

# snmp community は適当に書き換えるかこれも引数で受け取る
community=xxxxx

prev=

# $2 には前回のアクティブチェックの出力(SERVICEOUTPUT)を入れる
case "$2" in
  # SERVICEOUTPUT からホスト名を取り出す
  # SERVICEOUTPUT は Active:$hostname[, $message] みたいな形式になっています
  Active:*)
    prev=$2
    prev=${prev#*:}
    prev=${prev%%,*}
    ;;
esac

# snmpget でホスト名を取得
curr=$(snmpget -v2c -c "$community" -Oavq "$target" .1.3.6.1.2.1.1.5.0)

# 空かどうか&前回のホスト名と比較して結果を返す
if [ -z "$curr" ]; then
  # snmpget がしてるときは UNKNOWN(3)
  printf "%s\n" "snmpget returned empty"
  exit 3
elif [ -z "$prev" ]; then
  # 前回の結果が無いなら WARNING(1)
  printf "%s\n" "Active:$curr, Initial state"
  exit 1
elif  [ "$prev" != "$curr" ]; then
  # 前回の結果と異なるなら CRITICAL(2)
  printf "%s\n" "Active:$curr, Failover from $prev"
  exit 2
else
  # 前回の結果と同じなら OK(0)
  printf "%s\n" "Active:$curr"
  exit 0
fi

次に Nagios の設定です。

define command {
    command_name    check_failover
    # 対象サーバのIPアドレスとアクティブチェックの出力を渡す
    command_line    $USER1$/check_failover.sh "$HOSTADDRESS$" "$SERVICEOUTPUT$"
}
define service{
    use                     generic-service
    host_name               lb-active
    service_description     Failover
    check_command           check_failover
    # フェイルオーバが連続したときも通知したい
    is_volatile             1
    # 一発で HARD にして通知する
    max_check_attempts      1
    # 1回しか通知しない
    notification_interval   0
    # リカバリ(r)は通知しない
    notification_options    w,u,c
    # フラッピング検出は無効(有効でも良いかも)
    flap_detection_enabled  0
}

通知内容の時系列

  • [WARNING] Active:ore-no-server-01, Initial state
    • 監視を仕込んで初回のチェック
    • 直前の状態がわからないため発生します
  • [OK] Active:ore-no-server-01
    • 2回目以降のチェックは直前のホストと変わっていないので OK です
    • リカバリは通知しないのでこれは通知されません
  • [CRITICAL] Active:ore-no-server-01, Failover from ore-no-server-01
    • フェイルオーバーが発生したときにホスト名の変化を検出して通知します
  • [OK] Active:ore-no-server-02
    • フェイルオーバーを通知後はまた OK に戻ります

さいごに

ホスト名を直前のチェック時から比較すればよいかと思い、直前のチェック時の値を保持するためにどこかファイルにでも保存するかと思ったのですが、よく考えたら SERVICEOUTPUT マクロを見ればできました。

フェイルオーバーの前にアクティブ機への snmpget が失敗し、

  • [UNKNOWN] snmpget returned empty
  • [WARNING] Active:ore-no-server-02, Initial state

のように通知されることがありますが、まあ通知は飛ぶので良しとします。

Terraform でネストしたループを flatten する

これを、

variable "hoge" {
  default = [
    {
      name = "aaa"
      items = [
        111,
        222,
      ]
    },
    {
      name = "bbb"
      items = [
        333,
        444,
      ]
    },
  ]
}

こうしたかった

[
  {
    "item" = 111
    "name" = "aaa"
  },
  {
    "item" = 222
    "name" = "aaa"
  },
  {
    "item" = 333
    "name" = "bbb"
  },
  {
    "item" = 444
    "name" = "bbb"
  },
]

0.12 で追加された for が Python の内包表記っぽい感じだったので下記のように書けるかと試してみるものの・・

output "hoge" {
  value = [ for a in x.arr for x in var.hoge : { name = x.name, arr = a } ]
}

ダメでした。

Error: Invalid 'for' expression

  on main.tf line 22, in output "hoge":
  22:   value = [ for h in var.hoge for item in h.items : { name = h.name, item = item } ]

For expression requires a colon after the collection expression.

次のように for をネストしてリストのリストを作って flatten すれば OK です。

output "hoge" {
  value = flatten([ for h in var.hoge : [ for item in h.items : { name = h.name, item = item } ] ])
}

もっとスマートな方法ありそうと思って Issue を漁ってみたところ・・・同じような方法が紹介されていました。