Creating Cloud Run Service with Secret mounted in the environment

Requirement

Make the latest version database password stored in the secret manager available to the Cloud Run service as an environment variable. This needs to be done using Terraform so it’s properly documented in the infrastructure code.

Problem

The given feature is newly introduced by Google Cloud and not yet in General Availability. The support for this feature was only added in terraform google module 3.67.0 and the documentation on this is scarce. Even the example mentioned in terraform docs can get confusing as the concepts haven’t been explained correctly.

Solution

After wasting hours trying to figure out the right thing, I am sharing a fully functional code followed by an explanation on what needs to be looked at, by isolating the required code snippets. Hopefully this will help others figure what they need to look at and save time.

The Code

version.tf

terraform {
  required_version = ">=0.13"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "3.69.0"
    }

    google-beta = {
      source  = "hashicorp/google-beta"
      version = "3.69.0"
    }

    random = {
      source  = "hashicorp/random"
      version = "3.1.0"
    }
  }

}

provider "google" {
  project = var.project
  region  = var.region
}

provider "google-beta" {
  project = var.project
  region  = var.region
}

main.tf

resource "random_id" "db_name_suffix" {
  byte_length = 4
}

resource "random_password" "db_password" {
  length  = 20
  special = false
}

resource "google_sql_database_instance" "dbinstance" {
  name                = "db-instance-${random_id.db_name_suffix.hex}"
  database_version    = "POSTGRES_13"
  deletion_protection = true

  settings {
    tier = var.database_instance_type
  }
}

resource "google_sql_database" "db" {
  name     = "appdb"
  instance = google_sql_database_instance.dbinstance.name
}

resource "google_sql_user" "dbuser" {
  name     = "appuser"
  instance = google_sql_database_instance.dbinstance.name
  password = random_password.db_password.result
}

resource "google_secret_manager_secret" "db_password" {
  secret_id = "DB_PASSWORD"

  replication {
    user_managed {
      replicas {
        location = var.region
      }
    }
  }
}
resource "google_secret_manager_secret_version" "db_password_version" {
  secret = google_secret_manager_secret.db_password.id

  secret_data = google_sql_user.dbuser.password
}

resource "google_cloud_run_service" "run_service" {
  provider = google-beta
  name     = "myapp"
  location = var.region

  template {
    spec {
      containers {
        image = var.container_image
        ports {
          container_port = var.container_port
        }
        env {
          name = "PASSWORD"
          value_from {
            secret_key_ref {
              name = google_secret_manager_secret.db_password.id
              key  = split("/", "${google_secret_manager_secret_version.db_password_version.name}")[5]
            }
          }
        }
        env {
          name  = "ENVIRONMENT"
          value = var.environment
        }
      }
    }
    metadata {
      annotations = {
        "autoscaling.knative.dev/maxScale"      = 2
        "run.googleapis.com/cloudsql-instances" = google_sql_database_instance.dbinstance.connection_name
        "run.googleapis.com/client-name"        = "terraform"
      }
    }
  }

  metadata {
    annotations = {
      "run.googleapis.com/launch-stage" = "BETA"
    }
  }

  traffic {
    percent         = 100
    latest_revision = true
  }

  lifecycle {
    ignore_changes = [
      metadata.0.annotations,
    ]
  }
}

variables.tf

variable "project" {
  description = "Project ID where resources are to be created"
  type        = string
  default     = "nexsales-devops"
}

variable "region" {
  description = "Default region for the resources"
  type        = string
  default     = "us-central1"
}

variable "database_instance_type" {
  description = "Instance size for the database"
  type        = string
  default     = "db-f1-micro"
}

variable "environment" {
  description = "Environment for the app to operate in"
  type        = string
  default     = "production"
}

variable "container_image" {
  description = "URL of the image from where the container can be fetched"
  type        = string
  default     = "nginx:latest"
}

variable "container_port" {
  description = "Port number on which the container should listen"
  type        = number
  default     = 80
}

Description

Environment

The difference between the env block structure. The normal env variable block just has name and value as keys, whereas the secret mounted one uses name and secret_key_ref. Secret key reference has name and key as keys. The name refers to the name of the secret in secret manager and the key refers to the version of the secret.

env {
  name = "PASSWORD"
  value_from {
    secret_key_ref {
      name = google_secret_manager_secret.db_password.id
      key = split("/","${google_secret_manager_secret_version.db_password_version.name}")[5]
    }
  }
} 
env {
  name = "ENVIRONMENT"
  value = var.environment
}

Annotations

There are 2 annotation blocks, one inside the template, which defines autoscaling, sql connection among other things. The second is independent one which defines the launch stage of the service. Since this feature isn’t yet in General Availability, we need to use this block for cloud run services. Currently, the only supported value of the launch stage is ‘BETA’ but things can change from time to time, so keep an eye out for changes.

template {
  ...
  metadata {
    annotations = {
      "autoscaling.knative.dev/maxScale" = 2
      "run.googleapis.com/cloudsql-instances" = google_sql_database_instance.dbinstance.connection_name
      "run.googleapis.com/client-name" = "terraform"
    }
  }
}

The above section uses revision template metadata as described at https://cloud.google.com/run/docs/reference/rest/v1/RevisionTemplate

metadata {
  annotations = {
    "run.googleapis.com/launch-stage" = "BETA"
  }
}

Whereas this is object metadata as described at https://cloud.google.com/run/docs/reference/rest/v1/RevisionTemplate

and usage is shown here https://cloud.google.com/run/docs/troubleshooting#launch-stage-validation

Leave a Reply

Your email address will not be published. Required fields are marked *