AWS CloudFormationのLambda-BackedカスタムリソースでネストされたJSONを返しても参照できない

AWS CloudFormationn(CFn)のLambda-Backedカスタムリソースを利用していてレスポンスとしてネストされたJSONを返そうとしたらハマったので調べてみました。

## ハマった内容

Lambda-Backedカスタムリソースで

```py
import cfnresponse
def handler(event, context):
  data = {
    "hoge": "hoge",
    "foo": {
      "hoge": "foohoge"
    }
  }
  cfnresponse.send(event, context, cfnresponse.SUCCESS, data)
```

のようにCFnへレスポンスを返してCFnテンプレートで

```yaml
Outputs:
  hoge:
    Value: !GetAtt CustomResource.hoge
  foo:
    Value: !GetAtt CustomResource.foo.hoge
```

と、カスタムリソースの値を参照すると`!GetAtt CustomResource.foo.hoge` でエラーになりました。
エラー内容は`CustomResource attribute error: Vendor response doesn't contain foo.hoge key` 。


## 解決策

Lambda-BackedカスタムリソースでJSONを返す場合はネストせずフラットなJSONを返しましょう。


以下検証内容となります。


## 検証内容

### 前提

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

## エラーにならないケース

### テンプレート

Lambda関数ではネストされたJSONを返しますが、`!GetAtt` ではネストされた値を参照しないパターンです。
これでスタック作成してもエラーになりません。

```yaml: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
          def handler(event, context):
            data = {
              "hoge": "hoge",
              "foo": {
                "hoge": "foohoge"
              }
            }
            cfnresponse.send(event, context, cfnresponse.SUCCESS, data)
      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.hoge
```

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

```console
> aws cloudformation create-stack \
  --stack-name cfn-response-test \
  --template-body file://cfn-template.yaml \
  --capabilities CAPABILITY_IAM

{
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a"
}


> aws cloudformation describe-stacks \
  --stack-name cfn-response-test

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "StackName": "cfn-response-test",
            "CreationTime": "2019-06-26T01:47:20.177Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Outputs": [
                {
                    "OutputKey": "hoge",
                    "OutputValue": "hoge"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
```

正常に作成できました。Lambda関数のログを確認しておきます。

```console
> aws cloudformation list-stack-resources \
  --stack-name cfn-response-test

{
    "StackResourceSummaries": [
        {
            "LogicalResourceId": "CustomResource",
            "PhysicalResourceId": "2019/06/26/[$LATEST]f518c197bb8541f0b651a151bb201560",
            "ResourceType": "Custom::CustomResource",
            "LastUpdatedTimestamp": "2019-06-26T01:47:45.532Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "CustomResourceFunction",
            "PhysicalResourceId": "cfn-response-test-CustomResourceFunction-HI0ITG58NF7G",
            "ResourceType": "AWS::Lambda::Function",
            "LastUpdatedTimestamp": "2019-06-26T01:47:39.855Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        (略)
    ]
}


> aws logs get-log-events \
  --log-group-name /aws/lambda/cfn-response-test-CustomResourceFunction-HI0ITG58NF7G \
  --log-stream-name '2019/06/26/[$LATEST]f518c197bb8541f0b651a151bb201560' \
  --output=text \
  --query "events[*].message"

START RequestId: db73aff3-d14a-4ff1-92cc-6335d23279aa Version: $LATEST
        https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/(略)
        Response body:
        {"Status": "SUCCESS", "Reason": "See the details in CloudWatch Log Stream: 2019/06/26/[$LATEST]f518c197bb8541f0b651a151bb201560", "PhysicalResourceId": "2019/06/26/[$LATEST]f518c197bb8541f0b651a151bb201560", "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a", "RequestId": "c734ee9f-7963-4626-bf6e-40d7e24a99ab", "LogicalResourceId": "CustomResource", "NoEcho": false, "Data": {"hoge": "hoge", "foo": {"hoge": "foohoge"}}}
        Status code: OK
        END RequestId: db73aff3-d14a-4ff1-92cc-6335d23279aa
        REPORT RequestId: db73aff3-d14a-4ff1-92cc-6335d23279aa  Duration: 310.53 ms     Billed Duration: 400 ms         Memory Size: 128 MB   Max Memory Used: 24 MB
```

レスポンス内容の`Data` でネストされたJSONを返していることが確認できます。


## エラーにならないケース

`Outputs` の定義を変更して確認します。

### テンプレート(抜粋)

```yaml:cfn-template.yaml_抜粋
Outputs:
  hoge:
    Value: !GetAtt CustomResource.hoge
  foo:
    Value: !GetAtt CustomResource.foo.hoge
```

### スタック更新して確認する

```console
> aws cloudformation update-stack \
  --stack-name cfn-response-test \
  --template-body file://cfn-template.yaml \
  --capabilities CAPABILITY_IAM

{
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a"
}


> aws cloudformation describe-stacks \
  --stack-name cfn-response-test

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "StackName": "cfn-response-test",
            "CreationTime": "2019-06-26T01:47:20.177Z",
            "LastUpdatedTime": "2019-06-26T01:50:44.408Z",
            "RollbackConfiguration": {},
            "StackStatus": "UPDATE_ROLLBACK_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Outputs": [
                {
                    "OutputKey": "hoge",
                    "OutputValue": "hoge"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
```

`UPDATE_ROLLBACK_COMPLETE` となり更新に失敗しました。
イベントを取得してエラー内容を確認します。

```console
> aws cloudformation describe-stack-events \
  --stack-name cfn-response-test

{
    "StackEvents": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "EventId": "e01c4f90-97b4-11e9-bb14-12f81177eb92",
            "StackName": "cfn-response-test",
            "LogicalResourceId": "cfn-response-test",
            "PhysicalResourceId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "ResourceType": "AWS::CloudFormation::Stack",
            "Timestamp": "2019-06-26T01:51:12.126Z",
            "ResourceStatus": "UPDATE_ROLLBACK_COMPLETE"
        },
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "EventId": "df9dd0c0-97b4-11e9-9cbb-0eaa79b17470",
            "StackName": "cfn-response-test",
            "LogicalResourceId": "cfn-response-test",
            "PhysicalResourceId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "ResourceType": "AWS::CloudFormation::Stack",
            "Timestamp": "2019-06-26T01:51:11.295Z",
            "ResourceStatus": "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"
        },
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "EventId": "d1776100-97b4-11e9-a0b3-0a20b68b404c",
            "StackName": "cfn-response-test",
            "LogicalResourceId": "cfn-response-test",
            "PhysicalResourceId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "ResourceType": "AWS::CloudFormation::Stack",
            "Timestamp": "2019-06-26T01:50:47.544Z",
            "ResourceStatus": "UPDATE_ROLLBACK_IN_PROGRESS",
            "ResourceStatusReason": "CustomResource attribute error: Vendor response doesn't contain foo.hoge key in object arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a|CustomResource|c734ee9f-7963-4626-bf6e-40d7e24a99ab in S3 bucket cloudformation-custom-resource-storage-useast1"
        },
        (略)
    ]
}
```

`CustomResource attribute error: Vendor response doesn't contain foo.hoge key` とエラー内容が確認できます。


### 参照方法を変えてみる

`!GetAtt CustomResource.foo.hoge` としているのがダメなのかなと考えて参照方法を変更してみました。

### テンプレート(抜粋)

```yaml:cfn-template.yaml_抜粋
Outputs:
  hoge:
    Value: !GetAtt CustomResource.hoge
  foo:
    Value: !GetAtt CustomResource.foo['hoge']
```


### スタック更新して確認する

```console
> aws cloudformation update-stack \
  --stack-name cfn-response-test \
  --template-body file://cfn-template.yaml \
  --capabilities CAPABILITY_IAM

{
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a"
}


> aws cloudformation describe-stacks \
  --stack-name cfn-response-test

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "StackName": "cfn-response-test",
            "CreationTime": "2019-06-26T01:47:20.177Z",
            "LastUpdatedTime": "2019-06-26T01:54:45.226Z",
            "RollbackConfiguration": {},
            "StackStatus": "UPDATE_ROLLBACK_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Outputs": [
                {
                    "OutputKey": "hoge",
                    "OutputValue": "hoge"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}


> aws cloudformation describe-stack-events \
  --stack-name cfn-response-test

{
    "StackEvents": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "EventId": "6f1d0b80-97b5-11e9-b714-0a2e8057061e",
            "StackName": "cfn-response-test",
            "LogicalResourceId": "cfn-response-test",
            "PhysicalResourceId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "ResourceType": "AWS::CloudFormation::Stack",
            "Timestamp": "2019-06-26T01:55:12.038Z",
            "ResourceStatus": "UPDATE_ROLLBACK_COMPLETE"
        },
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "EventId": "6eb6f6b0-97b5-11e9-aaf5-0a437bda7b86",
            "StackName": "cfn-response-test",
            "LogicalResourceId": "cfn-response-test",
            "PhysicalResourceId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "ResourceType": "AWS::CloudFormation::Stack",
            "Timestamp": "2019-06-26T01:55:11.375Z",
            "ResourceStatus": "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"
        },
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "EventId": "609a23e0-97b5-11e9-a31e-0e366af4fd90",
            "StackName": "cfn-response-test",
            "LogicalResourceId": "cfn-response-test",
            "PhysicalResourceId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a",
            "ResourceType": "AWS::CloudFormation::Stack",
            "Timestamp": "2019-06-26T01:54:47.692Z",
            "ResourceStatus": "UPDATE_ROLLBACK_IN_PROGRESS",
            "ResourceStatusReason": "CustomResource attribute error: Vendor response doesn't contain foo['hoge'] key in object arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/55d8cd90-97b4-11e9-aa0c-128a65397f5a|CustomResource|c734ee9f-7963-4626-bf6e-40d7e24a99ab in S3 bucket cloudformation-custom-resource-storage-useast1"
        },
        (略)
    ]
}
```

`CustomResource attribute error: Vendor response doesn't contain foo['hoge'] key` とエラーになりました。


## まとめ

Lambda-BackedカスタムリソースでネストされたJSONを返しても`!GetAtt` では参照できないことがわかりました。ドキュメントにはDataの型が`Type: JSON object` と定義されているのですが制限があるみたいです。。。


カスタムリソースの応答オブジェクト - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html



## 参考

カスタムリソースの応答オブジェクト - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html