After implementing an AWS CloudFront distribution for serving content from
AWS S3 it is best practice to prevent direct access to the S3 bucket. This will prevent
duplicate content issues on search engines and will also mean your content can only be accessed by the domains you expect.
The first part of removing direct S3 access is to create an identity for the
AWS CloudFront distribution to use to identify itself to
AWS S3. To do this a CloudFrontOriginAccessIdentity resource needs to be created.
# Apply the Identity to the CloudFront Distribution
To apply the previous identity to the AWS CloudFront distribution the distribution
configuration in the resources.yml file needs to be updated. The setting is specific to an origin within the
configuration and can only be applied to an S3 Origin.
However, if you define a default root object, an end-user request for a subdirectory of your distribution does not
return the default root object.
[…]
The behavior of CloudFront default root objects is different from the behavior of Amazon S3 index documents. When you
configure an Amazon S3 bucket as a website and specify the index document, Amazon S3 returns the index document even if
a user requests a subdirectory in the bucket.
This means logic has to be added to display the index.html files Hexo generates in each directory.
The first step to implement this is to create a AWS Lambda function; then add the
function to Serverless Framework’s configuration; and finally hook the function to
the AWS CloudFront distribution.
// Extract the request from the CloudFront event that is sent to Lambda@Edge var request = event.Records[0].cf.request;
// Extract the URI from the request var olduri = request.uri;
// Match any '/' that occurs at the end of a URI. Replace it with a default index var newuri = olduri.replace(/\/$/, '\/index.html');
// Log the URI as received by CloudFront and the new URI to be used to fetch from origin console.log("Old URI: " + olduri); console.log("New URI: " + newuri);
// Replace the received URI with the URI that includes the index page request.uri = newuri;
// Return to CloudFront return callback(null, request); };
Serverless Framework now needs to know about the new function so it can coordinate
the deployment for the code, configuration of AWS Lambda, and linking the function to
the distribution. This is done by adding a new top level element to the serverless.yml file.
1 2 3 4 5 6 7 8 9 10 11
# Define the Lambda functions for the site functions: # This function will be deployed to Lambda@Edge and rewrite URLs to include index.html urlrewrite: name: ${self:service}-${self:custom.stage}-cf-url-rewriter handler: functions/urlRewrite.handler memorySize: 128 timeout: 1 lambdaAtEdge: distribution: WebsiteCloudFrontDistribution eventType: origin-request
Now an identifier for AWS CloudFront has been configured the policy on the
AWS S3 bucket can be restricted to only allow access via the CloudFront distribution.
In the resources.yml file there is a Statement for the S3 Bucket Policy. This needs to be adjusted to remove
public access and grant access to CloudFront.
Within the main serverless.yml file, under the custom.assets.targets element, configuration needs to be added to
ensure all files are uploaded as private.
All the requirements are in place for AWS CloudFront to be able to access the
AWS S3 bucket contents. All that’s left to do it remove all public access from the bucket.
To do this locate the bucket configuration in resources.yml, update the AccessControl to BucketOwnerFullControl
and delete the WebsiteConfiguration.
# Plugins for additional Serverless functionality plugins: -serverless-s3-deploy -serverless-plugin-scripts -'@silvermine/serverless-plugin-cloudfront-lambda-edge'
# Configuration for AWS provider: name:aws runtime:nodejs8.10 profile:serverless # Some future functionality requires us to use us-east-1 at this time region:us-east-1
# This enables us to use the default stage definition, but override it from the command line stage:${opt:stage,self:provider.stage} # This enables us to prepend the stage name for non-production environments domain: fulldomain: prod:${self:custom.domain.domain} other:${self:custom.stage}.${self:custom.domain.domain} # This value has been customised so I can maintain multiple demonstration sites domain:${self:custom.postname}.${self:custom.domain.zonename} domainname:${self:custom.domain.fulldomain.${self:custom.stage},self:custom.domain.fulldomain.other} # DNS Zone name (this is only required so I can maintain multiple demonstration sites) zonename:alphageek.com.au cacheControlMaxAgeHTMLByStage: # HTML Cache time for production environment prod:3600 # HTML Cache time for other environments other:0 cacheControlMaxAgeHTML:${self:custom.domain.cacheControlMaxAgeHTMLByStage.${self:custom.stage},self:custom.domain.cacheControlMaxAgeHTMLByStage.other} sslCertificateARN:arn:aws:acm:us-east-1:165657443288:certificate/61d202ea-12f2-4282-b602-9c3b83183c7a assets: targets: # Configuration for HTML files (overriding the default cache control age) -bucket: Ref:WebsiteS3Bucket acl:private files: -source:./public/ headers: CacheControl:max-age=${self:custom.domain.cacheControlMaxAgeHTML} empty:true globs: -'**/*.html' # Configuration for all assets -bucket: Ref:WebsiteS3Bucket acl:private files: -source:./public/ empty:true globs: -'**/*.js' -'**/*.css' -'**/*.jpg' -'**/*.png' -'**/*.gif' scripts: hooks: # Run these commands when creating the deployment artifacts package:createDeploymentArtifacts:> hexo clean && hexo generate # Run these commands after infrastructure changes have been completed deploy:finalize:> sls s3deploy -s ${self:custom.stage} # AWS Region to S3 website hostname mapping s3DNSName: us-east-2:s3-website.us-east-2.amazonaws.com us-east-1:s3-website-us-east-1.amazonaws.com us-west-1:s3-website-us-west-1.amazonaws.com us-west-2:s3-website-us-west-2.amazonaws.com ap-south-1:s3-website.ap-south-1.amazonaws.com ap-northeast-3:s3-website.ap-northeast-3.amazonaws.com ap-northeast-2:s3-website.ap-northeast-2.amazonaws.com ap-southeast-1:s3-website-ap-southeast-1.amazonaws.com ap-southeast-2:s3-website-ap-southeast-2.amazonaws.com ap-northeast-1:s3-website-ap-northeast-1.amazonaws.com ca-central-1:s3-website.ca-central-1.amazonaws.com eu-central-1:s3-website.eu-central-1.amazonaws.com eu-west-1:s3-website-eu-west-1.amazonaws.com eu-west-2:s3-website.eu-west-2.amazonaws.com eu-west-3:s3-website.eu-west-3.amazonaws.com eu-north-1:s3-website.eu-north-1.amazonaws.com sa-east-1:s3-website-sa-east-1.amazonaws.com # Determine what resources file to include based on the current stage customConfigFile:${self:custom.customConfigFiles.${self:custom.stage},self:custom.customConfigFiles.other} customConfigFiles: prod:prod other:other
# Define the Lambda functions for the site functions: # This function will be deployed to Lambda@Edge and rewrite URLs to include index.html urlrewrite: name:${self:service}-${self:custom.stage}-cf-url-rewriter handler:functions/urlRewrite.handler memorySize:128 timeout:1 lambdaAtEdge: distribution:WebsiteCloudFrontDistribution eventType:origin-request
# Define the resources we will need to host the site resources: # Include the resources file -${file(config/resources.yml)} # Include the outputs file -${file(config/outputs.yml)} # Include a custom configuration file based on the environment -${file(config/resources/environment/${self:custom.customConfigFile}.yml)}