The Build stage produced an artifact. Now the Deploy stage needs to get those files into S3 with proper cache headers, invalidate CloudFront, and add security headers. This post covers plain HTML/CSS/JS sites where filenames don’t change between deploys.
Security Headers Policy
Browsers enforce security features through HTTP response headers. CloudFront ResponseHeadersPolicies let you attach these headers at the CDN layer without modifying your application code.
SecurityHeadersPolicy:
Type: 'AWS::CloudFront::ResponseHeadersPolicy'
Properties:
ResponseHeadersPolicyConfig:
Name: !Sub '${AWS::StackName}-SecurityHeaders'
SecurityHeadersConfig:
StrictTransportSecurity:
AccessControlMaxAgeSec: 31536000
IncludeSubdomains: true
Preload: true
Override: true
ContentTypeOptions:
Override: true
FrameOptions:
FrameOption: DENY
Override: true
ReferrerPolicy:
ReferrerPolicy: strict-origin-when-cross-origin
Override: true
XSSProtection:
Protection: true
ModeBlock: true
Override: true
ContentSecurityPolicy:
Override: true
ContentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
Each header serves a specific purpose:
StrictTransportSecurity (HSTS) tells browsers to only connect over HTTPS for one year. IncludeSubdomains extends this to all subdomains. Preload lets you submit the domain to browser HSTS preload lists so the first visit is also HTTPS.
ContentTypeOptions sends X-Content-Type-Options: nosniff, preventing browsers from guessing file types. A CSS file stays a CSS file, even if the content looks like JavaScript.
FrameOptions: DENY prevents your site from being embedded in iframes. This blocks clickjacking attacks where an attacker overlays your site with invisible elements.
ReferrerPolicy: strict-origin-when-cross-origin sends the full URL as a referrer for same-origin requests but only the origin (no path) for cross-origin requests. This prevents leaking internal URL paths to third-party services.
XSSProtection enables the browser’s built-in XSS filter in block mode. Modern browsers handle XSS through CSP, but this provides defense in depth for older browsers.
ContentSecurityPolicy is the most important header. It restricts where the browser can load resources from. For a plain static site, 'self' on every directive means the browser only loads scripts, styles, images, and fonts from your own domain. frame-ancestors 'none' duplicates the iframe protection from FrameOptions. base-uri 'self' prevents <base> tag injection. form-action 'self' restricts form submissions to your domain.
Attach the policy to your CloudFront distribution by adding ResponseHeadersPolicyId to the DefaultCacheBehavior from post 7.
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # AWS Managed CachingOptimized policy
ResponseHeadersPolicyId: !Ref SecurityHeadersPolicy
Compress: true
Deploy Service Role
The deploy project needs its own IAM role with permissions to read artifacts, write to the site bucket, and invalidate CloudFront.
DeployServiceRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: DeployAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${AWS::StackName}-Deploy*'
- Effect: Allow
Action:
- s3:GetObject
- s3:GetBucketVersioning
Resource:
- !Sub '${PipelineArtifactBucket.Arn}/*'
- !GetAtt PipelineArtifactBucket.Arn
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- !GetAtt S3WebsiteBucket.Arn
- !Sub '${S3WebsiteBucket.Arn}/*'
- Effect: Allow
Action:
- 'cloudfront:CreateInvalidation'
Resource: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'
This role has four statements. CloudWatch Logs for build output. Artifact bucket read so CodeBuild can download the build artifact. Site bucket read/write/delete so it can sync files and remove stale ones. CloudFront invalidation so it can clear the edge cache after deploy.
Deploy Project
The DeployProject uses CodeBuild to run two aws s3 sync commands with different Cache-Control headers. Static sites without hashed filenames need a two-tier approach: long-lived caching for assets and immediate revalidation for HTML.
DeployProject:
Type: 'AWS::CodeBuild::Project'
Properties:
Name: !Sub '${AWS::StackName}-Deploy'
ServiceRole: !GetAtt DeployServiceRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
EnvironmentVariables:
- Name: DEPLOY_BUCKET
Value: !Ref S3WebsiteBucket
- Name: CLOUDFRONT_DIST_ID
Value: !Ref CloudFrontDistribution
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
build:
commands:
- aws s3 sync . s3://$DEPLOY_BUCKET --exclude "*.html" --delete --cache-control "max-age=31536000,public,immutable"
- aws s3 sync . s3://$DEPLOY_BUCKET --exclude "*" --include "*.html" --cache-control "max-age=0,must-revalidate"
post_build:
commands:
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DIST_ID --paths "/*"
The two s3 sync commands create a two-tier caching strategy:
The first sync uploads everything except HTML files with max-age=31536000,public,immutable. That is one year. CSS, JS, and image filenames do not change between builds on a plain static site, but the CloudFront invalidation after each deploy forces edge caches to fetch the latest versions. The immutable directive tells browsers to skip revalidation requests for these files entirely.
The second sync uploads only HTML files with max-age=0,must-revalidate. HTML pages are the entry points that reference all other assets. Browsers must check for updates on every request so they always get the latest version.
--delete on the first sync removes non-HTML files from S3 that no longer exist in the build output. The second sync does not use --delete because it only uploads HTML files and does not need to manage deletions separately.
post_build invalidates the CloudFront edge cache with /* so every edge location fetches the latest files from S3. Without this, edge caches would serve stale content until their TTL expires.
Deploy Stage
Add the Deploy stage to the pipeline’s Stages array. This stage uses CodeBuild as the provider, not the S3 deploy provider. CodeBuild gives you control over cache headers and the sync command.
- Name: Deploy
Actions:
- Name: SiteDeploy
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
InputArtifacts:
- Name: BuildArtifact
Configuration:
ProjectName: !Ref DeployProject
The Category is Build, not Deploy, because the action type is CodeBuild. The pipeline’s stage name (Deploy) is just a label. What matters is the action type.
InputArtifacts references BuildArtifact from the Build stage. The deploy project receives the built files and syncs them to S3.
Update the pipeline’s service role from post 9 to include the deploy project. Add !GetAtt DeployProject.Arn to the CodeBuild permissions statement.
- Effect: Allow
Action:
- 'codebuild:BatchGetBuilds'
- 'codebuild:StartBuild'
Resource:
- !GetAtt BuildProject.Arn
- !GetAtt DeployProject.Arn
Updating the Role
Your Git Sync role needs permissions for CloudFront response headers policies so CloudFormation can create and manage them. Add this statement to the inline policy in your role stack.
- Effect: Allow
Action:
- 'cloudfront:CreateResponseHeadersPolicy'
- 'cloudfront:GetResponseHeadersPolicy'
- 'cloudfront:UpdateResponseHeadersPolicy'
- 'cloudfront:DeleteResponseHeadersPolicy'
Resource: '*'