Can I force CloudFormation to delete non-empty S3 Bucket?
Amazon Web-ServicesAmazon S3Aws CliAmazon 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.