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
Comments