CloudFormation Custom Resource - Delete a non-empty S3 Bucket

CloudFormation Custom Resource - Delete a non-empty S3 Bucket

avatar

TrungTin Tran

2024.12.31

Chào mừng mọi người đến với bài blog đầu năm mới 2025 từ CloudMentorPro! Trong bài viết này, chúng ta sẽ giải quyết vấn đề xóa S3 bucket chứa objects gặp lỗi "The bucket you tried to delete is not empty" khi delete stack bằng cách sử dụng CloudFormation Custom Resource kết hợp với Lambda để tự động xóa objects trước khi xóa bucket. Qua đó, bạn sẽ nắm vững cách vận dụng và triển khai Custom Resource, biến những điều CloudFormation native bất khả thi thành có thể, giúp tự động hóa việc quản lý tài nguyên AWS hiệu quả.

Introduction

AWS CloudFormation Custom Resources?

  • AWS CloudFormation Custom Resources là một tính năng nâng cao cho phép mở rộng các khả năng vốn có của CloudFormation thông qua việc tích hợp các logic tùy chỉnh khi các resource types mặc định không đáp ứng được yêu cầu.

Cơ chế hoạt động:

Khi Create/Update/Delete stack:

  1. CloudFormation gặp Custom Resource trong template và dừng stack operation
  2. Custom Resource sẽ gọi tới Lambda/SNS với Request Type tương ứng (Create/Update/Delete) để thực hiện xử lý như mong muốn
  3. Lambda/SNS xử lý xong sẽ trả về response (SUCCESS/FAILED) cho CloudFormation
  4. CloudFormation nhận response và tiếp tục stack operation

Use cases:

  • Tích hợp với non-AWS services:
    • Third-party services
    • Internal APIs của doanh nghiệp
    • External systems
  • Thực thi các operations đặc thù, phức tạp mà CloudFormation native không support:
    • Xử lý vòng lặp phức tạp
    • Custom DNS configurations
    • Xóa non-empty S3 bucket

Lưu ý quan trọng khi sử dụng Custom Resource:

  • Để đảm bảo Custom Resource hoạt động, Lambda function hoặc SNS topic phải được tạo trong cùng region với CloudFormation stack
  • CloudFormation sẽ đợi response từ Lambda/SNS với Default timeout 1 giờ, nếu quá 1 giờ không nhận được response, CloudFormation sẽ coi như FAILED và rollback stack

Lab Introduction

  • AWS experience: Intermediate
  • Time to complete:  30+ minutes
  • AWS Region: US East (N. Virginia) us-east-1
  • Services used: CloudFormation, S3, Lambda, IAM

Architecture

Trong bài lab này, chúng ta sẽ implement một use case phổ biến Lambda-backed Custom Resource: tự động xóa các objects trong S3 bucket trước khi xóa bucket đó. Khi CloudFormation thực hiện DELETE stack, flow xử lý sẽ diễn ra như sau: CloudFormation phát hiện Custom Resource và gửi request DELETE tới Lambda, Lambda function sẽ xóa tất cả objects trong bucket, sau khi nhận được response SUCCESS từ Lambda, CloudFormation sẽ tiếp tục xóa bucket rỗng. Đây là một demo hoàn chỉnh cho thấy sức mạnh của Custom Resource trong việc mở rộng khả năng của CloudFormation.

Task Details

  1. Demo with Template CloudFormation Native
    1. Create CloudFormation template
    2. Deploy CloudFormation Template
    3. Upload Objects to S3 Bucket
    4. Delete stack
    5. Verify kết quả
  2. Demo with Template CloudFormation With Custom Resource
    1. Create CloudFormation Custom Resource template
    2. Deploy CloudFormation Custom Resource Template
    3. Upload Objects to S3 Bucket
    4. Delete stack
    5. Verify kết quả

1.Deploy Template CloudFormation Native

Trước khi vào việc ngay với CloudFormation Custom Resource, mình sẽ demo với CloudFormation Native để thấy được lỗi cũng như hạn chế của nó. Bạn nào đã gặp lỗi này rồi có thể bỏ qua step này và vào việc luôn với CloudFormation Custom Resource nha.

1.1 Create CloudFormation template

  • CloudFormation Template
s3-stack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Demo CloudFormation Native'

Parameters:
  BucketName:
    Type: String
    Default: d-s3-cmp-cleaning-on-delete-bucket
    Description: Name of the S3 bucket to create

Resources:
  # S3 Bucket
  demoBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
  • Mô tả các resource được tạo
  1. S3 Bucket (demoBucket):
    • Bucket Name: Default là d-s3-cmp-cleaning-on-delete-bucket

1.2 Deploy CloudFormation Custom Resource Template

  1. Copy nội dung Template phía trên và lưu lại với tên s3-stack.yaml
  2. Tại CloudFormation Console -> Chọn Menu Stack -> Create stack
  3. Upload file s3-stack.yaml và nhấn Next
  4. Nhập tên Stack: d-cfn-cmp-custom-resource-demo (Chú ý: Stack name không được trùng trong cùng 1 Region).
  5. Next cho đến khi hoàn thành việc tạo Stack
  6. Chúng ta sẽ đợi cho đến khi Resource được tạo xong.

Sau khi đợi vài phút thì Stack đã tạo thành công!

1.3 Upload Objects to S3 Bucket

Sau khi stack được tạo thành công, chúng ta sẽ upload file vào bucket thông qua AWS Console:

  1. Tại CloudFormation Console -> Chọn Menu Resources -> Click chọn url của S3
  2. Tại S3 bucket Console -> Menu Objects -> Click chọn Upload
  3. Add files và nhấn Upload
  4. File đã upload thành công!!

1.4 Delete stack

Bây giờ chúng ta sẽ xóa stack để thấy lỗi khi xóa bucket chứa object

Tại CloudFormation Console -> Menu Resources -> Click Delete

Chờ chút xíu để Cloudformation xóa stack

1.5 verify kết quả

Đúng như dự đoán Cloudformation báo lỗi không xoá được non-empty S3 Bucket

This AWS::S3::Bucket resource is in a DELETE_FAILED state.
Resource handler returned message: "The bucket you tried to delete is not empty

Solution:

  1. Xóa thủ công từ Amazon S3 Console
  2. Sử dụng AWS SDK
  3. Sử dụng Custom Resource

2.Deploy Template CloudFormation With Custom Resource

2.1 Update CloudFormation template With Custom Resource

  • CloudFormation Template With Custom Resource
s3-custom-resource-stack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Demo CloudFormation Custom Resource - delete a non-empty S3 Bucket'

Parameters:
  BucketName:
    Type: String
    Default: d-s3-cmp-cleaning-on-delete-bucket
    Description: Name of the S3 bucket to create

Resources:
  # S3 Bucket
  demoBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName

  # IAM Role for Lambda function - policy 
  cleanupFunctionRole:
    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'
        - 'arn:aws:iam::aws:policy/AmazonS3FullAccess'

  # Lambda function
  cleanupFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: 'index.lambda_handler'
      Role: !GetAtt cleanupFunctionRole.Arn
      Code:
        ZipFile: |
          import json
          import boto3
          import urllib3
          
          def lambda_handler(event, context):
              print(f"Event received: {json.dumps(event)}")
              
              try:
                  if event['RequestType'] == 'Delete':
                      bucket_name = event['ResourceProperties']['BucketName']
                      cleanup_bucket(bucket_name)
                      send_response(event, context, "SUCCESS", {})
                  else:
                      send_response(event, context, "SUCCESS", {})
              except Exception as e:
                  print(f"Error: {str(e)}")
                  send_response(event, context, "FAILED", {})

          def cleanup_bucket(bucket_name):
              s3 = boto3.client('s3')
              try:
                  paginator = s3.get_paginator('list_objects_v2')
                  page_iterator = paginator.paginate(Bucket=bucket_name)
                  
                  for page in page_iterator:
                      if 'Contents' in page:
                          objects = [{'Key': obj['Key']} for obj in page['Contents']]
                          s3.delete_objects(
                              Bucket=bucket_name,
                              Delete={'Objects': objects}
                          )
                          print(f"Deleted {len(objects)} objects")
              except s3.exceptions.NoSuchBucket:
                  print("Bucket already deleted")

          def send_response(event, context, status, data):
              response = {
                  'Status': status,
                  'Reason': f'See CloudWatch Log Stream: {context.log_stream_name}',
                  'PhysicalResourceId': 'CustomResourcePhysicalID',
                  'StackId': event['StackId'],
                  'RequestId': event['RequestId'],
                  'LogicalResourceId': event['LogicalResourceId'],
                  'Data': data
              }
              
              http = urllib3.PoolManager()
              try:
                  http.request(
                      'PUT',
                      event['ResponseURL'],
                      headers={'Content-Type': ''},
                      body=json.dumps(response)
                  )
              except Exception as e:
                  print(f"Failed to send response: {str(e)}")

      Runtime: 'python3.9'
      Timeout: 300
      MemorySize: 128

  # Custom Resources
  cleanupS3BucketOnDelete:
    Type: Custom::S3BucketCleanup
    Properties:
      ServiceToken: !GetAtt cleanupFunction.Arn
      BucketName: !Ref BucketName

  bucketDependency:
    Type: Custom::BucketDependency
    DependsOn: demoBucket
    Properties:
      ServiceToken: !GetAtt cleanupFunction.Arn
      BucketName: !Ref BucketName
  • Mô tả các resource được tạo
  1. S3 Bucket (demoBucket):
    • Bucket Name: Default là d-s3-cmp-cleaning-on-delete-bucket
  2. IAM Role (cleanupFunctionRole):
    • Role cho Lambda function
    • Có 2 managed policies: Lambda basic execution và S3 full access
  3. Lambda Function (cleanupFunction):
    • Runtime: Python 3.9
    • Timeout: 5 phút
    • Memory: 128MB
    • Code xử lý:
      • List objects trong bucket
      • Delete objects
      • Send response về CloudFormation
  4. Custom Resources:
    • cleanupS3BucketOnDelete: Trigger Lambda để cleanup bucket
    • bucketDependency:
      • Đảm bảo bucket tồn tại trước khi cleanup (DependsOn)
      • Điều khiển thứ tự xóa khi DELETE stack
      • Trigger cleanup trước khi xóa bucket
      • Xử lý race condition trong quá trình tạo/xóa resources

2.2 Deploy CloudFormation Template With Custom Resource

  1. Copy Template phía trên và lưu lại với tên s3-custom-resource-stack.yaml
  2. Tại CloudFormation Console -> Chọn Menu Stack -> Create stack
  3. Upload file s3-custom-resource-stack.yaml và nhấn Next
  4. Nhập tên Stack: d-cfn-cmp-custom-resource-demo (Chú ý: Stack name không được trùng trong cùng 1 Region).
  5. Next cho đến khi hoàn thành việc tạo Stack
  6. Chúng ta sẽ đợi cho đến khi Resource được tạo xong.

Sau khi đợi vài phút thì Stack đã tạo thành công!

2.3 Upload Objects to S3 Bucket

Sau khi stack được tạo thành công, chúng ta sẽ upload file vào bucket thông qua AWS Console:

  1. Tại CloudFormation Console -> Chọn Menu Resources -> Click chọn url của S3
  2. Tại S3 bucket Console -> Menu Objects -> Click chọn Upload
  3. Add files và nhấn Upload
  4. File đã upload thành công!!

2.4 Delete Custom Resource stack

Bây giờ chúng ta sẽ xóa stack để xem còn bị lỗi khi xóa non-empty bucket nữa không nha!

Tại CloudFormation Console -> Menu Resources -> Click Delete

Pop-up confirm: Chọn Delete

Chờ chút xíu để Cloudformation xóa stack

2.5 verify kết quả

Quả là "phép thuật" của Custom Resource, đã xoá được non-empty S3 Bucket thành công!!!

  • CloudFormation Console -> Menu Events - updated

  • CloudFormation Console -> Menu Resources

Challenge

Demo Custom Resource lần này lambda chỉ đơn giản là xóa objects trong S3 bucket. Trong thực tế, S3 cleanup phức tạp hơn nhiều với versioning, delete markers, object size... Mọi người có thể level up với các thử thách sau:

  • Mở rộng Lambda function xử lý phức tạp hơn:
    • Xoá versioned objects, Delete Markers
    • Xoá object có kích thước lớn tối ưu nhất
    • Thêm logging chi tiết
  • Thêm các tính năng:
    • Backup objects trước khi xóa
    • Xóa theo điều kiện (prefix, age, size)
    • Thông báo qua SNS khi xóa objects
  • Best practices cho Lambda deployment:
    • Tách Lambda code ra package riêng
    • Upload code lên S3 và reference trong CFN
    • Sử dụng SAM để deploy Lambda

Tổng Kết

Qua bài lab này, chúng ta vừa khám phá:

  • Khả năng "phép thuật" của Custom Resource: nơi CloudFormation và Lambda/SNS tạo nên điều kỳ diệu trong việc xử lý các yêu cầu phức tạp
  • Chinh phục thử thách delete "non-empty S3 bucket": từ mission impossible thành mission completed với Lambda function
  • Nghệ thuật tích hợp Lambda-CloudFormation: như một cặp đôi ăn ý, cùng nhau xử lý mọi thử thách

Kết luận: Custom Resource - "Người hùng thầm lặng" của CloudFormation, biến những điều tưởng chừng bất khả thi thành có thể!

Tài liệu Tham khảo: