From d9342a385733547b50234915ca4b06a7a6c9feb5 Mon Sep 17 00:00:00 2001 From: Brett Kochendorfer Date: Thu, 4 Jun 2026 10:22:40 -0500 Subject: [PATCH] feat(google_gke): expose node_pools_metadata for set-once node metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `node_pools_metadata` variable (per-pool map of key/value strings) that's threaded into `google_container_node_pool.pools.node_config.metadata`. The module already has `node_config[0].metadata` in `lifecycle.ignore_changes` (intentional — GKE auto-populates metadata keys we don't want terraform to fight), so this exposure is create-time-only by design. Use case: SoC2 hardening settings like `serial-port-logging-enable=false` that we want to seed at pool creation and never change afterward. Updating the variable on an existing pool will NOT propagate; pool rotation is required to change the value. Variable description warns about this. Existing consumers see no plan diff — when a pool has no metadata entry, the local projection yields `{}` and the conditional in cluster.tf sets `metadata = null`, which terraform treats as "attribute omitted." MZCLD-3238. --- google_gke/README.md | 1 + google_gke/cluster.tf | 3 +++ google_gke/locals.tf | 2 ++ google_gke/variables.tf | 6 ++++++ 4 files changed, 12 insertions(+) diff --git a/google_gke/README.md b/google_gke/README.md index 18f8680b..9d0a5a07 100644 --- a/google_gke/README.md +++ b/google_gke/README.md @@ -414,6 +414,7 @@ module "gke" { | [node\_pools](#input\_node\_pools) | Map containing node pools, with each node pool's name (or name\_prefix if `use_name_prefix` is true) being the key and the values being that node pool's configurations. Configurable options per node pool include: `disk_size_gb` (string), `disk_type` (string), `machine_type` (string), `max_count` (number), `max_surge` (number), `max_unavailable` (number), `min_count` (number), `use_name_prefix` (bool). See locals.tf for defaults. | `list(map(string))` |
[
{
"name": "tf-default-node-pool"
}
]
| no | | [node\_pools\_guest\_accelerator](#input\_node\_pools\_guest\_accelerator) | Map containing node pools guest accelerator. Each node pool's name is the key. See locals.tf for defaults. | `map(map(string))` |
{
"tf-default-node-pool": {}
}
| no | | [node\_pools\_labels](#input\_node\_pools\_labels) | Map containing node pools non-default labels (as a map of strings). Each key is used as node pool's name prefix. See locals.tf for defaults. | `map(map(string))` |
{
"tf-default-node-pool": {}
}
| no | +| [node\_pools\_metadata](#input\_node\_pools\_metadata) | Per-node-pool node metadata (key/value strings applied at pool creation, e.g. `serial-port-logging-enable: "false"`). Keyed by node pool name. NOTE: the module includes `node_config[0].metadata` in `lifecycle.ignore_changes`, so values here are applied ONLY at pool creation — drift / variable updates do not reconcile to existing pools. Use this for set-once hardening settings (rotate the pool to change them). | `map(map(string))` | `{}` | no | | [node\_pools\_oauth\_scopes](#input\_node\_pools\_oauth\_scopes) | Map containing node pools non-default OAuth scopes (as an list). Each node pool's name is the key. See locals.tf for defaults. | `map(list(string))` |
{
"tf-default-node-pool": []
}
| no | | [node\_pools\_shielded\_instance\_config](#input\_node\_pools\_shielded\_instance\_config) | Per-node-pool shielded instance config. Keyed by node pool name. Pools not present in this map fall back to GKE provider defaults (integrity monitoring on, secure boot off). Changing shielded\_instance\_config on an existing pool forces recreation, so opt in per pool. |
map(object({
enable_secure_boot = optional(bool, true)
enable_integrity_monitoring = optional(bool, true)
}))
| `{}` | no | | [node\_pools\_spot\_enabled](#input\_node\_pools\_spot\_enabled) | Map containing node pools spot enabled. Each node pool's name is the key. See locals.tf for defaults. | `map(bool)` |
{
"tf-default-node-pool": false
}
| no | diff --git a/google_gke/cluster.tf b/google_gke/cluster.tf index 8089c9bc..94fb2f9a 100644 --- a/google_gke/cluster.tf +++ b/google_gke/cluster.tf @@ -290,6 +290,9 @@ resource "google_container_node_pool" "pools" { image_type = "COS_CONTAINERD" labels = local.node_pools_labels[each.key] logging_variant = var.enable_high_throughput_logging ? "MAX_THROUGHPUT" : "DEFAULT" + # Set-once metadata applied at pool creation. node_config[0].metadata is in + # ignore_changes below, so values here are not reconciled afterward. + metadata = length(local.node_pools_metadata[each.key]) > 0 ? local.node_pools_metadata[each.key] : null dynamic "guest_accelerator" { for_each = length(local.node_pools_guest_accelerator[each.key]) != 0 ? [1] : [] diff --git a/google_gke/locals.tf b/google_gke/locals.tf index 38bf139a..06795564 100644 --- a/google_gke/locals.tf +++ b/google_gke/locals.tf @@ -57,6 +57,8 @@ locals { node_pools_shielded_instance_config = { for node_pool in var.node_pools : node_pool.name => lookup(var.node_pools_shielded_instance_config, node_pool.name, null) } + node_pools_metadata = { for node_pool in var.node_pools : node_pool.name => lookup(var.node_pools_metadata, node_pool.name, {}) } + # Google Group for RBAC cluster_authenticator_security_group = var.google_group_name == null ? [] : [{ security_group = var.google_group_name diff --git a/google_gke/variables.tf b/google_gke/variables.tf index 8c6d50e1..a4ac5066 100644 --- a/google_gke/variables.tf +++ b/google_gke/variables.tf @@ -258,6 +258,12 @@ variable "node_pools_shielded_instance_config" { default = {} } +variable "node_pools_metadata" { + description = "Per-node-pool node metadata (key/value strings applied at pool creation, e.g. `serial-port-logging-enable: \"false\"`). Keyed by node pool name. NOTE: the module includes `node_config[0].metadata` in `lifecycle.ignore_changes`, so values here are applied ONLY at pool creation — drift / variable updates do not reconcile to existing pools. Use this for set-once hardening settings (rotate the pool to change them)." + type = map(map(string)) + default = {} +} + variable "node_pools_tags" { description = "Map containing node pools non-default tags (as an list). Each node pool's name is the key. See locals.tf for defaults." type = map(list(string))