Using private domains for APIs with Serverless Framework and Terraform

Roberto Javier Yudice Monico
4 min readNov 23, 2020
Photo by Maarten Deckers on Unsplash

By default when creating an AWS API Gateway you will get an AWS generated domain name, which is neither easy to remember or reliable and it could change if you for some reason need to recreate your API gateway or the whole cloud formation stack. While normally the chances of recreating your gateway are low, this is not the case when working with the Serverless Framework, since it internally uses a cloud formation stack which can sometimes get desynched and you might need to recreate it. Also as mentioned before the DNS name created by AWS is not easy to remember and is not expressive either.

The generated DNS is also hard to remember as it is a subdomain with a bunch of random characters. To make your API’s more friendly you definitely want a domain name that is easy to remember or at least that it is readable so that it sticks more easily.

In our case we typically use custom domain names to differentiate between environments, such as “your-api.yourcompany.dev” or “your-api.yourcompany.staging” and so on. As you can see this are not standard TLD’s. We keep all of our API’s internally so that’s why we can work like this, I’m aware it might not be the case for you. With that said, we wanted to have friendlier DNS names for our API’s and we wanted them to be created through the Serverless Framework.

Serverless Domain Manager

Serverless Domain Manager is a Serverless Framework plugin for managing custom domains in AWS. It makes it much easier as you can define your API gateway domain within your serverless.yml file. It also gives you the possibility of set other options that are part of the domain creation, you can read more about it in the documentation.

Creating the Certificates

API gateway enforces HTTPS, which means that if you want to create a custom domain you have to also create a SSL Certificate that is valid for the domain that you want to create, and you will also have to create a CA (Certificate Authority). For this we are going to use the following Terraform script:

This is the locals that we will use for the whole tutorial, we will not use all of them for this first piece of the CA.

locals {
private_key_algorithm = "RSA"
private_key_rsa_bits = "2047"
private_key_ecdsa_curve = "P256"
ca_common_name = "your-company.dev"
ca_organization_name = "Your Company Development"
dns_names = [
"your-api-gateway.your-company.dev"
]
}

And this is where we create the Certificate Authority:

resource "tls_private_key" "ca" {
algorithm = local.private_key_algorithm
ecdsa_curve = local.private_key_ecdsa_curve
rsa_bits = local.private_key_rsa_bits
}
resource "tls_self_signed_cert" "ca" {
key_algorithm = tls_private_key.ca.algorithm
private_key_pem = tls_private_key.ca.private_key_pem
is_ca_certificate = true
validity_period_hours = 26280
allowed_uses = [
"cert_signing",
"key_encipherment",
"digital_signature",
]
subject {
common_name = local.ca_common_name
organization = local.ca_organization_name
}
}

Now that we have the Certificate Authority (CA) we can generate our certificates using this authority. The reason why we are using a CA is because then you can trust your CA in yours server to remove any self-signed cert warning. By trusting your CA you automatically trust all of the certificates generated based on the CA which is better for security and makes maintenance easier also.

resource "tls_private_key" "cert" {
algorithm = local.private_key_algorithm
ecdsa_curve = local.private_key_ecdsa_curve
rsa_bits = local.private_key_rsa_bits
}resource "tls_cert_request" "cert" {
key_algorithm = "${tls_private_key.cert.algorithm}"
private_key_pem = "${tls_private_key.cert.private_key_pem}"
dns_names = local.dns_names subject {
common_name = local.common_name
organization = local.organization_name
}
}
resource "tls_locally_signed_cert" "cert" {
cert_request_pem = "${tls_cert_request.cert.cert_request_pem}"
ca_key_algorithm = "${tls_private_key.ca.algorithm}"
ca_private_key_pem = "${tls_private_key.ca.private_key_pem}"
ca_cert_pem = "${tls_self_signed_cert.ca.cert_pem}"
validity_period_hours = "87600"
allowed_uses = [
"key_encipherment",
"digital_signature",
]
}

Getting the Certificate into ACM

Now we need to get the certificate into AWS ACM so that we can use it in our API Gateway.

The Certificate Chain

Now there is a tricky part in getting the certificate into ACM: You have to provide the certificate chain and currently Terraform doesn’t have a straightforward way to get it. However if you are familiar enough with TLS you will know that the certificate chain is simply your CA’s PEM and your Certificate’s PEM appended together in a text file, so we can generate simply by doing this:

resource "aws_acm_certificate" "pricing-gateway-apigateway-cert" {
private_key = tls_private_key.cert.private_key_pem
certificate_body = tls_locally_signed_cert.cert.cert_pem
certificate_chain = <<-EOT
${tls_self_signed_cert.ca.cert_pem}
${tls_locally_signed_cert.cert.cert_pem}
EOT
tags = {
Name = "self-signed-cert generated by terraform "
}
}

Notice that the certificate chain we are passing are the PEM’s of our CA and our Cert joined in a string.

Lastly we will need the ARN of our certificate to use in the Serverless Framework configuration so we need to add an output:

output "certificate_arn" {
description = "Certificate ARN"
value = aws_acm_certificate.acm-cert.arn
}

Adding the Certificate to your serverless.yml File

Now that we have the certificate we can set it up on our serverless.yml file. First you will need to setup the serverless domain manager plugin, please refer to its documentation.

After setting up the plugin you should have a “customDomain” section in your serverless.yml. This is how it will look with the certificate configured:

customDomain:
domainName: your-api-gateway.your-company.dev
basePath: '' # Adjust this to your needs
stage: ${self:provider.stage}
createRoute53Record: true # Adjust this to your needs
certificateArn: <Your Certificate ARN output from the Terraform script>
endpointType: 'regional' # Adjust this to your needs
hostedZoneId: <Your Hosted Zone ID>
hostedZonePrivate: true # Adjust this to your needs

A few things…

  • You will get certificate warnings on browsers and on HTTP clients from mostly any language platform because you are using a self-signed certificate. You can avoid this by trusting your CA at the operating system level. Here is how to do it in windows and in linux. Generally your Devops team should have an efficient way to do it.
  • You can avoid the certificate warnings by generating a CA using Amazon’s AWS Certificate Manager Private Certificate Authority. Be wary that it’s very pricy if you only need a few certificates.

--

--