【AWS】【Cloudformation】CustomResource作ったら、Stack更新時にDeleteアクション実行してしまう件

# TL;DR

* CustomResource作ったら、Stack更新時にDeleteアクション実行してしまう
* これが、起きるのはCustomResourceのlambda処理を更新したとき
* 原因はStackのCleanUp処理とPhysicalResourceIdの仕組み
* CustomResourceの更新時にPhysicalResourceIdが変わってしまうと、CustomResourceにDelete Requestを送ってしまう
* cfnresponse.sendで送るPhysicalResourceIdを固定することで、変更がなくなり、結果としてDeleteアクションを防げる
    * こんな感じで固定。Create時はeventにPhysicalResourceIdを持たないので、そこはうまくハンドル

* ```python
pycfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, event.get('PhysicalResourceId'))
```

# 環境

* cfnresponseを使用したCustomResource
* CustomResourceのlambdaはpythonで作成
    * SESのidentityにnotificationを設定するCustomResource
* `"RequestType": "Create", "RequestType": "Update", "RequestType": "Delete"` それぞれ処理をハンドルしている

# CustomResourceに送られるリクエストって？
* こんな感じのが変数`event`の中に入って送られてます

* Create

```json
{
  "RequestType": "Create",
  "ServiceToken": "arn:aws:lambda:us-west-2:******:function:set_identity_notifications",
  "ResponseURL": "https://cloudformation-custom-resource-response-uswest2.s3-us-west-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-west-2%3A535996888546%3Astack/ses-suppression-register/fa440490-497e-11ea-b69e-06fe8be0ff5e%7CProvideSetIdentityNotifications*******",
  "StackId": "arn:aws:cloudformation:us-west-2:535996888546:stack/ses-suppression-register/fa440490-497e-11ea-b69e-06fe8be0ff5e",
  "RequestId": "fe0f6642-9115-46d2-856d-98047f5c214b",
  "LogicalResourceId": "ProvideSetIdentityNotifications",
  "ResourceType": "AWS::CloudFormation::CustomResource",
  "ResourceProperties": {
    "ServiceToken": "arn:aws:lambda:us-west-2:******:function:set_identity_notifications",
    "HeadersInBounceNotificationsEnabled": "False",
    "ComplaintTopic": "",
    "HeadersInComplaintNotificationsEnabled": "False",
    "BounceTopic": "arn:aws:sns:us-west-2:*******:ses-suppression-list-topic",
    "Identities": "willco21.test.com"
  }
}
```

* Update

```json
{
  "RequestType": "Update",
  "ServiceToken": "arn:aws:lambda:us-west-2:*******:function:set_identity_notifications",
  "ResponseURL": "https://cloudformation-custom-resource-response-uswest2.s3-us-west-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-west-2%3A*******%3Astack/ses-suppression-register/6ba8e740-4989-11ea-9aac-06be751c1bd6%7CProvideSetId******",
  "StackId": "arn:aws:cloudformation:us-west-2:*******:stack/ses-suppression-register/6ba8e740-4989-11ea-9aac-06be751c1bd6",
  "RequestId": "c3a874a5-a181-4a5c-88fc-d2b2e1504aa0",
  "LogicalResourceId": "ProvideSetIdentityNotifications",
  "PhysicalResourceId": "2020/02/07/[$LATEST]0b367e84e59e46798738c00bece897be",
  "ResourceType": "AWS::CloudFormation::CustomResource",
  "ResourceProperties": {
    "ServiceToken": "arn:aws:lambda:us-west-2:*******:function:set_identity_notifications",
    "HeadersInBounceNotificationsEnabled": "False",
    "ComplaintTopic": "",
    "HeadersInComplaintNotificationsEnabled": "False",
    "BounceTopic": "arn:aws:sns:us-west-2:*******:ses-suppression-list-topic",
    "Identities": ["willco21.test.com", "example.co.jp"]
  },
  "OldResourceProperties": {
    "ServiceToken": "arn:aws:lambda:us-west-2:*******:function:set_identity_notifications",
    "HeadersInBounceNotificationsEnabled": "False",
    "ComplaintTopic": "",
    "HeadersInComplaintNotificationsEnabled": "False",
    "BounceTopic": "arn:aws:sns:us-west-2:*******:ses-suppression-list-topic",
    "Identities": ["willco21.test.com"]
  }
}
```

* Delete

```json
{
  "RequestType": "Delete",
  "ServiceToken": "arn:aws:lambda:us-west-2:*******:function:set_identity_notifications",
  "ResponseURL": "https://cloudformation-custom-resource-response-uswest2.s3-us-west-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-west-2%3A*******%3Astack/ses-suppression-register/6ba8e740-4989-11ea-9aac-06be751c1bd6%7C*******************",
  "StackId": "arn:aws:cloudformation:us-west-2:*******:stack/ses-suppression-register/6ba8e740-4989-11ea-9aac-06be751c1bd6",
  "RequestId": "33c8a15c-d8c1-477e-a88d-38c43fa46c67",
  "LogicalResourceId": "ProvideSetIdentityNotifications",
  "PhysicalResourceId": "2020/02/07/[$LATEST]0b367e84e59e46798738c00bece897be",
  "ResourceType": "AWS::CloudFormation::CustomResource",
  "ResourceProperties": {
    "ServiceToken": "arn:aws:lambda:us-west-2:*******:function:set_identity_notifications",
    "HeadersInBounceNotificationsEnabled": "False",
    "ComplaintTopic": "",
    "HeadersInComplaintNotificationsEnabled": "False",
    "BounceTopic": "arn:aws:sns:us-west-2:*******:ses-suppression-list-topic",
    "Identities": ["willco21.test.com"]
  }
}
```

# PhysicalResourceIdについて

* CFNで作成したリソースを判別するもの。実際に作成されたリソースを指すのが基本。PhysicalResourceId
* LogicalResourceIdとは違う。
    * LogicalResourceIdはテンプレートのResourceで定義するリソース名
* CustomResourceのlambdaには作成されたlambdaように作成された、CloudWatchLogsのLog streamが入る

# なぜUpdateでDeleteアクション実行されるの？？

* cfnresponse.sendは5つ目の引数にPhysicalResourceIdが渡せる。
* PhysicalResourceIdを渡さない場合は、新しいPhysicalResourceIdが勝手に送られる
* 順番はこんな感じ

1. CustomResourceのlambdaが更新 
2. 更新されたものでCustomResource実行 
3. 新しいLog stream作成 
4. cfnresponse.sendを実行するとそのLog streamでPhysicalResourceIdが更新される
5. PhysicalResourceIdが更新されたので、StackのCleanUp処理で古いPhysicalResourceIdに関連するリソースはDelete リクエストが送られる
6. CustomResourceがDeteleリクエストで実行される

# 回避策

* cfnresponse.sendで送るPhysicalResourceIdを固定することで、変更がなくなり、結果としてDeleteアクションを防げる
    * こんな感じで固定。Create時はeventにPhysicalResourceIdを持たないので、そこはうまくハンドル

* ```python
pycfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, event.get('PhysicalResourceId'))
```

# 参考

https://stackoverflow.com/questions/50599602/updating-custom-resources-causes-them-to-be-deleted