Something went wrong. Try again.
Tangled infrastructure definitions in Nix
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244locals { 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 powersresource "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 accountslocals { 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}