はじめに
マルチ AZ 構成は組んである。設計書にもそう書いてある。だが「本当に片方の AZ が落ちてもデータが流れ続けるか」を、誰も実際に確かめたことがなかった。
そこで AWS Fault Injection Service(FIS)で AZ 障害シナリオを作ることにした。対象は IoT データ基盤で、IoT Core → Kinesis → Lambda → S3 / Aurora / ElastiCache というよくある非同期パイプラインだ。
組んだテンプレートは 6 アクション同時実行。
| アクション | 対象 | 狙い |
|---|---|---|
aws:network:disrupt-connectivity |
AZ-a 側の private サブネット | AZ-a の通信を遮断 |
aws:rds:failover-db-cluster |
Aurora A | writer を他 AZ へ退避 |
aws:rds:failover-db-cluster |
Aurora B | writer を他 AZ へ退避 |
aws:elasticache:replicationgroup-interrupt-az-power |
Redis A の AZ-a ノード | ノード電源断 |
aws:elasticache:replicationgroup-interrupt-az-power |
Redis B の AZ-a ノード | ノード電源断 |
aws:elasticache:replicationgroup-interrupt-az-power |
Redis C の AZ-a ノード | ノード電源断 |
テンプレートは Terraform で管理した。手で作ると再現できないし、FIS は IAM の要求が地味に重い。stop_condition は none、duration は PT3H として手動停止で終える運用にした。
準備としては十分なつもりだった。だが実験を回して分かったのは、もっと手前の話だった。
この構成で作れるのは、AZ 障害ではなかった。
実 AZ 障害と FIS の、決定的な差
実際に AZ が障害を起こしたとき何が起きるかを分解すると、「通信が切れる」はそのうちの一部でしかない。
実障害では、それに加えて AWS 側から一連のシグナルが出る。
- ゾーンが切り離される
- サービスのエンドポイントが DNS から除外される
- 各マネージドサービスの自動フェイルオーバーが発火する
FIS が注入できるのは、このうち最初の「通信が切れる」だけだ。残りは注入されない。aws:network:disrupt-connectivity が実際にやっているのは、対象サブネットへの deny NACL の挿入である。IP レイヤのパケットは落ちるが、AWS のコントロールプレーンから見れば、その AZ は何事もなく生きている。
つまり FIS で作れるのは、
「AZ が落ちた状態」ではなく、「AZ が落ちているのに、誰もそれを知らない状態」
である。
EC2 中心の構成なら、この差はさほど問題にならないかもしれない。だが Lambda・Aurora・ElastiCache・VPC エンドポイントといったマネージドサービス主体の構成では、到達性を決めているのは DNS とサービス側のヘルスチェックであって、NACL ではない。シグナルが出ない以上、彼らは障害が起きたことを知らないまま動き続ける。
ここから先は、その差が実際にどう現れたかを 3 つ挙げる。
証拠 1:Redis を 3 本撃って、フェイルオーバーしたのは 1 本だけ
aws:elasticache:replicationgroup-interrupt-az-power は、名前こそ「AZ の電源断」だが、実際にやるのは指定した AZ にあるノードを落とすことだけだ。
レプリケーショングループから見れば、落ちたノードが primary なのか replica なのかで意味がまったく変わる。
- primary が AZ-a にあるクラスタ → 自動フェイルオーバーが発火する
- primary が AZ-c にあるクラスタ → AZ-a 側の replica が消えるだけ。primary は無傷で、何も起きない
今回、AZ-a に primary があったのは 3 本のうち 1 本だけだった。残り 2 本は「AZ-a のノードを落とした」という FIS のログだけが残り、アプリケーションから見れば何の変化もなかった。
実際の AZ 障害なら、3 本とも対処を迫られる。片方の AZ が丸ごと落ちるのだから、どのクラスタも例外ではない。
ここで効いてくるのが、冒頭の「誰もそれを知らない」だ。FIS の結果は、撃つ前の primary の居場所に完全に依存する。そして primary の位置は、過去のフェイルオーバーやメンテナンスで勝手に動く。同じテンプレートを同じ環境に撃っても、先週と今週では違う試験をしていることになる。
証拠 2:Aurora の writer が、生存 AZ から障害 AZ へ引っ越した
aws:rds:failover-db-cluster には、フェイルオーバー先の AZ やインスタンスを指定するパラメータがない。クラスタに対して「フェイルオーバーしろ」としか言えない。
これが何を意味するか。実験開始時に writer が AZ-c(生存側)にあった場合、このアクションは writer を AZ-a(これから遮断する側)へ動かしてしまう。
実障害で起こることの、真逆である。実障害では writer は障害 AZ から生存 AZ へ退避する。今回は生存 AZ から障害 AZ へ自ら飛び込んだ。しかも飛び込んだ先は NACL で遮断される。
これは「厳しい試験ができた」という話ではない。現実には起こり得ない配置を、自分で作り出してしまったということだ。この状態での観測結果は、AZ 障害耐性の証拠として使えない。
対策自体は単純で、撃つ前に writer を AZ-a に寄せておけばよい。
aws rds describe-db-clusters --db-cluster-identifier "$CLUSTER" \
--query 'DBClusters[0].DBClusterMembers[?IsClusterWriter==`true`].DBInstanceIdentifier' \
--output text
だが重要なのは手順ではなく、フェイルオーバー系のアクションは「撃つ前の配置」で結果が決まるという性質の方だ。証拠 1 と合わせて、FIS の AZ シナリオは初期状態に依存する非決定的な試験になっている。
証拠 3:VPC エンドポイントは DNS に残り続ける
遮断対象にしたサブネットには、interface 型の VPC エンドポイントが同居していた。Kinesis、SQS、Secrets Manager、CloudWatch Logs、SSM など一式である。
これらは privateDnsEnabled が有効なので、サービス名を名前解決すると、各 AZ のエンドポイント ENI の IP が返ってくる。そして NACL で遮断しても、エンドポイントは DNS から除外されない。AWS 側は障害だと思っていないのだから当然である。
結果どうなるか。AZ-c(生存側)に配置された Lambda も、名前解決の結果として AZ-a 側の IP を引く。引いた接続は NACL の deny に吸い込まれて、応答が返らないままタイムアウトまで張り付く。
つまり観測されたのは「AZ-a 側の処理が死ぬ」ではなく、**「AZ に関係なく、接続のたびに一定確率で失敗する」**だった。生存しているはずの AZ まで道連れになっている。
なお、この VPC エンドポイント巻き添えの現象については先行記事で詳しく検証されている。共通基盤 VPC や Transit Gateway が絡むと非対称ルーティングという形でも現れること、「実際の AWS 障害でも AZ 全体が落ちることは少なく、グレー障害が多い」という指摘は、本記事の主張とも地続きだ。我々の環境でも同じ現象を確認した。
本記事の文脈でこの事実が意味するのは、FIS は実障害より厳しい偽物を作ることがあるということだ。実 AZ 障害であればエンドポイントは DNS から除外され、生存 AZ の通信は生き残る。NACL 遮断ではそれが起きない。
整理:FIS の AZ 障害は、実 AZ 障害の部分集合でも上位集合でもない
ここまでの 3 つを並べると、関係が見えてくる。
| 実 AZ 障害 | FIS の「AZ 障害」 | |
|---|---|---|
| Redis | 全クラスタが対処を迫られる | primary の居場所次第。今回は 3 本中 1 本だけ |
| Aurora writer | 障害 AZ → 生存 AZ へ退避 | 行き先を指定できず、生存 AZ → 障害 AZ もあり得る |
| VPC エンドポイント | DNS から除外され、生存 AZ は生き残る | DNS に残り、生存 AZ まで道連れ |
弱い方向にも強い方向にもズレている。「本番より厳しめの試験ができた」でも「軽めだが傾向は掴めた」でもなく、性質の違う別の事象を観測していた、というのが正確なところだ。
だから「6 アクション同時実行= AZ 障害シナリオ」というテンプレートには、根本的な問題がある。合格しても不合格でも、何が証明されたのかを説明できない。
では、FIS をどう使うか
AZ 障害という単位を 1 シナリオで再現するのを諦め、目的ごとに分解するしかない。
1. 通信断そのものへの耐性試験として使う
aws:network:disrupt-connectivity は、これには適している。特定のサブネットが到達不能になったときにシステムがどう振る舞うかは、現実に起こりうる事象であり、価値のある検証だ。VPC エンドポイントの巻き添えのような、グレー障害に近い挙動も炙り出せる。
ただし、これを「AZ 障害試験」と呼んではいけない。「対象サブネット通信断試験」と名前を変えるだけで、合格基準の立て方が変わる。「AZ が落ちても大丈夫か?」ではなく「このサブネットへの経路が消えたとき、どこまで縮退して動くか?」という問いになり、こちらなら答えが出せる。
名前は思考を縛る。AZ 障害という名前をつけたせいで、我々は間違った問いを立て続けていた。
2. フェイルオーバー機構は、個別に、正しい初期配置から撃つ
Aurora と Redis のフェイルオーバーが正しく発火し、アプリケーションが追従できるか——これは通信断とは独立した検証項目であり、まとめて撃つ理由がない。
やるべきことは明確で、
- Redis は primary が AZ-a にあることを確認してから撃つ(でなければ何も起きない)
- Aurora は writer が AZ-a にあることを確認してから撃つ(でなければ逆方向に動く)
- 1 つずつ撃って、発火の有無・所要時間・アプリ側の影響を個別に記録する
ただし、我々はまだここに到達していない。 今回の実験は 6 アクション同時実行のまま回してしまい、個別の発火確認はできていない。本記事の主張——FIS の AZ シナリオは実 AZ 障害ではない——は、ここまでの 3 つの実測だけで十分に立つが、「ではフェイルオーバー機構自体は健全なのか」という問いには、まだ答えを持っていない。次の課題である。
3. 後片付けまでを試験手順に含める
これは地味だが効く。Aurora も ElastiCache も、自動ではフェイルバックしない。実験が終わっても writer や primary は移動先の AZ に残り続ける。
放置すると次回の実験の初期配置が前回と変わる。証拠 1 と 2 で見た通り、初期配置が変わるということは試験内容が変わるということだ。手動でフェイルオーバーして戻すところまでを手順書に書いて、はじめて再現性のある試験になる。
まとめ
FIS のアクション名——「AZ 電源断」「フェイルオーバー」——は、実障害の名前であって、実障害の再現ではない。
テンプレートを組む前に、アクションごとに一つずつ対応付けておくべきだった。
- このアクションは、実障害のどの部分を注入するのか
- そして、どの部分を注入しないのか
特に後者が重要で、今回で言えば「DNS からの除外が注入されない」「フェイルオーバーの方向が制御できない」の 2 点を事前に押さえていれば、テンプレートの形はまったく違っていたはずだ。
カオスエンジニアリング自体は有効だと思っている。実際、VPC エンドポイントの巻き添えという、設計書を何度読んでも出てこなかった挙動を見つけられた。
だが、合格しても何が証明されたのか言えない試験が、一番危ない。マルチ AZ 構成に「FIS で AZ 障害試験済み」というラベルだけが貼られ、中身が別物だったとき、それは検証していないより悪い。
終わりに
以上、Claudeに書いてもらったところ、とてもAIチックな読み味の文章ができました。
個人的に気に入った文言はこれ
名前は思考を縛る。AZ 障害という名前をつけたせいで、我々は間違った問いを立て続けていた。