locals { env = module.envs.envs[var.env] project = local.env.project region = local.env.region # this module sits inside the tree it uploads blueprint_dir = "${path.module}/../.." # infra manager's terraform, which the blueprint has to stay compatible with tf_version = "=1.5.7" } module "envs" { source = "../envs" } data "archive_file" "blueprint" { type = "zip" source_dir = local.blueprint_dir output_path = "${local.blueprint_dir}/.build/${var.env}.zip" excludes = [ ".build", ".gitignore", ".terraform", ".terraform.lock.hcl", "*.tfplan", "README.md", "control", "modules/control", "terraform.tfstate", "terraform.tfstate.backup", ] } resource "google_storage_bucket" "blueprints" { project = local.project name = "${local.project}-blueprints" location = local.region uniform_bucket_level_access = true public_access_prevention = "enforced" versioning { enabled = true } # revisions fetch their blueprint by name, so superseded generations only # need to outlive the audit window, not forever lifecycle_rule { condition { num_newer_versions = 10 } action { type = "Delete" } } } resource "google_storage_bucket_object" "blueprint" { bucket = google_storage_bucket.blueprints.name name = "${var.env}-${data.archive_file.blueprint.output_sha}.zip" source = data.archive_file.blueprint.output_path } # the deployment identity is created and granted HERE, outside the revisions # that run as it - the blueprint never declares its own powers resource "google_service_account" "deploy" { project = local.project account_id = "im-deployer-${local.env.env_suffix}" display_name = "infra manager deployment identity for ${var.env}" } resource "google_service_account" "ci" { project = local.project account_id = "ci-deployer-${local.env.env_suffix}" display_name = "ci deploy identity for ${var.env}" } resource "google_project_iam_member" "ci" { for_each = toset([ "roles/config.admin", "roles/config.agent", "roles/artifactregistry.writer", ]) project = local.project role = each.value member = google_service_account.ci.member } resource "google_service_account_iam_member" "ci_acts_as_deployment" { service_account_id = google_service_account.deploy.name role = "roles/iam.serviceAccountUser" member = google_service_account.ci.member } resource "google_project_service" "this" { for_each = toset(["certificatemanager.googleapis.com", "secretmanager.googleapis.com"]) project = local.project service = each.value disable_on_destroy = false } # the secrets services read live here, outside the blueprint, so a value can be added # (gcloud secrets versions add) before any revision needs it and never reaches state. # readers are named the way modules/environment names its service accounts locals { secret_reads = merge([ for svc, s in local.env.services : { for _, id in try(s.secrets, {}) : "${id}/${svc}" => { id = id, svc = svc } } ]...) } resource "google_secret_manager_secret" "this" { for_each = toset([for r in local.secret_reads : r.id]) project = local.project secret_id = each.value replication { auto {} } depends_on = [google_project_service.this] } resource "google_secret_manager_secret_iam_member" "readers" { for_each = local.secret_reads project = local.project secret_id = google_secret_manager_secret.this[each.value.id].secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${each.value.svc}-${local.env.env_suffix}@${local.project}.iam.gserviceaccount.com" } locals { ci_oidc = try(local.env.ci_oidc, null) ci_wif = local.ci_oidc == null ? toset([]) : toset(["spindle"]) } resource "google_project_service" "sts" { for_each = local.ci_wif project = local.project service = "sts.googleapis.com" disable_on_destroy = false } resource "google_iam_workload_identity_pool" "ci" { for_each = local.ci_wif project = local.project workload_identity_pool_id = "tangled-ci" display_name = "tangled ci" } resource "google_iam_workload_identity_pool_provider" "spindle" { for_each = local.ci_wif project = local.project workload_identity_pool_id = google_iam_workload_identity_pool.ci[each.key].workload_identity_pool_id workload_identity_pool_provider_id = "spindle" display_name = "tangled spindle" attribute_mapping = { "google.subject" = "assertion.sub" "attribute.repository" = "assertion.repository" "attribute.ref" = "assertion.ref" } # manual runs carry an empty ref, so repository alone would admit them attribute_condition = "assertion.repository == '${local.ci_oidc.repository}' && assertion.ref == '${local.ci_oidc.ref}'" oidc { issuer_uri = local.ci_oidc.issuer } depends_on = [google_project_service.sts] } resource "google_service_account_iam_member" "ci_federated" { for_each = local.ci_wif service_account_id = google_service_account.ci.name role = "roles/iam.workloadIdentityUser" member = "principal://iam.googleapis.com/${google_iam_workload_identity_pool.ci[each.key].name}/subject/${local.ci_oidc.repository}:${local.ci_oidc.ref}" } resource "google_project_iam_member" "deploy" { for_each = toset([ "roles/config.agent", "roles/run.admin", "roles/compute.networkAdmin", "roles/compute.loadBalancerAdmin", "roles/artifactregistry.admin", "roles/iam.serviceAccountUser", "roles/certificatemanager.editor", ]) project = local.project role = each.value member = google_service_account.deploy.member } resource "google_config_deployment" "this" { project = local.project name = "tangled-${var.env}" location = local.region service_account = google_service_account.deploy.id tf_version_constraint = local.tf_version deletion_policy = "ABANDON" terraform_blueprint { gcs_source = "gs://${google_storage_bucket.blueprints.name}/${google_storage_bucket_object.blueprint.name}" input_values { variable_name = "env_name" input_value = jsonencode(var.env) } # an empty map comes back from infra manager as "", which would plan as a change forever dynamic "input_values" { for_each = length(var.image_refs) == 0 ? [] : [var.image_refs] content { variable_name = "image_refs" input_value = jsonencode(input_values.value) } } } labels = { env = local.env.env_suffix } } resource "google_storage_bucket_iam_member" "deployment_reads_blueprint" { bucket = google_storage_bucket.blueprints.name role = "roles/storage.objectViewer" member = google_service_account.deploy.member }