Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
CloudFormation template to deploy a static website to S3. Details at https://www.macksmind.io/aws/2020/07/20/deploy-static-website-to-amazon-s3.html
AWSTemplateFormatVersion: 2010-09-09
Parameters:
GitHubOwner:
Type: String
Description: GitHub repo owner
MinLength: 1
GitHubRepo:
Type: String
Description: GitHub repo name
MinLength: 1
GitHubBranch:
Type: String
Default: master
Description: GitHub branch name
MinLength: 1
GitHubSecret:
Type: String
Description: Reference to AWS Secrets Manager
AllowedPattern: "\\{\\{resolve:secretsmanager:.+:SecretString:.+\\}\\}"
Subdomain:
Type: String
Default: www
Description: Subdomain of URL (optional)
RootDomain:
Type: String
Description: Root domain of URL
IndexPage:
Type: String
Default: index.html
Description: Default page in any directory
MinLength: 1
ErrorPage:
Type: String
Default: 404.html
Description: Page not found path (optional, but highly recommended)
Route53:
Type: String
Description: Automatically configure DNS in Route53?
AllowedValues:
- true
- false
Default: false
RedirectRoot:
Type: String
Description: Redirect RootDomain to Subdomain?
AllowedValues:
- true
- false
Default: false
JekyllEnv:
Type: String
Description: Jekyll build environment
Default: production
AllowedValues:
- production
- development
ChatbotSlackArn:
Type: String
Description: AWS Chatbot Channel ARN (optional)
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: GitHub Configuration
Parameters:
- GitHubOwner
- GitHubRepo
- GitHubBranch
- GitHubSecret
- Label:
default: Website Info
Parameters:
- Subdomain
- RootDomain
- IndexPage
- ErrorPage
- Label:
default: DNS Details
Parameters:
- Route53
- RedirectRoot
Conditions:
HasSubdomain:
Fn::Not:
- Fn::Equals:
- ''
- Ref: Subdomain
HasErrorPage:
Fn::Not:
- Fn::Equals:
- ''
- Ref: ErrorPage
RedirectRoot:
Fn::And:
- Condition: HasSubdomain
- Fn::Equals:
- 'true'
- Ref: RedirectRoot
Route53:
Fn::Equals:
- 'true'
- Ref: Route53
Route53Redirect:
Fn::And:
- Condition: Route53
- Condition: RedirectRoot
Chatbot:
Fn::Not:
- Fn::Equals:
- ''
- Ref: ChatbotSlackArn
Resources:
Certificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName:
Fn::If:
- HasSubdomain
- Fn::Sub: "${Subdomain}.${RootDomain}"
- Ref: RootDomain
DomainValidationOptions:
- DomainName:
Fn::If:
- HasSubdomain
- Fn::Sub: "${Subdomain}.${RootDomain}"
- Ref: RootDomain
ValidationDomain:
Ref: RootDomain
ValidationMethod: DNS
DeployBucket:
Type: AWS::S3::Bucket
Properties:
WebsiteConfiguration:
IndexDocument:
Ref: IndexPage
ErrorDocument:
Fn::If:
- HasErrorPage
- Ref: ErrorPage
- Ref: AWS::NoValue
DeployBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: DeployBucket
PolicyDocument:
Statement:
- Action:
- s3:GetObject
Effect: Allow
Principal: "*"
Resource:
Fn::Sub: "${DeployBucket.Arn}/*"
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
- Fn::If:
- HasSubdomain
- Fn::Sub: "${Subdomain}.${RootDomain}"
- Ref: RootDomain
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
Compress: true
ForwardedValues:
QueryString: false
MinTTL: 86400
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
Enabled: true
Origins:
- DomainName:
Fn::Sub: "${DeployBucket}.s3-website-${AWS::Region}.amazonaws.com"
Id: S3Origin
CustomOriginConfig:
OriginProtocolPolicy: http-only
HttpVersion: http2
PriceClass: PriceClass_100
ViewerCertificate:
AcmCertificateArn:
Ref: Certificate
MinimumProtocolVersion: TLSv1.2_2018
SslSupportMethod: sni-only
DistributionIP4:
Type: AWS::Route53::RecordSet
Condition: Route53
Properties:
AliasTarget:
DNSName:
Fn::GetAtt:
- Distribution
- DomainName
HostedZoneId: Z2FDTNDATAQYW2
HostedZoneName:
Fn::Sub: "${RootDomain}."
Name:
Fn::If:
- HasSubdomain
- Fn::Sub: "${Subdomain}.${RootDomain}"
- Ref: RootDomain
Type: A
DistributionIP6:
Type: AWS::Route53::RecordSet
Condition: Route53
Properties:
AliasTarget:
DNSName:
Fn::GetAtt:
- Distribution
- DomainName
HostedZoneId: Z2FDTNDATAQYW2
HostedZoneName:
Fn::Sub: "${RootDomain}."
Name:
Fn::If:
- HasSubdomain
- Fn::Sub: "${Subdomain}.${RootDomain}"
- Ref: RootDomain
Type: AAAA
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
ArtifactStore:
Location:
Ref: PipelineBucket
Type: S3
RoleArn:
Fn::GetAtt:
- PipelineRole
- Arn
Stages:
- Actions:
- ActionTypeId:
Category: Source
Owner: ThirdParty
Provider: GitHub
Version: 1
Configuration:
Owner:
Ref: GitHubOwner
Repo:
Ref: GitHubRepo
Branch:
Ref: GitHubBranch
OAuthToken:
Ref: GitHubSecret
PollForSourceChanges: false
Name: GitHubCommit
OutputArtifacts:
- Name: SourcePipe
Name: Source
- Actions:
- Name: JekyllDeploy
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
Configuration:
ProjectName:
Ref: Project
InputArtifacts:
- Name: SourcePipe
Name: Build
PipelineBucket:
Type: AWS::S3::Bucket
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
PipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Version: 2012-10-17
PipelineRoleDefaultPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Statement:
- Action:
- s3:GetObject*
- s3:GetBucket*
- s3:List*
- s3:DeleteObject*
- s3:PutObject*
- s3:Abort*
Effect: Allow
Resource:
- Fn::GetAtt:
- PipelineBucket
- Arn
- Fn::Sub: "${PipelineBucket.Arn}/*"
- Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
- codebuild:StopBuild
Effect: Allow
Resource:
Fn::GetAtt:
- Project
- Arn
Version: 2012-10-17
PolicyName: PipelineRoleDefaultPolicy
Roles:
- Ref: PipelineRole
PipelineWebhook:
Type: AWS::CodePipeline::Webhook
Properties:
Authentication: GITHUB_HMAC
AuthenticationConfiguration:
SecretToken:
Ref: GitHubSecret
Filters:
- JsonPath: "$.ref"
MatchEquals: refs/heads/{Branch}
TargetAction: GitHubCommit
TargetPipeline:
Ref: Pipeline
TargetPipelineVersion:
Fn::GetAtt:
- Pipeline
- Version
RegisterWithThirdParty: true
Project:
Type: AWS::CodeBuild::Project
Properties:
Name:
Ref: AWS::StackName
Artifacts:
Type: CODEPIPELINE
Cache:
Location:
Fn::Sub: "${PipelineBucket}/build_cache"
Type: S3
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:4.0
Type: LINUX_CONTAINER
ServiceRole:
Fn::GetAtt:
- ProjectRole
- Arn
Source:
BuildSpec:
Fn::Sub: |
version: 0.2
phases:
install:
runtime-versions:
ruby: 2.7
commands:
- mkdir -p _bundle_cache
- bundle config set path _bundle_cache
- bundle config set without 'development test'
- bundle install
build:
commands:
- JEKYLL_ENV=${JekyllEnv} bundle exec jekyll build
post_build:
commands:
- aws s3 sync --cache-control='no-cache' _site s3://${DeployBucket}/ --delete
- aws cloudfront create-invalidation --distribution-id ${Distribution} --paths '/*'
cache:
paths:
- '_bundle_cache/**/*'
Type: CODEPIPELINE
ProjectRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Version: 2012-10-17
ProjectRoleDefaultPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Statement:
- Action:
- s3:GetObject*
- s3:GetBucket*
- s3:List*
- s3:DeleteObject*
- s3:PutObject*
- s3:Abort*
Effect: Allow
Resource:
- Fn::GetAtt:
- PipelineBucket
- Arn
- Fn::Sub: "${PipelineBucket.Arn}/*"
- Action:
- s3:List*
- s3:DeleteObject*
- s3:PutObject*
- s3:Abort*
Effect: Allow
Resource:
- Fn::GetAtt:
- DeployBucket
- Arn
- Fn::Sub: "${DeployBucket.Arn}/*"
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource:
- Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${Project}
- Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${Project}:*
- Action: cloudfront:CreateInvalidation
Effect: Allow
Resource:
Fn::Sub: arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}
Version: 2012-10-17
PolicyName: ProjectRoleDefaultPolicy
Roles:
- Ref: ProjectRole
ProjectNotification:
Type: AWS::CodeStarNotifications::NotificationRule
Condition: Chatbot
Properties:
DetailType: FULL
EventTypeIds:
- codebuild-project-build-state-failed
- codebuild-project-build-state-succeeded
- codebuild-project-build-state-in-progress
- codebuild-project-build-state-stopped
Name:
Fn::Sub: "${AWS::StackName}-notification"
Resource:
Fn::GetAtt:
- Project
- Arn
Targets:
- TargetAddress:
Ref: ChatbotSlackArn
TargetType: AWSChatbotSlack
CertificateRedirect:
Type: AWS::CertificateManager::Certificate
Condition: RedirectRoot
Properties:
DomainName:
Ref: RootDomain
DomainValidationOptions:
- DomainName:
Ref: RootDomain
ValidationDomain:
Ref: RootDomain
ValidationMethod: DNS
DeployBucketRedirect:
Type: AWS::S3::Bucket
Condition: RedirectRoot
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
WebsiteConfiguration:
RedirectAllRequestsTo:
HostName:
Fn::Sub: "${Subdomain}.${RootDomain}"
Protocol: https
DistributionRedirect:
Type: AWS::CloudFront::Distribution
Condition: RedirectRoot
Properties:
DistributionConfig:
Aliases:
- Ref: RootDomain
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
ForwardedValues:
QueryString: false
TargetOriginId: S3OriginRedirect
ViewerProtocolPolicy: allow-all
Enabled: true
Origins:
- DomainName:
Fn::Sub: "${DeployBucketRedirect}.s3-website-${AWS::Region}.amazonaws.com"
Id: S3OriginRedirect
CustomOriginConfig:
OriginProtocolPolicy: http-only
HttpVersion: http2
PriceClass: PriceClass_100
ViewerCertificate:
AcmCertificateArn:
Ref: CertificateRedirect
MinimumProtocolVersion: TLSv1.2_2018
SslSupportMethod: sni-only
DistributionIP4Redirect:
Type: AWS::Route53::RecordSet
Condition: Route53Redirect
Properties:
AliasTarget:
DNSName:
Fn::GetAtt:
- DistributionRedirect
- DomainName
HostedZoneId: Z2FDTNDATAQYW2
HostedZoneName:
Fn::Sub: "${RootDomain}."
Name:
Ref: RootDomain
Type: A
DistributionIP6Redirect:
Type: AWS::Route53::RecordSet
Condition: Route53Redirect
Properties:
AliasTarget:
DNSName:
Fn::GetAtt:
- DistributionRedirect
- DomainName
HostedZoneId: Z2FDTNDATAQYW2
HostedZoneName:
Fn::Sub: "${RootDomain}."
Name:
Ref: RootDomain
Type: AAAA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment