AWS Lambda-backed カスタムリソースを利用するとAWS CloudFormationが対応していないリソースを管理することができて便利なのですが、AWS Lambdaで利用できるAWS SDK(ここではPythonのboto3)のバージョンが最新じゃない場合に困ることがあります。

AWS Lambda-backed カスタムリソース – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html

そんなときにどうしたら良いものか悩んでいたのですが、AWS Lambda Layersが利用できるみたいだったので試してみました。

前提

  • AWSアカウントがある
  • AWS CLIが利用できる
  • AWS Lambda、Lambda Layers、CloudFormationの作成権限がある

AWS Lambda Layersに最新のAWS SDKのLayerを作成する

AWS Lambda Layersで最新のAWS SDKを利用する方法は下記を参考にさせてもらいました。(感謝

Lambda Layers で最新の AWS SDK を使用する – Qiita
https://qiita.com/hayao_k/items/b9750cc8fa69d0ce91b0

> mkdir 任意のディレクトリ
> cd 任意のディレクトリ

> mkdir python
> pip install -t ./python boto3
> zip -r python.zip ./python

> aws lambda publish-layer-version \
  --layer-name boto3 \
  --zip-file fileb://python.zip \
  --compatible-runtimes python3.7

{
    "Content": {
        "Location": "https://prod-04-2014-layers.s3.amazonaws.com/snapshots/(略)",
        "CodeSha256": "JZM5sEEyGBPgips+y+F0/X5rHXJIPkcLYeazyXiLkTk=",
        "CodeSize": 8572137
    },
    "LayerArn": "arn:aws:lambda:us-east-1:xxxxxxxxxxxx:layer:boto3",
    "LayerVersionArn": "arn:aws:lambda:us-east-1:xxxxxxxxxxxx:layer:boto3:1",
    "Description": "",
    "CreatedDate": "2019-06-21T08:57:29.995+0000",
    "Version": 1,
    "CompatibleRuntimes": [
        "python3.7"
    ]
}

AWS CloudFormationのテンプレートを作成する

AWS Lambda-backedカスタムリソースでboto3 のバージョンを確認するテンプレートを作成します。
比較のためにLayer利用する/しないのリソースを準備します。

> touch cfn-template.yaml

cfn-template.yaml

Resources:
  NonUseLambdaLayer:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt NonUseLambdaLayerFunction.Arn

  UseLambdaLayer:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt UseLambdaLayerFunction.Arn

  # 標準のboto3を利用
  NonUseLambdaLayerFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt FunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          import boto3
          def handler(event, context):
            print(boto3.__version__)
            cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
      Runtime: python3.7

  # Lambda Layerのboto3を利用
  UseLambdaLayerFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt FunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          import boto3
          def handler(event, context):
            print(boto3.__version__)
            cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
      Runtime: python3.7
      Layers:
        - arn:aws:lambda:us-east-1:xxxxxxxxxxxx:layer:boto3:1

  FunctionExecutionRole:
    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:*:*:*"

AWS CLIからCloudFormationのスタックを作成します。Lambda関数実行用のロールを作成するので、--capabilities CAPABILITY_IAM オプションを指定します。

> aws cloudformation create-stack \
  --stack-name cfn-lambda-backed-test \
  --template-body file://cfn-template.yaml \
  --capabilities CAPABILITY_IAM

{
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-lambda-backed-test/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

スタック作成できたらリソース一覧からAWS Lambdaの関数名を取得します。

> aws cloudformation list-stack-resources \
  --stack-name cfn-lambda-backed-test

{
    "StackResourceSummaries": [
        {
            "LogicalResourceId": "FunctionExecutionRole",
            "PhysicalResourceId": "cfn-lambda-backed-test-FunctionExecutionRole-XXXXXXXXXXXX",
            "ResourceType": "AWS::IAM::Role",
            "LastUpdatedTimestamp": "2019-06-24T07:25:29.253Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "NonUseLambdaLayer",
            "PhysicalResourceId": "2019/06/24/[$LATEST]8e94b0b2ffc54b00acf35a004e68c522",
            "ResourceType": "Custom::CustomResource",
            "LastUpdatedTimestamp": "2019-06-24T07:25:37.368Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "NonUseLambdaLayerFunction",
            "PhysicalResourceId": "cfn-lambda-backed-te-NonUseLambdaLayerFunctio-XXXXXXXXXXXXX",
            "ResourceType": "AWS::Lambda::Function",
            "LastUpdatedTimestamp": "2019-06-24T07:25:32.817Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "UseLambdaLayer",
            "PhysicalResourceId": "2019/06/24/[$LATEST]200facdec8cb4d77ac3dd5f333ceb848",
            "ResourceType": "Custom::CustomResource",
            "LastUpdatedTimestamp": "2019-06-24T07:25:41.716Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "UseLambdaLayerFunction",
            "PhysicalResourceId": "cfn-lambda-backed-test-UseLambdaLayerFunction-XXXXXXXXXXXXX",
            "ResourceType": "AWS::Lambda::Function",
            "LastUpdatedTimestamp": "2019-06-24T07:25:36.240Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}

リソースが取得できたらLambda関数のログからboto3 のバージョンを確認します。
上記リソースリストにあるResourceTypeAWS::Lambda::FunctionPhysicalResourceIdが関数名になります。
スタック作成時のログを確認しても良いのですが、ここでは簡単にしたかったので関数を実行して確認します。

# 標準のboto3
> aws lambda invoke \
  --function-name cfn-lambda-backed-te-NonUseLambdaLayerFunctio-XXXXXXXXXXXX \
  --log-type Tail \
  outputfile.txt \
  --query 'LogResult' | tr -d '"' | base64 -D

START RequestId: 859e7e0b-4041-4fa6-bf61-84a31bd72cc9 Version: $LATEST
1.9.42
    responseUrl = event['ResponseURL']e 15, in sendCCESS, {})
END RequestId: 859e7e0b-4041-4fa6-bf61-84a31bd72cc9
REPORT RequestId: 859e7e0b-4041-4fa6-bf61-84a31bd72cc9  Duration: 54.69 ms      Billed Duration: 100 ms         Memory Size: 128 MB   Max Memory Used: 56 MB


# Lambda Layerのboto3
> aws lambda invoke \
  --function-name cfn-lambda-backed-test-UseLambdaLayerFunction-XXXXXXXXXXXXX \
  --log-type Tail \
  outputfile.txt \
  --query 'LogResult' | tr -d '"' | base64 -D

START RequestId: 2e422fb4-95ae-4274-b9a5-4ced212a78ec Version: $LATEST
1.9.173
    responseUrl = event['ResponseURL']e 15, in sendCCESS, {})
END RequestId: 2e422fb4-95ae-4274-b9a5-4ced212a78ec
REPORT RequestId: 2e422fb4-95ae-4274-b9a5-4ced212a78ec  Duration: 28.95 ms      Billed Duration: 100 ms         Memory Size: 128 MB   Max Memory Used: 35 MB

Layerを利用している関数で最新のAWS SDKを利用できることが確認できました。

まとめ

AWS Lambda LayersへのLayer作成部分もスタックに含めることができればよいのですが、Zipファイルを事前にS3へ上げるなりの準備が必要で、そうなるとS3のリソースを事前に作成しなきゃ。。。
など、、、
どうしても1アクションで完結しなさそうだったので、Layer作成は手動ですることにしました。

もう少し考えればうまくまとまりそうな気がしてますが、今のところはこれで満足です。

参考

AWS Lambda-backed カスタムリソース – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.htm

Lambda Layers で最新の AWS SDK を使用する – Qiita
https://qiita.com/hayao_k/items/b9750cc8fa69d0ce91b0

元記事はこちら

AWS CloudFormationのAWS Lambda-backedカスタムリソースで最新のAWS SDKを利用する