😊

[CloudFormation] CFnで作成したバケットを空にして削除する

2024/03/12に公開

やりたいこと

CloudFormation で作成した S3 バケットをスタック削除時に同時に削除できるようにする
→なにも対策を行っていない場合、作成したバケットにファイルが作成された状態でスタックを削除するとエラーが発生しスタックの削除に失敗します。
このため、カスタムリソースを作成しスタックの削除時にバケットを空にするコードを記載してあげる必要があります
※ECR なども同様です

コード

AWSTemplateFormatVersion: 2010-09-09
Description: If you use S3 bucket, you need to clean up used S3 bucket. so this template is cleanup example

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties: 
      AccessControl: Private

  CleanupBucketOnDelete:
    Type: Custom::cleanupBucket
    Properties:
      ServiceToken: !GetAtt CleanBucketFunction.Arn
    DependsOn: S3Bucket

  CleanBucketFunction:
    Type: AWS::Lambda::Function
    Properties:
      Description: Cleans out Bucket during delete
      Handler: index.handler
      Runtime: python3.9
      Role: !GetAtt CleanBucketRole.Arn
      Timeout: 900
      Environment:
        Variables: 
          BUCKET_NAME: !Ref S3Bucket
      Code:
        ZipFile: |
          import cfnresponse
          import logging
          import boto3
          import time, os
          status = cfnresponse.SUCCESS
          logger = logging.getLogger(__name__)
          logging.basicConfig(format='%(asctime)s %(message)s',level=logging.DEBUG)

          def handler(event, context):
              logger.debug(event)

              if event['RequestType'] == 'Delete':
                try:
                  BUCKETNAME = os.environ['BUCKET_NAME']
                  s3 = boto3.resource('s3')
                  time.sleep(90)
                  bucket = s3.Bucket(BUCKETNAME)
                  bucket_versioning = s3.BucketVersioning(BUCKETNAME)
                  if bucket_versioning.status == 'Enabled':  # disabled buclet versioning setting
                    bucket.object_versions.delete()
                  else:
                    bucket.objects.all().delete()
                  cfnresponse.send(event, context, status, {}, None)
                except Exception as e:
                  print(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {}, None)
              else:
                cfnresponse.send(event, context, status, {}, None)

  CleanBucketRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: lambda-bucketcleaner
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:DeleteObject
                  - s3:DeleteObjectVersion
                Resource: !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}/*'
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:GetBucketVersioning
                Resource: !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}'

コードの説明

S3

S3 バケットを作成します。
設定値についてはシンプルにしてます←

  S3Bucket:
    Type: AWS::S3::Bucket
    Properties: 
      AccessControl: Private

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-bucket.html

カスタムリソース

カスタムリソースとして Lambda を起動します。
この Lambda では起動時に event['RequestType'] に Create や Delete が含まれているためこれを確認しスタックの削除時の実行する形式にしてあります。
※作成時にも処理を行いたい場合は、event['RequestType'] == 'Create' を指定してコードを書きます

削除時に実行する動作としてはオブジェクトの削除となり、その際にバージョニングが有効化されている場合は全てのバージョンを削除する処理としています。

また、カスタムリソースでは正常に処理が完了したことをスタックに伝えるためにレスポンスを行う必要があります。今回はシンプルなコードなため、cfnresponse を使って成功/失敗のレスポンスを返すようにしています。
もしもう少し複雑な処理であったり、カスタムリソースで行った処理のデータをスタックに渡したい場合、Dict 形式でレスポンスに含めることでスタック内で !GetAtt を使うことで取得、利用することが可能です。

  CleanupBucketOnDelete:
    Type: Custom::cleanupBucket
    Properties:
      ServiceToken: !GetAtt CleanBucketFunction.Arn
    DependsOn: S3Bucket

  CleanBucketFunction:
    Type: AWS::Lambda::Function
    Properties:
      Description: Cleans out Bucket during delete
      Handler: index.handler
      Runtime: python3.9
      Role: !GetAtt CleanBucketRole.Arn
      Timeout: 900
      Environment:
        Variables: 
          BUCKET_NAME: !Ref S3Bucket
      Code:
        ZipFile: |
          import cfnresponse
          import logging
          import boto3
          import time, os
          status = cfnresponse.SUCCESS
          logger = logging.getLogger(__name__)
          logging.basicConfig(format='%(asctime)s %(message)s',level=logging.DEBUG)

          def handler(event, context):
              logger.debug(event)

              if event['RequestType'] == 'Delete':
                try:
                  BUCKETNAME = os.environ['BUCKET_NAME']
                  s3 = boto3.resource('s3')
                  time.sleep(90)
                  bucket = s3.Bucket(BUCKETNAME)
                  bucket_versioning = s3.BucketVersioning(BUCKETNAME)
                  if bucket_versioning.status == 'Enabled':  # disabled buclet versioning setting
                    bucket.object_versions.delete()
                  else:
                    bucket.objects.all().delete()
                  cfnresponse.send(event, context, status, {}, None)
                except Exception as e:
                  print(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {}, None)
              else:
                cfnresponse.send(event, context, status, {}, None)

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources.html

IAM

カスタムリソースで実行する Lambda に割り当てる権限を指定します。
S3 バケットを削除する際には、オブジェクトとオブジェクトのバージョニングの削除権とバージョニング状態の確認、バケットのリストの権限を付与します。

  CleanBucketRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: lambda-bucketcleaner
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:DeleteObject
                  - s3:DeleteObjectVersion
                Resource: !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}/*'
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:GetBucketVersioning
                Resource: !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}'

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html

最後に

よく自分で使うので備忘録も兼ねて公開しましたがもし他の方の参考になれば幸いですー

Discussion