Deploying Domain Mappings in GCP with GitLab CI
Many projects I’ve worked on of late have been deployed to Google’s Cloud Run service, a Container as a Service facility in Google Cloud Platform (GCP). Creating domain mappings for the services always remained a manual task after deployment, This post details how I’ve now started to deploy the domain mappings for those serverless instances in an automated fashion.
The Set Up
Most of these projects have a very similar setup, even though the application’s technologies differ quite a bit.
The application is built, packaged in a docker image, which is published to the container registry for the GCP project, after which the deployment of that image is issued towards Cloud Run, where it’s exposed to the outside world.
Cloud Run supports the creation of domain mappings, which configure the domain name (of a domain you are the verified owner of, of course) you would like to route to the instance that is deployed.
For instance, requests to my-site.my-domain.com
should be directed to the instance deployed in Cloud Run with identifier my-instance-production
, whereas those for test.my-domain.com
should go to the my-instance-test
instance.
Some projects are hosted in Bitbucket, others in GitLab instances; the pipelines to build them use the native pipelines features that those platforms offer and differ mainly in configuration syntax for these projects. For the purposes of the examples below, the GitLab flavour is used.
The Problem
Setting up the domain mappings in Google’s Cloud Console is a relatively straightforward process. You navigate to the project, then to the Cloud Run section to view the deployed instances and click a button to manage the domain mappings. When creating a new mapping, you select the instance you want to direct the traffic to, choose from the verified domains and configure the (sub)domain that should point to it. Then you just wait for the provisioning of the certificates and routing to complete setting up so you can start using the mapping. This action is performed once; subsequent deployments of the application’s Cloud Run instance don’t invalidate the mapping. Traffic for the domain mapping will be routed to the new revision of the instance automatically.
So far, so good. Once you’ve discovered the pattern for a couple of projects, it’s easy to set up. But it still requires the manual action to set up the mapping. Surely this can be automated?
The Google Cloud SDK, which is used in these projects to interact with the GCP APIs, offers a way to create these domain mappings automatically.
$ gcloud run domain-mappings create \ --service=my-instance-production \ --domain=my-site.my-domain.com
There are various other options that can be supplied, but that’s the gist of it.
This action can be added to our pipeline steps and that’s that.
At least, that’s what you might think.
This will definitely work, the first time you run the pipeline.
Subsequent runs will fail, because the pipeline will attempt to create the domain mapping again, which will be -rightfully- rejected by GCP.
The new problem is that although there are APIs to create, to delete and to list the domain mappings, there is no command to create-if-none-exists-yet.
What’s missing is something similar to the apply
command that kubectl
offers.
The Solution
Why this command doesn’t exist in the API, is a whole different discussion. Fortunately, we can use our pipeline to configure a conditional action, of sorts.
As it turns out, there is a command that will describe
a domain mapping.
If the mapping exists, a set of metadata about it is returned.
If the mapping doesn’t exist, the command will fail.
Assuming we have an environment variable DOMAIN
for the target mapping, we can issue the following command.
$ gcloud beta run domain-mappings describe \ --domain=${DOMAIN} \ --format='value(spec.routeName)'
Example output of this command would be (shortened for brevity):
apiVersion: domains.cloudrun.com/v1
kind: DomainMapping
metadata:
annotations:
serving.knative.dev/creator: <creator-email-address>
creationTimestamp: '2022-11-13T12:02:22.975505Z'
generation: 1
labels:
cloud.googleapis.com/location: europe-west4
run.googleapis.com/overrideAt: '2022-11-13T12:02:29.649Z'
name: my-site.my-domain.com
uid: 6787bcfa-3156-4fb0-a5c4-00aa7da7139c
spec:
certificateMode: AUTOMATIC
routeName: my-instance-production
status:
conditions:
...
mappedRouteName: my-instance-production
observedGeneration: 1
resourceRecords:
- name: my-site
rrdata: ghs.googlehosted.com.
type: CNAME
The details are actually not that important, but the domain name and the instance are clearly recognisable.
The final section is used to instruct you to create the appropriate CNAME
records in the domain’s DNS so they point to Google’s gateway.
In a first attempt to automate this, you might turn to the option to add a bit of multiline scripting to a pipeline’s script.
First, issue the describe
command.
Then, check the status of the previous command using the standard Bash variable, $?
, which will hold values 0
or 1
for successful and failed commands, respectively.
You would be on the right track, but checking $?
will not work in case the domain mapping is not yet in place.
This is due to the fact that a failed command to describe
the domain mapping immediately fails the pipeline too.
Another Bash feature comes to the rescue, though.
Instead of allowing the describe
command to fail on its own, a one-liner or statement can make sure the command always succeeds, but that we also capture the result of trying to describe the domain mapping.
The general structure to do that looks like the statement below.
$ some_command_that_may_fail || some_command_that_always_succeeds
By adding a variable assignment to the or
part of the statement (the part that will only run if the first part - our describe
- is unsuccessful), a subsequent if
block can act according to the value of that variable (or its non-existence).
Our job in the pipeline could look like the section below.
Note the use of the >
character to open a multiline block in the YAML and the assignment of the NO_MAPPING
variable in the or statement.
The if
block either creates the domain mapping for the first time when the pipeline runs, or simply logs that it is already in place and doesn’t perform the create
action again.
The variables used, such as $MY_INSTANCE
, $IMAGE_NAME
and $DOMAIN
are set up elsewhere in the pipeline, making them available for substitution here.
deploy:
stage: deploy
image: google/cloud-sdk
script:
- ... set up authentication and project details for GCP
- gcloud run deploy $MY_INSTANCE --image $IMAGE_NAME --allow-unauthenticated --port=80
- >
gcloud beta run domain-mappings describe --domain=${DOMAIN} --format='value(spec.routeName)' || NO_MAPPING=true;
if [ "$NO_MAPPING" = true ]; then
echo "Creating new domain mapping from ${DOMAIN} to instance ${MY_INSTANCE}"
gcloud beta run domain-mappings create --service ${MY_INSTANCE} --domain ${DOMAIN}
else
echo "The domain mapping from ${DOMAIN} to instance ${MY_INSTANCE} is already active."
fi
As so often, if you see the solution, it’s actually pretty simple. It is the combination of a couple of features that eliminates yet another manual step in the rollout of services.