AWS CloudFormationのLambda-BackedカスタムリソースでネストされてるっぽいJSONを返す方法

[cloudpack あら便利カレンダー 2019](https://benri2019.cloudpack.jp/) の記事となります。[誕生秘話](https://qiita.com/shinichi-takahashi/items/cea235795f904ccea207#%E3%81%82%E3%82%89%E4%BE%BF%E5%88%A9%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%E8%AA%95%E7%94%9F%E7%A7%98%E8%A9%B1) はこちら。

前回、AWS CloudFormation(CFn)のLambda-BackedカスタムリソースでネストされたJSONを返しても`!GetAtt` で参照できないことがわかったのですが、よくよく調べてみるとネストで返せそうだったのでさらに調べてみました。

AWS CloudFormationのLambda-BackedカスタムリソースでネストされたJSONを返しても参照できない - Qiita
https://qiita.com/kai_kou/items/61a2b3c69ae2af4f2e40


## 結論

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

ってするとそれっぽくなります。(白目


## 調べたこと

CFnの`GetAtt` 関数ドキュメントにあるテンプレート例で`SourceSecurityGroupOwnerId: !GetAtt myELB.SourceSecurityGroup.GroupName` と`myELB` リソースに対してネストされてる(っぽい)値`SourceSecurityGroup.GroupName` を参照しているのをみつけました。

Fn::GetAtt - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html


LoadBalancerのドキュメントをみると戻り値として確かに`SourceSecurityGroup.GroupName` は定義されています。

AWS::ElasticLoadBalancing::LoadBalancer - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-elb.html#aws-properties-ec2-elb-ref


通常のリソースの場合はネストされた情報が取り扱えるのでしょうか？
GetAttドキュメントの属性を確認すると、コンマ区切りで定義されている項目が確認できます。

Fn::GetAtt - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html#intrinsic-function-reference-getatt-attrib


## Lambda-Backedカスタムリソースでネストされてるっぽくしてみる

Lambda-Backedカスタムリソースでも同じように参照できるレスポンスデータの返し方を考えてみました。

### テンプレート定義

Lambda関数でフラットなJSONを返しますが、名前を`foo.hoge` とします(安直)。

```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": "hoge"
            }
            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
  foo:
    Value: !GetAtt CustomResource.foo.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/cea96c90-97b7-11e9-b384-0e75601403f8"
}


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

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/cea96c90-97b7-11e9-b384-0e75601403f8",
            "StackName": "cfn-response-test",
            "CreationTime": "2019-06-26T02:12:11.362Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Outputs": [
                {
                    "OutputKey": "hoge",
                    "OutputValue": "hoge"
                },
                {
                    "OutputKey": "foo",
                    "OutputValue": "hoge"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
```

無事に`!GetAtt CustomResource.foo.hoge` で値が参照できました。
一応関数のログを確認しておきます。

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

{
    "StackResourceSummaries": [
        {
            "LogicalResourceId": "CustomResource",
            "PhysicalResourceId": "2019/06/26/[$LATEST]0f6645081f974de7a40c44ac5a6b1493",
            "ResourceType": "Custom::CustomResource",
            "LastUpdatedTimestamp": "2019-06-26T02:12:38.029Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "CustomResourceFunction",
            "PhysicalResourceId": "cfn-response-test-CustomResourceFunction-RUGERLT0O78L",
            "ResourceType": "AWS::Lambda::Function",
            "LastUpdatedTimestamp": "2019-06-26T02:12:33.223Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        (略)
    ]
}


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

START RequestId: 9fb7a4fa-44b4-49ee-a5b7-bcbc57d40961 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]0f6645081f974de7a40c44ac5a6b1493", "PhysicalResourceId": "2019/06/26/[$LATEST]0f6645081f974de7a40c44ac5a6b1493", "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-response-test/cea96c90-97b7-11e9-b384-0e75601403f8", "RequestId": "bc048c80-c2f9-42ab-a596-6e031d8299f3", "LogicalResourceId": "CustomResource", "NoEcho": false, "Data": {"hoge": "hoge", "foo.hoge": "hoge"}}
        Status code: OK
        END RequestId: 9fb7a4fa-44b4-49ee-a5b7-bcbc57d40961
        REPORT RequestId: 9fb7a4fa-44b4-49ee-a5b7-bcbc57d40961  Duration: 351.55 ms     Billed Duration: 400 ms         Memory Size: 128 MB   Max Memory Used: 24 MB
```

フラットですがネストされてるっぽく参照できる`Data` を返していることが確認できます。


## まとめ

微妙に納得できない感じですが、テンプレートでどうしてもリソース情報のネスト構造を維持したまま参照したい場合にLambda-Backedカスタムリソース側で吸収するのに使えそうです。


## 参考

AWS CloudFormationのLambda-BackedカスタムリソースでネストされたJSONを返しても参照できない - Qiita
https://qiita.com/kai_kou/items/61a2b3c69ae2af4f2e40

Fn::GetAtt - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html

AWS::ElasticLoadBalancing::LoadBalancer - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-elb.html#aws-properties-ec2-elb-ref