AWS CloudFormation がサポートしていないリソースやアクションをカスタマイズするために便利なLambda-backed カスタムリソースですが、スタックが直接参照しないコードに変更があり更新をかける場合は一工夫必要となります。本記事ではその方法について CDK の具体的なコード例を交えながら紹介します。
ここでは具体的に下記のようなケースを想定します。
- カスタムリソースを含む独自のコンストラクト: MyConstructを作成している
- MyConstructでは指定されたフォルダの内容を S3 へアップロードし、リソースのライフサイクルイベント (- Create・- Update・- Delete) をハンドリングする Lambda から上記 S3 オブジェクトを取得し何らかの処理を実行する
// 利用側
const myConstruct = new MyConstruct(this, "MyConstruct", {
  path: "/path/to/dir",
});
// MyConstruct本体の定義
interface MyConstructProps {
  path: string;
}
export class MyConstruct extends Construct {
  constructor(scope: Construct, id: string, props: MyConstructProps) {
    super(scope, id);
    // path内のファイルをS3へデプロイ
    declare const bucket: s3.Bucket;
    new s3deploy.BucketDeployment(this, "S3Deploy", {
      sources: [s3deploy.Source.asset(props.path)],
      destinationBucket: bucket,
    });
    // Lambda-backedプロバイダー
    declare const handler: lambda.Function;
    const provider = new cr.Provider(this, "Provider", {
      onEventHandler: handler,
    });
    // カスタムリソースの定義
    const customResource = new CustomResource(this, `CustomResource`, {
      serviceToken: provider.serviceToken,
    });
  }
}
// Lambda
// ...略...
async function onUpdate(event) {
  // S3バケットの中身を取得し何らかの処理を行う
}
exports.handler = async function (event) {
  switch (event.RequestType) {
    case "Create":
      response = await onCreate(event);
      break;
    case "Update":
      response = await onUpdate(event);
      break;
    case "Delete":
      response = await onDelete(event);
      break;
  }
  // ...略...
};
上記のコードをデプロイした場合、スタックの作成時と削除時にはonCreateおよびonDeleteがコールされますが、指定されたフォルダ(ここでは/path/to/dir)内のコードに変更を加えてもonUpdateは実行されません。ドキュメントではカスタムリソースのプロパティに変更があった場合更新がされると記載があるので、ここでは該当フォルダ内のハッシュ値を計算しプロパティに付与する方法で更新をトリガーします。具体的にはたとえば下記のように実装します。
export class MyConstruct extends Construct {
  constructor(scope: Construct, id: string, props: MyConstructProps) {
    // ...略...
    // ディレクトリのハッシュを計算
    const hash = calculateDirHash(props.path);
    const customResource = new CustomResource(this, `CustomResource`, {
      serviceToken: provider.serviceToken
      properties: {
        // カスタムリソースはプロパティの値が変更されると更新がトリガーされる
        hash: hash,
      },
    });
  }
}
function calculateDirHashRecursive(dirPath: string, hash: crypto.Hash): void {
  const entries = fs.readdirSync(dirPath, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    if (entry.isFile()) {
      const stats = fs.statSync(fullPath);
      // ファイルのパス・更新時刻・サイズに基づいてハッシュ値を更新
      hash.update(fullPath + stats.size + stats.mtimeMs);
    } else if (entry.isDirectory()) {
      calculateDirHashRecursive(fullPath, hash);
    }
  }
}
function calculateDirHash(dirPath: string): string {
  const hash = crypto.createHash("sha256");
  calculateDirHashRecursive(dirPath, hash);
  return hash.digest("hex");
}
おわりに
本記事では、AWS CDK を使用して、リソース外のソースコードの変更に応じて Lambda-backed カスタムリソースの更新をトリガーする方法を紹介しました。ディレクトリ内のファイルのハッシュを計算し、カスタムリソースのプロパティとして設定することで、ファイルの変更があった際に自動的にカスタムリソースの更新がトリガーされるようになります。
この方法を利用することで、AWS CloudFormation スタックが直接参照していないコードにも対応した柔軟なインフラ管理が可能になります。今後も AWS CDK や Lambda-backed カスタムリソースを使用して、効率的なインフラ管理を実現していきましょう。
