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!