reference-architectures/automated-password-rotation/terraform/main.tf (274 lines of code) (raw):
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
data "google_project" "project" {
project_id = var.project_id
}
resource "google_pubsub_schema" "avro_schema" {
name = "my-avro-schema"
type = "AVRO"
definition = jsonencode({
"type" : "record",
"name" : "Avro",
"fields" : [
{ "name" : "secretid", "type" : "string" },
{ "name" : "instance_name", "type" : "string" },
{ "name" : "db_user", "type" : "string" },
{ "name" : "db_name", "type" : "string" },
{ "name" : "db_location", "type" : "string" },
]
})
depends_on = [
google_project_service.services
]
}
resource "google_pubsub_topic" "pubsub_topic" {
name = "pswd-rotation-topic"
schema_settings {
schema = google_pubsub_schema.avro_schema.id
encoding = "JSON"
}
depends_on = [
google_project_service.services
]
}
resource "google_service_account" "scheduler_account" {
project = var.project_id
account_id = var.scheduler_sa
display_name = "Cloud Scheduler Service Account for password rotation"
depends_on = [
google_project_service.services
]
}
resource "google_service_account" "function_account" {
project = var.project_id
account_id = var.function_sa
display_name = "Cloud Function Service Account for password rotation"
depends_on = [
google_project_service.services
]
}
resource "google_cloud_scheduler_job" "scheduler" {
name = var.scheduler_name
description = "Publishes password rotation message on 1st day of the month"
schedule = "0 0 1 * *" // Run at midnight on the 1st of the month
time_zone = "America/New_York"
pubsub_target {
topic_name = google_pubsub_topic.pubsub_topic.id
data = base64encode(jsonencode({ "secretid" : split("/", "${google_secret_manager_secret.cloudsql_pswd.id}")[3], "instance_name" : "${var.instance_name}", "db_user" : "${var.db_user}", "db_name" : "${var.db_name}", "db_location" : "${var.region}" }))
}
retry_config {
min_backoff_duration = "5s"
max_backoff_duration = "3600s"
max_retry_duration = "0s"
max_doublings = 5
retry_count = 0
}
depends_on = [
google_project_service.services
]
}
// Grant the scheduler permission to publish to the topic
resource "google_pubsub_topic_iam_binding" "scheduler_publisher" {
topic = google_pubsub_topic.pubsub_topic.name
role = "roles/pubsub.publisher"
members = [
"serviceAccount:${google_service_account.scheduler_account.email}",
]
depends_on = [
google_project_service.services
]
}
// Create GCS for storing function code
resource "google_storage_bucket" "function_bucket" {
name = "pswd-rotation-code-${var.project_id}"
location = var.region
project = var.project_id
}
// permisison the Cloud function SA to read the bucket
resource "google_storage_bucket_iam_member" "read_bucket" {
bucket = google_storage_bucket.function_bucket.name
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.function_account.email}"
depends_on = [google_storage_bucket.function_bucket, google_project_service.services]
}
// permisison the Cloud function SA to to work with CloudSql
resource "google_project_iam_member" "cloudsql_client" {
project = var.project_id
role = "roles/cloudsql.client"
member = "serviceAccount:${google_service_account.function_account.email}"
depends_on = [
google_project_service.services
]
}
// Generates an archive of the source code compressed as a .zip file.
data "archive_file" "source" {
type = "zip"
source_dir = "${path.module}/code"
output_path = "/tmp/function.zip"
}
// Add source code zip to the Cloud Function's bucket
resource "google_storage_bucket_object" "zip" {
source = data.archive_file.source.output_path
content_type = "application/zip"
# Append to the MD5 checksum of the file's content
# to force the zip to be updated as soon as a change occurs
name = "src-${data.archive_file.source.output_md5}.zip"
bucket = google_storage_bucket.function_bucket.name
depends_on = [
google_storage_bucket.function_bucket,
data.archive_file.source
]
}
//Creating a secretmanager to store the db password
resource "random_password" "pass-webhook" {
length = 10
special = false
}
resource "google_secret_manager_secret" "cloudsql_pswd" {
secret_id = "cloudsql-pswd"
replication {
auto {}
}
project = var.project_id
depends_on = [
google_project_service.services
]
}
resource "google_secret_manager_secret_version" "cloudsql_pswd_secret" {
secret = google_secret_manager_secret.cloudsql_pswd.id
secret_data = random_password.pass-webhook.result
}
resource "google_secret_manager_secret_iam_member" "cloudsql_pswd_secret_access" {
secret_id = google_secret_manager_secret.cloudsql_pswd.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.function_account.email}"
depends_on = [
google_project_service.services
]
}
resource "google_secret_manager_secret_iam_member" "cloudsql_pswd_secret_write" {
secret_id = google_secret_manager_secret.cloudsql_pswd.id
role = "roles/secretmanager.secretVersionAdder"
member = "serviceAccount:${google_service_account.function_account.email}"
depends_on = [
google_project_service.services
]
}
// Grant access to scheduler SA to invoke run for 2nd gen Cloud Function
resource "google_project_iam_member" "scheduler_runinvoke" {
project = var.project_id
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.scheduler_account.email}"
depends_on = [
google_project_service.services
]
}
//Have to enable cloudresourcemanager & serviceusage.googleapis.com API before running TF
resource "google_project_service" "services" {
for_each = toset(var.services)
service = each.value
disable_dependent_services = true
}
//Creating Serverless VPC connector to connect from Cloud Function to CloudSQL over private IP
resource "google_vpc_access_connector" "cloudsql_connector" {
provider = google
name = var.connector_name
project = var.project_id
region = var.region
ip_cidr_range = var.connector_cidr
network = var.vpc_network
machine_type = var.connector_machine_type
min_throughput = 200
max_throughput = 800
depends_on = [
google_project_service.services
]
}
// Setting up VPC peering for private IP access of the CloudSql instance
resource "google_compute_global_address" "private_ip_address" {
name = "private-ip-address"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = "projects/${var.project_id}/global/networks/${var.vpc_network}"
depends_on = [
google_project_service.services
]
}
resource "google_service_networking_connection" "private_vpc_connection" {
network = "projects/${var.project_id}/global/networks/${var.vpc_network}"
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]
depends_on = [
google_project_service.services
]
provider = google-beta
}
// Creating CloudSql
resource "google_sql_database" "test_db" {
name = var.db_name
instance = google_sql_database_instance.cloudsql_instance.name
}
resource "google_sql_database_instance" "cloudsql_instance" {
name = var.instance_name
database_version = var.database_version
depends_on = [google_vpc_access_connector.cloudsql_connector, google_project_service.services, google_service_networking_connection.private_vpc_connection]
settings {
tier = var.db_instance_tier
availability_type = "REGIONAL"
disk_size = 10
ip_configuration {
private_network = "projects/${var.project_id}/global/networks/${var.vpc_network}"
enable_private_path_for_google_cloud_services = true
ipv4_enabled = false
}
}
deletion_protection = false # so that TF destroy is able to cleanup the instance
}
resource "google_sql_user" "db_user" {
name = var.db_user
instance = google_sql_database_instance.cloudsql_instance.name
password = random_password.pass-webhook.result
}
// Cloud Function
resource "google_cloudfunctions2_function" "pubsub_handler" {
name = "pswd_rotator_function"
description = "Handles Pub/Sub messages to rotate cloudsql password"
build_config {
runtime = "python310"
entry_point = "password_rotation_function" # Set the entry point
source {
storage_source {
bucket = google_storage_bucket.function_bucket.name
object = google_storage_bucket_object.zip.name
}
}
}
service_config {
max_instance_count = 100
min_instance_count = 1
available_memory = "256M"
timeout_seconds = 60
vpc_connector = google_vpc_access_connector.cloudsql_connector.id
service_account_email = google_service_account.function_account.email
}
event_trigger {
trigger_region = var.region
event_type = "google.cloud.pubsub.topic.v1.messagePublished"
pubsub_topic = google_pubsub_topic.pubsub_topic.id
retry_policy = "RETRY_POLICY_DO_NOT_RETRY"
service_account_email = google_service_account.scheduler_account.email
}
location = var.region
depends_on = [
google_project_service.services
]
}
// Miscellaneous
// Deploying a Cloud Function runs CloudBuild. The default service account for CloudBuild is the default Compute Engine service account. We need provide the following permission Compte Engine SA so it can deploy the Cloud Function successfully.
resource "google_project_iam_member" "ce_sa_permission" {
for_each = toset(var.ce_sa_roles)
project = var.project_id
role = each.key
member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"
depends_on = [
google_project_service.services
]
}