# Onboards a GCP project for SubImage BYOC, as an alternative to running the # gcp-bootstrap.sh script shown in the management portal. It creates the same # APIs, service accounts, roles and identity pool, reading the account's # identity and Terraform roles from that script. # # Prerequisite: a GKE cluster with Workload Identity must have existed in the # project at least once, so that the .svc.id.goog identity pool # exists. Without it, the Workload Identity bindings below fail with # "Identity Pool does not exist". A temporary Autopilot cluster creates it: # # gcloud container clusters create-auto temp-pool --project= --region=us-central1 # gcloud container clusters delete temp-pool --project= --region=us-central1 --quiet # # Usage: fill in the locals, then `terraform init && terraform apply`. locals { # The project SubImage deploys into. project_id = "my-project" # The Account Config ID shown in the management portal for this account. account_config_id = "ac-xxxxxxxxxx" # Lets the agents change GCP infrastructure. Set to false outside approved # update windows: the agent and Terraform accounts then keep read-only roles, # and upgrades, scaling and repairs fail until it is set back to true. agent_infrastructure_mutation_enabled = true oidc_issuer = "https://oidc.eks.us-east-2.amazonaws.com/id/B785000B73171573504052986290EAB0" bootstrap_script = data.http.bootstrap_script.response_body config_identity = regex("bootstrap-(org-[a-z0-9]+)@", local.bootstrap_script)[0] # One Terraform service account per service model, with the roles the # script grants it. terraform_roles = { for call in regexall("omnistrate_custom_tf_service_account \"(sm-[A-Za-z0-9]+)\"([^\\n]*)", local.bootstrap_script) : call[0] => regexall("roles/[A-Za-z.]+", call[1]) } apis = [ "cloudresourcemanager.googleapis.com", "compute.googleapis.com", "container.googleapis.com", "file.googleapis.com", "iam.googleapis.com", "iamcredentials.googleapis.com", "serviceusage.googleapis.com", "storage.googleapis.com", "sts.googleapis.com", ] service_account_roles = { bootstrap = [ "roles/compute.admin", "roles/container.admin", "roles/iam.securityAdmin", "roles/iam.serviceAccountTokenCreator", "roles/iam.serviceAccountUser", "roles/storage.admin", ] omnistrate = [ "roles/iam.serviceAccountTokenCreator", "roles/logging.logWriter", "roles/monitoring.metricWriter", "roles/secretmanager.secretAccessor", "roles/storage.admin", "roles/storage.objectUser", ] omnistrate-da = [ "roles/compute.admin", "roles/container.admin", "roles/file.editor", "roles/iam.serviceAccountTokenCreator", "roles/iam.serviceAccountUser", "roles/storage.admin", "projects/${local.project_id}/roles/omnistrateServiceAccountPolicyManager", ] omnistrate-cm = [ "roles/dns.admin", ] } # What each mutating role becomes while infrastructure mutation is off. An # empty value drops the role. read_only_roles = { "roles/cloudkms.admin" = "roles/cloudkms.viewer" "roles/cloudsql.admin" = "roles/cloudsql.viewer" "roles/compute.admin" = "roles/compute.viewer" "roles/container.admin" = "roles/container.viewer" "roles/file.editor" = "roles/file.viewer" "roles/iam.securityAdmin" = "roles/iam.securityReviewer" "roles/iam.serviceAccountAdmin" = "roles/iam.serviceAccountViewer" "roles/memorystore.admin" = "roles/memorystore.viewer" "roles/networkconnectivity.consumerNetworkAdmin" = "" "roles/resourcemanager.projectIamAdmin" = "roles/iam.securityReviewer" "roles/secretmanager.admin" = "roles/secretmanager.viewer" "roles/serviceusage.serviceUsageAdmin" = "roles/serviceusage.serviceUsageViewer" "roles/storage.admin" = "roles/storage.objectViewer" "projects/${local.project_id}/roles/omnistrateServiceAccountPolicyManager" = "" # Project-wide, these would let a read-only account impersonate the # accounts that keep write access. The agent still reaches the Terraform # accounts through their own Token Creator bindings below. "roles/iam.serviceAccountTokenCreator" = "" "roles/iam.serviceAccountUser" = "" } agent_accounts = ["bootstrap", "omnistrate-da"] granted_service_account_roles = { for account, roles in local.service_account_roles : account => ( local.agent_infrastructure_mutation_enabled || !contains(local.agent_accounts, account) ? roles : distinct(compact([for role in roles : lookup(local.read_only_roles, role, role)])) ) } granted_terraform_roles = { for model, roles in local.terraform_roles : model => ( local.agent_infrastructure_mutation_enabled ? roles : distinct(compact([for role in roles : lookup(local.read_only_roles, role, role)])) ) } workload_pool = "${local.project_id}.svc.id.goog" da_service_account = google_service_account.this["omnistrate-da"] } terraform { required_providers { google = { source = "hashicorp/google" version = ">= 6.0" } http = { source = "hashicorp/http" version = ">= 3.0" } } } provider "google" { project = local.project_id } data "http" "bootstrap_script" { url = "https://api.omnistrate.cloud/2022-09-01-00/account-setup/gcp-bootstrap.sh?account_config_id=${local.account_config_id}" } data "google_project" "this" {} resource "google_project_service" "this" { for_each = toset(local.apis) service = each.value disable_on_destroy = false } resource "google_service_account" "this" { for_each = toset(keys(local.service_account_roles)) account_id = "${each.value}-${local.config_identity}" depends_on = [google_project_service.this] } resource "google_project_iam_custom_role" "service_account_policy_manager" { role_id = "omnistrateServiceAccountPolicyManager" title = "IAM Service Account Policy Manager (custom role)" permissions = ["iam.serviceAccounts.getIamPolicy", "iam.serviceAccounts.setIamPolicy"] depends_on = [google_project_service.this] } resource "google_project_iam_member" "service_accounts" { for_each = { for pair in flatten([ for account, roles in local.granted_service_account_roles : [ for role in roles : { account = account, role = role } ] ]) : "${pair.account} ${pair.role}" => pair } project = local.project_id role = each.value.role member = google_service_account.this[each.value.account].member depends_on = [google_project_iam_custom_role.service_account_policy_manager] } resource "google_iam_workload_identity_pool" "control_plane" { workload_identity_pool_id = "omnistrate-bootstrap-id-pool" display_name = "Control Plane Access Pool" description = "Workload Identity Pool for the control plane" depends_on = [google_project_service.this] } resource "google_iam_workload_identity_pool_provider" "control_plane" { workload_identity_pool_id = google_iam_workload_identity_pool.control_plane.workload_identity_pool_id workload_identity_pool_provider_id = "omnistrate-oidc-prov" display_name = "Control Plane OIDC Provider" description = "OIDC provider for the control plane" attribute_mapping = { "google.subject" = "assertion.sub" "attribute.subject" = "assertion.sub.extract(\"subject/{sub}/\")" } oidc { issuer_uri = local.oidc_issuer allowed_audiences = ["sts.amazonaws.com"] } } resource "google_service_account_iam_member" "bootstrap" { service_account_id = google_service_account.this["bootstrap"].name role = "roles/iam.workloadIdentityUser" member = "principal://iam.googleapis.com/projects/${data.google_project.this.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.control_plane.workload_identity_pool_id}/subject/system:serviceaccount:bootstrap:bootstrap-sa" } resource "google_service_account_iam_member" "dataplane_agent" { service_account_id = local.da_service_account.name role = "roles/iam.workloadIdentityUser" member = "serviceAccount:${local.workload_pool}[dataplane-agent/${local.da_service_account.account_id}]" } resource "google_service_account_iam_member" "cert_manager" { service_account_id = google_service_account.this["omnistrate-cm"].name role = "roles/iam.workloadIdentityUser" member = "serviceAccount:${local.workload_pool}[cert-manager-ns/cert-manager]" } resource "google_service_account" "terraform" { for_each = local.terraform_roles # Service model IDs are mixed case, which the provider rejects in an account # ID; GCP resolves service account emails case-insensitively. account_id = lower("omnistrate-tf-${each.key}") depends_on = [google_project_service.this] } resource "google_service_account_iam_member" "terraform_workload_identity" { for_each = google_service_account.terraform service_account_id = each.value.name role = "roles/iam.workloadIdentityUser" member = "serviceAccount:${local.workload_pool}[dataplane-agent/${local.da_service_account.account_id}]" } resource "google_service_account_iam_member" "terraform_token_creator" { for_each = google_service_account.terraform service_account_id = each.value.name role = "roles/iam.serviceAccountTokenCreator" member = local.da_service_account.member } resource "google_project_iam_member" "terraform" { for_each = { for pair in flatten([ for model, roles in local.granted_terraform_roles : [ for role in roles : { model = model, role = role } ] ]) : "${pair.model} ${pair.role}" => pair } project = local.project_id role = each.value.role member = google_service_account.terraform[each.value.model].member }