AWS CDK のカスタムリソースの3種類の AwsSdkCall それぞれのトリガータイミングを確認してみた
こんにちは、製造ビジネステクノロジー部の若槻です。
AWS CDK ではカスタムリソース(AwsCustomResource コンストラクト)を使うことにより、CDK の各種ライフサイクル時に任意の AWS API を呼び出したり Lambda 関数を実行したりできます。
カスタムリソースでは処理を呼び出したいタイミングによって下記の 3 種類のトリガー(AwsSdkCall)が使用できます。
onCreate?
Type: AwsSdkCall (optional, default: the call when the resource is updated)The AWS SDK call to make when the resource is created.
onDelete?
Type: AwsSdkCall (optional, default: no call)The AWS SDK call to make when the resource is deleted.
onUpdate?
Type: AwsSdkCall (optional, default: no call)The AWS SDK call to make when the resource is updated.
今回はこの AWS CDK のカスタムリソースの3種類の AwsSdkCall それぞれのトリガータイミングを実際に検証して確認してみました。
最初に結論
検証結果です。ライフサイクルごとに呼び出される AwsSdkCall は次の通りとなりました。
| ライフサイクル | AwsSdkCall | 
|---|---|
| スタック作成時 | onCreate | 
| スタック更新時 | onUpdate | 
| スタック削除時 | onDelete | 
| カスタムリソース追加時 | onCreate | 
| カスタムリソース更新時 | onUpdate | 
| カスタムリソース置き換え時 | onUpdate | 
| カスタムリソース削除時 | onDelete | 
| カスタムリソースに変更がない時 | onUpdate | 
確認してみた
CDK コード
検証に使用したベースとなる CDK コードは下記の通りです。コンテキストの値と AwsSdkCall のトリガータイミングによって SSM パラメーターに値を保存するカスタムリソースを作成しています。それにより、各ライフサイクル時に保存される値を確認します。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as iam from 'aws-cdk-lib/aws-iam';
interface MainStackProps {
  operation:
    | 'stack-created' // スタック作成時
    | 'stack-updated' // スタック更新時
    | 'stack-deleted' // スタック削除時
    | 'stack-rolled-back' // スタックのロールバック発生時
    | 'cr-added' // カスタムリソース追加時
    | 'cr-updated' // カスタムリソース更新時
    | 'cr-replaced' // カスタムリソース置き換え時
    | 'cr-deleted' // カスタムリソース削除時
    | 'cr-no-change'; // カスタムリソースに変更がない時
}
export class MainStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: MainStackProps) {
    super(scope, id);
    const { operation } = props;
    const parameterName = 'MyParameter';
    // SSM パラメーター "MyParameter" に "${operation}_${lifecycleMethod}" という値を保存するカスタムリソース
    new cr.AwsCustomResource(this, 'MyCustomResource', {
      onCreate: {
        service: 'SSM',
        action: 'putParameter',
        parameters: {
          Name: parameterName,
          Value: `${operation}_onCreate`,
          Type: 'String',
          Overwrite: true,
        },
        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
      },
      onUpdate: {
        service: 'SSM',
        action: 'putParameter',
        parameters: {
          Name: parameterName,
          Value: `${operation}_onUpdate`,
          Type: 'String',
          Overwrite: true,
        },
        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
      },
      onDelete: {
        service: 'SSM',
        action: 'putParameter',
        parameters: {
          Name: parameterName,
          Value: `${operation}_onDelete`,
          Type: 'String',
          Overwrite: true,
        },
        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
      },
      policy: cr.AwsCustomResourcePolicy.fromStatements([
        new iam.PolicyStatement({
          actions: ['ssm:PutParameter'],
          resources: ['*'],
        }),
      ]),
    });
  }
}
SSM パラメーターは下記の通り別途作成済みです。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
First Parameter
以下、各ライフサイクルごとの検証内容を記載していますが、検証しやすさのため最初の検証結果のテーブルの順とはなっていない点にはご留意ください。
スタック作成時
初回の CDK スタック作成のデプロイの場合。
cdk deploy -c operation=stack-created
onCreate が呼び出されました。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
stack-created_onCreate
カスタムリソースに変更がない時
カスタムリソース含めスタック全体のコードを変更せずにデプロイした場合。
cdk deploy -c operation=cr-no-change
onUpdate が呼び出されました。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
cr-no-change_onUpdate
カスタムリソース更新時
カスタムリソースのパラメーターを更新してデプロイした場合。
$ git diff lib/main-stack.ts
diff --git a/lib/main-stack.ts b/lib/main-stack.ts
index 3c411b1..01d2b37 100644
--- a/lib/main-stack.ts
+++ b/lib/main-stack.ts
@@ -44,6 +44,7 @@ export class MainStack extends cdk.Stack {
           Value: `${operation}_onUpdate`,
           Type: 'String',
           Overwrite: true,
+          hoge: 'fuga',
         },
         physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
       },
cdk deploy -c operation=cr-updated
カスタムリソースの更新が走るのでデプロイ完了までに作成時と同じくらい時間がかかります。
onUpdate が呼び出されました。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
cr-updated_onUpdate
スタック更新時
同じスタック内に適当なリソースの実装を追加した場合。
$ git diff
diff --git a/lib/main-stack.ts b/lib/main-stack.ts
index 01d2b37..f03a663 100644
--- a/lib/main-stack.ts
+++ b/lib/main-stack.ts
@@ -2,6 +2,7 @@ import * as cdk from 'aws-cdk-lib';
 import { Construct } from 'constructs';
 import * as cr from 'aws-cdk-lib/custom-resources';
 import * as iam from 'aws-cdk-lib/aws-iam';
+import * as s3 from 'aws-cdk-lib/aws-s3';
 interface MainStackProps {
   operation:
@@ -78,5 +79,8 @@ export class MainStack extends cdk.Stack {
         })
       );
     }
+
+    // 適当なリソースの実装を追加
+    new s3.Bucket(this, 'MyBucket');
   }
 }
cdk deploy -c operation=stack-updated
onUpdate が呼び出されました。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
stack-updated_onUpdate
カスタムリソース置き換え時
カスタムリソースの物理 ID を変更してデプロイした場合。
$ git diff
diff --git a/lib/main-stack.ts b/lib/main-stack.ts
index d30a31e..67b6c8d 100644
--- a/lib/main-stack.ts
+++ b/lib/main-stack.ts
@@ -34,7 +34,7 @@ export class MainStack extends cdk.Stack {
           Type: 'String',
           Overwrite: true,
         },
-        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
+        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id-2'),
       },
       onUpdate: {
         service: 'SSM',
cdk deploy -c operation=cr-replaced
onUpdate が呼び出されました。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
cr-replaced_onUpdate
カスタムリソース削除時
カスタムリソースを削除してデプロイした場合。
$ git diff
diff --git a/lib/main-stack.ts b/lib/main-stack.ts
index 2515848..869aefe 100644
--- a/lib/main-stack.ts
+++ b/lib/main-stack.ts
@@ -22,48 +22,5 @@ export class MainStack extends cdk.Stack {
     const { operation } = props;
     const parameterName = 'MyParameter';
-
-    // SSM パラメーター "MyParameter" に "${operation}_${lifecycleMethod}" という値を保存するカスタムリソース
-    new cr.AwsCustomResource(this, 'MyCustomResource', {
-      onCreate: {
-        service: 'SSM',
-        action: 'putParameter',
-        parameters: {
-          Name: parameterName,
-          Value: `${operation}_onCreate`,
-          Type: 'String',
-          Overwrite: true,
-        },
-        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
-      },
-      onUpdate: {
-        service: 'SSM',
-        action: 'putParameter',
-        parameters: {
-          Name: parameterName,
-          Value: `${operation}_onUpdate`,
-          Type: 'String',
-          Overwrite: true,
-        },
-        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
-      },
-      onDelete: {
-        service: 'SSM',
-        action: 'putParameter',
-        parameters: {
-          Name: parameterName,
-          Value: `${operation}_onDelete`,
-          Type: 'String',
-          Overwrite: true,
-        },
-        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
-      },
-      policy: cr.AwsCustomResourcePolicy.fromStatements([
-        new iam.PolicyStatement({
-          actions: ['ssm:PutParameter'],
-          resources: ['*'],
-        }),
-      ]),
-    });
   }
 }
cdk deploy -c operation=cr-deleted
onDelete が呼び出されました。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
cr-replaced_onDelete
カスタムリソース追加時
既存のスタックにカスタムリソースを追加してデプロイした場合。
$ git diff
diff --git a/lib/main-stack.ts b/lib/main-stack.ts
index 869aefe..2515848 100644
--- a/lib/main-stack.ts
+++ b/lib/main-stack.ts
@@ -22,5 +22,48 @@ export class MainStack extends cdk.Stack {
     const { operation } = props;
     const parameterName = 'MyParameter';
+
+    // SSM パラメーター "MyParameter" に "${operation}_${lifecycleMethod}" という値を保存するカスタムリソース
+    new cr.AwsCustomResource(this, 'MyCustomResource', {
+      onCreate: {
+        service: 'SSM',
+        action: 'putParameter',
+        parameters: {
+          Name: parameterName,
+          Value: `${operation}_onCreate`,
+          Type: 'String',
+          Overwrite: true,
+        },
+        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
+      },
+      onUpdate: {
+        service: 'SSM',
+        action: 'putParameter',
+        parameters: {
+          Name: parameterName,
+          Value: `${operation}_onUpdate`,
+          Type: 'String',
+          Overwrite: true,
+        },
+        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
+      },
+      onDelete: {
+        service: 'SSM',
+        action: 'putParameter',
+        parameters: {
+          Name: parameterName,
+          Value: `${operation}_onDelete`,
+          Type: 'String',
+          Overwrite: true,
+        },
+        physicalResourceId: cr.PhysicalResourceId.of('my-unique-id'),
+      },
+      policy: cr.AwsCustomResourcePolicy.fromStatements([
+        new iam.PolicyStatement({
+          actions: ['ssm:PutParameter'],
+          resources: ['*'],
+        }),
+      ]),
+    });
   }
 }
cdk deploy -c operation=cr-added
onCreate が呼び出されました。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
cr-added_onCreate
スタックの削除
スタックを削除した場合。
cdk destroy -c operation=stack-deleted
onDelete が呼び出されました。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
stack-created_onDelete
ちなみに operation が stack-created となっているのは、スタック作成時に使用されたコンテキスト値がそのまま削除時にも使用されるという挙動となるためのようです。この仕様で困る場面は限られそうですが、削除時にコンテキストに依存する処理がある場合は注意が必要そうです。
デプロイロールバック時は?
最後に番外編として次のようにデプロイが失敗してロールバックとなる実装を追加してデプロイしてみます。
$ git diff
diff --git a/lib/main-stack.ts b/lib/main-stack.ts
index 3c411b1..a569b55 100644
--- a/lib/main-stack.ts
+++ b/lib/main-stack.ts
@@ -77,5 +77,15 @@ export class MainStack extends cdk.Stack {
         })
       );
     }
+
+    // ロールバックを検証するために存在しないマネージドポリシーを指定したIAMロールを作成する
+    new iam.Role(this, 'MyRole', {
+      assumedBy: new iam.AccountRootPrincipal(),
+      managedPolicies: [
+        {
+          managedPolicyArn: 'arn:aws:iam::aws:policy/NotExistManagedPolicy',
+        },
+      ],
+    });
   }
 }
スタックの更新を行い、デプロイを失敗させてロールバックを発生させます。
cdk deploy -c operation=stack-rolled-back
Main: start: Building 7d699f15f2b169e8182fcb91df5a0c3a8443246e469795599df4071cb5e03766:current_account-current_region
Main: success: Built 7d699f15f2b169e8182fcb91df5a0c3a8443246e469795599df4071cb5e03766:current_account-current_region
Main: start: Publishing 7d699f15f2b169e8182fcb91df5a0c3a8443246e469795599df4071cb5e03766:current_account-current_region
Main: success: Published 7d699f15f2b169e8182fcb91df5a0c3a8443246e469795599df4071cb5e03766:current_account-current_region
Main: deploying... [1/1]
Main: updating stack...
Main |   0 | 4:08:01 PM | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack | Main User Initiated
Main |   0 | 4:08:04 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role        | MyRole (MyRoleF48FFE04)
Main |   0 | 4:08:04 PM | UPDATE_IN_PROGRESS   | Custom::AWS           | MyCustomResource/Resource/Default (MyCustomResource776A0129)
Main |   0 | 4:08:06 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role        | MyRole (MyRoleF48FFE04) Policy arn:aws:iam::aws:policy/NotExistManagedPolicy does not exist or is not attachable. (Service: Iam, Status Code: 404, Request ID: 7354189e-1e48-4312-a74e-81c9f680404d)
Main |   0 | 4:08:06 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role        | MyRole (MyRoleF48FFE04) Resource creation Initiated
Main |   0 | 4:08:06 PM | CREATE_FAILED        | AWS::IAM::Role        | MyRole (MyRoleF48FFE04) Resource handler returned message: "Policy arn:aws:iam::aws:policy/NotExistManagedPolicy does not exist or is not attachable. (Service: Iam, Status Code: 404, Request ID: 7354189e-1e48-4312-a74e-81c9f680404d)" (RequestToken: 1a5401de-918c-d411-3954-47abe67e21b7, HandlerErrorCode: NotFound)
Main |   0 | 4:08:06 PM | UPDATE_FAILED        | Custom::AWS           | MyCustomResource/Resource/Default (MyCustomResource776A0129) Resource update cancelled
Main |   0 | 4:08:07 PM | UPDATE_ROLLBACK_IN_P | AWS::CloudFormation::Stack | Main The following resource(s) failed to create: [MyRoleF48FFE04]. The following resource(s) failed to update: [MyCustomResource776A0129].
Main |   0 | 4:08:09 PM | UPDATE_IN_PROGRESS   | Custom::AWS           | MyCustomResource/Resource/Default (MyCustomResource776A0129)
Main |   1 | 4:08:11 PM | UPDATE_COMPLETE      | Custom::AWS           | MyCustomResource/Resource/Default (MyCustomResource776A0129)
Main |   2 | 4:08:11 PM | UPDATE_ROLLBACK_COMP | AWS::CloudFormation::Stack | Main
Main |   2 | 4:08:13 PM | DELETE_IN_PROGRESS   | AWS::IAM::Role        | MyRole (MyRoleF48FFE04)
Main |   3 | 4:08:22 PM | DELETE_COMPLETE      | AWS::IAM::Role        | MyRole (MyRoleF48FFE04)
Main |   4 | 4:08:23 PM | UPDATE_ROLLBACK_COMP | AWS::CloudFormation::Stack | Main
Failed resources:
Main | 4:08:06 PM | CREATE_FAILED        | AWS::IAM::Role        | MyRole (MyRoleF48FFE04) Resource handler returned message: "Policy arn:aws:iam::aws:policy/NotExistManagedPolicy does not exist or is not attachable. (Service: Iam, Status Code: 404, Request ID: 7354189e-1e48-4312-a74e-81c9f680404d)" (RequestToken: 1a5401de-918c-d411-3954-47abe67e21b7, HandlerErrorCode: NotFound)
❌  Main failed: Error: The stack named Main failed to deploy: UPDATE_ROLLBACK_COMPLETE: Resource handler returned message: "Policy arn:aws:iam::aws:policy/NotExistManagedPolicy does not exist or is not attachable. (Service: Iam, Status Code: 404, Request ID: 7354189e-1e48-4312-a74e-81c9f680404d)" (RequestToken: 1a5401de-918c-d411-3954-47abe67e21b7, HandlerErrorCode: NotFound)
するとパラメーターの値は更新されておらず、カスタムリソースは呼び出されていないようでした。
$ aws ssm get-parameter --name MyParameter --query 'Parameter.Value' --output text
# 変更なし
本部分の挙動確認はロールバックとなったリソースがカスタムリソースに依存している場合など、いくつかのパターンの検証が必要となりそうなのでまたの機会に行いたいと思います。
おわりに
AWS CDK のカスタムリソースの3種類の AwsSdkCall それぞれのトリガータイミングを実際に検証して確認してみました。
結論として「スタック作成時」と「カスタムリソース作成時」は onCreate、「スタック削除時」と「カスタムリソース削除時」は onDelete、それ以外は onUpdate が呼び出されるというそれなりに直感的な挙動となること確認できました。
参考
以上













