I’ve been using the image registry on GitLab for quite a while now and loved the convenience of having my images living in the same place as my code. However, recently GitLab introduced a soft limit on transfers and that’s cramping my style. I’m moving a lot of my images onto Amazon Elastic Container Registry (ECR). In this post I look at how to get this set up.
Setup Registry on ECR
The first thing that we’ll need to do is get a repository set up on ECR.
- Go to the ECR dashboard.
- Press .
- Choose a name for the repository and press . For the purpose of this post we’ll assume that the repository name is “crawler”.
- Select the newly created repository and press . This will give you all of the information that you need for login, tagging and pushing. Take a screenshot of this for future reference.
AWS Permissions
You can use existing AWS credentials. If you don’t have a suitable AWS user for pushing an image to ECR then you’ll need to create one. The quick and easy approach to this is to create an IAM user with programmatic access (I name it “registry”) and AmazonEC2ContainerRegistryFullAccess
permissions. These permissions are probably too permissive, but they will work for now.
Add the access key and secret access key to your ~/.aws/credentials
file. Let’s call that profile entry “registry” too (although you can choose another, more appropriate name!).
[registry]
aws_access_key_id = AKIAZUYM3M4576QJWSX6
aws_secret_access_key = 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
Test ECR Login
We can immediately test access to the repository by requesting login credentials using the aws
CLI. First we’ll set up a couple of environment variables to store the profile name, region and AWS account ID.
AWS_PROFILE=registry
AWS_REGION=us-east-1
AWS_ACCOUNT_ID=999999999999
Now request the credentials.
aws ecr get-login-password --region $AWS_REGION --profile $AWS_PROFILE
If all goes well then the response will be a Base64 encoded string. You can decode that with base64 -d
and then pipe into jq
. The result should be something like this (the values for the payload
and datakey
fields have been redacted):
{
"payload": "...",
"datakey": "...",
"version": "2",
"type": "DATA_KEY",
"expiration": 1645469652
}
The expiration value indicates (use https://www.epochconverter.com/ to convert to date and time) that the credentials are valid for 12 hours.
Pushing by Hand
Let’s start by taking a look at how to manually push an image to ECR. First we’ll need to connect our local Docker to the repository.
# Store credentials in environment variable.
ECR_LOGIN=$(aws ecr get-login-password --region $AWS_REGION --profile $AWS_PROFILE)
# Docker login to repository.
docker login --username AWS --password $ECR_LOGIN $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
Create a suitable tag.
# Store image name.
IMAGE=crawler
# Create tag (which bundles image name and account ID).
TAG_LATEST=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE:latest
Let’s see what that looks like.
echo $TAG_LATEST
999999999999.dkr.ecr.us-east-1.amazonaws.com/crawler:latest
It’s a bit clunky, but that’s because it includes not only the name for the image but also the AWS account ID and region.
Now all we need to do is push.
docker push $TAG_LATEST
If you pop over to the repository on ECR you’ll find that it now has an image in it. To use the image, click on the image tag in ECR and copy the image URI.
Pushing with a Makefile
Okay, so now we have established that the whole things works. Cool. But performing each of those steps every time we want to update the image is going to get very tiresome very quickly. Time to roll in some automation. We’ll do this incrementally. First we’ll set up a Makefile
which can be used to orchestrate the steps in the process with minimal typing. Then we’ll move the entire workload across onto GitLab CI.
But first the Makefile.
Start off by setting up some variables. These mirror the ones that we created in BASH above.
IMAGE = crawler
AWS_PROFILE = registry
AWS_REGION = us-east-1
AWS_ACCOUNT_ID = 999999999999
AWS_SERVER = $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com
TAG_LATEST = $(AWS_SERVER)/$(IMAGE):latest
Now define three targets: build
, login
and push
.
build:
docker build --cache-from $(TAG_LATEST) -t $(TAG_LATEST) .
login:
aws ecr get-login-password --region $(AWS_REGION) --profile $(AWS_PROFILE) | \
docker login --username AWS --password-stdin $(AWS_SERVER)
push: build loging
docker push $(TAG_LATEST)
The build
target will simply update the local image. The login
target probably never needs to be run explicitly. However, the push
target will run it implicitly. It’ll also run the build
target and then push the resulting image onto ECR.
Once the Makefile
is in place it’s a simple process to build and push the image.
make build # Not strictly necessary (implicit in "push" target).
make push
GitLab
Okay, so the Makefile
significantly reduced the amount of work required to get the image onto ECR. But ideally I don’t even want to have to even have to run make
. What if the image could be built and transferred to ECR each time I pushed my work to the remote Git repository? Let’s enlist GitLab CI to do precisely that.
There’s a little configuration work required on GitLab to get this working. I set up the following CI environment variables:
AWS_ACCOUNT_ID
AWS_REGION
AWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
.
The access key and secret access key are those for the IAM user created earlier.
The CI pipeline is configured via the .gitlab-ci.yml
file.
stages:
- build
variables:
IMAGE_NAME: crawler
TAG_LATEST: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:latest
TAG_COMMIT: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA
DOCKER_TLS_CERTDIR: ""
docker:
image: docker:stable
stage: build
only:
- master
services:
- docker:dind
before_script:
- apk add --no-cache python3 py3-pip
- pip3 install --no-cache-dir awscli
script:
- aws ecr get-login-password --region $AWS_REGION |
docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
- docker pull $TAG_LATEST || true
- docker build --cache-from $TAG_LATEST -t $TAG_COMMIT -t $TAG_LATEST .
- docker push $TAG_COMMIT
- docker push $TAG_LATEST
The variables are similar to the ones that we created in the Makefile
. I’ve added in another tag now which is linked to the commit SHA. This will mean that, in addition to the latest image being available on ECR, I’ll also have a history of all previous images. The meat of the configuration lies in the script
section of the image
job. Let’s step through each command:
- Login to the Docker registry on ECR. Unlike the corresponding command in the
Makefile
we’re not using the--profile
option here because the corresponding information is picked up from theAWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
environment variables. - Pull the most recent version of the image. This will speed up the build process because it will act as a cache and prevent unchanged layers from being rebuilt.
- Build the image (using the most recent version of the image as a cache). Add two tags.
- Push using the commit tag.
- Push using the
latest
tag.
Conclusion
I’ll be using this as a reference whenever I forget one of the stages in getting this all set up. I hope that it’s useful to you too.