0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CREATE_FAILED → ハング → 成功: AWS Config recorder の循環依存をCDK で解くまでの1日

0
Posted at

第2回で構築した Security Hub CSPM の2リージョン構成 — 実はこのデプロイ、一度で成功していません。AWS::Config::ConfigurationRecorderAWS::Config::DeliveryChannel の相互依存により同じ日に2回失敗し、3回目でようやく成功しました。

本稿はその全現象・原因・エビデンス・解決策を、実際の CloudFormation イベントと AWS CLI 出力(すべて一次記録)で振り返ります。AWS Config を CloudFormation / CDK で有効化しようとする人が必ず通る道なので、同じ場所でハマった方の時間節約になれば幸いです。

この記事でわかること

  • CloudFormation で AWS Config を有効化するときに起きる循環デッドロックの正体
  • 「エラーメッセージに従って順序を入れ替えたら、今度は別の理由で詰む」実例
  • ハングしたとき、CloudFormation の表示ではなく AWS 実体を照会して原因を切り分ける方法
  • AwsCustomResource ×3 で API を正しい順序で呼ぶ解決策(コード付き)

シリーズ: ① 概念編 → ② CDK 構築編 → ③ トラブル編(本記事)

1. 何が起きたか — 同じ日に「失敗 → ハング → 成功」

qiita-img-1-timeline.png

図1: 同じ日の3回のデプロイ。①recorder 先行 → CREATE_FAILED、②channel 先行 → ハング、③AwsCustomResource ×3 → 成功。

1.1 Deploy ① — recorder 先行 → CREATE_FAILED

最初の構成は素直な L1 リソース2つ。delivery channel が recorder に依存する形(recorder が先に作られる)でした。結果、recorder の作成が失敗しスタック全体がロールバック:

aws cloudformation describe-stack-events --stack-name SecurityBaselineStack \
  --query "StackEvents[?ResourceStatus=='CREATE_FAILED'].{LogicalId:LogicalResourceId,Type:ResourceType,Reason:ResourceStatusReason}"
{ "LogicalId": "ConfigRecorder...",
  "Type": "AWS::Config::ConfigurationRecorder",
  "Reason": "Delivery channel is not available to start configuration recorder.
             (Service: AmazonConfig; Status Code: 400;
              Error Code: NoAvailableDeliveryChannelException; ...)" }

さらにロールバック後、RemovalPolicy.RETAIN を付けていた S3 バケット3個が DELETE_SKIPPED となり、スタック消滅後も「孤児バケット」として残りました(手動削除が必要)。

リソース(ロールバック後の状態) 状態
ConfigRecorder(失敗の起点) CREATE_FAILED → DELETE_COMPLETE
Trail / Role / BucketPolicy ×3 など DELETE_COMPLETE(巻き戻し)
RETAIN 付き S3 バケット ×3 DELETE_SKIPPED — 孤児として残留

1.2 修正① — DependsOn を反転(これが罠だった)

エラーメッセージは「recorder を開始しようとしたが delivery channel が無い」と読めます。
ならば delivery channel を先に作ればよい、と依存方向を反転しました:

// 修正①: recorder が delivery channel に依存する形へ(channel が先に作られる)
this.recorder.addOverride("DependsOn", deliveryChannel.logicalId);

1.3 Deploy ② — channel 先行 → CREATE_IN_PROGRESS のままハング

再デプロイでは他のリソース(Security Hub・CloudTrail・バケットポリシー等)は次々にCREATE_COMPLETE になりましたが、DeliveryChannel だけが止まりました。

CLI の進捗表示は (12/15) で停止し、以後イベントが一切進みません:

| ConfigDeliveryChannel... | Resource creation Initiated | CREATE_IN_PROGRESS |
# …このまま何分待っても次のイベントが発生しない

ここで決定的だったのが、ハング中に AWS 実体を照会するという切り分けです:

aws configservice describe-configuration-recorders --region ap-northeast-1
# → { "ConfigurationRecorders": [] }   # recorder はまだ無い(DependsOn で待機中なので当然)

aws configservice describe-delivery-channels --region ap-northeast-1
# → { "DeliveryChannels": [] }         # ★ channel も無い = 作成 API が通っていない

CloudFormation 上は「作成中」でも、AWS Config 側には delivery channel が存在しない
つまり PutDeliveryChannel が(recorder 不在で)失敗し続けており、CloudFormation のハンドラがリトライで空回りしている状態でした。このデプロイは自然完了しないため手動で delete-stack し、残骸も再度整理しました。

2. 原因 — 「真の循環依存」

qiita-img-2-deadlock.png

図2: recorder と delivery channel は互いを前提とする。DependsOn をどちらに向けても解決できない。

原因は AWS Config の2つの仕様が組み合わさった、CloudFormation 上の循環デッドロックです:

  1. CloudFormation は AWS::Config::ConfigurationRecorder を作成すると同時に記録を自動開始しようとする
    開始には delivery channel が必要 → channel が無ければ NoAvailableDeliveryChannelException で失敗(Deploy ①)
  2. PutDeliveryChannel API は configuration recorder の存在を要求する → recorder が無ければNoAvailableConfigurationRecorderException で作成できない(Deploy ② のハングの正体)

つまり: 「recorder 先行」は①で即死、「channel 先行」は②で永久待機。
L1 リソース2つ + DependsOn という宣言的な表現では、この2つのリソースは絶対にデプロイできません
必要なのは「停止状態で recorder を作る」という中間状態ですが、L1 のAWS::Config::ConfigurationRecorder にはそれを表現するプロパティがありません。

3. 原因がこれだと判断できたエビデンス

E1 — Deploy ① のエラー原文 × 公式ドキュメントの一致

  • 実測: NoAvailableDeliveryChannelException — Delivery channel is not available to start configuration recorder。 「作成」ではなく「開始(start)」で失敗している点が重要
  • 公式(CloudFormation リソースリファレンス):
    "AWS CloudFormation starts the recorder as soon as the delivery channel is available."
    — CloudFormation が作成時に自動開始することが明記されている

E2 — Deploy ② のハング × PutDeliveryChannel API 仕様の一致

  • 実測: ハング中に describe-delivery-channels が空(§1.3)— CloudFormation は「作成中」なのに実体が無い = 作成 API が拒否され続けている
  • 公式(PutDeliveryChannel API リファレンスのエラー定義):
    "NoAvailableConfigurationRecorderException — There are no customer managed configuration recorders available to record your resources. Use the PutConfigurationRecorder operation to create the customer managed configuration recorder."
  • 公式(CloudFormation リソースリファレンス DeliveryChannel): "Before you can create a delivery channel, you must create a configuration recorder."

E3 — 両方向の実測失敗 = 循環の決定的証拠

「recorder 先行」も「channel 先行」も、それぞれ公式仕様どおりの理由で実際に失敗しました。片方向の失敗なら「順序ミス」ですが、両方向が失敗した時点で、これは順序の問題ではなく循環依存だと確定できます。解決には順序の入れ替えではなく、「循環を断ち切る中間状態(停止状態の recorder)」を作れる手段が必要になります。

4. 解決方法 — AwsCustomResource ×3 で API を正しい順序で呼ぶ

qiita-img-3-solution.png

図3: 各ステップの前提条件を直前のステップが充足する、デッドロックの無い唯一の順序。削除時は自動的に逆順。

AWS Config の API を直接見ると、デッドロックの無い順序が1つだけ存在します。
PutConfigurationRecorder は recorder を「停止状態」で作成する(開始しない)ため、前提条件が無いのがポイントです:

  1. PutConfigurationRecorder — 停止状態で作成(前提: なし)
  2. PutDeliveryChannel — 前提「recorder の存在」は①で充足済み
  3. StartConfigurationRecorder — 前提「delivery channel の存在」は②で充足済み

CDK ではこの3コールを AwsCustomResource 3つで表現し、node.addDependency で順序を保証します。削除時は各リソースの onDelete が依存の逆順で実行されるため、Stop → DeleteDeliveryChannel → DeleteConfigurationRecorder と綺麗に畳まれます:

// ── (1) PutConfigurationRecorder(停止状態で作成) ──
const putRecorder = new AwsCustomResource(this, "PutRecorder", {
  onCreate: {
    service: "@aws-sdk/client-config-service",
    action: "putConfigurationRecorder",
    parameters: { ConfigurationRecorder: { name: "default", roleARN: role.roleArn,
                  recordingGroup: props.recordingGroup } },
    physicalResourceId: PhysicalResourceId.of("default-recorder"),
  },
  onDelete: { service: "@aws-sdk/client-config-service",
              action: "deleteConfigurationRecorder",
              parameters: { ConfigurationRecorderName: "default" } },
  /* policy: config:Put/DeleteConfigurationRecorder + iam:PassRole(recorder ロール限定) */
});

// ── (2) PutDeliveryChannel(recorder が存在してから) ──
const putChannel = new AwsCustomResource(this, "PutDeliveryChannel", {
  onCreate: { service: "@aws-sdk/client-config-service", action: "putDeliveryChannel",
    parameters: { DeliveryChannel: { name: "default", s3BucketName: bucket.bucketName } },
    physicalResourceId: PhysicalResourceId.of("default-channel") },
  onDelete: { service: "@aws-sdk/client-config-service", action: "deleteDeliveryChannel",
              parameters: { DeliveryChannelName: "default" } },
});
putChannel.node.addDependency(putRecorder);       // ← 順序保証 ①→②
putChannel.node.addDependency(bucket.policy);     // バケットポリシー(Config の書込許可)も先に

// ── (3) StartConfigurationRecorder(delivery channel が存在してから) ──
const startRecorder = new AwsCustomResource(this, "StartRecorder", {
  onCreate: { service: "@aws-sdk/client-config-service", action: "startConfigurationRecorder",
    parameters: { ConfigurationRecorderName: "default" },
    physicalResourceId: PhysicalResourceId.of("default-start") },
  onDelete: { service: "@aws-sdk/client-config-service", action: "stopConfigurationRecorder",
              parameters: { ConfigurationRecorderName: "default" } },  // 削除時はまず停止
});
startRecorder.node.addDependency(putChannel);     // ← 順序保証 ②→③

コスト面の補足: AwsCustomResource は同一スタック内で1つのプロバイダ Lambda(シングルトン)を共有します(CDK ソースで固定 UUID を確認)。3つ使っても追加されるのは Lambda 1個 + IAM ポリシーのみ。

5. この解決で行けると判断したエビデンス(デプロイ前)

E4 — API 前提条件の充足分析(論理)

§2 の失敗条件はそれぞれ「開始時に channel が無い」「作成時に recorder が無い」でした。
新しい順序では、各 API の前提条件がすべて直前のステップで満たされます(図3)。
①には前提が無く、②の前提は①が、③の前提は②が作る。循環の輪に入る箇所が存在しません。

E5 — synth 産物(テンプレート)の実測検証

「コードが意図どおりの API 呼び出しを生成しているか」を、合成された CloudFormation テンプレートで確認しました:

# Custom リソースの Create パラメータ(抜粋)
putConfigurationRecorder: {"ConfigurationRecorder":{"name":"default","roleARN":"...",
  "recordingGroup":{"allSupported":true,"includeGlobalResourceTypes":true}}}
putDeliveryChannel:       {"DeliveryChannel":{"name":"default","s3BucketName":"..."}}
startConfigurationRecorder: {"ConfigurationRecorderName":"default"}

# DependsOn 連鎖(テンプレート実物)
Custom::ConfigDeliveryChannel: DependsOn=[..., ConfigPutRecorder...]     # ①→②
Custom::ConfigRecorderStart:   DependsOn=[..., ConfigPutDeliveryChannel...] # ②→③

パラメータ形状が AWS Config API リファレンスの要求と一致し、順序も DependsOn で強制されています。

E6 — 回帰テスト・静的解析

  • jest にテンプレートの順序を断言する回帰テストを追加(「channel は recorder に依存」「start は channel に依存」)
  • cdk-nag(AwsSolutionsChecks)エラー 0

正直な限界(デプロイ前時点): synth は実 API を呼ばないため、最終確認は実デプロイのみ。
ただし「論理的にデッドロック不可能な唯一の順序」+「テンプレート実測一致」で、前2回より遥かに強い根拠が揃っていました。

6. 結果 — Deploy ③ 成功と実測確認

3回目のデプロイは両スタック(東京 / us-east-1)とも CREATE_COMPLETE
核心の Config recorder は、両リージョンで実際に記録が動いていることを確認しました:

aws configservice describe-configuration-recorder-status --region ap-northeast-1
# → { "name": "default", "recording": true, "lastStatus": "SUCCESS", ... }

aws configservice describe-configuration-recorder-status --region us-east-1
# → { "name": "default", "recording": true, "lastStatus": "SUCCESS", ... }
確認項目 結果
CloudFormation スタック(東京 / us-east-1) CREATE_COMPLETE ×2
Config recorder(両リージョン) recording:true / lastStatus:SUCCESS / エラーなし
recordingGroup 東京=全タイプ+グローバル / us-east-1=WAFv2・CloudFront 7タイプ限定(設計どおり)
削除時の逆順動作 onDelete(Stop→DeleteChannel→DeleteRecorder)として実装済み
周辺リソース(Security Hub / 集約 / GuardDuty) すべて正常(FSBP 単独・集約リンク・ENABLED)

7. 教訓(まとめ)

  • cdk synth が通ることと、デプロイが成功することは別物 — テンプレートが構文的に正しくても、 リソースの API 前提条件までは CloudFormation は検証しない
  • エラーメッセージの「表面的な解決」は罠になり得る — 「channel が無い」→「channel を先に」と反転したら、今度は逆方向の前提で詰んだ。両方向が失敗した時点で循環を疑う
  • L1 リソースで表現できない中間状態は、AwsCustomResource で API を直接順序制御する —「停止状態の recorder」という中間状態が鍵だった。シングルトン Lambda 共有なのでコストも小さい
  • ハングしたら CloudFormation の表示ではなく AWS 実体を照会するdescribe-delivery-channels が空だったことが「作成 API が通っていない」ことの決定的証拠になった
  • RETAIN リソースは失敗ロールバックでも残る(DELETE_SKIPPED)— 再試行前の孤児整理を忘れずに

シリーズ: 第1回 概念編 / 第2回 CDK 構築編 / 第3回(本記事)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?