terraform/main.tf (271 lines of code) (raw):
/*
* Copyright 2025 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.
*
*/
terraform {
required_version = ">= 1.3"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 6.0"
}
time = {
source = "hashicorp/time"
version = "~> 0.9" # Or latest compatible version
}
}
}
locals {
tasks_sa_id = coalesce(var.tasks_service_account_name, "${var.scaler_service_name}-tasks")
scaler_sa_id = format("%s%s-sa", var.scaler_sa_name_prefix, var.scaler_service_name)
scheduler_job_name = "${var.scaler_service_name}-cron"
scheduler_sa_id = format("%s%s-sa", var.scheduler_sa_name_prefix, local.scheduler_job_name)
tasks_sa_email = "${local.tasks_sa_id}@${var.project_id}.iam.gserviceaccount.com"
scaler_sa_email = "${local.scaler_sa_id}@${var.project_id}.iam.gserviceaccount.com"
scheduler_sa_email = "${local.scheduler_sa_id}@${var.project_id}.iam.gserviceaccount.com"
# Environment variables for the scaler Cloud Run service
scaler_env_vars = {
KAFKA_TOPIC_ID = var.topic_id
CONSUMER_GROUP_ID = var.consumer_group_id
CYCLE_SECONDS = tostring(var.scaler_cycle_seconds)
INVOKER_SERVICE_ACCOUNT_EMAIL = local.tasks_sa_email
}
}
data "google_project" "project" {
project_id = var.project_id
}
# --- Cloud Tasks Resources ---
resource "google_cloud_tasks_queue" "queue" {
project = var.project_id
location = var.region
name = var.cloud_tasks_queue_name
}
resource "google_service_account" "tasks_sa" {
project = var.project_id
account_id = local.tasks_sa_id
create_ignore_already_exists = true
display_name = "Service Account for Cloud Tasks Invoker"
}
# --- Kafka Autoscaler Service Account and Permissions ---
resource "google_service_account" "scaler_sa" {
project = var.project_id
account_id = local.scaler_sa_id
create_ignore_already_exists = true
display_name = "Service Account for Kafka Autoscaler"
}
# Grant Scaler SA permission to impersonate Consumer SA
resource "google_service_account_iam_member" "scaler_impersonate_consumer" {
service_account_id = var.consumer_sa_email # This needs the projects/... format
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${local.scaler_sa_email}"
}
# Grant Scaler SA permission to read Artifact Registry
resource "google_project_iam_member" "scaler_artifactregistry_reader" {
project = var.project_id
role = "roles/artifactregistry.reader"
member = "serviceAccount:${local.scaler_sa_email}"
}
# Grant Scaler SA permission to access scaler config secret
resource "google_secret_manager_secret_iam_member" "scaler_access_scaler_config" {
project = var.project_id
secret_id = var.scaler_config_secret_name
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${local.scaler_sa_email}"
}
# Grant Scaler SA permission to access admin client secret
resource "google_secret_manager_secret_iam_member" "scaler_access_admin_client" {
project = var.project_id
secret_id = var.admin_client_secret_name
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${local.scaler_sa_email}"
}
# Grant Scaler SA permission to manage Cloud Run services
resource "google_project_iam_member" "scaler_run_admin" {
project = var.project_id
role = "roles/run.admin"
member = "serviceAccount:${local.scaler_sa_email}"
}
# Grant Scaler SA permission to enqueue tasks
resource "google_cloud_tasks_queue_iam_member" "scaler_tasks_enqueuer" {
project = google_cloud_tasks_queue.queue.project
location = google_cloud_tasks_queue.queue.location
name = google_cloud_tasks_queue.queue.name
role = "roles/cloudtasks.enqueuer"
member = "serviceAccount:${local.scaler_sa_email}"
}
# Grant Scaler SA permission to write metrics
resource "google_project_iam_member" "scaler_monitoring_writer" {
project = var.project_id
role = "roles/monitoring.metricWriter"
member = "serviceAccount:${local.scaler_sa_email}"
}
# Grant Scaler SA permission to view monitoring data
resource "google_project_iam_member" "scaler_monitoring_viewer" {
project = var.project_id
role = "roles/monitoring.viewer"
member = "serviceAccount:${local.scaler_sa_email}"
}
# Optional: Grant Scaler SA permission to use Managed Kafka
resource "google_project_iam_member" "scaler_managed_kafka_client" {
count = var.grant_managed_kafka_client_role ? 1 : 0
project = var.project_id
role = "roles/managedkafka.client"
member = "serviceAccount:${local.scaler_sa_email}"
}
resource "time_sleep" "wait_for_scaler_sa_propagation" {
# Wait for 15 seconds for the newly created service accounts to fully propagate
# Feel free to adjust this value if needed.
# This is needed to avoid errors like "Service account xxx does not exist" during `terraform apply`
# See https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep
create_duration = "15s"
# Re-run the sleep if any of the IAM resources are created OR changed.
# This ensures the delay is always applied, even if any of the IAM resources below are updated later.
triggers = merge(
# Map of triggers that are always present
{
scaler_impersonate_consumer = google_service_account_iam_member.scaler_impersonate_consumer.id
scaler_artifactregistry = google_project_iam_member.scaler_artifactregistry_reader.id
scaler_access_scaler_config = google_secret_manager_secret_iam_member.scaler_access_scaler_config.id
scaler_access_admin_client = google_secret_manager_secret_iam_member.scaler_access_admin_client.id
scaler_run_admin = google_project_iam_member.scaler_run_admin.id
scaler_tasks_enqueuer = google_cloud_tasks_queue_iam_member.scaler_tasks_enqueuer.id
scaler_monitoring_writer = google_project_iam_member.scaler_monitoring_writer.id
scaler_monitoring_viewer = google_project_iam_member.scaler_monitoring_viewer.id
},
# Conditionally add the kafka client role trigger ONLY if the resource exists (count > 0)
# Creates map { scaler_managed_kafka_client = "<resource_id>" } when true, or {} when false.
var.grant_managed_kafka_client_role ?
{ scaler_managed_kafka_client = google_project_iam_member.scaler_managed_kafka_client[0].id }
: {}
)
}
# --- Deploy Kafka Autoscaler Cloud Run Service ---
resource "google_cloud_run_v2_service" "scaler_service" {
project = var.project_id
location = var.region
name = var.scaler_service_name
labels = merge(
{
"created-by" = "scaler-kafka"
},
var.additional_labels
)
deletion_protection = false
# Ensure that IAM bindings involving the SA exist, before creating the service that uses it
depends_on = [
google_service_account_iam_member.scaler_impersonate_consumer,
google_project_iam_member.scaler_artifactregistry_reader,
google_secret_manager_secret_iam_member.scaler_access_scaler_config,
google_secret_manager_secret_iam_member.scaler_access_admin_client,
google_project_iam_member.scaler_run_admin,
google_cloud_tasks_queue_iam_member.scaler_tasks_enqueuer,
google_project_iam_member.scaler_monitoring_writer,
google_project_iam_member.scaler_monitoring_viewer,
time_sleep.wait_for_scaler_sa_propagation,
google_project_iam_member.scaler_managed_kafka_client,
]
ingress = "INGRESS_TRAFFIC_INTERNAL_ONLY"
template {
service_account = local.scaler_sa_email
scaling {
max_instance_count = 1
}
# VPC Access configuration - only include if network and subnet are provided
vpc_access {
# Set egress conditionally based on whether network/subnet are provided
# Note: Default is ALL_TRAFFIC if network/subnet are null
egress = (var.network != null && var.subnet != null) ? "PRIVATE_RANGES_ONLY" : "ALL_TRAFFIC"
# Dynamically create the network_interfaces block only if network and subnet are provided
dynamic "network_interfaces" {
# The for_each iterates once if the condition is true, zero times if false
for_each = (var.network != null && var.subnet != null) ? [1] : []
content {
network = var.network
subnetwork = var.subnet
}
}
}
volumes {
name = "kafka-config"
secret {
secret = var.admin_client_secret_name
items {
path = "kafka-client-properties"
version = var.admin_client_secret_version
}
}
}
volumes {
name = "scaler-config"
secret {
secret = var.scaler_config_secret_name
items {
path = "scaling"
version = var.scaler_config_secret_version
}
}
}
containers {
image = var.scaler_image_path
base_image_uri = "us-central1-docker.pkg.dev/serverless-runtimes/google-22/runtimes/java17"
env {
name = "KAFKA_TOPIC_ID"
value = local.scaler_env_vars.KAFKA_TOPIC_ID
}
env {
name = "CONSUMER_GROUP_ID"
value = local.scaler_env_vars.CONSUMER_GROUP_ID
}
env {
name = "CYCLE_SECONDS"
value = local.scaler_env_vars.CYCLE_SECONDS
}
env {
name = "FULLY_QUALIFIED_CLOUD_TASKS_QUEUE_NAME"
value = "projects/${var.project_id}/locations/${var.region}/queues/${google_cloud_tasks_queue.queue.name}"
}
env {
name = "INVOKER_SERVICE_ACCOUNT_EMAIL"
value = local.scaler_env_vars.INVOKER_SERVICE_ACCOUNT_EMAIL
}
volume_mounts {
name = "kafka-config"
mount_path = "/kafka-config"
}
volume_mounts {
name = "scaler-config"
mount_path = "/scaler-config"
}
# Define ports if needed, e.g.
# ports {
# container_port = 8080
# }
}
}
}
# Grant Tasks SA permission to invoke the Scaler Cloud Run service
resource "google_cloud_run_v2_service_iam_member" "tasks_invoke_scaler" {
project = google_cloud_run_v2_service.scaler_service.project
location = google_cloud_run_v2_service.scaler_service.location
name = google_cloud_run_v2_service.scaler_service.name
role = "roles/run.invoker"
member = "serviceAccount:${local.tasks_sa_email}"
depends_on = [
google_service_account.tasks_sa,
google_cloud_run_v2_service.scaler_service,
time_sleep.wait_for_scaler_sa_propagation, # Ensure the SA exists before granting permission
]
}
# --- Cloud Scheduler Resources ---
resource "google_service_account" "scheduler_sa" {
project = var.project_id
account_id = local.scheduler_sa_id
create_ignore_already_exists = true
display_name = "Service Account for Cloud Scheduler Job"
}
# Grant Scheduler SA permission to invoke the Scaler Cloud Run service
resource "google_cloud_run_v2_service_iam_member" "scheduler_invoke_scaler" {
project = google_cloud_run_v2_service.scaler_service.project
location = google_cloud_run_v2_service.scaler_service.location
name = google_cloud_run_v2_service.scaler_service.name
role = "roles/run.invoker"
member = "serviceAccount:${local.scheduler_sa_email}"
depends_on = [
google_service_account.scheduler_sa,
google_cloud_run_v2_service.scaler_service
]
}
resource "google_cloud_scheduler_job" "scaler_trigger" {
project = var.project_id
region = var.region
name = local.scheduler_job_name
schedule = var.scheduler_schedule
time_zone = "Etc/UTC"
http_target {
uri = google_cloud_run_v2_service.scaler_service.uri # Use the URI from the created service
oidc_token {
service_account_email = local.scheduler_sa_email
audience = google_cloud_run_v2_service.scaler_service.uri
}
}
depends_on = [
google_cloud_run_v2_service_iam_member.scheduler_invoke_scaler
]
}