Skip to content

Instantly share code, notes, and snippets.

@filipeandre
Forked from daaru00/template.yml
Last active November 8, 2024 22:50
Show Gist options
  • Save filipeandre/465335b376f9a83a9a2a3e4854acda22 to your computer and use it in GitHub Desktop.
Save filipeandre/465335b376f9a83a9a2a3e4854acda22 to your computer and use it in GitHub Desktop.
A SAM template that describe an Amazon CloudFront distribution that serve a static website from an S3 Bucket.
AWSTemplateFormatVersion: 2010-09-09
Description: "Personal Website"
Parameters:
DomainName:
Type: String
Description: "The domain name of website"
BucketName:
Type: String
Description: "The bucket name"
HostedZoneId:
Type: String
Description: "The Route53 hosted zone ID used for the domain"
AcmCertificateArn:
Type: String
Description: "The certificate arn for the domain name provided"
IndexDocument:
Type: String
Description: "The index document"
Default: "index.html"
ErrorDocument:
Type: String
Description: "The error document, ignored in SPA mode"
Default: "404.html"
RewriteMode:
Type: String
Description: "The request rewrite behaviour type"
Default: "STATIC"
AllowedValues:
- STATIC
- SPA
CloudFrontPriceClass:
Type: String
Description: "The price class for CloudFront distribution"
Default: "PriceClass_100"
AllowedValues:
- PriceClass_100
- PriceClass_200
- PriceClass_All
CreateBucket:
Type: String
Description: "Flag to determine if the bucket should be created by this stack"
Default: "true"
AllowedValues:
- "true"
- "false"
Conditions:
IsStaticMode: !Equals [!Ref RewriteMode, "STATIC"]
IsSPAMode: !Equals [!Ref RewriteMode, "SPA"]
ShouldCreateBucket: !Equals [!Ref CreateBucket, "true"]
NotShouldCreateBucket: !Equals [!Ref CreateBucket, "false"]
Resources:
OriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Ref AWS::StackName
Bucket:
Condition: ShouldCreateBucket
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref BucketName
WebsiteConfiguration:
IndexDocument: !Ref IndexDocument
ErrorDocument: !Ref ErrorDocument
AccessControl: Private
BucketPolicy:
Condition: ShouldCreateBucket
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref DomainName
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
CanonicalUser: !GetAtt OriginAccessIdentity.S3CanonicalUserId
Action: "s3:GetObject"
Resource: !Sub "${Bucket.Arn}/*"
BucketPolicyCustomResource:
Condition: NotShouldCreateBucket
Type: Custom::UpdateBucketPolicy
Properties:
ServiceToken: !GetAtt UpdateBucketPolicyFunction.Arn
BucketName: !Ref BucketName
OriginAccessIdentityId: !GetAtt OriginAccessIdentity.S3CanonicalUserId
UpdateBucketPolicyFunction:
Condition: NotShouldCreateBucket
Type: AWS::Lambda::Function
Properties:
Runtime: python3.11
Handler: index.handler
Code:
ZipFile: |
import json
import boto3
import cfnresponse
s3 = boto3.client('s3')
def handler(event, context):
request_type = event['RequestType'] # Get the request type (Create, Update, Delete)
bucket_name = event['ResourceProperties']['BucketName']
origin_access_identity_id = event['ResourceProperties']['OriginAccessIdentityId']
# Handle creation and update
if request_type in ['Create', 'Update']:
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"CanonicalUser": origin_access_identity_id
},
"Action": "s3:GetObject",
"Resource": f"arn:aws:s3:::{bucket_name}/*"
}
]
}
try:
# Apply the bucket policy
s3.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(policy))
# Return success response to CloudFormation
cfnresponse.send(event, context, cfnresponse.SUCCESS,
{'PhysicalResourceId': bucket_name},
"Bucket policy updated successfully")
except Exception as e:
# Return failure response to CloudFormation
cfnresponse.send(event, context, cfnresponse.FAILED,
{'Reason': str(e)},
"Failed to update bucket policy")
# Handle delete request
elif request_type == 'Delete':
# Return success response to CloudFormation on delete
cfnresponse.send(event, context, cfnresponse.SUCCESS,
{'PhysicalResourceId': bucket_name},
"Bucket policy deleted successfully")
Role: !GetAtt UpdateBucketPolicyRole.Arn
UpdateBucketPolicyRole:
Condition: NotShouldCreateBucket
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: UpdateBucketPolicyPermissions
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:PutBucketPolicy
Resource: !Sub "arn:aws:s3:::${BucketName}"
DnsRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref HostedZoneId
Name: !Ref DomainName
Type: A
AliasTarget:
DNSName: !GetAtt Distribution.DomainName
HostedZoneId: "Z2FDTNDATAQYW2" # CloudFront
WWWDnsRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref HostedZoneId
Name: !Sub "www.${DomainName}"
Type: A
AliasTarget:
DNSName: !GetAtt Distribution.DomainName
HostedZoneId: "Z2FDTNDATAQYW2" # CloudFront
RewriteRequestStaticFunction:
Condition: IsStaticMode
Type: AWS::CloudFront::Function
Properties:
Name: !Sub "${AWS::StackName}-req-static2"
AutoPublish: true
FunctionCode: !Sub |
function handler(event) {
const request = event.request;
const headers = request.headers;
const uri = request.uri;
// Redirect non-www to www
if (headers.host && !headers.host.value.startsWith('www.')) {
const host = 'www.' + headers.host.value; // Add 'www.'
const location = 'https://' + host + uri;
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
'location': { 'value': location },
'strict-transport-security': { 'value': 'max-age=31536000; includeSubDomains; preload' },
'x-content-type-options': { 'value': 'nosniff' },
'x-frame-options': { 'value': 'DENY' },
'x-xss-protection': { 'value': '1; mode=block' }
}
};
}
// Redirect to /index.html for directories
if (uri.endsWith('/') || !uri.includes('.')) {
request.uri = uri.replace(/\/?$/, '/index.html');
}
return request;
}
FunctionConfig:
Comment: !Sub "rewrite all paths to /${IndexDocument}"
Runtime: cloudfront-js-2.0
RewriteRequestSpaFunction:
Condition: IsSPAMode
Type: AWS::CloudFront::Function
Properties:
Name: !Sub "${AWS::StackName}-req-spa2"
AutoPublish: true
FunctionCode: !Sub |
function handler(event) {
const request = event.request;
const headers = request.headers;
const uri = request.uri;
// Redirect non-www to www
if (headers.host && !headers.host.value.startsWith('www.')) {
const host = 'www.' + headers.host.value; // Add 'www.'
const location = 'https://' + host + uri;
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
'location': { 'value': location },
'strict-transport-security': { 'value': 'max-age=31536000; includeSubDomains; preload' },
'x-content-type-options': { 'value': 'nosniff' },
'x-frame-options': { 'value': 'DENY' },
'x-xss-protection': { 'value': '1; mode=block' }
}
};
}
if (uri.includes('.') && !uri.endsWith('.html')) {
return request;
}
request.uri = '/${IndexDocument}';
return request;
}
FunctionConfig:
Comment: !Sub "rewrite sub-directory path with trailing /${IndexDocument}"
Runtime: cloudfront-js-2.0
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: 'true'
Comment: !Ref AWS::StackName
DefaultRootObject: !Ref IndexDocument
HttpVersion: http2
CustomErrorResponses:
- ErrorCachingMinTTL: 86400
ErrorCode: 403 # object not found in bucket
ResponseCode: !If [IsStaticMode, 404, 200]
ResponsePagePath: !If [IsStaticMode, !Sub "/${ErrorDocument}", !Sub "/${IndexDocument}"]
Origins:
- DomainName: !Sub "${BucketName}.s3.${AWS::Region}.amazonaws.com"
Id: bucketOrigin
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${OriginAccessIdentity}
DefaultCacheBehavior:
Compress: 'true'
AllowedMethods:
- GET
- HEAD
- OPTIONS
TargetOriginId: bucketOrigin
ForwardedValues:
QueryString: 'false'
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
FunctionAssociations:
- EventType: viewer-request
FunctionARN: !If [IsStaticMode, !GetAtt RewriteRequestStaticFunction.FunctionMetadata.FunctionARN, !GetAtt RewriteRequestSpaFunction.FunctionMetadata.FunctionARN]
PriceClass: !Ref CloudFrontPriceClass
Aliases:
- !Ref DomainName
- !Sub "www.${DomainName}"
ViewerCertificate:
AcmCertificateArn: !Ref AcmCertificateArn
SslSupportMethod: sni-only
Outputs:
WebsiteURL:
Description: "The URL of the website"
Value: !Sub "https://www.${DomainName}"
CloudFrontDistributionID:
Description: "CloudFront Distribution ID"
Value: !Ref Distribution
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment