内容
CDK で Custom Resource を利用する場合、
Provider + CustomResource を使用する方法と、
AwsCustomResourceを使用する方法がある。
それぞれの使用方法を記載する。
今回は、適当な Lambda Function (sample1Function, sample2Function) をそれぞれ実行する Custom Resource を作成する。
AwsCustomResource
概要
単一のAPIを呼び出すだけの目的で Custom Resource を利用する場合に使用できる。
今回は、Custom Resource で Invoke APIを実行する。
複数の Custom Resource を作成する場合に多少クセがあり、注意点を後述する。
実装
import * as path from 'node:path';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export class AwsCustomResourceSampleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    const stackName = this.stackName;
    // 適当なLambda Function 1
    const sample1FunctionName = `${stackName}-sample1Function`;
    const sample1Function = new lambda.Function(this, sample1FunctionName, {
      code: lambda.Code.fromAsset(path.join(__dirname, 'handlers')),
      handler: 'sample1.handler',
      runtime: lambda.Runtime.PYTHON_3_11,
      functionName: sample1FunctionName,
    });
    // 適当なLambda Function 2
    const sample2FunctionName = `${stackName}-sample2Function`;
    const sample2Function = new lambda.Function(this, sample2FunctionName, {
      code: lambda.Code.fromAsset(path.join(__dirname, 'handlers')),
      handler: 'sample2.handler',
      runtime: lambda.Runtime.PYTHON_3_11,
      functionName: sample2FunctionName,
    });
    // Lambda Functionを実行するCustom ResourceにアタッチするRole
    // ※ 全Custom Resourceを実行できるように権限を付与する
    const customResourceFunctionName = `${stackName}-CustomResourceFunction`;
    const customResourceFunctionRole = new iam.Role(this, `${customResourceFunctionName}Role`, {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      inlinePolicies: {
        lambdaPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ['lambda:InvokeFunction'],
              resources: [sample1Function.functionArn, sample2Function.functionArn],
            }),
          ],
        }),
      },
      roleName: `${customResourceFunctionName}Role`,
    });
    // Custom Resourceで実行する内容 (sample1Functionを実行する)
    const sample1FunctionInvokeContent = {
      action: 'Invoke',
      service: 'Lambda',
      parameters: {
        /* eslint-disable @typescript-eslint/naming-convention */
        FunctionName: sample1Function.functionName,
        InvocationType: 'Event',
        Payload: JSON.stringify({ datetime: Date.now() }),  // 毎回Custom Resourceを実行する工夫
        /* eslint-enable @typescript-eslint/naming-convention */
      },
      physicalResourceId: cr.PhysicalResourceId.of(`${stackName}-sample1FunctionInvokeCustomResource`),
    };
    // sample1Functionを実行するCustom Resource
    new cr.AwsCustomResource(this, `${stackName}-sample1FunctionInvokeCustomResource`, {
      installLatestAwsSdk: true,
      functionName: customResourceFunctionName,
      onCreate: sample1FunctionInvokeContent,
      onUpdate: sample1FunctionInvokeContent,
      role: customResourceFunctionRole,
    });
    // Custom Resourceで実行する内容 (sample2Functionを実行する)
    const sample2FunctionInvokeContent = {
      action: 'Invoke',
      service: 'Lambda',
      parameters: {
        /* eslint-disable @typescript-eslint/naming-convention */
        FunctionName: sample2Function.functionName,
        InvocationType: 'Event',
        /* eslint-enable @typescript-eslint/naming-convention */
      },
      physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()),  // 毎回Custom Resourceを実行する工夫
    };
    // sample2Functionを実行するCustom Resource
    new cr.AwsCustomResource(this, `${stackName}-sample2FunctionInvokeCustomResource`, {
      installLatestAwsSdk: true,
      functionName: customResourceFunctionName,
      onCreate: sample2FunctionInvokeContent,
      onUpdate: sample2FunctionInvokeContent,
      role: customResourceFunctionRole,
    });
  }
}
cr.AwsCustomResource の onCreate、onUpdate で指定する parameters は、Invokeを参照。
Custom Resource のUpdateイベントは、Custom Resourceに変更がない限り発生しない。
(Custom Resource から実行するLambda Functionに変更があっても発生しない)
そのため、Custom Resource を毎回実行したい場合は、日時Date.now()等を使って工夫する。
複数の Custom Resource を作成する場合の注意点
These calls are created using a singleton Lambda function.
--- (翻訳) ---
これらの呼び出しは、シングルトン Lambda 関数を使用して作成されます。
AwsCustomResource に上記が記載されている。
これはCDK実装から生成されるCFNテンプレートを見るとわかりやすい。
  # Lambda Functionを実行するCustom ResourceにアタッチするRole
  sampleAwsCustomResourceSampleStackCustomResourceFunctionRole2E76:
    Type: AWS::IAM::Role
    Properties:
      Policies:
        - PolicyDocument:
            Statement:
              - Action: lambda:InvokeFunction
                Effect: Allow
                Resource:
                  - Fn::GetAtt:
                      - sampleAwsCustomResourceSampleStacksample1Function7826
                      - Arn
                  - Fn::GetAtt:
                      - sampleAwsCustomResourceSampleStacksample2Function301A
                      - Arn
            Version: "2012-10-17"
          PolicyName: lambdaPolicy
      RoleName: sample-AwsCustomResourceSampleStack-CustomResourceFunctionRole
  # sample1Functionを実行するCustom Resource
  sampleAwsCustomResourceSampleStacksample1FunctionInvokeCustomResource361C:
    Type: Custom::AWS
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - AWS679f53fac002430cb0da5b7982bd22872D16
          - Arn
  # Custom Resourceとして実行されるLambda Function
  AWS679f53fac002430cb0da5b7982bd22872D16:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: sample-AwsCustomResourceSampleStack-CustomResourceFunction
      Handler: index.handler
      Role:
        Fn::GetAtt:
          - sampleAwsCustomResourceSampleStackCustomResourceFunctionRole2E76
          - Arn
      Runtime: nodejs18.x
  # sample2Functionを実行するCustom Resource
  sampleAwsCustomResourceSampleStacksample2FunctionInvokeCustomResource3FA7:
    Type: Custom::AWS
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - AWS679f53fac002430cb0da5b7982bd22872D16
          - Arn
CDK上でAwsCustomResourceで2つのCustom Resourceを作成しているが、
Custom Resourceとして実行されるLambda Functionは1つで、
共通パーツとして2つのCustom Resourceから使用される。
そして、そのLambda FunctionにアタッチされるRoleも1つなので、
Roleには全Custom Resourceを実行できるように権限を付与する必要がある。
Provider
概要
CFN や SAM で実装する Custom Resource に似ている。
複数のAPIを呼び出したり、ロジックを組み込んだ Custom Resource を利用する場合に使用。
実装
import * as path from 'node:path';
import { CustomResource, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export class CustomResourceSampleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    const stackName = this.stackName;
    // 適当なLambda Function 1
    const sample1FunctionName = `${stackName}-sample1Function`;
    const sample1Function = new lambda.Function(this, sample1FunctionName, {
      code: lambda.Code.fromAsset(path.join(__dirname, 'handlers')),
      handler: 'sample1.handler',
      runtime: lambda.Runtime.PYTHON_3_11,
      functionName: sample1FunctionName,
    });
    // 適当なLambda Function 2
    const sample2FunctionName = `${stackName}-sample2Function`;
    const sample2Function = new lambda.Function(this, sample2FunctionName, {
      code: lambda.Code.fromAsset(path.join(__dirname, 'handlers')),
      handler: 'sample2.handler',
      runtime: lambda.Runtime.PYTHON_3_11,
      functionName: sample2FunctionName,
    });
    // Lambda Functionを実行するCustom ResourceにアタッチするRole
    const customResourceFunctionRole = new iam.Role(this, `${stackName}-CustomResourceFunctionRole`, {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      inlinePolicies: {
        lambdaPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ['lambda:InvokeFunction'],
              resources: [sample1Function.functionArn, sample2Function.functionArn],
            }),
          ],
        }),
      },
      roleName: `${stackName}-CustomResourceFunctionRole`,
    });
    // Custom Resourceとして実行するLambda Function
    const invokeLambdaFunction = new lambda.Function(this, `${stackName}-invokeLambdaFunction`, {
      code: lambda.Code.fromAsset(path.join(__dirname, 'handlers')),
      handler: 'invoke_lambda.handler',
      runtime: lambda.Runtime.PYTHON_3_11,
      functionName: `${stackName}-invokeLambdaFunction`,
      role: customResourceFunctionRole,
    });
    // Custom Resourceとして実行するLambda FunctionをProviderに指定する
    const invokeLambdaProvider = new cr.Provider(this, `${stackName}-invokeLambdaProvider`, {
      onEventHandler: invokeLambdaFunction,
    });
    // sample1Functionを実行するCustom Resource
    new CustomResource(this, `${stackName}-invokeSample1CustomResource`, {
      serviceToken: invokeLambdaProvider.serviceToken,
      properties: {
        FUNCTION_NAME: sample1Function.functionName
      }
    });
    // sample2Functionを実行するCustom Resource
    new CustomResource(this, `${stackName}-invokeSample2CustomResource`, {
      serviceToken: invokeLambdaProvider.serviceToken,
      properties: {
        FUNCTION_NAME: sample2Function.functionName
      }
    });
  }
}
CustomResource の serviceToken には、
provider.serviceToken 以外にも、
Lambda FunctionのARNや、SNSのトピックARNを直に指定することも可能だが非推奨となっている。
import boto3  # type: ignore
def handler(event, context):
    request_type = event["RequestType"]
    properties = event["ResourceProperties"]
    lambda_client = boto3.client("lambda")
    if request_type == "Create" or request_type == "Update":
        return sync_invoke_lambda(lambda_client, properties["FUNCTION_NAME"])
    if request_type == "Delete":
        return
    raise Exception("Invalid request type: %s" % request_type)
def sync_invoke_lambda(client, function_name):
    client.invoke(
        FunctionName=function_name,
        InvocationType="RequestResponse",
    )
CFN、SAM での Custom Resource 実装とは異なり、cfn-response を呼び出す必要はない。
(Custom Resource Lambda Function の成功/失敗に応じてProviderがCFNに応答してくれる)
備考
Provider を使用した場合、CFNテンプレートは以下のようになっている。
Provider によって Custom Resource として実行する Lambda Function を呼び出す Lambda Function が作成される。
  # Lambda Functionを実行するCustom ResourceにアタッチするRole
  sampleCustomResourceSampleStackCustomResourceFunctionRole5814:
    Type: AWS::IAM::Role
    Properties:
      Policies:
        - PolicyDocument:
            Statement:
              - Action: lambda:InvokeFunction
                Effect: Allow
                Resource:
                  - Fn::GetAtt:
                      - sampleCustomResourceSampleStacksample1FunctionB498
                      - Arn
                  - Fn::GetAtt:
                      - sampleCustomResourceSampleStacksample2Function98EB
                      - Arn
            Version: "2012-10-17"
          PolicyName: lambdaPolicy
      RoleName: sample-CustomResourceSampleStack-CustomResourceFunctionRole
  # Custom Resourceとして実行するLambda Function
  sampleCustomResourceSampleStackinvokeLambdaFunction10F04414:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: sample-CustomResourceSampleStack-invokeLambdaFunction
      Handler: invoke_lambda.handler
      Role:
        Fn::GetAtt:
          - sampleCustomResourceSampleStackCustomResourceFunctionRole5814
          - Arn
      Runtime: python3.11
  # providerのLambda Function
  sampleCustomResourceSampleStackinvokeLambdaProviderframeworkonEvent93E9:
    Type: AWS::Lambda::Function
    Properties:
      Description: AWS CDK resource provider framework - onEvent (sample-CustomResourceSampleStack/sample-CustomResourceSampleStack-invokeLambdaProvider)
      Environment:
        Variables:
          USER_ON_EVENT_FUNCTION_ARN:
            Fn::GetAtt:
              - sampleCustomResourceSampleStackinvokeLambdaFunction10F04414
              - Arn
      Handler: framework.onEvent
      Runtime: nodejs18.x
  # sample1Functionを実行するCustom Resource
  sampleCustomResourceSampleStackinvokeSample1CustomResource:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - sampleCustomResourceSampleStackinvokeLambdaProviderframeworkonEvent93E9
          - Arn
      FUNCTION_NAME:
        Ref: sampleCustomResourceSampleStacksample1FunctionB498ADB6
  # sample2Functionを実行するCustom Resource
  sampleCustomResourceSampleStackinvokeSample2CustomResource:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - sampleCustomResourceSampleStackinvokeLambdaProviderframeworkonEvent93E9
          - Arn
      FUNCTION_NAME:
        Ref: sampleCustomResourceSampleStacksample2Function98EB7E85
