This article shows you how to can spin up a distribution in AWS Cloudfront using the Serverless Framework
You can find all the code required for this blog articles here on my github repo
We are going to make a script that spins up a private S3 Bucket, and then also adds a Cloudfront (CF) Distribution that points to that bucket.
We will also make sure that we can do the following:
cdn.example.com
https://www.example.com
or an entire domain and subdomains https://*.example.com
or anywhere on the internet *
{}
Once we have spun up our S3 Bucket and CloudFront Distribution - we should be able to add files to the S3 Bucket and then access them via the CF Distro. We shouldn't be able to view them via S3 directly, and they should have the correct CORS access headers for viewing only via our specified domain!
First make sure that you have an SSL certification for your domain and subdomain (perhaps even a wildcard certificate is best) - and that it is stored in AWS Certificate Manager (ACM). You will need to copy the ARN of the certificate to use in the CloudFront Distribution.
This script will also create a Route53 record for the domain and subdomain that you specify and point it to the CloudFront Distribution.
You will need to copy the .env.example
file to .env
and then edit the values to match your own.
IAM_PROFILE=default SERVICE_NAME=example-s3-cloudfront MY_REGION=us-east-1 MY_BUCKET=example-private-bucket MY_BUCKET_ORIGIN_ID=ExampleS3Origin MY_DISTRO_COMMENT=Example Distro # Set the price class to "Use Only North America and Europe" (PriceClass_200 is also europe etc, PriceClass_all is basically everywhere AWS supports) MY_DISTRO_PRICE_CLASS=PriceClass_100 # Allowed Origins Can be either a single * or https://*.example.com or https://www.example.com - Read more here https://docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html - if you wish to use multiple then edit the serverless.yml file itself ALLOWED_ORIGINS=* # optional 1 - using a domain alias (also comment out lines 77-81 of serverless.yml if not using this) MY_DOMAIN_ALIAS=subdomain.example.com MY_ACM_CERTIFICATE_ARN=arn:aws:acm:us-east-1:1111111111:certificate/5555555-1111-2222-3333-b82099e490bd # Your SSL Cert found in https://us-east-1.console.aws.amazon.com/acm/home?region=us-east-1 # optional 2 - get route 53 to assign our domain alias automatically (also comment out lines 100-117 of serverless.yml if not using this) MY_ROOT_DOMAIN=example.com
Please note - You don't have to use a .env file and you could simply edit the serverless.yml file directly, but I find it easier to use a .env file for my own values and then edit the serverless.yml file directly for any other values I need to change.
service: ${env:SERVICE_NAME} useDotenv: true provider: name: aws profile: ${env:IAM_PROFILE} region: ${env:MY_REGION} runtime: nodejs18.x resources: Resources: # The S3 Bucket MyS3Bucket: Type: AWS::S3::Bucket Properties: BucketName: ${env:MY_BUCKET} AccessControl: Private # Set the bucket access control to private # https://docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html CorsConfiguration: CorsRules: - AllowedOrigins: - ${env:ALLOWED_ORIGINS} AllowedMethods: - HEAD - GET AllowedHeaders: - "*" # The S3 Bucket Policy PrivateBucketPolicy: Type: AWS::S3::BucketPolicy Properties: PolicyDocument: Id: MyPolicy Version: "2012-10-17" Statement: - Sid: AllowCloudFrontServicePrincipalReadOnly Effect: Allow Principal: { "Service": "cloudfront.amazonaws.com" } Action: - s3:GetObject Resource: !Sub arn:aws:s3:::${MyS3Bucket}/* Condition: StringEquals: "aws:SourceArn": !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${MyCloudFrontDistribution} Bucket: Ref: MyS3Bucket OriginAccessControl: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Name: ${env:MY_BUCKET_ORIGIN_ID} OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 # The Cloud Front Distribution MyCloudFrontDistribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Enabled: true IPV6Enabled: true Comment: ${env:MY_DISTRO_COMMENT} # Add a description to the CloudFront distribution PriceClass: ${env:MY_DISTRO_PRICE_CLASS} # Set the price class to "Use Only North America and Europe" #optional 1 - Custom Domain alias - Comment out the 5 lines below if you don not want to use the domain alias and instead just use xxxxxxxx.cloudfront.net Aliases: - ${env:MY_DOMAIN_ALIAS} # Replace with your desired CloudFront distribution alias (Make sure you have a cert in "AWS Certificate Manager (ACM)" in the us-east-1 region) ViewerCertificate: AcmCertificateArn: ${env:MY_ACM_CERTIFICATE_ARN} # Replace with your ACM certificate ARN SslSupportMethod: sni-only Origins: - DomainName: ${env:MY_BUCKET}.s3.${env:MY_REGION}.amazonaws.com Id: ${env:MY_BUCKET_ORIGIN_ID} S3OriginConfig: {} OriginAccessControlId: !GetAtt OriginAccessControl.Id DefaultCacheBehavior: AllowedMethods: [GET, HEAD, OPTIONS] TargetOriginId: ${env:MY_BUCKET_ORIGIN_ID} CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # No caching OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf # CORS-S3 ResponseHeadersPolicyId: 67f7725c-6f97-4210-82d7-5512b31e9d03 # SecurityHeadersPolicy ViewerProtocolPolicy: redirect-to-https # Optional - Default Response example DefaultRootObject: index.json ## Optional - Any 403, and 404's will go directly to /404.json (just an example) CustomErrorResponses: - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: /404.json - ErrorCode: 404 ResponseCode: 200 # friendly response in this example ResponsePagePath: /404.json HttpVersion: http2 # optional 2 - Route53 Auto Assign subdomain - Comment out the below if you do not want Route53 to update DNS and add a record for your new subdomain Route53RecordIPv4: Type: AWS::Route53::RecordSet Properties: HostedZoneName: ${env:MY_ROOT_DOMAIN}. Name: ${env:MY_DOMAIN_ALIAS}. Type: A AliasTarget: DNSName: !GetAtt MyCloudFrontDistribution.DomainName HostedZoneId: Z2FDTNDATAQYW2 Route53RecordIPv6: Type: AWS::Route53::RecordSet Properties: HostedZoneName: ${env:MY_ROOT_DOMAIN}. Name: ${env:MY_DOMAIN_ALIAS}. Type: AAAA AliasTarget: DNSName: !GetAtt MyCloudFrontDistribution.DomainName HostedZoneId: Z2FDTNDATAQYW2 ## In order to print out the hosted domain via `serverless info` we need to define the DomainName output for CloudFormation Outputs: MyCloudFrontDomainName: Value: "Fn::GetAtt": [MyCloudFrontDistribution, DomainName]
This is optional but allows us to get back the CloudFront domain name
aws cloudformation --profile IAM_PROFILE --region REGION describe-stacks --stack-name SERVICE_NAME-STAGE --query "Stacks[0].Outputs"
You can either upload your testing files manually, or you can use the AWS CLI to upload them for you like this:
aws s3 cp ./test-files/ s3://example-private-bucket/ --recursive --profile default --region us-east-1
fetch('https://subdomain.example.com/test.json') .then(response => response.text()) .then(data => console.log(data));
If you ALLOWED_ORIGINS is set to *
then any domain should be able to access the test files. Howver if you've set a value, then you'll need to be testing from the allowed domains.
To do this you will need to edit the serverless file directly and add other domains?
CorsConfiguration: CorsRules: - AllowedOrigins: - ${env:ALLOWED_ORIGINS} - https://www.seconddomain.com
Yes you can add localhost to the allowed origins, but you will need to make sure that you are using the correct port number. For example if you are running a local server on port 3000 then you will need to add http://localhost:3000
to the allowed origins.
CorsConfiguration: CorsRules: - AllowedOrigins: - ${env:ALLOWED_ORIGINS} - https://www.seconddomain.com - http://localhost:3000
If you have a domain being managed in CloudFlare instead of Route 53, then to spin this up you want to do the following:
MY_ACM_CERTIFICATE_ARN
Route53RecordIPv4
and Route53RecordIPv6
sections in the serverless.yml
fileMY_ROOT_DOMAIN
in the .env filexxxyyyzzz123.cloudfront.net