CloudFormationのカスタムリソースにLambda関数を関連付けると、スタックを作る時とか更新する時に、Lambda関数を動かせる。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources.html
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html

テンプレートの書き方をいつもド忘れするから、ひな形をメモっておく。

まえがき: カスタムリソースの使い途とか書き方とか

カスタムリソースを使わず済むなら使わない方がテンプレートはシンプルになると思う。
Lambda関数呼べるとなると、もはやだいたいなんでもできるんだけども、例えばLambda関数でCreate*系のAPIを呼んでリソースを作ったりすると、テンプレートのどの部分でどうリソースが作られているのか把握するのが難しくなる気がしてる。
カスタムリソースで呼ばれるLambda関数は、テンプレート内で記述するリソースのプロパティの値として使う情報を取得するとか、CloudFormationがまだ対応していないプロパティを設定するとか、そういう用途で活躍する。

カスタムリソースに関連付けられるLambda関数のコードは、S3バケットに置いておいてテンプレート内で参照することもできるし、テンプレート内にベタ書きすることもできる。
個人的には、ベタ書きするほうが、一目でテンプレート全体を確認できるから便利と思う。
カスタムリソースのLambda関数はたいてい短いからベタ書きしてもテンプレートがゴチャつかないというのも、ベタ書きで良いと思う理由。
ベタ書きするとなると、YAMLで書くのが良い。
JSONだとコードをダブルクオーテーションで囲みまくる必要があってつらいけど、YAMLならコードをそのまま書ける。

ところで、カスタムリソースが呼び出すLambda関数に誤りがあってエラーになると、スタックの作成がCREATE_IN_PROGRESSで止まる。
CREATE_IN_PROGRESSが長時間続くなら、Lambda関数のログとかメトリクスを見てエラーが出ていないか確認するようにしてる。

ひな形

ひな形は下記。
<>で囲んでる部分は、置き換えるべき部分。

カスタムリソースの論理IDとCustom::Stringで指定するリソースタイプの名前(<カスタムリソースの論理ID>の部分)は同じにしなくてもいいハズだけど、揃えた方がわかりやすい気がするので揃えてる。

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  <カスタムリソースLambda関数の出力を利用するリソースの論理ID>:
    Type: <XX::XX::XX>
    Properties: 
      <XX>: !GetAtt <カスタムリソースの論理ID>.<Lambda関数が返すresponseDataオブジェクト配下のプロパティ名>

  <カスタムリソースの論理ID>:
    Type: Custom::<カスタムリソースの論理ID>
    Properties:
      ServiceToken: !GetAtt <カスタムリソースLambda関数の論理ID>.Arn
      <カスタムリソースLambda関数のevent.ResourcePropertiesオブジェクト配下のプロパティ名>: <プロパティの値>

  <カスタムリソースLambda関数の論理ID>:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt <カスタムリソースLambda関数の実行ロールの論理ID>.Arn
      Code:
        ZipFile: !Sub |
          var cfnresponse = require('cfn-response');
          var AWS = require('aws-sdk');
          exports.handler = function(event, context) {
            <処理を書く>
            var responseData = {};
            responseData.<Lambda関数が返すresponseDataオブジェクト配下のプロパティ名> = <何らかの値>;
            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, '', true);
          };
      Runtime: nodejs6.10

  <カスタムリソースLambda関数の実行ロールの論理ID>:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies: # 実行ロールの権限は、Lambda関数で実行するAPIに応じて変更する
      - PolicyName: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action: 
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: "arn:aws:logs:*:*:*"

実際の例

実際に動くテンプレートは下記。
カスタムリソースで呼ばれるLambda関数が出力した値を、パラメータストアに格納している。

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  Parameter:
    Type: AWS::SSM::Parameter
    Properties: 
      Name: /tmp/CustomResourceTest
      Type: String
      Value: !GetAtt CustomResource.data

  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CustomResourceFunction.Arn
      CustomResourceArgument: hoge

  CustomResourceFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt CustomResourceFunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          var cfnresponse = require('cfn-response');
          exports.handler = function(event, context) {
            console.log(JSON.stringify(event));
            console.log(JSON.stringify(context));
            var responseData = {};
            responseData.data = 'Hello from lambda invoked by custom resource! Argument is: ' + event.ResourceProperties.CustomResourceArgument;
            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, '', true);
          };
      Runtime: nodejs6.10

  CustomResourceFunctionExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action: 
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: "arn:aws:logs:*:*:*"

CLIでパラメータを取得すると、確かに格納されている。

$ aws ssm get-parameter --name /tmp/CustomResourceTest
{
    "Parameter": {
        "Version": 1, 
        "Type": "String", 
        "Name": "/tmp/CustomResourceTest", 
        "Value": "Hello from lambda invoked by custom resource! Argument is: hoge"
    }
}

ドキュメント

カスタムリソースで呼ばれるLambdaの書き方については、下記のドキュメントに詳しく書かれてる。
cfn-responseモジュールの詳細とかも書かれてる。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html