diff --git a/blueprints/data-solutions/data-platform-foundations/main.tf b/blueprints/data-solutions/data-platform-foundations/main.tf
index bdf5c279b..0bcae7353 100644
--- a/blueprints/data-solutions/data-platform-foundations/main.tf
+++ b/blueprints/data-solutions/data-platform-foundations/main.tf
@@ -1,4 +1,4 @@
-# Copyright 2023 Google LLC
+# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
diff --git a/blueprints/data-solutions/shielded-folder/kms.tf b/blueprints/data-solutions/shielded-folder/kms.tf
index 4a634fcc7..83f1c5fcb 100644
--- a/blueprints/data-solutions/shielded-folder/kms.tf
+++ b/blueprints/data-solutions/shielded-folder/kms.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -66,8 +66,8 @@ module "sec-project" {
prefix = (
var.project_config.billing_account_id == null ? null : var.prefix
)
- group_iam = {
- (local.groups.workload-security) = [
+ iam_by_principals = {
+ "group:${local.groups.workload-security}" = [
"roles/editor"
]
}
diff --git a/blueprints/data-solutions/shielded-folder/log-export.tf b/blueprints/data-solutions/shielded-folder/log-export.tf
index f0cf0ca78..5afc941c9 100644
--- a/blueprints/data-solutions/shielded-folder/log-export.tf
+++ b/blueprints/data-solutions/shielded-folder/log-export.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,19 +17,44 @@
# tfdoc:file:description Audit log project and sink.
locals {
+ _log_keys = var.enable_features.encryption ? {
+ bq = (
+ var.enable_features.log_sink
+ ? [format(
+ "projects/%s/locations/%s/keyRings/%s/cryptoKeys/bq",
+ module.sec-project.0.project_id,
+ var.log_locations.bq,
+ var.log_locations.bq
+ )]
+ : null
+ )
+ pubsub = (
+ var.enable_features.log_sink
+ ? [format(
+ "projects/%s/locations/%s/keyRings/%s/cryptoKeys/pubsub",
+ module.sec-project.0.project_id,
+ var.log_locations.pubsub,
+ var.log_locations.pubsub
+ )]
+ : null
+ )
+ storage = (
+ var.enable_features.log_sink
+ ? [format(
+ "projects/%s/locations/%s/keyRings/%s/cryptoKeys/storage",
+ module.sec-project.0.project_id,
+ var.log_locations.storage,
+ var.log_locations.storage
+ )]
+ : null
+ )
+ } : {}
gcs_storage_class = (
length(split("-", var.log_locations.storage)) < 2
? "MULTI_REGIONAL"
: "REGIONAL"
)
log_types = toset([for k, v in var.log_sinks : v.type])
-
- _log_keys = var.enable_features.encryption ? {
- bq = var.enable_features.log_sink ? ["projects/${module.sec-project.0.project_id}/locations/${var.log_locations.bq}/keyRings/${var.log_locations.bq}/cryptoKeys/bq"] : null
- pubsub = var.enable_features.log_sink ? ["projects/${module.sec-project.0.project_id}/locations/${var.log_locations.pubsub}/keyRings/${var.log_locations.pubsub}/cryptoKeys/pubsub"] : null
- storage = var.enable_features.log_sink ? ["projects/${module.sec-project.0.project_id}/locations/${var.log_locations.storage}/keyRings/${var.log_locations.storage}/cryptoKeys/storage"] : null
- } : {}
-
log_keys = {
for service, key in local._log_keys : service => key if key != null
}
@@ -42,9 +67,11 @@ module "log-export-project" {
parent = module.folder.id
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
- prefix = var.project_config.billing_account_id == null ? null : var.prefix
- group_iam = {
- (local.groups.workload-security) = [
+ prefix = (
+ var.project_config.billing_account_id == null ? null : var.prefix
+ )
+ iam_by_principals = {
+ "group:${local.groups.workload-security}" = [
"roles/editor"
]
}
diff --git a/blueprints/data-solutions/shielded-folder/main.tf b/blueprints/data-solutions/shielded-folder/main.tf
index f4424a52f..4524999ad 100644
--- a/blueprints/data-solutions/shielded-folder/main.tf
+++ b/blueprints/data-solutions/shielded-folder/main.tf
@@ -1,4 +1,4 @@
-# Copyright 2023 Google LLC
+# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -47,8 +47,8 @@ locals {
groups_iam = {
for k, v in local.groups : k => "group:${v}"
}
- group_iam = {
- (local.groups.workload-engineers) = [
+ iam_principals = {
+ "group:${local.groups.workload-engineers}" = [
"roles/editor",
"roles/iam.serviceAccountTokenCreator"
]
@@ -71,12 +71,12 @@ locals {
}
module "folder" {
- source = "../../../modules/folder"
- folder_create = var.folder_config.folder_create != null
- parent = try(var.folder_config.folder_create.parent, null)
- name = try(var.folder_config.folder_create.display_name, null)
- id = var.folder_config.folder_create != null ? null : var.folder_config.folder_id
- group_iam = local.group_iam
+ source = "../../../modules/folder"
+ folder_create = var.folder_config.folder_create != null
+ parent = try(var.folder_config.folder_create.parent, null)
+ name = try(var.folder_config.folder_create.display_name, null)
+ id = var.folder_config.folder_create != null ? null : var.folder_config.folder_id
+ iam_by_principals = local.iam_principals
factories_config = {
org_policies = var.data_dir != null ? "${var.data_dir}/org-policies" : null
}
diff --git a/blueprints/data-solutions/vertex-mlops/main.tf b/blueprints/data-solutions/vertex-mlops/main.tf
index 83ec5072f..55e5614e2 100644
--- a/blueprints/data-solutions/vertex-mlops/main.tf
+++ b/blueprints/data-solutions/vertex-mlops/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,9 +16,9 @@
locals {
- group_iam = merge(
+ iam_principals = merge(
var.groups.gcp-ml-viewer == null ? {} : {
- (var.groups.gcp-ml-viewer) = [
+ "group:${var.groups.gcp-ml-viewer}" = [
"roles/aiplatform.viewer",
"roles/artifactregistry.reader",
"roles/dataflow.viewer",
@@ -27,7 +27,7 @@ locals {
]
},
var.groups.gcp-ml-ds == null ? {} : {
- (var.groups.gcp-ml-ds) = [
+ "group:${var.groups.gcp-ml-ds}" = [
"roles/aiplatform.admin",
"roles/artifactregistry.admin",
"roles/bigquery.dataEditor",
@@ -47,7 +47,7 @@ locals {
]
},
var.groups.gcp-ml-eng == null ? {} : {
- (var.groups.gcp-ml-eng) = [
+ "group:${var.groups.gcp-ml-eng}" = [
"roles/aiplatform.admin",
"roles/artifactregistry.admin",
"roles/bigquery.dataEditor",
@@ -189,13 +189,13 @@ module "cloudnat" {
}
module "project" {
- source = "../../../modules/project"
- name = var.project_config.project_id
- parent = var.project_config.parent
- billing_account = var.project_config.billing_account_id
- project_create = var.project_config.billing_account_id != null
- prefix = var.prefix
- group_iam = local.group_iam
+ source = "../../../modules/project"
+ name = var.project_config.project_id
+ parent = var.project_config.parent
+ billing_account = var.project_config.billing_account_id
+ project_create = var.project_config.billing_account_id != null
+ prefix = var.prefix
+ iam_by_principals = local.iam_principals
iam = {
"roles/aiplatform.user" = [
module.service-account-mlops.iam_email,
diff --git a/blueprints/factories/project-factory/main.tf b/blueprints/factories/project-factory/main.tf
index 81f1d3165..1cb17be85 100644
--- a/blueprints/factories/project-factory/main.tf
+++ b/blueprints/factories/project-factory/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,10 +29,10 @@ module "projects" {
)
default_service_account = try(each.value.default_service_account, "keep")
descriptive_name = try(each.value.descriptive_name, null)
- group_iam = try(each.value.group_iam, {})
iam = try(each.value.iam, {})
iam_bindings = try(each.value.iam_bindings, {})
iam_bindings_additive = try(each.value.iam_bindings_additive, {})
+ iam_by_principals = try(each.value.iam_by_principals, {})
labels = merge(
each.value.labels, var.data_merges.labels
)
diff --git a/blueprints/gke/multitenant-fleet/README.md b/blueprints/gke/multitenant-fleet/README.md
index f35f60d87..1967e4ec7 100644
--- a/blueprints/gke/multitenant-fleet/README.md
+++ b/blueprints/gke/multitenant-fleet/README.md
@@ -63,8 +63,8 @@ module "gke-fleet" {
billing_account_id = var.billing_account_id
folder_id = var.folder_id
prefix = "myprefix"
- group_iam = {
- "gke-admin@example.com" = [
+ iam_by_principals = {
+ "group:gke-admin@example.com" = [
"roles/container.admin"
]
}
@@ -249,8 +249,8 @@ module "gke" {
| [fleet_configmanagement_templates](variables.tf#L103) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(any) | | {} |
| [fleet_features](variables.tf#L111) | Enable and configure fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null |
| [fleet_workload_identity](variables.tf#L124) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false |
-| [group_iam](variables.tf#L136) | Project-level IAM bindings for groups. Use group emails as keys, list of roles as values. | map(list(string)) | | {} |
-| [iam](variables.tf#L143) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam](variables.tf#L136) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_by_principals](variables.tf#L143) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
| [labels](variables.tf#L150) | Project-level labels. | map(string) | | {} |
| [nodepools](variables.tf#L156) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} |
| [project_services](variables.tf#L195) | Additional project services to enable. | list(string) | | [] |
diff --git a/blueprints/gke/multitenant-fleet/main.tf b/blueprints/gke/multitenant-fleet/main.tf
index dd117fbb7..4fb948575 100644
--- a/blueprints/gke/multitenant-fleet/main.tf
+++ b/blueprints/gke/multitenant-fleet/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,13 +27,13 @@ locals {
}
module "gke-project-0" {
- source = "../../../modules/project"
- billing_account = var.billing_account_id
- name = var.project_id
- parent = var.folder_id
- prefix = var.prefix
- group_iam = var.group_iam
- labels = var.labels
+ source = "../../../modules/project"
+ billing_account = var.billing_account_id
+ name = var.project_id
+ parent = var.folder_id
+ prefix = var.prefix
+ iam_by_principals = var.iam_by_principals
+ labels = var.labels
iam = merge(var.iam, {
"roles/gkehub.serviceAgent" = [
"serviceAccount:${module.gke-project-0.service_accounts.robots.fleet}"
diff --git a/blueprints/gke/multitenant-fleet/variables.tf b/blueprints/gke/multitenant-fleet/variables.tf
index 93886d0bc..c0f49ab84 100644
--- a/blueprints/gke/multitenant-fleet/variables.tf
+++ b/blueprints/gke/multitenant-fleet/variables.tf
@@ -133,15 +133,15 @@ variable "folder_id" {
type = string
}
-variable "group_iam" {
- description = "Project-level IAM bindings for groups. Use group emails as keys, list of roles as values."
+variable "iam" {
+ description = "Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format."
type = map(list(string))
default = {}
nullable = false
}
-variable "iam" {
- description = "Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format."
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
type = map(list(string))
default = {}
nullable = false
diff --git a/fast/assets/schemas/project.schema.yaml b/fast/assets/schemas/project.schema.yaml
index cd75aa1ed..2b3f14b3a 100644
--- a/fast/assets/schemas/project.schema.yaml
+++ b/fast/assets/schemas/project.schema.yaml
@@ -1,4 +1,4 @@
-# Copyright 2023 Google LLC
+# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,8 +17,8 @@ billing_alert: any(include('billing_alert'), null(), required=False) # If set to
dns_zones: list(str(), required=False)
essential_contacts: list(str(), required=False) # Also used for billing alerts
folder_id: str(matches='(organizations/|folders/)[0-9]*$')
-group_iam: map(list(str()), key=str(), required=False)
iam: map(list(str()), key=str(), required=False)
+iam_by_principals: map(list(str()), key=str(), required=False)
kms_service_agents: map(list(str()), key=str(), required=False)
labels: map(str(), key=str(), required=False)
org_policies: include('org_policies', required=False)
diff --git a/fast/docs/0-domainless-iam.md b/fast/docs/0-domainless-iam.md
new file mode 100644
index 000000000..da0c82307
--- /dev/null
+++ b/fast/docs/0-domainless-iam.md
@@ -0,0 +1,145 @@
+# Support for domain-less organizations
+
+**authors:** [Ludo](https://github.com/ludoo) \
+**date:** Feb 11, 2024
+
+## Status
+
+Implemented in [#2064](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2064) pending review and discussion.
+
+## Context
+
+The current FAST design assumes that operational groups come from the same Cloud Identity instance connected to the GCP organization.
+
+While this approach has worked well in the past, there are already designs that cannot be easily mapped (for example groups coming from a separate CI), and the situation will only get worse once domain-less organizations start to be in wider use.
+
+Removing the assumption that FAST logical principals (e.g. `gcp-organization-admins`) always map directly to groups is not entirely trivial, since FAST uses data from the `groups` variable in different places:
+
+- to define authoritative IAM bindings via the module-level `group_iam` interface
+- to define additive IAM bindings via the module-level `iam_bindings_additive` interface
+- to set essential contacts at the folder and project level
+
+This proposal removes the dependency from groups by allowing to pass in to FAST any principal type, while still trying to preserve the current default behaviour and code readability in IAM bindings.
+
+## Proposal
+
+### FAST variable type change and optional interpolation
+
+The current `groups` variable was meant as a simple mapping between logical profile names used internally by FAST, and actual group names. The default case was furthermore made easier by interpolating the organization domain when no domain was specified, and adding the `group:` principal prefix for IAM bindings.
+
+The new proposed variable maintains the legacy behaviour, but slightly changes it so that no interpolation happens if the variable attributes have a principal prefix. The variable type is also updated to use `optional`, so that individual logical profile / principal mappings can be specified without having to override the whole block.
+
+```hcl
+variable "groups" {
+ type = object({
+ gcp-billing-admins = optional(string, "gcp-billing-admins")
+ gcp-devops = optional(string, "gcp-devops")
+ gcp-network-admins = optional(string, "gcp-network-admins")
+ gcp-organization-admins = optional(string, "gcp-organization-admins")
+ gcp-security-admins = optional(string, "gcp-security-admins")
+ gcp-support = optional(string, "gcp-support")
+ })
+ nullable = false
+ default = {}
+}
+```
+
+Passing in different principals is intuitive:
+
+```hcl
+groups = {
+ gcp-devops = "principalSet://iam.googleapis.com/locations/global/workforcePools/mypool/group/abc123"
+ gcp-organization-admins = "group:gcp-organization-admins@other.domain"
+}
+```
+
+Internally, interpolation is fairly straightforward:
+
+```hcl
+locals {
+ groups = {
+ for k, v in var.group_principals : k => (
+ can(regex("^[a-zA-Z]+:", v))
+ ? v
+ : "group:${v}@${var.organization.domain}"
+ )
+ }
+}
+```
+
+### FAST IAM additive bindings and module interface change
+
+FAST leverages the `group_iam` module-level interface to improve code readability for authoritative bindings, which is a primary goal of the framework. Introducing support for any principal type prevents us from using this interface, with a non-trivial impact on the overall readability of IAM roles in FAST.
+
+This is an example use in the IaC project:
+
+```hcl
+ # human (groups) IAM bindings
+ group_iam = {
+ (local.groups.gcp-devops) = [
+ "roles/iam.serviceAccountAdmin",
+ "roles/iam.serviceAccountTokenCreator",
+ ]
+ (local.groups.gcp-organization-admins) = [
+ "roles/iam.serviceAccountTokenCreator",
+ "roles/iam.workloadIdentityPoolAdmin"
+ ]
+ }
+```
+
+This proposal addresses the issue by changing the module-level interface to support different principal types. The original goal for `group_iam` -- to allow for better readability -- is preserved at the cost of the slight increase in verbosity due to having to specify the principal type.
+
+The trade-off in verbosity seems acceptable as it makes the new interface more flexible, and allows using the interface for `principal:` and `principalSet:` types, which are becoming more and more important to support.
+
+FAST code remains unchanged, as the `groups` local already contains a prefix for each principal, either interpolated or passed in by the user.
+
+The module-level variable definition changes only its name and description:
+
+```hcl
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+```
+
+Actual use is basically unchanged from the current `group_iam` interface:
+
+```hcl
+# current interface
+ group_iam = {
+ "app1-admins@example.org" = [
+ "roles/owner",
+ "roles/resourcemanager.folderAdmin",
+ "roles/resourcemanager.projectCreator"
+ ]
+ }
+# proposed interface
+ iam_by_principals = {
+ "group:app1-admins@example.org" = [
+ "roles/owner",
+ "roles/resourcemanager.folderAdmin",
+ "roles/resourcemanager.projectCreator"
+ ]
+ "principalSet://iam.googleapis.com/locations/global/workforcePools/mypool/group/abc123": = [
+ "roles/owner",
+ "roles/resourcemanager.folderAdmin",
+ "roles/resourcemanager.projectCreator"
+ ]
+ }
+```
+
+### FAST essential contacts
+
+Having `group_principals` support different type of principals will make it impossible to use the same variable to set essential contacts, as the principal might not be a group.
+
+This will require introduction of a new `essential_contacts` top-level variable keyed by folder/project (the individual contexts on which to set contacts), with the added benefit of being able to specify different and potentially multiple contacts compared to now.
+
+## Decision
+
+Pending
+
+## Consequences
+
+Pending
diff --git a/fast/stages-multitenant/0-bootstrap-tenant/README.md b/fast/stages-multitenant/0-bootstrap-tenant/README.md
index 448ad721d..9e3a74d84 100644
--- a/fast/stages-multitenant/0-bootstrap-tenant/README.md
+++ b/fast/stages-multitenant/0-bootstrap-tenant/README.md
@@ -198,25 +198,25 @@ This configuration is possible but unsupported and only exists for development p
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L20) | Automation resources created by the organization-level bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [billing_account](variables.tf#L39) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…}) | ✓ | | |
-| [organization](variables.tf#L214) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
-| [prefix](variables.tf#L230) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
-| [tag_keys](variables.tf#L253) | Organization tag keys. | object({…}) | ✓ | | 1-resman |
-| [tag_names](variables.tf#L264) | Customized names for resource management tags. | object({…}) | ✓ | | 1-resman |
-| [tag_values](variables.tf#L275) | Organization resource management tag values. | map(string) | ✓ | | 1-resman |
-| [tenant_config](variables.tf#L282) | Tenant configuration. Short name must be 4 characters or less. If `short_name_is_prefix` is true, short name must be 9 characters or less, and will be used as the prefix as is. | object({…}) | ✓ | | |
+| [organization](variables.tf#L215) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
+| [prefix](variables.tf#L231) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
+| [tag_keys](variables.tf#L254) | Organization tag keys. | object({…}) | ✓ | | 1-resman |
+| [tag_names](variables.tf#L265) | Customized names for resource management tags. | object({…}) | ✓ | | 1-resman |
+| [tag_values](variables.tf#L276) | Organization resource management tag values. | map(string) | ✓ | | 1-resman |
+| [tenant_config](variables.tf#L283) | Tenant configuration. Short name must be 4 characters or less. If `short_name_is_prefix` is true, short name must be 9 characters or less, and will be used as the prefix as is. | object({…}) | ✓ | | |
| [cicd_repositories](variables.tf#L49) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | |
| [custom_roles](variables.tf#L95) | Custom roles defined at the organization level, in key => id format. | object({…}) | | null | 0-bootstrap |
| [fast_features](variables.tf#L105) | Selective control for top-level FAST features. | object({…}) | | {} | 0-bootstrap |
| [federated_identity_providers](variables.tf#L119) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | |
-| [group_iam](variables.tf#L133) | Tenant-level custom group IAM settings in group => [roles] format. | map(list(string)) | | {} | |
-| [groups](variables.tf#L139) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | object({…}) | | {} | 0-bootstrap |
-| [iam](variables.tf#L152) | Tenant-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | |
-| [iam_bindings_additive](variables.tf#L158) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | |
-| [locations](variables.tf#L173) | Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable. | object({…}) | | {…} | 0-bootstrap |
-| [log_sinks](variables.tf#L193) | Tenant-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | |
-| [outputs_location](variables.tf#L224) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | |
-| [project_parent_ids](variables.tf#L240) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the tenant folder as parent. | object({…}) | | {…} | |
-| [test_principal](variables.tf#L323) | Used when testing to bypass the data source returning the current identity. | string | | null | |
+| [groups](variables.tf#L133) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap |
+| [iam](variables.tf#L146) | Tenant-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | |
+| [iam_bindings_additive](variables.tf#L152) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | |
+| [iam_by_principals](variables.tf#L167) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | |
+| [locations](variables.tf#L174) | Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable. | object({…}) | | {…} | 0-bootstrap |
+| [log_sinks](variables.tf#L194) | Tenant-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | |
+| [outputs_location](variables.tf#L225) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | |
+| [project_parent_ids](variables.tf#L241) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the tenant folder as parent. | object({…}) | | {…} | |
+| [test_principal](variables.tf#L324) | Used when testing to bypass the data source returning the current identity. | string | | null | |
## Outputs
diff --git a/fast/stages-multitenant/0-bootstrap-tenant/automation.tf b/fast/stages-multitenant/0-bootstrap-tenant/automation.tf
index 145210dd2..046fd61a9 100644
--- a/fast/stages-multitenant/0-bootstrap-tenant/automation.tf
+++ b/fast/stages-multitenant/0-bootstrap-tenant/automation.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,12 +26,12 @@ module "automation-project" {
)
prefix = local.prefix
# human (groups) IAM bindings
- group_iam = {
- (local.groups.gcp-admins) = [
+ iam_by_principals = {
+ (local.principals.gcp-admins) = [
"roles/iam.serviceAccountAdmin",
"roles/iam.serviceAccountTokenCreator",
]
- (local.groups.gcp-admins) = [
+ (local.principals.gcp-admins) = [
"roles/iam.serviceAccountTokenCreator",
"roles/iam.workloadIdentityPoolAdmin"
]
diff --git a/fast/stages-multitenant/0-bootstrap-tenant/billing.tf b/fast/stages-multitenant/0-bootstrap-tenant/billing.tf
index b62b79cbc..604217e9a 100644
--- a/fast/stages-multitenant/0-bootstrap-tenant/billing.tf
+++ b/fast/stages-multitenant/0-bootstrap-tenant/billing.tf
@@ -30,7 +30,7 @@ resource "google_billing_account_iam_member" "billing_ext_admin" {
for_each = toset(
local.billing_mode == "resource"
? [
- "group:${local.groups.gcp-admins}",
+ local.principals.gcp-admins,
module.automation-tf-resman-sa.iam_email
]
: []
@@ -44,7 +44,7 @@ resource "google_billing_account_iam_member" "billing_ext_cost_manager" {
for_each = toset(
local.billing_mode == "resource"
? [
- "group:${local.groups.gcp-admins}",
+ local.principals.gcp-admins,
module.automation-tf-resman-sa.iam_email
]
: []
diff --git a/fast/stages-multitenant/0-bootstrap-tenant/main.tf b/fast/stages-multitenant/0-bootstrap-tenant/main.tf
index 011999e35..09a7c1408 100644
--- a/fast/stages-multitenant/0-bootstrap-tenant/main.tf
+++ b/fast/stages-multitenant/0-bootstrap-tenant/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,10 +20,6 @@ locals {
? "MULTI_REGIONAL"
: "REGIONAL"
)
- groups = {
- for k, v in var.tenant_config.groups :
- k => v == null ? null : can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
fast_features = {
for k, v in var.tenant_config.fast_features :
k => v == null ? var.fast_features[k] : v
@@ -32,7 +28,18 @@ locals {
for k, v in var.tenant_config.locations :
k => v == null || v == [] ? var.locations[k] : v
}
- prefix = var.tenant_config.short_name_is_prefix ? var.tenant_config.short_name : join("-", compact([var.prefix, var.tenant_config.short_name]))
+ prefix = (
+ var.tenant_config.short_name_is_prefix
+ ? var.tenant_config.short_name
+ : join("-", compact([var.prefix, var.tenant_config.short_name]))
+ )
+ principals = {
+ for k, v in var.tenant_config.groups : k => (
+ can(regex("^[a-zA-Z]+:", v)) || v == null
+ ? v
+ : "group:${v}@${var.organization.domain}"
+ )
+ }
resman_sa = (
var.test_principal == null
? data.google_client_openid_userinfo.resman-sa.0.email
@@ -68,8 +75,8 @@ module "tenant-folder-iam" {
source = "../../../modules/folder"
id = module.tenant-folder.id
folder_create = false
- group_iam = merge(var.group_iam, {
- (local.groups.gcp-admins) = [
+ iam_by_principals = merge(var.iam_by_principals, {
+ (local.principals.gcp-admins) = [
"roles/logging.admin",
"roles/owner",
"roles/resourcemanager.folderAdmin",
diff --git a/fast/stages-multitenant/0-bootstrap-tenant/organization.tf b/fast/stages-multitenant/0-bootstrap-tenant/organization.tf
index 342ccc372..d38b516d0 100644
--- a/fast/stages-multitenant/0-bootstrap-tenant/organization.tf
+++ b/fast/stages-multitenant/0-bootstrap-tenant/organization.tf
@@ -29,11 +29,11 @@ module "organization" {
iam_bindings_additive = merge(
{
admins_org_viewer = {
- member = "group:${local.groups.gcp-admins}"
+ member = local.principals.gcp-admins
role = "roles/resourcemanager.organizationViewer"
}
admins_org_policy_admin = {
- member = "group:${local.groups.gcp-admins}"
+ member = local.principals.gcp-admins
role = "roles/orgpolicy.policyAdmin"
condition = {
title = "org_policy_tag_${var.tenant_config.short_name}_scoped_admins"
@@ -53,11 +53,11 @@ module "organization" {
},
local.billing_mode != "org" ? {} : {
admins_billing_admin = {
- member = "group:${local.groups.gcp-admins}"
+ member = local.principals.gcp-admins
role = "roles/billing.admin"
}
admins_billing_costs_manager = {
- member = "group:${local.groups.gcp-admins}"
+ member = local.principals.gcp-admins
role = "roles/billing.costsManager"
}
sa_resman_billing_admin = {
@@ -89,7 +89,7 @@ resource "google_tags_tag_value_iam_member" "admins_tag_viewer" {
for_each = var.tag_values
tag_value = each.value
role = "roles/resourcemanager.tagViewer"
- member = "group:${local.groups.gcp-admins}"
+ member = local.principals.gcp-admins
}
# tag-based condition for service accounts is in the automation-sa file
diff --git a/fast/stages-multitenant/0-bootstrap-tenant/variables.tf b/fast/stages-multitenant/0-bootstrap-tenant/variables.tf
index 7abd5db68..74daa0a9f 100644
--- a/fast/stages-multitenant/0-bootstrap-tenant/variables.tf
+++ b/fast/stages-multitenant/0-bootstrap-tenant/variables.tf
@@ -130,23 +130,17 @@ variable "federated_identity_providers" {
nullable = false
}
-variable "group_iam" {
- description = "Tenant-level custom group IAM settings in group => [roles] format."
- type = map(list(string))
- default = {}
-}
-
variable "groups" {
# tfdoc:variable:source 0-bootstrap
# https://cloud.google.com/docs/enterprise/setup-checklist
- description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
+ description = "Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated."
type = object({
- gcp-devops = optional(string)
- gcp-network-admins = optional(string)
- gcp-security-admins = optional(string)
+ gcp-devops = optional(string, "gcp-devops")
+ gcp-network-admins = optional(string, "gcp-network-admins")
+ gcp-security-admins = optional(string, "gcp-security-admins")
})
- default = {}
nullable = false
+ default = {}
}
variable "iam" {
@@ -170,6 +164,13 @@ variable "iam_bindings_additive" {
default = {}
}
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
variable "locations" {
# tfdoc:variable:source 0-bootstrap
description = "Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable."
diff --git a/fast/stages-multitenant/1-resman-tenant/README.md b/fast/stages-multitenant/1-resman-tenant/README.md
index bf5c74d7f..b1d235cab 100644
--- a/fast/stages-multitenant/1-resman-tenant/README.md
+++ b/fast/stages-multitenant/1-resman-tenant/README.md
@@ -165,10 +165,10 @@ Once the configuration is done just go through the usual `init/apply` cycle. On
| [data_dir](variables.tf#L154) | Relative path for the folder storing configuration data. | string | | "data" | |
| [factories_config](variables.tf#L160) | Configuration for the organization policies factory. | object({…}) | | {} | |
| [fast_features](variables.tf#L169) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap |
-| [groups](variables.tf#L183) | Group names to grant organization-level permissions. | object({…}) | | {} | 0-bootstrap |
+| [groups](variables.tf#L183) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap |
| [locations](variables.tf#L196) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap |
| [outputs_location](variables.tf#L224) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | |
-| [team_folders](variables.tf#L268) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | |
+| [team_folders](variables.tf#L268) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | |
| [test_skip_data_sources](variables.tf#L278) | Used when testing to bypass data sources. | bool | | false | |
## Outputs
diff --git a/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf b/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf
index 3916d6358..cb8a5b007 100644
--- a/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf
+++ b/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf
@@ -27,11 +27,10 @@ module "branch-dp-folder" {
}
module "branch-dp-dev-folder" {
- source = "../../../modules/folder"
- count = var.fast_features.data_platform ? 1 : 0
- parent = module.branch-dp-folder.0.id
- name = "Development"
- group_iam = {}
+ source = "../../../modules/folder"
+ count = var.fast_features.data_platform ? 1 : 0
+ parent = module.branch-dp-folder.0.id
+ name = "Development"
iam = {
(local.custom_roles.service_project_network_admin) = [
local.automation_sas_iam.dp-dev
@@ -48,11 +47,10 @@ module "branch-dp-dev-folder" {
}
module "branch-dp-prod-folder" {
- source = "../../../modules/folder"
- count = var.fast_features.data_platform ? 1 : 0
- parent = module.branch-dp-folder.0.id
- name = "Production"
- group_iam = {}
+ source = "../../../modules/folder"
+ count = var.fast_features.data_platform ? 1 : 0
+ parent = module.branch-dp-folder.0.id
+ name = "Production"
iam = {
(local.custom_roles.service_project_network_admin) = [
local.automation_sas_iam.dp-prod
diff --git a/fast/stages-multitenant/1-resman-tenant/branch-gke.tf b/fast/stages-multitenant/1-resman-tenant/branch-gke.tf
index 9ece810bb..81ca005f7 100644
--- a/fast/stages-multitenant/1-resman-tenant/branch-gke.tf
+++ b/fast/stages-multitenant/1-resman-tenant/branch-gke.tf
@@ -70,9 +70,9 @@ module "branch-gke-dev-sa" {
iam = {
"roles/iam.serviceAccountTokenCreator" = concat(
(
- local.groups.gcp-devops == null
+ local.principals.gcp-devops == null
? []
- : ["group:${local.groups.gcp-devops}"]
+ : [local.principals.gcp-devops]
),
compact([
try(module.branch-gke-dev-sa-cicd.0.iam_email, null)
@@ -94,9 +94,9 @@ module "branch-gke-prod-sa" {
iam = {
"roles/iam.serviceAccountTokenCreator" = concat(
(
- local.groups.gcp-devops == null
+ local.principals.gcp-devops == null
? []
- : ["group:${local.groups.gcp-devops}"]
+ : [local.principals.gcp-devops]
),
compact([
try(module.branch-gke-prod-sa-cicd.0.iam_email, null)
diff --git a/fast/stages-multitenant/1-resman-tenant/branch-networking.tf b/fast/stages-multitenant/1-resman-tenant/branch-networking.tf
index 85490baf0..0c2c87f45 100644
--- a/fast/stages-multitenant/1-resman-tenant/branch-networking.tf
+++ b/fast/stages-multitenant/1-resman-tenant/branch-networking.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +20,8 @@ module "branch-network-folder" {
source = "../../../modules/folder"
parent = module.root-folder.id
name = "Networking"
- group_iam = local.groups.gcp-network-admins == null ? {} : {
- (local.groups.gcp-network-admins) = [
+ iam_by_principals = local.principals.gcp-network-admins == null ? {} : {
+ (local.principals.gcp-network-admins) = [
# add any needed roles for resources/services not managed via Terraform,
# or replace editor with ~viewer if no broad resource management needed
# e.g.
diff --git a/fast/stages-multitenant/1-resman-tenant/branch-security.tf b/fast/stages-multitenant/1-resman-tenant/branch-security.tf
index d7253cce1..998addd9d 100644
--- a/fast/stages-multitenant/1-resman-tenant/branch-security.tf
+++ b/fast/stages-multitenant/1-resman-tenant/branch-security.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +20,8 @@ module "branch-security-folder" {
source = "../../../modules/folder"
parent = module.root-folder.id
name = "Security"
- group_iam = local.groups.gcp-security-admins == null ? {} : {
- (local.groups.gcp-security-admins) = [
+ iam_by_principals = local.principals.gcp-security-admins == null ? {} : {
+ (local.principals.gcp-security-admins) = [
# add any needed roles for resources/services not managed via Terraform,
# e.g.
# "roles/bigquery.admin",
diff --git a/fast/stages-multitenant/1-resman-tenant/branch-teams.tf b/fast/stages-multitenant/1-resman-tenant/branch-teams.tf
index 57f221104..82b1582e1 100644
--- a/fast/stages-multitenant/1-resman-tenant/branch-teams.tf
+++ b/fast/stages-multitenant/1-resman-tenant/branch-teams.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -77,7 +77,7 @@ module "branch-teams-team-folder" {
"roles/resourcemanager.projectCreator" = [module.branch-teams-team-sa[each.key].iam_email]
"roles/compute.xpnAdmin" = [module.branch-teams-team-sa[each.key].iam_email]
}
- group_iam = each.value.group_iam == null ? {} : each.value.group_iam
+ iam_by_principals = each.value.iam_by_principals
}
module "branch-teams-team-sa" {
@@ -88,11 +88,7 @@ module "branch-teams-team-sa" {
display_name = "Terraform team ${each.key} service account."
prefix = var.prefix
iam = {
- "roles/iam.serviceAccountTokenCreator" = (
- each.value.impersonation_groups == null
- ? []
- : [for g in each.value.impersonation_groups : "group:${g}"]
- )
+ "roles/iam.serviceAccountTokenCreator" = each.value.impersonation_principals
}
}
@@ -118,8 +114,6 @@ module "branch-teams-team-dev-folder" {
parent = module.branch-teams-team-folder[each.key].id
# naming: environment descriptive name
name = "Development"
- # environment-wide human permissions on the whole teams environment
- group_iam = {}
iam = {
(local.custom_roles.service_project_network_admin) = (
local.branch_optional_sa_lists.pf-dev
@@ -143,8 +137,6 @@ module "branch-teams-team-prod-folder" {
parent = module.branch-teams-team-folder[each.key].id
# naming: environment descriptive name
name = "Production"
- # environment-wide human permissions on the whole teams environment
- group_iam = {}
iam = {
(local.custom_roles.service_project_network_admin) = (
local.branch_optional_sa_lists.pf-prod
diff --git a/fast/stages-multitenant/1-resman-tenant/main.tf b/fast/stages-multitenant/1-resman-tenant/main.tf
index eb29fe423..3c59886bb 100644
--- a/fast/stages-multitenant/1-resman-tenant/main.tf
+++ b/fast/stages-multitenant/1-resman-tenant/main.tf
@@ -69,11 +69,11 @@ locals {
? "MULTI_REGIONAL"
: "REGIONAL"
)
- groups = {
- for k, v in var.groups :
- k => v == null ? null : can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
- groups_iam = {
- for k, v in local.groups : k => v != null ? "group:${v}" : null
+ principals = {
+ for k, v in var.groups : k => (
+ can(regex("^[a-zA-Z]+:", v)) || v == null
+ ? v
+ : "group:${v}@${var.organization.domain}"
+ )
}
}
diff --git a/fast/stages-multitenant/1-resman-tenant/variables.tf b/fast/stages-multitenant/1-resman-tenant/variables.tf
index ff6799240..dfad81c5c 100644
--- a/fast/stages-multitenant/1-resman-tenant/variables.tf
+++ b/fast/stages-multitenant/1-resman-tenant/variables.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -183,7 +183,7 @@ variable "fast_features" {
variable "groups" {
# tfdoc:variable:source 0-bootstrap
# https://cloud.google.com/docs/enterprise/setup-checklist
- description = "Group names to grant organization-level permissions."
+ description = "Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated."
type = object({
gcp-devops = optional(string)
gcp-network-admins = optional(string)
@@ -268,9 +268,9 @@ variable "tags" {
variable "team_folders" {
description = "Team folders to be created. Format is described in a code comment."
type = map(object({
- descriptive_name = string
- group_iam = map(list(string))
- impersonation_groups = list(string)
+ descriptive_name = string
+ iam_by_principals = optional(map(list(string)), {})
+ impersonation_principals = optional(list(string), [])
}))
default = null
}
diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md
index c6b160aa6..1b89309b8 100644
--- a/fast/stages/0-bootstrap/README.md
+++ b/fast/stages/0-bootstrap/README.md
@@ -604,23 +604,24 @@ The `fast_features` variable consists of 4 toggles:
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| [billing_account](variables.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…}) | ✓ | | |
-| [organization](variables.tf#L245) | Organization details. | object({…}) | ✓ | | |
-| [prefix](variables.tf#L260) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | |
+| [organization](variables.tf#L242) | Organization details. | object({…}) | ✓ | | |
+| [prefix](variables.tf#L257) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | |
| [bootstrap_user](variables.tf#L27) | Email of the nominal user running this stage for the first time. | string | | null | |
| [cicd_repositories](variables.tf#L33) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | |
| [custom_roles](variables.tf#L79) | Map of role names => list of permissions to additionally create at the organization level. | map(list(string)) | | {} | |
-| [factories_config](variables.tf#L86) | Configuration for the resource factories or external data. | object({…}) | | {} | |
-| [fast_features](variables.tf#L98) | Selective control for top-level FAST features. | object({…}) | | {} | |
-| [federated_identity_providers](variables.tf#L111) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | |
-| [group_iam](variables.tf#L131) | Organization-level authoritative IAM binding for groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | |
-| [groups](variables.tf#L138) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | object({…}) | | {…} | |
-| [iam](variables.tf#L163) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | |
-| [iam_bindings_additive](variables.tf#L170) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | |
-| [locations](variables.tf#L185) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | |
-| [log_sinks](variables.tf#L199) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | |
-| [org_policies_config](variables.tf#L228) | Organization policies customization. | object({…}) | | {} | |
-| [outputs_location](variables.tf#L254) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | |
-| [project_parent_ids](variables.tf#L269) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {…} | |
+| [essential_contacts](variables.tf#L86) | Email used for essential contacts, unset if null. | string | | null | |
+| [factories_config](variables.tf#L92) | Configuration for the resource factories or external data. | object({…}) | | {} | |
+| [fast_features](variables.tf#L104) | Selective control for top-level FAST features. | object({…}) | | {} | |
+| [federated_identity_providers](variables.tf#L117) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | |
+| [groups](variables.tf#L137) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | |
+| [iam](variables.tf#L153) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | |
+| [iam_bindings_additive](variables.tf#L160) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | |
+| [iam_by_principals](variables.tf#L175) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | |
+| [locations](variables.tf#L182) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | |
+| [log_sinks](variables.tf#L196) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | |
+| [org_policies_config](variables.tf#L225) | Organization policies customization. | object({…}) | | {} | |
+| [outputs_location](variables.tf#L251) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | |
+| [project_parent_ids](variables.tf#L266) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {…} | |
## Outputs
diff --git a/fast/stages/0-bootstrap/automation.tf b/fast/stages/0-bootstrap/automation.tf
index 8f2a50eaa..9a46d5eb9 100644
--- a/fast/stages/0-bootstrap/automation.tf
+++ b/fast/stages/0-bootstrap/automation.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,16 +29,18 @@ module "automation-project" {
var.project_parent_ids.automation, "organizations/${var.organization.id}"
)
prefix = local.prefix
- contacts = var.bootstrap_user != null ? {} : {
- (local.groups.gcp-organization-admins) = ["ALL"]
- }
+ contacts = (
+ var.bootstrap_user != null || var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
# human (groups) IAM bindings
- group_iam = {
- (local.groups.gcp-devops) = [
+ iam_by_principals = {
+ (local.principals.gcp-devops) = [
"roles/iam.serviceAccountAdmin",
"roles/iam.serviceAccountTokenCreator",
]
- (local.groups.gcp-organization-admins) = [
+ (local.principals.gcp-organization-admins) = [
"roles/iam.serviceAccountTokenCreator",
"roles/iam.workloadIdentityPoolAdmin"
]
diff --git a/fast/stages/0-bootstrap/billing.tf b/fast/stages/0-bootstrap/billing.tf
index e027dfae8..7e2ab2ce0 100644
--- a/fast/stages/0-bootstrap/billing.tf
+++ b/fast/stages/0-bootstrap/billing.tf
@@ -19,8 +19,8 @@
locals {
# used here for convenience, in organization.tf members are explicit
billing_ext_admins = [
- local.groups_iam.gcp-billing-admins,
- local.groups_iam.gcp-organization-admins,
+ local.principals.gcp-billing-admins,
+ local.principals.gcp-organization-admins,
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
]
@@ -46,9 +46,11 @@ module "billing-export-project" {
var.project_parent_ids.billing, "organizations/${var.organization.id}"
)
prefix = local.prefix
- contacts = {
- (local.groups.gcp-organization-admins) = ["ALL"]
- }
+ contacts = (
+ var.bootstrap_user != null || var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
iam = {
"roles/owner" = [module.automation-tf-bootstrap-sa.iam_email]
"roles/viewer" = [module.automation-tf-bootstrap-r-sa.iam_email]
diff --git a/fast/stages/0-bootstrap/checklist.tf b/fast/stages/0-bootstrap/checklist.tf
index cab821f16..80762ae28 100644
--- a/fast/stages/0-bootstrap/checklist.tf
+++ b/fast/stages/0-bootstrap/checklist.tf
@@ -17,13 +17,13 @@
locals {
# group mapping from checklist to ours
_cl_groups = {
- BILLING_ADMINS = local.groups.gcp-billing-admins
- DEVOPS = local.groups.gcp-devops
+ BILLING_ADMINS = local.principals.gcp-billing-admins
+ DEVOPS = local.principals.gcp-devops
# LOGGING_ADMINS
# MONITORING_ADMINS
- NETWORK_ADMINS = local.groups.gcp-network-admins
- ORG_ADMINS = local.groups.gcp-organization-admins
- SECURITY_ADMINS = local.groups.gcp-security-admins
+ NETWORK_ADMINS = local.principals.gcp-network-admins
+ ORG_ADMINS = local.principals.gcp-organization-admins
+ SECURITY_ADMINS = local.principals.gcp-security-admins
}
# parse raw data from JSON files if they exist
_cl_data_raw = (
@@ -64,7 +64,7 @@ locals {
# compile the final data structure we will consume from various places
checklist = {
billing_account = try(local._cl_data.billing_account, null)
- group_iam = {
+ iam_principals = {
for k, v in local._cl_org_iam_bindings :
k => v.authoritative if v.is_group && length(v.authoritative) > 0
}
@@ -76,8 +76,8 @@ locals {
for k, v in local._cl_org_iam_bindings : [
for r in v.additive : [
{
- key = v.is_group ? "${r}-group:${k}" : "${r}-${k}"
- member = v.is_group ? "group:${k}" : k
+ key = "${r}-${k}"
+ member = k
role = r
}
]
@@ -91,7 +91,6 @@ locals {
var.factories_config.checklist_org_iam != null
)
}
-
check "checklist" {
# checklist data files don't need to be both present so we check independently
# version mismatch might be ok, we just alert users
diff --git a/fast/stages/0-bootstrap/log-export.tf b/fast/stages/0-bootstrap/log-export.tf
index af9a5f9db..a9b733705 100644
--- a/fast/stages/0-bootstrap/log-export.tf
+++ b/fast/stages/0-bootstrap/log-export.tf
@@ -44,9 +44,11 @@ module "log-export-project" {
)
prefix = local.prefix
billing_account = var.billing_account.id
- contacts = {
- (local.groups.gcp-organization-admins) = ["ALL"]
- }
+ contacts = (
+ var.bootstrap_user != null || var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
iam = {
"roles/owner" = [module.automation-tf-bootstrap-sa.iam_email]
"roles/viewer" = [module.automation-tf-bootstrap-r-sa.iam_email]
diff --git a/fast/stages/0-bootstrap/main.tf b/fast/stages/0-bootstrap/main.tf
index a833779c7..b9153bdda 100644
--- a/fast/stages/0-bootstrap/main.tf
+++ b/fast/stages/0-bootstrap/main.tf
@@ -20,13 +20,12 @@ locals {
? "MULTI_REGIONAL"
: "REGIONAL"
)
- groups = {
- for k, v in var.groups :
- k => can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
- groups_iam = {
- for k, v in local.groups :
- k => "group:${v}"
+ principals = {
+ for k, v in var.groups : k => (
+ can(regex("^[a-zA-Z]+:", v))
+ ? v
+ : "group:${v}@${var.organization.domain}"
+ )
}
locations = {
bq = var.locations.bq
diff --git a/fast/stages/0-bootstrap/organization-iam.tf b/fast/stages/0-bootstrap/organization-iam.tf
index 422034b25..79d107eb0 100644
--- a/fast/stages/0-bootstrap/organization-iam.tf
+++ b/fast/stages/0-bootstrap/organization-iam.tf
@@ -29,8 +29,8 @@ locals {
}
}
# human (groups) IAM bindings
- iam_group_bindings = {
- (local.groups.gcp-billing-admins) = {
+ iam_principal_bindings = {
+ (local.principals.gcp-billing-admins) = {
authoritative = []
additive = (
local.billing_mode != "org" ? [] : [
@@ -38,7 +38,7 @@ locals {
]
)
}
- (local.groups.gcp-network-admins) = {
+ (local.principals.gcp-network-admins) = {
authoritative = [
"roles/cloudasset.owner",
"roles/cloudsupport.techSupportEditor",
@@ -48,7 +48,7 @@ locals {
"roles/compute.xpnAdmin"
]
}
- (local.groups.gcp-organization-admins) = {
+ (local.principals.gcp-organization-admins) = {
authoritative = [
"roles/cloudasset.owner",
"roles/cloudsupport.admin",
@@ -69,7 +69,7 @@ locals {
]
)
}
- (local.groups.gcp-security-admins) = {
+ (local.principals.gcp-security-admins) = {
authoritative = [
"roles/cloudasset.owner",
"roles/cloudsupport.techSupportEditor",
@@ -83,7 +83,7 @@ locals {
"roles/orgpolicy.policyAdmin"
]
}
- (local.groups.gcp-support) = {
+ (local.principals.gcp-support) = {
authoritative = [
"roles/cloudsupport.techSupportEditor",
"roles/logging.viewer",
diff --git a/fast/stages/0-bootstrap/organization.tf b/fast/stages/0-bootstrap/organization.tf
index 3cb7c7907..cb383e6cb 100644
--- a/fast/stages/0-bootstrap/organization.tf
+++ b/fast/stages/0-bootstrap/organization.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@ locals {
local.iam_sa_bindings,
local.iam_user_bootstrap_bindings,
{
- for k, v in local.iam_group_bindings : "group:${k}" => {
+ for k, v in local.iam_principal_bindings : k => {
authoritative = []
additive = v.additive
}
@@ -59,8 +59,8 @@ locals {
"tenant_network_admin",
]
# intermediate values before we merge in what comes from the checklist
- _group_iam = {
- for k, v in local.iam_group_bindings : k => v.authoritative
+ _iam_principals = {
+ for k, v in local.iam_principal_bindings : k => v.authoritative
}
_iam = merge(
{
@@ -77,10 +77,10 @@ locals {
}
}
# final values combining all sources
- group_iam = {
- for k, v in local._group_iam : k => distinct(concat(
+ iam_principals = {
+ for k, v in local._iam_principals : k => distinct(concat(
v,
- try(local.checklist.group_iam[k], [])
+ try(local.checklist.iam_principals[k], [])
))
}
iam = {
@@ -98,7 +98,7 @@ locals {
)
# compute authoritative and additive roles for use by add-ons (checklist, etc.)
iam_roles_authoritative = distinct(concat(
- flatten(values(local._group_iam)),
+ flatten(values(local._iam_principals)),
keys(local._iam)
))
iam_roles_additive = distinct([
@@ -132,9 +132,9 @@ module "organization" {
source = "../../../modules/organization"
organization_id = "organizations/${var.organization.id}"
# human (groups) IAM bindings
- group_iam = {
- for k, v in local.group_iam :
- k => distinct(concat(v, lookup(var.group_iam, k, [])))
+ iam_by_principals = {
+ for k, v in local.iam_principals :
+ k => distinct(concat(v, lookup(var.iam_by_principals, k, [])))
}
# machine (service accounts) IAM bindings
iam = merge(
diff --git a/fast/stages/0-bootstrap/outputs.tf b/fast/stages/0-bootstrap/outputs.tf
index 03eabead3..33be73738 100644
--- a/fast/stages/0-bootstrap/outputs.tf
+++ b/fast/stages/0-bootstrap/outputs.tf
@@ -114,7 +114,7 @@ locals {
tfvars_globals = {
billing_account = var.billing_account
fast_features = var.fast_features
- groups = var.groups
+ groups = local.principals
locations = local.locations
organization = var.organization
prefix = var.prefix
diff --git a/fast/stages/0-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf
index 7e94af5eb..b027367b9 100644
--- a/fast/stages/0-bootstrap/variables.tf
+++ b/fast/stages/0-bootstrap/variables.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -83,6 +83,12 @@ variable "custom_roles" {
default = {}
}
+variable "essential_contacts" {
+ description = "Email used for essential contacts, unset if null."
+ type = string
+ default = null
+}
+
variable "factories_config" {
description = "Configuration for the resource factories or external data."
type = object({
@@ -128,36 +134,20 @@ variable "federated_identity_providers" {
# }
}
-variable "group_iam" {
- description = "Organization-level authoritative IAM binding for groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
variable "groups" {
# https://cloud.google.com/docs/enterprise/setup-checklist
- description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
+ description = "Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated."
type = object({
- gcp-billing-admins = string
- gcp-devops = string
- gcp-network-admins = string
- gcp-organization-admins = string
- gcp-security-admins = string
- gcp-support = string
+ gcp-billing-admins = optional(string, "gcp-billing-admins")
+ gcp-devops = optional(string, "gcp-devops")
+ gcp-network-admins = optional(string, "gcp-network-admins")
+ gcp-organization-admins = optional(string, "gcp-organization-admins")
+ gcp-security-admins = optional(string, "gcp-security-admins")
+ # aliased to gcp-devops as the checklist does not create it
+ gcp-support = optional(string, "gcp-devops")
})
- default = {
- gcp-billing-admins = "gcp-billing-admins"
- gcp-devops = "gcp-devops"
- gcp-network-admins = "gcp-network-admins"
- gcp-organization-admins = "gcp-organization-admins"
- gcp-security-admins = "gcp-security-admins"
- # gcp-support is not included in the official GCP Enterprise
- # Checklist, so by default we map gcp-support to gcp-devops.
- # However, we recommend creating gcp-support and updating the
- # value in the following line
- gcp-support = "gcp-devops"
- }
+ nullable = false
+ default = {}
}
variable "iam" {
@@ -182,6 +172,13 @@ variable "iam_bindings_additive" {
default = {}
}
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
variable "locations" {
description = "Optional locations for GCS, BigQuery, and logging buckets created here."
type = object({
diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md
index 8a0d4ee52..a28451a14 100644
--- a/fast/stages/1-resman/README.md
+++ b/fast/stages/1-resman/README.md
@@ -289,12 +289,12 @@ Consider the following example in a `tfvars` file:
team_folders = {
team-a = {
descriptive_name = "Team A"
- group_iam = {
- "team-a@gcp-pso-italy.net" = [
+ iam_by_principals = {
+ "group:team-a@gcp-pso-italy.net" = [
"roles/viewer"
]
}
- impersonation_groups = ["team-a-admins@gcp-pso-italy.net"]
+ impersonation_principals = ["group:team-a-admins@gcp-pso-italy.net"]
}
}
```
@@ -362,14 +362,14 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| [custom_roles](variables.tf#L135) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap |
| [factories_config](variables.tf#L145) | Configuration for the resource factories or external data. | object({…}) | | {} | |
| [fast_features](variables.tf#L154) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap |
-| [groups](variables.tf#L168) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | object({…}) | | {} | 0-bootstrap |
+| [groups](variables.tf#L168) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap |
| [locations](variables.tf#L183) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap |
| [org_policy_tags](variables.tf#L201) | Resource management tags for organization policy exceptions. | object({…}) | | {} | 0-bootstrap |
| [outputs_location](variables.tf#L223) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | |
| [tag_names](variables.tf#L240) | Customized names for resource management tags. | object({…}) | | {} | |
| [tags](variables.tf#L255) | Custome secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | |
-| [team_folders](variables.tf#L276) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | |
-| [tenants](variables.tf#L292) | Lightweight tenant definitions. | map(object({…})) | | {} | |
+| [team_folders](variables.tf#L276) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | |
+| [tenants](variables.tf#L292) | Lightweight tenant definitions. | map(object({…})) | | {} | |
| [tenants_config](variables.tf#L308) | Lightweight tenants shared configuration. Roles will be assigned to tenant admin group and service accounts. | object({…}) | | {} | |
## Outputs
diff --git a/fast/stages/1-resman/branch-data-platform.tf b/fast/stages/1-resman/branch-data-platform.tf
index 050350ff3..f4bc2593b 100644
--- a/fast/stages/1-resman/branch-data-platform.tf
+++ b/fast/stages/1-resman/branch-data-platform.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,11 +29,11 @@ module "branch-dp-folder" {
}
module "branch-dp-dev-folder" {
- source = "../../../modules/folder"
- count = var.fast_features.data_platform ? 1 : 0
- parent = module.branch-dp-folder.0.id
- name = "Development"
- group_iam = {}
+ source = "../../../modules/folder"
+ count = var.fast_features.data_platform ? 1 : 0
+ parent = module.branch-dp-folder.0.id
+ name = "Development"
+ iam_by_principals = {}
# owner and viewer roles are broad and might grant unwanted access
# replace them with more selective custom roles for production deployments
iam = {
@@ -58,11 +58,11 @@ module "branch-dp-dev-folder" {
}
module "branch-dp-prod-folder" {
- source = "../../../modules/folder"
- count = var.fast_features.data_platform ? 1 : 0
- parent = module.branch-dp-folder.0.id
- name = "Production"
- group_iam = {}
+ source = "../../../modules/folder"
+ count = var.fast_features.data_platform ? 1 : 0
+ parent = module.branch-dp-folder.0.id
+ name = "Production"
+ iam_by_principals = {}
# owner and viewer roles are broad and might grant unwanted access
# replace them with more selective custom roles for production deployments
iam = {
diff --git a/fast/stages/1-resman/branch-gke.tf b/fast/stages/1-resman/branch-gke.tf
index 3e87ab268..177e012fb 100644
--- a/fast/stages/1-resman/branch-gke.tf
+++ b/fast/stages/1-resman/branch-gke.tf
@@ -87,11 +87,7 @@ module "branch-gke-dev-sa" {
prefix = var.prefix
iam = {
"roles/iam.serviceAccountTokenCreator" = concat(
- (
- local.groups.gcp-devops == null
- ? []
- : ["group:${local.groups.gcp-devops}"]
- ),
+ [local.principals.gcp-devops],
compact([
try(module.branch-gke-dev-sa-cicd.0.iam_email, null)
])
@@ -114,11 +110,7 @@ module "branch-gke-prod-sa" {
prefix = var.prefix
iam = {
"roles/iam.serviceAccountTokenCreator" = concat(
- (
- local.groups.gcp-devops == null
- ? []
- : ["group:${local.groups.gcp-devops}"]
- ),
+ [local.principals.gcp-devops],
compact([
try(module.branch-gke-prod-sa-cicd.0.iam_email, null)
])
diff --git a/fast/stages/1-resman/branch-networking.tf b/fast/stages/1-resman/branch-networking.tf
index 71438af62..48bb88dcf 100644
--- a/fast/stages/1-resman/branch-networking.tf
+++ b/fast/stages/1-resman/branch-networking.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +20,8 @@ module "branch-network-folder" {
source = "../../../modules/folder"
parent = "organizations/${var.organization.id}"
name = "Networking"
- group_iam = local.groups.gcp-network-admins == null ? {} : {
- (local.groups.gcp-network-admins) = [
+ iam_by_principals = {
+ (local.principals.gcp-network-admins) = [
# owner and viewer roles are broad and might grant unwanted access
# replace them with more selective custom roles for production deployments
"roles/editor",
diff --git a/fast/stages/1-resman/branch-security.tf b/fast/stages/1-resman/branch-security.tf
index 866dad73d..b688d935a 100644
--- a/fast/stages/1-resman/branch-security.tf
+++ b/fast/stages/1-resman/branch-security.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +20,8 @@ module "branch-security-folder" {
source = "../../../modules/folder"
parent = "organizations/${var.organization.id}"
name = "Security"
- group_iam = local.groups.gcp-security-admins == null ? {} : {
- (local.groups.gcp-security-admins) = [
+ iam_by_principals = {
+ (local.principals.gcp-security-admins) = [
# owner and viewer roles are broad and might grant unwanted access
# replace them with more selective custom roles for production deployments
"roles/editor"
diff --git a/fast/stages/1-resman/branch-teams.tf b/fast/stages/1-resman/branch-teams.tf
index e1e50f40a..c11dafe31 100644
--- a/fast/stages/1-resman/branch-teams.tf
+++ b/fast/stages/1-resman/branch-teams.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -79,7 +79,7 @@ module "branch-teams-team-folder" {
"roles/resourcemanager.projectCreator" = [module.branch-teams-team-sa[each.key].iam_email]
"roles/compute.xpnAdmin" = [module.branch-teams-team-sa[each.key].iam_email]
}
- group_iam = each.value.group_iam == null ? {} : each.value.group_iam
+ iam_by_principals = each.value.iam_by_principals == null ? {} : each.value.iam_by_principals
}
# TODO: move into team's own IaC project
@@ -95,9 +95,9 @@ module "branch-teams-team-sa" {
"roles/iam.serviceAccountTokenCreator" = concat(
compact([try(module.branch-teams-team-sa-cicd[each.key].iam_email, null)]),
(
- each.value.impersonation_groups == null
+ each.value.impersonation_principals == null
? []
- : [for g in each.value.impersonation_groups : "group:${g}"]
+ : [for g in each.value.impersonation_principals : g]
)
)
}
@@ -126,7 +126,7 @@ module "branch-teams-team-dev-folder" {
# naming: environment descriptive name
name = "Development"
# environment-wide human permissions on the whole teams environment
- group_iam = {}
+ iam_by_principals = {}
iam = {
(local.custom_roles.service_project_network_admin) = (
local.branch_optional_sa_lists.pf-dev
@@ -153,7 +153,7 @@ module "branch-teams-team-prod-folder" {
# naming: environment descriptive name
name = "Production"
# environment-wide human permissions on the whole teams environment
- group_iam = {}
+ iam_by_principals = {}
iam = {
(local.custom_roles.service_project_network_admin) = (
local.branch_optional_sa_lists.pf-prod
diff --git a/fast/stages/1-resman/branch-tenants.tf b/fast/stages/1-resman/branch-tenants.tf
index ca39e58d8..aab02c0d3 100644
--- a/fast/stages/1-resman/branch-tenants.tf
+++ b/fast/stages/1-resman/branch-tenants.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,8 +45,8 @@ module "tenant-top-folder" {
for_each = var.tenants
parent = module.tenant-tenants-folder.id
name = each.value.descriptive_name
- group_iam = {
- (each.value.admin_group_email) = ["roles/browser"]
+ iam_by_principals = {
+ (each.value.admin_principal) = ["roles/browser"]
}
}
@@ -169,8 +169,8 @@ module "tenant-self-iac-project" {
name = "${each.key}-iac-core-0"
parent = module.tenant-self-folder[each.key].id
prefix = var.prefix
- group_iam = {
- (each.value.admin_group_email) = [
+ iam_by_principals = {
+ (each.value.admin_principal) = [
"roles/iam.serviceAccountAdmin",
"roles/iam.serviceAccountTokenCreator",
"roles/iam.workloadIdentityPoolAdmin"
diff --git a/fast/stages/1-resman/main.tf b/fast/stages/1-resman/main.tf
index 539ab5ce8..9d85a33f2 100644
--- a/fast/stages/1-resman/main.tf
+++ b/fast/stages/1-resman/main.tf
@@ -92,16 +92,16 @@ locals {
? "MULTI_REGIONAL"
: "REGIONAL"
)
- groups = {
- for k, v in var.groups :
- k => can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
- groups_iam = {
- for k, v in local.groups : k => v != null ? "group:${v}" : null
- }
identity_providers = coalesce(
try(var.automation.federated_identity_providers, null), {}
)
+ principals = {
+ for k, v in var.groups : k => (
+ can(regex("^[a-zA-Z]+:", v))
+ ? v
+ : "group:${v}@${var.organization.domain}"
+ )
+ }
}
data "google_client_openid_userinfo" "provider_identity" {
diff --git a/fast/stages/1-resman/variables.tf b/fast/stages/1-resman/variables.tf
index dd8fe9e52..3987e81f3 100644
--- a/fast/stages/1-resman/variables.tf
+++ b/fast/stages/1-resman/variables.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -168,16 +168,16 @@ variable "fast_features" {
variable "groups" {
# tfdoc:variable:source 0-bootstrap
# https://cloud.google.com/docs/enterprise/setup-checklist
- description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
+ description = "Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated."
type = object({
- gcp-billing-admins = optional(string)
- gcp-devops = optional(string)
- gcp-network-admins = optional(string)
- gcp-organization-admins = optional(string)
- gcp-security-admins = optional(string)
+ gcp-billing-admins = optional(string, "gcp-billing-admins")
+ gcp-devops = optional(string, "gcp-devops")
+ gcp-network-admins = optional(string, "gcp-network-admins")
+ gcp-organization-admins = optional(string, "gcp-organization-admins")
+ gcp-security-admins = optional(string, "gcp-security-admins")
})
- default = {}
nullable = false
+ default = {}
}
variable "locations" {
@@ -276,9 +276,9 @@ variable "tags" {
variable "team_folders" {
description = "Team folders to be created. Format is described in a code comment."
type = map(object({
- descriptive_name = string
- group_iam = map(list(string))
- impersonation_groups = list(string)
+ descriptive_name = string
+ iam_by_principals = map(list(string))
+ impersonation_principals = list(string)
cicd = optional(object({
branch = string
identity_provider = string
@@ -292,9 +292,9 @@ variable "team_folders" {
variable "tenants" {
description = "Lightweight tenant definitions."
type = map(object({
- admin_group_email = string
- descriptive_name = string
- billing_account = optional(string)
+ admin_principal = string
+ descriptive_name = string
+ billing_account = optional(string)
organization = optional(object({
customer_id = string
domain = string
diff --git a/fast/stages/2-networking-a-peering/README.md b/fast/stages/2-networking-a-peering/README.md
index d5e234f5d..27272d0ec 100644
--- a/fast/stages/2-networking-a-peering/README.md
+++ b/fast/stages/2-networking-a-peering/README.md
@@ -389,21 +389,21 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L42) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap |
-| [folder_ids](variables.tf#L110) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
-| [organization](variables.tf#L130) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
-| [prefix](variables.tf#L146) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
+| [folder_ids](variables.tf#L116) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
+| [organization](variables.tf#L126) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
+| [prefix](variables.tf#L142) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | object({…}) | | {…} | |
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap |
| [dns](variables.tf#L72) | DNS configuration. | object({…}) | | {} | |
| [enable_cloud_nat](variables.tf#L82) | Deploy Cloud NAT. | bool | | false | |
-| [factories_config](variables.tf#L89) | Configuration for network resource factories. | object({…}) | | {…} | |
-| [groups](variables.tf#L120) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | object({…}) | | {} | 0-bootstrap |
-| [outputs_location](variables.tf#L140) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
+| [essential_contacts](variables.tf#L89) | Email used for essential contacts, unset if null. | string | | null | |
+| [factories_config](variables.tf#L95) | Configuration for network resource factories. | object({…}) | | {…} | |
+| [outputs_location](variables.tf#L136) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
| [peering_configs](variables-peerings.tf#L19) | Peering configurations. | object({…}) | | {} | |
-| [psa_ranges](variables.tf#L157) | IP ranges used for Private Service Access (CloudSQL, etc.). | object({…}) | | null | |
-| [regions](variables.tf#L176) | Region definitions. | object({…}) | | {…} | |
-| [service_accounts](variables.tf#L188) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
-| [vpn_onprem_primary_config](variables.tf#L202) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | |
+| [psa_ranges](variables.tf#L153) | IP ranges used for Private Service Access (CloudSQL, etc.). | object({…}) | | null | |
+| [regions](variables.tf#L172) | Region definitions. | object({…}) | | {…} | |
+| [service_accounts](variables.tf#L184) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
+| [vpn_onprem_primary_config](variables.tf#L198) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | |
## Outputs
diff --git a/fast/stages/2-networking-a-peering/main.tf b/fast/stages/2-networking-a-peering/main.tf
index 44cf7bd34..9637c492b 100644
--- a/fast/stages/2-networking-a-peering/main.tf
+++ b/fast/stages/2-networking-a-peering/main.tf
@@ -17,10 +17,6 @@
# tfdoc:file:description Networking folder and hierarchical policy.
locals {
- groups = {
- for k, v in var.groups :
- k => can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
# combine all regions from variables and subnets
regions = distinct(concat(
values(var.regions),
@@ -49,9 +45,11 @@ module "folder" {
name = "Networking"
folder_create = var.folder_ids.networking == null
id = var.folder_ids.networking
- contacts = {
- (local.groups.gcp-network-admins) = ["ALL"]
- }
+ contacts = (
+ var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
firewall_policy = {
name = "default"
policy = module.firewall-policy-default.id
diff --git a/fast/stages/2-networking-a-peering/variables.tf b/fast/stages/2-networking-a-peering/variables.tf
index b1f51b4c7..fc8255f33 100644
--- a/fast/stages/2-networking-a-peering/variables.tf
+++ b/fast/stages/2-networking-a-peering/variables.tf
@@ -86,6 +86,12 @@ variable "enable_cloud_nat" {
nullable = false
}
+variable "essential_contacts" {
+ description = "Email used for essential contacts, unset if null."
+ type = string
+ default = null
+}
+
variable "factories_config" {
description = "Configuration for network resource factories."
type = object({
@@ -117,16 +123,6 @@ variable "folder_ids" {
})
}
-variable "groups" {
- # tfdoc:variable:source 0-bootstrap
- description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
- type = object({
- gcp-network-admins = optional(string)
- })
- default = {}
- nullable = false
-}
-
variable "organization" {
# tfdoc:variable:source 0-bootstrap
description = "Organization details."
diff --git a/fast/stages/2-networking-b-vpn/README.md b/fast/stages/2-networking-b-vpn/README.md
index 6cd4a9ba9..708c9e942 100644
--- a/fast/stages/2-networking-b-vpn/README.md
+++ b/fast/stages/2-networking-b-vpn/README.md
@@ -413,21 +413,21 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L42) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap |
-| [folder_ids](variables.tf#L110) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
-| [organization](variables.tf#L130) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
-| [prefix](variables.tf#L146) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
+| [folder_ids](variables.tf#L116) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
+| [organization](variables.tf#L126) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
+| [prefix](variables.tf#L142) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | object({…}) | | {…} | |
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap |
| [dns](variables.tf#L72) | DNS configuration. | object({…}) | | {} | |
| [enable_cloud_nat](variables.tf#L82) | Deploy Cloud NAT. | bool | | false | |
-| [factories_config](variables.tf#L89) | Configuration for network resource factories. | object({…}) | | {…} | |
-| [groups](variables.tf#L120) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | object({…}) | | {} | 0-bootstrap |
-| [outputs_location](variables.tf#L140) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
-| [psa_ranges](variables.tf#L157) | IP ranges used for Private Service Access (CloudSQL, etc.). | object({…}) | | null | |
-| [regions](variables.tf#L176) | Region definitions. | object({…}) | | {…} | |
-| [service_accounts](variables.tf#L188) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
+| [essential_contacts](variables.tf#L89) | Email used for essential contacts, unset if null. | string | | null | |
+| [factories_config](variables.tf#L95) | Configuration for network resource factories. | object({…}) | | {…} | |
+| [outputs_location](variables.tf#L136) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
+| [psa_ranges](variables.tf#L153) | IP ranges used for Private Service Access (CloudSQL, etc.). | object({…}) | | null | |
+| [regions](variables.tf#L172) | Region definitions. | object({…}) | | {…} | |
+| [service_accounts](variables.tf#L184) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
| [vpn_configs](variables-vpn.tf#L17) | Hub to spokes VPN configurations. | object({…}) | | {} | |
-| [vpn_onprem_primary_config](variables.tf#L202) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | |
+| [vpn_onprem_primary_config](variables.tf#L198) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | |
## Outputs
diff --git a/fast/stages/2-networking-b-vpn/main.tf b/fast/stages/2-networking-b-vpn/main.tf
index 44cf7bd34..9637c492b 100644
--- a/fast/stages/2-networking-b-vpn/main.tf
+++ b/fast/stages/2-networking-b-vpn/main.tf
@@ -17,10 +17,6 @@
# tfdoc:file:description Networking folder and hierarchical policy.
locals {
- groups = {
- for k, v in var.groups :
- k => can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
# combine all regions from variables and subnets
regions = distinct(concat(
values(var.regions),
@@ -49,9 +45,11 @@ module "folder" {
name = "Networking"
folder_create = var.folder_ids.networking == null
id = var.folder_ids.networking
- contacts = {
- (local.groups.gcp-network-admins) = ["ALL"]
- }
+ contacts = (
+ var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
firewall_policy = {
name = "default"
policy = module.firewall-policy-default.id
diff --git a/fast/stages/2-networking-b-vpn/variables.tf b/fast/stages/2-networking-b-vpn/variables.tf
index b1f51b4c7..fc8255f33 100644
--- a/fast/stages/2-networking-b-vpn/variables.tf
+++ b/fast/stages/2-networking-b-vpn/variables.tf
@@ -86,6 +86,12 @@ variable "enable_cloud_nat" {
nullable = false
}
+variable "essential_contacts" {
+ description = "Email used for essential contacts, unset if null."
+ type = string
+ default = null
+}
+
variable "factories_config" {
description = "Configuration for network resource factories."
type = object({
@@ -117,16 +123,6 @@ variable "folder_ids" {
})
}
-variable "groups" {
- # tfdoc:variable:source 0-bootstrap
- description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
- type = object({
- gcp-network-admins = optional(string)
- })
- default = {}
- nullable = false
-}
-
variable "organization" {
# tfdoc:variable:source 0-bootstrap
description = "Organization details."
diff --git a/fast/stages/2-networking-c-nva/README.md b/fast/stages/2-networking-c-nva/README.md
index a4aae565a..b708137b4 100644
--- a/fast/stages/2-networking-c-nva/README.md
+++ b/fast/stages/2-networking-c-nva/README.md
@@ -458,23 +458,23 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L42) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap |
-| [folder_ids](variables.tf#L110) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
-| [organization](variables.tf#L153) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
-| [prefix](variables.tf#L169) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
+| [folder_ids](variables.tf#L116) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
+| [organization](variables.tf#L149) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
+| [prefix](variables.tf#L165) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | object({…}) | | {…} | |
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap |
| [dns](variables.tf#L72) | DNS configuration. | object({…}) | | {} | |
| [enable_cloud_nat](variables.tf#L82) | Deploy Cloud NAT. | bool | | false | |
-| [factories_config](variables.tf#L89) | Configuration for network resource factories. | object({…}) | | {…} | |
-| [gcp_ranges](variables.tf#L120) | GCP address ranges in name => range format. | map(string) | | {…} | |
-| [groups](variables.tf#L135) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | object({…}) | | {} | 0-bootstrap |
-| [onprem_cidr](variables.tf#L145) | Onprem addresses in name => range format. | map(string) | | {…} | |
-| [outputs_location](variables.tf#L163) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
-| [psa_ranges](variables.tf#L180) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | |
-| [regions](variables.tf#L199) | Region definitions. | object({…}) | | {…} | |
-| [service_accounts](variables.tf#L211) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
-| [vpn_onprem_primary_config](variables.tf#L225) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | |
-| [vpn_onprem_secondary_config](variables.tf#L268) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | |
+| [essential_contacts](variables.tf#L89) | Email used for essential contacts, unset if null. | string | | null | |
+| [factories_config](variables.tf#L95) | Configuration for network resource factories. | object({…}) | | {…} | |
+| [gcp_ranges](variables.tf#L126) | GCP address ranges in name => range format. | map(string) | | {…} | |
+| [onprem_cidr](variables.tf#L141) | Onprem addresses in name => range format. | map(string) | | {…} | |
+| [outputs_location](variables.tf#L159) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
+| [psa_ranges](variables.tf#L176) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | |
+| [regions](variables.tf#L195) | Region definitions. | object({…}) | | {…} | |
+| [service_accounts](variables.tf#L207) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
+| [vpn_onprem_primary_config](variables.tf#L221) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | |
+| [vpn_onprem_secondary_config](variables.tf#L264) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | |
## Outputs
diff --git a/fast/stages/2-networking-c-nva/main.tf b/fast/stages/2-networking-c-nva/main.tf
index 91028b319..2f5fead60 100644
--- a/fast/stages/2-networking-c-nva/main.tf
+++ b/fast/stages/2-networking-c-nva/main.tf
@@ -18,10 +18,6 @@
locals {
custom_roles = coalesce(var.custom_roles, {})
- groups = {
- for k, v in var.groups :
- k => can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
# combine all regions from variables and subnets
regions = distinct(concat(
values(var.regions),
@@ -50,9 +46,11 @@ module "folder" {
name = "Networking"
folder_create = var.folder_ids.networking == null
id = var.folder_ids.networking
- contacts = {
- (local.groups.gcp-network-admins) = ["ALL"]
- }
+ contacts = (
+ var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
firewall_policy = {
name = "default"
policy = module.firewall-policy-default.id
diff --git a/fast/stages/2-networking-c-nva/variables.tf b/fast/stages/2-networking-c-nva/variables.tf
index 9d100143a..452b672aa 100644
--- a/fast/stages/2-networking-c-nva/variables.tf
+++ b/fast/stages/2-networking-c-nva/variables.tf
@@ -86,6 +86,12 @@ variable "enable_cloud_nat" {
nullable = false
}
+variable "essential_contacts" {
+ description = "Email used for essential contacts, unset if null."
+ type = string
+ default = null
+}
+
variable "factories_config" {
description = "Configuration for network resource factories."
type = object({
@@ -132,16 +138,6 @@ variable "gcp_ranges" {
}
}
-variable "groups" {
- # tfdoc:variable:source 0-bootstrap
- description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
- type = object({
- gcp-network-admins = optional(string)
- })
- default = {}
- nullable = false
-}
-
variable "onprem_cidr" {
description = "Onprem addresses in name => range format."
type = map(string)
diff --git a/fast/stages/2-networking-d-separate-envs/README.md b/fast/stages/2-networking-d-separate-envs/README.md
index 75fc109f3..8a8e838c1 100644
--- a/fast/stages/2-networking-d-separate-envs/README.md
+++ b/fast/stages/2-networking-d-separate-envs/README.md
@@ -332,21 +332,21 @@ Regions are defined via the `regions` variable which sets up a mapping between t
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L42) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap |
-| [folder_ids](variables.tf#L111) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
-| [organization](variables.tf#L131) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
-| [prefix](variables.tf#L147) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
+| [folder_ids](variables.tf#L117) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
+| [organization](variables.tf#L127) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
+| [prefix](variables.tf#L143) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | object({…}) | | {…} | |
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap |
| [dns](variables.tf#L72) | DNS configuration. | object({…}) | | {} | |
| [enable_cloud_nat](variables.tf#L83) | Deploy Cloud NAT. | bool | | false | |
-| [factories_config](variables.tf#L90) | Configuration for network resource factories. | object({…}) | | {…} | |
-| [groups](variables.tf#L121) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | object({…}) | | {} | 0-bootstrap |
-| [outputs_location](variables.tf#L141) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
-| [psa_ranges](variables.tf#L158) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | |
-| [regions](variables.tf#L177) | Region definitions. | object({…}) | | {…} | |
-| [service_accounts](variables.tf#L187) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
-| [vpn_onprem_dev_primary_config](variables.tf#L201) | VPN gateway configuration for onprem interconnection from dev in the primary region. | object({…}) | | null | |
-| [vpn_onprem_prod_primary_config](variables.tf#L244) | VPN gateway configuration for onprem interconnection from prod in the primary region. | object({…}) | | null | |
+| [essential_contacts](variables.tf#L90) | Email used for essential contacts, unset if null. | string | | null | |
+| [factories_config](variables.tf#L96) | Configuration for network resource factories. | object({…}) | | {…} | |
+| [outputs_location](variables.tf#L137) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
+| [psa_ranges](variables.tf#L154) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | |
+| [regions](variables.tf#L173) | Region definitions. | object({…}) | | {…} | |
+| [service_accounts](variables.tf#L183) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
+| [vpn_onprem_dev_primary_config](variables.tf#L197) | VPN gateway configuration for onprem interconnection from dev in the primary region. | object({…}) | | null | |
+| [vpn_onprem_prod_primary_config](variables.tf#L240) | VPN gateway configuration for onprem interconnection from prod in the primary region. | object({…}) | | null | |
## Outputs
diff --git a/fast/stages/2-networking-d-separate-envs/main.tf b/fast/stages/2-networking-d-separate-envs/main.tf
index 56fba2043..e9f632c98 100644
--- a/fast/stages/2-networking-d-separate-envs/main.tf
+++ b/fast/stages/2-networking-d-separate-envs/main.tf
@@ -18,10 +18,6 @@
locals {
custom_roles = coalesce(var.custom_roles, {})
- groups = {
- for k, v in var.groups :
- k => can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
# combine all regions from variables and subnets
regions = distinct(concat(
values(var.regions),
@@ -45,9 +41,11 @@ module "folder" {
name = "Networking"
folder_create = var.folder_ids.networking == null
id = var.folder_ids.networking
- contacts = {
- (local.groups.gcp-network-admins) = ["ALL"]
- }
+ contacts = (
+ var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
firewall_policy = {
name = "default"
policy = module.firewall-policy-default.id
diff --git a/fast/stages/2-networking-d-separate-envs/variables.tf b/fast/stages/2-networking-d-separate-envs/variables.tf
index a105750af..f1cb47e8d 100644
--- a/fast/stages/2-networking-d-separate-envs/variables.tf
+++ b/fast/stages/2-networking-d-separate-envs/variables.tf
@@ -87,6 +87,12 @@ variable "enable_cloud_nat" {
nullable = false
}
+variable "essential_contacts" {
+ description = "Email used for essential contacts, unset if null."
+ type = string
+ default = null
+}
+
variable "factories_config" {
description = "Configuration for network resource factories."
type = object({
@@ -118,16 +124,6 @@ variable "folder_ids" {
})
}
-variable "groups" {
- # tfdoc:variable:source 0-bootstrap
- description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
- type = object({
- gcp-network-admins = optional(string)
- })
- default = {}
- nullable = false
-}
-
variable "organization" {
# tfdoc:variable:source 0-bootstrap
description = "Organization details."
diff --git a/fast/stages/2-networking-e-nva-bgp/README.md b/fast/stages/2-networking-e-nva-bgp/README.md
index 04db384dd..5e3a636bc 100644
--- a/fast/stages/2-networking-e-nva-bgp/README.md
+++ b/fast/stages/2-networking-e-nva-bgp/README.md
@@ -484,25 +484,25 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L42) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap |
-| [folder_ids](variables.tf#L110) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
-| [organization](variables.tf#L164) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
-| [prefix](variables.tf#L180) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
+| [folder_ids](variables.tf#L116) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
+| [organization](variables.tf#L160) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
+| [prefix](variables.tf#L176) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | object({…}) | | {…} | |
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap |
| [dns](variables.tf#L72) | DNS configuration. | object({…}) | | {} | |
| [enable_cloud_nat](variables.tf#L82) | Deploy Cloud NAT. | bool | | false | |
-| [factories_config](variables.tf#L89) | Configuration for network resource factories. | object({…}) | | {…} | |
-| [gcp_ranges](variables.tf#L120) | GCP address ranges in name => range format. | map(string) | | {…} | |
-| [groups](variables.tf#L135) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | object({…}) | | {} | 0-bootstrap |
-| [ncc_asn](variables.tf#L145) | The NCC Cloud Routers ASN configuration. | map(number) | | {…} | |
-| [onprem_cidr](variables.tf#L156) | Onprem addresses in name => range format. | map(string) | | {…} | |
-| [outputs_location](variables.tf#L174) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
-| [psa_ranges](variables.tf#L191) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | |
-| [regions](variables.tf#L210) | Region definitions. | object({…}) | | {…} | |
-| [service_accounts](variables.tf#L222) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
-| [vpn_onprem_primary_config](variables.tf#L236) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | |
-| [vpn_onprem_secondary_config](variables.tf#L279) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | |
-| [zones](variables.tf#L322) | Zones in which NVAs are deployed. | list(string) | | ["b", "c"] | |
+| [essential_contacts](variables.tf#L89) | Email used for essential contacts, unset if null. | string | | null | |
+| [factories_config](variables.tf#L95) | Configuration for network resource factories. | object({…}) | | {…} | |
+| [gcp_ranges](variables.tf#L126) | GCP address ranges in name => range format. | map(string) | | {…} | |
+| [ncc_asn](variables.tf#L141) | The NCC Cloud Routers ASN configuration. | map(number) | | {…} | |
+| [onprem_cidr](variables.tf#L152) | Onprem addresses in name => range format. | map(string) | | {…} | |
+| [outputs_location](variables.tf#L170) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | |
+| [psa_ranges](variables.tf#L187) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | |
+| [regions](variables.tf#L206) | Region definitions. | object({…}) | | {…} | |
+| [service_accounts](variables.tf#L218) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman |
+| [vpn_onprem_primary_config](variables.tf#L232) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | |
+| [vpn_onprem_secondary_config](variables.tf#L275) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | |
+| [zones](variables.tf#L318) | Zones in which NVAs are deployed. | list(string) | | ["b", "c"] | |
## Outputs
diff --git a/fast/stages/2-networking-e-nva-bgp/main.tf b/fast/stages/2-networking-e-nva-bgp/main.tf
index 91028b319..2f5fead60 100644
--- a/fast/stages/2-networking-e-nva-bgp/main.tf
+++ b/fast/stages/2-networking-e-nva-bgp/main.tf
@@ -18,10 +18,6 @@
locals {
custom_roles = coalesce(var.custom_roles, {})
- groups = {
- for k, v in var.groups :
- k => can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
# combine all regions from variables and subnets
regions = distinct(concat(
values(var.regions),
@@ -50,9 +46,11 @@ module "folder" {
name = "Networking"
folder_create = var.folder_ids.networking == null
id = var.folder_ids.networking
- contacts = {
- (local.groups.gcp-network-admins) = ["ALL"]
- }
+ contacts = (
+ var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
firewall_policy = {
name = "default"
policy = module.firewall-policy-default.id
diff --git a/fast/stages/2-networking-e-nva-bgp/variables.tf b/fast/stages/2-networking-e-nva-bgp/variables.tf
index 3a47e0958..2d2af4417 100644
--- a/fast/stages/2-networking-e-nva-bgp/variables.tf
+++ b/fast/stages/2-networking-e-nva-bgp/variables.tf
@@ -86,6 +86,12 @@ variable "enable_cloud_nat" {
nullable = false
}
+variable "essential_contacts" {
+ description = "Email used for essential contacts, unset if null."
+ type = string
+ default = null
+}
+
variable "factories_config" {
description = "Configuration for network resource factories."
type = object({
@@ -132,16 +138,6 @@ variable "gcp_ranges" {
}
}
-variable "groups" {
- # tfdoc:variable:source 0-bootstrap
- description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
- type = object({
- gcp-network-admins = optional(string)
- })
- default = {}
- nullable = false
-}
-
variable "ncc_asn" {
description = "The NCC Cloud Routers ASN configuration."
type = map(number)
diff --git a/fast/stages/2-security/README.md b/fast/stages/2-security/README.md
index d7d713e0e..a3c3f5531 100644
--- a/fast/stages/2-security/README.md
+++ b/fast/stages/2-security/README.md
@@ -294,17 +294,17 @@ Some references that might be useful in setting up this stage:
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [billing_account](variables.tf#L25) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap |
-| [folder_ids](variables.tf#L38) | Folder name => id mappings, the 'security' folder name must exist. | object({…}) | ✓ | | 1-resman |
-| [organization](variables.tf#L98) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
-| [prefix](variables.tf#L114) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
-| [service_accounts](variables.tf#L125) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…}) | ✓ | | 1-resman |
-| [groups](variables.tf#L46) | Group names to grant organization-level permissions. | map(string) | | {…} | 0-bootstrap |
-| [kms_keys](variables.tf#L61) | KMS keys to create, keyed by name. | map(object({…})) | | {} | |
-| [outputs_location](variables.tf#L108) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | |
-| [vpc_sc_access_levels](variables.tf#L136) | VPC SC access level definitions. | map(object({…})) | | {} | |
-| [vpc_sc_egress_policies](variables.tf#L165) | VPC SC egress policy definitions. | map(object({…})) | | {} | |
-| [vpc_sc_ingress_policies](variables.tf#L185) | VPC SC ingress policy definitions. | map(object({…})) | | {} | |
-| [vpc_sc_perimeters](variables.tf#L206) | VPC SC regular perimeter definitions. | object({…}) | | {} | |
+| [folder_ids](variables.tf#L44) | Folder name => id mappings, the 'security' folder name must exist. | object({…}) | ✓ | | 1-resman |
+| [organization](variables.tf#L89) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
+| [prefix](variables.tf#L105) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap |
+| [service_accounts](variables.tf#L116) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…}) | ✓ | | 1-resman |
+| [essential_contacts](variables.tf#L38) | Email used for essential contacts, unset if null. | string | | null | |
+| [kms_keys](variables.tf#L52) | KMS keys to create, keyed by name. | map(object({…})) | | {} | |
+| [outputs_location](variables.tf#L99) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | |
+| [vpc_sc_access_levels](variables.tf#L127) | VPC SC access level definitions. | map(object({…})) | | {} | |
+| [vpc_sc_egress_policies](variables.tf#L156) | VPC SC egress policy definitions. | map(object({…})) | | {} | |
+| [vpc_sc_ingress_policies](variables.tf#L176) | VPC SC ingress policy definitions. | map(object({…})) | | {} | |
+| [vpc_sc_perimeters](variables.tf#L197) | VPC SC regular perimeter definitions. | object({…}) | | {} | |
## Outputs
diff --git a/fast/stages/2-security/main.tf b/fast/stages/2-security/main.tf
index 491811c6c..a72bd7b78 100644
--- a/fast/stages/2-security/main.tf
+++ b/fast/stages/2-security/main.tf
@@ -15,10 +15,6 @@
*/
locals {
- groups = {
- for k, v in var.groups :
- k => can(regex(".*@.*", v)) ? v : "${v}@${var.organization.domain}"
- }
# additive IAM binding for delegated KMS admins
kms_restricted_admin_template = {
role = "roles/cloudkms.admin"
@@ -64,7 +60,9 @@ module "folder" {
name = "Security"
folder_create = var.folder_ids.security == null
id = var.folder_ids.security
- contacts = {
- (local.groups.gcp-security-admins) = ["ALL"]
- }
+ contacts = (
+ var.essential_contacts == null
+ ? {}
+ : { (var.essential_contacts) = ["ALL"] }
+ )
}
diff --git a/fast/stages/2-security/variables.tf b/fast/stages/2-security/variables.tf
index 39a3daf8a..f4e1c0fb9 100644
--- a/fast/stages/2-security/variables.tf
+++ b/fast/stages/2-security/variables.tf
@@ -35,6 +35,12 @@ variable "billing_account" {
}
}
+variable "essential_contacts" {
+ description = "Email used for essential contacts, unset if null."
+ type = string
+ default = null
+}
+
variable "folder_ids" {
# tfdoc:variable:source 1-resman
description = "Folder name => id mappings, the 'security' folder name must exist."
@@ -43,21 +49,6 @@ variable "folder_ids" {
})
}
-variable "groups" {
- # tfdoc:variable:source 0-bootstrap
- description = "Group names to grant organization-level permissions."
- type = map(string)
- # https://cloud.google.com/docs/enterprise/setup-checklist
- default = {
- gcp-billing-admins = "gcp-billing-admins",
- gcp-devops = "gcp-devops",
- gcp-network-admins = "gcp-network-admins"
- gcp-organization-admins = "gcp-organization-admins"
- gcp-security-admins = "gcp-security-admins"
- gcp-support = "gcp-support"
- }
-}
-
variable "kms_keys" {
description = "KMS keys to create, keyed by name."
type = map(object({
diff --git a/fast/stages/3-gke-multitenant/dev/README.md b/fast/stages/3-gke-multitenant/dev/README.md
index eba77e01b..77ee84b09 100644
--- a/fast/stages/3-gke-multitenant/dev/README.md
+++ b/fast/stages/3-gke-multitenant/dev/README.md
@@ -217,7 +217,7 @@ Leave all these variables unset (or set to `null`) to disable fleet management.
| [automation](variables.tf#L21) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [billing_account](variables.tf#L29) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap |
| [folder_ids](variables.tf#L175) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
-| [host_project_ids](variables.tf#L190) | Host project for the shared VPC. | object({…}) | ✓ | | 2-networking |
+| [host_project_ids](variables.tf#L183) | Host project for the shared VPC. | object({…}) | ✓ | | 2-networking |
| [prefix](variables.tf#L242) | Prefix used for resources that need unique names. | string | ✓ | | |
| [vpc_self_links](variables.tf#L258) | Self link for the shared VPC. | object({…}) | ✓ | | 2-networking |
| [clusters](variables.tf#L42) | Clusters configuration. Refer to the gke-cluster-standard module for type details. | map(object({…})) | | {} | |
@@ -225,8 +225,8 @@ Leave all these variables unset (or set to `null`) to disable fleet management.
| [fleet_configmanagement_templates](variables.tf#L120) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | |
| [fleet_features](variables.tf#L155) | Enable and configure fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | |
| [fleet_workload_identity](variables.tf#L168) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | |
-| [group_iam](variables.tf#L183) | Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | |
-| [iam](variables.tf#L198) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | |
+| [iam](variables.tf#L191) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | |
+| [iam_by_principals](variables.tf#L198) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | |
| [labels](variables.tf#L205) | Project-level labels. | map(string) | | {} | |
| [nodepools](variables.tf#L211) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | |
| [outputs_location](variables.tf#L236) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | |
diff --git a/fast/stages/3-gke-multitenant/dev/main.tf b/fast/stages/3-gke-multitenant/dev/main.tf
index 0fd29f5a1..261b2477c 100644
--- a/fast/stages/3-gke-multitenant/dev/main.tf
+++ b/fast/stages/3-gke-multitenant/dev/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ module "gke-multitenant" {
billing_account_id = var.billing_account.id
folder_id = var.folder_ids.gke-dev
project_id = "gke-0"
- group_iam = var.group_iam
+ iam_by_principals = var.iam_by_principals
iam = var.iam
labels = merge(var.labels, { environment = "dev" })
prefix = "${var.prefix}-dev"
diff --git a/fast/stages/3-gke-multitenant/dev/variables.tf b/fast/stages/3-gke-multitenant/dev/variables.tf
index ce6a58095..eadcbe088 100644
--- a/fast/stages/3-gke-multitenant/dev/variables.tf
+++ b/fast/stages/3-gke-multitenant/dev/variables.tf
@@ -180,13 +180,6 @@ variable "folder_ids" {
})
}
-variable "group_iam" {
- description = "Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
variable "host_project_ids" {
# tfdoc:variable:source 2-networking
description = "Host project for the shared VPC."
@@ -202,6 +195,13 @@ variable "iam" {
nullable = false
}
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
variable "labels" {
description = "Project-level labels."
type = map(string)
diff --git a/modules/__docs/20230816-iam-refactor.md b/modules/__docs/20230816-iam-refactor.md
index 469166576..80b3e564f 100644
--- a/modules/__docs/20230816-iam-refactor.md
+++ b/modules/__docs/20230816-iam-refactor.md
@@ -1,12 +1,13 @@
# Refactor IAM interface
**authors:** [Ludo](https://github.com/ludoo), [Julio](https://github.com/juliocc)
-**last modified:** August 17, 2023
+**last modified:** February 12, 2024
## Status
-Implemented in [#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595).
-Authoritative bindings type changed as per [#1622](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/issues/1622).
+- Implemented in [#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595).
+- Authoritative bindings type changed as per [#1622](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/issues/1622).
+- Extended by [#2064](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/issues/2064).
## Context
@@ -113,10 +114,30 @@ variable "iam_bindings_additive" {
The **proposal** is to remove the IAM policy variable and resources, as its coverage is very uneven and we never used it in practice. This will also simplify data access log management, which is currently split between its own variable/resource and the IAM policy ones.
+### IAM by Principals
+> [!NOTE]
+> This section was added on 2024-02-12
+
+[#2064](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/issues/2064). replaced `group_iam` with `iam_by_principals`. The structure of `iam_by_principals` is similar to the original `group_iam` with the difference that now the user has to specify the principal type with the correct prefix. The new variable format is shown below
+
+```hcl
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+```
+
+
+See #2064 and [this ADR](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/blob/ludo/iam-changes/fast/docs/0-domainless-iam.md) for more details.
+
+
## Decision
The proposal above summarizes the state of discussions between the authors, and implementation will be tested.
+
## Consequences
### FAST
@@ -180,13 +201,36 @@ variable "iam_bindings_additive" {
default = {}
nullable = false
}
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
```
```terraform
# iam.tf
+locals {
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
+ ]
+ }
+ iam = {
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
+ role => concat(
+ try(var.iam[role], []),
+ try(local._iam_principals[role], [])
+ )
+ }
+}
resource "google_RESOURCE_TYPE_iam_binding" "authoritative" {
- for_each = var.iam
+ for_each = local.iam
role = each.key
members = each.value
// add extra attributes (e.g. resource id)
diff --git a/modules/billing-account/README.md b/modules/billing-account/README.md
index b5ddae2f1..23a73a133 100644
--- a/modules/billing-account/README.md
+++ b/modules/billing-account/README.md
@@ -38,9 +38,6 @@ Billing account IAM bindings implement [the same interface](../__docs/20230816-i
module "billing-account" {
source = "./fabric/modules/billing-account"
id = "012345-ABCDEF-012345"
- group_iam = {
- "billing-admins@example.org" = ["roles/billing.admin"]
- }
iam = {
"roles/billing.admin" = [
"serviceAccount:foo@myprj.iam.gserviceaccount.com"
@@ -66,6 +63,9 @@ module "billing-account" {
role = "roles/billing.user"
}
}
+ iam_by_principals = {
+ "group:billing-admins@example.org" = ["roles/billing.admin"]
+ }
}
# tftest modules=1 resources=3 inventory=iam.yaml
```
@@ -260,16 +260,16 @@ update_rules:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [id](variables.tf#L175) | Billing account id. | string | ✓ | |
+| [id](variables.tf#L131) | Billing account id. | string | ✓ | |
| [budget_notification_channels](variables.tf#L17) | Notification channels used by budget alerts. | map(object({…})) | | {} |
| [budgets](variables.tf#L47) | Billing budgets. Notification channels are either keys in corresponding variable, or external ids. | map(object({…})) | | {} |
| [factory_config](variables.tf#L121) | Path to folder containing budget alerts data files. | object({…}) | | {} |
-| [group_iam](variables.tf#L131) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} |
-| [iam](variables.tf#L138) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L145) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L160) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [logging_sinks](variables.tf#L180) | Logging sinks to create for the organization. | map(object({…})) | | {} |
-| [projects](variables.tf#L213) | Projects associated with this billing account. | list(string) | | [] |
+| [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [logging_sinks](variables.tf#L136) | Logging sinks to create for the organization. | map(object({…})) | | {} |
+| [projects](variables.tf#L169) | Projects associated with this billing account. | list(string) | | [] |
## Outputs
diff --git a/modules/billing-account/iam.tf b/modules/billing-account/iam.tf
index d8cec2581..9d6704a86 100644
--- a/modules/billing-account/iam.tf
+++ b/modules/billing-account/iam.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,17 +17,18 @@
# tfdoc:file:description IAM bindings.
locals {
- _group_iam_roles = distinct(flatten(values(var.group_iam)))
- _group_iam = {
- for r in local._group_iam_roles : r => [
- for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
]
}
iam = {
- for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
role => concat(
try(var.iam[role], []),
- try(local._group_iam[role], [])
+ try(local._iam_principals[role], [])
)
}
}
diff --git a/modules/billing-account/variables-iam.tf b/modules/billing-account/variables-iam.tf
new file mode 100644
index 000000000..4299d4554
--- /dev/null
+++ b/modules/billing-account/variables-iam.tf
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "iam" {
+ description = "IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
diff --git a/modules/billing-account/variables.tf b/modules/billing-account/variables.tf
index db57c4e69..7ae5bd16a 100644
--- a/modules/billing-account/variables.tf
+++ b/modules/billing-account/variables.tf
@@ -128,50 +128,6 @@ variable "factory_config" {
default = {}
}
-variable "group_iam" {
- description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam" {
- description = "IAM bindings in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam_bindings" {
- description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "iam_bindings_additive" {
- description = "Individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
variable "id" {
description = "Billing account id."
type = string
diff --git a/modules/data-catalog-policy-tag/README.md b/modules/data-catalog-policy-tag/README.md
index 139c74708..49f1e80f1 100644
--- a/modules/data-catalog-policy-tag/README.md
+++ b/modules/data-catalog-policy-tag/README.md
@@ -18,11 +18,11 @@ Note: Data Catalog is still in beta, hence this module currently uses the beta p
IAM is managed via several variables that implement different features and levels of control:
-- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
+- `iam` and `iam_by_principals` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
-The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
+The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `iam_by_principals` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
Refer to the [project module](../project/README.md#iam) for examples of the IAM interface.
@@ -79,17 +79,17 @@ module "cmn-dc" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L77) | Name of this taxonomy. | string | ✓ | |
-| [project_id](variables.tf#L92) | GCP project id. | string | ✓ | |
+| [name](variables.tf#L35) | Name of this taxonomy. | string | ✓ | |
+| [project_id](variables.tf#L50) | GCP project id. | string | ✓ | |
| [activated_policy_types](variables.tf#L17) | A list of policy types that are activated for this taxonomy. | list(string) | | ["FINE_GRAINED_ACCESS_CONTROL"] |
| [description](variables.tf#L23) | Description of this taxonomy. | string | | "Taxonomy - Terraform managed" |
-| [group_iam](variables.tf#L29) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} |
-| [iam](variables.tf#L35) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L41) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L56) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [location](variables.tf#L71) | Data Catalog Taxonomy location. | string | | "eu" |
-| [prefix](variables.tf#L82) | Optional prefix used to generate project id and name. | string | | null |
-| [tags](variables.tf#L97) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(object({…})) | | {} |
+| [iam](variables-iam.tf#L23) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L29) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L44) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L17) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [location](variables.tf#L29) | Data Catalog Taxonomy location. | string | | "eu" |
+| [prefix](variables.tf#L40) | Optional prefix used to generate project id and name. | string | | null |
+| [tags](variables.tf#L55) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(object({…})) | | {} |
## Outputs
diff --git a/modules/data-catalog-policy-tag/iam.tf b/modules/data-catalog-policy-tag/iam.tf
index 06c307631..4abd98a92 100644
--- a/modules/data-catalog-policy-tag/iam.tf
+++ b/modules/data-catalog-policy-tag/iam.tf
@@ -17,17 +17,18 @@
# tfdoc:file:description Data Catalog Taxonomy IAM definition.
locals {
- _group_iam = {
- for r in local._group_iam_roles : r => [
- for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
]
}
- _group_iam_roles = distinct(flatten(values(var.group_iam)))
iam = {
- for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
role => concat(
try(var.iam[role], []),
- try(local._group_iam[role], [])
+ try(local._iam_principals[role], [])
)
}
tags_iam = flatten([
diff --git a/modules/data-catalog-policy-tag/variables-iam.tf b/modules/data-catalog-policy-tag/variables-iam.tf
new file mode 100644
index 000000000..fbe243d8d
--- /dev/null
+++ b/modules/data-catalog-policy-tag/variables-iam.tf
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+}
+
+variable "iam" {
+ description = "IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
diff --git a/modules/data-catalog-policy-tag/variables.tf b/modules/data-catalog-policy-tag/variables.tf
index a41990b91..af06cd30b 100644
--- a/modules/data-catalog-policy-tag/variables.tf
+++ b/modules/data-catalog-policy-tag/variables.tf
@@ -26,48 +26,6 @@ variable "description" {
default = "Taxonomy - Terraform managed"
}
-variable "group_iam" {
- description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
-}
-
-variable "iam" {
- description = "IAM bindings in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
-}
-
-variable "iam_bindings" {
- description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "iam_bindings_additive" {
- description = "Individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
variable "location" {
description = "Data Catalog Taxonomy location."
type = string
diff --git a/modules/dataplex-datascan/README.md b/modules/dataplex-datascan/README.md
index ca61c7614..4053b6069 100644
--- a/modules/dataplex-datascan/README.md
+++ b/modules/dataplex-datascan/README.md
@@ -382,11 +382,11 @@ module "dataplex-datascan" {
IAM is managed via several variables that implement different features and levels of control:
-- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
+- `iam` and `iam_by_principals` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
-The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
+The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `iam_by_principals` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
An example is provided below for using some of these variables. Refer to the [project module](../project/README.md#iam) for complete examples of the IAM interface.
@@ -409,8 +409,8 @@ module "dataplex-datascan" {
"user:admin-user@example.com"
]
}
- group_iam = {
- "user-group@example.com" = [
+ iam_by_principals = {
+ "group:user-group@example.com" = [
"roles/dataplex.dataScanViewer"
]
}
@@ -431,21 +431,21 @@ module "dataplex-datascan" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [data](variables.tf#L17) | The data source for DataScan. The source can be either a Dataplex `entity` or a BigQuery `resource`. | object({…}) | ✓ | |
-| [name](variables.tf#L162) | Name of Dataplex Scan. | string | ✓ | |
-| [project_id](variables.tf#L173) | The ID of the project where the Dataplex DataScan will be created. | string | ✓ | |
-| [region](variables.tf#L178) | Region for the Dataplex DataScan. | string | ✓ | |
+| [name](variables.tf#L118) | Name of Dataplex Scan. | string | ✓ | |
+| [project_id](variables.tf#L129) | The ID of the project where the Dataplex DataScan will be created. | string | ✓ | |
+| [region](variables.tf#L134) | Region for the Dataplex DataScan. | string | ✓ | |
| [data_profile_spec](variables.tf#L29) | DataProfileScan related setting. Variable descriptions are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataProfileSpec. | object({…}) | | null |
| [data_quality_spec](variables.tf#L38) | DataQualityScan related setting. Variable descriptions are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataQualitySpec. | object({…}) | | null |
| [data_quality_spec_file](variables.tf#L85) | Path to a YAML file containing DataQualityScan related setting. Input content can use either camelCase or snake_case. Variables description are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataQualitySpec. | object({…}) | | null |
| [description](variables.tf#L93) | Custom description for DataScan. | string | | null |
| [execution_schedule](variables.tf#L99) | Schedule DataScan to run periodically based on a cron schedule expression. If not specified, the DataScan is created with `on_demand` schedule, which means it will not run until the user calls `dataScans.run` API. | string | | null |
-| [group_iam](variables.tf#L105) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} |
-| [iam](variables.tf#L112) | Dataplex DataScan IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L119) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L134) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [incremental_field](variables.tf#L149) | The unnested field (of type Date or Timestamp) that contains values which monotonically increase over time. If not specified, a data scan will run for all data in the table. | string | | null |
-| [labels](variables.tf#L155) | Resource labels. | map(string) | | {} |
-| [prefix](variables.tf#L167) | Optional prefix used to generate Dataplex DataScan ID. | string | | null |
+| [iam](variables-iam.tf#L24) | Dataplex DataScan IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L31) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L46) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L17) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [incremental_field](variables.tf#L105) | The unnested field (of type Date or Timestamp) that contains values which monotonically increase over time. If not specified, a data scan will run for all data in the table. | string | | null |
+| [labels](variables.tf#L111) | Resource labels. | map(string) | | {} |
+| [prefix](variables.tf#L123) | Optional prefix used to generate Dataplex DataScan ID. | string | | null |
## Outputs
diff --git a/modules/dataplex-datascan/iam.tf b/modules/dataplex-datascan/iam.tf
index 9ed591444..dea671a95 100644
--- a/modules/dataplex-datascan/iam.tf
+++ b/modules/dataplex-datascan/iam.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,17 +15,18 @@
*/
locals {
- _group_iam_roles = distinct(flatten(values(var.group_iam)))
- _group_iam = {
- for r in local._group_iam_roles : r => [
- for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
]
}
iam = {
- for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
role => concat(
try(var.iam[role], []),
- try(local._group_iam[role], [])
+ try(local._iam_principals[role], [])
)
}
}
diff --git a/modules/dataplex-datascan/variables-iam.tf b/modules/dataplex-datascan/variables-iam.tf
new file mode 100644
index 000000000..81a93f9ff
--- /dev/null
+++ b/modules/dataplex-datascan/variables-iam.tf
@@ -0,0 +1,60 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam" {
+ description = "Dataplex DataScan IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
diff --git a/modules/dataplex-datascan/variables.tf b/modules/dataplex-datascan/variables.tf
index f1cb05b65..cab105bfe 100644
--- a/modules/dataplex-datascan/variables.tf
+++ b/modules/dataplex-datascan/variables.tf
@@ -102,50 +102,6 @@ variable "execution_schedule" {
default = null
}
-variable "group_iam" {
- description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam" {
- description = "Dataplex DataScan IAM bindings in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam_bindings" {
- description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "iam_bindings_additive" {
- description = "Individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
variable "incremental_field" {
description = "The unnested field (of type Date or Timestamp) that contains values which monotonically increase over time. If not specified, a data scan will run for all data in the table."
type = string
diff --git a/modules/dataproc/README.md b/modules/dataproc/README.md
index f00101345..0c6b40527 100644
--- a/modules/dataproc/README.md
+++ b/modules/dataproc/README.md
@@ -92,11 +92,11 @@ module "processing-dp-cluster" {
IAM is managed via several variables that implement different features and levels of control:
-- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
+- `iam` and `iam_by_principals` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
-The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
+The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `iam_by_principals` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
Refer to the [project module](../project/README.md#iam) for examples of the IAM interface.
@@ -109,8 +109,8 @@ module "processing-dp-cluster" {
name = "my-cluster"
region = "europe-west1"
prefix = "prefix"
- group_iam = {
- "gcp-data-engineers@example.net" = [
+ iam_by_principals = {
+ "group:gcp-data-engineers@example.net" = [
"roles/dataproc.viewer"
]
}
@@ -146,17 +146,17 @@ module "processing-dp-cluster" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L235) | Cluster name. | string | ✓ | |
-| [project_id](variables.tf#L250) | Project ID. | string | ✓ | |
-| [region](variables.tf#L255) | Dataproc region. | string | ✓ | |
+| [name](variables.tf#L191) | Cluster name. | string | ✓ | |
+| [project_id](variables.tf#L206) | Project ID. | string | ✓ | |
+| [region](variables.tf#L211) | Dataproc region. | string | ✓ | |
| [dataproc_config](variables.tf#L17) | Dataproc cluster config. | object({…}) | | {} |
-| [group_iam](variables.tf#L185) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} |
-| [iam](variables.tf#L192) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L199) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L214) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [labels](variables.tf#L229) | The resource labels for instance to use to annotate any related underlying resources, such as Compute Engine VMs. | map(string) | | {} |
-| [prefix](variables.tf#L240) | Optional prefix used to generate project id and name. | string | | null |
-| [service_account](variables.tf#L260) | Service account to set on the Dataproc cluster. | string | | null |
+| [iam](variables-iam.tf#L24) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L31) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L46) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L17) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [labels](variables.tf#L185) | The resource labels for instance to use to annotate any related underlying resources, such as Compute Engine VMs. | map(string) | | {} |
+| [prefix](variables.tf#L196) | Optional prefix used to generate project id and name. | string | | null |
+| [service_account](variables.tf#L216) | Service account to set on the Dataproc cluster. | string | | null |
## Outputs
diff --git a/modules/dataproc/iam.tf b/modules/dataproc/iam.tf
index ef0428d13..b3fc6aa47 100644
--- a/modules/dataproc/iam.tf
+++ b/modules/dataproc/iam.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,20 +14,21 @@
* limitations under the License.
*/
-# # tfdoc:file:description Generic IAM bindings and roles.
+# tfdoc:file:description IAM bindings.
locals {
- _group_iam_roles = distinct(flatten(values(var.group_iam)))
- _group_iam = {
- for r in local._group_iam_roles : r => [
- for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
]
}
iam = {
- for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
role => concat(
try(var.iam[role], []),
- try(local._group_iam[role], [])
+ try(local._iam_principals[role], [])
)
}
}
diff --git a/modules/dataproc/variables-iam.tf b/modules/dataproc/variables-iam.tf
new file mode 100644
index 000000000..16f24608a
--- /dev/null
+++ b/modules/dataproc/variables-iam.tf
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam" {
+ description = "IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
diff --git a/modules/dataproc/variables.tf b/modules/dataproc/variables.tf
index f4170a94f..7d599d3a9 100644
--- a/modules/dataproc/variables.tf
+++ b/modules/dataproc/variables.tf
@@ -182,50 +182,6 @@ variable "dataproc_config" {
default = {}
}
-variable "group_iam" {
- description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam" {
- description = "IAM bindings in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam_bindings" {
- description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "iam_bindings_additive" {
- description = "Individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
variable "labels" {
description = "The resource labels for instance to use to annotate any related underlying resources, such as Compute Engine VMs."
type = map(string)
diff --git a/modules/folder/README.md b/modules/folder/README.md
index 8a731a955..e52eb1e13 100644
--- a/modules/folder/README.md
+++ b/modules/folder/README.md
@@ -23,8 +23,8 @@ module "folder" {
source = "./fabric/modules/folder"
parent = var.folder_id
name = "Folder name"
- group_iam = {
- "${var.group_email}" = [
+ iam_by_principals = {
+ "group:${var.group_email}" = [
"roles/owner",
"roles/resourcemanager.folderAdmin",
"roles/resourcemanager.projectCreator"
@@ -47,11 +47,11 @@ module "folder" {
IAM is managed via several variables that implement different features and levels of control:
-- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
+- `iam` and `iam_by_principals` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
-The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
+The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `iam_by_principals` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
Refer to the [project module](../project/README.md#iam) for examples of the IAM interface.
@@ -334,6 +334,7 @@ module "folder" {
| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_org_policy_policy |
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [tags.tf](./tags.tf) | None | google_tags_tag_binding |
+| [variables-iam.tf](./variables-iam.tf) | None | |
| [variables.tf](./variables.tf) | Module variables. | |
| [versions.tf](./versions.tf) | Version pins. | |
@@ -345,18 +346,18 @@ module "folder" {
| [factories_config](variables.tf#L24) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} |
| [firewall_policy](variables.tf#L33) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null |
| [folder_create](variables.tf#L42) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true |
-| [group_iam](variables.tf#L48) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} |
-| [iam](variables.tf#L55) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L62) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L77) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [id](variables.tf#L92) | Folder ID in case you use folder_create=false. | string | | null |
-| [logging_data_access](variables.tf#L98) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} |
-| [logging_exclusions](variables.tf#L113) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} |
-| [logging_sinks](variables.tf#L120) | Logging sinks to create for the folder. | map(object({…})) | | {} |
-| [name](variables.tf#L151) | Folder name. | string | | null |
-| [org_policies](variables.tf#L157) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} |
-| [parent](variables.tf#L184) | Parent in folders/folder_id or organizations/org_id format. | string | | null |
-| [tag_bindings](variables.tf#L194) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null |
+| [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [id](variables.tf#L48) | Folder ID in case you use folder_create=false. | string | | null |
+| [logging_data_access](variables.tf#L54) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} |
+| [logging_exclusions](variables.tf#L69) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} |
+| [logging_sinks](variables.tf#L76) | Logging sinks to create for the folder. | map(object({…})) | | {} |
+| [name](variables.tf#L107) | Folder name. | string | | null |
+| [org_policies](variables.tf#L113) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} |
+| [parent](variables.tf#L140) | Parent in folders/folder_id or organizations/org_id format. | string | | null |
+| [tag_bindings](variables.tf#L150) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null |
## Outputs
diff --git a/modules/folder/iam.tf b/modules/folder/iam.tf
index 56ed62126..ba852837c 100644
--- a/modules/folder/iam.tf
+++ b/modules/folder/iam.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,17 +17,18 @@
# tfdoc:file:description IAM bindings.
locals {
- _group_iam_roles = distinct(flatten(values(var.group_iam)))
- _group_iam = {
- for r in local._group_iam_roles : r => [
- for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
]
}
iam = {
- for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
role => concat(
try(var.iam[role], []),
- try(local._group_iam[role], [])
+ try(local._iam_principals[role], [])
)
}
}
diff --git a/modules/folder/variables-iam.tf b/modules/folder/variables-iam.tf
new file mode 100644
index 000000000..4299d4554
--- /dev/null
+++ b/modules/folder/variables-iam.tf
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "iam" {
+ description = "IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf
index 0349135f0..414593e94 100644
--- a/modules/folder/variables.tf
+++ b/modules/folder/variables.tf
@@ -45,50 +45,6 @@ variable "folder_create" {
default = true
}
-variable "group_iam" {
- description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam" {
- description = "IAM bindings in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam_bindings" {
- description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "iam_bindings_additive" {
- description = "Individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
variable "id" {
description = "Folder ID in case you use folder_create=false."
type = string
diff --git a/modules/organization/README.md b/modules/organization/README.md
index 5ed4a5b06..f21bee99b 100644
--- a/modules/organization/README.md
+++ b/modules/organization/README.md
@@ -36,8 +36,8 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b
module "org" {
source = "./fabric/modules/organization"
organization_id = var.organization_id
- group_iam = {
- (var.group_email) = ["roles/owner"]
+ iam_by_principals = {
+ "group:${var.group_email}" = ["roles/owner"]
}
iam = {
"roles/resourcemanager.projectCreator" = ["group:${var.group_email}"]
@@ -121,11 +121,11 @@ module "org" {
IAM is managed via several variables that implement different features and levels of control:
-- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
+- `iam` and `iam_by_principals` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
-The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
+The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `iam_by_principals` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
Refer to the [project module](../project/README.md#iam) for examples of the IAM interface.
@@ -473,13 +473,14 @@ module "org" {
| name | description | resources |
|---|---|---|
-| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_organization_iam_binding · google_organization_iam_custom_role · google_organization_iam_member |
+| [iam.tf](./iam.tf) | IAM bindings. | google_organization_iam_binding · google_organization_iam_custom_role · google_organization_iam_member |
| [logging.tf](./logging.tf) | Log sinks and data access logs. | google_bigquery_dataset_iam_member · google_logging_organization_exclusion · google_logging_organization_sink · google_organization_iam_audit_config · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member |
| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_firewall_policy_association · google_essential_contacts_contact |
| [org-policy-custom-constraints.tf](./org-policy-custom-constraints.tf) | None | google_org_policy_custom_constraint |
| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_policy |
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding |
+| [variables-iam.tf](./variables-iam.tf) | None | |
| [variables-tags.tf](./variables-tags.tf) | None | |
| [variables.tf](./variables.tf) | Module variables. | |
| [versions.tf](./versions.tf) | Version pins. | |
@@ -488,21 +489,21 @@ module "org" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [organization_id](variables.tf#L189) | Organization id in organizations/nnnnnn format. | string | ✓ | |
+| [organization_id](variables.tf#L145) | Organization id in organizations/nnnnnn format. | string | ✓ | |
| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} |
| [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} |
| [factories_config](variables.tf#L31) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} |
| [firewall_policy](variables.tf#L42) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null |
-| [group_iam](variables.tf#L51) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} |
-| [iam](variables.tf#L58) | IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L65) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L80) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [logging_data_access](variables.tf#L95) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} |
-| [logging_exclusions](variables.tf#L110) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} |
-| [logging_sinks](variables.tf#L117) | Logging sinks to create for the organization. | map(object({…})) | | {} |
+| [iam](variables-iam.tf#L17) | IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [logging_data_access](variables.tf#L51) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} |
+| [logging_exclusions](variables.tf#L66) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} |
+| [logging_sinks](variables.tf#L73) | Logging sinks to create for the organization. | map(object({…})) | | {} |
| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} |
-| [org_policies](variables.tf#L148) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} |
-| [org_policy_custom_constraints](variables.tf#L175) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} |
+| [org_policies](variables.tf#L104) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} |
+| [org_policy_custom_constraints](variables.tf#L131) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} |
| [tag_bindings](variables-tags.tf#L45) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} |
| [tags](variables-tags.tf#L52) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} |
diff --git a/modules/organization/iam.tf b/modules/organization/iam.tf
index e0726467f..2e2512174 100644
--- a/modules/organization/iam.tf
+++ b/modules/organization/iam.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-# tfdoc:file:description IAM bindings, roles and audit logging resources.
+# tfdoc:file:description IAM bindings.
locals {
_custom_roles = {
@@ -23,10 +23,11 @@ locals {
file("${var.factories_config.custom_roles}/${f}")
)
}
- _group_iam_roles = distinct(flatten(values(var.group_iam)))
- _group_iam = {
- for r in local._group_iam_roles : r => [
- for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
]
}
custom_roles = merge(
@@ -44,10 +45,10 @@ locals {
}
)
iam = {
- for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
role => concat(
try(var.iam[role], []),
- try(local._group_iam[role], [])
+ try(local._iam_principals[role], [])
)
}
}
diff --git a/modules/organization/variables-iam.tf b/modules/organization/variables-iam.tf
new file mode 100644
index 000000000..f4fecb7c4
--- /dev/null
+++ b/modules/organization/variables-iam.tf
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "iam" {
+ description = "IAM bindings, in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf
index 696cead60..45aacbe49 100644
--- a/modules/organization/variables.tf
+++ b/modules/organization/variables.tf
@@ -48,50 +48,6 @@ variable "firewall_policy" {
default = null
}
-variable "group_iam" {
- description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam" {
- description = "IAM bindings, in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam_bindings" {
- description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "iam_bindings_additive" {
- description = "Individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
variable "logging_data_access" {
description = "Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services."
type = map(map(list(string)))
diff --git a/modules/project/README.md b/modules/project/README.md
index efd6dcc24..292b63a42 100644
--- a/modules/project/README.md
+++ b/modules/project/README.md
@@ -48,13 +48,13 @@ module "project" {
IAM is managed via several variables that implement different features and levels of control:
-- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
+- `iam` and `iam_by_principals` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
-The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
+The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `iam_by_principals` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
-Be mindful about service identity roles when using authoritative IAM, as you might inadvertently remove a role from a [service identity](https://cloud.google.com/iam/docs/service-account-types#google-managed) or default service account. For example, using `roles/editor` with `iam` or `group_iam` will remove the default permissions for the Cloud Services identity. A simple workaround for these scenarios is described below.
+Be mindful about service identity roles when using authoritative IAM, as you might inadvertently remove a role from a [service identity](https://cloud.google.com/iam/docs/service-account-types#google-managed) or default service account. For example, using `roles/editor` with `iam` or `iam_principals` will remove the default permissions for the Cloud Services identity. A simple workaround for these scenarios is described below.
### Authoritative IAM
@@ -84,7 +84,7 @@ module "project" {
# tftest modules=1 resources=4 inventory=iam-authoritative.yaml
```
-The `group_iam` variable uses group email addresses as keys and is a convenient way to assign roles to humans following Google's best practices. The end result is readable code that also serves as documentation.
+The `iam_by_principals` variable uses [principals](https://cloud.google.com/iam/docs/principal-identifiers) as keys and is a convenient way to assign roles to humans following Google's best practices. The end result is readable code that also serves as documentation.
```hcl
module "project" {
@@ -93,8 +93,8 @@ module "project" {
name = "project"
parent = var.folder_id
prefix = var.prefix
- group_iam = {
- (var.group_email) = [
+ iam_by_principals = {
+ "group:${var.group_email}" = [
"roles/cloudasset.owner",
"roles/cloudsupport.techSupportEditor",
"roles/iam.securityReviewer",
@@ -721,7 +721,6 @@ module "project" {
# tftest modules=1 resources=8
```
-
## Outputs
Most of this module's outputs depend on its resources, to allow Terraform to compute all dependencies required for the project to be correctly configured. This allows you to reference outputs like `project_id` in other modules or resources without having to worry about setting `depends_on` blocks manually.
@@ -768,8 +767,8 @@ module "project" {
prefix = var.prefix
project_create = false
- group_iam = {
- (var.group_email) = [
+ iam_by_principals = {
+ "group:${var.group_email}" = [
"roles/cloudasset.owner",
"roles/cloudsupport.techSupportEditor",
"roles/iam.securityReviewer",
@@ -964,7 +963,7 @@ module "bucket" {
| name | description | resources |
|---|---|---|
-| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member |
+| [iam.tf](./iam.tf) | IAM bindings. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member |
| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_audit_config · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member |
| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien |
| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_org_policy_policy |
@@ -972,6 +971,7 @@ module "bucket" {
| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_default_service_accounts · google_project_iam_member · google_project_service_identity |
| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_compute_subnetwork_iam_member · google_project_iam_member |
| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding |
+| [variables-iam.tf](./variables-iam.tf) | None | |
| [variables-tags.tf](./variables-tags.tf) | None | |
| [variables.tf](./variables.tf) | Module variables. | |
| [versions.tf](./versions.tf) | Version pins. | |
@@ -981,7 +981,7 @@ module "bucket" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L196) | Project name and id suffix. | string | ✓ | |
+| [name](variables.tf#L152) | Project name and id suffix. | string | ✓ | |
| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false |
| [billing_account](variables.tf#L23) | Billing account id. | string | | null |
| [compute_metadata](variables.tf#L29) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string) | | {} |
@@ -990,29 +990,29 @@ module "bucket" {
| [default_service_account](variables.tf#L50) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" |
| [descriptive_name](variables.tf#L63) | Name of the project name. Used for project name instead of `name` variable. | string | | null |
| [factories_config](variables.tf#L69) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} |
-| [group_iam](variables.tf#L79) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} |
-| [iam](variables.tf#L86) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L93) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L108) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [labels](variables.tf#L123) | Resource labels. | map(string) | | {} |
-| [lien_reason](variables.tf#L130) | If non-empty, creates a project lien with this description. | string | | null |
-| [logging_data_access](variables.tf#L136) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} |
-| [logging_exclusions](variables.tf#L151) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} |
-| [logging_sinks](variables.tf#L158) | Logging sinks to create for this project. | map(object({…})) | | {} |
-| [metric_scopes](variables.tf#L189) | List of projects that will act as metric scopes for this project. | list(string) | | [] |
+| [iam](variables-iam.tf#L17) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [labels](variables.tf#L79) | Resource labels. | map(string) | | {} |
+| [lien_reason](variables.tf#L86) | If non-empty, creates a project lien with this description. | string | | null |
+| [logging_data_access](variables.tf#L92) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} |
+| [logging_exclusions](variables.tf#L107) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} |
+| [logging_sinks](variables.tf#L114) | Logging sinks to create for this project. | map(object({…})) | | {} |
+| [metric_scopes](variables.tf#L145) | List of projects that will act as metric scopes for this project. | list(string) | | [] |
| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} |
-| [org_policies](variables.tf#L201) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} |
-| [parent](variables.tf#L228) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null |
-| [prefix](variables.tf#L238) | Optional prefix used to generate project id and name. | string | | null |
-| [project_create](variables.tf#L248) | Create project. When set to false, uses a data source to reference existing project. | bool | | true |
-| [service_config](variables.tf#L254) | Configure service API activation. | object({…}) | | {…} |
-| [service_encryption_key_ids](variables.tf#L266) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} |
-| [service_perimeter_bridges](variables.tf#L273) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null |
-| [service_perimeter_standard](variables.tf#L280) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null |
-| [services](variables.tf#L286) | Service APIs to enable. | list(string) | | [] |
-| [shared_vpc_host_config](variables.tf#L292) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null |
-| [shared_vpc_service_config](variables.tf#L301) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} |
-| [skip_delete](variables.tf#L329) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false |
+| [org_policies](variables.tf#L157) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} |
+| [parent](variables.tf#L184) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null |
+| [prefix](variables.tf#L194) | Optional prefix used to generate project id and name. | string | | null |
+| [project_create](variables.tf#L204) | Create project. When set to false, uses a data source to reference existing project. | bool | | true |
+| [service_config](variables.tf#L210) | Configure service API activation. | object({…}) | | {…} |
+| [service_encryption_key_ids](variables.tf#L222) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} |
+| [service_perimeter_bridges](variables.tf#L229) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null |
+| [service_perimeter_standard](variables.tf#L236) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null |
+| [services](variables.tf#L242) | Service APIs to enable. | list(string) | | [] |
+| [shared_vpc_host_config](variables.tf#L248) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null |
+| [shared_vpc_service_config](variables.tf#L257) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} |
+| [skip_delete](variables.tf#L285) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false |
| [tag_bindings](variables-tags.tf#L45) | Tag bindings for this project, in key => tag value id format. | map(string) | | null |
| [tags](variables-tags.tf#L51) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} |
diff --git a/modules/project/iam.tf b/modules/project/iam.tf
index dfd14473c..09c854658 100644
--- a/modules/project/iam.tf
+++ b/modules/project/iam.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-# tfdoc:file:description Generic and OSLogin-specific IAM bindings and roles.
+# tfdoc:file:description IAM bindings.
# IAM notes:
# - external users need to have accepted the invitation email to join
@@ -26,10 +26,11 @@ locals {
file("${var.factories_config.custom_roles}/${f}")
)
}
- _group_iam_roles = distinct(flatten(values(var.group_iam)))
- _group_iam = {
- for r in local._group_iam_roles : r => [
- for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
]
}
custom_roles = merge(
@@ -47,10 +48,10 @@ locals {
}
)
iam = {
- for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
role => concat(
try(var.iam[role], []),
- try(local._group_iam[role], [])
+ try(local._iam_principals[role], [])
)
}
}
diff --git a/modules/project/variables-iam.tf b/modules/project/variables-iam.tf
new file mode 100644
index 000000000..c8980ebf9
--- /dev/null
+++ b/modules/project/variables-iam.tf
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "iam" {
+ description = "Authoritative IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
diff --git a/modules/project/variables.tf b/modules/project/variables.tf
index 009fc5eb0..85ad34c0e 100644
--- a/modules/project/variables.tf
+++ b/modules/project/variables.tf
@@ -76,50 +76,6 @@ variable "factories_config" {
default = {}
}
-variable "group_iam" {
- description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam" {
- description = "Authoritative IAM bindings in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam_bindings" {
- description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "iam_bindings_additive" {
- description = "Individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
variable "labels" {
description = "Resource labels."
type = map(string)
diff --git a/modules/source-repository/README.md b/modules/source-repository/README.md
index c60ba7e46..31c384da2 100644
--- a/modules/source-repository/README.md
+++ b/modules/source-repository/README.md
@@ -65,9 +65,10 @@ module "repo" {
| name | description | resources |
|---|---|---|
-| [iam.tf](./iam.tf) | IAM resources. | google_sourcerepo_repository_iam_binding · google_sourcerepo_repository_iam_member |
+| [iam.tf](./iam.tf) | IAM bindings. | google_sourcerepo_repository_iam_binding · google_sourcerepo_repository_iam_member |
| [main.tf](./main.tf) | Module-level locals and resources. | google_cloudbuild_trigger · google_sourcerepo_repository |
| [outputs.tf](./outputs.tf) | Module outputs. | |
+| [variables-iam.tf](./variables-iam.tf) | None | |
| [variables.tf](./variables.tf) | Module variables. | |
| [versions.tf](./versions.tf) | Version pins. | |
@@ -75,13 +76,13 @@ module "repo" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L61) | Repository name. | string | ✓ | |
-| [project_id](variables.tf#L66) | Project used for resources. | string | ✓ | |
-| [group_iam](variables.tf#L17) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} |
-| [iam](variables.tf#L24) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L31) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L46) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [triggers](variables.tf#L71) | Cloud Build triggers. | map(object({…})) | | {} |
+| [name](variables.tf#L17) | Repository name. | string | ✓ | |
+| [project_id](variables.tf#L22) | Project used for resources. | string | ✓ | |
+| [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [triggers](variables.tf#L27) | Cloud Build triggers. | map(object({…})) | | {} |
## Outputs
diff --git a/modules/source-repository/iam.tf b/modules/source-repository/iam.tf
index 1b225d1b2..43de49231 100644
--- a/modules/source-repository/iam.tf
+++ b/modules/source-repository/iam.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,20 +14,21 @@
* limitations under the License.
*/
-# tfdoc:file:description IAM resources.
+# tfdoc:file:description IAM bindings.
locals {
- _group_iam_roles = distinct(flatten(values(var.group_iam)))
- _group_iam = {
- for r in local._group_iam_roles : r => [
- for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
]
}
iam = {
- for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
role => concat(
try(var.iam[role], []),
- try(local._group_iam[role], [])
+ try(local._iam_principals[role], [])
)
}
}
diff --git a/modules/source-repository/variables-iam.tf b/modules/source-repository/variables-iam.tf
new file mode 100644
index 000000000..4299d4554
--- /dev/null
+++ b/modules/source-repository/variables-iam.tf
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "iam" {
+ description = "IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
diff --git a/modules/source-repository/variables.tf b/modules/source-repository/variables.tf
index 23bfa789e..a467f9fe0 100644
--- a/modules/source-repository/variables.tf
+++ b/modules/source-repository/variables.tf
@@ -14,50 +14,6 @@
* limitations under the License.
*/
-variable "group_iam" {
- description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam" {
- description = "IAM bindings in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "iam_bindings" {
- description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "iam_bindings_additive" {
- description = "Individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
variable "name" {
description = "Repository name."
type = string
diff --git a/tests/fast/stages/s0_bootstrap/checklist.tfvars b/tests/fast/stages/s0_bootstrap/checklist.tfvars
index 50d24a3db..5b97f548a 100644
--- a/tests/fast/stages/s0_bootstrap/checklist.tfvars
+++ b/tests/fast/stages/s0_bootstrap/checklist.tfvars
@@ -6,6 +6,7 @@ organization = {
billing_account = {
id = "000000-111111-222222"
}
+essential_contacts = "gcp-organization-admins@fast.example.com"
factories_config = {
checklist_data = "checklist-data.json"
checklist_org_iam = "checklist-org-iam.json"
diff --git a/tests/fast/stages/s0_bootstrap/simple.tfvars b/tests/fast/stages/s0_bootstrap/simple.tfvars
index 74699bebd..335283c3c 100644
--- a/tests/fast/stages/s0_bootstrap/simple.tfvars
+++ b/tests/fast/stages/s0_bootstrap/simple.tfvars
@@ -6,8 +6,12 @@ organization = {
billing_account = {
id = "000000-111111-222222"
}
-prefix = "fast"
+essential_contacts = "gcp-organization-admins@fast.example.com"
+prefix = "fast"
org_policies_config = {
import_defaults = false
}
outputs_location = "/fast-config"
+groups = {
+ gcp-support = "group:gcp-support@example.com"
+}
diff --git a/tests/fast/stages/s0_bootstrap/simple.yaml b/tests/fast/stages/s0_bootstrap/simple.yaml
index 14738b8a8..073124a3f 100644
--- a/tests/fast/stages/s0_bootstrap/simple.yaml
+++ b/tests/fast/stages/s0_bootstrap/simple.yaml
@@ -12,6 +12,29 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+values:
+ module.organization.google_organization_iam_binding.authoritative["roles/cloudsupport.techSupportEditor"]:
+ condition: []
+ members:
+ - group:gcp-network-admins@fast.example.com
+ - group:gcp-security-admins@fast.example.com
+ - group:gcp-support@example.com
+ org_id: '123456789012'
+ role: roles/cloudsupport.techSupportEditor
+ module.organization.google_organization_iam_binding.authoritative["roles/logging.viewer"]:
+ condition: []
+ members:
+ - group:gcp-support@example.com
+ - serviceAccount:fast-prod-bootstrap-0r@fast-prod-iac-core-0.iam.gserviceaccount.com
+ - serviceAccount:fast-prod-resman-0r@fast-prod-iac-core-0.iam.gserviceaccount.com
+ org_id: '123456789012'
+ role: roles/logging.viewer
+ module.organization.google_organization_iam_binding.authoritative["roles/monitoring.viewer"]:
+ condition: []
+ members:
+ - group:gcp-support@example.com
+ org_id: '123456789012'
+ role: roles/monitoring.viewer
counts:
google_bigquery_dataset: 1
google_bigquery_default_service_account: 3
diff --git a/tests/fast/stages/s2_networking_a_peering/common.tfvars b/tests/fast/stages/s2_networking_a_peering/common.tfvars
index 8add6316e..bdff7a433 100644
--- a/tests/fast/stages/s2_networking_a_peering/common.tfvars
+++ b/tests/fast/stages/s2_networking_a_peering/common.tfvars
@@ -11,7 +11,8 @@ dns = {
resolvers = ["10.10.10.10"]
enable_logging = true
}
-enable_cloud_nat = true
+enable_cloud_nat = true
+essential_contacts = "gcp-network-admins@fast.example.com"
folder_ids = {
networking = null
networking-dev = null
diff --git a/tests/fast/stages/s2_networking_b_vpn/common.tfvars b/tests/fast/stages/s2_networking_b_vpn/common.tfvars
index a285f822f..24d3a8e03 100644
--- a/tests/fast/stages/s2_networking_b_vpn/common.tfvars
+++ b/tests/fast/stages/s2_networking_b_vpn/common.tfvars
@@ -11,7 +11,8 @@ dns = {
resolvers = ["10.10.10.10"]
enable_logging = true
}
-enable_cloud_nat = true
+enable_cloud_nat = true
+essential_contacts = "gcp-network-admins@fast.example.com"
folder_ids = {
networking = null
networking-dev = null
diff --git a/tests/fast/stages/s2_networking_c_nva/common.tfvars b/tests/fast/stages/s2_networking_c_nva/common.tfvars
index 0a60b65d6..fca8913f8 100644
--- a/tests/fast/stages/s2_networking_c_nva/common.tfvars
+++ b/tests/fast/stages/s2_networking_c_nva/common.tfvars
@@ -11,7 +11,8 @@ dns = {
resolvers = ["10.10.10.10"]
enable_logging = true
}
-enable_cloud_nat = true
+enable_cloud_nat = true
+essential_contacts = "gcp-network-admins@fast.example.com"
folder_ids = {
networking = null
networking-dev = null
diff --git a/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars b/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars
index 3900d54d9..071011dd5 100644
--- a/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars
+++ b/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars
@@ -12,7 +12,8 @@ dns = {
prod_resolvers = ["10.20.10.10"]
enable_logging = true
}
-enable_cloud_nat = true
+enable_cloud_nat = true
+essential_contacts = "gcp-network-admins@fast.example.com"
folder_ids = {
networking = null
networking-dev = null
diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/common.tfvars b/tests/fast/stages/s2_networking_e_nva_bgp/common.tfvars
index 0a60b65d6..fca8913f8 100644
--- a/tests/fast/stages/s2_networking_e_nva_bgp/common.tfvars
+++ b/tests/fast/stages/s2_networking_e_nva_bgp/common.tfvars
@@ -11,7 +11,8 @@ dns = {
resolvers = ["10.10.10.10"]
enable_logging = true
}
-enable_cloud_nat = true
+enable_cloud_nat = true
+essential_contacts = "gcp-network-admins@fast.example.com"
folder_ids = {
networking = null
networking-dev = null
diff --git a/tests/fast/stages/s2_security/common.tfvars b/tests/fast/stages/s2_security/common.tfvars
index 6fbb60b64..2b2269861 100644
--- a/tests/fast/stages/s2_security/common.tfvars
+++ b/tests/fast/stages/s2_security/common.tfvars
@@ -4,6 +4,7 @@ automation = {
billing_account = {
id = "000000-111111-222222"
}
+essential_contacts = "gcp-security-admins@fast.example.com"
folder_ids = {
security = null
}
diff --git a/tests/modules/dataplex_datascan/datascan_test_inputs.tfvars b/tests/modules/dataplex_datascan/datascan_test_inputs.tfvars
index 7fa71450f..58f4870c8 100644
--- a/tests/modules/dataplex_datascan/datascan_test_inputs.tfvars
+++ b/tests/modules/dataplex_datascan/datascan_test_inputs.tfvars
@@ -1,4 +1,4 @@
-# Copyright 2023 Google LLC
+# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -26,8 +26,8 @@ iam = {
"user:user@example.com",
]
}
-group_iam = {
- "user-group@example.com" = [
+iam_by_principals = {
+ "group:user-group@example.com" = [
"roles/dataplex.dataScanEditor"
]
}
@@ -113,4 +113,4 @@ data_quality_spec = {
}
}
]
-}
\ No newline at end of file
+}