Merge remote-tracking branch 'origin/master' into fast-dev

This commit is contained in:
Ludo
2025-04-07 08:47:00 +02:00
35 changed files with 916 additions and 451 deletions

View File

@@ -2,11 +2,20 @@
This module allows simplified creation and management of one a service account and its IAM bindings.
The Service Account `key` can be generated with `openssl` library and only the public part uploaded to the Service Account, for more refer to the [Onprem SA Key Management](../../blueprints/cloud-operations/onprem-sa-key-management/) example.
Note that outputs have no dependencies on IAM bindings to prevent resource cycles.
## Example
## TOC
<!-- BEGIN TOC -->
- [TOC](#toc)
- [Simple Example](#simple-example)
- [Tag Bindings](#tag-bindings)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
## Simple Example
```hcl
module "myproject-default-service-accounts" {
@@ -27,6 +36,25 @@ module "myproject-default-service-accounts" {
}
# tftest modules=1 resources=4 inventory=basic.yaml e2e
```
## Tag Bindings
Use the `tag_bindings` variable to attach tags to the service account. Provide `project_number` to prevent potential permadiffs with the tag binding resource.
```hcl
module "service-account-with-tags" {
source = "./fabric/modules/iam-service-account"
project_id = var.project_id
name = "test-service-account"
project_number = var.project_number
tag_bindings = {
foo = "tagValues/123456789"
}
}
# tftest modules=1 resources=2 inventory=tags.yaml
```
<!-- TFDOC OPTS files:1 -->
<!-- BEGIN TFDOC -->
## Files
@@ -34,7 +62,7 @@ module "myproject-default-service-accounts" {
| name | description | resources |
|---|---|---|
| [iam.tf](./iam.tf) | IAM bindings. | <code>google_billing_account_iam_member</code> · <code>google_folder_iam_member</code> · <code>google_organization_iam_member</code> · <code>google_project_iam_member</code> · <code>google_service_account_iam_binding</code> · <code>google_service_account_iam_member</code> · <code>google_storage_bucket_iam_member</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_service_account</code> · <code>google_service_account_key</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_service_account</code> · <code>google_tags_tag_binding</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [variables.tf](./variables.tf) | Module variables. | |
| [versions.tf](./versions.tf) | Version pins. | |
@@ -43,22 +71,24 @@ module "myproject-default-service-accounts" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [name](variables.tf#L108) | Name of the service account to create. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L123) | Project id where service account will be created. | <code>string</code> | ✓ | |
| [description](variables.tf#L17) | Optional description. | <code>string</code> | | <code>null</code> |
| [display_name](variables.tf#L23) | Display name of the service account to create. | <code>string</code> | | <code>&#34;Terraform-managed.&#34;</code> |
| [iam](variables.tf#L29) | IAM bindings on the service account in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_billing_roles](variables.tf#L36) | Billing account roles granted to this service account, by billing account id. Non-authoritative. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings](variables.tf#L43) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings_additive](variables.tf#L58) | Individual additive IAM bindings on the service account. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_folder_roles](variables.tf#L73) | Folder roles granted to this service account, by folder id. Non-authoritative. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_organization_roles](variables.tf#L80) | Organization roles granted to this service account, by organization id. Non-authoritative. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_project_roles](variables.tf#L87) | Project roles granted to this service account, by project id. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_sa_roles](variables.tf#L94) | Service account roles granted to this service account, by service account name. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_storage_roles](variables.tf#L101) | Storage roles granted to this service account, by bucket name. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [prefix](variables.tf#L113) | Prefix applied to service account names. | <code>string</code> | | <code>null</code> |
| [public_keys_directory](variables.tf#L128) | Path to public keys data files to upload to the service account (should have `.pem` extension). | <code>string</code> | | <code>&#34;&#34;</code> |
| [service_account_create](variables.tf#L134) | Create service account. When set to false, uses a data source to reference an existing service account. | <code>bool</code> | | <code>true</code> |
| [name](variables.tf#L118) | Name of the service account to create. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L133) | Project id where service account will be created. | <code>string</code> | ✓ | |
| [create_ignore_already_exists](variables.tf#L17) | If set to true, skip service account creation if a service account with the same email already exists. | <code>bool</code> | | <code>null</code> |
| [description](variables.tf#L27) | Optional description. | <code>string</code> | | <code>null</code> |
| [display_name](variables.tf#L33) | Display name of the service account to create. | <code>string</code> | | <code>&#34;Terraform-managed.&#34;</code> |
| [iam](variables.tf#L39) | IAM bindings on the service account in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_billing_roles](variables.tf#L46) | Billing account roles granted to this service account, by billing account id. Non-authoritative. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings](variables.tf#L53) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings_additive](variables.tf#L68) | Individual additive IAM bindings on the service account. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_folder_roles](variables.tf#L83) | Folder roles granted to this service account, by folder id. Non-authoritative. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_organization_roles](variables.tf#L90) | Organization roles granted to this service account, by organization id. Non-authoritative. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_project_roles](variables.tf#L97) | Project roles granted to this service account, by project id. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_sa_roles](variables.tf#L104) | Service account roles granted to this service account, by service account name. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_storage_roles](variables.tf#L111) | Storage roles granted to this service account, by bucket name. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [prefix](variables.tf#L123) | Prefix applied to service account names. | <code>string</code> | | <code>null</code> |
| [project_number](variables.tf#L138) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. | <code>string</code> | | <code>null</code> |
| [service_account_create](variables.tf#L144) | Create service account. When set to false, uses a data source to reference an existing service account. | <code>bool</code> | | <code>true</code> |
| [tag_bindings](variables.tf#L151) | Tag bindings for this service accounts, in key => tag value id format. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs
@@ -69,5 +99,4 @@ module "myproject-default-service-accounts" {
| [id](outputs.tf#L33) | Fully qualified service account id. | |
| [name](outputs.tf#L41) | Service account name. | |
| [service_account](outputs.tf#L49) | Service account resource. | |
| [service_account_credentials](outputs.tf#L54) | Service account json credential templates for uploaded public keys data. | |
<!-- END TFDOC -->

View File

@@ -30,36 +30,13 @@ locals {
? try(google_service_account.service_account[0], null)
: try(data.google_service_account.service_account[0], null)
)
service_account_credential_templates = {
for file, _ in local.public_keys_data : file => jsonencode(
{
type : "service_account",
project_id : var.project_id,
private_key_id : split("/", google_service_account_key.upload_key[file].id)[5]
private_key : "REPLACE_ME_WITH_PRIVATE_KEY_DATA"
client_email : local.resource_email_static
client_id : local.service_account.unique_id,
auth_uri : "https://accounts.google.com/o/oauth2/auth",
token_uri : "https://oauth2.googleapis.com/token",
auth_provider_x509_cert_url : "https://www.googleapis.com/oauth2/v1/certs",
client_x509_cert_url : "https://www.googleapis.com/robot/v1/metadata/x509/${urlencode(local.resource_email_static)}"
}
)
}
public_keys_data = (
var.public_keys_directory != ""
? {
for file in fileset("${path.root}/${var.public_keys_directory}", "*.pem")
: file => filebase64("${path.root}/${var.public_keys_directory}/${file}") }
: {}
)
# universe-related locals
universe = try(regex("^([^:]*):[a-z]", var.project_id)[0], "")
project_id_no_universe = element(split(":", var.project_id), 1)
sa_domain = join(".", compact([local.project_id_no_universe, local.universe]))
}
data "google_service_account" "service_account" {
count = var.service_account_create ? 0 : 1
project = var.project_id
@@ -67,15 +44,16 @@ data "google_service_account" "service_account" {
}
resource "google_service_account" "service_account" {
count = var.service_account_create ? 1 : 0
project = var.project_id
account_id = "${local.prefix}${local.name}"
display_name = var.display_name
description = var.description
count = var.service_account_create ? 1 : 0
project = var.project_id
account_id = "${local.prefix}${local.name}"
display_name = var.display_name
description = var.description
create_ignore_already_exists = var.create_ignore_already_exists
}
resource "google_service_account_key" "upload_key" {
for_each = local.public_keys_data
service_account_id = local.service_account.email
public_key_data = each.value
resource "google_tags_tag_binding" "binding" {
for_each = var.tag_bindings
parent = "//iam.googleapis.com/projects/${coalesce(var.project_number, var.project_id)}/serviceAccounts/${local.service_account.unique_id}"
tag_value = each.value
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2022 Google LLC
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,8 +50,3 @@ output "service_account" {
description = "Service account resource."
value = local.service_account
}
output "service_account_credentials" {
description = "Service account json credential templates for uploaded public keys data."
value = local.service_account_credential_templates
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2022 Google LLC
* Copyright 2025 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,6 +14,16 @@
* limitations under the License.
*/
variable "create_ignore_already_exists" {
description = "If set to true, skip service account creation if a service account with the same email already exists."
type = bool
default = null
validation {
condition = !(var.create_ignore_already_exists == true && var.service_account_create == false)
error_message = "Cannot set create_ignore_already_exists when service_account_create is false."
}
}
variable "description" {
description = "Optional description."
type = string
@@ -125,14 +135,22 @@ variable "project_id" {
type = string
}
variable "public_keys_directory" {
description = "Path to public keys data files to upload to the service account (should have `.pem` extension)."
variable "project_number" {
description = "Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings."
type = string
default = ""
default = null
}
variable "service_account_create" {
description = "Create service account. When set to false, uses a data source to reference an existing service account."
type = bool
default = true
nullable = false
}
variable "tag_bindings" {
description = "Tag bindings for this service accounts, in key => tag value id format."
type = map(string)
nullable = false
default = {}
}

View File

@@ -494,10 +494,10 @@ service_accounts:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [factories_config](variables.tf#L120) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; string&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; context &#61; optional&#40;object&#40;&#123;&#10; folder_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; perimeters &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; vpc_host_projects &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; notification_channels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; folders_data_path &#61; optional&#40;string&#41;&#10; projects_data_path &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [factories_config](variables.tf#L121) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; string&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; context &#61; optional&#40;object&#40;&#123;&#10; folder_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; vpc_host_projects &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; notification_channels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; folders_data_path &#61; optional&#40;string&#41;&#10; projects_data_path &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; factories_config &#61; optional&#40;object&#40;&#123;&#10; custom_roles &#61; optional&#40;string&#41;&#10; observability &#61; optional&#40;string&#41;&#10; org_policies &#61; optional&#40;string&#41;&#10; quotas &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; shared_vpc_service_config &#61; optional&#40;object&#40;&#123;&#10; host_project &#61; string&#10; network_users &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_agent_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_agent_subnet_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; network_subnet_users &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123; host_project &#61; null &#125;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;object&#40;&#123;&#10; ADMIN_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_WRITE &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_merges](variables.tf#L64) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | <code title="object&#40;&#123;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_overrides](variables.tf#L83) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; factories_config &#61; optional&#40;object&#40;&#123;&#10; custom_roles &#61; optional&#40;string&#41;&#10; observability &#61; optional&#40;string&#41;&#10; org_policies &#61; optional&#40;string&#41;&#10; quotas &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;object&#40;&#123;&#10; ADMIN_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_WRITE &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_overrides](variables.tf#L83) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; factories_config &#61; optional&#40;object&#40;&#123;&#10; custom_roles &#61; optional&#40;string&#41;&#10; observability &#61; optional&#40;string&#41;&#10; org_policies &#61; optional&#40;string&#41;&#10; quotas &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;object&#40;&#123;&#10; ADMIN_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_WRITE &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs

View File

@@ -21,7 +21,7 @@
# data_defaults = ...
# })
# outputs:
# projects - map
# local._projects_output - map
locals {
__projects_config = {
data_defaults = merge({
@@ -78,9 +78,10 @@ locals {
},
try(local._projects_config.data_defaults, {})
)
# data_overrides default to null's, to mark that they should not override
data_overrides = merge({
billing_account = null
contacts = {}
contacts = null
factories_config = merge({
custom_roles = null
observability = null
@@ -95,40 +96,48 @@ locals {
)
parent = null
prefix = null
service_encryption_key_ids = {}
service_encryption_key_ids = null
storage_location = null
tag_bindings = {}
services = []
service_accounts = {}
vpc_sc = merge({
perimeter_name = null
perimeter_bridges = []
is_dry_run = false
}, try(local._projects_config.data_overrides.vpc_sc, {
perimeter_name = null
perimeter_bridges = []
is_dry_run = false
})
tag_bindings = null
services = null
service_accounts = null
vpc_sc = try(
merge(
{
perimeter_name = null
perimeter_bridges = []
is_dry_run = false
},
local._projects_config.data_overrides.vpc_sc
),
null
)
logging_data_access = {}
logging_data_access = null
},
try(local._projects_config.data_overrides, {})
)
}
_projects_output = {
for k, v in local._projects_input : lookup(v, "name", k) => merge(v, {
billing_account = try(coalesce(
# Semantics of the merges are:
# * if data_overrides.<field> is not null, use this value
# * if _projects_inputs.<field> is not null, use this value
# * use data_default value, which if not set, will provide "empty" type
# This logic is easily implemented using coalesce, even on maps and list and allows to
# set data_overrides.<field> to "", [] or {} to ensure, that empty value is always passed, or do
# the same in _projects_input to prevent falling back to default value
for k, v in local._projects_input : k => merge(v, {
billing_account = try(coalesce( # type: string
local.__projects_config.data_overrides.billing_account,
try(v.billing_account, null),
local.__projects_config.data_defaults.billing_account
), null)
contacts = coalesce(
contacts = coalesce( # type: map
local.__projects_config.data_overrides.contacts,
try(v.contacts, null),
local.__projects_config.data_defaults.contacts
)
factories_config = {
custom_roles = try(
factories_config = { # type: object
custom_roles = try( # type: string
coalesce(
local.__projects_config.data_overrides.factories_config.custom_roles,
try(v.factories_config.custom_roles, null),
@@ -136,21 +145,21 @@ locals {
),
null
)
observability = try(
observability = try( # type: string
coalesce(
local.__projects_config.data_overrides.factories_config.observability,
try(v.factories_config.observability, null),
local.__projects_config.data_defaults.factories_config.observability
),
null)
org_policies = try(
org_policies = try( # type: string
coalesce(
local.__projects_config.data_overrides.factories_config.org_policies,
try(v.factories_config.org_policies, null),
local.__projects_config.data_defaults.factories_config.org_policies
),
null)
quotas = try(
quotas = try( # type: string
coalesce(
local.__projects_config.data_overrides.factories_config.quotas,
try(v.factories_config.quotas, null),
@@ -158,36 +167,46 @@ locals {
),
null)
}
labels = coalesce(
iam = try(v.iam, {}) # type: map(list(string))
iam_bindings = try(v.iam_bindings, {}) # type: map(object({...}))
iam_bindings_additive = try(v.iam_bindings_additive, {}) # type: map(object({...}))
iam_by_principals_additive = try(v.iam_by_principals_additive, {}) # type: map(list(string))
iam_by_principals = try(v.iam_by_principals, {}) # map(list(string))
labels = coalesce( # type: map(string)
try(v.labels, null),
local.__projects_config.data_defaults.labels
)
metric_scopes = coalesce(
metric_scopes = coalesce( # type: list(string)
try(v.metric_scopes, null),
local.__projects_config.data_defaults.metric_scopes
)
org_policies = try(v.org_policies, {})
parent = coalesce(
local.__projects_config.data_overrides.parent,
try(v.parent, null),
local.__projects_config.data_defaults.parent
name = lookup(v, "name", k) # type: string
org_policies = try(v.org_policies, {}) # type: map(object({...}))
parent = try( # type: string, nullable
coalesce(
local.__projects_config.data_overrides.parent,
try(v.parent, null),
local.__projects_config.data_defaults.parent
), null
)
prefix = coalesce(
local.__projects_config.data_overrides.prefix,
try(v.prefix, null),
local.__projects_config.data_defaults.prefix
prefix = try( # type: string, nullable
coalesce(
local.__projects_config.data_overrides.prefix,
try(v.prefix, null),
local.__projects_config.data_defaults.prefix
), null
)
service_encryption_key_ids = coalesce(
service_encryption_key_ids = coalesce( # type: map(list(string))
local.__projects_config.data_overrides.service_encryption_key_ids,
try(v.service_encryption_key_ids, null),
local.__projects_config.data_defaults.service_encryption_key_ids
)
services = coalesce(
services = coalesce( # type: list(string)
local.__projects_config.data_overrides.services,
try(v.services, null),
local.__projects_config.data_defaults.services
)
shared_vpc_host_config = (
shared_vpc_host_config = ( # type: object({...})
try(v.shared_vpc_host_config, null) != null
? merge(
{ service_projects = [] },
@@ -195,7 +214,7 @@ locals {
)
: null
)
shared_vpc_service_config = (
shared_vpc_service_config = ( # type: object({...})
try(v.shared_vpc_service_config, null) != null
? merge(
{
@@ -210,7 +229,7 @@ locals {
)
: local.__projects_config.data_defaults.shared_vpc_service_config
)
tag_bindings = coalesce(
tag_bindings = coalesce( # type: map(string)
local.__projects_config.data_overrides.tag_bindings,
try(v.tag_bindings, null),
local.__projects_config.data_defaults.tag_bindings
@@ -246,7 +265,7 @@ locals {
: local.__projects_config.data_defaults.vpc_sc
)
)
logging_data_access = coalesce(
logging_data_access = coalesce( # type: map(object({...}))
local.__projects_config.data_overrides.logging_data_access,
try(v.logging_data_access, null),
local.__projects_config.data_defaults.logging_data_access

View File

@@ -17,10 +17,10 @@
# tfdoc:file:description Projects factory locals.
locals {
_hierarchy_projects = (
_hierarchy_projects_full_path = (
{
for f in try(fileset(local._folders_path, "**/*.yaml"), []) :
basename(trimsuffix(f, ".yaml")) => merge(
trimsuffix(f, ".yaml") => merge(
{ parent = dirname(f) == "." ? "default" : dirname(f) },
yamldecode(file("${local._folders_path}/${f}"))
)
@@ -28,13 +28,16 @@ locals {
}
)
_project_path = try(pathexpand(var.factories_config.projects_data_path), null)
_projects_input = merge(
{
for f in try(fileset(local._project_path, "**/*.yaml"), []) :
basename(trimsuffix(f, ".yaml")) => yamldecode(file("${local._project_path}/${f}"))
},
local._hierarchy_projects
)
_projects_full_path = {
for f in try(fileset(local._project_path, "**/*.yaml"), []) :
trimsuffix(f, ".yaml") => yamldecode(file("${local._project_path}/${f}"))
}
_projects_input = {
# will raise error, if the same filename is used multiple times
# and project name is not set via name in YAML
for k, v in merge(local._hierarchy_projects_full_path, local._projects_full_path) :
lookup(v, "name", basename(k)) => v
}
_project_budgets = flatten([
for k, v in local._projects_input : [
for b in try(v.billing_budgets, []) : {
@@ -59,7 +62,8 @@ locals {
buckets = flatten([
for k, v in local.projects : [
for name, opts in v.buckets : {
project = k
project_key = k
project_name = v.name
name = name
description = lookup(opts, "description", "Terraform-managed.")
encryption_key = lookup(opts, "encryption_key", null)
@@ -87,10 +91,10 @@ locals {
]
])
service_accounts = flatten([
for k, v in local.projects : [
for name, opts in v.service_accounts : {
project = k
name = name
for k, project in local.projects : [
for name, opts in project.service_accounts : {
project_key = k
name = name
display_name = coalesce(
try(var.data_overrides.service_accounts.display_name, null),
try(opts.display_name, null),

View File

@@ -232,11 +232,11 @@ module "projects-iam" {
module "buckets" {
source = "../gcs"
for_each = {
for k in local.buckets : "${k.project}/${k.name}" => k
for k in local.buckets : "${k.project_key}/${k.name}" => k
}
project_id = module.projects[each.value.project].project_id
project_id = module.projects[each.value.project_key].project_id
prefix = each.value.prefix
name = "${each.value.project}-${each.value.name}"
name = "${each.value.project_name}-${each.value.name}"
encryption_key = each.value.encryption_key
iam = {
for k, v in each.value.iam : k => [
@@ -320,9 +320,9 @@ module "buckets" {
module "service-accounts" {
source = "../iam-service-account"
for_each = {
for k in local.service_accounts : "${k.project}/${k.name}" => k
for k in local.service_accounts : "${k.project_key}/${k.name}" => k
}
project_id = module.projects[each.value.project].project_id
project_id = module.projects[each.value.project_key].project_id
name = each.value.name
display_name = each.value.display_name
iam = {
@@ -349,7 +349,7 @@ module "service-accounts" {
lookup(var.factories_config.context.vpc_host_projects, k, k) => v
},
each.value.iam_self_roles == null ? {} : {
(module.projects[each.value.project].project_id) = each.value.iam_self_roles
(module.projects[each.value.project_key].project_id) = each.value.iam_self_roles
}
)
}

View File

@@ -83,6 +83,7 @@ variable "data_merges" {
variable "data_overrides" {
description = "Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`."
type = object({
# data overrides default to null to mark that they should not override
billing_account = optional(string)
contacts = optional(map(list(string)))
factories_config = optional(object({
@@ -111,7 +112,7 @@ variable "data_overrides" {
ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
})), {})
})))
})
nullable = false
default = {}

View File

@@ -120,9 +120,7 @@ The regular perimeters variable exposes all the complexity of the underlying res
If you need to refer to access levels created by the same module in regular service perimeters, you can either use the module's outputs in the provided variables, or the key used to identify the relevant access level. The example below shows how to do this in practice.
/*
Resources for both perimeters have a `lifecycle` block that ignores changes to `spec` and `status` resources (projects), to allow using the additive resource `google_access_context_manager_service_perimeter_resource` at project creation. If this is not needed, the `lifecycle` blocks can be safely commented in the code.
*/
If you are managing perimeter membership outside of this module via `google_access_context_manager_service_perimeter_resource`, for example at project creation in a project factory, you might want to uncomment the lifecycle blocks that are defined but currently unused in `service-perimeters-regular.tf` and `service-perimeters-bridge.tf`.
#### Bridge type