AWS CloudFormationのLambda-backedカスタムリソースでリソースの更新・削除をする方法

[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のLambda-backedカスタムリソースを利用するとAWS CloudFormationで管理できないリソースも管理することができますが、Lambda-backedで作成したリソースの更新・削除するのにリソースのIDをどうやって取り回そうか悩みました。

下記は解決策の1案となりますが、他に良い方法があれば教えてほしいです！


## 前提

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


## CloudFormationのテンプレート作成

```console
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> touch cfn-template.yaml
```

Lambda-backedカスタムリソースを利用して何かしらのリソースを作成・更新・削除するテンプレートとなります。
ポイントとしては`CreateResource` のひとつで完結できたら良かったのですが、`CreateResource` で作成したリソースのIDを自前で取り回せなかったので、更新と削除を別リソース`UpdateResource` で行うようにしました。~~うーん、めんどうです~~


```yaml:cfn-template.yaml
Resources:
  CreateResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CreateResourceFunction.Arn

  UpdateResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt UpdateResourceFunction.Arn
      ResourceId: !GetAtt CreateResource.Id

  CreateResourceFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt FunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          def handler(event, context):
            if event['RequestType'] == 'Create':
              # なんかリソース作成
              response = {'Id': 'hoge'}
              print('create resources ' + response['Id'])
              cfnresponse.send(event, context, cfnresponse.SUCCESS, response)
              return

            # 他のRequestTypeは無視
            cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
      Runtime: python3.7

  UpdateResourceFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt FunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          def handler(event, context):
            Id = event['ResourceProperties']['ResourceId']
            if event['RequestType'] == 'Update':
              # なんかリソース更新
              print('update resources ' + Id)
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              return

            if event['RequestType'] == 'Delete':
              # なんかリソース削除
              print('delete resources ' + Id)
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              return

            # 他のRequestTypeは無視
            cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
      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:*:*:*"
```


## 動作確認

### スタック作成

```console
> 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/3bed13d0-96e9-11e9-90fb-122d883fe268"
}
```

スタック作成して各リソースが作成できたらLambda関数のログを確認します。
各リソースと関数の`PhysicalResourceId` をパラメータに`aws logs get-log-events` コマンドで取得します。

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

{
    "StackResourceSummaries": [
        {
            "LogicalResourceId": "CreateResource",
            "PhysicalResourceId": "2019/06/25/[$LATEST]587b7bbfa5b74a38a54846ff44a9a592",
            "ResourceType": "Custom::CustomResource",
            "LastUpdatedTimestamp": "2019-06-25T01:33:55.358Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "CreateResourceFunction",
            "PhysicalResourceId": "cfn-lambda-backed-test-CreateResourceFunction-LBG1WB6FV88B",
            "ResourceType": "AWS::Lambda::Function",
            "LastUpdatedTimestamp": "2019-06-25T01:33:49.819Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "FunctionExecutionRole",
            "PhysicalResourceId": "cfn-lambda-backed-test-FunctionExecutionRole-17PMYTJ3NS41Z",
            "ResourceType": "AWS::IAM::Role",
            "LastUpdatedTimestamp": "2019-06-25T01:33:46.370Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "UpdateResource",
            "PhysicalResourceId": "2019/06/25/[$LATEST]59ec7f3b07ec48f5b5feab288384b268",
            "ResourceType": "Custom::CustomResource",
            "LastUpdatedTimestamp": "2019-06-25T01:34:01.068Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        },
        {
            "LogicalResourceId": "UpdateResourceFunction",
            "PhysicalResourceId": "cfn-lambda-backed-test-UpdateResourceFunction-BMXD99ZLKXP0",
            "ResourceType": "AWS::Lambda::Function",
            "LastUpdatedTimestamp": "2019-06-25T01:33:49.516Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}


> aws logs get-log-events \
  --log-group-name /aws/lambda/cfn-lambda-backed-test-CreateResourceFunction-LBG1WB6FV88B \
  --log-stream-name '2019/06/25/[$LATEST]587b7bbfa5b74a38a54846ff44a9a592' \
  --output=text \
  --query "events[*].message"

START RequestId: e8ad68ad-86be-4ea1-ad3f-736617882ce7 Version: $LATEST
        create resources hoge
        https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/(略)
        Response body:
        {"Status": "SUCCESS", "Reason": "See the details in CloudWatch Log Stream: 2019/06/25/[$LATEST]587b7bbfa5b74a38a54846ff44a9a592", "PhysicalResourceId": "2019/06/25/[$LATEST]587b7bbfa5b74a38a54846ff44a9a592", "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-lambda-backed-test/3bed13d0-96e9-11e9-90fb-122d883fe268", "RequestId": "1cb162f1-979f-4e6f-b277-dd7e729d23cc", "LogicalResourceId": "CreateResource", "NoEcho": false, "Data": {"Id": "hoge"}}
        Status code: OK
        END RequestId: e8ad68ad-86be-4ea1-ad3f-736617882ce7
        REPORT RequestId: e8ad68ad-86be-4ea1-ad3f-736617882ce7  Duration: 292.74 ms     Billed Duration: 300 ms         Memory Size: 128 MB   Max Memory Used: 29 MB


> aws logs get-log-events \
  --log-group-name /aws/lambda/cfn-lambda-backed-test-UpdateResourceFunction-BMXD99ZLKXP0 \
  --log-stream-name '2019/06/25/[$LATEST]59ec7f3b07ec48f5b5feab288384b268' \
  --output=text \
  --query "events[*].message"

START RequestId: d593f33a-9231-4284-8157-97b5b799f70e 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/25/[$LATEST]59ec7f3b07ec48f5b5feab288384b268", "PhysicalResourceId": "2019/06/25/[$LATEST]59ec7f3b07ec48f5b5feab288384b268", "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-lambda-backed-test/3bed13d0-96e9-11e9-90fb-122d883fe268", "RequestId": "fe1b2ba8-0aff-44a6-ac1f-f46863594b0f", "LogicalResourceId": "UpdateResource", "NoEcho": false, "Data": {}}
        Status code: OK
        END RequestId: d593f33a-9231-4284-8157-97b5b799f70e
        REPORT RequestId: d593f33a-9231-4284-8157-97b5b799f70e  Duration: 242.26 ms     Billed Duration: 300 ms         Memory Size: 128 MB   Max Memory Used: 29 MB
```

スタック作成時には`CreateResource` でリソースの作成、`UpdateResource` は呼び出しのみとなることが確認できました。


### スタック削除

スタックを削除して動作を確認します。

```console
> aws cloudformation delete-stack \
  --stack-name cfn-lambda-backed-test

{
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-lambda-backed-test/3bed13d0-96e9-11e9-90fb-122d883fe268"
}
```

スタック削除すると当然のことながらリソースが取得できなくなるので、ログストリーム名を取得してからログを確認します。

```console
> aws logs describe-log-streams \
  --log-group-name /aws/lambda/cfn-lambda-backed-test-CreateResourceFunction-LBG1WB6FV88B \
  --output=text \
  --query "logStreams[*].logStreamName"

2019/06/25/[$LATEST]587b7bbfa5b74a38a54846ff44a9a592    2019/06/25/[$LATEST]8d6fac4288674ecbb7b6f7a577b8b932


> aws logs get-log-events \
  --log-group-name /aws/lambda/cfn-lambda-backed-test-CreateResourceFunction-LBG1WB6FV88B \
  --log-stream-name '2019/06/25/[$LATEST]8d6fac4288674ecbb7b6f7a577b8b932' \
  --output=text \
  --query "events[*].message"

START RequestId: 8a0c34a7-8c3a-47b5-9128-3a49748aca52 Version: $LATEST



> aws logs describe-log-streams \
  --log-group-name /aws/lambda/cfn-lambda-backed-test-UpdateResourceFunction-BMXD99ZLKXP0 \
  --output=text \
  --query "logStreams[*].logStreamName"

2019/06/25/[$LATEST]0c2223e7635d4ee0b04c508e0fc76511    2019/06/25/[$LATEST]59ec7f3b07ec48f5b5feab288384b268


> aws logs get-log-events \
  --log-group-name /aws/lambda/cfn-lambda-backed-test-UpdateResourceFunction-BMXD99ZLKXP0 \
  --log-stream-name '2019/06/25/[$LATEST]0c2223e7635d4ee0b04c508e0fc76511' \
  --output=text \
  --query "events[*].message"

START RequestId: fcde5258-b444-4e33-aef6-d2dcff63e2c2 Version: $LATEST
        delete resources hoge
        https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/(略)
        Response body:
        {"Status": "SUCCESS", "Reason": "See the details in CloudWatch Log Stream: 2019/06/25/[$LATEST]0c2223e7635d4ee0b04c508e0fc76511", "PhysicalResourceId": "2019/06/25/[$LATEST]0c2223e7635d4ee0b04c508e0fc76511", "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/cfn-lambda-backed-test/3bed13d0-96e9-11e9-90fb-122d883fe268", "RequestId": "5d390a57-55b6-4ddd-8820-0104c94ab705", "LogicalResourceId": "UpdateResource", "NoEcho": false, "Data": {}}
        Status code: OK
        END RequestId: fcde5258-b444-4e33-aef6-d2dcff63e2c2
        REPORT RequestId: fcde5258-b444-4e33-aef6-d2dcff63e2c2  Duration: 644.07 ms     Billed Duration: 700 ms         Memory Size: 128 MB   Max Memory Used: 56 MB
```

スタック削除時には`CreateResource` は呼び出しのみ、`UpdateResource` でリソースの削除がされることが確認できました。


## まとめ

若干定義が面倒になりますがAWS CloudFormationのLambda-backedカスタムリソースを利用してリソースを更新・削除できることが確認できました。


## 参考

Blue21: lambdaのログを aws-cli で見る
https://blue21neo.blogspot.com/2018/02/lambda-aws-cli.html