Registry, identities, and the Cloud Run service. The image tag is deliberately kept out of Terraform's control. CI owns it, so applying infrastructure never rolls the site back to the bootstrap placeholder.
# ---------------------------------------------------------------------------
# Enabled services
# ---------------------------------------------------------------------------
locals {
services = [
"run.googleapis.com",
"artifactregistry.googleapis.com",
"iamcredentials.googleapis.com",
"sts.googleapis.com",
"secretmanager.googleapis.com",
"firestore.googleapis.com",
"iam.googleapis.com",
"storage.googleapis.com",
# Gen-2 Cloud Functions are Cloud Run services built by Cloud Build and
# wired through Eventarc, so all four are required even for a plain HTTP
# function. terraform plan does not check API enablement, so a missing one
# here surfaces only as a failed apply.
"cloudfunctions.googleapis.com",
"cloudbuild.googleapis.com",
"eventarc.googleapis.com",
"pubsub.googleapis.com",
"monitoring.googleapis.com",
"logging.googleapis.com",
# Terraform reads project services and IAM through it. Applying from a
# laptop never needed it, because gcloud bills those calls to its own
# project; CI's service account bills them here, so planning in CI failed
# with "Cloud Resource Manager API has not been used in project" until it
# was on.
"cloudresourcemanager.googleapis.com",
]
}
resource "google_project_service" "enabled" {
for_each = toset(local.services)
project = var.project_id
service = each.value
# Leave the APIs on if this config is torn down; disabling them would break
# anything else in the project that came to depend on them.
disable_on_destroy = false
}
# ---------------------------------------------------------------------------
# Container images
# ---------------------------------------------------------------------------
resource "google_artifact_registry_repository" "images" {
location = var.region
repository_id = "zayda-dev"
description = "Container images for zayda.dev"
format = "DOCKER"
# Without this, every deploy leaves an image behind forever and storage cost
# grows without bound.
cleanup_policies {
id = "keep-recent"
action = "KEEP"
most_recent_versions {
keep_count = 10
}
}
cleanup_policies {
id = "drop-old-untagged"
action = "DELETE"