Skip to content

Terraform infrastructure for GCP #11

@jonaskay

Description

@jonaskay

Problem

ThreadOps has no infrastructure-as-code yet. The two Cloud Run services (webhook, processor), Pub/Sub topic, Secret Manager secrets, and Artifact Registry are either unprovisioned or would need to be created by hand.

Proposal

Add a complete, apply-ready Terraform configuration under infra/ that provisions all GCP resources ThreadOps needs. Work is split into six phases so each can be validated independently.

Prerequisites (manual, one-time)

Create the GCS bucket for remote state — it cannot be managed by the same Terraform config it backs:

gcloud storage buckets create gs://threadops-tfstate \
  --location=europe-north1 \
  --uniform-bucket-level-access

What to build

Phase 1 — Scaffold and provider foundation

Create the infra/ directory as the Terraform root with:

  • versions.tf — pin hashicorp/google (>= 6.0) and hashicorp/google-beta.
  • backend.tf — GCS backend pointing at the state bucket.
  • variables.tf — declare project_id, region, environment, image_tag.
  • outputs.tf — placeholder file, populated in later phases.
  • main.tf — configure the google provider with project and region.
  • apis.tf — enable required GCP APIs via google_project_service for_each: run, pubsub, secretmanager, artifactregistry, iam.

Verify: terraform init && terraform validate.

Phase 2 — IAM and service accounts

Create iam.tf with two service accounts:

  • threadops-receiverroles/pubsub.publisher.
  • threadops-workerroles/pubsub.subscriber, roles/secretmanager.secretAccessor, roles/artifactregistry.reader.

Use google_project_iam_member exclusively (never _binding — it's destructive). Output both SA emails in outputs.tf.

Phase 3 — Secret Manager

Create secrets.tf with three google_secret_manager_secret resources:

  • threadops-slack-signing-secret
  • threadops-github-token
  • threadops-llm-api-key

Set replication { auto {} } on each. Do not create google_secret_manager_secret_version resources — values are seeded manually or via CI to keep secrets out of state.

Phase 4 — Artifact Registry and Pub/Sub

artifact_registry.tf:

  • google_artifact_registry_repository with repository_id = "threadops", format = "DOCKER".
  • Grant roles/artifactregistry.reader to both SAs.

pubsub.tf:

  • google_pubsub_topic "thread_transcripts".
  • google_pubsub_subscription "worker_pull"pull subscription (ack_deadline_seconds = 60) with a dead-letter topic. Pull avoids a circular dependency with the worker Cloud Run URL.

Phase 5 — Cloud Run services

cloud_run_receiver.tfgoogle_cloud_run_v2_service:

  • ingress = "INGRESS_TRAFFIC_ALL" (must accept Slack webhooks from the internet).
  • Env vars: PUBSUB_TOPIC (matches webhook service), PROJECT_ID.
  • Secret ref: SLACK_SIGNING_SECRET from Secret Manager.
  • SA: threadops-receiver.
  • Min instances: 1 (Slack requires a 3-second ack — avoid cold starts).
  • IAM: allUsers invoker.

cloud_run_worker.tfgoogle_cloud_run_v2_service:

  • ingress = "INGRESS_TRAFFIC_INTERNAL_ONLY".
  • Env vars: PUBSUB_SUBSCRIPTION_ID, GCP_PROJECT_ID, GITHUB_REPO.
  • Secret refs: GITHUB_TOKEN, LLM_API_KEY (mapped from threadops-llm-api-key) from Secret Manager.
  • SA: threadops-worker.
  • Min instances: 0 (worker latency is not user-facing).

Add both service URLs to outputs.tf.

Env var naming note: the webhook service currently uses PROJECT_ID and PUBSUB_TOPIC; the processor uses GITHUB_TOKEN, GITHUB_REPO, ANTHROPIC_API_KEY, ANTHROPIC_MODEL, SLACK_BOT_TOKEN. The Terraform env var names must match what the Go code reads from os.Getenv.

Phase 6 — Final wiring and review

  • locals.tf — shared name prefixes and labels (app = "threadops", managed_by = "terraform"). Apply labels to all resources.
  • infra/README.md — prerequisites, init/plan/apply instructions, how to update image tags via var.image_tag.
  • Run terraform fmt -recursive and terraform validate.

Target file structure

infra/
├── README.md
├── apis.tf
├── artifact_registry.tf
├── backend.tf
├── cloud_run_receiver.tf
├── cloud_run_worker.tf
├── iam.tf
├── locals.tf
├── main.tf
├── outputs.tf
├── pubsub.tf
├── secrets.tf
├── variables.tf
└── versions.tf

Constraints

  • Never store secret values in .tf files or state — stubs only.
  • Use google_project_iam_member exclusively — never _binding.
  • Pull subscription only — no push (avoids circular Cloud Run URL dependency).
  • INGRESS_TRAFFIC_ALL on receiver, INTERNAL_ONLY on worker — this is a security boundary.
  • Work phase by phase; run terraform validate before moving on.
  • Run terraform fmt -recursive at the end of every phase.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions