Websites with AWS: Hosting, HTTPS and Custom Domain
Tags: aws, webdev, frontend, tutorial
Published: 2023-08-16 08:55:30

Hosting websites like static sites or single page applications on Amazon S3 is a charm. However, it gets a bit complicated if you throw in HTTPS and your own domain name. There are literally a million article on the web on how to host a website on S3, especially since S3 was one of Amazon's earliest services, having been available since 2006. This article is not meant to be yet another step-by-step guide. Rather, it's going to be a summary of important notes and common questions I encountered, enriched with lots of links to the official docs and other sources. I will share a CloudFormation template at the end that can be used as a blueprint and be easily deployed.

## Hosting
Amazon S3 supports [static website hosting](https://docs.aws.amazon.com/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html) as a feature. You can [enable](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EnableWebsiteHosting.html) this feature in your S3 bucket's properties, but only after the bucket has been created.
I refer to this type of hosting as the _classic approach_. There is now a second, _modern approach_, to host websites with S3 that I will write about in a future post.

### Naming
Let's start with naming. There are a few sources on the web that suggest that you have to name your bucket as your domain name. 

> To support requests from both the root domain and subdomain, you create two buckets: Domain bucket `example.com` and  Subdomain bucket `www.example.com`
>These bucket names must match your domain name exactly. In this example, the domain name is example.com. You host your content out of the root domain bucket (example.com). 
<small>[Configuring a static website using a custom domain registered with Route 53](https://docs.aws.amazon.com/AmazonS3/latest/userguide/website-hosting-custom-domain-walkthrough.html)</small>

This requirement is a bit outdated and not well explained. If you're using Amazon Route 53 as your DNS service and you want to map a domain like `example.com` to your S3 bucket, then the bucket name must match the domain name. If you're using a subdomain like `www.example.com`, then the bucket name must match the subdomain.

However, Amazon S3 website endpoints do not support secure HTTPS connections. That means your domain will only be accessible via unsecure HTTP. If you want to use HTTPS (and you should!), you need to use another service like Amazon CloudFront that serves the static website hosted on Amazon S3 and it doesn't require the bucket to be named like the domain.

### Permission and Public Access
It's important to note that your bucket and all its content must be publicly available, in order to host a public website. That means every single object in the bucket can be accessed on the web. So keep that in mind and put only content into the bucket that is related to your website and must be accessible on the web.

To make your bucket accessible on the web, you need to disable [block all public access](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteAccessPermissionsReqd.html#block-public-access-static-site) and add a [bucket policy](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteAccessPermissionsReqd.html#bucket-policy-static-site) that allows `GetObject` access.

On a side note, there are ways to keep the S3 bucket private (i.e. block public access setting) by using [Amazon CloudFront origin access control (OAC)](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html). I will come back to this in a future post.

### Access Control Lists
[Access Control Lists](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html) (ACL) grant access to a specific resource in the bucket. However, you don't need to add any ACLs and they are [disabled by default](https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html#object-ownership-considerations) for new buckets.  

> A majority of modern use cases in Amazon S3 no longer require the use of ACLs. We recommend that you keep ACLs disabled, except in unusual circumstances where you need to control access for each object individually. With ACLs disabled, you can use policies to control access to all objects in your bucket, regardless of who uploaded the objects to your bucket. For more information, see Controlling ownership of objects and disabling ACLs for your bucket.
<small>[Managing access with ACLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acls.html)</small>

### Endpoints
Amazon S3 provides a [Website endpoint](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteEndpoints.html) and a [REST endpoint](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html). The website endpoint follows one of these two formats: 
- `http://<bucket>.s3-website-<region>.amazonaws.com/<key>` 
- `http://<bucket>.s3-website.<region>.amazonaws.com/<key>` 

Note the **dot** or **dash** between `s3-website` and `<region>`. The right format depends on your region, for example US East (N. Virginia) uses `<bucket>.s3-website-us-east-1.amazonaws.com` while Europe (Frankfurt) uses `<bucket>.s3-website.eu-central-1.amazonaws.com`. The full list is available on the [S3 service endpoints](https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_website_region_endpoints).

> Amazon S3 website endpoints do not support HTTPS or access points. If you want to use HTTPS, you can use Amazon CloudFront to serve a static website hosted on Amazon S3.
<small>[Key differences between a website endpoint and a REST API endpoint](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteEndpoints.html#WebsiteRestEndpointDiff)</small>

The REST endpoint supports both a virtual-hosted-style and a path-style format: 
- `https://<bucket>.s3.<region>.amazonaws.com/<key>` 
- `https://s3.<region>.amazonaws.com/<bucket>/<key>` 

Note the `<bucket>` is in the host portion or in the path portion of the URL. However, the path-style format has been [deprecated](https://aws.amazon.com/blogs/aws/amazon-s3-path-deprecation-plan-the-rest-of-the-story/) and shouldn't be used. 

You may notice, that the REST endpoint supports HTTPS. However, it can't be used for static website hosting in a setup like this. The _modern approach_ of hosting a website on Amazon S3 uses the REST endpoint instead of the website endpoint. More on that in my future post. 

### Index Document
The [index document](https://docs.aws.amazon.com/AmazonS3/latest/userguide/IndexDocumentSupport.html) is the name of the file that Amazon S3 returns for a request to the root or any subfolder. 

> If you configure your website with `index.html` as the index document, either of the following URLs returns `index.html`.
> `http://<bucket>.s3-website.<region>.amazonaws.com/`
> `http://<bucket>.s3-website.<region>.amazonaws.com`
<small>[Index document and folders](https://docs.aws.amazon.com/AmazonS3/latest/userguide/IndexDocumentSupport.html#IndexDocumentsandFolders)</small>

If your bucket has subfolders, the index document must have the same name on each level. However, which file is returned depends if the URL ends with a slash `/`.

> When a user specifies a URL that resembles a folder lookup, the presence or absence of a trailing slash determines the behavior of the website. For example, the following URL, with a trailing slash, returns the `photos/index.html` index document.
`http://bucket-name.s3-website.Region.amazonaws.com/photos/``

> However, if you exclude the trailing slash from the preceding URL, Amazon S3 first looks for an object `photos` in the bucket. If the photos object is not found, it searches for an index document, `photos/index.html`.
<small>[Index document and folders](https://docs.aws.amazon.com/AmazonS3/latest/userguide/IndexDocumentSupport.html#IndexDocumentsandFolders)</small>

### Error Document
The [error document](https://docs.aws.amazon.com/AmazonS3/latest/userguide/CustomErrorDocSupport.html) is the file that is returned when an error occurs, but only for HTTP status code _404 Not Found_. 

> The website endpoint responds with _404 Not Found_ for the following reasons:
- Amazon S3 determines that the URL of the website refers to an object key that does not exist.
- Amazon S3 infers that the request is for an index document that does not exist.
- A bucket specified in the URL does not exist.
- A bucket specified in the URL exists, but isn't configured as a website.

><small>[Amazon S3 HTTP response codes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/CustomErrorDocSupport.html#s3-http-error-codes)</small>

## HTTPS
Amazon CloudFront is a global content delivery network (CDN) and provides caching, security and other features. We are using it to [serve HTTPS requests for the Amazon S3 bucket](https://repost.aws/knowledge-center/cloudfront-https-requests-s3). 

### Origin
When you create a new distribution, you have to select your S3 bucket endpoint as the origin domain. Note that the dropdown for S3 buckets only shows the REST endpoints (`<bucket>.s3.<region>.amazonaws.com`). However, if you select an entry, CloudFront will notice you that you should be using the S3 website endpoint `<bucket>.s3-website-<region>.amazonaws.com`, because your bucket has static website hosting enabled. 

There are various ways to use [Amazon S3 as origin](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html#using-s3-as-origin) for CloudFront. We're using an Amazon S3 bucket that's configured as a website endpoint.

The Amazon S3 website endpoint only supports HTTP connections. That's the reason the protocol between CloudFront and the origin S3 will be HTTP only. This is different to the protocol between the viewer and CloudFront, which can be configured to be HTTPS. 

### HTTPS
The [HTTPS support](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-viewers-to-cloudfront.html) is a CloudFront behavior and has two options: redirect HTTP to HTTPS or HTTPS only. The first option will redirect with status code _301  Moved permanently_ while the latter will all HTTP requests via status code _403 Forbidden_.  

## Custom Domain
To use our own custom domain, we have to add an [alternate domain name (CNAME)](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-alternate-domain-names.html) and a custom SSL certificate. The certificate must be [requested](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html) or [imported](https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) into AWS Certificate Manager (ACM). 

### Certificate
When [requesting](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html#request-public-console) a new certificate, it makes sense to add a domain name (such as `example.com`) and another name with wildcard (such as `*.example.com`) to the certificate, because an existing certificate can't be changed.  

> To use a certificate in AWS Certificate Manager (ACM) to require HTTPS between viewers and CloudFront, make sure you request (or import) the certificate in the US East (N. Virginia) Region (us-east-1).
<small>[AWS Region for AWS Certificate Manager](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html#https-requirements-aws-region)</small>

### DNS
The DNS settings must be updated to route the traffic to the CloudFront domain name for your distribution. This step depends on your DNS provider. If you've registered your domains with Amazon Route 53, routing to CloudFront is straightforward. Create an [Alias record](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-cloudfront-distribution.html#routing-to-cloudfront-distribution-config) that points to the corresponding CloudFront distribution's domain.  

> An alias record is a Route 53 extension to DNS. It's similar to a CNAME record, but you can create an alias record both for the root domain, such as example.com, and for subdomains, such as www.example.com. (You can create CNAME records only for subdomains.)
<small>[Choosing between alias and non-alias records](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-choosing-alias-non-alias.html)</small>

The [DNS record type](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html) can be `A` for IPv4 addresses and `AAAA` for IPv6 addresses.

> If IPv6 is enabled for the distribution, create two resource record sets to route traffic to your distribution, one with a value of A and one with a value of AAAA.
<small>[AWS CloudFormation: Route53 RecordSet Type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html#cfn-route53-recordset-type)</small>

The alias is the target resource that the traffic is being routed to. When you work with the CloudFormation template directly, you have to specify a hosted zone id. This is always the same for CloudFront distributions. 

> Specify `Z2FDTNDATAQYW2`. This is always the hosted zone ID when you create an alias record that routes traffic to a CloudFront distribution.
<small>[AWS CloudFormation: Route53 RecordSet AliasTarget](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html#cfn-route53-aliastarget-hostedzoneid)</small>

## CloudFormation Template
This template requires three inputs:

- ARN of an existing ACM certificate
- Name of the S3 that hosts the website
- Name of the Route53 domain

It can be deployed via [CloudFormation console](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-console-create-stack.html) or the [AWS CLI](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-cli-creating-stack.html). 

After the stack was created and deployed, it outputs three values:

- HTTP URL of the S3 website endpoint `http://<bucket>.s3-website-<region>.amazonaws.com`
- HTTPS URL of the CloudFront endpoint `https://<distribution>.cloudfront.net`
- HTTPS of the Route53 domain name `https://<domain>`

{% embed https://github.com/zirkelc/cloudformation-s3-static-website-https %}

---

I hope you found this post helpful. If you have any questions or comments, feel free to leave them below. If you'd like to connect with me, you can find me on [LinkedIn](https://www.linkedin.com/in/christian-zirkel/) or [GitHub](https://github.com/zirkelc). Thanks for reading!