Post

Deploying to Cloud Run with Terraform

Cloud Run is a popular choice for serverless computing on GCP. It allows you to easily deploy and run a Docker Image with very little work. It is much simpler than maintaing your own Kubernetes cluster. There is even a source code based deploy option if you don’t want to make your own image.

Cloud Run is a great alternative to using App Engine and comes recommended from Google. GCP: App Engine vs Cloud Run

Cloud Run can be setup as a service to host an API or as a job. In this example we’ll deploy a Cloud Run service. It will host a Scala application running the Play Framework to create a REST API.

Building the Scala App

Scala Prerequisites

  • Git
  • SBT Installed (or IntelliJ with Scala Plugin)
  • JDK 11 or Greater

Play Framework

The Play Framework is one of many popular REST API frameworks in the Scala ecosystem. Play Framework may not be as new and shiny as some frameworks built off of aschronous composable runtimes, such as Cats Effect and ZIO, but its a good choice for those looking to stand up a REST API quickly in Scala.

Source Code

The source code for this entire example, including both the Scala application and Terraform, is available in Github: https://github.com/brandon-setegn/scala-play-example

1
git clone https://github.com/brandon-setegn/scala-play-example.git

Building the Scala Application

After you clone the repo, either open it in IntelliJ or navigate to it from the command line. Open the project in SBT.

Run the Application Locally

To run the project, simply enter run into the SBT shell. By default, it should start the app listening on port 9000. You can view the output at http://localhost:9000. There is only one endpoint serving “Hello World” at the base URL.

1
2
# Hello World
GET   /     controllers.HelloWorldController.index()

Building a Deploy Package

To build a Linux compatible package as a tgz file, use the sbt Universal / packageZipTarball command. More details here: Play: The Native Packager. If you are running this project from Windows, I recommend using Windows Subsystem for Linux to run a Linux shell and execute this command inside sbt.

1
Universal / packageZipTarball

If the build was successfull, you should see this package: \target\universal\play-scala-rest-api-example-1.0-SNAPSHOT.tgz" in your project folder.

Build and Push a Docker Image

There is a Dockerfile located in the root of the project folder. This will be be used to build a Docker Image that will be pushed to Google Artifact Registry (GAR). Cloud Run will run this image for us when the process is complete..

1
2
3
4
5
6
7
FROM amazoncorretto:21.0.2

EXPOSE 8080

ADD ./target/universal/play-scala-rest-api-example-1.0-SNAPSHOT.tgz /app

CMD /app/play-scala-rest-api-example-1.0-SNAPSHOT/bin/play-scala-rest-api-example -Dhttp.port=8080

Docker Build Command and Push to GCP

Now, from the console you can build the Docker Image with the following command. When we build this Docker Image, we will tag it with a specific name that will tell it where to go inside Google Cloud Platform (GCP).

Configure Docker and Google Artifact Registry

Follow the guide below to full setup Google Artifact Registry (GAR) and your local Docker authentication. GCP Guide: Store Docker Image in Artifact Registry

If you have trouble pushing your image to GAR, make sure to follow the steps to authenticate GCP Docker Auth

The Image will be tagged us-east1-docker.pkg.dev/{your-project-id}/cloud-run-example/scala-play-example. Repalce {your-project-id} with your Project ID from GCP.

This tag is important. It specifies which location in GCP to go to, us-east1, and which project to go to, {your-project-id}.

1
docker build -t us-east1-docker.pkg.dev/{your-project-id}/cloud-run-example/scala-play-example .

If docker build ran succesfully, we can now push our Docker Image to GCP:

1
docker push us-east1-docker.pkg.dev/{your-project-id}/cloud-run-example/scala-play-example

Now that the image has been pushed to GAR, we’re ready to work on our Terraform to create our Cloud Run Service.

Terraform

⚠️Important: Make sure not to hard code any GCP values or Environment Variables.

Terraform Prerequisites

This post won’t cover all the intricacies of setting up your system to run gcloud and terraform. Follow the links below for more information on how to accomplish that.

Add Terraform to the Project

Terraform This sample project already has the Terraform files you need to deploy to Cloud Run. All of the Terraform files go in the terraform directory in the project. This is where you will run Terraform commands from the command line.

Terraform Project Setup

This Terraform guide describes building your own local module: https://developer.hashicorp.com/terraform/tutorials/modules/module-create

FileDescription
main.tfwill contain the main set of configuration for your module
output.tfwill contain the variable definitions for your module
variables.tfwill contain the output definitions for your module

Configure Terraform for Cloud Run

This post will only cover the specifics of using Terraform for Cloud Run. For details on the output.tf and variables.tf files checkout the tutorial from Terraform above.

main.tf

This is our module that will be run to deploy to Cloud Run. There are two sections to our main.tf, service_account and cloud_run.

Service Account Module

This section creates a service account in GCP to be used by our Cloud Run service. This section must come before the cloud_run section. Since our Cloud Run service will access a few Secrets in GCP to be used as environment variables, it needs the roles/secretmanager.secretAccessor role.

⚠️Important: This section uses the var.project_id variable. This is so your project_id isn’t checked into GitHub and visible to others.

1
2
3
4
5
6
7
8
module "service_account" {
  source     = "terraform-google-modules/service-accounts/google"
  version    = "~> 4.2"
  project_id = var.project_id
  prefix     = "sa-cloud-run"
  names      = ["simple"]
  project_roles = ["${var.project_id}=>roles/secretmanager.secretAccessor"]
}

Cloud Run Module

This section creates the actual Cloud Run service. It also uses the var.project_id variable to make sure your project id and other sensitive information isn’t checked into GitHub.

Cloud Run Environment Variables and Secrets

The Scala application requires two enviornment variables to run. These MUST be created in manually GCP Secrets Manager before the Cloud Run service is created. Follow this QuickStart: Secret Manager guide to learn how to create secrets in GCP.

Environment Variable NameDescription
APPLICATION_SECRETSecret key used by Play Framework
ALLOWED_HOSTHosts allowed to connect to service

The APPLICATION_SECRET can be set to any approriate length string necessary. However,ALLOWED_HOST must be set to the final URL of our Cloud Run service. This can be found by running terraform plan as described below. One of the outputs will be the service_url needed to set the ALLOWED_HOST environment variable.

Note: The docker tag used previously is the same used for the Cloud Run image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module "cloud_run" {
  source  = "GoogleCloudPlatform/cloud-run/google"
  version = "~> 0.10"

  service_name          = "cloud-run-example-scala"
  project_id            = var.project_id
  location              = "us-east1"
  image                 = "us-east1-docker.pkg.dev/${var.project_id}/cloud-run-example/scala-play-example:latest"
  service_account_email = module.service_account.email
  env_secret_vars       = [
    {
      name = "APPLICATION_SECRET"
      value_from = [
        {
          secret_key_ref = {
            name = "cloud-run-example-play-secret"
            key  = "latest"
          }
        }
      ]
    },
    {
      name = "ALLOWED_HOST"
      value_from = [
        {
          secret_key_ref = {
            name = "cloud-run-example-play-host"
            key  = "latest"
          }
        }
      ]
    }]
}

IAM Resource

⚠️Important: This section grants invoker to allUsers. This means that ANYONE can hit the URL of your Cloud Run service. Make sure to turn off your service when done testing or your may incur extra charges.

1
2
3
4
5
6
7
8
9
resource "google_cloud_run_service_iam_binding" "binding" {
  project  = module.cloud_run.project_id
  location = module.cloud_run.location
  service  = module.cloud_run.service_name
  role     = "roles/run.invoker"
  members = [
    "allUsers"
  ]
}

Running Terraform

The sample project comes with the Terraform files required to run and deploy the Cloud Run service. All you need to have completed to run the Terraform step is to have configured and installed Terraform.

Test the Terraform Module

To simply see the output of the Terraform process in our console, run the following command. You will be asked to input your GCP project id.

1
2
3
4
5
6
terraform plan
# Output below
var.project_id
  The project ID to deploy to

  Enter a value: {your-project-id}

Running the Terraform Module

To actually create the Cloud Run service, run this command. You will be asked to input your GCP project id.

1
2
3
4
5
6
7
8
9
terraform apply
# Output below
var.project_id
  The project ID to deploy to

  Enter a value: {your-project-id}
  ....
# Final Line outputted hsould be
service_url = "https://{your-unique-cloud-run-url}.app"

Testing Our Cloud Run Service

Simply navigate to the service_url outputted from our terraform apply command. You should be greated with Hello World.

Cleaning Up Our Service

⚠️Make sure to clean up your project so that you are not billed for the service.

1
2
# Destroy the GCP resources created by Terraform
terraform destroy
This post is licensed under CC BY 4.0 by the author.