Create a CloudFront distribution

With CloudFront, Amazon created a CDN (Content Delivery Network), which can be used for serving static files in a fast manner. The actual files are being managed at one single place, but are provided by different servers. The particular server for a request is chosen based on the requester’s location. If a US-based client is requesting a static file from the CDN, the file is being provided by a server based somewhere in the USA. However, if the client’s location is Germany, the file won’t be served from a US-based, but a German server. This is done in order to save loading-time.

The servers, which hold a cached version of the static files are called Edge Locations. In the image below, each blue dot stands for at least one Edge Location. Some dots stand for up to 3 of them. More details about the AWS-Regions and Edge Locations can be found here.

AWS Edge Locations

RegionCountryCityELs
NORAMUSAshburn, VA3
NORAMUSAtlanta, GA1
NORAMUSDallas, TX2
NORAMUSHayward, CA1
NORAMUSJacksonville, FL1
NORAMUSLos Angeles2
NORAMUSMiami, FL1
NORAMUSNew York, NY3
NORAMUSNewark, NJ1
NORAMUSPalo Alto, CA1
NORAMUSSan Jose, CA1
NORAMUSSeattle, WA1
NORAMUSSouth Bend, IN1
NORAMUSSt. Louis, MO1
RegionCountryCityELs
LATAMBRRio de Janeiro1
LATAMBRSão Paulo1
RegionCountryCityELs
EMEANLAmsterdam2
EMEAIEDublin1
EMEADEFrankfurt3
EMEAUKLondon3
EMEAESMadrid1
EMEAFRMarseilles1
EMEAITMilan1
EMEAFRParis2
EMEASEStockholm1
EMEAPLWarsaw1
RegionCountryCityELs
APACINChennai1
APACCHHong Kong2
APACPHManila1
APACAUMelbourne1
APACINMumbai1
APACJPOsaka1
APACKRSeoul2
APACSGSingapore2
APACAUSydney1
APACTWTaipei1
APACJPTokyo2
RegionEdge Locations
NORAM20
LATAM2
EMEA16
APAC15
Total53

Create S3-bucket

The static files for the distribution need to be stored at some place. There are also other concepts like e.g. origin pull, but here I will focus on a S3-bucket. Therefore, you first need to create a S3-bucket. It’s a best practice to give the S3-bucket the same name as the domain-name we use later on.

$ aws s3 mb s3://aws-blog.io

With the command above you can create a S3-bucket with the name aws-blog.io. Keep in mind, that a S3-bucket-name needs to be unique for the whole service and not only your account. The command below is used to delete the bucket, in case it’s not needed anymore. That statement contains a –force at the end, which means that S3 will delete the bucket, even it has content within it. I normally do include the force-option, because I already have decided to delete a bucket, even if it contains data.

$ aws s3 rb s3://aws-blog.io --force

For the CloudFront-distribution to work properly, there needs to be some access-rights adjustments. With the following bucket-policy, a download of any file within that bucket is allowed for any user. For a more professional setup, this should get reducded to CloudFront-sources / Principals only.

Just edit the following JSON-statement accordingly and save it as bucket-policy.json. It actually can be named anything, but within this post I will reference it.

{
	"Version": "2012-10-17",
	"Id": "Policy1431883602565",
	"Statement": [
		{
			"Sid": "Stmt1431883600330",
			"Effect": "Allow",
			"Principal": "*",
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::aws-blog.io/*"
		}
	]
}

Now that you have an adjusted bucket-policy, it needs to be applied on the bucket itself. This is done by using aws s3api, as stated in the command below.

$ aws s3api put-bucket-policy --bucket aws-blog.io --policy file://bucket-policy.json

Everything is now setup from a S3-environment point-of-view. The only thing that’s missing is the actual data respectively the static files which later on should get served by CloudFront. With the next command, a local folder and a S3-bucket are getting synchronized. Here, the source is the local folder /home/flo/aws-blog.io/_site and the target is the s3-bucket aws-blog.io. Sources and targets can both be either local folders or S3-buckets. The –delete amendment will delete every file in the target, if it’s not existent within the source.

$ aws s3 sync /home/flo/aws-blog.io/_site s3://aws-blog.io/ --delete

Setup CloudFront-distribution

Create a web-distribution

During both creation-modes (Method 1 and Method 2), the following values in the JSON-statement need to get adjusted to your individual account:

  • DistributionConfig.Origins.Items[*].Id
  • DistributionConfig.Origins.Items[*].DomainName
  • DistributionConfig.DefaultCacheBehaviour.TragetOriginId
  • DistributionConfig.CallerReference (I normally set here the current timestamp, however it needs to be unique within all your previous AWS-API-calls)
  • DistributionConfig.Aliases.Items[*]

If you chose Method 1, you need to specify more parameters, as we here generate a default-distribution-config with the command amendment –generate-cli-skeleton, whereas for Method 2 we use a pre-configured config-file.

Method 1

For this alternative of setting up a CloudFront-distribution, we need a software called jq, which helps editing json-files. It does make sense to install that software now, as we also need it later on.

$ sudo apt-get install jq
$ aws cloudfront create-distribution --generate-cli-skeleton > /tmp/aws-blog.io-distribution.json
$ vi /tmp/aws-blog.io-distribution.json
$ cat /tmp/aws-blog.io-distribution.json | jq '. | .DistributionConfig' > /tmp/aws-blog.io-distribution-only-config.json
$ aws cloudfront create-distribution --distribution-config file:///tmp/aws-blog.io-distribution-only-config.json

Method 2

$ aws cloudfront create-distribution --cli-input-json '
> {
>     "DistributionConfig": {
>         "Comment": "", 
>         "CacheBehaviors": {
>             "Quantity": 0
>         }, 
>         "Logging": {
>             "Bucket": "", 
>             "Prefix": "", 
>             "Enabled": false, 
>             "IncludeCookies": false
>         }, 
>         "Origins": {
>             "Items": [
>                 {
>                     "OriginPath": "", 
>                     "CustomOriginConfig": {
>                         "OriginProtocolPolicy": "http-only", 
>                         "HTTPPort": 80, 
>                         "HTTPSPort": 443
>                     }, 
>                     "Id": "custom-aws-blog.io.s3-website.eu-central-1.amazonaws.com", 
>                     "DomainName": "aws-blog.io.s3-website.eu-central-1.amazonaws.com"
>                 }
>             ], 
>             "Quantity": 1
>         }, 
>         "DefaultRootObject": "index.html", 
>         "PriceClass": "PriceClass_All", 
>         "Enabled": true, 
>         "DefaultCacheBehavior": {
>             "TrustedSigners": {
>                 "Enabled": false, 
>                 "Quantity": 0
>             }, 
>             "TargetOriginId": "custom-aws-blog.io.s3-website.eu-central-1.amazonaws.com", 
>             "ViewerProtocolPolicy": "allow-all", 
>             "ForwardedValues": {
>                 "Headers": {
>                     "Quantity": 0
>                 }, 
>                 "Cookies": {
>                     "Forward": "none"
>                 }, 
>                 "QueryString": false
>             }, 
>             "SmoothStreaming": false, 
>             "AllowedMethods": {
>                 "Items": [
>                     "GET", 
>                     "HEAD"
>                 ], 
>                 "CachedMethods": {
>                     "Items": [
>                         "GET", 
>                         "HEAD"
>                     ], 
>                     "Quantity": 2
                 }, 
>                 "Quantity": 2
>             }, 
>             "MinTTL": 0
>         }, 
>         "CallerReference": "Mon May 25 21:39:53 CEST 2015", 
>         "CustomErrorResponses": {
>             "Quantity": 0
>         }, 
>         "Restrictions": {
>             "GeoRestriction": {
>                 "RestrictionType": "none", 
>                 "Quantity": 0
>             }
>         }, 
>         "Aliases": {
>             "Items": [
>                 "aws-blog.io"
>             ], 
>             "Quantity": 1
>         }
>     }
> }
> '

Creating a DNS-record for distribution

This setup is intended for domains hosted within Route53. It’s also possible with other hosters. There you just need to create a CNAME for your distributions DNS-name. If you don’t have a domain within Route53 yet, there’s already a blog-post on who to set that up. You can find that post here. For the creation of the CNAME-DNS entries, it’s advised to wait until the status of the distribution has changed from In Progress to Deployed. The state of all distributions can be checked with aws cloudfront list-distributions, as described below.

$ aws cloudfront list-distributions --query "DistributionList.Items[*].{Domain: join(', ', Aliases.Items), DistributionID: Id, Status: Status, CloudFrontDomain: DomainName}" --output table

You then can create a file called aws-blog.io-cloudfront-alias.json (you actually can name it anything, but I’ll reference it later on), copy & paste the following JSON-statement and edit accordingly. The mentioned DNSName of the distribution is also in the command above, which you just used to check the distribution’s state.

NOTE: The HostedZoneId in AliasTarget is AWS’s HostedZoneId of CloudFront, so you need so set that specific one with the ID Z2FDTNDATAQYW2 and not your own one.

{
  "Comment": "CloudFront CNAME",
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "aws-blog.io",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId" : "Z2FDTNDATAQYW2",
          "DNSName": "d2bpXXXXXztnay.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    }
  ]
}

The previously created JSON-statement gets used as a parameter for the next command. The only other unique parameter is the hosted-zone-id.

$ aws route53 list-hosted-zones --query "HostedZones[*].{Name: Name, FullID: Id"}
$ aws route53 change-resource-record-sets --hosted-zone-id Z3GXXXXXXGTO --change-batch file://aws-blog.io-cloudfront-alias.json
$ sudo apt-get install jq
$ cat /tmp/aws-blog.io-distribution.json | jq '. | .DistributionConfig' > /tmp/aws-blog.io-distribution-only-config.json
$ aws cloudfront create-distribution --distribution-config file:///tmp/aws-blog.io-distribution-only-config.json
$ aws cloudfront list-distributions --query "DistributionList.Items[*].{Domain: join(', ', Aliases.Items), DistributionID: Id, Status: Status}" --output table
-------------------------------------------------------
|                  ListDistributions                  |
+-----------------+-----------------------+-----------+
| DistributionID  |        Domain         |  Status   |
+-----------------+-----------------------+-----------+
|  E2NGXXXXXBI7IC |  aws-blog.io          |  Deployed |
|  E190XXXXXL0LJU |  staging.aws-blog.io  |  Deployed |
+-----------------+-----------------------+-----------+

Delete distribution

In order to delete a distribution, it first needs to get in a disabled-state. After this step had been done, it can get deleted.

For a more easy way of editing JSON-files, the software json (formerly known as json-tools) and jq is being installed.

$ sudo npm install -g json
$ sudo apt-get install jq

After the installation of the mentioned tools, you can download the current distribution-config and set enabled: false in it. This is all done with the next command-block. The only parameter that needs to get adjusted is the –id, which you can get with a listing of all domains as previously done. For the update-process you need the value of ETag, which will be listed with the very first command.

$ aws cloudfront get-distribution-config --id E3GXXXXXXSPXOX --query "{ETag: ETag}"
$ aws cloudfront get-distribution-config --id E3GXXXXXXSPXOX | jq '. | .DistributionConfig' > /tmp/aws-blog.io-distribution.json
$ cat aws-blog.io-distribution.json | json -e 'this.Enabled=false' > aws-blog.io-distribution-disabled.json
$ aws cloudfront update-distribution --id E3GXXXXXXSPXOX --if-match E2IPXXXXXXWKX0 --distribution-config file:///tmp/aws-blog.io-distribution-disabled.json

The status of the distribution needs to be Deployed, in order to proceed. You can always check the status of all of your distributions with the following command, which just lists all distributions and their states.

$ aws cloudfront list-distributions --query "DistributionList.Items[*].{Domain: join(', ', Aliases.Items), DistributionID: Id, Status: Status}" --output table

When the state of the distribution has changed to Deployed, you can trigger the actual deletion with the following command.

$ aws cloudfront delete-distribution --id E3GXXXXXXXSPXOX --if-match E2IPXXXXXXWKX0

Invalidations

An invalidations is always needed, when a file has been adjusted before it’s specified expiration-date. When an invalidation is triggered in CloudFront, all cached versions of the files on all Edge Locations are getting renewed. Big websites with many deployments should rather look into an asset-pipeline concept with hashes as filenames, because after 1.000 invalidations per month, you will get charged for it.

NOTE: An invalidation call like the one below needs an unique CallerReference. That one needs to be specific for all calls to the AWS-API. As I actually don’t use the CallerReference, I just set a current timestamp for it.

$ invalidation_batch='{"Paths": {"Quantity": 1,"Items": ["/*"]},"CallerReference": "'`LC_ALL=en_US.UTF-8 date`'"}'
$ aws cloudfront create-invalidation --distribution-id E190XXXXXL0LJU --invalidation-batch "$invalidation_batch"



Written on 2015-05-24