開発技術部の柴田です。
皆さんはCloudFormationのリソースインポートを使ったことがありますか?既存のリソースをスタックに取り込める機能なのですが、使ってみると意外と「あれ?」っとなることがあります。
私も先日、既存のロググループをインポートした際に意図した挙動にならずに困りました。ロググループを取り込んで、ログの保持期間を変更したかったのですが、差分が検出されずに適用できなかったのです。
今回は、この挙動の原因であるCloudFormationの状態管理モデルの説明と、期待通りの結果を生むためのワークフローを紹介します。
インポートしたのに設定が反映されない
今回の経緯ですが、Lambda関数のロググループの保持期間を設定し忘れていたことに気づき、CloudFormationのリソースインポートで既存のロググループをスタックに取り込みました。
テンプレートにはRetentionInDays: 30を書いてインポートし、次にスタックを更新すれば保持期間が変わると思っていました。
ところが、スタックを更新しようとしても「変更なし」と言われて、ログの保持期間を更新できません。
なぜこうなるのか。CloudFormationの状態管理モデルを知ると、原因が分かります。
CloudFormationの状態管理モデル
CloudFormationには「テンプレート」「スタック記録」「実リソース」の3つの状態が存在します。 それぞれの操作で「どの状態を比較するか」を整理すると次のようになります。
| 操作 | 比較対象 | 実リソースを見るか |
|---|---|---|
| Change Set | 新しいテンプレート vs スタック記録(前回デプロイしたテンプレート) | 見ない |
| ドリフト検出 | スタック記録 vs 実リソース | 見る |
| インポート | なし(テンプレートをスタック記録に書くだけ) | 存在確認のみ |
Change Set
新しいテンプレートとスタック記録を比較し、差分があればAPIを呼んで実リソースを変更します。 ポイントは、実リソースの現在の状態は見ないということです。CloudFormationは「前回デプロイしたテンプレートの内容」を記録しており、それと新しいテンプレートの差分だけで判断します。
ドリフト検出
スタックに記録されたテンプレートの内容と実リソースの現在の状態を比較します。公式ドキュメントには以下のように記載されています。
リソースがドリフトしたかどうかを判断するために、CloudFormation はスタックテンプレートとテンプレートパラメータとして指定された値で定義されているように、期待されるリソースプロパティ値を決定します。CloudFormation は、それらの期待値を現在スタックに存在しているリソースプロパティの実際の値と比較します。
なお、テンプレートで明示的に設定していないプロパティ(デフォルト値に任せているもの)はドリフト検出の対象外です。公式ドキュメントにも以下の注記があります。
CloudFormation は、スタックテンプレートを通じて、またはテンプレートパラメータを指定することによって、明示的に設定されたプロパティについてのみドリフトを判断します。これには、リソースプロパティのデフォルト値は含まれません。
インポート
テンプレートの内容をスタック記録に書き込むだけの操作です。実リソースへのAPI呼び出しも、テンプレートとの一致検証もしません。これが私がハマった原因です。
なぜインポートしても設定が変わらないのか
インポート時に何が起きるかを分解すると、こうなります。
起きること:
- テンプレートに書かれたリソース定義を読む
- 指定された識別子(LogGroupName等)で実リソースの存在を確認する
- 実リソースが存在すれば、テンプレートの内容をスタック記録に書き込む
- 完了
起きないこと:
- テンプレートの設定値と実リソースの設定値の比較
- 実リソースへの設定変更API呼び出し
- ドリフトの検出や警告
つまりインポートは「このリソースをこのスタックの管理下に置く」という宣言であって、「このリソースをテンプレートの状態に合わせる」操作ではありません。
AWSのドキュメントにも以下の記述があります。
CloudFormation は、テンプレート構成がリソースプロパティの実際の構成と一致しているかどうかをチェックしません。
さらに、インポート後のドリフト検出を推奨する記述もあります。
インポートが完了した後、後続のスタックオペレーションを実行する前に、インポートされたリソースでドリフト検出を実行することをお勧めします。ドリフト検出により、テンプレート構成が実際の構成と一致することが保証されます。
インポート後のドリフト検出を推奨しているということは、裏を返せば「インポートだけでは一致が保証されない」ことを意味しています。
私のケースに当てはめると以下の様な動作をしていました。
インポート時:
- テンプレートに
RetentionInDays: 30と書いてインポートした - 「前回デプロイしたテンプレート」として
RetentionInDays: 30が記録された - 実リソースは無期限のまま(何も変わっていない)
次のデプロイ時:
- 新テンプレート:
RetentionInDays: 30 - 前回デプロイしたテンプレート:
RetentionInDays: 30(インポート時に記録された) - 差分なし → 「変更なし」
通常のChange Setは実リソースを見ないので、ログの保存期間が無期限のままであることに気づけません。新テンプレートと前回テンプレートが一致しているので、「何もする必要がない」と判断します。
インポート後の正しいワークフロー
では、どうすれば良かったのでしょうか。ポイントは「インポート時のテンプレートは現状に合わせて、変更は後からデプロイで行う」です。
Step 1: 現状と一致するテンプレートでインポートする
インポート時にテンプレートに書く内容は「あるべき状態」ではなく「現在の状態」です。
Resources: MyFunctionLogGroup: Type: AWS::Logs::LogGroup DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: LogGroupName: /aws/lambda/my-function # RetentionInDaysは書かない(現状の「無期限」と一致させる)
このテンプレートを使ってインポートします。マネジメントコンソールの場合は、スタックの「スタックアクション」→「スタックへのリソースのインポート」から実行します。 こうすることで、スタック記録に書き込まれる内容が実リソースと一致した状態でインポートが完了します。
Step 2: ドリフト検出で確認する(推奨)
インポートが完了したら、念のためドリフト検出を走らせて乖離がないことを確認しておくと安心です。 マネジメントコンソールの場合はスタックの「スタックアクション」→「ドリフトの検出」から実行します。
Step 1で正しく現状に合わせていれば、ドリフトは検出されないはずです。
Step 3: テンプレートを「あるべき状態」に修正する
テンプレートを最終的に適用したい状態に変更します。
Resources: MyFunctionLogGroup: Type: AWS::Logs::LogGroup DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: LogGroupName: /aws/lambda/my-function RetentionInDays: 30 # ← ここで追加
Step 4: デプロイする
最後に、更新したテンプレートでスタックを更新します。 マネジメントコンソールの場合はスタックの「更新」から実行します。
テンプレート(RetentionInDays: 30)とスタック記録(RetentionInDaysなし)の差分が検出され、実リソースに保持期間が設定されます。
ハマりやすいリソースタイプ
ロググループ以外にも、インポートで同じ問題が起きやすいリソースがあります。
デフォルト値が暗黙的に適用されるリソース
| リソース | プロパティ | テンプレートで省略した場合 | 注意点 |
|---|---|---|---|
| LogGroup | RetentionInDays | 無期限 | 省略と無期限が一致するので比較的安全 |
| S3 Bucket | VersioningConfiguration | 無効 | 「一度有効にして停止(Suspended)」と「一度も有効にしていない」は異なる状態 |
| DynamoDB Table | BillingMode | PROVISIONED | 実リソースがPAY_PER_REQUESTの場合、省略するとドリフトする |
S3バケットのVersioningは特にハマりやすいです。テンプレートで省略すると「無効」として扱われますが、実リソースが「Suspended」の場合は状態が異なります。
Replacementが発生するプロパティに注意
インポート後にテンプレートを修正してデプロイしたとき、プロパティによってはUpdateではなくReplacement(削除→再作成)になることがあります。
MyFunctionLogGroup: Type: AWS::Logs::LogGroup DeletionPolicy: Retain # スタック削除時にリソースを残す UpdateReplacePolicy: Retain # Replacement時に既存リソースを残す Properties: LogGroupName: /aws/lambda/my-function RetentionInDays: 30
UpdateReplacePolicy: Retainを付けておかないと、Replacementが発生した場合に既存リソース(とそのデータ)が削除されます。インポートするリソースには両方のポリシーを付けておくのが安全です。
まとめ
CloudFormationのインポートは「テンプレートの内容をスタック記録に書き込む」操作であり、実リソースへの変更は行いません。そのため、インポート時のテンプレートには「あるべき状態」ではなく「実リソースの現在の状態」を書く必要があります。あるべき状態への変更は、インポートが完了してからテンプレートを修正してデプロイする、という2段階で行います。