1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS CDK - Custom Resource (AwsCustomResource と Provider)

Posted at

内容

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.AwsCustomResourceonCreateonUpdate で指定する 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を直に指定することも可能だが非推奨となっている。

handlers/invoke_lambda.py
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

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?