Last reviewed: May 2026
Build the AWS services on the PCD exam with plain Terraform — one block at a time, each tied back to an exam domain. The same code works on OpenTofu.
By the end of this lab you'll have provisioned, with plain Terraform, the smallest realistic PCD event-driven app substrate — an Artifact Registry Docker repo to host your container images, a Pub/Sub topic + subscription as the async messaging backbone, a Cloud Run service running a placeholder image, and Secret Manager for app secrets. Five blocks; every PCD exam scenario composes on this base.
Drop the snippets into a single main.tf, run terraform init, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.your-project-id in the provider block.All free or near-free at lab scope:
min_instances = 0 — zero idle cost.~$0/month at lab volume. Cloud Run with min_instances = 0 is the PCD-recommended default — no idle billing.
Enable Cloud Run, Artifact Registry, Pub/Sub, and Secret Manager APIs.
terraform {
required_version = ">= 1.5"
required_providers {
google = { source = "hashicorp/google", version = "~> 6.0" }
}
}
provider "google" {
project = "your-project-id" # REPLACE
region = "us-central1"
}
locals {
labels = {
project = "certlabpro-pcd"
managed_by = "terraform"
}
}
resource "google_project_service" "run" {
service = "run.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "artifactregistry" {
service = "artifactregistry.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "pubsub" {
service = "pubsub.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "secretmanager" {
service = "secretmanager.googleapis.com"
disable_on_destroy = false
}Artifact Registry is GCP's container + package registry — the modern replacement for the deprecated Container Registry (gcr.io). PCD exam tests this push to Artifact Registry → deploy from Artifact Registry pattern as the canonical CI/CD shape.
We create a regional Docker-format repository. Push to it after terraform apply via:
gcloud auth configure-docker us-central1-docker.pkg.dev
docker tag my-app:latest us-central1-docker.pkg.dev/$PROJECT/certlabpro-pcd-images/my-app:latest
docker push us-central1-docker.pkg.dev/$PROJECT/certlabpro-pcd-images/my-app:latest
resource "google_artifact_registry_repository" "images" {
repository_id = "certlabpro-pcd-images"
location = "us-central1"
format = "DOCKER"
description = "PCD lab Docker images"
labels = local.labels
depends_on = [google_project_service.artifactregistry]
}Pub/Sub is GCP's managed messaging — the foundation of every PCD-pattern event-driven app. Topics are publish-targets; subscriptions are consumer endpoints (pull or push). PCD exam tests at-least-once delivery + idempotent consumers as the load-bearing semantics.
We create:
orders — where producers publish.orders-worker — consumers pull from this. Production deployments often use push subscriptions to Cloud Run endpoints instead (the PCD-canonical event-driven shape: Pub/Sub push → Cloud Run); we stay pull here to keep IAM simpler.ack_deadline_seconds = 60 gives consumers 60 seconds to ack; longer than the Cloud Run default request timeout and a recurring PCD exam tuning knob.
resource "google_pubsub_topic" "orders" {
name = "orders"
labels = local.labels
depends_on = [google_project_service.pubsub]
}
resource "google_pubsub_subscription" "orders_worker" {
name = "orders-worker"
topic = google_pubsub_topic.orders.id
ack_deadline_seconds = 60
message_retention_duration = "604800s" # 7 days (max)
retry_policy {
minimum_backoff = "10s"
maximum_backoff = "600s"
}
labels = local.labels
}Cloud Run is GCP's container-as-a-service — runs Docker containers on Google's serverless infra, scales to zero. PCD-recommended pattern: each Cloud Run service runs as its own service account, never the default Compute service account.
We deploy a service running the public hello image as a placeholder, attach a per-service service account with roles/pubsub.subscriber on the topic from Step 3, and grant roles/run.invoker to allUsers for unauthenticated access. Switch to IAM-gated invoker for production — the PCD exam tests this allUsers ↔ specific-identity flip.
resource "google_service_account" "worker" {
account_id = "certlabpro-pcd-worker"
display_name = "PCD lab Cloud Run worker"
}
resource "google_pubsub_subscription_iam_member" "worker_subscriber" {
subscription = google_pubsub_subscription.orders_worker.name
role = "roles/pubsub.subscriber"
member = "serviceAccount:${google_service_account.worker.email}"
}
resource "google_cloud_run_v2_service" "worker" {
name = "certlabpro-pcd-worker"
location = "us-central1"
template {
service_account = google_service_account.worker.email
scaling {
min_instance_count = 0 # scale-to-zero — no idle billing
max_instance_count = 5
}
containers {
image = "us-docker.pkg.dev/cloudrun/container/hello"
resources {
limits = {
cpu = "1"
memory = "512Mi"
}
}
}
}
labels = local.labels
depends_on = [google_project_service.run]
}
resource "google_cloud_run_v2_service_iam_member" "public_invoker" {
name = google_cloud_run_v2_service.worker.name
location = google_cloud_run_v2_service.worker.location
role = "roles/run.invoker"
member = "allUsers" # lab-only; production = specific identity
}Secret Manager is GCP's secret store — versioned values, IAM-controlled access, automatic rotation hooks. PCD-recommended pattern: app config that's not a secret goes in env vars; app config that is a secret (API keys, DB passwords, OAuth tokens) goes in Secret Manager, referenced by the Cloud Run service.
We create a secret + initial version, then grant the Cloud Run service account from Step 4 roles/secretmanager.secretAccessor on it. To wire the secret into Cloud Run as an env var, you'd add a env { name = "..."; value_source { secret_key_ref { ... } } } block in the service template (deliberately omitted to keep this step focused).
resource "google_secret_manager_secret" "db_password" {
secret_id = "certlabpro-pcd-db-password"
replication {
auto {}
}
labels = local.labels
depends_on = [google_project_service.secretmanager]
}
resource "google_secret_manager_secret_version" "db_password_v1" {
secret = google_secret_manager_secret.db_password.id
secret_data = "lab-placeholder-rotate-me"
}
resource "google_secret_manager_secret_iam_member" "worker_reader" {
secret_id = google_secret_manager_secret.db_password.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.worker.email}"
}terraform destroy tears down everything. The Cloud Run service destroys (with min_instances = 0 it was already costing nothing). The Artifact Registry repo destroys (force_destroy is implicit since the lab doesn't push any images). Pub/Sub + Secret Manager resources destroy cleanly.
PCD covers many GCP app-developer surfaces this lab can't fit — Cloud Functions (the older / lighter alternative to Cloud Run), Cloud Tasks (durable async queue), Cloud Scheduler (cron-as-a-service), Cloud Endpoints / API Gateway (managed API surface in front of Cloud Run), Cloud Trace + Cloud Profiler + Cloud Debugger (app-perf telemetry — the [[gcp-pcde]] tier), Firebase (the mobile/web app platform), Cloud Storage (used as static asset hosting for SPAs), Cloud Build (CI/CD — [[gcp-pcde]]), Cloud Deploy (CD — [[gcp-pcde]]), Eventarc (event router from GCP services to Cloud Run / Functions), Workflows (orchestration), Firestore (NoSQL DB for apps), Memorystore (Redis), Vertex AI for in-app GenAI, and the entire App Engine product (legacy but still on the PCD exam blueprint).
We stick to the Artifact Registry + Pub/Sub + Cloud Run + Secret Manager primitives because they're the PCD-canonical event-driven app spine. Cloud Functions + Cloud Run share the same Artifact Registry image substrate. Eventarc routes events into Cloud Run / Functions. Cloud Tasks is the synchronous-RPC variant of Pub/Sub. Master the substrate; the alternatives slot in.
For service-by-service conceptual coverage, see the Browse, Playbook, and Editorial sections of this cert page.