💥

CloudFormation スタックで生じた乖離を解消するためのステップ

Serverless Framework で作成される CloudFormation (CFn) スタックについて、スタック上で管理されるリソース ID と実際のリソース ID が乖離しているという事象に遭遇しました。本記事ではこのような乖離を解消するための手順について解説します。

背景

Serverless Framework について

当社では IaC 管理ツールとして基本的に Terraform を使用していますが、サーバーレスアプリケーションの開発・デプロイに一部 Serverless Framework を使用しています。このサーバーレスアプリケーションでは、Lambda 関数や、Lambda 関数を統合バックエンドとする API Gateway リソースなどを管理しています。

Serverless Framework ではデプロイ実行の際、内部的に serverless.yml の内容を CFn テンプレートにコンパイルし、スタックを作成・更新します。serverless.yml に CFn リソース定義を含めることも可能ですが、基本的に CFn 操作は隠蔽されており、serverless コマンドだけ覚えておけばユーザー側で意識する必要はありません。

発端

いつものようにリリース作業を実行していた際、上記のサーバーレスアプリケーションのデプロイに失敗しました。原因はどうやら新たに追加された API Gateway メソッドに関連するもののようでしたが、ひとまず当該の PR を Revert して詳細を調査することにしました。

調査の結果、API Gateway メソッドに紐づく Authorizer について、(a) CFn スタックのリソース一覧上で表示されるリソース ID と (b) API Gateway コンソール上で確認できるリソース ID が異なる値になっていることに気が付きました。CFn スタック更新時に、実在しない AuthorizerID が設定された API Gateway メソッドを PutMethod しようとしたことでエラーが発生し、スタック更新に失敗したものと考えられます。

残念ながらこの乖離がいつ・なぜ発生したものかまで特定できませんでしたが、以前のデプロイ失敗に伴うロールバックが正常に完了しなかった 等が考えられます。いずれにしても今後のデプロイのために上記の乖離を解消しておく必要があるため、手順について調査・検証の上、実施することにしました。

手順概要

公式ドキュメント の手順を参考に実施していきます。対象となるリソースがインポートに対応したリソースタイプ [1] のものであれば、本記事の手順は実施できると思います。

大まかな流れ

Step-1. CFn 管理から除外するための準備を行う

後述の Step-3. にて 変更セット を使用したリソースのインポートを行うことになりますが、そのためにまずは対象リソースを CFn スタックの管理外にする必要があります。対象リソースの定義を削除してスタック更新するだけだとリソース自体も削除されてしまうため、DeletionPolicy 属性 を設定してリソースが保持されるようにします。

Step-2. 一時的にリソースを CFn 管理から除外する

Step-1. で DeletionPolicy 属性 を設定したため、安全に CFn 管理外にできるようになりました。ただし、削除する対象リソース定義への依存が残っているとスタック更新できません。リソース定義と併せて Ref 参照DependsOn 属性 なども一緒に削除し、スタック更新することで対象リソースを CFn 管理外とします。

Step-3. 変更セットを作成・実行する

対象リソースを CFn 管理外としたことで、実在する(=正しいリソース ID を持つ)リソースをインポートできるようになりました。対象リソースとテンプレートを指定した 変更セット を作成・実行することで、リソースをインポートします。

Step-4. 原状回復

Step-3. のインポートを完了した段階では、元々のテンプレートとスタック最新状態のテンプレートに差分が生じているため、元々のテンプレートでスタックを更新し直します。その後、通常と同じデプロイ方法でスタックを更新し、動作に異常がないか確認します。

想定する前提

本記事では、当社環境で実際に乖離が発生した AWS::ApiGateway::Authorizer のリソースを具体例として扱っていきます。CFn スタック上の論理リソース ID は MyAuthorizer とし、以降「対象リソース」と呼称します。

以下のような状況を想定するものとします。(実施される場合は、ご自身の環境に合わせて適宜読み替えてください)

項目
CFn スタック名 ProductA-stg
CFn テンプレート用のバケット名 my-serverless-bucket
対象リソースが紐づく REST API の ID pqrstu3456
対象リソース ( MyAuthorizer ) の ID CFn スタック上の値 = abc012 / 実際の値 = xyz789

また、CFn テンプレート用バケットは以下のような構造を想定します。Serverless Framework であれば S3 プレフィックス中のタイムスタンプから最新のコンパイル済みテンプレートを特定できます。以降、これを「オリジナルテンプレート」と呼称します。また、オリジナルテンプレートを基に手順の中でいくつかの修正済みテンプレートを配置するため、tmp/ フォルダを作成します。

s3://my-serverless-bucket/
└── serverless/
    └── ProductA/
        └── stg/
            ├── 1715577028791-2024-05-13T05:10:28.791Z/
            ├── 1715579105315-2024-05-13T05:45:05.315Z/    # 最新の Serverless 管理フォルダ
            │   ├── compiled-cloudformation-template.json  # オリジナルテンプレート
            │   └── serverless-state.json
            └── tmp/  # 本記事の手順のために手動で作成するフォルダ
                ├── modified-template1.json
                ├── modified-template2.json
                └── modified-template3.json

Step-1. CFn 管理から除外するための準備を行う

1.1. テンプレートのダウンロード & フォーマット

S3 または CFn スタック詳細の [テンプレート] から、現在デプロイされている CFn スタックのテンプレートを取得します。(前述の通り、Serverless Framework であれば、S3 上に compiled-cloudformation-template.json というファイル名で保存されています)

編集しやすいよう、取得したテンプレートを jq コマンドなどでフォーマットしておきます。

% cat compiled-cloudformation-template.json | jq . > formatted-template.json

1.2. DeletionPolicy 属性を設定する

formatted-template.json を修正し、対象リソースに DeletionPolicy 属性Retain に設定します。これにより、対象リソースを CFn テンプレートから削除してスタック更新してもリソースを保持することができます。

modified-template1.json
    "Resources": {
        ...
        "MyAuthorizer": {
            "Type": "AWS::ApiGateway::Authorizer",
+           "DeletionPolicy": "Retain",
            "Properties": {...}
        },
        ...
    },

1.3. スタックを更新する

修正後のテンプレートを modified-template1.json として保存し、S3 にアップロードします。CFn スタック更新を実行し、DeletionPolicy 属性追加の変更を反映させます。

% aws cloudformation update-stack \
    --stack-name ProductA-stg \
    --template-url https://my-serverless-bucket.s3.ap-northeast-1.amazonaws.com/serverless/ProductA/stg/tmp/modified-template1.json \
    --capabilities CAPABILITY_NAMED_IAM

Step-2. 一時的にリソースを CFn 管理から除外する

対象リソースの定義を安全に削除する準備が整ったので、実際に定義を削除してスタックを更新し、一時的に CFn 管理外としていきます。modified-template1.json を以下のように編集した、modified-template2.json を作成します。

2.1. Ref 参照をハードコードする

対象リソースを Ref 参照 している箇所がある場合には、全てハードコードされた値に置き換えます。以下は AuthorizerId を Ref で参照している API Gateway メソッドの例です。

modified-template2.json
    "Resources": {
        "ApiGatewayMethodReportsPost": {
            "Type": "AWS::ApiGateway::Method",
            "Properties": {
                ...
-               "AuthorizerId": {
-                   "Ref": "MyAuthorizer"
-               },
+               "AuthorizerId": "xyz789"
                ...
            }
            "DependsOn": [
                "MyAuthorizer"
            ]
        }
    }

2.2. DependsOn 属性を削除する

対象リソースへの DependsOn 属性 が設定されている場合には、設定を全て削除します。

modified-template2.json
    "Resources": {
        "ApiGatewayMethodReportsPost": {
            "Type": "AWS::ApiGateway::Method",
            "Properties": {
                ...
                "AuthorizerId": "xyz789"
                ...
            }
-           "DependsOn": [
-               "MyAuthorizer"
-           ]
        }
    }

2.3. リソース定義を削除する

Ref 参照と DependsOn 属性がなくなったことで、対象リソースの定義を削除できるようになったので、該当箇所を削除します。

modified-template2.json
    "Resources": {
        ...
-       "MyAuthorizer": {
-           "Type": "AWS::ApiGateway::Authorizer",
-           "DeletionPolicy": "Retain",
-           "Properties": {...}
-       },
        ...
    },

2.4. スタックを更新する

修正後のテンプレートを modified-template2.json として保存し、S3 にアップロードします。CFn スタック更新を実行することで、対象リソースである API Gateway Authorizer を保持しつつ、CFn スタックのリソース一覧からは表示されなくなります。

% aws cloudformation update-stack \
    --stack-name ProductA-stg \
    --template-url https://my-serverless-bucket.s3.ap-northeast-1.amazonaws.com/serverless/ProductA/stg/tmp/modified-template2.json \
    --capabilities CAPABILITY_NAMED_IAM

Step-3. 変更セットを作成・実行する

既存のリソース定義を CFn から削除したことで、実在するリソースをインポートする準備が整ったので、変更セット を作成・実行することで実際にインポートしていきます。modified-template2.json を以下のように編集した、modified-template3.json を作成します。

3.1. インポート先の CFn テンプレートを作成する

modified-template2.json を作成する時点で削除したリソース定義を再度追加します。これが変更セットのインポート先のリソース定義になります。

modified-template3.json
    "Resources": {
        ...
+       "MyAuthorizer": {
+           "Type": "AWS::ApiGateway::Authorizer",
+           "DeletionPolicy": "Retain",
+           "Properties": {...}
+       },
        ...
    },

3.2. 変更セットを作成する

修正後のテンプレートを modified-template3.json として保存し、S3 にアップロードします。以下のコマンドを実行し、変更セットを作成します。変更セット名 ( --change-set-name ) は任意の名前に置き換えてください。

% aws cloudformation create-change-set \
    --stack-name ProductA-stg \
    --change-set-name ImportMyAuthorizer \
    --change-set-type IMPORT \
    --resources-to-import "[{\"ResourceType\":\"AWS::ApiGateway::Authorizer\",\"LogicalResourceId\":\"MyAuthorizer\",\"ResourceIdentifier\":{\"RestApiId\":\"pqrstu3456\", \"AuthorizerId\":\"xyz789\"}}]" \
    --template-url https://my-serverless-bucket.s3.ap-northeast-1.amazonaws.com/serverless/ProductA/stg/tmp/modified-template3.json \
    --capabilities CAPABILITY_NAMED_IAM

3.3. 変更セットを実行する

describe-change-set コマンドで、作成した変更セットの内容を確認できます。以下の実行結果に記載したような情報が得られます。

% aws cloudformation describe-change-set \
    --change-set-name ImportMyAuthorizer \
    --stack-name ProductA-stg
実行結果
{
    "Changes": [
        {
            "Type": "Resource",
            "ResourceChange": {
                "Action": "Import",
                "LogicalResourceId": "MyAuthorizer",
                "PhysicalResourceId": "xyz789",
                "ResourceType": "AWS::ApiGateway::Authorizer",
                "Scope": [],
                "Details": []
            }
        }
    ],
    "ChangeSetName": "ImportMyAuthorizer",
    "ChangeSetId": "arn:aws:cloudformation:ap-northeast-1:000000000000:changeSet/ImportMyAuthorizer/xxxxd6f7-f0f4-4b97-9e71-ad0b81551e73",
    "StackId": "arn:aws:cloudformation:ap-northeast-1:000000000000:stack/ProductA-stg/xxxx8200-147d-11ee-805c-0a9891ad6881",
    "StackName": "ProductA-stg",
    "Description": null,
    "Parameters": null,
    "CreationTime": "2024-05-13T04:23:20.751000+00:00",
    "ExecutionStatus": "AVAILABLE",
    "Status": "CREATE_COMPLETE",
    "StatusReason": "Verify that resources and their properties defined in the template match the intended configuration of the resource import to avoid unexpected changes.",
    "NotificationARNs": [],
    "RollbackConfiguration": {},
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ],
    "Tags": null,
    "ParentChangeSetId": null,
    "IncludeNestedStacks": false,
    "RootChangeSetId": null
}

describe-change-set コマンドで変更セットを実行します。

% aws cloudformation execute-change-set \
    --change-set-name ImportMyAuthorizer \
    --stack-name ProductA-stg

完了すると、CFn スタックのリソース一覧に正しいリソース ID で表示されるようになります。

3.4. ドリフト検出を実行し、必要に応じて修正する【任意】

CFn 上のリソース定義とインポートした実際のリソースとの間に差分(=ドリフト)がないか、以下のコマンドを実行することで確認できます。ドリフトが発生している場合には、リソース定義を修正するか、リソースを直接更新します。(筆者環境においては実施する必要がなかったため、公式ドキュメント に記載のコマンド例をそのまま引用します)

% aws cloudformation detect-stack-drift --stack-name TargetStack
{ "Stack-Drift-Detection-Id" : "624af370-311a-11e8-b6b7-500cexample" }

% aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id 624af370-311a-11e8-b6b7-500cexample
% aws cloudformation describe-stack-resource-drifts --stack-name TargetStack

Step-4. 原状回復

4.1. オリジナルテンプレートでスタック更新する

最後にデプロイしたテンプレートである modified-template3.json は、オリジナルのものからの差分を含んでいるため、オリジナルテンプレートで再度スタック更新を実行します。

% aws cloudformation update-stack \
    --stack-name ProductA-stg \
    --template-url https://my-serverless-bucket.s3.ap-northeast-1.amazonaws.com/serverless/ProductA/stg/1715579105315-2024-05-13T05%3A45%3A05.315Z/compiled-cloudformation-template.json \
    --capabilities CAPABILITY_NAMED_IAM

4.2. 本来のデプロイ方法で動作確認する

最後に、本来のデプロイ方法で再度スタックを更新し、動作に問題がないか確認しておきます。例えば当社では、サーバーレスアプリケーションのデプロイを GitHub Actions からの serverless deploy によって実行しているため、これと同じ方法でデプロイを実行します。

基本的には問題は生じないはずですが、筆者の環境では GitHub Actions が serverless deploy 用として使用している IAM ロールにリソースタグ関連の IAM 権限が不足しており、デプロイに失敗してしまいました。必要な IAM 権限を追加して再度実行したところ、今度は成功しました。

さいごに

CloudFormation スタックで生じた乖離を解消するための手順についてまとめてみました。

(筆者もそうですが)普段 Terraform で IaC 管理されている方からすると、いざ CFn スタックの操作が必要になった際に少し戸惑うのではないかと思い、今回備忘も兼ねて記事にしてみました。一方で、CloudFormation も StackSets を使用したマルチアカウントデプロイなどは個人的に非常に便利だと思っており、ユースケースに合わせて適切に使い分けていきたいなと思います。

最後まで読んで頂き、ありがとうございました。

脚注
  1. リソースタイプのサポート - AWS CloudFormation ユーザーガイド ↩︎

SimpleForm Tech Blog

Discussion