CDK で Custom resources を作成する
投稿日: 2024/02/08
はじめに
こんにちは、CTCの山本です。
本記事では、Custom Resource Provider Frameworkを用いたCDKでカスタムリソース実装方法の解説していきたいと思います。
具体的な内容は以下になります。
- Custom Resources の概要説明
- CDKの Custom Resource Provider Framework を用いた実装方法の基本的な説明
- Provider Framework を使用したCDKのサンプル実装(やってみた)
Custom Resources について
Custom Resources とは
Custom Resources (カスタムリソース) とは、CloudFormationからの「Create/Update/Delete」イベントをLambda や SNS に関連付けて処理する仕組みを実装することでCloudFormationを拡張する仕組みとなります。
CloudFormation のリソースタイプに存在しなかったり、独自の処理でプロビジョニングを行いたい場合は、カスタムリソースの仕組みを用いてリソースの構成管理ができます。

参考: AWS/Documentation/AWS CloudFormation/User Guide/Custom resources
【参考】 CloudFormation レジストリ
また、カスタムリソースとは別に、CloudFormation レジストリという仕組みもあります。
こちらはLambdaやSNSの関連付けが不要、ドリフト検出をサポートする等、AWS が提供するリソースタイプと同じような使用かできるといった利点があるため、しっかりと構築する場合はこちらが候補になるでしょう。
CloudFormationの拡張方法
CloudFormation拡張方法 | 説明 |
---|---|
Custom Resources(カスタムリソース) | CloudFormationからの「Create/Update/Delete」イベントをLambda や SNS に関連付けて処理する仕組みを実装することで拡張する仕組み。 |
CloudFormation レジストリ | AWS アカウント で使用できるリソース、モジュール、フックの拡張を管理できる仕組み。AWS が提供するリソースタイプと同じように使用できるようになる。 |
CDK における Custom Resources
CDK では、以下のCustom Resourcesの実装手段があります。
CDK における Custom Resourcesの実装方法
カスタムリソースプロバイダーの実装方法 | 特徴 |
---|---|
SNS Topic |
|
AWS Lambda Function |
|
core.CustomResourceProvider class |
|
Custom Resource Provider Framework (推奨) |
|
CDKでは、カスタムリソースプロバイダーに必要な処理の実装支援を提供するCustom Resource Provider Framework
の使用が推奨されています。
参考: AWS CDK Reference Documentation / Custom Resources
Provider Framework を用いたCDKの実装方法
Custom Resource Provider Framework を用いたカスタムリソースの実装方法について基本的な内容を説明します。
本記事では、以下のProvider Frameworkの公式ドキュメントを参考に要点を嚙み砕いて説明するとともに、正確であることを心掛けてはおりますが、 より詳細かつ正確で最新な情報を求める方は直接公式ドキュメントを参照して頂けますと幸いです。
参考: AWS CDK Reference Documentation / Provider Framework
Provider Framework を用いたCDKコードの構成
Provider Framework を用いたCDKの実装イメージは以下になります。

Provider Frameworkを利用する際、CDKでは以下の要素を実装する必要があります。
- Lambda (onEvent handler)
- Provider Construct
- CustomResource
以下、各実装要素に絞って説明していきます。
記事後半にてサンプルコードがありますので、合わせてご覧ください。
Lambda (onEvent handler)
リソースのライフサイクルイベント(Create / Update/ Delete)に対応する処理をLambdaで実装します。
実装に当たって、最低限、抑えて置く必要がある要素に絞って説明します。
(※実装する処理の内容によっては以下の限りではありませんが、一般的な使用方法の観点で説明します。)
onEvent 処理の実装のポイント
- 「Create / Update/ Delete」の情報は Lambdaの入力イベントのオブジェクトに含まれる
RequestType
から取得できるため、このフラグに応じて、イベントを判別します。 - 要求されるリソースの状態(
CustomResource
で定義したproperties
)は入力イベントのオブジェクトに含まれるResourceProperties
から取得できるため、この内容に合わせて、管理するリソースの状態を変更する処理を実装していきます。 - 戻り値のオブジェクトの
PhysicalResourceId
パラメータには、生成したリソースの物理IDを返すようにします。※物理リソースIDについては後述で説明 - 「Update/ Delete」イベントでは、入力イベントのオブジェクトに含まれる
PhysicalResourceId
を取得して、このリソースに対して更新・削除するように処理を実装します。
Provider Construct
Provider Frameworkを使用して、カスタムリソースプロバイダーを作成します。
Provider Frameworkにより、CloudFormationとのやり取り等の大部分の処理が管理されるため、実装を大幅に簡略化できます。
Provider Frameworkにより管理される処理
- AWS CloudFormation への応答処理
- ハンドラーの戻り値の検証と実装支援
- タイムアウトの管理
- 物理リソース ID の管理
最低限、onEvent handler Lambdaを関連付けるだけで、カスタムリソースプロバイダーが作成できます。
CustomResource
カスタムリソースのコンストラクトを作成します。
最低限、以下の処理を実装します。
実装のポイント
- カスタムリソースプロバイダーを
serviceToken
に指定する - カスタムリソースプロバイダーに渡すプロパティをpropertiesに指定する。
物理リソースIDについての補足説明
Physical Resource IDs(物理リソースID)とは
物理リソースIDはCloudFormationで管理されるリソースのIDです。
公式ドキュメントを要約すると、物理リソースIDは以下のような説明がされています。
- カスタムリソースプロバイダーを
serviceToken
に指定する - Createオペレーションで、明示的に物理リソースIDを返さない場合は、フレームワークにより、リクエストIDが設定される
- Updateオペレーションで、デフォルトの処理としては、現行の物理リソースIDを返す。
- Updateオペレーションで、
onEvent
が現行の物理リソースIDと異なる物理リソースIDを返した場合、CloudFormation上、置換処理として扱う。
すなわち、物理リソースIDはUpdate、Delete イベントの処理の中で、更新・削除対象のリソースのキー情報としての役割で使用します。
そのため、実装する処理にもよりますが、管理対象リソースに対応するSDKのAPIのキーとなる情報を設定するのがよいでしょう。
大抵は管理対象のリソースのARNやリソースID等を物理リソースIDとして設定する形になるかと思います。
なお、論理IDと物理リソースIDはCloudFormationのマネージドコンソール上から確認できます。

物理リソースIDと更新処理
前述でも触れましたが、Update処理で返却する物理リソースIDによって、CloudFormationが発行するオペレーション処理が変わりますので、実装時にどちらの処理が適切か留意して実装する必要があります。
- Updateイベントの中で、プロバイダーが既存と同じ物理リソースIDを返した場合は、通常の更新処理となる。
- Updateイベントの中で、プロバイダーが既存と異なる物理リソースIDを返した場合、更新が置換処理の扱いとなり、CloudFormationは続けて既存リソースに対するDeleteオペレーションを発行する挙動となる。
図にすると以下のようなイメージです。

エラー時の処理の考慮
CloudFormationの処理の中でエラーが発生すると、ロールバック処理が実行されますので、ロールバック動作にも留意して実装する必要があります。
Provider Framework のドキュメントを読み解くと、Provider Frameworkでは、エラー時の動作は以下のようになると説明されています。
Create イベントで失敗した場合
- リソースの生成に失敗した場合、プロバイダーフレームワークはDeleteオペレーションを無視する。
- 必要に応じて、エラーハンドリングとして、クリーンアップ処理を実装する必要がある。
Update イベントで失敗した場合
- リソースの更新に失敗した場合、CloudFormationから
ResourceProperties
とOldResourceProperties
の内容が入れ替えられたUpdate オペレーションが発行される。 - 異なる物理リソースIDが返した後(置換アップデートの後)、更新に失敗した場合は、クリーンアップの為、新しく作成したリソースに対する Delete オペレーションが発行される。
Delete イベントで失敗した場合
- リソースの削除に失敗した場合、CloudFormationはリソースを放棄する。
カスタムリソースの処理として、他のコンストラクトで管理するリソースとの複数の関連や、複数のAPIを実行するような処理を検討する場合は、多くのユースケースに対応する必要性が発生してしまいます。
個人的な所感ですが、カスタムコンストラクトの処理の中で多くのことを行わず、可能な限りシンプルに設計するのが良いかと思います。
AwsCustomResource コンストラクト
AWS API を1つだけ呼び出すような処理のカスタムリソースを作成したい場合、Custom Resource Provider Frameworkでは、AwsCustomResource
コンストラクトが用意されています。
AwsCustomResource
コンストラクトを使用する場合、イベント処理の実装をフレームワーク側が支援してくれるため、Lambdaのハンドラ処理のコードの実装が不要になります。
Provider Framework を使用したCDKのサンプル実装
本記事では、CDK (Typescript)で Provider Framework を用いた簡単な実装(ECR プライベートレジストリのスキャン設定のプロビジョニング)を実演したいと思います。
ECR プライベートレジストリのスキャン設定の変更は公式のCloudFormation リソースタイプでサポートされていないため、こちらを題材にしました。
注意!
本記事のコードを実行すると、ECR プライベートレジストリのスキャン設定が変更されるとともに、Inspector がActive になり、利用料金が発生する可能性があります。
本記事のコードを実行する場合は、内容を理解した上で自己責任で実施をお願いします。
実装対象の説明
ECR プライベートレジストリのスキャン設定は以下の2つの要素で構成されます。
- スキャンタイプ
- リポジトリフィルタ

仕様の検討
対応するSDKの調査
プロビジョニング処理はLambda(Node.js)で処理するものとして、ECR プライベートレジストリのスキャン設定に対応するSDKのライブラリを調べます。
SDKのリファレンスによるとPutRegistryScanningConfigurationCommand
で処理できそうです。
AWS SDK for JavaScript v3 / PutRegistryScanningConfigurationCommand
各ライフサイクルイベントの処理を考える
次に、「Create/Update/Delete」イベントに対してどのような処理を実装するか検討します。
本記事では、次のように実装するものとします。
ライフサイクルイベント | 処理 |
---|---|
Create | 「指定されたスキャンタイプ」、「リポジトリフィルタ」の状態にECR プライベートレジストリのスキャン設定を変更する。 |
Update | 「指定されたスキャンタイプ」、「リポジトリフィルタ」の状態にECR プライベートレジストリのスキャン設定を変更する。 |
Delete | 「スキャンタイプ = "Basic"」、「リポジトリフィルタ = 未設定」の状態(出荷時の状態)に変更する。 |
CDK の実装
CDKプロジェクトの作成
以下を実行して、CDKプロジェクトを作成します。
mkdir cdk-cr-ecr-registry
cd cdk-cr-ecr-registry
cdk init app --language typescript
また、以下も必要となるため、追加でインストールします。
npm install -D @types/aws-lambda # lambdaの型定義
npm i -S @aws-sdk/client-ecr # AWS SDKのclient-ecr モジュール
ディレクトリ構成
コードのディレクトリ構成とファイル名は以下のようにします。
.
├── bin
│ └── cdk-cr-ecr-registry.ts // プログラムのエントリーポイント
└── lib
├── cdk-cr-ecr-registry-stack.ts // スタックを定義
└── construct
├── ecr-registry-scanning-configuration-construct.ts // ECR プライベートレジストリのスキャン設定を扱うカスタムコンストラクトを定義
└── src
└── index.ts // カスタムリソースのイベントハンドラ (lambda) を実装
エントリーポイント (bin/cdk-cr-ecr-registry.ts)
- CDKコードのエントリーポイントとなります。
- 後述するスタック
CdkCrEcrRegistryStack
を呼び出しています。
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { CdkCrEcrRegistryStack } from "../lib/cdk-cr-ecr-registry-stack";
const app = new cdk.App();
new CdkCrEcrRegistryStack(app, "CdkCrEcrRegistryStack", {});
スタック (lib/cdk-cr-ecr-registry-stack.ts)
- スタック
CdkCrEcrRegistryStack
の定義となります。 - カスタムコンストラクト
EcrRegistryScanningConfigurationConstruct
にECR プライベートレジストリのスキャン設定を渡してリソースを変更します。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { EcrRegistryScanningConfigurationConstruct } from "./construct/ecr-registry-scanning-configuration-construct2";
export class CdkCrEcrRegistryStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new EcrRegistryScanningConfigurationConstruct(
this,
"EcrRegistryScanningConfiguration",
{
scanType: "ENHANCED",
rules: [
{
scanFrequency: "CONTINUOUS_SCAN",
repositoryFilters: [{ filter: "prd-*", filterType: "WILDCARD" }],
},
{
scanFrequency: "SCAN_ON_PUSH",
repositoryFilters: [{ filter: "dev-*", filterType: "WILDCARD" }],
},
],
}
);
}
}
カスタムコンストラクト ( lib\construct\ecr-registry-scanning-configuration-construct.ts)
- ECR プライベートレジストリのスキャン設定を扱うカスタムコンストラクトの定義となります。
- 内部で
Provider Construct
、CustomResource
、Lambda (onEvent handler)
を作成し、カスタムリソースを実装します。
import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as cr from "aws-cdk-lib/custom-resources";
import * as iam from "aws-cdk-lib/aws-iam";
import * as nodejsLambda from "aws-cdk-lib/aws-lambda-nodejs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as path from "path";
import { ResourceProperties } from "./src/index";
export class EcrRegistryScanningConfigurationConstruct extends Construct {
constructor(scope: Construct, id: string, props: ResourceProperties) {
super(scope, id);
// IAM Policy を作成
const policyStatement = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"ecr:PutRegistryScanningConfiguration",
"inspector2:ListAccountPermissions",
"inspector2:Disable",
"inspector2:Enable",
"iam:CreateServiceLinkedRole",
],
resources: ["*"],
});
// カスタムリソースのイベントハンドラ (Lambda Function)を作成
const lambdaFunction = new nodejsLambda.NodejsFunction(
this,
"OnEventHandler",
{
runtime: lambda.Runtime.NODEJS_18_X,
entry: path.join(__dirname, "./src/index.ts"),
handler: "handler",
initialPolicy: [policyStatement],
}
);
// カスタムリソースプロバイダーを作成
const provider = new cr.Provider(this, "Provider", {
onEventHandler: lambdaFunction,
});
// カスタムリソース作成
new cdk.CustomResource(this, "CustomResource", {
serviceToken: provider.serviceToken,
properties: props,
});
}
}
onEvent handler (lib\construct\src\index.ts)
Lambda (onEvent handler)
の実装となります。- SDKを呼びだして、ECR プライベートレジストリのスキャン設定を変更します。
- 更新対象にIDは必要ないので、物理IDは特に設定していません。
import {
ECRClient,
PutRegistryScanningConfigurationCommand,
PutRegistryScanningConfigurationCommandInput,
PutRegistryScanningConfigurationCommandOutput,
ScanType,
RegistryScanningRule,
} from "@aws-sdk/client-ecr";
import { CdkCustomResourceHandler } from "aws-lambda";
// ResourcePropertiesの期待する入力の型を定義
export interface ResourceProperties {
scanType: ScanType;
rules: RegistryScanningRule[];
}
const client = new ECRClient({});
/** Lambdaのエントリーポイント */
export const handler: CdkCustomResourceHandler = async (event) => {
// ResourcePropertiesに更新対象のプロパティが含まれる。
const properties: ResourceProperties = {
scanType: event.ResourceProperties.scanType,
rules: event.ResourceProperties.rules,
};
// RequestTypeにイベントフラグが含まれる。
// RequestTypeに応じて各処理を実装。
switch (event.RequestType) {
case "Create":
await onCreate(properties);
return {};
case "Update":
await onUpdate(properties);
return {};
case "Delete":
await onDelete();
return {};
default:
throw new Error("Failed");
}
};
/**Create の処理 */
async function onCreate(
props: ResourceProperties
): Promise {
const command = new PutRegistryScanningConfigurationCommand(props);
const result = await client.send(command);
return result;
}
/**Update の処理 */
async function onUpdate(
props: ResourceProperties
): Promise {
const command = new PutRegistryScanningConfigurationCommand(props);
const result = await client.send(command);
return result;
}
/**Delete の処理 */
async function onDelete(): Promise {
// Delete処理では、デフォルト設定に戻す。
const input: PutRegistryScanningConfigurationCommandInput = {
rules: [],
scanType: "BASIC",
};
const command = new PutRegistryScanningConfigurationCommand(input);
const result = await client.send(command);
return result;
}
動作確認
「Create/Update/Delete」イベントの各処理が正常に処理されるか確認します。

「Create/Update/Delete」イベントの正常系の動作に関しては、正常に動作しているようです。
なお、本記事では記載していませんが、異常系の処理(ロールバック時の処理等)についても動作確認をする必要があります。
AwsCustomResource で実装した場合
前述のカスタムリソースは単純にPutRegistryScanningConfigurationCommand
を使用するだけなので、実は、
AwsCustomResource
を使用してさらに簡単に実装することが可能です。
AwsCustomResource のコード
- 前述の
ecr-registry-scanning-configuration-construct.ts
を以下のコードで置換すると同様の処理がAwsCustomResourceで実現できます。 - また、Lambda (onEvent handler)処理の
lib\construct\src\index.ts
も不要です。
import { Construct } from "constructs";
import * as cr from "aws-cdk-lib/custom-resources";
import * as iam from "aws-cdk-lib/aws-iam";
import { PutRegistryScanningConfigurationCommandInput } from "@aws-sdk/client-ecr";
export class EcrRegistryScanningConfigurationConstruct extends Construct {
constructor(
scope: Construct,
id: string,
props: PutRegistryScanningConfigurationCommandInput
) {
super(scope, id);
// IAMポリシー
const policyStatement = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"ecr:PutRegistryScanningConfiguration",
"inspector2:ListAccountPermissions",
"inspector2:Disable",
"inspector2:Enable",
"iam:CreateServiceLinkedRole",
],
resources: ["*"],
});
// カスタムリソースを作成
new cr.AwsCustomResource(this, "CustomResource", {
onCreate: {
action: "PutRegistryScanningConfigurationCommand",
service: "ECR",
parameters: props,
physicalResourceId: cr.PhysicalResourceId.of("ecr"),//本ケースでは、物理IDは必要ないので適当に指定。
},
onUpdate: {
action: "PutRegistryScanningConfigurationCommand",
service: "ECR",
parameters: props,
},
onDelete: {
action: "PutRegistryScanningConfigurationCommand",
service: "ECR",
parameters: {
rules: [],
scanType: "BASIC",
},
},
policy: cr.AwsCustomResourcePolicy.fromStatements([policyStatement]),
});
}
}
まとめ
CDKでは、Provider Frameworkがカスタムリソースのコントロールの大部分を管理してくれるため、カスタムリソースを比較的に簡単に実装できます。
また、プロビジョニングの各処理が1つのSDKのAPIのみで完結するのであれば、AwsCustomResource
を使用することで、Lambdaの実装自体も不要となります。
しかしながら、今回は簡単な例でしたので動作に特に問題ありませんでしたが、参照関係を含んだり、複雑な処理を含む処理のカスタムリソースを作成する場合、様々なリソースの状態を考慮して実装する必要があります。
中途半端な実装のカスタムリソースをプロダクション環境に導入した場合、最悪、ロールバックに失敗し、スタックが壊れる可能性もありますので、入念にテストしてから導入しましょう。
CTCは、AWSのビジネス利活用に向けて、お客様のステージに合わせた幅広い構築・運用支援サービスを提供しています。
経験豊富なエンジニアが、ワンストップかつ柔軟にご支援します。
ぜひ、お気軽にお問い合わせください。