A custom domain name in front of ECR

Amazon Web Services’ ECR (Elastic Container Registry) is a great solution for hosting a private docker image registry.

At Codeface we use ECR to host ready-made development environments for all our projects. On each project we have a docker-compose.yml file that will contain a line like:

image: 123456789012.dkr.ecr.eu-west-2.amazonaws.com/myapp:latest

When someone dives in to work on one of our projects, they just grab the ready-made development environment from that ECR address, and don’t have to wait for a docker build to complete.

But that is one ugly looking url, and it just feels a bit wrong to have such a specific ECR address baked into our code. What we really wanted to see in there was something more like…

image: container-registry.codeface.com/myapp:latest

That would give us more flexibility in future - if we decided to use ECR on a different AWS account, or use a different service altogether, we wouldn’t have to change the address in potentially dozens of different places.

It turns out that right now (early 2024), Amazon don’t make it easy to have a custom domain like that in front of ECR. Or at least, they don’t make it trivial - there’s no built-in setting to allow this.

If you simply add a CNAME to point at the relevant registry url, unsurprisingly the first hurdle is certificate errors:

Error response from daemon: Get "https://custom-name.codeface.com/v2/": x509: certificate is valid for *.dkr.ecr.eu-west-2.amazonaws.com, *.dkr.ecr.eu-west-2.vpce.amazonaws.com, not custom-name.codeface.com

And there’s no option available at ECR to add a custom name and attach a certificate from Amazon Certificate Manager or anything handy like that.

However, AWS does have a couple of services that spring to mind where you can set up a custom name and attach a certificate - namely, CloudFront and API Gateway.

And fortunately some smart people on this discussion of the issue on github have mapped out some solutions. In particular, the solutions from amancevice and naftulikay inspired me to have a go.

The proposed solution from amancevice is packaged up using Terraform, and the one from naftulikay uses a Lambda function running a docker image. These were not quite what I wanted, but gave me enough confidence to try setting up a basic solution from scratch, just using the AWS web control panel. It turns out not to be too scary at all. Here’s what you need to do:

Set up a simple Lambda function

Set up simple Lambda function to do a 307 redirect for all requests. I grabbed this from the source code of the Terraform project mentioned above:

"use strict";
const AWS_ECR_REGISTRY = process.env.AWS_ECR_REGISTRY;
exports.handler = (event, context, callback) => {
  console.log(`EVENT ${JSON.stringify(event)}`);
  const path = event.rawPath;
  const location = `https://${AWS_ECR_REGISTRY}${path}`;
  const redirect = { statusCode: 307, headers: { location: location } };
  callback(null, redirect);
};

(NB When I first tried setting this up, I pasted in the code and it got automatically named as a .mjs which didn’t work, so I manually renamed it as .js which fixed the problem)

Set up an API using API Gateway

So now we’ve got a Lambda function which will do a 307 redirect for all requests. Lambda offers an option “Configure Function URL” which gives us a quick and easy way to attach an https url to our lambda function. However there’s no option to add a custom domain name to that, so we need to use another AWS service, API Gateway.

(An alternative might be CloudFront, but it sounded from the discussion that people were getting intermittent problems with that)

I’d actually never used API Gateway before. I was pleasantly surprised at how easy it was to set up an API.

It went something like this:

Create the API

We just need one HTTP API which we’ll configure to handle any requests with our Lambda function.

Create the default route

We’ll just need one route, a default route which handles all incoming requests:

Attach the Lambda function to the default route

We want any requests to our default route to be handled by our Lambda function; to achieve that we create an “integration” and link the route to it:

Create a Custom Host Name

Before setting up a Custom domain name in API gateway, you first need to get a certificate for the name you plan to use. Once we’ve done that, we can set up the custom name:

Set up a CNAME at domain registrar

Now that our Custom Domain Name has been set up at API Gateway, we can grab the ‘API Gateway domain name’ and that’s what we need to point our custom hostname at at the registrar. This will be a host name something like d-abc123aaaa.execute-api.eu-west-2.amazonaws.com

Once that CNAME has been set up, we can check it with dig container-registry.codeface.com cname and expect to get an answer something like…

;; ANSWER SECTION: container-registry.codeface.com. 600 IN CNAME d-abc0aa0aa0.execute-api.eu-west-2.amazonaws.com.

So, now any request for container-registry.codeface.com/blah will get a 307 redirect to the corresponding backend ECR address.

I can confirm that with

curl -v https://container-registry.codeface.com/blah

and in the response I see…

< HTTP/2 307
< content-length: 0
< location: https://123456789012.dkr.ecr.eu-west-2.amazonaws.com/blah

It works!

So now, once I’ve logged in using… aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin container-registry.codeface.com

…I can do e.g….

docker pull container-registry.codeface.com/my-project:latest
docker push container-registry.codeface.com/my-project:latest

And I can update all my docker-compose.yml files with the much neater:

image: container-registry.codeface.com/myapp:latest

Super-satisfying to have this up and running. It felt like something that ought to be possible, but I must confess I went round in circles a fair bit before discovering the solution described here!

Valid HTML This page was handcrafted in Brighton by Codeface