はじめに
最近、職場でAWS CDKによるIaC(Infrastructure as Code)を導入したいと考え過去に作成されたアプリをCDKで構築する作業をおこなっていた。構築時は生成AIによる手助けを得ながら構築していったが、その中でカスタムリソースというものを使用している場面があり、これによって恩恵も得つつ混乱することもあったため、具体的にどういうものなのかを調べることにした。
カスタムリソースとは
カスタムリソースを使うことで、CloudFormation や CDK が標準では直接扱えない処理を、スタックの作成時・更新時・削除時に実行させることができる。カスタムリソースではスタックの作成時、更新時、削除時の処理を記載することで、各フェーズにおける処理を実施させることができる。一般的なカスタムリソースでは、作成時・更新時・削除時のイベントを受け取るプロバイダーを用意し、CloudFormationからそのプロバイダーに処理を委譲する。CDKでは、自前の Lambdaで処理を書くCustomResourceと、AWS SDK呼び出しを簡潔に書けるAwsCustomResourceがよく使われる。
カスタムリソースが利用される場面としては以下が挙げられる。
- AWS CDKで定義されていない外部サービスの実行
例)作成したアプリの情報を社内システムに登録するAPIの実行 - AWS CDKで実行できないリソースのセットアップ
例)RDSのテーブルの作成や初期データの投入、ユーザーの作成・権限付与
CustomResource vs AwsCustomResource
カスタムリソースの作成方法として、自身で処理を記述しそれをカスタムリソースとして実行させる方法(CustomResource)や単純なSDKによるAPIを実行させる方法(AwsCustomResouce)が存在している。
CustomResourceでは実施したい処理をLambda関数として記載することができる。そのため、CDKのコードとは別にLambda用のコードが必要になる。自分で処理が記載できるため細かい制御が可能だが、CDKでLambdaのリソースを作成する必要がありそのLambdaのランタイムやコードを管理する必要が出てくる。CDKのコードの記載例としては以下の通り。
import * as path from 'path';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cr from 'aws-cdk-lib/custom-resources';
export class AppStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
const lambda = new lambda.Function(this, 'Lambda', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda')),
});
const provider = new cr.Provider(this, 'Provider', {
onEventHandler: lambda,
});
new cdk.CustomResource(this, 'CustomResource', {
serviceToken: provider.serviceToken,
properties: {
message: 'hello',
},
});
}
}
上記のように記載することでLambdaでの処理をCustomResourceとして実行させることができる。CustomResourceのpropertiesでLambdaにパラメーターを渡すことができる。カスタムリソースが実行されるフェーズ(onCreate、onUpdate、onDelete)はevent.RequestTypeとして渡されるためLambda側で各フェーズでどのように処理するか記載する必要がある。
AwsCustomResourceではLambdaのコードの記載やLambda自体のリソースを準備する必要なくAWS SDKのAPIを実行することができる。しかし、実態としてはLambdaが作成されており実際の環境にも展開されている。CustomResourceとは違いランタイムの管理はCDKの実装に依存するため、開発者が作成されたLambdaのランタイムを管理する必要はない。記載例としては以下の通り。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as cr from 'aws-cdk-lib/custom-resources';
export class SampleStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// 既にAuroraとしてclusterを構築しているとする
new cr.AwsCustomResource(this, 'CreateTable', {
onCreate: {
service: 'RDSDataService',
action: 'executeStatement',
parameters: {
resourceArn: cluster.clusterArn,
secretArn: cluster.secret!.secretArn,
database: 'appdb',
sql: 'CREATE TABLE users (id bigint primary key, name varchar(100));',
},
physicalResourceId: cr.PhysicalResourceId.of('CreateTable'),
},
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});
}
}
CustomResourceではLambda側で実行フェーズごとの処理を記載したが、AwsCustomResourceではパラメータでフェーズごとの実行内容としてAPIを記載する。policyに記載した内容が自動で作成されるLambdaに付与されるIAMポリシーになる。今回はfromSdkCallsによって必要なIAMポリシーを作成しており、リソースもANY_RESOURCEで広めのポリシーを作成している。
AwsCustomResourceにおける注意点
この記事を書くまで先に述べたLambdaが自動で構築されるということを知らず、ポリシーにはただSDKで操作したい対象と実行するアクションを許可すればよいと思っていた。そのため、AwsCustomResourceのポリシーにおいてSDKを実行する対象のリソースに絞って細かく記載していたところ、これによって依存関係が作成されてしまい循環依存の原因になっていた。例えば以下のようなAwsCustomResourceを作成したとする。
import * as cdk from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3'
import * as cr from 'aws-cdk-lib/custom-resources'
// import * as sqs from 'aws-cdk-lib/aws-sqs';
export class AwsCustomResourceTestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, 'TestBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
new cr.AwsCustomResource(this, 'PutObjectCustomResource',{
onCreate: {
service: 'S3',
action: 'putObject',
parameters: {
Bucket: bucket.bucketName,
Key: 'onCreate.txt',
Body: 'created by AwsCustomResource on Create'
},
physicalResourceId: cr.PhysicalResourceId.of('sample-txt'),
},
onUpdate: {
service: 'S3',
action: 'putObject',
parameters: {
Bucket: bucket.bucketName,
Key: 'onUpdate.txt',
Body: 'created by AwsCustomResource on Update'
},
},
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: [ `${bucket.bucketArn}/*`]
}),
});
}
}
この場合、ポリシーにてbucket.bucketArnと指定しており、この部分によって裏で作られるLambda用のIAMポリシーにこのARNが記載されることになる。このようにpolicyに対象リソースのARNを記載すると、そのARNを参照するIAMポリシーが生成されるため、他のリソース参照と組み合わさった場合に依存関係が複雑化し、構成によっては循環依存の原因になる。そのため、単なるSDK呼び出しのラッパーとして軽く捉えるのではなく、裏でLambdaとIAMリソースが作成され、その参照関係がCloudFormationの依存関係に影響することを意識して使う必要がある。
上記スタックを展開してみると確かにLambdaやIAMロールが作成されていた。
そして、IAMロールを見てみるとpolicyに指定した内容で自動で作成されていた。
Lambda を見てみるとコードは1行で記載されており、私が JavaScript をあまり知らないこともあって、内容をそのまま理解するのは難しかった。調べてみると、このコードは minified と呼ばれる圧縮処理が施されたコードで、改行や空白、コメントなどが削除され、変数名なども短い名前に置き換えられているようだった。
まとめ
業務で使って気になったCDKのカスタムリソースについて調べてみた。AwsCustomResourceを実際に使っていてその時はただSDKを実行するメソッドだと思っていたが、裏でLambdaやそれ用のIAMロール・ポリシーが作成されているとは知らなかった。AwsCustomResourceを使うときは裏でLambdaが作成されてそれによって依存関係ができる可能性があると肝に銘じておく。

