Files
Abhishek 58301c9eda Add containerd_config support to gke-nodepool (#3973)
* Add ephemeral_storage_local_ssd_config support to modules/gke-nodepool

Adds ephemeral_storage_local_ssd_count to node_config variable and the
corresponding dynamic ephemeral_storage_local_ssd_config block in the
node pool resource, enabling use of local SSDs as ephemeral storage.

* feat(gke-nodepool): add flex_start support to node_config

Add `flex_start` as an optional bool to the `node_config` variable type
and wire it through to the `google_container_node_pool` resource's
node_config block. This enables DWS (Dynamic Workload Scheduler)
flex-start mode for node pools, used for on-demand capacity access
without requiring ProvisioningRequest objects (e.g. spot TPU pools).

* feat(gke-nodepool): add flex_start support to node_config

Add `flex_start` as an optional bool to the `node_config` variable type
and wire it through to the `google_container_node_pool` resource's
node_config block. This enables DWS (Dynamic Workload Scheduler)
flex-start mode for node pools, which allows the Cluster Autoscaler to
request capacity on-demand without requiring ProvisioningRequest objects
(unlike queued_provisioning). Typical use case is spot TPU node pools.

* feat(gke-nodepool): add advanced_machine_features support to node_config

Add `advanced_machine_features` as an optional object to the `node_config`
variable type and wire it through to the `google_container_node_pool`
resource via a dynamic block. This allows callers to configure
`threads_per_core` (e.g. set to 1 to disable hyperthreading) and
`enable_nested_virtualization` for node pools that require fine-grained
CPU threading control or nested hypervisor support.

GKE auto-sets `advanced_machine_features` (threads_per_core=1) on
ct6e/TPU machine types; exposing this field also lets consumers add it to
ignore_changes in their own lifecycle blocks to avoid forced replacements.

* feat(gke-nodepool): add containerd_config support to node_config

Add `containerd_config` as an optional object to the `node_config` variable
and wire it through to the `google_container_node_pool` resource via a
dynamic block. This allows callers to configure private registry mirrors or
custom containerd registry hosts per node pool — useful for air-gapped
environments and internal registry proxies.

The `registry_hosts` list maps each upstream server to one or more mirror
hosts, with optional `capabilities`, `override_path`, and `dial_timeout`
fields (all defaulting to sensible values).

* refactor(gke-nodepool): use maps for containerd_config registry_hosts and hosts

Convert registry_hosts and hosts from lists to maps so that the registry
server and host URLs serve as stable keys, avoiding index-shifting issues
with for_each. Add default values for capabilities, override_path, and
dial_timeout. Update README example and test inventory accordingly.

* Remove default values from containerd_config hosts fields

Leave capabilities, override_path, and dial_timeout without defaults
so the provider/API picks them rather than the module imposing values.

* Refine containerd_config variable interface

- Simplify header to optional(map(list(string)))
- Flatten ca, client cert/key to strings with descriptive names
- Derive private_registry_access_config enabled from ca domain config list
- Simplify writable_cgroups to optional(bool)
- Flatten gcp_secret_manager_certificate_config to string
- Remove redundant defaults where try() handles null in main.tf
- Fix long lines in main.tf to stay within 79-char limit
- Update copyright year to 2026 in inventory files

* fix(gke-nodepool): run terraform fmt to fix attribute alignment in containerd_config

* docs(gke-nodepool): regenerate README with updated variable line numbers

* fix(gke-nodepool): use coalesce instead of try for null header map in for_each

* tests(gke-nodepool): update containerd-config inventory to match actual plan output

---------

Co-authored-by: Julio Castillo <jccb@google.com>
2026-05-27 10:00:26 +00:00

445 lines
17 KiB
HCL

/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
_image = coalesce(var.node_config.image_type, "-")
image = {
is_cos = length(regexall("COS", local._image)) > 0
is_cos_containerd = (
var.node_config.image_type == null
||
length(regexall("COS_CONTAINERD", local._image)) > 0
)
is_win = length(regexall("WIN", local._image)) > 0
}
node_metadata = var.node_config.metadata == null ? null : merge(
var.node_config.metadata,
{ disable-legacy-endpoints = "true" }
)
# if no attributes passed for service account, use the GCE default
# if no email specified, create service account
service_account_email = (
var.service_account.create
? google_service_account.service_account[0].email
: var.service_account.email
)
service_account_scopes = (
var.service_account.oauth_scopes != null
? var.service_account.oauth_scopes
: [
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring",
"https://www.googleapis.com/auth/monitoring.write",
"https://www.googleapis.com/auth/userinfo.email"
]
)
service_account_display_name = (
var.service_account.display_name != null
? var.service_account.display_name
: "Terraform GKE ${var.cluster_name} ${var.name}."
)
taints = merge(var.taints, !local.image.is_win ? {} : {
"node.kubernetes.io/os" = {
value = "windows"
effect = "NO_EXECUTE"
}
})
}
resource "google_service_account" "service_account" {
count = var.service_account.create ? 1 : 0
project = var.project_id
account_id = (
var.service_account.email != null
? split("@", var.service_account.email)[0]
: "tf-gke-${var.name}"
)
display_name = local.service_account_display_name
}
resource "google_container_node_pool" "nodepool" {
provider = google-beta
project = var.project_id
cluster = coalesce(var.cluster_id, var.cluster_name)
location = var.location
name = var.name
version = var.gke_version
max_pods_per_node = var.max_pods_per_node
initial_node_count = var.node_count.initial
node_count = var.node_count.current
node_locations = var.node_locations
dynamic "autoscaling" {
for_each = (
try(var.nodepool_config.autoscaling, null) != null
&&
!try(var.nodepool_config.autoscaling.use_total_nodes, false)
? [""] : []
)
content {
location_policy = try(var.nodepool_config.autoscaling.location_policy, null)
max_node_count = try(var.nodepool_config.autoscaling.max_node_count, null)
min_node_count = try(var.nodepool_config.autoscaling.min_node_count, null)
}
}
dynamic "autoscaling" {
for_each = (
try(var.nodepool_config.autoscaling.use_total_nodes, false) ? [""] : []
)
content {
location_policy = try(var.nodepool_config.autoscaling.location_policy, null)
total_max_node_count = try(var.nodepool_config.autoscaling.max_node_count, null)
total_min_node_count = try(var.nodepool_config.autoscaling.min_node_count, null)
}
}
dynamic "management" {
for_each = try(var.nodepool_config.management, null) != null ? [""] : []
content {
auto_repair = try(var.nodepool_config.management.auto_repair, null)
auto_upgrade = try(var.nodepool_config.management.auto_upgrade, null)
}
}
dynamic "network_config" {
for_each = var.network_config != null ? [""] : []
content {
create_pod_range = var.network_config.pod_range.create
enable_private_nodes = var.network_config.enable_private_nodes
pod_ipv4_cidr_block = var.network_config.pod_range.cidr
pod_range = var.network_config.pod_range.name
dynamic "additional_node_network_configs" {
for_each = try(var.network_config.additional_node_network_configs, [])
content {
network = additional_node_network_configs.value.network
subnetwork = additional_node_network_configs.value.subnetwork
}
}
dynamic "additional_pod_network_configs" {
for_each = try(var.network_config.additional_pod_network_configs, [])
content {
subnetwork = additional_pod_network_configs.value.subnetwork
secondary_pod_range = additional_pod_network_configs.value.secondary_pod_range
max_pods_per_node = additional_pod_network_configs.value.max_pods_per_node
}
}
dynamic "network_performance_config" {
for_each = try(var.network_config.total_egress_bandwidth_tier, null) != null ? [""] : []
content {
total_egress_bandwidth_tier = var.network_config.total_egress_bandwidth_tier
}
}
dynamic "pod_cidr_overprovision_config" {
for_each = var.network_config.pod_cidr_overprovisioning_disabled ? [""] : []
content {
disabled = true
}
}
}
}
dynamic "upgrade_settings" {
for_each = try(var.nodepool_config.upgrade_settings, null) != null ? [""] : []
content {
max_surge = try(var.nodepool_config.upgrade_settings.max_surge, null)
max_unavailable = try(var.nodepool_config.upgrade_settings.max_unavailable, null)
strategy = try(var.nodepool_config.upgrade_settings.strategy, null)
dynamic "blue_green_settings" {
for_each = try(var.nodepool_config.upgrade_settings.blue_green_settings, null) != null ? [""] : []
content {
node_pool_soak_duration = var.nodepool_config.upgrade_settings.blue_green_settings.node_pool_soak_duration
dynamic "standard_rollout_policy" {
for_each = try(var.nodepool_config.upgrade_settings.blue_green_settings.standard_rollout_policy, null) != null ? [""] : []
content {
batch_percentage = var.nodepool_config.upgrade_settings.blue_green_settings.standard_rollout_policy.batch_percentage
batch_node_count = var.nodepool_config.upgrade_settings.blue_green_settings.standard_rollout_policy.batch_node_count
batch_soak_duration = var.nodepool_config.upgrade_settings.blue_green_settings.standard_rollout_policy.batch_soak_duration
}
}
}
}
}
}
dynamic "placement_policy" {
for_each = try(var.nodepool_config.placement_policy, null) != null ? [""] : []
content {
type = var.nodepool_config.placement_policy.type
policy_name = var.nodepool_config.placement_policy.policy_name
tpu_topology = var.nodepool_config.placement_policy.tpu_topology
}
}
dynamic "queued_provisioning" {
for_each = try(var.nodepool_config.queued_provisioning, false) ? [""] : []
content {
enabled = var.nodepool_config.queued_provisioning
}
}
node_config {
boot_disk_kms_key = try(var.node_config.boot_disk.kms_key, var.node_config.boot_disk_kms_key)
boot_disk {
size_gb = try(var.node_config.boot_disk.size_gb, var.node_config.disk_size_gb)
disk_type = try(var.node_config.boot_disk.type, var.node_config.disk_type)
provisioned_iops = try(
var.node_config.boot_disk.provisioned_iops, null
)
provisioned_throughput = try(
var.node_config.boot_disk.provisioned_throughput, null
)
}
image_type = var.node_config.image_type
labels = var.k8s_labels
resource_labels = var.labels
local_ssd_count = var.node_config.local_ssd_count
machine_type = var.node_config.machine_type
metadata = local.node_metadata
min_cpu_platform = var.node_config.min_cpu_platform
node_group = var.sole_tenant_nodegroup
oauth_scopes = local.service_account_scopes
preemptible = var.node_config.preemptible
service_account = local.service_account_email
spot = (
var.node_config.spot == true && var.node_config.preemptible != true
)
flex_start = var.node_config.flex_start
tags = var.tags
resource_manager_tags = var.resource_manager_tags
dynamic "ephemeral_storage_config" {
for_each = var.node_config.ephemeral_ssd_count != null ? [""] : []
content {
local_ssd_count = var.node_config.ephemeral_ssd_count
}
}
dynamic "ephemeral_storage_local_ssd_config" {
for_each = var.node_config.ephemeral_storage_local_ssd_count != null ? [""] : []
content {
local_ssd_count = var.node_config.ephemeral_storage_local_ssd_count
}
}
dynamic "gcfs_config" {
for_each = var.node_config.gcfs && local.image.is_cos_containerd ? [""] : []
content {
enabled = true
}
}
dynamic "guest_accelerator" {
for_each = var.node_config.guest_accelerator != null ? [""] : []
content {
count = var.node_config.guest_accelerator.count
type = var.node_config.guest_accelerator.type
gpu_partition_size = var.node_config.guest_accelerator.gpu_driver == null ? null : var.node_config.guest_accelerator.gpu_driver.partition_size
dynamic "gpu_sharing_config" {
for_each = try(var.node_config.guest_accelerator.gpu_driver.max_shared_clients_per_gpu, null) != null ? [""] : []
content {
gpu_sharing_strategy = var.node_config.guest_accelerator.gpu_driver.max_shared_clients_per_gpu != null ? "TIME_SHARING" : null
max_shared_clients_per_gpu = var.node_config.guest_accelerator.gpu_driver.max_shared_clients_per_gpu
}
}
dynamic "gpu_driver_installation_config" {
for_each = var.node_config.guest_accelerator.gpu_driver != null ? [""] : []
content {
gpu_driver_version = var.node_config.guest_accelerator.gpu_driver.version
}
}
}
}
dynamic "local_nvme_ssd_block_config" {
for_each = coalesce(var.node_config.local_nvme_ssd_count, 0) > 0 ? [""] : []
content {
local_ssd_count = var.node_config.local_nvme_ssd_count
}
}
dynamic "gvnic" {
for_each = var.node_config.gvnic && local.image.is_cos ? [""] : []
content {
enabled = true
}
}
dynamic "kubelet_config" {
for_each = var.node_config.kubelet_config != null ? [""] : []
content {
cpu_manager_policy = var.node_config.kubelet_config.cpu_manager_policy
cpu_cfs_quota = var.node_config.kubelet_config.cpu_cfs_quota
cpu_cfs_quota_period = var.node_config.kubelet_config.cpu_cfs_quota_period
insecure_kubelet_readonly_port_enabled = var.node_config.kubelet_config.insecure_kubelet_readonly_port_enabled
pod_pids_limit = var.node_config.kubelet_config.pod_pids_limit
container_log_max_size = var.node_config.kubelet_config.container_log_max_size
container_log_max_files = var.node_config.kubelet_config.container_log_max_files
image_gc_low_threshold_percent = var.node_config.kubelet_config.image_gc_low_threshold_percent
image_gc_high_threshold_percent = var.node_config.kubelet_config.image_gc_high_threshold_percent
image_minimum_gc_age = var.node_config.kubelet_config.image_minimum_gc_age
image_maximum_gc_age = var.node_config.kubelet_config.image_maximum_gc_age
allowed_unsafe_sysctls = var.node_config.kubelet_config.allowed_unsafe_sysctls
}
}
dynamic "linux_node_config" {
for_each = var.node_config.linux_node_config != null ? [""] : []
content {
sysctls = var.node_config.linux_node_config.sysctls
cgroup_mode = try(var.node_config.linux_node_config.cgroup_mode, "CGROUP_MODE_UNSPECIFIED")
}
}
dynamic "reservation_affinity" {
for_each = var.reservation_affinity != null ? [""] : []
content {
consume_reservation_type = var.reservation_affinity.consume_reservation_type
key = var.reservation_affinity.key
values = var.reservation_affinity.values
}
}
dynamic "sandbox_config" {
for_each = (
var.node_config.sandbox_config_gvisor == true &&
local.image.is_cos_containerd != null
? [""]
: []
)
content {
sandbox_type = "gvisor"
}
}
dynamic "shielded_instance_config" {
for_each = var.node_config.shielded_instance_config != null ? [""] : []
content {
enable_secure_boot = var.node_config.shielded_instance_config.enable_secure_boot
enable_integrity_monitoring = var.node_config.shielded_instance_config.enable_integrity_monitoring
}
}
dynamic "taint" {
for_each = local.taints
content {
key = taint.key
value = taint.value.value
effect = taint.value.effect
}
}
dynamic "workload_metadata_config" {
for_each = var.node_config.workload_metadata_config_mode != null ? [""] : []
content {
mode = var.node_config.workload_metadata_config_mode
}
}
dynamic "advanced_machine_features" {
for_each = var.node_config.advanced_machine_features != null ? [""] : []
content {
enable_nested_virtualization = var.node_config.advanced_machine_features.enable_nested_virtualization
threads_per_core = var.node_config.advanced_machine_features.threads_per_core
}
}
dynamic "containerd_config" {
for_each = var.node_config.containerd_config != null ? [""] : []
content {
dynamic "private_registry_access_config" {
for_each = try(var.node_config.containerd_config.private_registry_access_config, null) != null ? [""] : []
content {
enabled = (
length(try(
var.node_config.containerd_config
.private_registry_access_config
.certificate_authority_domain_config,
[]
)) > 0
)
dynamic "certificate_authority_domain_config" {
for_each = try(
var.node_config.containerd_config
.private_registry_access_config
.certificate_authority_domain_config,
[]
)
content {
fqdns = certificate_authority_domain_config.value.fqdns
gcp_secret_manager_certificate_config {
secret_uri = (
certificate_authority_domain_config.value
.gcp_secret_manager_certificate_config_secret_uri
)
}
}
}
}
}
dynamic "writable_cgroups" {
for_each = var.node_config.containerd_config.writable_cgroups != null ? [""] : []
content {
enabled = var.node_config.containerd_config.writable_cgroups
}
}
dynamic "registry_hosts" {
for_each = try(var.node_config.containerd_config.registry_hosts, {})
content {
server = registry_hosts.key
dynamic "hosts" {
for_each = registry_hosts.value.hosts
content {
host = hosts.key
capabilities = hosts.value.capabilities
override_path = hosts.value.override_path
dial_timeout = hosts.value.dial_timeout
dynamic "header" {
for_each = coalesce(hosts.value.header, {})
content {
key = header.key
value = header.value
}
}
dynamic "ca" {
for_each = (
hosts.value.ca_gcp_secret_manager_secret_uri != null
? [hosts.value.ca_gcp_secret_manager_secret_uri]
: []
)
content {
gcp_secret_manager_secret_uri = ca.value
}
}
dynamic "client" {
for_each = (
hosts.value.client != null ? [hosts.value.client] : []
)
content {
cert {
gcp_secret_manager_secret_uri = (
client.value.cert_gcp_secret_manager_secret_uri
)
}
dynamic "key" {
for_each = (
client.value.key_gcp_secret_manager_secret_uri != null
? [client.value.key_gcp_secret_manager_secret_uri]
: []
)
content {
gcp_secret_manager_secret_uri = key.value
}
}
}
}
}
}
}
}
}
}
}
}