どうも

#ただそれだけ の CloudFormation 初心者, 川原です.

EC2 や ELB, そして, ELB のログを記録する S3 バケットを含むスタックを削除しようとすると, 軒並み以下のようなエラーとなってスタックの削除が失敗に終わる.

Target group 'arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/oreno-api-debug-alb-target/d429742cee94a2c2' is currently in use by a listener or a rule (Service: AmazonElasticLoadBalancingV2; Status Code: 400; Error Code: ResourceInUse; Request ID: 66f8369a-6636-11e8-9cc4-23b879baae28)

えええーっとなって, 慌てて手動でバケットを削除してから, 改めてスタックを削除すると削除は成功で終わる.

これを回避する方法って無いのかなーと思ったのでメモ.

どういうことなの?

DeletionPolicy 属性というドキュメントを読むと, 以下のように記述されている.

削除

AWS CloudFormation はスタックの削除時にリソースと (該当する場合) そのすべてのコンテンツを削除します。この削除ポリシーは、あらゆるリソースタイプに追加することができます。デフォルトでは、DeletionPolicy を指定しない場合、リソースは削除されます。 ただし、以下の点を考慮する必要があります。

  • AWS::RDS::DBCluster リソースの場合、デフォルトポリシーは Snapshot です。
  • DBClusterIdentifier プロパティを指定しない AWS::RDS::DBInstance リソースの場合、デフォルトポリシーは Snapshot です。
  • Amazon S3 バケットでは、削除を成功させるためにはバケットのすべてのオブジェクトを削除する必要があります。

なるほど, S3 バケットの中身は削除しなければ, スタックを削除する際に一緒に S3 バケットを削除することは出来ないとのこと.

じゃあ, どうすれば良いのか

DeletionPolicy で Retain を設定して, スタックの削除とは切り離す

AWS CloudFormation におけるリソースの削除処理の方法は、DeletionPolicy 属性で指定します。

docs.aws.amazon.com

以下のように, DeletionPolicyRetain を定義する.
i

 "ALBLOGBUCKET": {
      "Type": "AWS::S3::Bucket",
      "DeletionPolicy" : "Retain",
      "Properties": {
        "BucketName": { "Fn::Join" : [ "", [{ "Ref": "Project" }, "-", { "Ref": "Env" }, "-alb-log"]]}
      }
    },

Retain を設定することで, スタックを削除する際にも S3 バケット自体は削除せずに, 個別に手動 (AWS CLI 等) で S3 バケットを削除する.

カスタムリソースを利用して, Lambda ファンクションでオブジェクトを削除してからバケットを削除する

CloudFormation テンプレートでは, AWS::CloudFormation::CustomResource または Custom::String リソースタイプを使用して、カスタムリソースを指定することが出来る. また, スタックイベントに応じて Lambda ファンクションを実行することが出来る.

AWS CloudFormation スタックに AWS 以外のリソースを格納できるよう、AWS::CloudFormation::CustomResource リソースを使用してカスタムリソースを指定します。

docs.aws.amazon.com

これを利用してスタックを削除する際に Lambda ファンクションを呼び出してバケット内のオブジェクトを削除してからバケットを削除するという方法を検討する. この方法は, 以下の記事を参考にさせて頂いた.

Is there any way to force CloudFormation to delete a non-empty S3 Bucket?

stackoverflow.com

カスタムリソースについて

改めて, カスタムリソースについて, 以下のドキュメントを参考に整理してみる.

AWS CloudFormation スタックに AWS 以外のリソースを格納できるよう、AWS::CloudFormation::CustomResource リソースを使用してカスタムリソースを指定します。

docs.aws.amazon.com

カスタムリソースとは, スタックを作成, 更新, 削除する度に CloudFormation がカスタムリソースで定義されたロジック (Lambda ファンクション等) を実行することが出来る機能で, 以下の三者が関連して動作している. (ドキュメントより引用しているが, 内容を理解し辛かったので意訳も入っている)

登場人物 役割
template developer 要するに CloudFormation のテンプレートで, カスタムリソースを定義する
cusom resource privider CloudFormation から要求される処理と応答を行う (例えば, Lambda ファンクションとか), テンプレート内では ServiceToken の値として指定する
CloudFormation スタックオペレーション中にテンプレートで指定されたリクエストを ServiceToken に送信し, スタックオペレーションを進める前に応答を待機する

テンプレート内ではカスタムリソースを以下のように定義する.

MyCustomResource: 
  Type: "Custom::TestLambdaCrossStackRef"
  Properties: 
    ServiceToken:
      !Sub |
        arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunctionName}
    StackName: 
      Ref: "NetworkStackName"

オブジェクトをまるっと削除する Lambda ファンクション (1)

Lambda ファンクションを実装するにあたって, CloudFormation 側から送信されるイベントと, Lambda ファンクションが実行された後に CloudFormation に返す情報が必要となる. これらの情報についても, ドキュメントに明記されており, 例えば, スタック削除の場合には, 以下のようなイベント (ドキュメントより引用) が CloudFormation から送信される.

{
   "RequestType" : "Delete",
   "RequestId" : "unique id for this delete request",
   "ResponseURL" : "pre-signed-url-for-delete-response",
   "ResourceType" : "Custom::MyCustomResourceType",
   "LogicalResourceId" : "name of resource in template",
   "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
   "PhysicalResourceId" : "custom resource provider-defined physical id",
   "ResourceProperties" : {
      "key1" : "string",
      "key2" : [ "list" ],
      "key3" : { "key4" : "map" }
   }
}

また, Lambda ファンクションが実行された際に CloudFormation に返す情報は以下のような内容となっている. こちらも, スタック削除時のメッセージとなる.

成功した場合.

{
   "Status" : "SUCCESS",
   "RequestId" : "unique id for this delete request (copied from request)",
   "LogicalResourceId" : "name of resource in template (copied from request)",
   "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
   "PhysicalResourceId" : "custom resource provider-defined physical id"
}

失敗した場合.

{
  "Status" : "FAILED",
  "Reason" : "Required failure reason string",
  "RequestId" : "unique id for this delete request (copied from request)",
  "LogicalResourceId" : "name of resource in template (copied from request)",
  "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
  "PhysicalResourceId" : "custom resource provider-defined physical id"
}  

これらのイベントメッセージをやり取り出来るように Lambda ファンクションを実装する必要がある.

オブジェクトをまるっと削除する Lambda ファンクション (2)

以下のリポジトリにアップした. serverless framework でデプロイ出来るようにしてある.

Contribute to CleanupBucketOnDelete development by creating an account on GitHub.

github.com

テンプレートは以下のように書くことで, バケットを削除する前にカスタムリソースが呼ばれて, バケットの中身を削除した後にバケットが削除されるようになる.

AWSTemplateFormatVersion: "2010-09-09"
Description: "Clean up Bucket on CloudFormation stack delete demo."
Parameters:
  S3BucketName:
    Type: String
  CleanUpBucketFunction:
    Type: String
Resources:
   BucketResource:
     Type: AWS::S3::Bucket
     Properties:
       BucketName: !Ref S3BucketName
   CleanupBucketOnDelete:
     Type: Custom::CleanupBucket
     Properties:
       ServiceToken:
         Fn::Join:
           - ""
           - - "arn:aws:lambda:"
             - Ref: AWS::Region
             - ":"
             - Ref: AWS::AccountId
             - ":function:"
             - !Ref CleanUpBucketFunction
       BucketName: !Ref S3BucketName
     DependsOn: BucketResource

スタックを削除する

上記に掲載した CloudFormation テンプレートを利用して, オブジェクトが入っている場合と入っていない場合でバケットの削除を試してみる. このテンプレートを利用することで, oreno-sample-bucket という名前のバケットを作成する.

$ ./deploy.sh aws-profile oreno-sample-bucket demo create
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/demo-oreno-sample-bucket/1387e400-6680-11e8-9468-50fa13f2a811"
}
Create Stack Success.

一応, バケットが作成出来たかを確認してみる.

$ aws s3api list-buckets --query=Buckets[].Name | grep "oreno-sample-bucket"
    "oreno-sample-bucket",

バケットにオブジェクトを放り込んでみる.

$ aws s3 cp test.txt s3://oreno-sample-bucket/
upload: ./test.txt to s3://oreno-sample-bucket/test.txt
$ aws s3 ls s3://oreno-sample-bucket/
2018-06-03 01:20:40          0 test.txt

AWS CLI でバケットの削除を試みてみる.

$ aws s3 rb s3://oreno-sample-bucket
remove_bucket failed: s3://oreno-sample-bucket An error occurred (BucketNotEmpty) when calling the DeleteBucket operation: The bucket you tried to delete is not empty

上記のようにエラーとなる. これまで書いたように, バケットを空にする必要がある.

では, この状態でスタックを削除してみる.

$ ./deploy.sh aws-profile oreno-sample-bucket demo delete
Delete Stack Success.

正常にスタックの削除が完了した. Lambda から CloudFormation には, 以下のようなイベントが送信されている (CloudWatch Logs にダンプした).

{
    "Status": "SUCCESS",
    "Reason": "Log stream name: 2018/06/02/[$LATEST]018724fd1cc242deade18d90a525daf0",
    "PhysicalResourceId": "2018/06/02/[$LATEST]018724fd1cc242deade18d90a525daf0",
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/demo-oreno-sample-bucket/0580ea80-667d-11e8-80c3-50a68a175a82",
    "RequestId": "53fdeee6-feca-46b2-8073-94751f752386",
    "LogicalResourceId": "CleanupBucketOnDelete",
    "Data": {}
}

懸念点

Lambda ファンクションを用いて, オブジェクトを削除するこの方法だが, 一点だけ気になることがある. それは, オブジェクトの数がとてつもなく多い場合に削除に時間が掛かってしまい, スタックの削除自体が正常に終了しない可能性がある. これはどうしても回避することは出来ないので, やはり, テンプレートに DeletionPolicyRetain に設定して, 手動でバケットを削除する方法が良いと思われる.

以上

CloudFormation 作った S3 バケットにおいて, オブジェクトが入っている状態でスタックを削除する方法について考察した. シンプルなのは DeletionPolicyRetain を定義しておいて, 後から手動で S3 バケットを削除する, もしくは, Lambda ファンクションを実装する必要があるけど, カスタムリソースを利用することで, 一気通貫で削除することが出来ることが解った. いずれにせよひと手間かかるのは変わらないし, オブジェクト数次第では, カスタムリソースでは対応しきれない可能性がある為, 適材適所で使い分けるようにしたい.

また, CloudFormation について, カスタムリソースの存在を知れたのはとても良かった. うまく活用していきたいと思う.

元記事はこちら

CloudFormation で作った S3 バケットにおいて, オブジェクトが入っている状態でスタックを削除しようとすると軒並みエラーになるので, その対処方法について検討した #ただそれだけ