カスタムリソースを定期的に更新をかけるような運用がなかなか調べても出てこなかったのでそれについてまとめました。 カスタムリソースは調べていてどういう書き方をしたらいいのかなかなか調査コストが高かったので、仕組みなども含めたまとめています。
【カスタムリソースのつらみ】
- 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リソースを生成・更新・削除を行います。
ポイントとしてはあくまで、CDK が直接 IaC として機能するというよりは、CloudFormation Template を生成している、ということは常に意識することです。
裏側で CloudFormation がどのような動作をしているのかは頭に入れておかないと、開発を完了してから後々で色々なところで綻びが生じて、酷い目にあうことが多かったです。
カスタムリソース周りの用語について
用語の定義はめんどくさいけれど、 カスタムリソース周りは似たような用語が多くて混乱するので、最初に記載。
自分の中での理解なのでドキュメントとのズレはあるかもしれないが大体こんな感じのはず。
- カスタムリソース
・カスタムリソースプロバイダ:上記のカスタムリソースを展開するために必要な lambda などのリソース・仕組み、カスタムリソースを管理するためのリソース
・Provider Framework :カスタムリソースプロバイダを CDK で簡単に使えるようにするためのクラス・仕組み
イメージはこんな感じ。
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.
カスタムリソースの場合も基本的にこの流れは同じで
対象の 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.
対象の 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.
対象の 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
カスタムリソースの挙動
では、一般のリソースと比較してカスタムリソースはどうかというと、 カスタムリソースも同じ枠組みで利用ができるように、PhysicalResouceIdという概念が用意されています。
こいつが何者かというと、カスタムリソースが同一のものであることを保証するためのIDで、カスタムリソース以外の CloudFormation 上のリソースでも普通に利用されているIDです。
Create
によってカスタムリソースが作成されると、カスタムリソースプロバイダからこのPhysicalResourceId
が CloudFormation へ返却され、管理されます。
リソースのUpdate
が起こると、カスタムリソースプロバイダへ現在のPhysicalResourceId
が渡されます。
ここでユーザーが Lambda 上のロジックにて、PhysicalResourceId
の更新をかけると、CloudFormation はリソースのリプレース(PhysicalResourceId
が変更 = リソースの変更)と判断して、さらにDelete
イベントをカスタムリソースプロバイダへ発行します。
Create
, Update
, Delete
のイベントをどのように処理するかは、ユーザーが Lambda 内で条件分岐によって自由に記述できますが、
基本的に文字通りの操作を記述するとすれば、以下のような動作となるため、通常リソースと同じような動作を実現できるようになっています。
Update
イベントによりリプレース対象のリソースを新たに作成して、PhysicalResourceId
を更新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.
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 を
CloudFormation とリクエスト・レスポンスをやり取りする関数
ユーザーが定義したリソースを作成するロジックに集中する関数
の2つに分類するようなラッパーフレームワークで、つまり、1つのカスタムリソース作成のロジックのために、Lamda が2つデプロイされることになります。
CloudFormation の Stack としては、以下の3つのリソースが作成されます。
- CloudFormation とリクエスト・レスポンスをやり取りする Lambda
- ユーザーが定義したリソースを作成するロジックに集中する Lambda
- カスタムリソース
- の 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,
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');
↓リンクより引用
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).
余談2)cfn-response モジュール
カスタムリソース周りを調べていると、cfn-response モジュールを使って簡単に CloudFormation とやり取りできるよ!といった記述が見つかります。
これは確かにそうなんですが、CDK で Provider Framework を使っている場合特に使用することはないです。
というのも、cfn-response モジュールは CloudFormation に返却する必要なレスポンスオブジェクトを簡潔に記述することができるためのもので、 CloudFormation とのやり取りは Provider Framework が別の Lambda にて隠蔽してくれている部分なので不要となります。