Use a template to dynamically create and save a file to an S3 bucket
In the following AWS CloudFormation template, it creates a public Amazon S3 bucket to host a web site, files for each of the web pages, and a custom Lambda function that saves those files to that S3 bucket.
Create the following template in your working directly called template.yaml
Resources:
# Create a bucket with random name
S3Bucket:
Type: AWS::S3::Bucket
Properties:
WebsiteConfiguration:
ErrorDocument: not-found
IndexDocument: index.html
PublicAccessBlockConfiguration:
BlockPublicAcls: TRUE
BlockPublicPolicy: FALSE
IgnorePublicAcls: TRUE
RestrictPublicBuckets: FALSE
# Make bucket open to the web
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Statement:
- Effect: Allow
Principal: "*"
Action:
- "s3:GetObject"
Resource:
- !Sub "${S3Bucket.Arn}/*"
# Add a file for the home page
S3ContentHome:
Type: Custom::Lambda
Properties:
ServiceToken: !GetAtt S3ContentCustomResource.Arn
BucketName: !Ref S3Bucket
Key: index.html
ContentType: "text/html"
Body: |
<!DOCTYPE html>
<html>
<head>
<title>Home!</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<h1>Home!</h1>
<a href="/">Home</a> | <a href="about-us">About Us</a>
</body>
</html>
# Add a file for the about-us page
S3ContentAboutUs:
Type: Custom::Lambda
Properties:
ServiceToken: !GetAtt S3ContentCustomResource.Arn
BucketName: !Ref S3Bucket
Key: about-us
ContentType: "text/html"
Body: |
<!DOCTYPE html>
<html>
<head>
<title>About Us!</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<h1>About Us!</h1>
<a href="/">Home</a> | <a href="about-us">About Us</a>
</body>
</html>
# Add a file for the 404 page
S3ContentNotFound:
Type: Custom::Lambda
Properties:
ServiceToken: !GetAtt S3ContentCustomResource.Arn
BucketName: !Ref S3Bucket
Key: not-found
ContentType: "text/html"
Body: |
<!DOCTYPE html>
<html>
<head>
<title>Oops, not found!</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<h1>Oops, not found!</h1>
<a href="/">« Go back home</a>
</body>
</html>
# Add favicon
S3ContentFavicon:
Type: Custom::Lambda
Properties:
ServiceToken: !GetAtt S3ContentCustomResource.Arn
BucketName: !Ref S3Bucket
Key: favicon.ico
ContentType: "image/x-icon"
IsBase64Encoded: true
Body: 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
# Custom Lambda function that creates an object in specified S3 bucket and prefix
S3ContentCustomResource:
Type: AWS::Lambda::Function
DependsOn: S3ContentCustomResourceLogGroup
Properties:
Code:
ZipFile: |
const { S3Client, PutObjectCommand, DeleteObjectCommand } = require("@aws-sdk/client-s3");
const s3Client = new S3Client();
exports.handler = async function(event, context) {
console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));
let responseStatus = "FAILED";
let responseData = {};
let physicalResourceId = event.ResourceProperties.Key;
// For Delete requests, delete object.
if (event.RequestType == "Delete") {
console.log(`Deleting s3://${event.ResourceProperties.BucketName}/${event.ResourceProperties.Key}`);
try {
const deleteObjectCommand = new DeleteObjectCommand({
Bucket: event.ResourceProperties.BucketName,
Key: event.ResourceProperties.Key
});
await s3Client.send(deleteObjectCommand);
responseStatus = "SUCCESS";
console.log("Deleted");
} catch (e) {
console.error(`Failed to delete object: ${e.message}`);
}
} else {
const body = typeof event.ResourceProperties.IsBase64Encoded == "string" && event.ResourceProperties.IsBase64Encoded.toLowerCase() == "true" ? Buffer.from(event.ResourceProperties.Body, 'base64') : event.ResourceProperties.Body;
console.log(`Saving s3://${event.ResourceProperties.BucketName}/${event.ResourceProperties.Key}`);
try {
const putObjectCommand = new PutObjectCommand({
Body: body,
Bucket: event.ResourceProperties.BucketName,
Key: event.ResourceProperties.Key,
ContentType: event.ResourceProperties.ContentType
});
await s3Client.send(putObjectCommand);
console.log("Saved");
responseData["BucketName"] = event.ResourceProperties.BucketName;
responseData["Key"] = event.ResourceProperties.Key;
responseData["ContentType"] = event.ResourceProperties.ContentType;
responseStatus = "SUCCESS";
} catch (e) {
console.log(`Could not save to S3: ${e.message}`);
}
}
return await sendResponse(event, context, responseStatus, responseData, physicalResourceId);
};
// Send response to the pre-signed S3 URL
const sendResponse = async function(event, context, responseStatus, responseData, physicalResourceId) {
let responseBody = JSON.stringify({
Status: responseStatus,
Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
});
console.log("RESPONSE BODY:\n", responseBody);
await sendRequest(event.ResponseURL, {
method: "PUT",
body: responseBody
})
};
// Web request
const sendRequest = async function(url, opt) {
opt = opt ? opt : {};
const parsedUrl = require("url").parse(url);
let headers = opt.headers ? opt.headers : {};
headers["Content-length"] = opt.body ? opt.body.length : 0;
const options = {
hostname: parsedUrl.hostname,
port: opt.port ? opt.port : (parsedUrl.protocol == "https:" ? 443 : 80),
path: parsedUrl.path,
method: opt.method ? opt.method : "GET",
headers: headers
};
let response = await new Promise(function(res, err) {
let request = require(parsedUrl.protocol == "https:" ? "https" : "http").request(options, function(response) {
let responseText = [];
response.on("data", function(d) {
responseText.push(d);
});
response.on("end", function() {
response.responseText = responseText.join("");
res(response);
});
});
request.on("error", function(error) {
console.error("sendRequest Error: " + error);
err(error);
});
request.write(opt.body ? opt.body : "");
request.end();
});
return response;
};
FunctionName: !Sub
- "S3ContentCustomResource-${id}"
- id: !Select
- 0
- !Split
- "-"
- !Select
- 2
- !Split
- "/"
- !Ref AWS::StackId
Handler: index.handler
Role: !GetAtt S3ContentCustomResourceRole.Arn
Runtime: nodejs20.x
# Role for custom Lambda function to log activity and put/delete objects in S3 bucket created in this template
S3ContentCustomResourceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: LambdaExecute
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource:
- !Sub
- "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/S3ContentCustomResource-${id}*"
- id: !Select
- 0
- !Split
- "-"
- !Select
- 2
- !Split
- "/"
- !Ref AWS::StackId
- Effect: Allow
Action:
- "s3:PutObject"
- "s3:DeleteObject"
Resource: !Sub "${S3Bucket.Arn}/*"
# Log group for Lambda function
S3ContentCustomResourceLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub
- "/aws/lambda/S3ContentCustomResource-${id}"
- id: !Select
- 0
- !Split
- "-"
- !Select
- 2
- !Split
- "/"
- !Ref AWS::StackId
RetentionInDays: 7
Outputs:
# URL of S3 bucket open to the web
HomeUrl:
Value: !Sub "http://${S3Bucket}.s3-website-${AWS::Region}.amazonaws.com"
To deploy
aws cloudformation create-stack \
--region us-east-1 \
--stack-name test-s3-content \
--template-body file://template.yaml \
--capabilities CAPABILITY_IAM;
To get URL to visit
aws cloudformation describe-stacks \
--region us-east-1 \
--stack-name test-s3-content \
--query 'Stacks[0].Outputs[0].OutputValue' \
--output text;
To delete
aws cloudformation delete-stack \
--region us-east-1 \
--stack-name test-s3-content;