# CDN From "help me set up a CDN" to an app whose scripts, styles, images and fonts load from Amazon CloudFront, near each visitor. Pages still come from the app. Start step 1 as soon as the user asks. This is AWS whatever the app is deployed on; another CDN only when the user names one, and then it is `ASSET_URL` in `production.env` (see [Load Balancer](load-balancer)). What to say: only the quoted lines on this page, word for word. Nothing else reaches the user: no hostnames, ids, file names, config, tool names, reasoning, or what comes next. How to run: each block is one command and stands alone. It reads what it needs from `.elements/id`, `config.jsoc` and AWS, so nothing carries over in shell variables, and every `aws` call names `--profile elements`. No scripts around them, no `set -e`, no `exit`. ## 1. Deployed First The CDN fetches from the live app, so it needs one. With no `deploy.domain` in `config.jsoc`, deploy first: start [AWS](aws) at step 1, and come back when it is live. The first name in `deploy.domain` is the origin below. ## 2. Sign In ```bash Terminal aws sts get-caller-identity --profile elements --query Account --output text ``` An account number means signed in. Anything else: do step 1 of [AWS](aws), then come back here. ## 3. Price Never a price from memory. This prints CloudFront's rate per GB for the first band of traffic out of North America and Europe: ```bash Terminal aws pricing get-products --profile elements --region us-east-1 --service-code AmazonCloudFront --filters 'Type=TERM_MATCH,Field=transferType,Value=CloudFront Outbound' 'Type=TERM_MATCH,Field=fromLocation,Value=United States' --query 'PriceList[]' --output text | grep -o '\$[0-9.]* per GB[^"]*' | grep first ``` Round to cents. Ask with a question tool when there is one (options "Go ahead (recommended)" and "Use my own address"): > A CDN costs about $ per GB it serves, and nothing when idle. It > comes with its own address, so you don't need one. Go ahead? "Use my own address" goes to Your Own Address below first, then step 4. Never offer it otherwise. ## 4. Create It > Setting up your CDN, about five minutes… ```bash Terminal APP=$(cat .elements/id); D=; A= X=""; [ -n "$A" ] && X=",\"Aliases\": {\"Quantity\": 1, \"Items\": [\"$A\"]}, \"ViewerCertificate\": {\"ACMCertificateArn\": \"$(aws acm list-certificates --profile elements --region us-east-1 --query "CertificateSummaryList[?DomainName=='$A'].CertificateArn | [0]" --output text)\", \"SSLSupportMethod\": \"sni-only\", \"MinimumProtocolVersion\": \"TLSv1.2_2021\"}" aws cloudfront create-distribution --profile elements --query 'Distribution.DomainName' --output text --distribution-config "{ \"CallerReference\": \"elements-$APP-$(date +%s)\", \"Comment\": \"elements-$APP\", \"Enabled\": true, \"HttpVersion\": \"http2and3\", \"Origins\": {\"Quantity\": 1, \"Items\": [{\"Id\": \"app\", \"DomainName\": \"$D\", \"CustomOriginConfig\": {\"HTTPPort\": 80, \"HTTPSPort\": 443, \"OriginProtocolPolicy\": \"https-only\"}}]}, \"DefaultCacheBehavior\": {\"TargetOriginId\": \"app\", \"ViewerProtocolPolicy\": \"redirect-to-https\", \"CachePolicyId\": \"658327ea-f89d-4fab-a63d-7e88639e58f6\", \"Compress\": true, \"AllowedMethods\": {\"Quantity\": 2, \"Items\": [\"GET\", \"HEAD\"]}}$X }" ``` It prints the CDN's address, `d1234abcd.cloudfront.net`. The cache policy is AWS's own "caching optimized": it keeps what the app's headers say, and every asset URL carries a content hash, so a deploy never serves a stale file. Wait for it. Each run waits at most four minutes; run it again until it prints `Deployed`: ```bash Terminal APP=$(cat .elements/id) ID=$(aws cloudfront list-distributions --profile elements --query "DistributionList.Items[?Comment=='elements-$APP'].Id | [0]" --output text) for i in 1 2 3 4 5 6 7 8; do S=$(aws cloudfront get-distribution --profile elements --id $ID --query Distribution.Status --output text); [ "$S" = Deployed ] && break; sleep 30; done echo $S ``` ## 5. Connect the App In `config.jsoc`, inside `build` (add the block when there is none): ```jsoc config.jsoc build: { assetUrl: env("ASSET_URL", ""), }, ``` ```bash Terminal APP=$(cat .elements/id); F=config/env/production.env; H= [ -n "$H" ] || H=$(aws cloudfront list-distributions --profile elements --query "DistributionList.Items[?Comment=='elements-$APP'].DomainName | [0]" --output text) touch $F; grep -v '^ASSET_URL=' $F > $F.tmp; mv $F.tmp $F echo "ASSET_URL=https://$H" >> $F curl -sI https://$H/ | head -1 ``` A `200` or a `30x` is the app answering through the CDN. With an own address, first add a `CNAME` from it to the `cloudfront.net` address (see Adding a Record, below), and run this block again until it answers. > Turning on your CDN… ```bash Terminal elements deploy -json ``` No errors: > Your CDN is on. Scripts, styles and images now load from the location > nearest each visitor. ## Your Own Address > What address should it use? For example, cdn.. Amazon needs a certificate for it, which it issues once a record proves the domain is the user's: ```bash Terminal A=
aws acm request-certificate --profile elements --region us-east-1 --domain-name $A --validation-method DNS --query CertificateArn --output text >/dev/null sleep 5 aws acm list-certificates --profile elements --region us-east-1 --query "CertificateSummaryList[?DomainName=='$A'].CertificateArn | [0]" --output text | xargs aws acm describe-certificate --profile elements --region us-east-1 --query 'Certificate.DomainValidationOptions[0].ResourceRecord.[Type,Name,Value]' --output text --certificate-arn ``` Add that record (Adding a Record, below), then wait. Each run waits at most four minutes; run it again until it prints `ISSUED`: ```bash Terminal A=
C=$(aws acm list-certificates --profile elements --region us-east-1 --query "CertificateSummaryList[?DomainName=='$A'].CertificateArn | [0]" --output text) for i in 1 2 3 4 5 6 7 8; do S=$(aws acm describe-certificate --profile elements --region us-east-1 --certificate-arn $C --query Certificate.Status --output text); [ "$S" = ISSUED ] && break; sleep 30; done echo $S ``` ## Adding a Record This prints the DNS zone and its name server, which says how the record is added: ```bash Terminal A=
; Z=$(dig +noall +answer +authority SOA $A | awk '$4=="SOA" {print $1; exit}'); echo "zone ${Z%.} $(dig +short NS $Z | head -1)" ``` `awsdns` (Route 53): ```bash Terminal ZN=; T=; N=; V= Z=$(aws route53 list-hosted-zones-by-name --profile elements --dns-name $ZN --query "HostedZones[?Name=='$ZN.'].Id" --output text) aws route53 change-resource-record-sets --profile elements --hosted-zone-id $Z --change-batch "{\"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{\"Name\":\"$N\",\"Type\":\"$T\",\"TTL\":300,\"ResourceRecords\":[{\"Value\":\"$V\"}]}}]}" --query ChangeInfo.Status --output text ``` `digitalocean.com`, with `doctl` signed in: ```bash Terminal ZN=; T=; N=; V=; N=${N%.}.; N=${N%.$ZN.} doctl compute domain records create $ZN --context elements --record-type $T --record-name $N --record-data ${V%.}. --record-ttl 300 >/dev/null && echo ok ``` Anything else: the user adds it. Fill in the type, name and value: > Add this record where you manage 's DNS, then come back. ## Teardown Confirm with the user first. Take the CDN out of the app before deleting it: remove `ASSET_URL` from `production.env` and deploy. Then disable it, wait until `Deployed` (the wait block above), and delete it: ```bash Terminal APP=$(cat .elements/id) ID=$(aws cloudfront list-distributions --profile elements --query "DistributionList.Items[?Comment=='elements-$APP'].Id | [0]" --output text) aws cloudfront get-distribution-config --profile elements --id $ID --query DistributionConfig --output json | sed 's/"Enabled": true/"Enabled": false/' > /tmp/elements-cdn.json aws cloudfront update-distribution --profile elements --id $ID --if-match $(aws cloudfront get-distribution-config --profile elements --id $ID --query ETag --output text) --distribution-config file:///tmp/elements-cdn.json --query Distribution.Status --output text ``` ```bash Terminal APP=$(cat .elements/id) ID=$(aws cloudfront list-distributions --profile elements --query "DistributionList.Items[?Comment=='elements-$APP'].Id | [0]" --output text) aws cloudfront delete-distribution --profile elements --id $ID --if-match $(aws cloudfront get-distribution-config --profile elements --id $ID --query ETag --output text) && rm -f /tmp/elements-cdn.json ``` With an own address, then `aws acm delete-certificate --profile elements --region us-east-1 --certificate-arn `, and its records by hand.