Extend FAST to support different principal types (#2064)
* add doc draft * typos * typo * typo * typos * rewording * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * move iam variables to a separate file * move billing-account module to iam_principals * move data-catalog-policy-tag module to iam_principals * move dataplex-datascan module to iam_principals * move dataproc module to iam_principals * move folder module to iam_principals * copyright * move organization module to iam_principals * move project module to iam_principals * move source-repository module to iam_principals * update blueprints for iam_principals interface * FAST bootstrap * module READMEs fixes * FAST bootstrap * FAST networking stages * FAST security stage * FAST gke stage * FAST multitenant bootstrap stage * FAST multitenant resman stage * tfdoc * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Update 0-domainless-iam.md * fix module test * Update 0-domainless-iam.md * Update 0-domainless-iam.md * Rename iam_principals to iam_by_principals * Update IAM template to include iam_by_principals * Update Resman README * Fix ADR link format --------- Co-authored-by: Julio Castillo <jccb@google.com>
This commit is contained in:
parent
3397d4cd52
commit
71a64487d5
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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. | <code>map(any)</code> | | <code>{}</code> |
|
||||
| [fleet_features](variables.tf#L111) | Enable and configure fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | <code title="object({ appdevexperience = optional(bool, false) configmanagement = optional(bool, false) identityservice = optional(bool, false) multiclusteringress = optional(string, null) multiclusterservicediscovery = optional(bool, false) servicemesh = optional(bool, false) })">object({…})</code> | | <code>null</code> |
|
||||
| [fleet_workload_identity](variables.tf#L124) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | <code>bool</code> | | <code>false</code> |
|
||||
| [group_iam](variables.tf#L136) | Project-level IAM bindings for groups. Use group emails as keys, list of roles as values. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L143) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L136) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [labels](variables.tf#L150) | Project-level labels. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [nodepools](variables.tf#L156) | Nodepools configuration. Refer to the gke-nodepool module for type details. | <code title="map(map(object({ gke_version = optional(string) labels = optional(map(string), {}) max_pods_per_node = optional(number) name = optional(string) node_config = optional(any, { disk_type = "pd-balanced" }) node_count = optional(map(number), { initial = 1 }) node_locations = optional(list(string)) nodepool_config = optional(any) pod_range = optional(any) reservation_affinity = optional(any) service_account = optional(any) sole_tenant_nodegroup = optional(string) tags = optional(list(string)) taints = optional(map(object({ value = string effect = string }))) })))">map(map(object({…})))</code> | | <code>{}</code> |
|
||||
| [project_services](variables.tf#L195) | Additional project services to enable. | <code>list(string)</code> | | <code>[]</code> |
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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. | <code title="object({ outputs_bucket = string project_id = string project_number = string federated_identity_pool = string federated_identity_providers = map(object({ audiences = list(string) issuer = string issuer_uri = string name = string principal_branch = string principal_repo = string })) })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [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`. | <code title="object({ id = string is_org_level = optional(bool, true) no_iam = optional(bool, false) })">object({…})</code> | ✓ | | |
|
||||
| [organization](variables.tf#L214) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L230) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [tag_keys](variables.tf#L253) | Organization tag keys. | <code title="object({ context = string environment = string tenant = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [tag_names](variables.tf#L264) | Customized names for resource management tags. | <code title="object({ context = string environment = string tenant = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [tag_values](variables.tf#L275) | Organization resource management tag values. | <code>map(string)</code> | ✓ | | <code>1-resman</code> |
|
||||
| [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. | <code title="object({ descriptive_name = string groups = object({ gcp-admins = string gcp-devops = optional(string) gcp-network-admins = optional(string) gcp-security-admins = optional(string) }) short_name = string short_name_is_prefix = optional(bool, false) fast_features = optional(object({ data_platform = optional(bool) gke = optional(bool) project_factory = optional(bool) sandbox = optional(bool) teams = optional(bool) }), {}) locations = optional(object({ bq = optional(string) gcs = optional(string) logging = optional(string) pubsub = optional(list(string)) }), {}) })">object({…})</code> | ✓ | | |
|
||||
| [organization](variables.tf#L215) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L231) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [tag_keys](variables.tf#L254) | Organization tag keys. | <code title="object({ context = string environment = string tenant = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [tag_names](variables.tf#L265) | Customized names for resource management tags. | <code title="object({ context = string environment = string tenant = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [tag_values](variables.tf#L276) | Organization resource management tag values. | <code>map(string)</code> | ✓ | | <code>1-resman</code> |
|
||||
| [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. | <code title="object({ descriptive_name = string groups = object({ gcp-admins = string gcp-devops = optional(string) gcp-network-admins = optional(string) gcp-security-admins = optional(string) }) short_name = string short_name_is_prefix = optional(bool, false) fast_features = optional(object({ data_platform = optional(bool) gke = optional(bool) project_factory = optional(bool) sandbox = optional(bool) teams = optional(bool) }), {}) locations = optional(object({ bq = optional(string) gcs = optional(string) logging = optional(string) pubsub = optional(list(string)) }), {}) })">object({…})</code> | ✓ | | |
|
||||
| [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. | <code title="object({ bootstrap = optional(object({ branch = optional(string) identity_provider = string name = string type = string })) resman = optional(object({ branch = optional(string) identity_provider = string name = string type = string })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [custom_roles](variables.tf#L95) | Custom roles defined at the organization level, in key => id format. | <code title="object({ service_project_network_admin = string tenant_network_admin = string })">object({…})</code> | | <code>null</code> | <code>0-bootstrap</code> |
|
||||
| [fast_features](variables.tf#L105) | Selective control for top-level FAST features. | <code title="object({ data_platform = optional(bool, true) gke = optional(bool, true) project_factory = optional(bool, true) sandbox = optional(bool, true) teams = optional(bool, true) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [federated_identity_providers](variables.tf#L119) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | <code title="map(object({ attribute_condition = optional(string) issuer = string custom_settings = optional(object({ issuer_uri = optional(string) audiences = optional(list(string), []) }), {}) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [group_iam](variables.tf#L133) | Tenant-level custom group IAM settings in group => [roles] format. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [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. | <code title="object({ gcp-devops = optional(string) gcp-network-admins = optional(string) gcp-security-admins = optional(string) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [iam](variables.tf#L152) | Tenant-level custom IAM settings in role => [principal] format. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [iam_bindings_additive](variables.tf#L158) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [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. | <code title="object({ bq = string gcs = string logging = string pubsub = list(string) })">object({…})</code> | | <code title="{ bq = "EU" gcs = "EU" logging = "global" pubsub = [] }">{…}</code> | <code>0-bootstrap</code> |
|
||||
| [log_sinks](variables.tf#L193) | Tenant-level log sinks, in name => {type, filter} format. | <code title="map(object({ filter = string type = string }))">map(object({…}))</code> | | <code title="{ audit-logs = { filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\"" type = "logging" } }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L224) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [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. | <code title="object({ automation = string logging = string })">object({…})</code> | | <code title="{ automation = null logging = null }">{…}</code> | |
|
||||
| [test_principal](variables.tf#L323) | Used when testing to bypass the data source returning the current identity. | <code>string</code> | | <code>null</code> | |
|
||||
| [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. | <code title="object({ gcp-devops = optional(string, "gcp-devops") gcp-network-admins = optional(string, "gcp-network-admins") gcp-security-admins = optional(string, "gcp-security-admins") })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [iam](variables.tf#L146) | Tenant-level custom IAM settings in role => [principal] format. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [iam_bindings_additive](variables.tf#L152) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [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. | <code title="object({ bq = string gcs = string logging = string pubsub = list(string) })">object({…})</code> | | <code title="{ bq = "EU" gcs = "EU" logging = "global" pubsub = [] }">{…}</code> | <code>0-bootstrap</code> |
|
||||
| [log_sinks](variables.tf#L194) | Tenant-level log sinks, in name => {type, filter} format. | <code title="map(object({ filter = string type = string }))">map(object({…}))</code> | | <code title="{ audit-logs = { filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\"" type = "logging" } }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L225) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [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. | <code title="object({ automation = string logging = string })">object({…})</code> | | <code title="{ automation = null logging = null }">{…}</code> | |
|
||||
| [test_principal](variables.tf#L324) | Used when testing to bypass the data source returning the current identity. | <code>string</code> | | <code>null</code> | |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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
|
||||
]
|
||||
: []
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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. | <code>string</code> | | <code>"data"</code> | |
|
||||
| [factories_config](variables.tf#L160) | Configuration for the organization policies factory. | <code title="object({ org_policy = optional(string, "data/org-policies") })">object({…})</code> | | <code>{}</code> | |
|
||||
| [fast_features](variables.tf#L169) | Selective control for top-level FAST features. | <code title="object({ data_platform = optional(bool, false) gke = optional(bool, false) project_factory = optional(bool, false) sandbox = optional(bool, false) teams = optional(bool, false) })">object({…})</code> | | <code>{}</code> | <code>0-0-bootstrap</code> |
|
||||
| [groups](variables.tf#L183) | Group names to grant organization-level permissions. | <code title="object({ gcp-devops = optional(string) gcp-network-admins = optional(string) gcp-security-admins = optional(string) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [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. | <code title="object({ gcp-devops = optional(string) gcp-network-admins = optional(string) gcp-security-admins = optional(string) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [locations](variables.tf#L196) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object({ bq = string gcs = string logging = string pubsub = list(string) })">object({…})</code> | | <code title="{ bq = "EU" gcs = "EU" logging = "global" pubsub = [] }">{…}</code> | <code>0-bootstrap</code> |
|
||||
| [outputs_location](variables.tf#L224) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [team_folders](variables.tf#L268) | Team folders to be created. Format is described in a code comment. | <code title="map(object({ descriptive_name = string group_iam = map(list(string)) impersonation_groups = list(string) }))">map(object({…}))</code> | | <code>null</code> | |
|
||||
| [team_folders](variables.tf#L268) | Team folders to be created. Format is described in a code comment. | <code title="map(object({ descriptive_name = string iam_by_principals = optional(map(list(string)), {}) impersonation_principals = optional(list(string), []) }))">map(object({…}))</code> | | <code>null</code> | |
|
||||
| [test_skip_data_sources](variables.tf#L278) | Used when testing to bypass data sources. | <code>bool</code> | | <code>false</code> | |
|
||||
|
||||
## Outputs
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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`. | <code title="object({ id = string is_org_level = optional(bool, true) no_iam = optional(bool, false) })">object({…})</code> | ✓ | | |
|
||||
| [organization](variables.tf#L245) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | |
|
||||
| [prefix](variables.tf#L260) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | |
|
||||
| [organization](variables.tf#L242) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | |
|
||||
| [prefix](variables.tf#L257) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | |
|
||||
| [bootstrap_user](variables.tf#L27) | Email of the nominal user running this stage for the first time. | <code>string</code> | | <code>null</code> | |
|
||||
| [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. | <code title="object({ bootstrap = optional(object({ name = string type = string branch = optional(string) identity_provider = optional(string) })) resman = optional(object({ name = string type = string branch = optional(string) identity_provider = optional(string) })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [custom_roles](variables.tf#L79) | Map of role names => list of permissions to additionally create at the organization level. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [factories_config](variables.tf#L86) | Configuration for the resource factories or external data. | <code title="object({ checklist_data = optional(string) checklist_org_iam = optional(string) custom_roles = optional(string, "data/custom-roles") org_policy = optional(string, "data/org-policies") })">object({…})</code> | | <code>{}</code> | |
|
||||
| [fast_features](variables.tf#L98) | Selective control for top-level FAST features. | <code title="object({ data_platform = optional(bool, false) gke = optional(bool, false) project_factory = optional(bool, false) sandbox = optional(bool, false) teams = optional(bool, false) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [federated_identity_providers](variables.tf#L111) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | <code title="map(object({ attribute_condition = optional(string) issuer = string custom_settings = optional(object({ issuer_uri = optional(string) audiences = optional(list(string), []) jwks_json = optional(string) }), {}) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [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. | <code title="object({ gcp-billing-admins = string gcp-devops = string gcp-network-admins = string gcp-organization-admins = string gcp-security-admins = string gcp-support = string })">object({…})</code> | | <code title="{ 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-devops" }">{…}</code> | |
|
||||
| [iam](variables.tf#L163) | Organization-level custom IAM settings in role => [principal] format. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [iam_bindings_additive](variables.tf#L170) | Organization-level custom additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [locations](variables.tf#L185) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object({ bq = optional(string, "EU") gcs = optional(string, "EU") logging = optional(string, "global") pubsub = optional(list(string), []) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [log_sinks](variables.tf#L199) | Org-level log sinks, in name => {type, filter} format. | <code title="map(object({ filter = string type = string }))">map(object({…}))</code> | | <code title="{ audit-logs = { filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\" OR protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.TransparencyLog\"" type = "logging" } vpc-sc = { filter = "protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata\"" type = "logging" } workspace-audit-logs = { filter = "logName:\"/logs/cloudaudit.googleapis.com%2Fdata_access\" and protoPayload.serviceName:\"login.googleapis.com\"" type = "logging" } }">{…}</code> | |
|
||||
| [org_policies_config](variables.tf#L228) | Organization policies customization. | <code title="object({ constraints = optional(object({ allowed_policy_member_domains = optional(list(string), []) }), {}) import_defaults = optional(bool, false) tag_name = optional(string, "org-policies") tag_values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) id = optional(string) })), {}) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [outputs_location](variables.tf#L254) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [project_parent_ids](variables.tf#L269) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | <code title="object({ automation = string billing = string logging = string })">object({…})</code> | | <code title="{ automation = null billing = null logging = null }">{…}</code> | |
|
||||
| [essential_contacts](variables.tf#L86) | Email used for essential contacts, unset if null. | <code>string</code> | | <code>null</code> | |
|
||||
| [factories_config](variables.tf#L92) | Configuration for the resource factories or external data. | <code title="object({ checklist_data = optional(string) checklist_org_iam = optional(string) custom_roles = optional(string, "data/custom-roles") org_policy = optional(string, "data/org-policies") })">object({…})</code> | | <code>{}</code> | |
|
||||
| [fast_features](variables.tf#L104) | Selective control for top-level FAST features. | <code title="object({ data_platform = optional(bool, false) gke = optional(bool, false) project_factory = optional(bool, false) sandbox = optional(bool, false) teams = optional(bool, false) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [federated_identity_providers](variables.tf#L117) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | <code title="map(object({ attribute_condition = optional(string) issuer = string custom_settings = optional(object({ issuer_uri = optional(string) audiences = optional(list(string), []) jwks_json = optional(string) }), {}) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [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. | <code title="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-devops") })">object({…})</code> | | <code>{}</code> | |
|
||||
| [iam](variables.tf#L153) | Organization-level custom IAM settings in role => [principal] format. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [iam_bindings_additive](variables.tf#L160) | Organization-level custom additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [locations](variables.tf#L182) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object({ bq = optional(string, "EU") gcs = optional(string, "EU") logging = optional(string, "global") pubsub = optional(list(string), []) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [log_sinks](variables.tf#L196) | Org-level log sinks, in name => {type, filter} format. | <code title="map(object({ filter = string type = string }))">map(object({…}))</code> | | <code title="{ audit-logs = { filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\" OR protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.TransparencyLog\"" type = "logging" } vpc-sc = { filter = "protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata\"" type = "logging" } workspace-audit-logs = { filter = "logName:\"/logs/cloudaudit.googleapis.com%2Fdata_access\" and protoPayload.serviceName:\"login.googleapis.com\"" type = "logging" } }">{…}</code> | |
|
||||
| [org_policies_config](variables.tf#L225) | Organization policies customization. | <code title="object({ constraints = optional(object({ allowed_policy_member_domains = optional(list(string), []) }), {}) import_defaults = optional(bool, false) tag_name = optional(string, "org-policies") tag_values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) id = optional(string) })), {}) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [outputs_location](variables.tf#L251) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [project_parent_ids](variables.tf#L266) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | <code title="object({ automation = string billing = string logging = string })">object({…})</code> | | <code title="{ automation = null billing = null logging = null }">{…}</code> | |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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. | <code title="object({ service_project_network_admin = string storage_viewer = string })">object({…})</code> | | <code>null</code> | <code>0-bootstrap</code> |
|
||||
| [factories_config](variables.tf#L145) | Configuration for the resource factories or external data. | <code title="object({ checklist_data = optional(string) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [fast_features](variables.tf#L154) | Selective control for top-level FAST features. | <code title="object({ data_platform = optional(bool, false) gke = optional(bool, false) project_factory = optional(bool, false) sandbox = optional(bool, false) teams = optional(bool, false) })">object({…})</code> | | <code>{}</code> | <code>0-0-bootstrap</code> |
|
||||
| [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. | <code title="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) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [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. | <code title="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") })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [locations](variables.tf#L183) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object({ bq = string gcs = string logging = string pubsub = list(string) })">object({…})</code> | | <code title="{ bq = "EU" gcs = "EU" logging = "global" pubsub = [] }">{…}</code> | <code>0-bootstrap</code> |
|
||||
| [org_policy_tags](variables.tf#L201) | Resource management tags for organization policy exceptions. | <code title="object({ key_id = optional(string) key_name = optional(string) values = optional(map(string), {}) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [outputs_location](variables.tf#L223) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [tag_names](variables.tf#L240) | Customized names for resource management tags. | <code title="object({ context = optional(string, "context") environment = optional(string, "environment") tenant = optional(string, "tenant") })">object({…})</code> | | <code>{}</code> | |
|
||||
| [tags](variables.tf#L255) | Custome secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | <code title="map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) id = optional(string) })), {}) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [team_folders](variables.tf#L276) | Team folders to be created. Format is described in a code comment. | <code title="map(object({ descriptive_name = string group_iam = map(list(string)) impersonation_groups = list(string) cicd = optional(object({ branch = string identity_provider = string name = string type = string })) }))">map(object({…}))</code> | | <code>null</code> | |
|
||||
| [tenants](variables.tf#L292) | Lightweight tenant definitions. | <code title="map(object({ admin_group_email = string descriptive_name = string billing_account = optional(string) organization = optional(object({ customer_id = string domain = string id = number })) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [team_folders](variables.tf#L276) | Team folders to be created. Format is described in a code comment. | <code title="map(object({ descriptive_name = string iam_by_principals = map(list(string)) impersonation_principals = list(string) cicd = optional(object({ branch = string identity_provider = string name = string type = string })) }))">map(object({…}))</code> | | <code>null</code> | |
|
||||
| [tenants](variables.tf#L292) | Lightweight tenant definitions. | <code title="map(object({ admin_principal = string descriptive_name = string billing_account = optional(string) organization = optional(object({ customer_id = string domain = string id = number })) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [tenants_config](variables.tf#L308) | Lightweight tenants shared configuration. Roles will be assigned to tenant admin group and service accounts. | <code title="object({ core_folder_roles = optional(list(string), []) tenant_folder_roles = optional(list(string), []) top_folder_roles = optional(list(string), []) })">object({…})</code> | | <code>{}</code> | |
|
||||
|
||||
## Outputs
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
])
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. | <code title="object({ outputs_bucket = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object({ id = string is_org_level = optional(bool, true) })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L110) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L130) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L146) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L116) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L126) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L142) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | <code title="object({ vpn_tunnel_established = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) user_labels = optional(map(string), {}) })) vpn_tunnel_bandwidth = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) threshold_mbys = optional(string, "187.5") user_labels = optional(map(string), {}) })) })">object({…})</code> | | <code title="{ vpn_tunnel_established = {} vpn_tunnel_bandwidth = {} }">{…}</code> | |
|
||||
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | <code title="object({ service_project_network_admin = string })">object({…})</code> | | <code>null</code> | <code>0-bootstrap</code> |
|
||||
| [dns](variables.tf#L72) | DNS configuration. | <code title="object({ enable_logging = optional(bool, true) resolvers = optional(list(string), []) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [enable_cloud_nat](variables.tf#L82) | Deploy Cloud NAT. | <code>bool</code> | | <code>false</code> | |
|
||||
| [factories_config](variables.tf#L89) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [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. | <code title="object({ gcp-network-admins = optional(string) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [outputs_location](variables.tf#L140) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [essential_contacts](variables.tf#L89) | Email used for essential contacts, unset if null. | <code>string</code> | | <code>null</code> | |
|
||||
| [factories_config](variables.tf#L95) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L136) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [peering_configs](variables-peerings.tf#L19) | Peering configurations. | <code title="object({ dev = optional(object({ export = optional(bool, true) import = optional(bool, true) public_export = optional(bool) public_import = optional(bool) }), {}) prod = optional(object({ export = optional(bool, true) import = optional(bool, true) public_export = optional(bool) public_import = optional(bool) }), {}) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [psa_ranges](variables.tf#L157) | IP ranges used for Private Service Access (CloudSQL, etc.). | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L176) | Region definitions. | <code title="object({ primary = string secondary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" secondary = "europe-west4" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L188) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_onprem_primary_config](variables.tf#L202) | VPN gateway configuration for onprem interconnection in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L153) | IP ranges used for Private Service Access (CloudSQL, etc.). | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L172) | Region definitions. | <code title="object({ primary = string secondary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" secondary = "europe-west4" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L184) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_onprem_primary_config](variables.tf#L198) | VPN gateway configuration for onprem interconnection in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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. | <code title="object({ outputs_bucket = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object({ id = string is_org_level = optional(bool, true) })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L110) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L130) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L146) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L116) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L126) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L142) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | <code title="object({ vpn_tunnel_established = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) user_labels = optional(map(string), {}) })) vpn_tunnel_bandwidth = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) threshold_mbys = optional(string, "187.5") user_labels = optional(map(string), {}) })) })">object({…})</code> | | <code title="{ vpn_tunnel_established = {} vpn_tunnel_bandwidth = {} }">{…}</code> | |
|
||||
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | <code title="object({ service_project_network_admin = string })">object({…})</code> | | <code>null</code> | <code>0-bootstrap</code> |
|
||||
| [dns](variables.tf#L72) | DNS configuration. | <code title="object({ enable_logging = optional(bool, true) resolvers = optional(list(string), []) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [enable_cloud_nat](variables.tf#L82) | Deploy Cloud NAT. | <code>bool</code> | | <code>false</code> | |
|
||||
| [factories_config](variables.tf#L89) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [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. | <code title="object({ gcp-network-admins = optional(string) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [outputs_location](variables.tf#L140) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L157) | IP ranges used for Private Service Access (CloudSQL, etc.). | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L176) | Region definitions. | <code title="object({ primary = string secondary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" secondary = "europe-west4" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L188) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [essential_contacts](variables.tf#L89) | Email used for essential contacts, unset if null. | <code>string</code> | | <code>null</code> | |
|
||||
| [factories_config](variables.tf#L95) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L136) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L153) | IP ranges used for Private Service Access (CloudSQL, etc.). | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L172) | Region definitions. | <code title="object({ primary = string secondary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" secondary = "europe-west4" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L184) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_configs](variables-vpn.tf#L17) | Hub to spokes VPN configurations. | <code title="object({ dev = optional(object({ asn = optional(number, 65501) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }), {}) landing = optional(object({ asn = optional(number, 65500) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }), {}) prod = optional(object({ asn = optional(number, 65502) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }), {}) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [vpn_onprem_primary_config](variables.tf#L202) | VPN gateway configuration for onprem interconnection in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [vpn_onprem_primary_config](variables.tf#L198) | VPN gateway configuration for onprem interconnection in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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. | <code title="object({ outputs_bucket = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object({ id = string is_org_level = optional(bool, true) })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L110) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L153) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L169) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L116) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L149) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L165) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | <code title="object({ vpn_tunnel_established = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) user_labels = optional(map(string), {}) })) vpn_tunnel_bandwidth = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) threshold_mbys = optional(string, "187.5") user_labels = optional(map(string), {}) })) })">object({…})</code> | | <code title="{ vpn_tunnel_established = {} vpn_tunnel_bandwidth = {} }">{…}</code> | |
|
||||
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | <code title="object({ service_project_network_admin = string })">object({…})</code> | | <code>null</code> | <code>0-bootstrap</code> |
|
||||
| [dns](variables.tf#L72) | DNS configuration. | <code title="object({ enable_logging = optional(bool, true) resolvers = optional(list(string), []) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [enable_cloud_nat](variables.tf#L82) | Deploy Cloud NAT. | <code>bool</code> | | <code>false</code> | |
|
||||
| [factories_config](variables.tf#L89) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [gcp_ranges](variables.tf#L120) | GCP address ranges in name => range format. | <code>map(string)</code> | | <code title="{ gcp_dev_primary = "10.68.0.0/16" gcp_dev_secondary = "10.84.0.0/16" gcp_landing_trusted_primary = "10.64.0.0/17" gcp_landing_trusted_secondary = "10.80.0.0/17" gcp_landing_untrusted_primary = "10.64.127.0/17" gcp_landing_untrusted_secondary = "10.80.127.0/17" gcp_prod_primary = "10.72.0.0/16" gcp_prod_secondary = "10.88.0.0/16" }">{…}</code> | |
|
||||
| [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. | <code title="object({ gcp-network-admins = optional(string) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [onprem_cidr](variables.tf#L145) | Onprem addresses in name => range format. | <code>map(string)</code> | | <code title="{ main = "10.0.0.0/24" }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L163) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L180) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L199) | Region definitions. | <code title="object({ primary = string secondary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" secondary = "europe-west4" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L211) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_onprem_primary_config](variables.tf#L225) | VPN gateway configuration for onprem interconnection in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [vpn_onprem_secondary_config](variables.tf#L268) | VPN gateway configuration for onprem interconnection in the secondary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [essential_contacts](variables.tf#L89) | Email used for essential contacts, unset if null. | <code>string</code> | | <code>null</code> | |
|
||||
| [factories_config](variables.tf#L95) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [gcp_ranges](variables.tf#L126) | GCP address ranges in name => range format. | <code>map(string)</code> | | <code title="{ gcp_dev_primary = "10.68.0.0/16" gcp_dev_secondary = "10.84.0.0/16" gcp_landing_trusted_primary = "10.64.0.0/17" gcp_landing_trusted_secondary = "10.80.0.0/17" gcp_landing_untrusted_primary = "10.64.127.0/17" gcp_landing_untrusted_secondary = "10.80.127.0/17" gcp_prod_primary = "10.72.0.0/16" gcp_prod_secondary = "10.88.0.0/16" }">{…}</code> | |
|
||||
| [onprem_cidr](variables.tf#L141) | Onprem addresses in name => range format. | <code>map(string)</code> | | <code title="{ main = "10.0.0.0/24" }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L159) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L176) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L195) | Region definitions. | <code title="object({ primary = string secondary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" secondary = "europe-west4" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L207) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_onprem_primary_config](variables.tf#L221) | VPN gateway configuration for onprem interconnection in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [vpn_onprem_secondary_config](variables.tf#L264) | VPN gateway configuration for onprem interconnection in the secondary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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. | <code title="object({ outputs_bucket = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object({ id = string is_org_level = optional(bool, true) })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L111) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L131) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L147) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L117) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L127) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L143) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | <code title="object({ vpn_tunnel_established = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) user_labels = optional(map(string), {}) })) vpn_tunnel_bandwidth = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) threshold_mbys = optional(string, "187.5") user_labels = optional(map(string), {}) })) })">object({…})</code> | | <code title="{ vpn_tunnel_established = {} vpn_tunnel_bandwidth = {} }">{…}</code> | |
|
||||
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | <code title="object({ service_project_network_admin = string })">object({…})</code> | | <code>null</code> | <code>0-bootstrap</code> |
|
||||
| [dns](variables.tf#L72) | DNS configuration. | <code title="object({ dev_resolvers = optional(list(string), []) enable_logging = optional(bool, true) prod_resolvers = optional(list(string), []) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [enable_cloud_nat](variables.tf#L83) | Deploy Cloud NAT. | <code>bool</code> | | <code>false</code> | |
|
||||
| [factories_config](variables.tf#L90) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [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. | <code title="object({ gcp-network-admins = optional(string) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [outputs_location](variables.tf#L141) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L158) | IP ranges used for Private Service Access (e.g. CloudSQL). | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L177) | Region definitions. | <code title="object({ primary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L187) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_onprem_dev_primary_config](variables.tf#L201) | VPN gateway configuration for onprem interconnection from dev in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [vpn_onprem_prod_primary_config](variables.tf#L244) | VPN gateway configuration for onprem interconnection from prod in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [essential_contacts](variables.tf#L90) | Email used for essential contacts, unset if null. | <code>string</code> | | <code>null</code> | |
|
||||
| [factories_config](variables.tf#L96) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L137) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L154) | IP ranges used for Private Service Access (e.g. CloudSQL). | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L173) | Region definitions. | <code title="object({ primary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L183) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_onprem_dev_primary_config](variables.tf#L197) | VPN gateway configuration for onprem interconnection from dev in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [vpn_onprem_prod_primary_config](variables.tf#L240) | VPN gateway configuration for onprem interconnection from prod in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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. | <code title="object({ outputs_bucket = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [billing_account](variables.tf#L50) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object({ id = string is_org_level = optional(bool, true) })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L110) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L164) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L180) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L116) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ networking = string networking-dev = string networking-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L160) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L176) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [alert_config](variables.tf#L17) | Configuration for monitoring alerts. | <code title="object({ vpn_tunnel_established = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) user_labels = optional(map(string), {}) })) vpn_tunnel_bandwidth = optional(object({ auto_close = optional(string, null) duration = optional(string, "120s") enabled = optional(bool, true) notification_channels = optional(list(string), []) threshold_mbys = optional(string, "187.5") user_labels = optional(map(string), {}) })) })">object({…})</code> | | <code title="{ vpn_tunnel_established = {} vpn_tunnel_bandwidth = {} }">{…}</code> | |
|
||||
| [custom_roles](variables.tf#L63) | Custom roles defined at the org level, in key => id format. | <code title="object({ service_project_network_admin = string })">object({…})</code> | | <code>null</code> | <code>0-bootstrap</code> |
|
||||
| [dns](variables.tf#L72) | DNS configuration. | <code title="object({ enable_logging = optional(bool, true) resolvers = optional(list(string), []) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [enable_cloud_nat](variables.tf#L82) | Deploy Cloud NAT. | <code>bool</code> | | <code>false</code> | |
|
||||
| [factories_config](variables.tf#L89) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [gcp_ranges](variables.tf#L120) | GCP address ranges in name => range format. | <code>map(string)</code> | | <code title="{ gcp_dev_primary = "10.68.0.0/16" gcp_dev_secondary = "10.84.0.0/16" gcp_landing_trusted_primary = "10.64.0.0/17" gcp_landing_trusted_secondary = "10.80.0.0/17" gcp_landing_untrusted_primary = "10.64.127.0/17" gcp_landing_untrusted_secondary = "10.80.127.0/17" gcp_prod_primary = "10.72.0.0/16" gcp_prod_secondary = "10.88.0.0/16" }">{…}</code> | |
|
||||
| [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. | <code title="object({ gcp-network-admins = optional(string) })">object({…})</code> | | <code>{}</code> | <code>0-bootstrap</code> |
|
||||
| [ncc_asn](variables.tf#L145) | The NCC Cloud Routers ASN configuration. | <code>map(number)</code> | | <code title="{ nva_primary = 64513 nva_secondary = 64514 trusted = 64515 untrusted = 64512 }">{…}</code> | |
|
||||
| [onprem_cidr](variables.tf#L156) | Onprem addresses in name => range format. | <code>map(string)</code> | | <code title="{ main = "10.0.0.0/24" }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L174) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L191) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L210) | Region definitions. | <code title="object({ primary = string secondary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" secondary = "europe-west4" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L222) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_onprem_primary_config](variables.tf#L236) | VPN gateway configuration for onprem interconnection in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [vpn_onprem_secondary_config](variables.tf#L279) | VPN gateway configuration for onprem interconnection in the secondary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [zones](variables.tf#L322) | Zones in which NVAs are deployed. | <code>list(string)</code> | | <code>["b", "c"]</code> | |
|
||||
| [essential_contacts](variables.tf#L89) | Email used for essential contacts, unset if null. | <code>string</code> | | <code>null</code> | |
|
||||
| [factories_config](variables.tf#L95) | Configuration for network resource factories. | <code title="object({ data_dir = optional(string, "data") dns_policy_rules_file = optional(string, "data/dns-policy-rules.yaml") firewall_policy_name = optional(string, "net-default") })">object({…})</code> | | <code title="{ data_dir = "data" }">{…}</code> | |
|
||||
| [gcp_ranges](variables.tf#L126) | GCP address ranges in name => range format. | <code>map(string)</code> | | <code title="{ gcp_dev_primary = "10.68.0.0/16" gcp_dev_secondary = "10.84.0.0/16" gcp_landing_trusted_primary = "10.64.0.0/17" gcp_landing_trusted_secondary = "10.80.0.0/17" gcp_landing_untrusted_primary = "10.64.127.0/17" gcp_landing_untrusted_secondary = "10.80.127.0/17" gcp_prod_primary = "10.72.0.0/16" gcp_prod_secondary = "10.88.0.0/16" }">{…}</code> | |
|
||||
| [ncc_asn](variables.tf#L141) | The NCC Cloud Routers ASN configuration. | <code>map(number)</code> | | <code title="{ nva_primary = 64513 nva_secondary = 64514 trusted = 64515 untrusted = 64512 }">{…}</code> | |
|
||||
| [onprem_cidr](variables.tf#L152) | Onprem addresses in name => range format. | <code>map(string)</code> | | <code title="{ main = "10.0.0.0/24" }">{…}</code> | |
|
||||
| [outputs_location](variables.tf#L170) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [psa_ranges](variables.tf#L187) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | <code title="object({ dev = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) prod = object({ ranges = map(string) export_routes = optional(bool, false) import_routes = optional(bool, false) peered_domains = optional(list(string), []) }) })">object({…})</code> | | <code>null</code> | |
|
||||
| [regions](variables.tf#L206) | Region definitions. | <code title="object({ primary = string secondary = string })">object({…})</code> | | <code title="{ primary = "europe-west1" secondary = "europe-west4" }">{…}</code> | |
|
||||
| [service_accounts](variables.tf#L218) | Automation service accounts in name => email format. | <code title="object({ data-platform-dev = string data-platform-prod = string gke-dev = string gke-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | | <code>null</code> | <code>1-resman</code> |
|
||||
| [vpn_onprem_primary_config](variables.tf#L232) | VPN gateway configuration for onprem interconnection in the primary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [vpn_onprem_secondary_config](variables.tf#L275) | VPN gateway configuration for onprem interconnection in the secondary region. | <code title="object({ peer_external_gateways = map(object({ redundancy_type = string interfaces = list(string) })) router_config = object({ create = optional(bool, true) asn = number name = optional(string) keepalive = optional(number) custom_advertise = optional(object({ all_subnets = bool ip_ranges = map(string) })) }) tunnels = map(object({ bgp_peer = object({ address = string asn = number route_priority = optional(number, 1000) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool all_peer_vpc_subnets = bool ip_ranges = map(string) })) }) bgp_session_range = string ike_version = optional(number, 2) peer_external_gateway_interface = optional(number) peer_gateway = optional(string, "default") router = optional(string) shared_secret = optional(string) vpn_gateway_interface = number })) })">object({…})</code> | | <code>null</code> | |
|
||||
| [zones](variables.tf#L318) | Zones in which NVAs are deployed. | <code>list(string)</code> | | <code>["b", "c"]</code> | |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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. | <code title="object({ outputs_bucket = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [billing_account](variables.tf#L25) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object({ id = string is_org_level = optional(bool, true) })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L38) | Folder name => id mappings, the 'security' folder name must exist. | <code title="object({ security = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L98) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L114) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [service_accounts](variables.tf#L125) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | <code title="object({ data-platform-dev = string data-platform-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [groups](variables.tf#L46) | Group names to grant organization-level permissions. | <code>map(string)</code> | | <code title="{ 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" }">{…}</code> | <code>0-bootstrap</code> |
|
||||
| [kms_keys](variables.tf#L61) | KMS keys to create, keyed by name. | <code title="map(object({ rotation_period = optional(string, "7776000s") labels = optional(map(string)) locations = optional(list(string), ["europe", "europe-west1", "europe-west3", "global"]) purpose = optional(string, "ENCRYPT_DECRYPT") skip_initial_version_creation = optional(bool, false) version_template = optional(object({ algorithm = string protection_level = optional(string, "SOFTWARE") })) iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) })), {}) iam_bindings_additive = optional(map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) })), {}) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [outputs_location](variables.tf#L108) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [vpc_sc_access_levels](variables.tf#L136) | VPC SC access level definitions. | <code title="map(object({ combining_function = optional(string) conditions = optional(list(object({ device_policy = optional(object({ allowed_device_management_levels = optional(list(string)) allowed_encryption_statuses = optional(list(string)) require_admin_approval = bool require_corp_owned = bool require_screen_lock = optional(bool) os_constraints = optional(list(object({ os_type = string minimum_version = optional(string) require_verified_chrome_os = optional(bool) }))) })) ip_subnetworks = optional(list(string), []) members = optional(list(string), []) negate = optional(bool) regions = optional(list(string), []) required_access_levels = optional(list(string), []) })), []) description = optional(string) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [vpc_sc_egress_policies](variables.tf#L165) | VPC SC egress policy definitions. | <code title="map(object({ from = object({ identity_type = optional(string, "ANY_IDENTITY") identities = optional(list(string)) }) to = object({ operations = optional(list(object({ method_selectors = optional(list(string)) service_name = string })), []) resources = optional(list(string)) resource_type_external = optional(bool, false) }) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [vpc_sc_ingress_policies](variables.tf#L185) | VPC SC ingress policy definitions. | <code title="map(object({ from = object({ access_levels = optional(list(string), []) identity_type = optional(string) identities = optional(list(string)) resources = optional(list(string), []) }) to = object({ operations = optional(list(object({ method_selectors = optional(list(string)) service_name = string })), []) resources = optional(list(string)) }) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [vpc_sc_perimeters](variables.tf#L206) | VPC SC regular perimeter definitions. | <code title="object({ dev = optional(object({ access_levels = optional(list(string), []) egress_policies = optional(list(string), []) ingress_policies = optional(list(string), []) resources = optional(list(string), []) }), {}) landing = optional(object({ access_levels = optional(list(string), []) egress_policies = optional(list(string), []) ingress_policies = optional(list(string), []) resources = optional(list(string), []) }), {}) prod = optional(object({ access_levels = optional(list(string), []) egress_policies = optional(list(string), []) ingress_policies = optional(list(string), []) resources = optional(list(string), []) }), {}) })">object({…})</code> | | <code>{}</code> | |
|
||||
| [folder_ids](variables.tf#L44) | Folder name => id mappings, the 'security' folder name must exist. | <code title="object({ security = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [organization](variables.tf#L89) | Organization details. | <code title="object({ domain = string id = number customer_id = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [prefix](variables.tf#L105) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [service_accounts](variables.tf#L116) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | <code title="object({ data-platform-dev = string data-platform-prod = string project-factory-dev = string project-factory-prod = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [essential_contacts](variables.tf#L38) | Email used for essential contacts, unset if null. | <code>string</code> | | <code>null</code> | |
|
||||
| [kms_keys](variables.tf#L52) | KMS keys to create, keyed by name. | <code title="map(object({ rotation_period = optional(string, "7776000s") labels = optional(map(string)) locations = optional(list(string), ["europe", "europe-west1", "europe-west3", "global"]) purpose = optional(string, "ENCRYPT_DECRYPT") skip_initial_version_creation = optional(bool, false) version_template = optional(object({ algorithm = string protection_level = optional(string, "SOFTWARE") })) iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) })), {}) iam_bindings_additive = optional(map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) })), {}) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [outputs_location](variables.tf#L99) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
| [vpc_sc_access_levels](variables.tf#L127) | VPC SC access level definitions. | <code title="map(object({ combining_function = optional(string) conditions = optional(list(object({ device_policy = optional(object({ allowed_device_management_levels = optional(list(string)) allowed_encryption_statuses = optional(list(string)) require_admin_approval = bool require_corp_owned = bool require_screen_lock = optional(bool) os_constraints = optional(list(object({ os_type = string minimum_version = optional(string) require_verified_chrome_os = optional(bool) }))) })) ip_subnetworks = optional(list(string), []) members = optional(list(string), []) negate = optional(bool) regions = optional(list(string), []) required_access_levels = optional(list(string), []) })), []) description = optional(string) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [vpc_sc_egress_policies](variables.tf#L156) | VPC SC egress policy definitions. | <code title="map(object({ from = object({ identity_type = optional(string, "ANY_IDENTITY") identities = optional(list(string)) }) to = object({ operations = optional(list(object({ method_selectors = optional(list(string)) service_name = string })), []) resources = optional(list(string)) resource_type_external = optional(bool, false) }) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [vpc_sc_ingress_policies](variables.tf#L176) | VPC SC ingress policy definitions. | <code title="map(object({ from = object({ access_levels = optional(list(string), []) identity_type = optional(string) identities = optional(list(string)) resources = optional(list(string), []) }) to = object({ operations = optional(list(object({ method_selectors = optional(list(string)) service_name = string })), []) resources = optional(list(string)) }) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [vpc_sc_perimeters](variables.tf#L197) | VPC SC regular perimeter definitions. | <code title="object({ dev = optional(object({ access_levels = optional(list(string), []) egress_policies = optional(list(string), []) ingress_policies = optional(list(string), []) resources = optional(list(string), []) }), {}) landing = optional(object({ access_levels = optional(list(string), []) egress_policies = optional(list(string), []) ingress_policies = optional(list(string), []) resources = optional(list(string), []) }), {}) prod = optional(object({ access_levels = optional(list(string), []) egress_policies = optional(list(string), []) ingress_policies = optional(list(string), []) resources = optional(list(string), []) }), {}) })">object({…})</code> | | <code>{}</code> | |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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"] }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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. | <code title="object({ outputs_bucket = string })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [billing_account](variables.tf#L29) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object({ id = string is_org_level = optional(bool, true) })">object({…})</code> | ✓ | | <code>0-bootstrap</code> |
|
||||
| [folder_ids](variables.tf#L175) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | <code title="object({ gke-dev = string })">object({…})</code> | ✓ | | <code>1-resman</code> |
|
||||
| [host_project_ids](variables.tf#L190) | Host project for the shared VPC. | <code title="object({ dev-spoke-0 = string })">object({…})</code> | ✓ | | <code>2-networking</code> |
|
||||
| [host_project_ids](variables.tf#L183) | Host project for the shared VPC. | <code title="object({ dev-spoke-0 = string })">object({…})</code> | ✓ | | <code>2-networking</code> |
|
||||
| [prefix](variables.tf#L242) | Prefix used for resources that need unique names. | <code>string</code> | ✓ | | |
|
||||
| [vpc_self_links](variables.tf#L258) | Self link for the shared VPC. | <code title="object({ dev-spoke-0 = string })">object({…})</code> | ✓ | | <code>2-networking</code> |
|
||||
| [clusters](variables.tf#L42) | Clusters configuration. Refer to the gke-cluster-standard module for type details. | <code title="map(object({ cluster_autoscaling = optional(any) description = optional(string) enable_addons = optional(any, { horizontal_pod_autoscaling = true, http_load_balancing = true }) enable_features = optional(any, { shielded_nodes = true workload_identity = true }) issue_client_certificate = optional(bool, false) labels = optional(map(string)) location = string logging_config = optional(object({ enable_system_logs = optional(bool, true) enable_workloads_logs = optional(bool, true) enable_api_server_logs = optional(bool, false) enable_scheduler_logs = optional(bool, false) enable_controller_manager_logs = optional(bool, false) }), {}) maintenance_config = optional(any, { daily_window_start_time = "03:00" recurring_window = null maintenance_exclusion = [] }) max_pods_per_node = optional(number, 110) min_master_version = optional(string) monitoring_config = optional(object({ enable_system_metrics = optional(bool, true) enable_api_server_metrics = optional(bool, false) enable_controller_manager_metrics = optional(bool, false) enable_scheduler_metrics = optional(bool, false) enable_daemonset_metrics = optional(bool, false) enable_deployment_metrics = optional(bool, false) enable_hpa_metrics = optional(bool, false) enable_pod_metrics = optional(bool, false) enable_statefulset_metrics = optional(bool, false) enable_storage_metrics = optional(bool, false) enable_managed_prometheus = optional(bool, true) }), {}) node_locations = optional(list(string)) private_cluster_config = optional(any) release_channel = optional(string) vpc_config = object({ subnetwork = string network = optional(string) secondary_range_blocks = optional(object({ pods = string services = string })) secondary_range_names = optional(object({ pods = optional(string, "pods") services = optional(string, "services") })) master_authorized_ranges = optional(map(string)) master_ipv4_cidr_block = optional(string) }) }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
|
@ -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. | <code title="map(object({ binauthz = bool config_sync = object({ git = object({ gcp_service_account_email = string https_proxy = string policy_dir = string secret_type = string sync_branch = string sync_repo = string sync_rev = string sync_wait_secs = number }) prevent_drift = string source_format = string }) hierarchy_controller = object({ enable_hierarchical_resource_quota = bool enable_pod_tree_labels = bool }) policy_controller = object({ audit_interval_seconds = number exemptable_namespaces = list(string) log_denies_enabled = bool referential_rules_enabled = bool template_library_installed = bool }) version = string }))">map(object({…}))</code> | | <code>{}</code> | |
|
||||
| [fleet_features](variables.tf#L155) | Enable and configure fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | <code title="object({ appdevexperience = bool configmanagement = bool identityservice = bool multiclusteringress = string multiclusterservicediscovery = bool servicemesh = bool })">object({…})</code> | | <code>null</code> | |
|
||||
| [fleet_workload_identity](variables.tf#L168) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | <code>bool</code> | | <code>false</code> | |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [iam](variables.tf#L198) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [iam](variables.tf#L191) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> | |
|
||||
| [labels](variables.tf#L205) | Project-level labels. | <code>map(string)</code> | | <code>{}</code> | |
|
||||
| [nodepools](variables.tf#L211) | Nodepools configuration. Refer to the gke-nodepool module for type details. | <code title="map(map(object({ gke_version = optional(string) labels = optional(map(string), {}) max_pods_per_node = optional(number) name = optional(string) node_config = optional(any, { disk_type = "pd-balanced" }) node_count = optional(map(number), { initial = 1 }) node_locations = optional(list(string)) nodepool_config = optional(any) pod_range = optional(any) reservation_affinity = optional(any) service_account = optional(any) sole_tenant_nodegroup = optional(string) tags = optional(list(string)) taints = optional(map(object({ value = string effect = string }))) })))">map(map(object({…})))</code> | | <code>{}</code> | |
|
||||
| [outputs_location](variables.tf#L236) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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. | <code>string</code> | ✓ | |
|
||||
| [id](variables.tf#L131) | Billing account id. | <code>string</code> | ✓ | |
|
||||
| [budget_notification_channels](variables.tf#L17) | Notification channels used by budget alerts. | <code title="map(object({ project_id = string type = string description = optional(string) display_name = optional(string) enabled = optional(bool, true) force_delete = optional(bool) labels = optional(map(string)) sensitive_labels = optional(list(object({ auth_token = optional(string) password = optional(string) service_key = optional(string) }))) user_labels = optional(map(string)) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [budgets](variables.tf#L47) | Billing budgets. Notification channels are either keys in corresponding variable, or external ids. | <code title="map(object({ amount = object({ currency_code = optional(string) nanos = optional(number) units = optional(number) use_last_period = optional(bool) }) display_name = optional(string) filter = optional(object({ credit_types_treatment = optional(object({ exclude_all = optional(bool) include_specified = optional(list(string)) })) label = optional(object({ key = string value = string })) period = optional(object({ calendar = optional(string) custom = optional(object({ start_date = object({ day = number month = number year = number }) end_date = optional(object({ day = number month = number year = number })) })) })) projects = optional(list(string)) resource_ancestors = optional(list(string)) services = optional(list(string)) subaccounts = optional(list(string)) })) threshold_rules = optional(list(object({ percent = number forecasted_spend = optional(bool) })), []) update_rules = optional(map(object({ disable_default_iam_recipients = optional(bool) monitoring_notification_channels = optional(list(string)) pubsub_topic = optional(string) })), {}) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [factory_config](variables.tf#L121) | Path to folder containing budget alerts data files. | <code title="object({ budgets_data_path = optional(string, "data/billing-budgets") })">object({…})</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L138) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L145) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L160) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [logging_sinks](variables.tf#L180) | Logging sinks to create for the organization. | <code title="map(object({ destination = string type = string bq_partitioned_table = optional(bool, false) description = optional(string) disabled = optional(bool, false) exclusions = optional(map(object({ filter = string description = optional(string) disabled = optional(bool) })), {}) filter = optional(string) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [projects](variables.tf#L213) | Projects associated with this billing account. | <code>list(string)</code> | | <code>[]</code> |
|
||||
| [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [logging_sinks](variables.tf#L136) | Logging sinks to create for the organization. | <code title="map(object({ destination = string type = string bq_partitioned_table = optional(bool, false) description = optional(string) disabled = optional(bool, false) exclusions = optional(map(object({ filter = string description = optional(string) disabled = optional(bool) })), {}) filter = optional(string) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [projects](variables.tf#L169) | Projects associated with this billing account. | <code>list(string)</code> | | <code>[]</code> |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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], [])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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. | <code>string</code> | ✓ | |
|
||||
| [project_id](variables.tf#L92) | GCP project id. | <code>string</code> | ✓ | |
|
||||
| [name](variables.tf#L35) | Name of this taxonomy. | <code>string</code> | ✓ | |
|
||||
| [project_id](variables.tf#L50) | GCP project id. | <code>string</code> | ✓ | |
|
||||
| [activated_policy_types](variables.tf#L17) | A list of policy types that are activated for this taxonomy. | <code>list(string)</code> | | <code>["FINE_GRAINED_ACCESS_CONTROL"]</code> |
|
||||
| [description](variables.tf#L23) | Description of this taxonomy. | <code>string</code> | | <code>"Taxonomy - Terraform managed"</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L35) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L41) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L56) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [location](variables.tf#L71) | Data Catalog Taxonomy location. | <code>string</code> | | <code>"eu"</code> |
|
||||
| [prefix](variables.tf#L82) | Optional prefix used to generate project id and name. | <code>string</code> | | <code>null</code> |
|
||||
| [tags](variables.tf#L97) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | <code title="map(object({ description = optional(string) iam = optional(map(list(string)), {}) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam](variables-iam.tf#L23) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables-iam.tf#L29) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables-iam.tf#L44) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [location](variables.tf#L29) | Data Catalog Taxonomy location. | <code>string</code> | | <code>"eu"</code> |
|
||||
| [prefix](variables.tf#L40) | Optional prefix used to generate project id and name. | <code>string</code> | | <code>null</code> |
|
||||
| [tags](variables.tf#L55) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | <code title="map(object({ description = optional(string) iam = optional(map(list(string)), {}) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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 = {}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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`. | <code title="object({ entity = optional(string) resource = optional(string) })">object({…})</code> | ✓ | |
|
||||
| [name](variables.tf#L162) | Name of Dataplex Scan. | <code>string</code> | ✓ | |
|
||||
| [project_id](variables.tf#L173) | The ID of the project where the Dataplex DataScan will be created. | <code>string</code> | ✓ | |
|
||||
| [region](variables.tf#L178) | Region for the Dataplex DataScan. | <code>string</code> | ✓ | |
|
||||
| [name](variables.tf#L118) | Name of Dataplex Scan. | <code>string</code> | ✓ | |
|
||||
| [project_id](variables.tf#L129) | The ID of the project where the Dataplex DataScan will be created. | <code>string</code> | ✓ | |
|
||||
| [region](variables.tf#L134) | Region for the Dataplex DataScan. | <code>string</code> | ✓ | |
|
||||
| [data_profile_spec](variables.tf#L29) | DataProfileScan related setting. Variable descriptions are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataProfileSpec. | <code title="object({ sampling_percent = optional(number) row_filter = optional(string) })">object({…})</code> | | <code>null</code> |
|
||||
| [data_quality_spec](variables.tf#L38) | DataQualityScan related setting. Variable descriptions are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataQualitySpec. | <code title="object({ sampling_percent = optional(number) row_filter = optional(string) post_scan_actions = optional(object({ bigquery_export = optional(object({ results_table = optional(string) })) })) rules = list(object({ column = optional(string) ignore_null = optional(bool, null) dimension = string threshold = optional(number) non_null_expectation = optional(object({})) range_expectation = optional(object({ min_value = optional(number) max_value = optional(number) strict_min_enabled = optional(bool) strict_max_enabled = optional(bool) })) regex_expectation = optional(object({ regex = string })) set_expectation = optional(object({ values = list(string) })) uniqueness_expectation = optional(object({})) statistic_range_expectation = optional(object({ statistic = string min_value = optional(number) max_value = optional(number) strict_min_enabled = optional(bool) strict_max_enabled = optional(bool) })) row_condition_expectation = optional(object({ sql_expression = string })) table_condition_expectation = optional(object({ sql_expression = string })) })) })">object({…})</code> | | <code>null</code> |
|
||||
| [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. | <code title="object({ path = string })">object({…})</code> | | <code>null</code> |
|
||||
| [description](variables.tf#L93) | Custom description for DataScan. | <code>string</code> | | <code>null</code> |
|
||||
| [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. | <code>string</code> | | <code>null</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L112) | Dataplex DataScan IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L119) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L134) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>string</code> | | <code>null</code> |
|
||||
| [labels](variables.tf#L155) | Resource labels. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [prefix](variables.tf#L167) | Optional prefix used to generate Dataplex DataScan ID. | <code>string</code> | | <code>null</code> |
|
||||
| [iam](variables-iam.tf#L24) | Dataplex DataScan IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables-iam.tf#L31) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables-iam.tf#L46) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [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. | <code>string</code> | | <code>null</code> |
|
||||
| [labels](variables.tf#L111) | Resource labels. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [prefix](variables.tf#L123) | Optional prefix used to generate Dataplex DataScan ID. | <code>string</code> | | <code>null</code> |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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], [])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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], [])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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. | <code>google_org_policy_policy</code> |
|
||||
| [outputs.tf](./outputs.tf) | Module outputs. | |
|
||||
| [tags.tf](./tags.tf) | None | <code>google_tags_tag_binding</code> |
|
||||
| [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. | <code title="object({ org_policies = optional(string) })">object({…})</code> | | <code>{}</code> |
|
||||
| [firewall_policy](variables.tf#L33) | Hierarchical firewall policy to associate to this folder. | <code title="object({ name = string policy = string })">object({…})</code> | | <code>null</code> |
|
||||
| [folder_create](variables.tf#L42) | Create folder. When set to false, uses id to reference an existing folder. | <code>bool</code> | | <code>true</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L55) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L62) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L77) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [id](variables.tf#L92) | Folder ID in case you use folder_create=false. | <code>string</code> | | <code>null</code> |
|
||||
| [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. | <code>map(map(list(string)))</code> | | <code>{}</code> |
|
||||
| [logging_exclusions](variables.tf#L113) | Logging exclusions for this folder in the form {NAME -> FILTER}. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [logging_sinks](variables.tf#L120) | Logging sinks to create for the folder. | <code title="map(object({ bq_partitioned_table = optional(bool, false) description = optional(string) destination = string disabled = optional(bool, false) exclusions = optional(map(string), {}) filter = string iam = optional(bool, true) include_children = optional(bool, true) type = string }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [name](variables.tf#L151) | Folder name. | <code>string</code> | | <code>null</code> |
|
||||
| [org_policies](variables.tf#L157) | Organization policies applied to this folder keyed by policy name. | <code title="map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) rules = optional(list(object({ allow = optional(object({ all = optional(bool) values = optional(list(string)) })) deny = optional(object({ all = optional(bool) values = optional(list(string)) })) enforce = optional(bool) # for boolean policies only. condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) }), {}) })), []) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [parent](variables.tf#L184) | Parent in folders/folder_id or organizations/org_id format. | <code>string</code> | | <code>null</code> |
|
||||
| [tag_bindings](variables.tf#L194) | Tag bindings for this folder, in key => tag value id format. | <code>map(string)</code> | | <code>null</code> |
|
||||
| [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [id](variables.tf#L48) | Folder ID in case you use folder_create=false. | <code>string</code> | | <code>null</code> |
|
||||
| [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. | <code>map(map(list(string)))</code> | | <code>{}</code> |
|
||||
| [logging_exclusions](variables.tf#L69) | Logging exclusions for this folder in the form {NAME -> FILTER}. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [logging_sinks](variables.tf#L76) | Logging sinks to create for the folder. | <code title="map(object({ bq_partitioned_table = optional(bool, false) description = optional(string) destination = string disabled = optional(bool, false) exclusions = optional(map(string), {}) filter = string iam = optional(bool, true) include_children = optional(bool, true) type = string }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [name](variables.tf#L107) | Folder name. | <code>string</code> | | <code>null</code> |
|
||||
| [org_policies](variables.tf#L113) | Organization policies applied to this folder keyed by policy name. | <code title="map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) rules = optional(list(object({ allow = optional(object({ all = optional(bool) values = optional(list(string)) })) deny = optional(object({ all = optional(bool) values = optional(list(string)) })) enforce = optional(bool) # for boolean policies only. condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) }), {}) })), []) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [parent](variables.tf#L140) | Parent in folders/folder_id or organizations/org_id format. | <code>string</code> | | <code>null</code> |
|
||||
| [tag_bindings](variables.tf#L150) | Tag bindings for this folder, in key => tag value id format. | <code>map(string)</code> | | <code>null</code> |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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], [])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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. | <code>google_organization_iam_binding</code> · <code>google_organization_iam_custom_role</code> · <code>google_organization_iam_member</code> |
|
||||
| [iam.tf](./iam.tf) | IAM bindings. | <code>google_organization_iam_binding</code> · <code>google_organization_iam_custom_role</code> · <code>google_organization_iam_member</code> |
|
||||
| [logging.tf](./logging.tf) | Log sinks and data access logs. | <code>google_bigquery_dataset_iam_member</code> · <code>google_logging_organization_exclusion</code> · <code>google_logging_organization_sink</code> · <code>google_organization_iam_audit_config</code> · <code>google_project_iam_member</code> · <code>google_pubsub_topic_iam_member</code> · <code>google_storage_bucket_iam_member</code> |
|
||||
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_compute_firewall_policy_association</code> · <code>google_essential_contacts_contact</code> |
|
||||
| [org-policy-custom-constraints.tf](./org-policy-custom-constraints.tf) | None | <code>google_org_policy_custom_constraint</code> |
|
||||
| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | <code>google_org_policy_policy</code> |
|
||||
| [outputs.tf](./outputs.tf) | Module outputs. | |
|
||||
| [tags.tf](./tags.tf) | None | <code>google_tags_tag_binding</code> · <code>google_tags_tag_key</code> · <code>google_tags_tag_key_iam_binding</code> · <code>google_tags_tag_value</code> · <code>google_tags_tag_value_iam_binding</code> |
|
||||
| [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. | <code>string</code> | ✓ | |
|
||||
| [organization_id](variables.tf#L145) | Organization id in organizations/nnnnnn format. | <code>string</code> | ✓ | |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [factories_config](variables.tf#L31) | Paths to data files and folders that enable factory functionality. | <code title="object({ custom_roles = optional(string) org_policies = optional(string) org_policy_custom_constraints = optional(string) })">object({…})</code> | | <code>{}</code> |
|
||||
| [firewall_policy](variables.tf#L42) | Hierarchical firewall policies to associate to the organization. | <code title="object({ name = string policy = string })">object({…})</code> | | <code>null</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L58) | IAM bindings, in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L65) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L80) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(map(list(string)))</code> | | <code>{}</code> |
|
||||
| [logging_exclusions](variables.tf#L110) | Logging exclusions for this organization in the form {NAME -> FILTER}. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [logging_sinks](variables.tf#L117) | Logging sinks to create for the organization. | <code title="map(object({ bq_partitioned_table = optional(bool, false) description = optional(string) destination = string disabled = optional(bool, false) exclusions = optional(map(string), {}) filter = string iam = optional(bool, true) include_children = optional(bool, true) type = string }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam](variables-iam.tf#L17) | IAM bindings, in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(map(list(string)))</code> | | <code>{}</code> |
|
||||
| [logging_exclusions](variables.tf#L66) | Logging exclusions for this organization in the form {NAME -> FILTER}. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [logging_sinks](variables.tf#L73) | Logging sinks to create for the organization. | <code title="map(object({ bq_partitioned_table = optional(bool, false) description = optional(string) destination = string disabled = optional(bool, false) exclusions = optional(map(string), {}) filter = string iam = optional(bool, true) include_children = optional(bool, true) type = string }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code title="map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) id = optional(string) network = string # project_id/vpc_name values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) })), {}) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [org_policies](variables.tf#L148) | Organization policies applied to this organization keyed by policy name. | <code title="map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) rules = optional(list(object({ allow = optional(object({ all = optional(bool) values = optional(list(string)) })) deny = optional(object({ all = optional(bool) values = optional(list(string)) })) enforce = optional(bool) # for boolean policies only. condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) }), {}) })), []) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [org_policy_custom_constraints](variables.tf#L175) | Organization policy custom constraints keyed by constraint name. | <code title="map(object({ display_name = optional(string) description = optional(string) action_type = string condition = string method_types = list(string) resource_types = list(string) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [org_policies](variables.tf#L104) | Organization policies applied to this organization keyed by policy name. | <code title="map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) rules = optional(list(object({ allow = optional(object({ all = optional(bool) values = optional(list(string)) })) deny = optional(object({ all = optional(bool) values = optional(list(string)) })) enforce = optional(bool) # for boolean policies only. condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) }), {}) })), []) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [org_policy_custom_constraints](variables.tf#L131) | Organization policy custom constraints keyed by constraint name. | <code title="map(object({ display_name = optional(string) description = optional(string) action_type = string condition = string method_types = list(string) resource_types = list(string) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [tag_bindings](variables-tags.tf#L45) | Tag bindings for this organization, in key => tag value id format. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [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. | <code title="map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) id = optional(string) values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) id = optional(string) })), {}) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
|
||||
|
|
|
@ -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], [])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)))
|
||||
|
|
|
@ -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. | <code>google_project_iam_binding</code> · <code>google_project_iam_custom_role</code> · <code>google_project_iam_member</code> |
|
||||
| [iam.tf](./iam.tf) | IAM bindings. | <code>google_project_iam_binding</code> · <code>google_project_iam_custom_role</code> · <code>google_project_iam_member</code> |
|
||||
| [logging.tf](./logging.tf) | Log sinks and supporting resources. | <code>google_bigquery_dataset_iam_member</code> · <code>google_logging_project_exclusion</code> · <code>google_logging_project_sink</code> · <code>google_project_iam_audit_config</code> · <code>google_project_iam_member</code> · <code>google_pubsub_topic_iam_member</code> · <code>google_storage_bucket_iam_member</code> |
|
||||
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_compute_project_metadata_item</code> · <code>google_essential_contacts_contact</code> · <code>google_monitoring_monitored_project</code> · <code>google_project</code> · <code>google_project_service</code> · <code>google_resource_manager_lien</code> |
|
||||
| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | <code>google_org_policy_policy</code> |
|
||||
|
@ -972,6 +971,7 @@ module "bucket" {
|
|||
| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | <code>google_kms_crypto_key_iam_member</code> · <code>google_project_default_service_accounts</code> · <code>google_project_iam_member</code> · <code>google_project_service_identity</code> |
|
||||
| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | <code>google_compute_shared_vpc_host_project</code> · <code>google_compute_shared_vpc_service_project</code> · <code>google_compute_subnetwork_iam_member</code> · <code>google_project_iam_member</code> |
|
||||
| [tags.tf](./tags.tf) | None | <code>google_tags_tag_binding</code> · <code>google_tags_tag_key</code> · <code>google_tags_tag_key_iam_binding</code> · <code>google_tags_tag_value</code> · <code>google_tags_tag_value_iam_binding</code> |
|
||||
| [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. | <code>string</code> | ✓ | |
|
||||
| [name](variables.tf#L152) | Project name and id suffix. | <code>string</code> | ✓ | |
|
||||
| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | <code>bool</code> | | <code>false</code> |
|
||||
| [billing_account](variables.tf#L23) | Billing account id. | <code>string</code> | | <code>null</code> |
|
||||
| [compute_metadata](variables.tf#L29) | Optional compute metadata key/values. Only usable if compute API has been enabled. | <code>map(string)</code> | | <code>{}</code> |
|
||||
|
@ -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`. | <code>string</code> | | <code>"keep"</code> |
|
||||
| [descriptive_name](variables.tf#L63) | Name of the project name. Used for project name instead of `name` variable. | <code>string</code> | | <code>null</code> |
|
||||
| [factories_config](variables.tf#L69) | Paths to data files and folders that enable factory functionality. | <code title="object({ custom_roles = optional(string) org_policies = optional(string) })">object({…})</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L86) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L93) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L108) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [labels](variables.tf#L123) | Resource labels. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [lien_reason](variables.tf#L130) | If non-empty, creates a project lien with this description. | <code>string</code> | | <code>null</code> |
|
||||
| [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. | <code>map(map(list(string)))</code> | | <code>{}</code> |
|
||||
| [logging_exclusions](variables.tf#L151) | Logging exclusions for this project in the form {NAME -> FILTER}. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [logging_sinks](variables.tf#L158) | Logging sinks to create for this project. | <code title="map(object({ bq_partitioned_table = optional(bool, false) description = optional(string) destination = string disabled = optional(bool, false) exclusions = optional(map(string), {}) filter = string iam = optional(bool, true) type = string unique_writer = optional(bool, true) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [metric_scopes](variables.tf#L189) | List of projects that will act as metric scopes for this project. | <code>list(string)</code> | | <code>[]</code> |
|
||||
| [iam](variables-iam.tf#L17) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [labels](variables.tf#L79) | Resource labels. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [lien_reason](variables.tf#L86) | If non-empty, creates a project lien with this description. | <code>string</code> | | <code>null</code> |
|
||||
| [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. | <code>map(map(list(string)))</code> | | <code>{}</code> |
|
||||
| [logging_exclusions](variables.tf#L107) | Logging exclusions for this project in the form {NAME -> FILTER}. | <code>map(string)</code> | | <code>{}</code> |
|
||||
| [logging_sinks](variables.tf#L114) | Logging sinks to create for this project. | <code title="map(object({ bq_partitioned_table = optional(bool, false) description = optional(string) destination = string disabled = optional(bool, false) exclusions = optional(map(string), {}) filter = string iam = optional(bool, true) type = string unique_writer = optional(bool, true) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [metric_scopes](variables.tf#L145) | List of projects that will act as metric scopes for this project. | <code>list(string)</code> | | <code>[]</code> |
|
||||
| [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. | <code title="map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) id = optional(string) network = string # project_id/vpc_name values = optional(map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) })), {}) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [org_policies](variables.tf#L201) | Organization policies applied to this project keyed by policy name. | <code title="map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) rules = optional(list(object({ allow = optional(object({ all = optional(bool) values = optional(list(string)) })) deny = optional(object({ all = optional(bool) values = optional(list(string)) })) enforce = optional(bool) # for boolean policies only. condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) }), {}) })), []) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [parent](variables.tf#L228) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | <code>string</code> | | <code>null</code> |
|
||||
| [prefix](variables.tf#L238) | Optional prefix used to generate project id and name. | <code>string</code> | | <code>null</code> |
|
||||
| [project_create](variables.tf#L248) | Create project. When set to false, uses a data source to reference existing project. | <code>bool</code> | | <code>true</code> |
|
||||
| [service_config](variables.tf#L254) | Configure service API activation. | <code title="object({ disable_on_destroy = bool disable_dependent_services = bool })">object({…})</code> | | <code title="{ disable_on_destroy = false disable_dependent_services = false }">{…}</code> |
|
||||
| [service_encryption_key_ids](variables.tf#L266) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [service_perimeter_bridges](variables.tf#L273) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | <code>list(string)</code> | | <code>null</code> |
|
||||
| [service_perimeter_standard](variables.tf#L280) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | <code>string</code> | | <code>null</code> |
|
||||
| [services](variables.tf#L286) | Service APIs to enable. | <code>list(string)</code> | | <code>[]</code> |
|
||||
| [shared_vpc_host_config](variables.tf#L292) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | <code title="object({ enabled = bool service_projects = optional(list(string), []) })">object({…})</code> | | <code>null</code> |
|
||||
| [shared_vpc_service_config](variables.tf#L301) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | <code title="object({ host_project = string network_users = optional(list(string), []) service_identity_iam = optional(map(list(string)), {}) service_identity_subnet_iam = optional(map(list(string)), {}) service_iam_grants = optional(list(string), []) network_subnet_users = optional(map(list(string)), {}) })">object({…})</code> | | <code title="{ host_project = null }">{…}</code> |
|
||||
| [skip_delete](variables.tf#L329) | Allows the underlying resources to be destroyed without destroying the project itself. | <code>bool</code> | | <code>false</code> |
|
||||
| [org_policies](variables.tf#L157) | Organization policies applied to this project keyed by policy name. | <code title="map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) rules = optional(list(object({ allow = optional(object({ all = optional(bool) values = optional(list(string)) })) deny = optional(object({ all = optional(bool) values = optional(list(string)) })) enforce = optional(bool) # for boolean policies only. condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) }), {}) })), []) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [parent](variables.tf#L184) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | <code>string</code> | | <code>null</code> |
|
||||
| [prefix](variables.tf#L194) | Optional prefix used to generate project id and name. | <code>string</code> | | <code>null</code> |
|
||||
| [project_create](variables.tf#L204) | Create project. When set to false, uses a data source to reference existing project. | <code>bool</code> | | <code>true</code> |
|
||||
| [service_config](variables.tf#L210) | Configure service API activation. | <code title="object({ disable_on_destroy = bool disable_dependent_services = bool })">object({…})</code> | | <code title="{ disable_on_destroy = false disable_dependent_services = false }">{…}</code> |
|
||||
| [service_encryption_key_ids](variables.tf#L222) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [service_perimeter_bridges](variables.tf#L229) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | <code>list(string)</code> | | <code>null</code> |
|
||||
| [service_perimeter_standard](variables.tf#L236) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | <code>string</code> | | <code>null</code> |
|
||||
| [services](variables.tf#L242) | Service APIs to enable. | <code>list(string)</code> | | <code>[]</code> |
|
||||
| [shared_vpc_host_config](variables.tf#L248) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | <code title="object({ enabled = bool service_projects = optional(list(string), []) })">object({…})</code> | | <code>null</code> |
|
||||
| [shared_vpc_service_config](variables.tf#L257) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | <code title="object({ host_project = string network_users = optional(list(string), []) service_identity_iam = optional(map(list(string)), {}) service_identity_subnet_iam = optional(map(list(string)), {}) service_iam_grants = optional(list(string), []) network_subnet_users = optional(map(list(string)), {}) })">object({…})</code> | | <code title="{ host_project = null }">{…}</code> |
|
||||
| [skip_delete](variables.tf#L285) | Allows the underlying resources to be destroyed without destroying the project itself. | <code>bool</code> | | <code>false</code> |
|
||||
| [tag_bindings](variables-tags.tf#L45) | Tag bindings for this project, in key => tag value id format. | <code>map(string)</code> | | <code>null</code> |
|
||||
| [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. | <code title="map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) id = optional(string) values = optional(map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) id = optional(string) })), {}) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
|
||||
|
|
|
@ -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], [])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -65,9 +65,10 @@ module "repo" {
|
|||
|
||||
| name | description | resources |
|
||||
|---|---|---|
|
||||
| [iam.tf](./iam.tf) | IAM resources. | <code>google_sourcerepo_repository_iam_binding</code> · <code>google_sourcerepo_repository_iam_member</code> |
|
||||
| [iam.tf](./iam.tf) | IAM bindings. | <code>google_sourcerepo_repository_iam_binding</code> · <code>google_sourcerepo_repository_iam_member</code> |
|
||||
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_cloudbuild_trigger</code> · <code>google_sourcerepo_repository</code> |
|
||||
| [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. | <code>string</code> | ✓ | |
|
||||
| [project_id](variables.tf#L66) | Project used for resources. | <code>string</code> | ✓ | |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L24) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L31) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L46) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [triggers](variables.tf#L71) | Cloud Build triggers. | <code title="map(object({ filename = string included_files = list(string) service_account = string substitutions = map(string) template = object({ branch_name = string project_id = string tag_name = string }) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [name](variables.tf#L17) | Repository name. | <code>string</code> | ✓ | |
|
||||
| [project_id](variables.tf#L22) | Project used for resources. | <code>string</code> | ✓ | |
|
||||
| [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [triggers](variables.tf#L27) | Cloud Build triggers. | <code title="map(object({ filename = string included_files = list(string) service_account = string substitutions = map(string) template = object({ branch_name = string project_id = string tag_name = string }) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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], [])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue