Streamlining Cloud Run Deployment with Terraform
victor
Victor Mwania
August 8, 2023

Terraform is an open-source infrastructure as code (IaC) tool developed by HashiCorp. It allows you to define and manage your infrastructure using declarative configuration files, making provisioning and managing cloud resources and other infrastructure components easier.

Cloud Run is a serverless computing platform provided by Google Cloud. Cloud Run allows you to deploy and run containerised applications in a fully managed environment, abstracting away much of the underlying infrastructure management

In this article, we will explore the process of deploying a service on Cloud Run using Terraform. By the end, we will have established the infrastructure using Terraform and successfully deployed a service that will be accessible via a public URL.

Before we begin we will need to do the following:

  • Install Terraform CLI
  • Google Cloud SDK
  • GCP Service Account Keys - Create a service account key with permission to access the API
  • A GCP project
Implementation

We're preparing to deploy a basic URL shortener coded in Go. The application features three essential APIs: a health check API, a short URL creation API, and an API for retrieving shortened URLs. The repo for the project can be found here to follow along.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/go-chi/chi"
	"github.com/speps/go-hashids"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

type ErrorResponse struct {
	Message string `json:"message"`
}

type ShortURLResponse struct {
	ShortURL string `json:"short_url"`
}

type URL struct {
	gorm.Model
	ShortUrl string `json:"short_url"`
	FullUrl  string `json:"full_url"`
}

func main() {
	dsn := "host=localhost user=postgres password=password dbname=url_shortnerer port=5432 sslmode=disable"
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&URL{})

	if err != nil {
		log.Fatal(err)
	}

	log.Println("Connected to database")

	r := chi.NewRouter()

	r.Get("/{shortURL}", func(w http.ResponseWriter, r *http.Request) {
		shortURL := chi.URLParam(r, "shortURL")

		fmt.Println(shortURL, "shortURL")
		var url URL
		result := db.Where(&URL{ShortUrl: shortURL}).First(&url)
		// result := db.First(&url)
		fmt.Println(url.FullUrl, "result")
		if result.Error != nil {
			http.NotFound(w, r)
			return
		}

		http.Redirect(w, r, url.FullUrl, http.StatusSeeOther)
	})

	r.Post("/create", func(w http.ResponseWriter, r *http.Request) {
		fullURL := r.FormValue("url")

		// Generate a short ID using hashids.
		hd := hashids.NewData()
		hd.Salt = "url_shortner_salt" // should be read from config
		h, _ := hashids.NewWithData(hd)
		shortID, _ := h.Encode([]int{123})

		// Store the shortID and fullURL in the database.
		url := URL{ShortUrl: shortID, FullUrl: fullURL}
		result := db.Create(&url)
		if result.Error != nil {
			w.WriteHeader(http.StatusInternalServerError)
			errorResponse := ErrorResponse{Message: "Error storing URL"}
			jsonResponse, _ := json.Marshal(errorResponse)
			_, _ = w.Write(jsonResponse)
			return
		}

		shortURL := fmt.Sprintf("/%s", shortID)
		response := ShortURLResponse{ShortURL: shortURL}
		jsonResponse, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		_, _ = w.Write(jsonResponse)
	})

	r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		response := []byte(`{"status": "ok"}`)
		_, _ = w.Write(response)
	})

	port := ":8090"
	log.Printf("Server is listening on port %s", port)
	log.Fatal(http.ListenAndServe(port, r))
}

We need the docker image of this application which we will require to deploy the service. Gcloud SDK will help us build the docker image and push the docker image to Artifact Registry where we have a repository with the format docker.

gcloud builds submit --region=us-central1 --tag us-central1-docker.pkg.dev/REPOSITORY/docker/url-shortner:latest

You can read further on how to build with Google Cloud from this article.

Terraform

We begin with the defining the cloud provider and the configuration details for connecting to that provider's services which in our case will be GCP incdicateds google.

provider "google" {
  project     = "PROJECT_ID"
  region      = "us-central1"
  credentials = file("key.json")
}

Authentication credentials needed to access GCP are contained in the JSON file where we specify the path to that JSON file. The JSON file can be generated for a service account in the IAM & Admin section. Replace PROJECT_ID with your project

Now we define the cloud-run service resource

resource "google_cloud_run_service" "url_shortner" {
  name     = "url-shortner"
  location = "us-central1"

  template {
    spec {
      containers {
        image = "us-central1-docker.pkg.dev/REPOSITORY/docker/url-shortner:latest"
        ports {
          container_port = 8090
        }
      }
    }
  }
}

name: This attribute specifies the name of the Cloud Run service that will be created. In this case, it's set to "url-shortner".

location: This attribute specifies the location or region where the Cloud Run service will be deployed. Here, it's set to "us-central1", which is the US Central region.

template: This block defines the template configuration for the Cloud Run service, which includes the specifications for running the containers. Here, you define the container that will be deployed in the Cloud Run service by specifying the Docker image to use for the container

We can also have a mechanism that helps ensure the availability and reliability of a running container by periodically checking its health by adding liveness_probe. This will be inside the containers block

    liveness_probe {
        http_get {
            path = "/health"
         }
    }

To be able to access our service from the service_url we need to allow unauthenticated acces to the service with google_cloud_run_service_iam_member resource

resource "google_cloud_run_service_iam_member" "run_all_users" {
  service  = "url-shortner"
  location = "us-central1"
  role     = "roles/run.invoker"
  member   = "allUsers"
}

And here is the final terraform configuaration


provider "google" {
  project     = "PROJECT_ID"
  region      = "us-central1"
  credentials = file("key.json")

}

resource "google_cloud_run_service" "url_shortner" {
  name     = "url-shortner"
  location = "us-central1"

  template {
    spec {
      containers {
        image = "us-central1-docker.pkg.dev/PROJECT_ID/docker/url-shortner:latest"
        ports {
          container_port = 8090
        }

        liveness_probe {
          http_get {
            path = "/health"
          }
        }
      }
    }
  }

  traffic {
    percent         = 100
    latest_revision = true
  }

}

resource "google_cloud_run_service_iam_member" "run_all_users" {
  service  = "url-shortner"
  location = "us-central1"
  role     = "roles/run.invoker"
  member   = "allUsers"
}

data "google_cloud_run_service" "url_shortner_data" {
  name     = google_cloud_run_service.url_shortner.name
  location = google_cloud_run_service.url_shortner.location
}

output "service_url" {
  value = data.google_cloud_run_service.url_shortner_data.status[0].url
}

Deploying The Infrastructure

First, we initialise the Terraform configuaration

terraform init

Run terraform plan to preview the changes that Terraform will make to your infrastructure before actually applying those changes.

terraform apply

Run terraform apply to apply Terraform configuration to bring your infrastructure into the desired state as specified in your configuration files.

terraform apply

This will also output the service URL after successful deployment.

Cleanup

To delete all resources created, terraform destroy will remove all the resources defined in your Terraform configuration files, effectively reverting your infrastructure back to its initial state or completely removing it from your cloud provider.

terraform destroy

Thank you for reach this far!