Serve a static website using Cloud Run

TL;DR;

Using the Google Cloud SDK, we will run a container that will host the website https://www.cursodegit.com in Cloud Run. The container will use nginx to serve a static site generated using webpack. We will also configure a custom domain mapping and create the appropriate records in our DNS so we can access the website using our domain name.

Motivation

At the begining of 2020, about one month before the first and biggest lockdown due to COVID-19 began in Spain, I started to use G-Suite (called Google Workspaces now) for my domain www.cursodegit.com. I migrated the email to GMail and I found myself with a 300$ credit to try Google Cloud.

With 300 bucks to spend, I decided to host the website there. I did the following:

  • Created a bucket holding the static site generated by Webpack
  • Spin up a load balancer that used that bucket as a backend service

The cost of this setup, with my current traffic (which is embarrasingly low) is about 16€ / month. I found that amount of money a little too much for such a low traffic site… but I was OK with it, since I was using my credit. Unfortunately, I run out of credit last April and I decided to look for an alternative.

Since Cloud Run only charges you for the CPU you use, I have the feeling that using it instead of my current set up with the Load Balancer is going to cut the costs a lot. So, let’s try it!

Deploy the container

In a previous post, I explained how we uploaded the image contaning the website to a private bucket in the Google Container Registry. We will deploy this image in Cloud Run.

As we did in that post, we will run the gcloud command inside a docker container. You will see some long commands for this reason (you can read here why I’m doing this)

> docker run --rm --volumes-from gcloud-config \
    gcr.io/google.com/cloudsdktool/cloud-sdk \
    gcloud run deploy web-cursodegit-com \
    --image eu.gcr.io/web-cursodegit-com/web \
    --region europe-west1 \
    --project web-cursodegit-com \
    --port=80 \
    --allow-unauthenticated

Deploying container to Cloud Run service [web-cursodegit-com] in project [web-cursodegit-com] region [europe-west1]
Deploying new service...
Setting IAM Policy...............done
Creating Revision.................................done
Routing traffic.....done
Done.
Service [web-cursodegit-com] revision [web-cursodegit-com-00001-paj] has been deployed and is serving 100 percent of traffic.
Service URL: https://web-cursodegit-com-b2t44kuhpa-ew.a.run.app

We chose europe-west1 because europe-west2, europe-west3 and europe-west6 were not available to use custom domain mappings at the time I did this. See here.

Custom domain mapping

After the container is in place, we can see the website using the following URL: https://web-cursodegit-com-b2t44kuhpa-ew.a.run.app. We will configure a custom domain mapping so we can access it using the url https://www.cursodegit.com.

The first step is to verify the domain, so let’s do it:

> docker run --rm --volumes-from gcloud-config \
    gcr.io/google.com/cloudsdktool/cloud-sdk \
    gcloud domains verify www.cursodegit.com

Opening [https://www.google.com/webmasters/verification/verification?authuser=0&domain=www.cursodegit.com&pli=1] 
in a new tab in your default browser.

A new browser window should pop-up with that URL. If not, we will copy and paste the URL and open it in our favorite browser. We will select Other as the domain name provider, since we are using CloudFlare as our DNS server. This will display the instructions we need to follow:

We will create the requested record in CloudFlare, wait for a few seconds until we can verify with the dig command that the records are in place, and click on “Verify”. We should see a message like this one when the verification succeeds::

Once the domain is verified, we can add a custom domain mapping:

> docker run --rm --volumes-from gcloud-config \
    gcr.io/google.com/cloudsdktool/cloud-sdk \
    gcloud beta run domain-mappings create \
    --service web-cursodegit-com \
    --domain www.cursodegit.com \
    --region europe-west1 \
    --project web-cursodegit-com

Creating......
............................done.
Waiting for certificate provisioning. You must configure your DNS records for certificate issuance to begin.
NAME                RECORD TYPE  CONTENTS
web-cursodegit-com  A            216.239.32.21
web-cursodegit-com  A            216.239.34.21
web-cursodegit-com  A            216.239.36.21
web-cursodegit-com  A            216.239.38.21
web-cursodegit-com  AAAA         2001:4860:4802:32::15
web-cursodegit-com  AAAA         2001:4860:4802:34::15
web-cursodegit-com  AAAA         2001:4860:4802:36::15
web-cursodegit-com  AAAA         2001:4860:4802:38::15

Now, we go to CloudFlare and add those records. Once the records were created, we needed to wait for about 5 minutes before the certificate was correctly issued by google:

> docker run --rm --volumes-from gcloud-config \
    gcr.io/google.com/cloudsdktool/cloud-sdk \
    gcloud beta run domain-mappings list \
    --region europe-west1 \
    --project web-cursodegit-com

   DOMAIN              SERVICE             REGION
✔  www.cursodegit.com  web-cursodegit-com  europe-west1

We can check that the domain is in place by issuing this command:

> docker run --rm --volumes-from gcloud-config \
    gcr.io/google.com/cloudsdktool/cloud-sdk \
    gcloud beta run domain-mappings describe \
    --domain=www.cursodegit.com \
    --region europe-west1 \
    --project web-cursodegit-com

apiVersion: domains.cloudrun.com/v1
kind: DomainMapping
metadata:
  annotations:
    serving.knative.dev/creator: [email protected]
    serving.knative.dev/lastModifier: [email protected]
  creationTimestamp: '2021-06-19T07:22:26.866083Z'
  generation: 1
  labels:
    cloud.googleapis.com/location: europe-west1
    run.googleapis.com/overrideAt: '2021-06-19T07:22:29.683Z'
  name: www.cursodegit.com
  namespace: '499154617149'
  resourceVersion: AAXFGX4UMkg
  selfLink: /apis/domains.cloudrun.com/v1/namespaces/499154617149/domainmappings/www.cursodegit.com
  uid: 1496316d-1610-4a1c-ba6e-d80a2e4362dc
spec:
  routeName: web-cursodegit-com
status:
  conditions:
  - lastTransitionTime: '2021-06-19T07:36:03.660360Z'
    status: 'True'
    type: Ready
  - lastTransitionTime: '2021-06-19T07:36:03.660360Z'
    status: 'True'
    type: CertificateProvisioned
  - lastTransitionTime: '2021-06-19T07:22:29.952459Z'
    status: 'True'
    type: DomainRoutable
  mappedRouteName: web-cursodegit-com
  observedGeneration: 1
  resourceRecords:
  - rrdata: 216.239.32.21
    type: A
  - rrdata: 216.239.34.21
    type: A
  - rrdata: 216.239.36.21
    type: A
  - rrdata: 216.239.38.21
    type: A
  - rrdata: 2001:4860:4802:32::15
    type: AAAA
  - rrdata: 2001:4860:4802:34::15
    type: AAAA
  - rrdata: 2001:4860:4802:36::15
    type: AAAA
  - rrdata: 2001:4860:4802:38::15
    type: AAAA

Look for status.conditions in the JSON output. When the certificate is issued, we should see:

...
status:
  conditions:
  - lastTransitionTime: '2021-06-19T07:36:03.660360Z'
    status: 'True'
    type: Ready
...

Once issued, we can changed the records in CloudFlare to be proxied:

After a few seconds, we should be able to access the website https://www.cursodegit.com, but this time being served by Cloud Run instead of our load balancer. If we have a look at the metrics in the Console, we should start seeing some activity:

Now, let’s wait for a few days and see what is the cost of serving the website using Cloud Run compared to the current setup.

Remove the service

In case we need to remove the service, we can issue the following command:

> docker run --rm --volumes-from gcloud-config \
    gcr.io/google.com/cloudsdktool/cloud-sdk 
    gcloud run services delete web-cursodegit-com \
    --region europe-west1 \
    --project web-cursodegit-com \ 
    --quiet

References