+++ title = "Hosting a Static Site on AWS" date = "2020-07-17T12:31:32-07:00" author = "alejandro" tags = ["info-dump"] keywords = ["AWS", "S3", "CloudFront", "Route 53"] showFullContent = false +++ I bought a domain name on a whim recently and felt like I had to justify my purchase by building a site to make use of it. In a past life I might have used WordPress or Django but I was feeling especially lazy. At first I was going to use GitHub Pages but that seemed too easy. I haven't really touched AWS before and I felt like I might as well make this into a learning opportunity. As you can probably tell by the title, I ended up using AWS to host a static site. This post documents the steps I took to get this working. These steps were compiled after I had everything working so it's possible there may be some typos. If you stumbled upon this and have a question on a step, feel free to reach out (you can find my contact info on the [about page](/about#contact)). ## Configuring AWS CLI Log on to the AWS console and create an IAM user with administrator access. Keep the user's credentials safe since this user has full access to your AWS account. Install the aws cli utility and run `aws configure` and use the admin user's credentials that were just generated. It's not necessary but I also went ahead and set my region to `us-east-1`. ## Setting up S3 Bucket I didn't already have an AWS account (or at least not one that I had the password to) so I fired up `https://aws.amazon.com/` and created an account. From there, I went to [the AWS console](https://s3.console.aws.amazon.com/) and created a new bucket with the same name as my domain name (this part is important). So if your domain name is `supercool.tld` your bucket name would also be `supercool.tld`. Once the bucket is created, static website hosting needs to be enabled. Set the index document value to `index.html` even though there's nothing in the bucket yet. An S3 bucket is used to house the site's static files. ```bash aws s3 mb s3:// ``` The bucket also has to be configured for static website hosting and the files should be public. Prepare a json file to give anyone read permissions to objects in the bucket. ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::/*" } ] } ``` ```bash aws s3api put-bucket-policy --bucket --policy file:// aws s3 website s3:// --index-document index.html ``` ## Setting up Route 53 Create a hosted zone in AWS. A hosted zone contains the configuration (A records, CNAME, records, etc.) for a domain. ```bash aws route53 create-hosted-zone --name --caller-reference $(date -Ins) ``` Caller reference just needs to be a unique string so we use the current datetime (with nanosecond precision). Now that a hosted zone is setup, we'll need to get it's zone ID so we can set up a certificate. ```bash aws route53 list-hosted-zones ``` ## Setting up Certificate A cerificate is needed for https support. To do so a cert has to be requeted from AWS's aptly named AWS Certificate Manager (ACM). Once a request is in, domain ownership needs to be validated (AWS can't be giving out certs for just any domain). Validation can be done through DNS or email. Email validation requires controlling an email address like `admin@suprecool.tld` and clicking a link in an email sent to it. DNS validation requires adding a CNAME record in a hosted zone. ACM certs are automatically renewed as long as the domain validation does not expire. Email validation has to be revalidated every 825 days (about every two years). For DNS validation, this just means leaving the CNAME record up. DNS validation is a bit more straightforward so we'll use that. Run the following command and make note of the ARN, it'll be needed later. ```bash aws acm request-certificate --domain-name --validation-method DNS ``` ACM will provide the CNAME data expected for validation. Run the command below and look at `DomainValidationOptions` for the expected values. ```bash aws acm describe-certificate --certificate-arn ``` Prepare a json file to add the required CNAME. ```json { "Changes": [{ "Action": "CREATE", "ResourceRecordSet": { "Name": "", "Type": "CNAME", "TTL": 300, "ResourceRecords": [{ "Value": "" }] } }] } ``` ```bash aws route53 change-resource-record-sets --hosted-zone-id --change-batch-file file:// ``` It'll take a bit for ACM to validate your new CNAME record. But once it's validated you'll be able to see the certificate by running the following: ```bash aws acm get-certificate --certificate-arn ``` ## Setting up CloudFront Since S3 website endpoints don't support HTTPS, we'll configure CloudFront (AWS's CDN service) to serve up our files. The template for the JSON required is below. The region name is whatever was configured when `aws configure` was ran earlier. ```json { "Aliases": { "Quantity": 1, "Items": [ "" ] }, "DefaultRootObject": "index.html", "Origins": { "Quantity": 1, "Items": [ { "Id": "S3-Website-.s3-website-.amazonaws.com", "DomainName": ".s3-website-.amazonaws.com", "OriginPath": "", "CustomHeaders": { "Quantity": 0 }, "CustomOriginConfig": { "HTTPPort": 80, "HTTPSPort": 443, "OriginProtocolPolicy": "http-only", "OriginSslProtocols": { "Quantity": 3, "Items": [ "TLSv1", "TLSv1.1", "TLSv1.2" ] }, "OriginReadTimeout": 30, "OriginKeepaliveTimeout": 5 }, "ConnectionAttempts": 3, "ConnectionTimeout": 10 } ] }, "OriginGroups": { "Quantity": 0 }, "DefaultCacheBehavior": { "TargetOriginId": "S3-Website-.s3-website-.amazonaws.com", "ForwardedValues": { "QueryString": false, "Cookies": { "Forward": "none" }, "Headers": { "Quantity": 0 }, "QueryStringCacheKeys": { "Quantity": 0 } }, "TrustedSigners": { "Enabled": false, "Quantity": 0 }, "ViewerProtocolPolicy": "redirect-to-https", "MinTTL": 0, "AllowedMethods": { "Quantity": 2, "Items": [ "HEAD", "GET" ], "CachedMethods": { "Quantity": 2, "Items": [ "HEAD", "GET" ] } }, "SmoothStreaming": false, "DefaultTTL": 86400, "MaxTTL": 31536000, "Compress": false, "LambdaFunctionAssociations": { "Quantity": 0 }, "FieldLevelEncryptionId": "" }, "CacheBehaviors": { "Quantity": 0 }, "CustomErrorResponses": { "Quantity": 1, "Items": [ { "ErrorCode": 404, "ResponsePagePath": "/404.html", "ResponseCode": "404", "ErrorCachingMinTTL": 60 } ] }, "Comment": "", "Logging": { "Enabled": false, "IncludeCookies": false, "Bucket": "", "Prefix": "" }, "PriceClass": "PriceClass_All", "Enabled": true, "ViewerCertificate": { "ACMCertificateArn": "", "SSLSupportMethod": "sni-only", "MinimumProtocolVersion": "TLSv1.2_2018", "Certificate": "", "CertificateSource": "acm" }, "Restrictions": { "GeoRestriction": { "RestrictionType": "none", "Quantity": 0 } }, "WebACLId": "", "HttpVersion": "http2", "IsIPV6Enabled": true } ``` ```bash aws create-distribution --distribution-config file:// ``` ## Finish Setting up Route 53 Now that that CloudFront is configured we can add an A record to our DNS config. Prepare a JSON file for the request. Make sure to include the trailing `.` for the `Name` and `DNSName` values. Speaking of `DNSName`, run the following to get the CloudFront distribution's domain name: ```bash aws cloudfront list-distributions | grep DomainName | head -n1 ``` The above command only works correctly if you only have one CloudFront distribution configured in your account. If you have more than one distribution, log in to the AWS console and grab the domain name from the distribution that way. Prepare a json file for the Route 53 request. Fun fact: `HostedZoneId` is hardcoded for CloudFront distributions. ```json { "Changes": [{ "Action": "CREATE", "ResourceRecordSet": { "Name": ".", "Type": "A", "AliasTarget": { "HostedZoneId": "Z2FDTNDATAQYW2", "DNSName": ".", "EvaluateTargetHealth": false } } }] } ``` ```bash aws route53 change-resource-record-sets --hosted-zone-id --change-batch-file file:// ``` ## Upload Files Everything should be ready now, we just need to add some files. Create a sample index file and uptload it to test. ```html

It works!

``` Let's also create a sample 404 error page file. ```html

Page does not exist

``` ```bash aws s3 cp s3:///index.html aws s3 cp s3:///404.html ``` Fire up your site and cross your fingers. If all went well you should see the `It works!` message when you point your browser to your URL and a page does not exist message when you try accessing something like `/foo`.