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

This commit is contained in:
Ludovico Magnocavallo
2025-09-10 11:51:46 +00:00
32 changed files with 1345 additions and 561 deletions

View File

@@ -280,7 +280,7 @@ module "cf-http" {
project_id = var.project_number # use project_number to avoid perm-diff
secret = reverse(split("/", module.secret-manager.secrets["credentials"].name))[0]
versions = [
"${module.secret-manager.version_versions["credentials:v1"]}:/ver1"
"${module.secret-manager.version_versions["credentials/v1"]}:/ver1"
]
}
}
@@ -288,7 +288,7 @@ module "cf-http" {
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e
# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu
```
### Using CMEK to encrypt function resources

View File

@@ -293,7 +293,7 @@ module "cf-http" {
project_id = var.project_id
secret = reverse(split("/", module.secret-manager.secrets["credentials"].name))[0]
versions = [
"${module.secret-manager.version_versions["credentials:v1"]}:ver1"
"${module.secret-manager.version_versions["credentials/v1"]}:ver1"
]
}
}
@@ -302,7 +302,7 @@ module "cf-http" {
]
}
# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e
# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu
```
<!-- BEGIN TFDOC -->
## Variables

View File

@@ -46,7 +46,7 @@ module "cloud_run" {
env_from_key = {
SECRET1 = {
secret = module.secret-manager.secrets["credentials"].name
version = module.secret-manager.version_versions["credentials:v1"]
version = module.secret-manager.version_versions["credentials/v1"]
}
}
}
@@ -56,7 +56,7 @@ module "cloud_run" {
}
deletion_protection = false
}
# tftest fixtures=fixtures/secret-credentials.tf inventory=service-iam-env.yaml e2e
# tftest fixtures=fixtures/secret-credentials.tf inventory=service-iam-env.yaml e2e skip-tofu
```
## Mounting secrets as volumes
@@ -86,7 +86,7 @@ module "cloud_run" {
}
deletion_protection = false
}
# tftest fixtures=fixtures/secret-credentials.tf inventory=service-volume-secretes.yaml e2e
# tftest fixtures=fixtures/secret-credentials.tf inventory=service-volume-secretes.yaml e2e skip-tofu
```
## Mounting GCS buckets
@@ -491,18 +491,17 @@ module "secrets" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
otel-config = {}
}
iam = {
otel-config = {
"roles/secretmanager.secretAccessor" = [
"serviceAccount:${var.project_number}-compute@developer.gserviceaccount.com",
]
}
}
versions = {
otel-config = {
v1 = { enabled = true, data = file("${path.module}/config/otel-config.yaml") }
iam = {
"roles/secretmanager.secretAccessor" = [
"serviceAccount:${var.project_number}-compute@developer.gserviceaccount.com"
]
}
versions = {
v1 = {
data = file("${path.module}/config/otel-config.yaml")
}
}
}
}
}
@@ -555,7 +554,7 @@ module "cloud_run" {
}
deletion_protection = false
}
# tftest files=otel-config inventory=service-otel-sidecar.yaml e2e
# tftest files=otel-config inventory=service-otel-sidecar.yaml e2e skip-tofu
```
## Eventarc triggers
@@ -754,6 +753,7 @@ Unsupported variables / attributes:
- containers.resources.startup_cpu_boost
Additional configuration can be passwed as `job_config`:
- max_retries - maximum of retries per task
- task_count - desired number of tasks
- timeout - max allowed time per task, in seconds with up to nine fractional digits, ending with 's'. Example: `3.5s`

View File

@@ -29,11 +29,12 @@ module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
credentials = {}
}
iam = {
credentials = {
"roles/secretmanager.secretAccessor" = [module.cloud_run.service_account_iam_email]
iam = {
"roles/secretmanager.secretAccessor" = [
module.cloud_run.service_account_iam_email
]
}
}
}
}
@@ -63,7 +64,7 @@ module "cloud_run" {
}
service_account_create = true
}
# tftest modules=2 resources=5 inventory=simple.yaml e2e
# tftest modules=2 resources=5 inventory=simple.yaml e2e skip-tofu
```
## Mounting secrets as volumes
@@ -73,16 +74,15 @@ module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
credentials = {}
}
versions = {
credentials = {
v1 = { enabled = true, data = "foo bar baz" }
}
}
iam = {
credentials = {
"roles/secretmanager.secretAccessor" = [module.cloud_run.service_account_iam_email]
iam = {
"roles/secretmanager.secretAccessor" = [
module.cloud_run.service_account_iam_email
]
}
versions = {
v1 = { data = "foo bar baz" }
}
}
}
}
@@ -112,7 +112,7 @@ module "cloud_run" {
}
}
}
# tftest modules=2 resources=5 inventory=secrets.yaml e2e
# tftest modules=2 resources=5 inventory=secrets.yaml e2e skip-tofu
```
## Revision annotations

View File

@@ -941,10 +941,10 @@ module "sole-tenancy" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [name](variables.tf#L270) | Instance name. | <code>string</code> | ✓ | |
| [network_interfaces](variables.tf#L282) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | <code title="list&#40;object&#40;&#123;&#10; network &#61; string&#10; subnetwork &#61; string&#10; alias_ips &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; nat &#61; optional&#40;bool, false&#41;&#10; nic_type &#61; optional&#40;string&#41;&#10; stack_type &#61; optional&#40;string&#41;&#10; addresses &#61; optional&#40;object&#40;&#123;&#10; internal &#61; optional&#40;string&#41;&#10; external &#61; optional&#40;string&#41;&#10; &#125;&#41;, null&#41;&#10; network_tier &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | ✓ | |
| [project_id](variables.tf#L367) | Project id. | <code>string</code> | ✓ | |
| [zone](variables.tf#L480) | Compute zone. | <code>string</code> | ✓ | |
| [name](variables.tf#L277) | Instance name. | <code>string</code> | ✓ | |
| [network_interfaces](variables.tf#L289) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | <code title="list&#40;object&#40;&#123;&#10; network &#61; string&#10; subnetwork &#61; string&#10; alias_ips &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; nat &#61; optional&#40;bool, false&#41;&#10; nic_type &#61; optional&#40;string&#41;&#10; stack_type &#61; optional&#40;string&#41;&#10; addresses &#61; optional&#40;object&#40;&#123;&#10; internal &#61; optional&#40;string&#41;&#10; external &#61; optional&#40;string&#41;&#10; &#125;&#41;, null&#41;&#10; network_tier &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | ✓ | |
| [project_id](variables.tf#L374) | Project id. | <code>string</code> | ✓ | |
| [zone](variables.tf#L487) | Compute zone. | <code>string</code> | ✓ | |
| [attached_disk_defaults](variables.tf#L17) | Defaults for attached disks options. | <code title="object&#40;&#123;&#10; auto_delete &#61; optional&#40;bool, false&#41;&#10; mode &#61; string&#10; replica_zone &#61; string&#10; type &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; auto_delete &#61; true&#10; mode &#61; &#34;READ_WRITE&#34;&#10; replica_zone &#61; null&#10; type &#61; &#34;pd-balanced&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [attached_disks](variables.tf#L37) | Additional disks, if options is null defaults will be used in its place. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null. | <code title="list&#40;object&#40;&#123;&#10; name &#61; optional&#40;string&#41;&#10; device_name &#61; optional&#40;string&#41;&#10; size &#61; string&#10; snapshot_schedule &#61; optional&#40;list&#40;string&#41;&#41;&#10; source &#61; optional&#40;string&#41;&#10; source_type &#61; optional&#40;string&#41;&#10; options &#61; optional&#40;&#10; object&#40;&#123;&#10; auto_delete &#61; optional&#40;bool, false&#41;&#10; mode &#61; optional&#40;string, &#34;READ_WRITE&#34;&#41;&#10; replica_zone &#61; optional&#40;string&#41;&#10; type &#61; optional&#40;string, &#34;pd-balanced&#34;&#41;&#10; &#125;&#41;,&#10; &#123;&#10; auto_delete &#61; true&#10; mode &#61; &#34;READ_WRITE&#34;&#10; replica_zone &#61; null&#10; type &#61; &#34;pd-balanced&#34;&#10; &#125;&#10; &#41;&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#91;&#93;</code> |
| [boot_disk](variables.tf#L82) | Boot disk properties. Initialize params are ignored when source is set. | <code title="object&#40;&#123;&#10; auto_delete &#61; optional&#40;bool, true&#41;&#10; snapshot_schedule &#61; optional&#40;list&#40;string&#41;&#41;&#10; source &#61; optional&#40;string&#41;&#10; initialize_params &#61; optional&#40;object&#40;&#123;&#10; image &#61; optional&#40;string, &#34;projects&#47;debian-cloud&#47;global&#47;images&#47;family&#47;debian-11&#34;&#41;&#10; size &#61; optional&#40;number, 10&#41;&#10; type &#61; optional&#40;string, &#34;pd-balanced&#34;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; use_independent_disk &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; initialize_params &#61; &#123;&#125;&#10;&#125;">&#123;&#8230;&#125;</code> |
@@ -962,18 +962,19 @@ module "sole-tenancy" {
| [instance_type](variables.tf#L246) | Instance type. | <code>string</code> | | <code>&#34;f1-micro&#34;</code> |
| [labels](variables.tf#L252) | Instance labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [metadata](variables.tf#L258) | Instance metadata. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [min_cpu_platform](variables.tf#L264) | Minimum CPU platform. | <code>string</code> | | <code>null</code> |
| [network_attached_interfaces](variables.tf#L275) | Network interfaces using network attachments. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [network_tag_bindings](variables.tf#L303) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [options](variables.tf#L310) | Instance options. | <code title="object&#40;&#123;&#10; advanced_machine_features &#61; optional&#40;object&#40;&#123;&#10; enable_nested_virtualization &#61; optional&#40;bool&#41;&#10; enable_turbo_mode &#61; optional&#40;bool&#41;&#10; enable_uefi_networking &#61; optional&#40;bool&#41;&#10; performance_monitoring_unit &#61; optional&#40;string&#41;&#10; threads_per_core &#61; optional&#40;number&#41;&#10; visible_core_count &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; allow_stopping_for_update &#61; optional&#40;bool, true&#41;&#10; deletion_protection &#61; optional&#40;bool, false&#41;&#10; graceful_shutdown &#61; optional&#40;object&#40;&#123;&#10; enabled &#61; optional&#40;bool, false&#41;&#10; max_duration_secs &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; max_run_duration &#61; optional&#40;object&#40;&#123;&#10; nanos &#61; optional&#40;number&#41;&#10; seconds &#61; number&#10; &#125;&#41;&#41;&#10; node_affinities &#61; optional&#40;map&#40;object&#40;&#123;&#10; values &#61; list&#40;string&#41;&#10; in &#61; optional&#40;bool, true&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; spot &#61; optional&#40;bool, false&#41;&#10; termination_action &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; allow_stopping_for_update &#61; true&#10; deletion_protection &#61; false&#10; spot &#61; false&#10; termination_action &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [project_number](variables.tf#L372) | Project number. Used in tag bindings to avoid a permadiff. | <code>string</code> | | <code>null</code> |
| [scratch_disks](variables.tf#L378) | Scratch disks configuration. | <code title="object&#40;&#123;&#10; count &#61; number&#10; interface &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; count &#61; 0&#10; interface &#61; &#34;NVME&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [service_account](variables.tf#L390) | Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account. | <code title="object&#40;&#123;&#10; auto_create &#61; optional&#40;bool, false&#41;&#10; email &#61; optional&#40;string&#41;&#10; scopes &#61; optional&#40;list&#40;string&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [shielded_config](variables.tf#L400) | Shielded VM configuration of the instances. | <code title="object&#40;&#123;&#10; enable_secure_boot &#61; bool&#10; enable_vtpm &#61; bool&#10; enable_integrity_monitoring &#61; bool&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [snapshot_schedules](variables.tf#L410) | Snapshot schedule resource policies that can be attached to disks. | <code title="map&#40;object&#40;&#123;&#10; schedule &#61; object&#40;&#123;&#10; daily &#61; optional&#40;object&#40;&#123;&#10; days_in_cycle &#61; number&#10; start_time &#61; string&#10; &#125;&#41;&#41;&#10; hourly &#61; optional&#40;object&#40;&#123;&#10; hours_in_cycle &#61; number&#10; start_time &#61; string&#10; &#125;&#41;&#41;&#10; weekly &#61; optional&#40;list&#40;object&#40;&#123;&#10; day &#61; string&#10; start_time &#61; string&#10; &#125;&#41;&#41;&#41;&#10; &#125;&#41;&#10; description &#61; optional&#40;string&#41;&#10; retention_policy &#61; optional&#40;object&#40;&#123;&#10; max_retention_days &#61; number&#10; on_source_disk_delete_keep &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10; snapshot_properties &#61; optional&#40;object&#40;&#123;&#10; chain_name &#61; optional&#40;string&#41;&#10; guest_flush &#61; optional&#40;bool&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; storage_locations &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [tag_bindings](variables.tf#L453) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance and zonal disks, and modifiable without impacting the main resource lifecycle. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [tag_bindings_immutable](variables.tf#L460) | Immutable resource manager tag bindings, in tagKeys/id => tagValues/id format. These are set on the instance or instance template at creation time, and trigger recreation if changed. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [tags](variables.tf#L474) | Instance network tags for firewall rule targets. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [metadata_startup_script](variables.tf#L264) | Instance startup script. Will trigger recreation on change, even after importing. | <code>string</code> | | <code>null</code> |
| [min_cpu_platform](variables.tf#L271) | Minimum CPU platform. | <code>string</code> | | <code>null</code> |
| [network_attached_interfaces](variables.tf#L282) | Network interfaces using network attachments. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [network_tag_bindings](variables.tf#L310) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [options](variables.tf#L317) | Instance options. | <code title="object&#40;&#123;&#10; advanced_machine_features &#61; optional&#40;object&#40;&#123;&#10; enable_nested_virtualization &#61; optional&#40;bool&#41;&#10; enable_turbo_mode &#61; optional&#40;bool&#41;&#10; enable_uefi_networking &#61; optional&#40;bool&#41;&#10; performance_monitoring_unit &#61; optional&#40;string&#41;&#10; threads_per_core &#61; optional&#40;number&#41;&#10; visible_core_count &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; allow_stopping_for_update &#61; optional&#40;bool, true&#41;&#10; deletion_protection &#61; optional&#40;bool, false&#41;&#10; graceful_shutdown &#61; optional&#40;object&#40;&#123;&#10; enabled &#61; optional&#40;bool, false&#41;&#10; max_duration_secs &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; max_run_duration &#61; optional&#40;object&#40;&#123;&#10; nanos &#61; optional&#40;number&#41;&#10; seconds &#61; number&#10; &#125;&#41;&#41;&#10; node_affinities &#61; optional&#40;map&#40;object&#40;&#123;&#10; values &#61; list&#40;string&#41;&#10; in &#61; optional&#40;bool, true&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; spot &#61; optional&#40;bool, false&#41;&#10; termination_action &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; allow_stopping_for_update &#61; true&#10; deletion_protection &#61; false&#10; spot &#61; false&#10; termination_action &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [project_number](variables.tf#L379) | Project number. Used in tag bindings to avoid a permadiff. | <code>string</code> | | <code>null</code> |
| [scratch_disks](variables.tf#L385) | Scratch disks configuration. | <code title="object&#40;&#123;&#10; count &#61; number&#10; interface &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; count &#61; 0&#10; interface &#61; &#34;NVME&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [service_account](variables.tf#L397) | Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account. | <code title="object&#40;&#123;&#10; auto_create &#61; optional&#40;bool, false&#41;&#10; email &#61; optional&#40;string&#41;&#10; scopes &#61; optional&#40;list&#40;string&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [shielded_config](variables.tf#L407) | Shielded VM configuration of the instances. | <code title="object&#40;&#123;&#10; enable_secure_boot &#61; bool&#10; enable_vtpm &#61; bool&#10; enable_integrity_monitoring &#61; bool&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [snapshot_schedules](variables.tf#L417) | Snapshot schedule resource policies that can be attached to disks. | <code title="map&#40;object&#40;&#123;&#10; schedule &#61; object&#40;&#123;&#10; daily &#61; optional&#40;object&#40;&#123;&#10; days_in_cycle &#61; number&#10; start_time &#61; string&#10; &#125;&#41;&#41;&#10; hourly &#61; optional&#40;object&#40;&#123;&#10; hours_in_cycle &#61; number&#10; start_time &#61; string&#10; &#125;&#41;&#41;&#10; weekly &#61; optional&#40;list&#40;object&#40;&#123;&#10; day &#61; string&#10; start_time &#61; string&#10; &#125;&#41;&#41;&#41;&#10; &#125;&#41;&#10; description &#61; optional&#40;string&#41;&#10; retention_policy &#61; optional&#40;object&#40;&#123;&#10; max_retention_days &#61; number&#10; on_source_disk_delete_keep &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10; snapshot_properties &#61; optional&#40;object&#40;&#123;&#10; chain_name &#61; optional&#40;string&#41;&#10; guest_flush &#61; optional&#40;bool&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; storage_locations &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [tag_bindings](variables.tf#L460) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance and zonal disks, and modifiable without impacting the main resource lifecycle. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [tag_bindings_immutable](variables.tf#L467) | Immutable resource manager tag bindings, in tagKeys/id => tagValues/id format. These are set on the instance or instance template at creation time, and trigger recreation if changed. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [tags](variables.tf#L481) | Instance network tags for firewall rule targets. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
## Outputs

View File

@@ -158,6 +158,7 @@ resource "google_compute_instance" "default" {
enable_display = var.enable_display
labels = var.labels
metadata = var.metadata
metadata_startup_script = var.metadata_startup_script
resource_policies = local.ischedule_attach
dynamic "advanced_machine_features" {

View File

@@ -20,19 +20,20 @@ locals {
}
resource "google_compute_instance_template" "default" {
provider = google-beta
count = local.template_create && !local.template_regional ? 1 : 0
project = var.project_id
region = local.region
name_prefix = "${var.name}-"
description = var.description
tags = var.tags
machine_type = var.instance_type
min_cpu_platform = var.min_cpu_platform
can_ip_forward = var.can_ip_forward
metadata = var.metadata
labels = var.labels
resource_manager_tags = var.tag_bindings_immutable
provider = google-beta
count = local.template_create && !local.template_regional ? 1 : 0
project = var.project_id
region = local.region
name_prefix = "${var.name}-"
description = var.description
tags = var.tags
machine_type = var.instance_type
min_cpu_platform = var.min_cpu_platform
can_ip_forward = var.can_ip_forward
metadata = var.metadata
metadata_startup_script = var.metadata_startup_script
labels = var.labels
resource_manager_tags = var.tag_bindings_immutable
dynamic "advanced_machine_features" {
for_each = local.advanced_mf != null ? [""] : []
@@ -211,19 +212,20 @@ resource "google_compute_instance_template" "default" {
}
resource "google_compute_region_instance_template" "default" {
provider = google-beta
count = local.template_create && local.template_regional ? 1 : 0
project = var.project_id
region = local.region
name_prefix = "${var.name}-"
description = var.description
tags = var.tags
machine_type = var.instance_type
min_cpu_platform = var.min_cpu_platform
can_ip_forward = var.can_ip_forward
metadata = var.metadata
labels = var.labels
resource_manager_tags = var.tag_bindings_immutable
provider = google-beta
count = local.template_create && local.template_regional ? 1 : 0
project = var.project_id
region = local.region
name_prefix = "${var.name}-"
description = var.description
tags = var.tags
machine_type = var.instance_type
min_cpu_platform = var.min_cpu_platform
can_ip_forward = var.can_ip_forward
metadata = var.metadata
metadata_startup_script = var.metadata_startup_script
labels = var.labels
resource_manager_tags = var.tag_bindings_immutable
dynamic "advanced_machine_features" {
for_each = local.advanced_mf != null ? [""] : []

View File

@@ -261,6 +261,13 @@ variable "metadata" {
default = {}
}
variable "metadata_startup_script" {
description = "Instance startup script. Will trigger recreation on change, even after importing."
type = string
nullable = true
default = null
}
variable "min_cpu_platform" {
description = "Minimum CPU platform."
type = string

View File

@@ -2,8 +2,12 @@
This module allows managing a dataform repository, allows adding IAM permissions. Also enables attaching a remote repository.
## TODO
[] Add validation rules to variable.
<!-- BEGIN TOC -->
- [Examples](#examples)
- [Simple dataform repository with access configuration](#simple-dataform-repository-with-access-configuration)
- [Repository with an attached remote repository](#repository-with-an-attached-remote-repository)
- [Variables](#variables)
<!-- END TOC -->
## Examples
@@ -34,11 +38,9 @@ module "secret" {
project_id = "fast-bi-fabric"
secrets = {
my-secret = {
}
}
versions = {
my-secret = {
v1 = { enabled = true, data = "MYTOKEN" }
versions = {
v1 = { data = "MYTOKEN" }
}
}
}
}
@@ -51,10 +53,10 @@ module "dataform" {
remote_repository_settings = {
url = "my-url"
secret_name = "my-secret"
token = module.secret.version_ids["my-secret:v1"]
token = module.secret.version_ids["my-secret/v1"]
}
}
# tftest modules=2 resources=3
# tftest modules=2 resources=3 skip-tofu
```
<!-- BEGIN TFDOC -->
## Variables

View File

@@ -17,6 +17,7 @@ Due to the complexity of the underlying resources, changes to the configuration
- [Hybrid NEG creation](#hybrid-neg-creation)
- [Serverless NEG creation](#serverless-neg-creation)
- [Private Service Connect NEG creation](#private-service-connect-neg-creation)
- [Private Service Connect NEG creation with Cross-project PSC and back-end](#private-service-connect-neg-creation-with-cross-project-psc-and-back-end)
- [Internet NEG creation](#internet-neg-creation)
- [URL Map](#url-map)
- [SSL Certificates](#ssl-certificates)
@@ -443,6 +444,45 @@ module "ilb-l7" {
# tftest modules=1 resources=5 e2e
```
#### Private Service Connect NEG creation with Cross-project PSC and back-end
This example shows how to create the load balancer in one project `prj-host` while using a shared VPC deployed in the `prj-svc` project. Please note that the load balancer and its front-end will be created in the `prj-host` project and the back-end will be created in the `prj-svc` project. This is useful for situations where a shared VPC is being used that has been deployed in another project. Two subnetworks are needed, one for the loab balancer and another one for the PSC endpoint.
```hcl
module "ilb-l7" {
source = "./fabric/modules/net-lb-app-int"
name = "ilb-test"
project_id = "prj-host"
region = "us-central1"
backend_service_configs = {
default = {
project_id = "prj-svc"
backends = [{
group = "neg-01"
}]
health_check_configs = {}
neg_configs = {
neg-01 = {
project_id = "prj-svc"
description = "Network Endpoint Group for service accessed using Private Service Connect"
psc = {
region = "us-central1"
target_service = "projects/producer_project/regions/us-central1/serviceAttachments/project_id"
network = var.vpc.self_link
subnetwork = "projects/prj-svc/regions/us-central1/subnetworks/psc_subnet"
}
}
}
}
}
vpc_config = {
network = var.vpc.self_link
subnetwork = var.subnet.self_link
}
}
```
#### Internet NEG creation
This example shows how to create and manage internet NEGs:

View File

@@ -207,9 +207,13 @@ resource "google_compute_region_network_endpoint_group" "default" {
resource "google_compute_region_network_endpoint_group" "psc" {
for_each = local.neg_regional_psc
project = var.project_id
region = each.value.psc.region
name = "${var.name}-${each.key}"
project = (
each.value.project_id == null
? var.project_id
: each.value.project_id
)
region = each.value.psc.region
name = "${var.name}-${each.key}"
//description = coalesce(each.value.description, var.description)
network_endpoint_type = "PRIVATE_SERVICE_CONNECT"
psc_target_service = each.value.psc.target_service
@@ -219,7 +223,6 @@ resource "google_compute_region_network_endpoint_group" "psc" {
# ignore until https://github.com/hashicorp/terraform-provider-google/issues/20576 is fixed
ignore_changes = [psc_data]
}
}
locals {

View File

@@ -1,16 +1,13 @@
# Google Secret Manager Module
# Google Secret Manager
Simple Secret Manager module that allows managing one or more secrets, their versions, and IAM bindings.
Secret Manager locations are available via the `gcloud secrets locations list` command.
**Warning:** managing versions will persist their data (the actual secret you want to protect) in the Terraform state in unencrypted form, accessible to any identity able to read or pull the state file.
This module allows managing one or more secrets with versions and IAM bindings. For global secrets, this module optionally supports [write-only attributes](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only) for versions, which do not save data in state. Write-only attributes are not yet supported in OpenTofu, so this module is only compatible with Terraform until [OpenTofu support](https://github.com/opentofu/opentofu/issues/2834) has been released.
<!-- BEGIN TOC -->
- [Secrets](#secrets)
- [Secret IAM bindings](#secret-iam-bindings)
- [Secret versions](#secret-versions)
- [Secret with customer managed encryption key](#secret-with-customer-managed-encryption-key)
- [Global Secrets](#global-secrets)
- [Regional Secrets](#regional-secrets)
- [IAM Bindings](#iam-bindings)
- [Secret Versions](#secret-versions)
- [Context Interpolations](#context-interpolations)
- [Variables](#variables)
- [Outputs](#outputs)
- [Requirements](#requirements)
@@ -18,9 +15,11 @@ Secret Manager locations are available via the `gcloud secrets locations list` c
- [APIs](#apis)
<!-- END TOC -->
## Secrets
## Global Secrets
The secret replication policy is automatically managed if no location is set, or manually managed if a list of locations is passed to the secret.
Secrets are created as global by default, with auto replication policy. For auto managed replication secrets the `kms_key` attribute can be used to configure CMEK via a global key.
To configure a secret for user managed replication configure the `global_replica_locations` attribute. Non-auto secrets ignore the `kms_key` attribute, but use each element of the locations map to configure keys.
```hcl
module "secret-manager" {
@@ -28,183 +27,207 @@ module "secret-manager" {
project_id = var.project_id
secrets = {
test-auto = {}
test-manual = {
expire_time = "2025-10-02T15:01:23Z"
locations = [var.regions.primary, var.regions.secondary]
test-auto-cmek = {
kms_key = "projects/test-0/locations/global/keyRings/test-g/cryptoKeys/sec"
}
}
}
# tftest modules=1 resources=2 inventory=secret.yaml e2e
```
## Secret IAM bindings
IAM bindings can be set per secret in the same way as for most other modules supporting IAM, using the `iam` variable.
```hcl
module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
test-auto = {}
test-manual = {
locations = [var.regions.primary, var.regions.secondary]
}
}
iam = {
test-auto = {
"roles/secretmanager.secretAccessor" = ["group:${var.group_email}"]
}
test-manual = {
"roles/secretmanager.secretAccessor" = ["group:${var.group_email}"]
}
}
}
# tftest modules=1 resources=4 inventory=iam.yaml e2e
```
## Secret versions
As mentioned above, please be aware that **version data will be stored in state in unencrypted form**.
```hcl
module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
test-auto = {}
test-manual = {
locations = [var.regions.primary, var.regions.secondary]
}
}
versions = {
test-auto = {
v1 = { enabled = false, data = "auto foo bar baz" }
v2 = { enabled = true, data = "auto foo bar spam" }
},
test-manual = {
v1 = { enabled = true, data = "manual foo bar spam" }
}
}
}
# tftest modules=1 resources=5 inventory=versions.yaml e2e
```
## Secret with customer managed encryption key
CMEK will be used if an encryption key is set in the `keys` field of `secrets` object for the secret region. For secrets with auto-replication, a global key must be specified.
```hcl
module "project" {
source = "./fabric/modules/project"
name = "sec-mgr"
billing_account = var.billing_account_id
prefix = var.prefix
parent = var.folder_id
services = [
"cloudkms.googleapis.com",
"secretmanager.googleapis.com",
]
}
module "kms-global" {
source = "./fabric/modules/kms"
project_id = module.project.project_id
keyring = {
location = "global"
name = "${var.prefix}-keyring-global"
}
keys = {
"key-global" = {
}
}
iam = {
"roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
module.project.service_agents.secretmanager.iam_email
]
}
}
module "kms-primary-region" {
source = "./fabric/modules/kms"
project_id = module.project.project_id
keyring = {
location = var.regions.primary
name = "${var.prefix}-keyring-regional"
}
keys = {
"key-regional" = {
}
}
iam = {
"roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
module.project.service_agents.secretmanager.iam_email
]
}
}
module "kms-secondary-region" {
source = "./fabric/modules/kms"
project_id = module.project.project_id
keyring = {
location = var.regions.secondary
name = "${var.prefix}-keyring-regional"
}
keys = {
"key-regional" = {
}
}
iam = {
"roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
module.project.service_agents.secretmanager.iam_email
]
}
}
module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = module.project.project_id
secrets = {
test-auto = {
keys = {
global = module.kms-global.keys.key-global.id
test-user = {
global_replica_locations = {
europe-west1 = null
europe-west3 = null
}
}
test-auto-nokeys = {}
test-manual = {
locations = [var.regions.primary, var.regions.secondary]
keys = {
"${var.regions.primary}" = module.kms-primary-region.keys.key-regional.id
"${var.regions.secondary}" = module.kms-secondary-region.keys.key-regional.id
test-user-cmek = {
global_replica_locations = {
europe-west1 = "projects/test-0/locations/europe-west1/keyRings/test-g/cryptoKeys/sec-ew1"
europe-west3 = "projects/test-0/locations/europe-west3/keyRings/test-g/cryptoKeys/sec-ew3"
}
}
}
}
# tftest inventory=secret-cmek.yaml e2e
# tftest modules=1 resources=4 inventory=secret.yaml skip-tofu
```
## Regional Secrets
Regional secrets are identified by having the `location` attribute defined, and share the same interface with a few exceptions: the `global_replica_locations` is of course ignored, and versions only support a subset of attributes and can't use write-only attributes.
```hcl
module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
test = {
location = "europe-west1"
}
test-cmek = {
location = "europe-west1"
kms_key = "projects/test-0/locations/global/keyRings/test-g/cryptoKeys/sec"
}
}
}
# tftest modules=1 resources=2 inventory=secret-regional.yaml skip-tofu
```
## IAM Bindings
This module supports the same IAM interface as all other modules in this repository. IAM bindings are defined per secret, if you need cross-secret IAM bindings use project-level ones.
```hcl
module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
test = {
iam = {
"roles/secretmanager.admin" = [
"user:test-0@example.com"
]
}
iam_bindings = {
test = {
role = "roles/secretmanager.secretAccessor"
members = [
"user:test-1@example.com"
]
condition = {
title = "Test."
expression = "resource.matchTag('1234567890/environment', 'test')"
}
}
}
iam_bindings_additive = {
test = {
role = "roles/secretmanager.viewer"
member = "user:test-2@example.com"
}
}
}
}
}
# tftest modules=1 resources=4 inventory=iam.yaml skip-tofu
```
## Secret Versions
Versions are defined per secret via the `versions` attribute, and by default they accept string data which is stored in state. The `data_config` attributes allow configuring each secret:
- `data_config.is_file` instructs the module to read version data from a file (`data` is then used as the file path)
- `data_config.is_base64` instructs the provider to treat data as Base64
- `data_config.write_only_version` instructs the module to **use write-only attributes so that data is not set in state**, each time the write-only version is changed data is reuploaded to the secret version
As mentioned before write-only attributes are only available for global secrets. Regional secrets still use the potentially insecure way of storing data.
```hcl
module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
test = {
versions = {
a = {
# potentially unsafe
data = "foo"
}
b = {
# potentially unsafe, reads from file
data = "test-data/secret-b.txt"
data_config = {
is_file = true
}
}
c = {
# uses safer write-only attribute
data = "bar"
data_config = {
# bump this version when data needs updating
write_only_version = 1
}
}
}
}
}
}
# tftest files=0 modules=1 resources=4 inventory=versions.yaml skip-tofu
```
```txt
foo-secret
# tftest-file id=0 path=test-data/secret-b.txt
```
## Context Interpolations
Similarly to other core modules in this repository, this module also supports context-based interpolations, which are populated via the `context` variable.
This is a summary table of the available contexts, which can be used whenever an attribute expects the relevant information. Refer to the [project factory module](../project-factory/README.md#context-based-interpolation) for more details on context replacements.
- `$custom_roles:my_role`
- `$iam_principals:my_principal`
- `$kms_keys:my_key`
- `$locations:my_location`
- `$project_ids:my_project`
- `$tag_keys:my_key`
- `$tag_values:my_value`
- custom template variables used in IAM conditions
This is a simple example that uses context interpolation.
```hcl
module "secret-manager" {
source = "./fabric/modules/secret-manager"
context = {
iam_principals = {
mysa = "serviceAccount:test@foo-prod-test-0.iam.gserviceaccount.com"
myuser = "user:test@example.com"
}
kms_keys = {
primary = "projects/test-0/locations/europe-west1/keyRings/test-g/cryptoKeys/sec-ew1"
secondary = "projects/test-0/locations/europe-west3/keyRings/test-g/cryptoKeys/sec-ew3"
}
locations = {
primary = "europe-west1"
secondary = "europe-west3"
}
project_ids = {
test = "foo-prod-test-0"
}
}
project_id = "$project_ids:test"
secrets = {
test-user-cmek = {
global_replica_locations = {
"$locations:primary" = "$kms_keys:primary"
"$locations:secondary" = "$kms_keys:secondary"
}
iam = {
"roles/secretmanager.viewer" = [
"$iam_principals:mysa", "$iam_principals:myuser"
]
}
}
}
}
# tftest modules=1 resources=2 inventory=context.yaml skip-tofu
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [project_id](variables.tf#L29) | Project id where the keyring will be created. | <code>string</code> | ✓ | |
| [iam](variables.tf#L17) | IAM bindings in {SECRET => {ROLE => [MEMBERS]}} format. | <code>map&#40;map&#40;list&#40;string&#41;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [labels](variables.tf#L23) | Optional labels for each secret. | <code>map&#40;map&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [project_number](variables.tf#L34) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. | <code>string</code> | | <code>null</code> |
| [secrets](variables.tf#L40) | Map of secrets to manage, their optional expire time, version destroy ttl, locations and KMS keys in {LOCATION => KEY} format. {GLOBAL => KEY} format enables CMEK for automatic managed secrets. If locations is null, automatic management will be set. | <code title="map&#40;object&#40;&#123;&#10; expire_time &#61; optional&#40;string&#41;&#10; locations &#61; optional&#40;list&#40;string&#41;&#41;&#10; keys &#61; optional&#40;map&#40;string&#41;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; version_destroy_ttl &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [versions](variables.tf#L52) | Optional versions to manage for each secret. Version names are only used internally to track individual versions. | <code title="map&#40;map&#40;object&#40;&#123;&#10; enabled &#61; bool&#10; data &#61; string&#10;&#125;&#41;&#41;&#41;">map&#40;map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [project_id](variables.tf#L40) | Project id where the keyring will be created. | <code>string</code> | ✓ | |
| [context](variables.tf#L17) | Context-specific interpolations. | <code title="object&#40;&#123;&#10; condition_vars &#61; optional&#40;map&#40;map&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; custom_roles &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; kms_keys &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; project_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_keys &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [project_number](variables.tf#L45) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. | <code>string</code> | | <code>null</code> |
| [secrets](variables.tf#L51) | Map of secrets to manage. Defaults to global secrets unless region is set. | <code title="map&#40;object&#40;&#123;&#10; annotations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; deletion_protection &#61; optional&#40;bool&#41;&#10; kms_key &#61; optional&#40;string&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; global_replica_locations &#61; optional&#40;map&#40;string&#41;&#41;&#10; location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; tags &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; expiration_config &#61; optional&#40;object&#40;&#123;&#10; time &#61; optional&#40;string&#41;&#10; ttl &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;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;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;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;, &#123;&#125;&#41;&#10; version_config &#61; optional&#40;object&#40;&#123;&#10; aliases &#61; optional&#40;map&#40;number&#41;&#41;&#10; destroy_ttl &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; versions &#61; optional&#40;map&#40;object&#40;&#123;&#10; data &#61; string&#10; deletion_policy &#61; optional&#40;string&#41;&#10; enabled &#61; optional&#40;bool&#41;&#10; data_config &#61; optional&#40;object&#40;&#123;&#10; is_base64 &#61; optional&#40;bool, false&#41;&#10; is_file &#61; optional&#40;bool, false&#41;&#10; write_only_version &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [ids](outputs.tf#L17) | Fully qualified secret ids. | |
| [secrets](outputs.tf#L27) | Secret resources. | |
| [version_ids](outputs.tf#L36) | Version ids keyed by secret name : version name. | |
| [version_versions](outputs.tf#L46) | Version versions keyed by secret name : version name. | |
| [versions](outputs.tf#L56) | Secret versions. | ✓ |
| [ids](outputs.tf#L28) | Fully qualified secret ids. | |
| [secrets](outputs.tf#L41) | Secret resources. | |
| [version_ids](outputs.tf#L54) | Fully qualified version ids. | |
| [version_versions](outputs.tf#L67) | Version versions. | |
| [versions](outputs.tf#L80) | Version resources. | ✓ |
<!-- END TFDOC -->
## Requirements

View File

@@ -0,0 +1,131 @@
/**
* 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.
* 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.
*/
locals {
_tag_template_global = (
"//secretmanager.googleapis.com/projects/%s/secrets/%s"
)
}
resource "google_secret_manager_secret" "default" {
for_each = { for k, v in var.secrets : k => v if v.location == null }
project = local.project_id
secret_id = each.key
labels = each.value.labels
annotations = each.value.annotations
version_aliases = try(each.value.version_config.aliases, null)
version_destroy_ttl = try(each.value.version_config.destroy_ttl, null)
expire_time = try(each.value.expiration_config.time, null)
ttl = try(each.value.expiration_config.ttl, null)
tags = {
for k, v in each.value.tags :
lookup(local.ctx.tag_keys, k, k) => lookup(local.ctx.tag_values, v, v)
}
deletion_protection = each.value.deletion_protection
dynamic "replication" {
for_each = try(each.value.global_replica_locations, null) == null ? [""] : []
content {
auto {
dynamic "customer_managed_encryption" {
for_each = each.value.kms_key == null ? [] : [""]
content {
kms_key_name = lookup(
local.ctx.kms_keys, each.value.kms_key, each.value.kms_key
)
}
}
}
}
}
dynamic "replication" {
for_each = try(each.value.global_replica_locations, null) != null ? [""] : []
content {
user_managed {
dynamic "replicas" {
for_each = each.value.global_replica_locations
content {
location = lookup(local.ctx.locations, replicas.key, replicas.key)
dynamic "customer_managed_encryption" {
for_each = replicas.value == null ? [] : [""]
content {
kms_key_name = lookup(
local.ctx.kms_keys, replicas.value, replicas.value
)
}
}
}
}
}
}
}
# dynamic "rotation" {
# for_each = try(each.value.rotation_config, null) == null ? [] : [""]
# content {
# next_rotation_time = each.value.rotation_config.next_time
# rotation_period = each.value.rotation_config.period
# }
# }
# topics
lifecycle {
ignore_changes = [
rotation[0].next_rotation_time
]
}
}
resource "google_secret_manager_secret_version" "default" {
for_each = {
for v in local.versions :
"${v.secret}/${v.version}" => v if v.location == null
}
secret = google_secret_manager_secret.default[each.value.secret].id
deletion_policy = each.value.deletion_policy
enabled = each.value.enabled
is_secret_data_base64 = try(
each.value.data_config.is_base64, null
)
secret_data_wo_version = try(
each.value.data_config.write_only_version, null
)
secret_data = (
try(each.value.data_config.write_only_version, null) != null
? null
: (
try(each.value.data_config.is_file, null) == true
? file(each.value.data)
: each.value.data
)
)
secret_data_wo = (
try(each.value.data_config.write_only_version, null) == null
? null
: (
try(each.value.data_config.is_file, null) == true
? file(each.value.data)
: each.value.data
)
)
}
resource "google_tags_tag_binding" "binding" {
for_each = { for k, v in local.tag_bindings : k => v if v.location == null }
parent = format(
local._tag_template_global,
local.tag_project,
google_secret_manager_secret.default[each.value.secret].secret_id
)
tag_value = lookup(local.ctx.tag_values, each.value.tag, each.value.tag)
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright 2023 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.
*/
resource "google_secret_manager_regional_secret_iam_binding" "authoritative" {
for_each = {
for binding in local.secret_iam :
"${binding.secret}.${binding.role}" => binding if !binding.global
}
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
secret_id = google_secret_manager_regional_secret.default[each.value.secret].id
location = google_secret_manager_regional_secret.default[each.value.secret].location
members = [
for v in each.value.members :
lookup(local.ctx.iam_principals, v, v)
]
}
resource "google_secret_manager_regional_secret_iam_binding" "bindings" {
for_each = {
for k, v in local.secret_iam_bindings : k => v if !v.global
}
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
secret_id = google_secret_manager_regional_secret.default[each.value.secret].id
location = google_secret_manager_regional_secret.default[each.value.secret].location
members = [
for v in each.value.members :
lookup(local.ctx.iam_principals, v, v)
]
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = templatestring(
each.value.condition.expression, var.context.condition_vars
)
title = each.value.condition.title
description = each.value.condition.description
}
}
}
resource "google_secret_manager_regional_secret_iam_member" "members" {
for_each = {
for k, v in local.secret_iam_bindings_additive : k => v if !v.global
}
secret_id = google_secret_manager_regional_secret.default[each.value.secret].id
location = google_secret_manager_regional_secret.default[each.value.secret].location
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
member = lookup(local.ctx.iam_principals, each.value.member, each.value.member)
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = templatestring(
each.value.condition.expression, var.context.condition_vars
)
title = each.value.condition.title
description = each.value.condition.description
}
}
}

View File

@@ -0,0 +1,106 @@
/**
* Copyright 2023 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.
*/
locals {
secret_iam = flatten([
for k, v in var.secrets : [
for role, members in v.iam : {
secret = k
role = role
members = members
global = v.location == null
}
]
])
secret_iam_bindings = merge([
for k, v in var.secrets : {
for binding_key, data in v.iam_bindings :
"${k}-${binding_key}" => {
secret = k
role = data.role
members = data.members
condition = data.condition
global = v.location == null
}
}
]...)
secret_iam_bindings_additive = merge([
for k, v in var.secrets : {
for binding_key, data in v.iam_bindings_additive :
"${k}-${binding_key}" => {
secret = k
role = data.role
member = data.member
condition = data.condition
global = v.location == null
}
}
]...)
}
resource "google_secret_manager_secret_iam_binding" "authoritative" {
for_each = {
for binding in local.secret_iam :
"${binding.secret}.${binding.role}" => binding if binding.global
}
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
secret_id = google_secret_manager_secret.default[each.value.secret].id
members = [
for v in each.value.members :
lookup(local.ctx.iam_principals, v, v)
]
}
resource "google_secret_manager_secret_iam_binding" "bindings" {
for_each = {
for k, v in local.secret_iam_bindings : k => v if v.global
}
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
secret_id = google_secret_manager_secret.default[each.value.secret].id
members = [
for v in each.value.members :
lookup(local.ctx.iam_principals, v, v)
]
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = templatestring(
each.value.condition.expression, var.context.condition_vars
)
title = each.value.condition.title
description = each.value.condition.description
}
}
}
resource "google_secret_manager_secret_iam_member" "members" {
for_each = {
for k, v in local.secret_iam_bindings_additive : k => v if v.global
}
secret_id = google_secret_manager_secret.default[each.value.secret].id
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
member = lookup(local.ctx.iam_principals, each.value.member, each.value.member)
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = templatestring(
each.value.condition.expression, var.context.condition_vars
)
title = each.value.condition.title
description = each.value.condition.description
}
}
}

View File

@@ -15,99 +15,40 @@
*/
locals {
# distinct is needed to make the expanding function argument work
iam = flatten([
for secret, roles in var.iam : [
for role, members in roles : {
secret = secret
role = role
members = members
}
]
])
ctx = {
for k, v in var.context : k => {
for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv
} if k != "condition_vars"
}
ctx_p = "$"
project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id)
tag_project = coalesce(var.project_number, var.project_id)
tag_bindings = merge([
for k, v in var.secrets : {
for kk, vv in v.tag_bindings : "${k}/${kk}" => {
parent = "//secretmanager.googleapis.com/projects/${coalesce(var.project_number, var.project_id)}/secrets/${google_secret_manager_secret.default[k].secret_id}"
tag_value = vv
location = v.location
secret = k
tag = vv
}
}
if v.tag_bindings != null
]...)
version_pairs = flatten([
for secret, versions in var.versions : [
for name, attrs in versions : merge(attrs, { name = name, secret = secret })
versions = flatten([
for k, v in var.secrets : [
for sk, sv in v.versions : merge(sv, {
secret = k
version = sk
location = v.location
})
]
])
version_keypairs = {
for pair in local.version_pairs : "${pair.secret}:${pair.name}" => pair
}
}
resource "google_secret_manager_secret" "default" {
for_each = var.secrets
project = var.project_id
secret_id = each.key
labels = lookup(var.labels, each.key, null)
expire_time = each.value.expire_time
version_destroy_ttl = each.value.version_destroy_ttl
dynamic "replication" {
for_each = each.value.locations == null ? [""] : []
content {
auto {
dynamic "customer_managed_encryption" {
for_each = try(lookup(each.value.keys, "global", null) == null ? [] : [""], [])
content {
kms_key_name = each.value.keys["global"]
}
}
}
}
}
dynamic "replication" {
for_each = each.value.locations == null ? [] : [""]
content {
user_managed {
dynamic "replicas" {
for_each = each.value.locations
iterator = location
content {
location = location.value
dynamic "customer_managed_encryption" {
for_each = try(lookup(each.value.keys, location.value, null) == null ? [] : [""], [])
content {
kms_key_name = each.value.keys[location.value]
}
}
}
}
}
}
}
}
resource "google_secret_manager_secret_version" "default" {
provider = google-beta
for_each = local.version_keypairs
secret = google_secret_manager_secret.default[each.value.secret].id
enabled = each.value.enabled
secret_data = each.value.data
}
resource "google_secret_manager_secret_iam_binding" "default" {
provider = google-beta
for_each = {
for binding in local.iam : "${binding.secret}.${binding.role}" => binding
}
role = each.value.role
secret_id = google_secret_manager_secret.default[each.value.secret].id
members = each.value.members
}
resource "google_tags_tag_binding" "binding" {
for_each = local.tag_bindings
parent = each.value.parent
tag_value = each.value.tag_value
}
# resource "google_kms_key_handle" "my_key_handle" {
# provider = google-beta
# for_each = var.kms_autokey_config
# project = var.project_id
# name = each.key
# location = each.value
# resource_type_selector = "secretmanager.googleapis.com/Secret"
# }

View File

@@ -14,50 +14,79 @@
* limitations under the License.
*/
locals {
o_secrets = merge(
google_secret_manager_secret.default,
google_secret_manager_regional_secret.default
)
o_versions = merge(
google_secret_manager_secret_version.default,
google_secret_manager_regional_secret_version.default
)
}
output "ids" {
description = "Fully qualified secret ids."
value = {
for k, v in google_secret_manager_secret.default : v.secret_id => v.id
}
value = { for k, v in local.o_secrets : k => v.id }
depends_on = [
google_secret_manager_secret_iam_binding.default
google_secret_manager_secret_iam_binding.authoritative,
google_secret_manager_secret_iam_binding.bindings,
google_secret_manager_secret_iam_member.members,
google_secret_manager_regional_secret_iam_binding.authoritative,
google_secret_manager_regional_secret_iam_binding.bindings,
google_secret_manager_regional_secret_iam_member.members
]
}
output "secrets" {
description = "Secret resources."
value = google_secret_manager_secret.default
value = local.o_secrets
depends_on = [
google_secret_manager_secret_iam_binding.default
google_secret_manager_secret_iam_binding.authoritative,
google_secret_manager_secret_iam_binding.bindings,
google_secret_manager_secret_iam_member.members,
google_secret_manager_regional_secret_iam_binding.authoritative,
google_secret_manager_regional_secret_iam_binding.bindings,
google_secret_manager_regional_secret_iam_member.members
]
}
output "version_ids" {
description = "Version ids keyed by secret name : version name."
value = {
for k, v in google_secret_manager_secret_version.default : k => v.id
}
description = "Fully qualified version ids."
value = { for k, v in local.o_versions : k => v.id }
depends_on = [
google_secret_manager_secret_iam_binding.default
google_secret_manager_secret_iam_binding.authoritative,
google_secret_manager_secret_iam_binding.bindings,
google_secret_manager_secret_iam_member.members,
google_secret_manager_regional_secret_iam_binding.authoritative,
google_secret_manager_regional_secret_iam_binding.bindings,
google_secret_manager_regional_secret_iam_member.members
]
}
output "version_versions" {
description = "Version versions keyed by secret name : version name."
value = {
for k, v in google_secret_manager_secret_version.default : k => v.version
}
description = "Version versions."
value = { for k, v in local.o_versions : k => v.version }
depends_on = [
google_secret_manager_secret_iam_binding.default
google_secret_manager_secret_iam_binding.authoritative,
google_secret_manager_secret_iam_binding.bindings,
google_secret_manager_secret_iam_member.members,
google_secret_manager_regional_secret_iam_binding.authoritative,
google_secret_manager_regional_secret_iam_binding.bindings,
google_secret_manager_regional_secret_iam_member.members
]
}
output "versions" {
description = "Secret versions."
value = google_secret_manager_secret_version.default
description = "Version resources."
value = local.o_versions
sensitive = true
depends_on = [
google_secret_manager_secret_iam_binding.default
google_secret_manager_secret_iam_binding.authoritative,
google_secret_manager_secret_iam_binding.bindings,
google_secret_manager_secret_iam_member.members,
google_secret_manager_regional_secret_iam_binding.authoritative,
google_secret_manager_regional_secret_iam_binding.bindings,
google_secret_manager_regional_secret_iam_member.members
]
}

View File

@@ -0,0 +1,91 @@
/**
* 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.
* 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.
*/
locals {
_tag_template_regional = (
"//secretmanager.googleapis.com/projects/%s/locations/%s/secrets/%s"
)
}
resource "google_secret_manager_regional_secret" "default" {
for_each = { for k, v in var.secrets : k => v if v.location != null }
project = local.project_id
location = lookup(
local.ctx.locations, each.value.location, each.value.location
)
secret_id = each.key
labels = each.value.labels
annotations = each.value.annotations
version_aliases = try(each.value.version_config.aliases, null)
version_destroy_ttl = try(each.value.version_config.destroy_ttl, null)
expire_time = try(each.value.expiration_config.time, null)
ttl = try(each.value.expiration_config.ttl, null)
tags = each.value.tags
deletion_protection = each.value.deletion_protection
dynamic "customer_managed_encryption" {
for_each = each.value.kms_key == null ? [] : [""]
content {
kms_key_name = lookup(
local.ctx.kms_keys, each.value.kms_key, each.value.kms_key
)
}
}
# dynamic "rotation" {
# for_each = try(each.value.rotation_config, null) == null ? [] : [""]
# content {
# next_rotation_time = each.value.rotation_config.next_time
# rotation_period = each.value.rotation_config.period
# }
# }
# topics
lifecycle {
ignore_changes = [
rotation[0].next_rotation_time
]
}
}
resource "google_secret_manager_regional_secret_version" "default" {
for_each = {
for v in local.versions :
"${v.secret}/${v.version}" => v if v.location != null
}
secret = google_secret_manager_regional_secret.default[each.value.secret].id
deletion_policy = each.value.deletion_policy
enabled = each.value.enabled
is_secret_data_base64 = try(
each.value.data_config.is_base64, null
)
secret_data = (
try(each.value.data_config.is_file, null) == true
? file(each.value.data)
: each.value.data
)
}
resource "google_tags_location_tag_binding" "binding" {
for_each = { for k, v in local.tag_bindings : k => v if v.location != null }
parent = format(
local._tag_template_regional,
local.tag_project,
lookup(local.ctx.locations, each.value.location, each.value.location),
google_secret_manager_regional_secret.default[each.value.secret].secret_id
)
location = lookup(local.ctx.locations, each.value.location, each.value.location)
tag_value = lookup(local.ctx.tag_values, each.value.tag, each.value.tag)
}

View File

@@ -14,17 +14,28 @@
* limitations under the License.
*/
variable "iam" {
description = "IAM bindings in {SECRET => {ROLE => [MEMBERS]}} format."
type = map(map(list(string)))
default = {}
variable "context" {
description = "Context-specific interpolations."
type = object({
condition_vars = optional(map(map(string)), {})
custom_roles = optional(map(string), {})
iam_principals = optional(map(string), {})
kms_keys = optional(map(string), {})
locations = optional(map(string), {})
project_ids = optional(map(string), {})
tag_keys = optional(map(string), {})
tag_values = optional(map(string), {})
})
default = {}
nullable = false
}
variable "labels" {
description = "Optional labels for each secret."
type = map(map(string))
default = {}
}
# variable "kms_autokey_config" {
# description = "Key handle definitions for KMS autokey, in name => location format. Injected in the context $kms_keys:autokey/ namespace."
# type = map(string)
# nullable = false
# default = {}
# }
variable "project_id" {
description = "Project id where the keyring will be created."
@@ -38,22 +49,83 @@ variable "project_number" {
}
variable "secrets" {
description = "Map of secrets to manage, their optional expire time, version destroy ttl, locations and KMS keys in {LOCATION => KEY} format. {GLOBAL => KEY} format enables CMEK for automatic managed secrets. If locations is null, automatic management will be set."
description = "Map of secrets to manage. Defaults to global secrets unless region is set."
type = map(object({
expire_time = optional(string)
locations = optional(list(string))
keys = optional(map(string))
tag_bindings = optional(map(string))
version_destroy_ttl = optional(string)
annotations = optional(map(string), {})
deletion_protection = optional(bool)
kms_key = optional(string)
labels = optional(map(string), {})
global_replica_locations = optional(map(string))
location = optional(string)
tag_bindings = optional(map(string))
tags = optional(map(string), {})
expiration_config = optional(object({
time = optional(string)
ttl = optional(string)
}))
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)
}))
})), {})
version_config = optional(object({
aliases = optional(map(number))
destroy_ttl = optional(string)
}), {})
versions = optional(map(object({
data = string
deletion_policy = optional(string)
enabled = optional(bool)
data_config = optional(object({
is_base64 = optional(bool, false)
is_file = optional(bool, false)
write_only_version = optional(number)
}))
})), {})
# rotation_config = optional(object({
# next_time = string
# period = number
# }))
# topics
}))
default = {}
}
variable "versions" {
description = "Optional versions to manage for each secret. Version names are only used internally to track individual versions."
type = map(map(object({
enabled = bool
data = string
})))
default = {}
validation {
condition = alltrue([
for k, v in var.secrets :
try(v.expiration_config.time, null) == null ||
try(v.expiration_config.ttl, null) == null
])
error_message = "Only one of time and ttl can be set in expiration config."
}
validation {
condition = alltrue([
for k, v in var.secrets :
v.location == null || v.global_replica_locations == null
])
error_message = "Global replication cannot be configured on regional secrets."
}
validation {
condition = alltrue(flatten([
for k, v in var.secrets : [
for sk, sv in v.versions : contains(
["DELETE", "DISABLE", "ABANDON"], coalesce(sv.deletion_policy, "DELETE")
)
]
]))
error_message = "Invalid version deletion policy."
}
}