How to get a Serverless web app working with your own domain and SSL on AWS, the right way

Recently, I built a very simple CMS software to serve as the engine for my website La Siesta Americana (literally The American Siesta, like The American Dream but lazier). Why to build a CMS in 2020? There’s actually little or no reason, but I wanted to publish my content and literally no option in the market satisfied me, so I built my own for fun and to make a statement: we need no complex services, no plugins, no templating engines, no databases to publish content.

When my good friend and business partner Pedro saw it working, suggested that I published the software, and named it “Siesta” or similar, and here we are, ten hours later. I thought a CMS worth of its name needed to be shown in action, so I decided to use it to build its own product website.

The application is based on the Serverless framework, and I wanted to deploy it to my AWS account using a custom domain, with HTTP to HTTPS redirection that worked both for the www domain and the naked version (the one with no www). So, in other words: http://siestacms.com, https://siestacms.com, http://www.siestacms.com all needed to redirect to https://www.siestacms.com seamlessly.

Image for post
My final setup

I also wanted two subdomains: dev.siestacms.com to host the development stage, and prod.siestacms.com to host the production one. www.siestacms.com would replicate or act as an alias for the production subdomain. The naked domain siestacms.com will just redirect to www, but I wanted such redirection to work for both HTTP and HTTPS.

If you’ve played around with AWS CloudFront you know that was going to be an epic battle.

The ultimate step-by-step guide

Since this was more complex than I first thought, I want to share my struggle so humanity won’t solve this problem more than once. This guide will walk you through the process I followed.

First, create your app

I used Serverless framework to build my service. You can check out the code on GitHub if you want to use it as a template. I’ll be using it as a reference to make my instructions a bit easier to understand.

Create your domain and setup a certificate

I registered my domain on Route 53, but you can use whatever you prefer. If you’re using another registrar, you’ll need to create a hosted zone on Route 53 for your domain, and point their DNS servers there.

You’ll need a certificate for your domain. This is free and takes 5 minutes, so go to AWS Certificate Manager and select “Request a certificate”, then “Public” and enter 2 domain names for your certificate: *.yourdomain.com and yourdomain.com. Having one certificate for both domain names will save you some suffering later.

Also, if you want your certificate to play nice with CloudFront, you’ll have to create it under the us-east-1 zone.

Image for post

AWS will ask you to verify that you own the domain. I usually do it with DNS but this is up to you. It’ll take a few minutes so go grab some coffee.

Once your certificate is issued and verified, you can go ahead and prepare your application to use this domain.

Configure your Serverless application to use a custom domain

I used the plugin serverless-domain-manager and configured it this way:

customDomain:
domainName: ${self:provider.stage}.siestacms.com
certificateName: '*.siestacms.com'
basePath: ''
stage: ${self:provider.stage}
createRoute53Record: true
endpointType: 'regional'
autoDomain: false

The domain name is based on the stage, so this way I could have dev.siestacms.com for dev stage and prod.siestacms.com for prod.

Deploy your lambdas

First, you have to run Serverless command sls create_domain to create the basic resources (the A/AAAA records on AWS Route 53 as well as the custom domain on AWS API Gateway).

Once that’s ready, deploy your code with sls deploy.

In my case, I wanted to setup my dev environment first:

$ sls create_domain --stage=dev$ sls deploy --stage=dev

The first command creates a custom domain on API Gateway (dev.siestacms in my case) and an A/AAAA pair of DNS records on AWS Route 53.

Image for post

These DNS records will point to a new custom domain that should appear under the API Gateway custom domain list:

Image for post

API Gateway exposes its own internal domain name (you can see it under “Endpoint configuration”, listed as “API Gateway domain name”). This must match the DNS records on Route 53.

Once this is done, we just need to wait for about 40 minutes, and our domain will start accepting traffic. But you can do some tests in the meantime.

Test your Lambda functions

Now, if you try to access the API Gateway internal domain directly from your browser it won’t work, but don’t freak out. That endpoint is supposed to be called via your custom domain. You can check the behavior with curl:

If you just try to access the content, it’ll fail:

$ curl https://d-705khj8nx2.execute-api.us-east-1.amazonaws.com
{"message":"Forbidden"}

However, if you set the Host header to your domain, it should work. API Gateway’s custom domains are just a system that allows you to expose an application through a hostname.

$ curl --header "Host: dev.siestacms.com" https://d-705khj8nx2.execute-api.us-east-1.amazonaws.com

At this point, our Lambda application should be accessible using its subdomain. Please make sure you add https to test it, as we are not managing the redirection (yet).

Configure the production application

Once the dev environment was working, I prepared the one for production:

$ sls create_domain --stage=prod$ sls deploy --stage=prod

And now we’re ready to configure our production subdomain, www. This subdomain will point behind the scenes to whatever we’re running on the prod one.

One alternative solution here would be to create a new custom domain for www on API Gateway that points to our production Lambda application. However, in that case we won’t be able to redirect HTTP to HTTPS, as Lambda functions can listen only to HTTPS traffic. Also, I didn’t want to link the A/AAAA records directly to the API Gateway custom domains, as they change every time you run the create_domain command, potentially breaking the application and requiring a manual update.

In order to fix these two issues we need some good old CloudFront magic. We need to create two different distributions:

  1. One distribution will listen to the www subdomain (www.siestacms.com in my example) and connect it with the prod environment through its subdomain. This distribution will also redirect the traffic to HTTPS.
  2. And another distribution will listen to the naked domain (siestacms.com in this case) for both HTTP and HTTPS, and redirect the requests to the HTTPS, non-naked version (https://www.siestacms.com).

Create the CloudFront distribution for the Lambda application

To create the first distribution, we’ll set the following options:

  • Origin Domain Name: the destination (yup) subdomain, in this case prod.siestacms.com. Don’t specify the protocol in that field, just the subdomain.
  • Origin Path: blank.
  • Minimum Origin SSL Protocol: Select TLSv1.2.
  • Origin Protocol Policy: “HTTPS Only” (remember this is the protocol that our destination accepts. Our Lambda application works only on HTTPS).
  • Viewer Protocol Policy: “Redirect HTTP to HTTPS”. We need this so HTTP traffic that comes through www will be redirected to HTTPS.
  • Cache Policy: I’d suggest you play it safe and select “Managed-CachingDisabled” as the policy. This way your CloudFront distribution won’t cache anything, making this easier to debug.
  • Alternate Domain Names (CNAMEs): enter your domain with www, in my case it’d be www.siestacms.com
  • SSL Certificate: select the certificate you created for your domain.

And that should be it. Grab more coffee and start working in the second distribution.

Create the CloudFront distribution for the naked domain

This second distribution will listen to the naked domain (remember, the root one, with no www) and redirect all the traffic, HTTPS or not, to the www subdomain, always over HTTPS.

To do this, the easiest way as far as I know is to use S3. Yeah, it sounds hacky but is not THAT bad. We need to create a bucket on S3 with the same name as our domain’s (this is super important!)

Then, we’ll deselect “Block all public access” in the options and confirm that we know whatever we’re doing. Once the bucket is created, we’ll open it and go to the “Properties” tab to set up the redirection. Select “Static website hosting” and then enable it. Select “Redirect requests for an object” and enter the URL with www: www.siestacms.com in my case. Then mark “https” as the protocol.

Now, why can’t we just point the root A record to the S3 bucket? We could, but S3 buckets don’t support HTTPS. If we want to support the redirection over SSL too, we need to use CloudFront.

Now that our S3 bucket is correctly configured, we’ll create our CloudFront distribution with this configuration:

  • Origin Domain Name: You might feel tempted to select your bucket from the list but don’t. Instead, just type: yourdomain.com.s3-website-us-east-1.amazonaws.com. In my case, this was siestacms.com.s3-website-us-east-1.amazonaws.com. Why? Who knows.
  • Origin Path: blank.
  • Viewer Protocol Policy: Redirect HTTP to HTTPS as well.
  • Alternate Domain Names (CNAMEs): enter your naked domain. In my case it was siestacms.com.
  • SSL Certificate: select the certificate you created for your domain.

And finally, we just need to configure our domain to send traffic to both distributions.

Configure the domain to use the CloudFront distributions

Back to Route 53!

For our www domain, select “Create a record”, then “Simple routing” and then “Define simple record”. Then we type “www” in the “Record Name” field, and select “Alias to CloudFront Distribution” as the value. We select the region and then our CloudFront distribution associated to www should appear in the dropdown.

We’ll repeat this step for the naked domain, the difference is that we’ll leave the “Record Name” field empty (as we want to assign the record to our root domain).

And that should be it! Changes might take some time to propagate so be patient. Make sure you hard-refresh your browser before you start running in circles or making changes.

Hope this helps!

Troubleshooting

There are a lot of moving parts here, so naturally lots of things can go wrong. If you find yourself into trouble, I’d suggest you debug down-top. In other words:

  1. Start with your Lambda functions, get their URL and invoke them. Do they work?
  2. If the Lambda functions work, the next stop would be API Gateway. Do the custom domains work as well? Are the API mappings correctly set? Can you access the Lambda application via the custom domain using curl?
  3. If that works, is CloudFront configuration fine? Is the distribution pointing to the custom domain? Is the distribution still being deployed or updated? Is it enabled?
  4. And lastly, are the Route 53 records pointing to the right CloudFront distributions?

This is my configuration, for you reference:

CloudFront:

Image for post

Route 53:

Image for post

Written by

I’ve been into software engineering for the most part of my life so I have thought long and hard about it. Now I‘m just writing it down.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store