Custom Infrastructure (Terraform)
Assumes /google-agents-cli-scaffold scaffolding. These patterns apply to projects with
deployment/terraform/directories.
Where to Put Custom Terraform
| Scenario | Location |
|---|---|
| Single-project infrastructure | deployment/terraform/single-project/ |
| CI/CD environments (staging/prod) | deployment/terraform/cicd/ |
Example: Custom Resources
# deployment/terraform/single-project/custom_resources.tf
resource "google_pubsub_topic" "events" {
name = "${var.project_name}-events"
project = var.project_id
}
resource "google_bigquery_dataset" "analytics" {
dataset_id = "${replace(var.project_name, "-", "_")}_analytics"
project = var.project_id
location = var.region
}
# Eventarc trigger for Cloud Storage
resource "google_eventarc_trigger" "storage_trigger" {
name = "${var.project_name}-storage-trigger"
location = var.region
project = var.project_id
matching_criteria {
attribute = "type"
value = "google.cloud.storage.object.v1.finalized"
}
matching_criteria {
attribute = "bucket"
value = google_storage_bucket.uploads.name
}
destination {
cloud_run_service {
service = google_cloud_run_v2_service.app.name
region = var.region
# ADK trigger-source route; on other frameworks point this at your app's own path
path = "/apps/${var.project_name}/trigger/eventarc"
}
}
service_account = google_service_account.app_sa.email
}For CI/CD environments (staging/prod):
Add resources to deployment/terraform/cicd/ (applies to staging and prod):
# deployment/terraform/cicd/custom_resources.tf
# Resources here are created in BOTH staging and prod projects
# Use for_each with local.deploy_project_ids for multi-environment
resource "google_pubsub_topic" "events" {
for_each = local.deploy_project_ids
name = "${var.project_name}-events"
project = each.value
}IAM for Custom Resources
Single-project (deployment/terraform/single-project/):
resource "google_pubsub_topic_iam_member" "app_publisher" {
topic = google_pubsub_topic.events.name
project = var.project_id
role = "roles/pubsub.publisher"
member = "serviceAccount:${google_service_account.app_sa.email}"
}
# Grant BigQuery data editor
resource "google_bigquery_dataset_iam_member" "app_editor" {
dataset_id = google_bigquery_dataset.analytics.dataset_id
project = var.project_id
role = "roles/bigquery.dataEditor"
member = "serviceAccount:${google_service_account.app_sa.email}"
}CI/CD (deployment/terraform/cicd/) — use for_each to apply across environments:
resource "google_pubsub_topic_iam_member" "app_publisher" {
for_each = local.deploy_project_ids
topic = google_pubsub_topic.events[each.key].name
project = each.value
role = "roles/pubsub.publisher"
member = "serviceAccount:${google_service_account.app_sa[each.key].email}"
}Applying Custom Infrastructure
# For single-project infrastructure
agents-cli infra single-project --apply # Runs terraform apply in deployment/terraform/single-project/
# For CI/CD, infrastructure is applied automatically on pushCommon Patterns
The
/apps/{app_name}/trigger/*paths below are ADK's trigger-source routes. The Terraform is the same on any framework — point it at whatever path your app serves.
Cloud Storage trigger (Eventarc):
- Create bucket in Terraform
- Create Eventarc trigger pointing to
/apps/{app_name}/trigger/eventarcendpoint - Grant
eventarc.eventReceiverrole to app service account
Pub/Sub processing:
- Create topic and push subscription in Terraform
- Point subscription to
/apps/{app_name}/trigger/pubsubendpoint - Grant
iam.serviceAccountTokenCreatorrole for push auth
Go trigger endpoints are not enabled by default, and sit under
/api. The scaffoldedmain.goalready registers thepubsubandeventarcsub-launchers, but the containerENTRYPOINTomits their keywords, so the routes are not served. Each trigger launcher keeps its own-path_prefix. The launcher prints both URLs at startup; point the subscription at those.
BigQuery Remote Function:
- Create BigQuery connection in Terraform
- Grant connection service account permission to invoke Cloud Run
- Create the remote function via SQL after deployment
Cloud SQL sessions:
- Already configured when using
--session-type cloud_sqlvia the Agents CLI (see /google-agents-cli-scaffold) - Additional tables/schemas can be added via migration scripts
Terraform State Management
Remote State (Default)
By default, infra cicd creates a GCS bucket for remote Terraform state:
# Auto-configured backend in deployment/terraform/cicd/backend.tf
terraform {
backend "gcs" {
bucket = "{cicd_project}-terraform-state"
prefix = "{repository_name}/{prod|single-project}"
}
}The state bucket is named {cicd_project}-terraform-state and uses the repository name + environment as the prefix to isolate state per project and environment.
Local State
Use the --local-state flag with infra cicd to skip remote backend setup and store state locally:
agents-cli infra cicd \
--staging-project STAGING_PROJECT \
--prod-project PROD_PROJECT \
--repository-name REPO_NAME \
--create \
--local-stateLocal state is stored in deployment/terraform/cicd/terraform.tfstate. This is suitable for single-developer projects but not recommended for teams (state conflicts).
Importing Existing Resources
If resources already exist (e.g., created manually or by a previous deployment), import them into Terraform state:
# Import a Cloud Run service
cd deployment/terraform/single-project
terraform import google_cloud_run_v2_service.app \
projects/PROJECT_ID/locations/REGION/services/SERVICE_NAME
# Import a service account
terraform import google_service_account.app_sa \
projects/PROJECT_ID/serviceAccounts/SA_EMAIL
# Import a secret
terraform import google_secret_manager_secret.my_secret \
projects/PROJECT_ID/secrets/SECRET_NAMEAfter importing, run terraform plan to verify the imported state matches the configuration. Fix any drift before applying.