PF-02 · GCP · IaC
GCP Terraform GKE platform
A module set that keeps capacity during GKE node pool upgrades with SURGE, and opens only what a VPN needs through a firewall baseline, all as code.
- Role
- Design and implementation
- Period
- Personal project · released Nov 2026
- Cloud
- GCP
- Scale
- 22 modules · 3 examples
- Key tech
- Terraform · GCP · GKE · Cloud Logging · Cloud Monitoring
- 22
- Terraform modules
- 5
- Extra nodes first (max_surge)
- 0
- Nodes down at once (max_unavailable)
At a glance
Problem
A GKE node upgrade must not reduce serving capacity, and management ports should open only from a VPN.
Approach
The node pool upgrades with SURGE: five new nodes come up first and unavailable stays at zero. The c2sfirewall module opens management ports from the VPN at priority 1000 and denies other tcp and udp outbound at priority 65534.
Result
Old nodes leave only after replacements are ready. Host errors flow from the hostError audit log to a log-based metric, an alert policy, and a Slack channel.
Up close
Node pool SURGE settings
What this code does: Defines the game node pool to autoscale between 3 and 12 nodes and sets the upgrade strategy to SURGE. max_surge 5 brings five new nodes up first, and max_unavailable 0 means no existing node is taken away before its replacement is ready, so serving capacity does not drop during an upgrade. As the comment says, the soak time below is read only if the strategy is switched to BLUE_GREEN.
node_pool = {
game = {
autoscaling_enable = true
min_node_count = 3
max_node_count = 12
location_policy = "BALANCED"
node_count = null
pool_version = "1.31"
machine_type = "e2-standard-4"
tags = ["gke-node"]
# Five extra nodes come up first, and no node is taken away until its replacement is ready.
strategy = ["SURGE"]
max_surge = 5
max_unavailable = 0
# Soak time is read only when the strategy is switched to BLUE_GREEN.
node_pool_soak_duration = "3600s"
}
}
A VPN-only firewall baseline
What this code does: Declares firewall rules as a map, and the c2s-inbound-vpn rule allows incoming (INGRESS) tcp traffic. The only sources are the VPN ranges passed in as a variable (vpn_source_ranges), only the management ports from another variable (management_ports) are opened, and priority 1000 makes it apply before the deny rules that follow. Because ranges and ports are variables, the same rule structure is reused across environments.
locals {
firewall_rules = {
c2s-inbound-vpn = {
direction = "INGRESS"
action = "allow"
ranges = var.vpn_source_ranges
sources = null
targets = null
use_service_accounts = false
enable_logging = false
priority = "1000"
rules = [
{
protocol = "tcp"
ports = var.management_ports
}
]
}What I learned
I learned that judging behavior from a setting's name can be wrong. A value like soak time sounds as if it applies to every upgrade, but it is read only under the BLUE_GREEN strategy, so the SURGE example states in a comment that the value has no effect there. Protecting capacity during upgrades also works only when the intent is pinned in numbers, such as bring five new nodes up first (max_surge 5) and take none away (max_unavailable 0), so the result stays the same whoever runs it. The firewall follows the same idea: the module ships a baseline that opens only management ports at priority 1000 and blocks the rest at 65534, so every new environment starts from the same rules.