AWS CDK でカスタムリソースの更新をかけるには? - 図とかにするブログ

図とかにするブログ

監査系コンサル会社にて、IT エンジニアとして勤務している人のブログです。 AWS・Azure などのクラウドインフラと、バックエンド関連の技術を触っています。 なるべく図解と結論ファーストを意識した、読み手にとってタイパの良い技術発信を心がけます。

AWS CDK でカスタムリソースの更新をかけるには?

カスタムリソースを定期的に更新をかけるような運用がなかなか調べても出てこなかったのでそれについてまとめました。 カスタムリソースは調べていてどういう書き方をしたらいいのかなかなか調査コストが高かったので、仕組みなども含めたまとめています。

【カスタムリソースのつらみ】

  • CloudFormation と CDK の2つの理解しなきゃだから学習コストが高い&情報が交錯しがち
  • 特にカスタムリソース周りのドキュメントがとっ散らかっていてどれを見たらいいのかわからない
    • 2次情報も情報が多いが故に、どれがベストプラクティスなの?ってなりがち
  • あれこれどうなんだっけ?ってなった時に、そこらへんうまいこと爆速で振り返るための記事があんまないな印象
    • ドキュメントとしてはこれが全てを詳細に解説してくれているが、いかんせん英語だし長いしで読みづらい

結論

とりあえず手っ取り早く使いたければ、こういう書き方をすればよさそう。

Provider Framework というものでこちらが、公式推奨の書き方です。

ダミーのPropertyも用意して、IaC コード上から自由にリソース更新をかけられるようにしています。

    const lmd = new lambda.Function(this, `TestMetricsLambdaFunction`, {
      runtime: lambda.Runtime.PYTHON_3_12,
      handler: "index.handler",
      code: // ここで lambda のコードを引っ張る
      timeout: cdk.Duration.minutes(15),
    });

    const provider = new cr.Provider(this, `TestCustomResourceProvider`, {
      onEventHandler: lmd,
    });

    new cdk.CustomResource(this, `TestCustomResource`, {
      serviceToken: provider.serviceToken,
      properties: {
        ServiceTimeout: 60,  // CloudFormation のタイムアウトは 1h がデフォなため、タイムアウトをちゃんと設定しておかないと酷い目に遭う
        dummyForUpdate: "triggerd at 2024-11-19",         // lambda を再度実行したい場合、このパラメータを変更することで lambda に update イベントが送られて実行される
      },
    });

ちゃんと理解しておいた方がいいのは↓あたりなので、以降解説。

  • CloudFormation の Updateイベントの発行タイミング
  • Updateイベントの際のカスタムリソースの挙動
  • Provide Framework の仕組み

そもそもの CDK の基本的な流れ

まず、CDK〜CloudFormation の基本的な動作について。

これは AWS BlackBelt 以上に詳しくかつわかりやすい資料がない気がするので、こちらを引用。

基本的に TypeScript などの一般に開発に用いられる言語を用いて、CloudFormation Template を生成(Synthesize)するコードを記述することができます。

CloudFormation はこの Template から Stack と呼ばれる単位で AWS リソース群を状態管理し、API を呼び出しによって実際の AWSリソースを生成・更新・削除を行います。

AWS BlackBelt Online Seminar より

https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_AWS-CDK-Basic-1-Overview_0731_v1.pdf

ポイントとしてはあくまで、CDK が直接 IaC として機能するというよりは、CloudFormation Template を生成している、ということは常に意識することです。

裏側で CloudFormation がどのような動作をしているのかは頭に入れておかないと、開発を完了してから後々で色々なところで綻びが生じて、酷い目にあうことが多かったです。

カスタムリソース周りの用語について

用語の定義はめんどくさいけれど、 カスタムリソース周りは似たような用語が多くて混乱するので、最初に記載。

自分の中での理解なのでドキュメントとのズレはあるかもしれないが大体こんな感じのはず。

  • カスタムリソース
    • 広義:CloudFormation 上でサポートされていない AWS リソースを CloudFormation や CDK を用いて管理するための枠組み全般のこと
    • 狭義:CloudFromation でユーザー定義の lambda 関数などを用いて生成された一般に CloudFormation では管理できないAWSリソースを、CloudFormation の Stack の枠組みで管理するためのリソース

カスタムリソースプロバイダ:上記のカスタムリソースを展開するために必要な lambda などのリソース・仕組み、カスタムリソースを管理するためのリソース

Provider Framework :カスタムリソースプロバイダを CDK で簡単に使えるようにするためのクラス・仕組み

イメージはこんな感じ。

Custom Resource Provider と Provider Framewowrk

CloudFormation 上でのカスタムリソースの動き

基本的な動作として、CloudFormation のリソースは、 CloudFormation Template を元に、CloudFormation Stack が作成(Create)され、AWS リソースが展開されます。

この Stack を更新(Update)・削除(Delete)することで、実際の AWS リソースも更新・削除されます。

When creating a stack, CloudFormation makes underlying service calls to AWS to provision and configure your resources.

docs.aws.amazon.com

カスタムリソースの場合も基本的にこの流れは同じで 対象の lambda に実行された動作(Create, Update, Delete)イベントの情報と一緒にリクエストが送られます。

CloudFormation がリクエストを送る先は、サービストークン(Service Tokenと呼ばれます。

いまいち変な名前をつけていてわかりづらいけれど、要は対象の lambda の ARN です。

whenever anyone uses the template to create, update, or delete the custom resource, CloudFormation sends a request to the specified service token, and then waits for a response before proceeding with the stack operation.

docs.aws.amazon.com

Service Token

対象の lambda に対しては実行された動作の情報と一緒に S3 の署名付きURL が送信されます。

急に署名付きURL が登場して混乱しますが、これは、カスタムリソースは CloudFormation に直接レスポンスを返すのではなく、S3 経由で CloudFormation へレスポンスを返却する仕様のよう。

どうにもここで署名付きURLが採用されている明確な理由はドキュメントがなかったので分からず、、、

The request includes information such as the request type and a pre-signed Amazon Simple Storage Service URL, where the custom resource sends responses to.

S3署名つきURL

対象の Lambda は CloudFormation からのリクエストを処理すると、リソース作成成否をSUCCESS またはFAILEDとして、上述の通り S3 へレスポンスを返却します。

SUCCESSが返却されれば、CloudFormation は、Stack の操作を継続し、それ以外であれば操作を中止します。(この時に、CloudFormation の Export に利用するような key value のデータを一緒に返却することもできる)

The custom resource provider processes the CloudFormation request and returns a response of SUCCESS or FAILED to the pre-signed URL. The custom resource provider provides the response in a JSON-formatted file and uploads it to the pre-signed S3 URL.

レスポンス

CloudFormation がカスタムリソースを操作する場合の一連の流れはこのような感じです。

Updateイベントの際のリプレースメントの挙動

流れを理解したところで、CloudFormation におけるUpdateの動作について確認します。

作成Createと削除Deleteの動作はシンプルなので特に気をつけなくてもいいけれど、リプレースを伴うUpdateは、ハマりポイントの温床なので意識しておいたほうが吉です。

通常のリソースの挙動

通常のリソースは、基本的に↓のような順序で実行されます。(全てのリソースがそうではないらしい)

1. リプレース対象のリソースを新たに作成して、 2. 古いリソースを削除する

そのため、名前を一意にする必要があるリソースなど(sns, s3 etc...)は、一度削除したり、違う名前で一旦デプロイした後に、再度同じ名前でデプロイする、と言った操作をする必要があって面倒くさいです。

このほかにもIP を固定している ec2 は、ip が変わるため変更できなかったり、初めて CDK を触る場合ハマりポイントが多い印象です。 (これは CloudFormation, CDK のハマりポイントというより、IaC の、かもしれない)

AWS CloudFormation usually creates the replacement resource first, changes references from other dependent resources to point to the replacement resource, and then deletes the old resource

docs.aws.amazon.com

リプレースの起こる Update

カスタムリソースの挙動

では、一般のリソースと比較してカスタムリソースはどうかというと、 カスタムリソースも同じ枠組みで利用ができるように、PhysicalResouceIdという概念が用意されています。

こいつが何者かというと、カスタムリソースが同一のものであることを保証するためのIDで、カスタムリソース以外の CloudFormation 上のリソースでも普通に利用されているIDです。

Createによってカスタムリソースが作成されると、カスタムリソースプロバイダからこのPhysicalResourceIdが CloudFormation へ返却され、管理されます。

リソースのUpdateが起こると、カスタムリソースプロバイダへ現在のPhysicalResourceIdが渡されます。

ここでユーザーが Lambda 上のロジックにて、PhysicalResourceIdの更新をかけると、CloudFormation はリソースのリプレース(PhysicalResourceIdが変更 = リソースの変更)と判断して、さらにDeleteイベントをカスタムリソースプロバイダへ発行します。

Create, Update, Delete のイベントをどのように処理するかは、ユーザーが Lambda 内で条件分岐によって自由に記述できますが、 基本的に文字通りの操作を記述するとすれば、以下のような動作となるため、通常リソースと同じような動作を実現できるようになっています。

  1. Updateイベントによりリプレース対象のリソースを新たに作成して、PhysicalResourceIdを更新
  2. PhysicalResourceIdの変更で発行されるDeleteイベントにより、元のリソースが削除される

When a resource is created, the PhysicalResourceId returned from the Create operation is stored by AWS CloudFormation and assigned to the logical ID defined for this resource in the template. If a Create operation returns without a PhysicalResourceId, the framework will use RequestId as the default.

For Update and Delete operations, the resource event will always include the current PhysicalResourceId of the resource. When an Update operation occurs, the default behavior is to return the current physical resource ID. if the onEvent returns a PhysicalResourceId which is different from the current one, AWS CloudFormation will treat this as a resource replacement, and it will issue a subsequent Delete operation for the old resource.

docs.aws.amazon.com

カスタムリソースの Update

Updateイベントを発行するには?

Updateイベントが発行された際の流れは、これまで見てきた通りですが、そもそもUpdateイベントを発行しなければロジックを組んでも意味がありません。

Updateイベントは Stack をデプロイし直せば毎回発行されるようなものではなく、Stack 上の対象リソースの CloudFormation Template のPropertyが変更されないと発行されません。

なので、カスタムリソースをデプロイするための Lambda コードを変更したりしても新しくデプロイした Lambd が実行されるわけではないという罠があります。

ただ、カスタムリソースは自分で独自のプロパティを追加できるので、Updateをかけたい時に変更するような適当なダミーのPropertyを仕込んでおくことでUpdateイベントを発行することができます。

CDK でもこのカスタムのPropertyを利用していきます。

これを踏まえて CDK のカスタムリソースの更新を考える

ようやく本題です。

CDK ではこれまで見てきたCloudFormation の実装に加えて Provider Framework という概念が導入されています

簡潔かつ便利に扱える反面、高度に抽象化されているので初学者には分かりづらいかつ、適当に使っていると思っている挙動をせずに困ります。

Provider Framework は、CloudFormation で実装していた Lambda を

  1. CloudFormation とリクエスト・レスポンスをやり取りする関数

  2. ユーザーが定義したリソースを作成するロジックに集中する関数

の2つに分類するようなラッパーフレームワークで、つまり、1つのカスタムリソース作成のロジックのために、Lamda が2つデプロイされることになります

CloudFormation の Stack としては、以下の3つのリソースが作成されます。

  1. CloudFormation とリクエスト・レスポンスをやり取りする Lambda
  2. ユーザーが定義したリソースを作成するロジックに集中する Lambda
  3. カスタムリソース

Custom Resource Provider Framework を利用したデプロイ

  1. の CloudFormation とリクエスト・レスポンスをやり取りする関数はきちんと使うと CDK 側で勝手にデプロイしてくれるので、ユーザーはロジックの記述のみに集中することができるようになっています。

そしてもう一つややこしいポイントとして、カスタムリソース周りの検索性の低さがあります。

実際に「AWS Custom Resource」などで検索してみると、似たようなクラスがいくつもヒットするのでこれまたとっつきづらいです。

ネット上にも色々な書き方が出回っているので、最初はどれがベストプラクティスなのか判断するのにも一苦労します。(どれも間違ってるわけではないけれど)

CDK を記述する上で、まず利用すべきなのは Custom Resource Provider Framework ですので、それに該当するProviderクラスとCustom Resourceクラスを利用します

ドキュメント公式でも、特に理由がないのであればこの枠組みの使用を推奨しています。

we recommend you use this module unless you have good reasons not to. For an overview of different provider types you could be using,

docs.aws.amazon.com

Custom Resource Provider Framework のメインはProviderクラスで、 実体としては、先ほど説明した 1. CloudFormation とリクエスト・レスポンスをやり取りする関数 に当たると思われます。

実際にAWSリソースをデプロイする Lambda をこのProviderクラスに登録し、さらにそのProviderクラスをCustomResourceクラスに登録して利用します。

CustomResourceクラスは、狭義のカスタムリソースを管理する CloudFormation Stack 上のリソースです。

こいつを使って、カスタムリソースに独自のPropertyを追加します。

更新をかけたい際にはこのPropertyを変更することで、CloudFormation Template の値が変更され、Updateイベントが発行されます。

ついでに、タイムアウトの設定もします。

コート上にコメントしていますが、CloudFormation のデフォルトのタイムアウトが1時間なので、何もせずに変なリソースをデプロイしてタイムアウト待ちになると地獄なので、きっちり設定しておくのが推奨です。

これにて最初に紹介したコードの完成です。

カスタムリソースの先で管理されている AWS リソースの更新をかけるために Lambda を起動したい場合、ダミーのPropertyを手動で更新をかけることでUpdateイベントが送信されるので、 CloudFormation の一般的な挙動にしたければUpdateで新規リソースの作成とPhysicalResourceIdの更新をかけて、さらにDeleteイベントを発行して古いリソースを削除することができるわけです。 (綺麗な実装は結構めんどくさい気がします)

これで自由に更新がかけられるので、運用でも利用可能なカスタムリソース管理体制の完成です。 (理論上綺麗ではありますが、実状としてはリプレースが毎回発生するわけなのでなかなか難しいですね)

    const lmd = new lambda.Function(this, `TestMetricsLambdaFunction`, {
      runtime: lambda.Runtime.PYTHON_3_12, // 好きな言語を選択
      handler: "index.handler",
      code: // ここで lambda のコードを引っ張る
      timeout: cdk.Duration.minutes(15),
    });

    const provider = new cr.Provider(this, `TestCustomResourceProvider`, {
      onEventHandler: lmd,
    });

    new cdk.CustomResource(this, `TestCustomResource`, {
      serviceToken: provider.serviceToken,
      properties: {
        ServiceTimeout: 60,  // CloudFormation のタイムアウトは 1h がデフォなため、タイムアウトをちゃんと設定しておかないと酷い目に遭う
        dummyForUpdate: "triggerd at 2024-11-19",         // lambda を再度実行したい場合、このパラメータを変更することで lambda に update イベントが送られて実行される
      },
    });

余談1)その他の書き方

カスタムリソースのその他のクラスも紹介しておきます。

AwsCustomResourceクラス

難しいロジックを必要とせず、単に AWS API を1回呼ぶだけ、といったカスタムリソースプロバイダを利用したいケースもあると思います。

その場合、こちらのAwsCustomResourceを利用すると、自分でAWSリソースを作成する Lambda コードすら書かずに簡単にかくことができます。

const getParameter = new cr.AwsCustomResource(this, 'GetParameter', {
  onUpdate: { // will also be called for a CREATE event
    service: 'SSM',
    action: 'GetParameter',
    parameters: {
      Name: 'my-parameter',
      WithDecryption: true,
    },
    physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()), // Update physical id to always fetch the latest version
  },
  policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
    resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
  }),
});

// Use the value in another construct with
getParameter.getResponseField('Parameter.Value');

↓リンクより引用

docs.aws.amazon.com

SNS backed Custom Resource

Lambda の代わりに SNS を使ってカスタムリソースを管理する方法もあります。

基本的には、Lambda 上ではなく、例えば EC2 でコードをホストしているなど。あんまり使用するケースはない気はします。

CustomResourceProviderクラス

こちらも検索するとヒットしがちなので、念の為紹介。

名前がまたややこしいが、こいつは CDK の Construct を記述するために利用するクラスで、CDK のユーザーが利用するクラスではないようです。

Application builders do not need to use this provider type. This is not a generic custom resource provider class. It is specifically intended to be used only by constructs in the AWS CDK Construct Library, and only exists here because of reverse dependency issues (for example, it cannot use iam.PolicyStatement objects, since the iam library already depends on the CDK core library and we cannot have cyclic dependencies).

docs.aws.amazon.com

余談2)cfn-response モジュール

カスタムリソース周りを調べていると、cfn-response モジュールを使って簡単に CloudFormation とやり取りできるよ!といった記述が見つかります。

これは確かにそうなんですが、CDK で Provider Framework を使っている場合特に使用することはないです。

というのも、cfn-response モジュールは CloudFormation に返却する必要なレスポンスオブジェクトを簡潔に記述することができるためのもので、 CloudFormation とのやり取りは Provider Framework が別の Lambda にて隠蔽してくれている部分なので不要となります。

docs.aws.amazon.com