AWS CloudFormation(CFn)でAmazon Managed Blockchainのリソースを管理しようとテンプレート作成してた際にハマったエラーです。

エラー原因

JSONに含まれる日付がdatetime.datetime()となっているため発生します。
cfnresponse.send でのjson.dumps実行時にエラーとなります。

解決策

JSONに含まれる日付をdatetime.datetime() から文字列に変換して対応します。
下記が参考になりました。

[Python] dateやdatetimeをjson.dumpでエラーなく出力する – YoheiM .NET
https://www.yoheim.net/blog.php?q=20170703

エラー再現と対応

前提

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

エラーとなるテンプレート定義

cfn-template.yaml

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CustomResourceFunction.Arn

  CustomResourceFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt FunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          import datetime
          def handler(event, context):
            response = {}
            if event['RequestType'] == 'Create':
              response = {
                "Id": "hoge",
                "Datetime": datetime.datetime.today()
              }

            cfnresponse.send(event, context, cfnresponse.SUCCESS, response)
      Runtime: python3.7

  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:*:*:*"
Outputs:
  hoge:
    Value: !GetAtt CustomResource.Datetime

Lambda関数を抜粋してみるとcfnresponse.sendのパラメータdataのJSONdatetime.datetime.today()で設定して値がdatetime.datetime() となるようにします。

cfn-template.yaml_抜粋

import cfnresponse
import datetime
def handler(event, context):
  response = {}
  if event['RequestType'] == 'Create':
    response = {
      "Id": "hoge",
      "Datetime": datetime.datetime.today()
    }

  cfnresponse.send(event, context, cfnresponse.SUCCESS, response)

スタック作成して確認する

aws cloudformation create-stack \
  --stack-name cfn-json-error \
  --template-body file://cfn-template.yaml \
  --capabilities CAPABILITY_IAM

{
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-json-error/2371e5d0-97e6-11e9-8372-0abba895ce2c"
}

# エラーでロールバックするまでしばらくかかります...
aws cloudformation describe-stacks \
  --stack-name cfn-json-error

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-json-error/2371e5d0-97e6-11e9-8372-0abba895ce2c",
            "StackName": "cfn-json-error",
            "CreationTime": "2019-06-26T07:43:50.455Z",
            "DeletionTime": "2019-06-26T08:44:33.971Z",
            "RollbackConfiguration": {},
            "StackStatus": "ROLLBACK_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}

Lambda関数のエラーログを一部抜粋します。

[ERROR] TypeError: Object of type datetime is not JSON serializable
Traceback (most recent call last):
  File "/var/task/index.py", line 11, in handler
    cfnresponse.send(event, context, cfnresponse.SUCCESS, data)
  File "/var/task/cfnresponse.py", line 29, in send
    json_responseBody = json.dumps(responseBody)
  File "/var/lang/lib/python3.7/json/__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
  File "/var/lang/lib/python3.7/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/var/lang/lib/python3.7/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/var/lang/lib/python3.7/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '

エラー対応する

テンプレート定義

エラー対応したテンプレート定義です。

cfn-template.yaml_対応版

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CustomResourceFunction.Arn

  CustomResourceFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt FunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          import json
          from datetime import date, datetime
          def json_serial(obj):
            if isinstance(obj, (datetime, date)):
              return obj.isoformat()
            raise TypeError ('Type %s not serializable' % type(obj))

          def handler(event, context):
            response = {}
            if event['RequestType'] == 'Create':
              response = {
                "Id": "hoge",
                "Datetime": datetime.today()
              }
              response = json.loads(json.dumps(response, default=json_serial))

            cfnresponse.send(event, context, cfnresponse.SUCCESS, response)
      Runtime: python3.7

  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:*:*:*"
Outputs:
  hoge:
    Value: !GetAtt CustomResource.Datetime

ポイント

json.dumpsdefaultを指定して日付を文字列に変換する

実装そのままです。
json.dumpsでJSONを文字列変換する際にdefaultに指定したjson_serialメソッドで日付を文字列に変換します。
cfnresponse.sendにはJSONを渡す必要があるのでjson.loadsで文字列から再度JSONに戻しています。

cfn-template.yaml_対応版_抜粋

import cfnresponse
import json
from datetime import date, datetime
def json_serial(obj):
  if isinstance(obj, (datetime, date)):
    return obj.isoformat()
  raise TypeError ('Type %s not serializable' % type(obj))

def handler(event, context):
  response = {}
  if event['RequestType'] == 'Create':
    response = {
      "Id": "hoge",
      "Datetime": datetime.today()
    }
    response = json.loads(json.dumps(response, default=json_serial))

  cfnresponse.send(event, context, cfnresponse.SUCCESS, response)

再度スタック作成して確認する

aws cloudformation delete-stack \
  --stack-name cfn-json-error

> aws cloudformation create-stack \
  --stack-name cfn-json-error \
  --template-body file://cfn-template.yaml \
  --capabilities CAPABILITY_IAM

{
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-json-error/fead9f00-97ef-11e9-b4fc-0e16aabfe77c"
}


> aws cloudformation describe-stacks \
  --stack-name cfn-json-error

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-json-error/fead9f00-97ef-11e9-b4fc-0e16aabfe77c",
            "StackName": "cfn-json-error",
            "CreationTime": "2019-06-26T08:54:23.736Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Outputs": [
                {
                    "OutputKey": "hoge",
                    "OutputValue": "2019-06-26T08:54:48.738374"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}

無事に日付が文字列に変換されてスタック作成できたのが確認できました。

参考

[Python] dateやdatetimeをjson.dumpでエラーなく出力する – YoheiM .NET
https://www.yoheim.net/blog.php?q=20170703

元記事はこちら

AWS CloudFormationのLambda-Backedカスタムリソースで&aposicfnresponse.send()&aposi 実行時に&aposiTypeError: Object of type datetime is not JSON serializable&aposi エラーになった際の対応方法