Deploy a static Blog quickly on AWS
Posted by Chris McKinnel - 13 November 20206 minute read
Today I found myself in the position of needing to rapidly spin up a personal blog site where I can post some of the cool stuff I’ve been involved in using AWS in 2020.
I wanted something that:
- Was very low cost (or free)
- Served at the edge around the world using a CDN
- Served using TLS (without having to pay for a certificate – it’s 2020 after all)
- Didn’t need any servers to feed and water
- Could be deployed by just pushing to source control
I was torn between just going to wordpress.com and clicking through the wizard to get a blog and whipping something up on AWS. One of the things I often tell my customers is just spend a little bit of extra time up front and it’ll pay dividends down the track – so today I’m eating my own dogfood and I’m going to spend a little more time up front to get a blog deployed on AWS using CloudFront and S3, orchestrated using CodePipeline and CodeBuild and all defined with Infrastructure as Code.
Architecture
First thing’s first – what is the architecture going to look like?
Site components
CloudFront serving our website from S3 will be fully serverless.
Deployment pipeline components
We'll use CodePipeline and CodeBuild to automatically deploy changes to our blog site.
This should cover our requirements so far:
- Fully serverless – nails requirements #1 and #4
- CloudFront – covers #2
- Certificate Manager – covers #3
- CodePipeline and CodeBuild – covers #5
Sweet! So what do we start with? My preference has always been to get the CI / CD stuff deployed first, so as you’re developing you can just push to source control with every iteration and it automatically gets built.
Unfortunately, in a lot of software (and infrastructure) projects this step gets skipped because you’re desperate to get something working – often to prove that it’s even possible – and you end up losing a bunch of time re-deploying, refactoring and automating what you’ve done so far. I’ve deployed many, many projects and I still make this mistake from time to time.
Not today, though!
First, let’s define a simple CloudFormation template as a placeholder for our blog infrastructure:
infra.yml
Resources:
TemporaryBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
We’ll replace the contents of this template with our website infrastructure once our CI / CD pipeline is successfully running and launching this template for us.
Next, let’s define a simple CodePipeline that uses GitHub as a source and deploys our infra.yml template.
pipeline.yml
Parameters:
GitHubOwner:
Type: String
Description: The username or org that owns the repo
GitHubRepo:
Type: String
Description: The name of the repo as it appears in Github
GitHubBranch:
Type: String
Description: The branch to pull
Default: master
GitHubTokenSecretName:
Type: String
Description: The name of the Secrets Manager secret that contains a GitHub personal access token
Default: Github
GitHubTokenSecretKeyName:
Type: String
Description: The JSON key name of the Secrets Manager value
Default: PersonalAccessToken
Resources:
PipelineBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Pipeline:
Type: AWS::CodePipeline::Pipeline
DependsOn: CodePipelineRole
Properties:
Name: !Ref AWS::StackName
ArtifactStore:
Type: S3
Location: !Ref PipelineBucket
RestartExecutionOnUpdate: true
RoleArn: !GetAtt CodePipelineRole.Arn
Stages:
- Name: Source
Actions:
- Name: GitHub
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
OutputArtifacts:
- Name: source
Configuration:
Owner: !Ref GitHubOwner
Repo: !Ref GitHubRepo
Branch: !Ref GitHubBranch
OAuthToken: !Sub '{{resolve:secretsmanager:${GitHubTokenSecretName}:SecretString:${GitHubTokenSecretKeyName}}}'
RunOrder: 1
- Name: Upload
Actions:
- Name: Site
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
InputArtifacts:
- Name: source
Configuration:
ProjectName: !Ref CodeBuildProject
RunOrder: 1
- Name: Deploy
Actions:
- Name: Cloudfront
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
InputArtifacts:
- Name: source
Configuration:
ActionMode: REPLACE_ON_FAILURE
StackName: !Sub ${AWS::StackName}-infra
RoleArn: !GetAtt CodePipelineRole.Arn
Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
TemplatePath: source::infra.yml
RunOrder: 1
CodeBuildProject:
Type: AWS::CodeBuild::Project
DependsOn: CodePipelineRole
Properties:
Name: !Ref AWS::StackName
ServiceRole: !GetAtt CodeBuildRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:4.0
Source:
Type: CODEPIPELINE
BuildSpec:
!Sub |
version: 0.2
phases:
build:
commands:
- echo "fake build done!"
CodePipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
- cloudformation.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
CodeBuildRole:
Type: AWS::IAM::Role
DependsOn: CodePipelineRole
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
- arn:aws:iam::aws:policy/AmazonS3FullAccess
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
We're using Secrets Manager to store our GitHub personal access token.
Note: we’re giving our CodePipelineRole administrator access here, which is a bit loose. We should probably spend the time and figure out exactly what permissions our pipeline needs instead of being lazy and just granting administrator access. This wouldn’t fly if I was doing this for a customer, so it shouldn’t fly writing public blog posts.
We'll also need to define some parameters that we pass into our CloudFormation template:
cfn-parameters.json
[
{
"ParameterKey": "GitHubOwner",
"ParameterValue": "chrismckinnel"
},
{
"ParameterKey": "GitHubRepo",
"ParameterValue": "test-blog-site"
},
{
"ParameterKey": "GitHubBranch",
"ParameterValue": "master"
},
{
"ParameterKey": "GitHubTokenSecretName",
"ParameterValue": "Github"
},
{
"ParameterKey": "GitHubTokenSecretKeyName",
"ParameterValue": "PersonalAccessToken"
}
]
And finally push our GitHub personal access token secret into secrets manager so our pipeline can fetch it from CloudFormation later.
aws secretsmanager create-secret --name Github --description "Personal access token for GitHub" --secret-string "{\"PersonalAccessToken\":\"xxxxxxxxx\"}" --region us-east-1
{
"VersionId": "cc28f99a-406c-4643-84ce-xxx",
"Name": "Github",
"ARN": "arn:aws:secretsmanager:ap-southeast-2:xxx:secret:Github-dLVxxx"
}
Create a new git repo and commit and push our code, then we'll deploy the CloudFormation template:
git add .
git commit
[master (root-commit) 7eeaff6] Initial commit
2 files changed, 145 insertions(+)
create mode 100644 infra.yml
create mode 100644 pipeline.yml
git push origin master
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 2 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 1.44 KiB | 1.44 MiB/s, done.
Total 4 (delta 0), reused 0 (delta 0)
To github.com:chrismckinnel/test-blog-site.git
* [new branch] master -> master
aws cloudformation create-stack --stack-name blog-pipeline --template-body file://pipeline.yml -parameters file://cfn-parameters.json --capabilities CAPABILITY_IAM --region us-east-1
{
"StackId": "arn:aws:cloudformation:us-east-1:046671368452:stack/blog-pipeline/b5b3ba90-4186-11eb-98ff-0ab512dce13f"
}
Note: we’re deploying this stack into North Virginia mainly because our ACM certificate has to live there to work with CloudFormation. Alternatively, we could deploy the certificate separately and deploy our pipeline and infrastructure in our region of choice, but the path of least resistance is to just chuck it all in North Virginia.
We can check the status of our CloudFormation stack using the API:
aws cloudformation describe-stacks --stack-name blog-pipeline --region us-east-1 | jq '.Stacks[0].StackStatus'
"CREATE_COMPLETE"
This should have deployed a CloudFormation stack that deployed our CI / CD pipeline. It’s pretty basic at the moment – CodeBuild, the step that will end up building our website source, just echoes a message and completes successfully.
The deploy step in the pipeline will have created another CloudFormation
stack called blog-pipeline-infra
which deploys the infra.yml
template we defined at the beginning of the post. We’ll update this later
to deploy our website with CloudFront.
So, we’ve got a CI / CD pipeline!
One of the cool things you can do with CodePipeline is configure it so it updates its parent CloudFormation template, making it self-updating. This means you’re able to just push updates to the CodePipeline configuration to source control and it will take care of the rest. Without the self-update step you’d need to run a `cloudformation update-stack` on it every time you wanted to make a change (like adding a stage, changing configuration, updating parameters, etc).
Shout-out to Rupert Byrant-Green for showing me how to do this! And making his code public - a lot of this blog post's CloudFormation is "borrowed" from him.
A future blog post will go into more detail on this self-update step, but for now let’s add it in and update our stack.
pipeline.yml
Parameters:
GitHubOwner:
Type: String
Description: The username or org that owns the repo
GitHubRepo:
Type: String
Description: The name of the repo as it appears in Github
GitHubBranch:
Type: String
Description: The branch to pull
Default: master
GitHubTokenSecretName:
Type: String
Description: The name of the Secrets Manager secret that contains a GitHub personal access token
Default: Github
GitHubTokenSecretKeyName:
Type: String
Description: The JSON key name of the Secrets Manager value
Default: PersonalAccessToken
Resources:
PipelineBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Pipeline:
Type: AWS::CodePipeline::Pipeline
DependsOn: CodePipelineRole
Properties:
Name: !Ref AWS::StackName
ArtifactStore:
Type: S3
Location: !Ref PipelineBucket
RestartExecutionOnUpdate: true
RoleArn: !GetAtt CodePipelineRole.Arn
Stages:
- Name: Source
Actions:
- Name: GitHub
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
OutputArtifacts:
- Name: source
Configuration:
Owner: !Ref GitHubOwner
Repo: !Ref GitHubRepo
Branch: !Ref GitHubBranch
OAuthToken: !Sub '{{resolve:secretsmanager:${GitHubTokenSecretName}:SecretString:${GitHubTokenSecretKeyName}}}'
RunOrder: 1
- Name: Update
Actions:
- Name: Pipeline
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
InputArtifacts:
- Name: source
Configuration:
ActionMode: REPLACE_ON_FAILURE
StackName: !Ref AWS::StackName
RoleArn: !GetAtt CodePipelineRole.Arn
Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
TemplatePath: source::pipeline.yml
TemplateConfiguration: source::codepipeline-parameters.json
RunOrder: 1
- Name: Upload
Actions:
- Name: Site
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
InputArtifacts:
- Name: source
Configuration:
ProjectName: !Ref CodeBuildProject
RunOrder: 1
- Name: Deploy
Actions:
- Name: Cloudfront
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
InputArtifacts:
- Name: source
Configuration:
ActionMode: REPLACE_ON_FAILURE
StackName: !Sub ${AWS::StackName}-infra
RoleArn: !GetAtt CodePipelineRole.Arn
Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
TemplatePath: source::infra.yml
RunOrder: 1
CodeBuildProject:
Type: AWS::CodeBuild::Project
DependsOn: CodePipelineRole
Properties:
Name: !Ref AWS::StackName
ServiceRole: !GetAtt CodeBuildRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:4.0
Source:
Type: CODEPIPELINE
BuildSpec:
!Sub |
version: 0.2
phases:
build:
commands:
- echo "fake build done!"
CodePipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
- cloudformation.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
CodeBuildRole:
Type: AWS::IAM::Role
DependsOn: CodePipelineRole
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
- arn:aws:iam::aws:policy/AmazonS3FullAccess
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
We also need to create a new file for our parameters in the format that CodePipeline expects (why are they a different format?? AWS devs owe me a couple of hours for that one).
codepipeline-parameters.json
{
"Parameters": {
"GitHubOwner": "chrismckinnel",
"GitHubRepo": "test-blog-site",
"GitHubBranch": "master",
"GitHubTokenSecretName": "Github",
"GitHubTokenSecretKeyName": "PersonalAccessToken"
}
}
Let's commit and push the change above, and update our CloudFormation stack.
git add .
git commit
[master b61e330] Add update step
1 file changed, 23 insertions(+)
git push origin master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 443 bytes | 443.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:chrismckinnel/test-blog-site.git
94c6d3e..b61e330 master -> master
aws cloudformation update-stack --stack-name blog-pipeline --template-body file://pipeline.yml --parameters file://cfn-parameters.json --capabilities CAPABILITY_IAM --region us-east-1
{
"StackId": "arn:aws:cloudformation:us-east-1:046671368452:stack/blog-pipeline/b6f2c2e0-4189-11eb-afef-xxxxxx"
}
Now you should have a self-update step in your pipeline:
Awesome! Now all we need to do to update our blog site is commit and push to source control and our pipeline will take care of the rest for us.
Let’s add in some parameters to pipeline.yml
and see if they end up deploying themselves.
pipeline.yml
Parameters:
GitHubOwner:
Type: String
Description: The username or org that owns the repo
GitHubRepo:
Type: String
Description: The name of the repo as it appears in Github
GitHubBranch:
Type: String
Description: The branch to pull
Default: master
GitHubTokenSecretName:
Type: String
Description: The name of the Secrets Manager secret that contains a GitHub personal access token
Default: Github
GitHubTokenSecretKeyName:
Type: String
Description: The JSON key name of the Secrets Manager value
Default: PersonalAccessToken
SiteDomainName:
Type: String
Description: Domain name for site e.g. mckinnel.me (no www)
HostedZoneId:
Type: String
Description: Existing HostedZone's ID
WebsiteSourceDirectory:
Type: String
Description: The directory name in the repo where the website source is
Default: dist
Resources:
PipelineBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Pipeline:
Type: AWS::CodePipeline::Pipeline
DependsOn: CodePipelineRole
Properties:
Name: !Ref AWS::StackName
ArtifactStore:
Type: S3
Location: !Ref PipelineBucket
RestartExecutionOnUpdate: true
RoleArn: !GetAtt CodePipelineRole.Arn
Stages:
- Name: Source
Actions:
- Name: GitHub
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
OutputArtifacts:
- Name: source
Configuration:
Owner: !Ref GitHubOwner
Repo: !Ref GitHubRepo
Branch: !Ref GitHubBranch
OAuthToken: !Sub '{{resolve:secretsmanager:${GitHubTokenSecretName}:SecretString:${GitHubTokenSecretKeyName}}}'
RunOrder: 1
- Name: Update
Actions:
- Name: Pipeline
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
InputArtifacts:
- Name: source
Configuration:
ActionMode: REPLACE_ON_FAILURE
StackName: !Ref AWS::StackName
RoleArn: !GetAtt CodePipelineRole.Arn
Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
TemplatePath: source::pipeline.yml
TemplateConfiguration: source::codepipeline-parameters.json
RunOrder: 1
- Name: Upload
Actions:
- Name: Site
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
InputArtifacts:
- Name: source
Configuration:
ProjectName: !Ref CodeBuildProject
RunOrder: 1
- Name: Deploy
Actions:
- Name: Cloudfront
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
InputArtifacts:
- Name: source
Configuration:
ActionMode: REPLACE_ON_FAILURE
StackName: !Sub ${AWS::StackName}-infra
RoleArn: !GetAtt CodePipelineRole.Arn
TemplatePath: source::infra.yml
Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
RunOrder: 1
CodeBuildProject:
Type: AWS::CodeBuild::Project
DependsOn: CodePipelineRole
Properties:
Name: !Ref AWS::StackName
ServiceRole: !GetAtt CodeBuildRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:4.0
Source:
Type: CODEPIPELINE
BuildSpec:
!Sub |
version: 0.2
phases:
build:
commands:
- echo "fake build done!"
CodePipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
- cloudformation.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
CodeBuildRole:
Type: AWS::IAM::Role
DependsOn: CodePipelineRole
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
- arn:aws:iam::aws:policy/AmazonS3FullAccess
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
We'll also need to update these params in cfn-parameters.json
and
codepipeline-parameters.json
.
cfn-parameters.json
[
{
"ParameterKey": "GitHubOwner",
"ParameterValue": "chrismckinnel"
},
{
"ParameterKey": "GitHubRepo",
"ParameterValue": "test-blog-site"
},
{
"ParameterKey": "GitHubBranch",
"ParameterValue": "master"
},
{
"ParameterKey": "GitHubTokenSecretName",
"ParameterValue": "Github"
},
{
"ParameterKey": "GitHubTokenSecretKeyName",
"ParameterValue": "PersonalAccessToken"
},
{
"ParameterKey": "WebsiteSourceDirectory",
"ParameterValue": "dist"
},
{
"ParameterKey": "SiteDomainName",
"ParameterValue": "mckinnel.me"
},
{
"ParameterKey": "HostedZoneId",
"ParameterValue": "xxx"
}
]
codepipeline-parameters.json
{
"Parameters": {
"GitHubOwner": "chrismckinnel",
"GitHubRepo": "test-blog-site",
"GitHubBranch": "master",
"GitHubTokenSecretName": "Github",
"GitHubTokenSecretKeyName": "PersonalAccessToken",
"WebsiteSourceDirectory": "dist",
"SiteDomainName": "mckinnel.me",
"HostedZoneId": "xxx"
}
}
Let's push to source control:
git add .
git commit
[master b61e330] Add parameters
1 file changed, 23 insertions(+)
git push origin master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 443 bytes | 443.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:chrismckinnel/test-blog-site.git
94c6d3e..b61e330 master -> master
Once the pipeline has run through, we can now check the parameters section of the CloudFormation dashboard and see our parameters are now applied to the stack. Great!
The HostedZoneId parameter should be updated with your own Hosted Zone ID.
Now we’re ready to actually deploy our website infrastructure – all we need to do is update infra.yml, make a couple of final changes to pipeline.yml and push it all to source control.
infra.yml
Parameters:
HostedZoneId:
Type: String
Description: Existing HostedZone's ID
ConstraintDescription: Must be an AWS Route53 HostedZoneId
SiteDomainName:
Type: String
Description: Domain name for site e.g. mckinnel.me (no www)
ConstraintDescription: Must be a valid domain name
WebsiteBucket:
Type: String
WebsiteBucketDomainName:
Type: String
Resources:
AcmCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Ref SiteDomainName
SubjectAlternativeNames:
- !Sub www.${SiteDomainName}
ValidationMethod: DNS
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref WebsiteBucket
PolicyDocument:
Statement:
- Sid: CloudFrontAccess
Effect: Allow
Principal:
CanonicalUser: !GetAtt CloudFrontIdentity.S3CanonicalUserId
Action: s3:GetObject
Resource: !Sub arn:aws:s3:::${WebsiteBucket}/*
CloudFrontIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub Identity for ${SiteDomainName}
CloudFrontDistro:
Type: AWS::CloudFront::Distribution
DependsOn:
- CloudFrontIdentity
Properties:
DistributionConfig:
Comment: Cloudfront Distribution pointing to S3 bucket
Origins:
- DomainName: !Ref WebsiteBucketDomainName
Id: S3Origin
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontIdentity}
Enabled: true
HttpVersion: http2
DefaultRootObject: index.html
Aliases:
- !Ref SiteDomainName
- !Sub www.${SiteDomainName}
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
Compress: true
DefaultTTL: 0
MaxTTL: 0
MinTTL: 0
TargetOriginId: S3Origin
ForwardedValues:
QueryString: true
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
ViewerCertificate:
AcmCertificateArn: !Ref AcmCertificate
SslSupportMethod: sni-only
WebsiteDNSName:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: !Ref HostedZoneId
RecordSets:
- Name: !Sub ${SiteDomainName}.
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt CloudFrontDistro.DomainName
- Name: !Sub www.${SiteDomainName}.
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt CloudFrontDistro.DomainName
pipeline.yml
Parameters:
Parameters:
GitHubOwner:
Type: String
Description: The username or org that owns the repo
GitHubRepo:
Type: String
Description: The name of the repo as it appears in Github
GitHubBranch:
Type: String
Description: The branch to pull
Default: master
GitHubTokenSecretName:
Type: String
Description: The name of the Secrets Manager secret that contains a GitHub personal access token
Default: Github
GitHubTokenSecretKeyName:
Type: String
Description: The JSON key name of the Secrets Manager value
Default: PersonalAccessToken
SiteDomainName:
Type: String
Description: Domain name for site e.g. mckinnel.me (no www)
ConstraintDescription: Must be a valid domain name
HostedZoneId:
Type: String
Description: Existing HostedZone's ID
ConstraintDescription: Must be an AWS Route53 HostedZoneId
WebsiteSourceDirectory:
Type: String
Description: The directory name in the repo where the website source is
Default: src
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
PipelineBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Pipeline:
Type: AWS::CodePipeline::Pipeline
DependsOn: CodePipelineRole
Properties:
Name: !Ref AWS::StackName
ArtifactStore:
Type: S3
Location: !Ref PipelineBucket
RestartExecutionOnUpdate: true
RoleArn: !GetAtt CodePipelineRole.Arn
Stages:
- Name: Source
Actions:
- Name: GitHub
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
OutputArtifacts:
- Name: source
Configuration:
Owner: !Ref GitHubOwner
Repo: !Ref GitHubRepo
Branch: !Ref GitHubBranch
OAuthToken: !Sub '{{resolve:secretsmanager:${GitHubTokenSecretName}:SecretString:${GitHubTokenSecretKeyName}}}'
RunOrder: 1
- Name: Update
Actions:
- Name: Pipeline
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
InputArtifacts:
- Name: source
Configuration:
ActionMode: REPLACE_ON_FAILURE
StackName: !Ref AWS::StackName
RoleArn: !GetAtt CodePipelineRole.Arn
TemplatePath: source::pipeline.yml
Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
ParameterOverrides: !Sub |
{
"SiteDomainName": "${SiteDomainName}",
"GitHubOwner": "${GitHubOwner}",
"GitHubRepo": "${GitHubRepo}",
"GitHubBranch": "${GitHubBranch}",
"HostedZoneId": "${HostedZoneId}",
"WebsiteSourceDirectory": "${WebsiteSourceDirectory}"
}
RunOrder: 1
- Name: Upload
Actions:
- Name: Site
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
InputArtifacts:
- Name: source
Configuration:
ProjectName: !Ref CodeBuildProject
RunOrder: 1
- Name: Deploy
Actions:
- Name: Cloudfront
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
InputArtifacts:
- Name: source
Configuration:
ActionMode: REPLACE_ON_FAILURE
StackName: !Sub ${AWS::StackName}-site
RoleArn: !GetAtt CodePipelineRole.Arn
TemplatePath: source::infra.yml
Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
ParameterOverrides: !Sub |
{
"SiteDomainName": "${SiteDomainName}",
"HostedZoneId": "${HostedZoneId}",
"WebsiteBucketDomainName": "${WebsiteBucket.DomainName}",
"WebsiteBucket":" ${WebsiteBucket}"
}
RunOrder: 1
CodeBuildProject:
Type: AWS::CodeBuild::Project
DependsOn: CodePipelineRole
Properties:
Name: !Ref AWS::StackName
ServiceRole: !GetAtt CodeBuildRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:4.0
Source:
Type: CODEPIPELINE
BuildSpec:
!Sub |
version: 0.2
phases:
install:
commands:
- npm install
build:
commands:
- node_modules/.bin/gulp build
post_build:
commands:
- aws s3 sync ${WebsiteSourceDirectory} s3://${WebsiteBucket} --delete
CodePipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
- cloudformation.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
CodeBuildRole:
Type: AWS::IAM::Role
DependsOn: CodePipelineRole
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
- arn:aws:iam::aws:policy/AmazonS3FullAccess
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
Above we have moved the creation of our Website S3 bucket into our pipeline CloudFormation template so we can pass it into our buildspec definition. This means that CodeBuild can upload our static files to S3.
I've chosen to use Start Bootstrap's Clean Blog
for my website, and it uses gulp to build the website files into a dist
directory.
If you've been following along, you'll need to update the buildspec section of your CodeBuild project to suit your own website.
Let's commit our changes to source control.
git add .
git commit
[master b61e330] Add website infra
1 file changed, 23 insertions(+)
git push origin master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 443 bytes | 443.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:chrismckinnel/test-blog-site.git
94c6d3e..b61e330 master -> master
Finally, we can see our website infrastructure deployed!
Now if you want to make changes to your website, you just update your HTML / CSS / JS, push to GitHub and it'll automatically get published.