Can I force CloudFormation to delete non-empty S3 Bucket?

Amazon Web-ServicesAmazon S3Aws Cli

Amazon Web-Services Problem Overview


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

Amazon Web-Services Solutions


Solution 1 - Amazon Web-Services

You can create a lambda function to clean up your bucket and invoke your lambda from your CloudFormation stack using a CustomResource.

Below a lambda example cleaning up your bucket:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import json
import boto3
from botocore.vendored import requests


def lambda_handler(event, context):
    try:
        bucket = event['ResourceProperties']['BucketName']

        if event['RequestType'] == 'Delete':
            s3 = boto3.resource('s3')
            bucket = s3.Bucket(bucket)
            for obj in bucket.objects.filter():
                s3.Object(bucket.name, obj.key).delete()

        sendResponseCfn(event, context, "SUCCESS")
    except Exception as e:
        print(e)
        sendResponseCfn(event, context, "FAILED")


def sendResponseCfn(event, context, responseStatus):
    response_body = {'Status': responseStatus,
                     'Reason': 'Log stream name: ' + context.log_stream_name,
                     'PhysicalResourceId': context.log_stream_name,
                     'StackId': event['StackId'],
                     'RequestId': event['RequestId'],
                     'LogicalResourceId': event['LogicalResourceId'],
                     'Data': json.loads("{}")}

    requests.put(event['ResponseURL'], data=json.dumps(response_body))

After you create the lambda above, just put the CustomResource in your CloudFormation stack:

 ---
 AWSTemplateFormatVersion: '2010-09-09'
 
 Resources:
 
   myBucketResource:
     Type: AWS::S3::Bucket
     Properties:
       BucketName: my-test-bucket-cleaning-on-delete

   cleanupBucketOnDelete:
     Type: Custom::cleanupbucket
     Properties:
       ServiceToken: arn:aws:lambda:eu-west-1:123456789012:function:clean-bucket-lambda
       BucketName: !Ref myBucketResource

> Remember to attach a role to your lambda that has permission to remove objects from your bucket.

Furthermore keep in mind that you can create a lambda function that accepts CLI command line using the lambda function cli2cloudformation. You can download and install from here. Using that you just need to create a CustomResource like bellow:

"removeBucket": {
        "Type": "Custom::cli2cloudformation",
        "Properties": {
          "ServiceToken": "arn:aws:lambda:eu-west-1:123456789000:function:custom-lambda-name",
          "CliCommandDelete": "aws s3 rb s3://bucket-name --force",
        }
}

Solution 2 - Amazon Web-Services

I think your DependsOn is in wrong resource, at least it did not work for me properly because on stack deletion (via console), it would try to force bucket deletion first which will fail and then will attempt to delete custom resource, which triggers the lambda to empty the bucket. This will empty the bucket but the stack deletion will fail because it attempted to delete the bucket before it was empty. We want to initiate custom resource deletion first and then attempt to delete bucket after custom resource is deleted, so I did it like this and it works for me:

myBucketResource:
  Type: AWS::S3::Bucket
  Properties:
    BucketName: my-test-bucket-cleaning-on-delete

cleanupBucketOnDelete:
  Type: Custom::cleanupbucket
  Properties:
    ServiceToken: arn:aws:lambda:eu-west-1:123456789012:function:clean-bucket-lambda
    BucketName: my-test-bucket-cleaning-on-delete
  DependsOn: myBucketResource

This way you ensure the bucket deletion does not come first because there is another resource that depends on it, hence the depending resource is deleted first (which triggeres the lambda to empty the bucket) and then bucket is deleted. Hope someone finds it helpful.

Solution 3 - Amazon Web-Services

You should empty the bucket:

$ aws s3 rm s3://bucket-name --recursive

Then delete the Bucket

$ aws cloudformation delete-stack --stack-name mys3stack

Solution 4 - Amazon Web-Services

botocore.vendored is deprecated and will be removed from Lambda after 2021/01/30.

Here is an update version

Type: AWS::Lambda::Function
Properties:
  Code: 
    ZipFile: 
      !Sub |
        import json, boto3, logging
        import cfnresponse
        logger = logging.getLogger()
        logger.setLevel(logging.INFO)

        def lambda_handler(event, context):
            logger.info("event: {}".format(event))
            try:
                bucket = event['ResourceProperties']['BucketName']
                logger.info("bucket: {}, event['RequestType']: {}".format(bucket,event['RequestType']))
                if event['RequestType'] == 'Delete':
                    s3 = boto3.resource('s3')
                    bucket = s3.Bucket(bucket)
                    for obj in bucket.objects.filter():
                        logger.info("delete obj: {}".format(obj))
                        s3.Object(bucket.name, obj.key).delete()

                sendResponseCfn(event, context, cfnresponse.SUCCESS)
            except Exception as e:
                logger.info("Exception: {}".format(e))
                sendResponseCfn(event, context, cfnresponse.FAILED)

        def sendResponseCfn(event, context, responseStatus):
            responseData = {}
            responseData['Data'] = {}
            cfnresponse.send(event, context, responseStatus, responseData, "CustomResourcePhysicalID")            

  Handler: "index.lambda_handler"
  Runtime: python3.7
  MemorySize: 128
  Timeout: 60
  Role: !GetAtt TSIemptyBucketOnDeleteFunctionRole.Arn    

Solution 5 - Amazon Web-Services

It kind took a little bit to get it to work on python 3.8, so I am sharing it with the community.

Python 3.8 lambda doesn't support any longer from botocore.vendored import requests.

You can use the code below to inform Cloudformation.

import urllib

request = urllib.request.Request(event['ResponseURL'],
                                 method="PUT",
                                 data=json.dumps(response_body).encode('utf-8'),
                                 headers={'Content-Type': "''"})

with urllib.request.urlopen(request) as response:
    print("Status code: " + response.reason)

Another small note: when the lambda receive a create request from cloudformation you can put whatever unique value in the PhysicalResourceId response. When it is an UPDATE/DELETE you also receive that parameter from CloudFormation and you must reuse the value from the input in the response.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJamie CzuyView Question on Stackoverflow
Solution 1 - Amazon Web-ServicesLucio VelosoView Answer on Stackoverflow
Solution 2 - Amazon Web-ServicesOkayWhateverView Answer on Stackoverflow
Solution 3 - Amazon Web-ServicesKlykooView Answer on Stackoverflow
Solution 4 - Amazon Web-ServicesAndrej MayaView Answer on Stackoverflow
Solution 5 - Amazon Web-Servicesuser1237981View Answer on Stackoverflow