terraform-google-kubernetes-engine: one opinionated GKE module with three flavours of cluster
Configures opinionated GKE clusters
At a glance
- What is it?
- The Google Cloud Terraform module for Kubernetes Engine builds a cluster plus its node pools, and it also reaches into the cluster's configuration to replace the kube-dns configmap, enable network policy and install the ip-masq-agent configmap. This looks at what it creates, what the private, beta private and beta public submodules change, and where the module stops being a good fit.
- Who is it for?
- This module is worth adopting when a team wants a private or standard GKE cluster with predictable defaults, node pools it declares itself, and the DNS and masquerade configuration handled in the same apply rather than by hand afterwards. It is the wrong tool for a cluster whose shape has to be negotiated with a platform team, and for anyone who needs GKE features the beta submodules lag behind, since the stable path is the one tested against released APIs.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly HCL, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Four things the module creates, and two of them are not cloud resources
The README opens by naming exactly what the module does, which is unusual and useful. It handles opinionated Google Cloud Platform Kubernetes Engine cluster creation and configuration with Node Pools, IP MASQ, Network Policy, and so on. The list of what it creates or triggers has four entries.
The first two are ordinary infrastructure: a GKE cluster with the provided addons, and GKE node pools with the provided configuration attached to that cluster. The third and fourth are the interesting ones, because they are edits to the running cluster rather than new resources. If `stub_domains` are provided, the module replaces the default kube-dns configmap. If `network_policy` is true, it activates network policy. If `configure_ip_masq` is true, it adds an `ip-masq-agent` configmap carrying the provided `non_masquerade_cidrs`.
That last group is the module's real character. A bare cluster resource would give you a cluster and leave DNS stubbing, network policy and SNAT behaviour to a separate step in a runbook. Here they are conditional on named inputs, so a cluster created with `network_policy = false` and one created with it true come out of the same module with different networking, and the difference is visible in the configuration rather than in tribal knowledge.
The conditionals are also a cost. Because the module rewrites the kube-dns configmap when `stub_domains` are set, a cluster that already had a configmap edited by hand will lose that edit on the next apply. Whatever else the module does, that replacement is destructive by design, and it is worth knowing before the first apply rather than after it.
Private, beta private and beta public are three different modules
The module is not a single entry point. Sub modules are provided for creating private clusters, beta private clusters, and beta public clusters, and the beta ones allow for the use of various GKE beta features. The repository has a modules/ directory holding them, and the autogen/ directory with an autogen_modules.json file suggests the set of submodules is machine-tracked rather than hand-maintained.
This is the decision a new user has to make before writing a line of configuration, and it is not a trivial one, because the stable module is the default and the beta ones are a different set of behaviour. The same README input table that documents the stable module also exposes inputs that only make sense in one of the three, such as `anonymous_authentication_config_mode` with valid values `ENABLED` and `LIMITED`, which sits alongside the more conventional `cluster_autoscaling` and `add_cluster_firewall_rules` entries.
The compatibility section is equally blunt about versioning. The module is meant for use with Terraform 1.3+ and tested using Terraform 1.14+. If you find incompatibilities using Terraform `>=1.3`, the instruction is to open an issue. And the module preserves older compatibility lines rather than dropping them: the last released version intended for Terraform 0.13.x is 27.0.0, and the last released version intended for Terraform 0.12.x is 12.3.0.
Those two pinned old versions are the escape hatch for a team on an old Terraform, and they also tell you something about the shape of the project. The recent releases are v45.0.0 from 2026-09-02, v44.3.0 from 2026-07-06 and v44.2.0 from 2026-05-18, so the module moves with major version bumps that carry breaking input changes, and the last push was on 2026-10-14. Pinning the version in the source rather than tracking main is the only sane approach for a cluster module.
The kubernetes provider is a required part of the usage, not an optional extra
The simple usage example in the README carries a comment that reads like a warning: google_client_config and kubernetes provider must be explicitly specified like the following. The example therefore begins with the client config data source and the provider block, and only then declares the module.
data "google_client_config" "default" {}
provider "kubernetes" {
host = "https://${module.gke.endpoint}"
token = data.google_client_config.default.access_token
cluster_ca_certificate = base64decode(module.gke.ca_certificate)
}The reason is visible in the provider block itself. The host is built from `module.gke.endpoint` and the CA certificate from `module.gke.ca_certificate`, both of which are outputs of the module declared further down in the same file. The kubernetes provider is therefore configured against a cluster that does not exist yet at the time the provider block is read, and the values are resolved from the module's plan.
This is a real constraint on how the file is written. The provider block has to appear before the module block that produces the values it consumes, and the module has to be in the same configuration. It also means a second cluster in the same project cannot simply reuse the same provider stanza, since each one needs its own endpoint and certificate, and a reader who expects to define a provider once and reuse it will find the example does not allow it.
The module block that follows declares a full set of variables in one flat list, and it is worth reading as a description of the module's opinionated defaults. It sets `http_load_balancing`, `network_policy`, `filestore_csi_driver` and `dns_cache` to false, and `horizontal_pod_autoscaling` to true. It names three zones, a network and a subnetwork, and separate `ip_range_pods` and `ip_range_services` ranges. The `node_pools` list is a list of objects, each with machine type, counts, disk settings, image type, taint and label maps, and GPU fields including `accelerator_type`, `gpu_driver_version` and `gpu_sharing_strategy`.
Per node pool configuration is a set of parallel maps keyed by pool name
Most of the module's inputs are not simple scalars but maps keyed by node pool name, with an `all` key that applies to every pool. The example makes the pattern clear.
node_pools_labels = {
all = {}
default-node-pool = {
default-node-pool = true
}
}
node_pools_metadata = {
all = {}
default-node-pool = {
node-pool-metadata-custom-value = "my-node-pool"
}
}
node_pools_taints = {
all = []
default-node-pool = [
{
key = "default-node-pool"
value = true
effect = "PREFER_NO_SCHEDULE"
},
]
}The same shape is used for `node_pools_oauth_scopes`, which in the example grants `https://www.googleapis.com/auth/logging.write` and `https://www.googleapis.com/auth/monitoring` to all pools, and for `node_pools_tags`, where the `all` key is an empty list and the named pool carries a single tag.
What this design buys is that adding a second pool does not require editing every map, because the `all` key carries the shared values and a named key carries the exception. What it costs is that the pool names have to be repeated as keys in five separate maps, and a typo in one of them is a silent no-op rather than an error, since Terraform will simply find no matching key for that pool. The node pool name in the `node_pools` list is the anchor for all of them, and that anchor is duplicated five times per pool.
The inputs documented in the README's table go further than the example shows. Alongside `additional_ip_range_pods` and `additional_ip_ranges_config`, which takes a list of objects with `subnetwork` and `pod_ipv4_range_names`, there is `additive_vpc_scope_dns_domain`, which has a hard precondition: for it to work, `cluster_dns = CLOUD_DNS` and `cluster_dns_scope = CLUSTER_SCOPE` must both be set as well, and the domain name must be unique within the VPC. That kind of conditional input is where this module's opinionated character shows most clearly.
The file layout separates clusters, firewalls, DNS, masquerade and service accounts
The repository root is a map of the module's concerns, and the file names do the explaining: cluster.tf, dns.tf, firewall.tf, masq.tf, networks.tf, sa.tf, moved.tf, main.tf, outputs.tf, variables.tf, variables_defaults.tf and versions.tf.
The split matters for two reasons. The first is that the DNS, masquerade and network policy behaviour described at the start of the README lives in dns.tf and masq.tf rather than inside cluster.tf, so a change to how `stub_domains` is handled is a change in a file that is separate from the cluster resource itself. The second is variables_defaults.tf sitting next to variables.tf, which is a pattern for keeping a large input surface manageable: the defaults are separated from the declarations, and the input table in the README is large enough to justify that, running to dozens of rows with types, defaults and required flags.
Then there is moved.tf, which is a Terraform block for recording that a resource has moved to a different address in the configuration. Its presence in the root says that this module has changed the addresses of its own resources between versions, which is what happens to a module with a long release history and major version bumps, and it is also what makes a version upgrade across several majors survivable rather than a destroy and recreate.
The service account file, sa.tf, matches the `service_account` field in the node pool example, which points at a project service account address, and the examples folder includes a directory named shared_vpc and one named regional_private_node_pool_oauth_scopes, so the OAuth scope map and the service account are first-class parts of what the module manages rather than leftovers in the example.
Testing happens in a container with credentials, and the examples are the real documentation
The Makefile shows how this module is tested, and the approach has consequences for anyone who wants to contribute.
DOCKER_TAG_VERSION_DEVELOPER_TOOLS := 1.26
DOCKER_IMAGE_DEVELOPER_TOOLS := cft/developer-tools
REGISTRY_URL := gcr.io/cloud-foundation-cicdThe make targets run docker with the `cft/developer-tools` image at tag 1.26 from gcr.io/cloud-foundation-cicd, mounting the current directory at /workspace, and the test preparation target passes in `SERVICE_ACCOUNT_JSON`, `TF_VAR_org_id`, `TF_VAR_folder_id` and `TF_VAR_billing_account` as environment variables, running `/usr/local/bin/execute_with_credentials.sh prepare_environment`. There is a matching cleanup target, and the file states that Make will use bash instead of sh.
Two things follow. First, running the test suite needs real Google Cloud credentials and organisation, folder and billing identifiers, so this is not a suite that runs on a laptop with no account. Second, the image is a fixed tag, and the CI registry is where the tooling comes from, so the environment the tests run in is one someone else maintains.
The header of the Makefile adds a governance note that matters for a module used widely: it was generated from the terraform-google-module-template, and the comment asks that relevant changes be contributed upstream. This is a template-driven repository, which is also why autogen/, metadata.yaml and metadata.display.yaml sit at the root.
For adoption rather than contribution, the examples are the part to read, and there are many of them. The list includes a node_pool example, a node_pool_update_variant and two beta variants of it, gke_autopilot_cluster, gke_standard_cluster, safer_cluster and confidential_autopilot_private, several private configurations including simple_autopilot_private and private_zonal_with_networking, shared_vpc, island_cluster_anywhere_in_gcp_design, island_cluster_with_vm_router, and a deploy_service example. The naming alone tells you the module's coverage, and choosing the example closest to your case is the fastest way to find out whether the module handles it.
Where this module is the wrong tool
The limits come from the same design choices that make the module useful.
The first is that it is opinionated. That word is in the README's own description, and it cuts both ways. A module that decides for you which addons are on, which firewall rules exist and how DNS and masquerading behave will conflict with a platform team that has its own opinion about any of those, and the conflict will surface as a Terraform plan that wants to change things somebody else owns. The inputs that let you turn behaviour off, such as `network_policy` or `configure_ip_masq`, are off by default in the example, so the module's defaults are the opposite of what a locked-down network posture usually wants.
The second is the beta submodules. Two of the three provided flavours are beta, and the beta ones exist to reach GKE beta features. That is a deliberate trade of stability for reach, and it means a cluster built on the beta private or beta public submodule carries a different upgrade risk than one on the stable module.
The third is the input surface. The README's table is long, and each input is a decision you now own. `boot_disk_kms_key` alone carries a required shape, projects/[KEY_PROJECT_ID]/locations/[LOCATION]/keyRings/[RING_NAME]/cryptoKeys/[KEY_NAME], and `authenticator_security_group` requires a group name in the format [email protected]. This is normal for a mature module, and it is also the reason the examples exist: they encode the decisions that are hard to derive from a table.
The fourth is the upgrade path across majors. v45.0.0, v44.3.0 and v44.2.0 in the release list are major and minor steps, and the presence of moved.tf is the Terraform mechanism for keeping those moves from destroying running infrastructure. A team that upgrades a live cluster needs to read the CHANGELOG.md in the repository root, which the module keeps, rather than bumping the version and applying.
Editorial conclusion
This module is worth adopting when a team wants a private or standard GKE cluster with predictable defaults, node pools it declares itself, and the DNS and masquerade configuration handled in the same apply rather than by hand afterwards. It is the wrong tool for a cluster whose shape has to be negotiated with a platform team, and for anyone who needs GKE features the beta submodules lag behind, since the stable path is the one tested against released APIs. Before adopting, read the examples folder for the shape closest to your case, check the input table in the README for the defaults you are not overriding, and confirm the version of the module you pin still targets a Terraform version your team actually uses.
Frequently asked questions
What does the terraform-google-kubernetes-engine module create?
It creates a GKE cluster with the provided addons and GKE node pools attached to it. It also replaces the default kube-dns configmap when stub_domains are provided, activates network policy when network_policy is true, and adds an ip-masq-agent configmap with non_masquerade_cidrs when configure_ip_masq is true.
Which Terraform versions does the module support?
The module is meant for use with Terraform 1.3+ and is tested using Terraform 1.14+. For older Terraform, the README points to 27.0.0 as the last release intended for Terraform 0.13.x and 12.3.0 as the last release intended for Terraform 0.12.x.
Why do I need to declare the kubernetes provider explicitly?
The README states that google_client_config and kubernetes provider must be explicitly specified. The provider block reads the module outputs, with host set from module.gke.endpoint and cluster_ca_certificate from base64decode(module.gke.ca_certificate), so the provider depends on the module declared in the same configuration.
How do I set labels, taints and metadata for a specific node pool?
Each of node_pools_labels, node_pools_metadata, node_pools_taints, node_pools_tags and node_pools_oauth_scopes is a map keyed by node pool name, with an all key that applies to every pool. The pool name in the node_pools list is the anchor for all of these maps.
How is this module tested?
The Makefile runs the tests in a Docker container using the cft/developer-tools image at tag 1.26 from gcr.io/cloud-foundation-cicd, passing in SERVICE_ACCOUNT_JSON, TF_VAR_org_id, TF_VAR_folder_id and TF_VAR_billing_account, and calling execute_with_credentials.sh prepare_environment. Running the suite therefore needs real Google Cloud credentials.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/terraform-google-modules-terraform-google-kubernetes-engine)Was this useful?