Migrating GKE to Dataplane V2 (Cilium / eBPF)
GKE is switching its default networking from iptables to Cilium and eBPF, and the field that controls it cannot be changed on a running cluster. Here is how I turned a forced destroy-and-recreate into a versioned GitOps bootstrap and an 18-point validation plan.
GKE announced that from March 30, 2027, new clusters default to Dataplane V2 (DPv2), which replaces the old kube-proxy and iptables data plane (Dataplane V1) with Cilium and eBPF. The catch: the datapath_provider field is immutable. You cannot flip a running cluster from V1 to V2. The only way across is to destroy and recreate the cluster.
Why move early, not at the deadline
Waiting until 2027 is the expensive option. I moved while it was cheap and safe:
- The window was open. Three of the four clusters (dev, staging and sandbox) were empty, zero nodes and zero live workloads. That is the lowest-risk, lowest-cost moment this migration will ever have.
- Avoid a surprise later. If I waited, the next time any cluster was recreated after the deadline it would inherit DPv2 by default, with no prior validation of the platform stack (ingress, cert-manager, external secrets, autoscaling, progressive delivery, GitOps).
- Pay down debt in the process. Recreating a cluster forces you to have a real bootstrap. That was the opening to replace years of manual setup with a repeatable pipeline.
Making the immutable field explicit
The root cause of the "surprise in 2027" risk is that the field had always been left to the cloud default. I made datapath_provider an explicit, required Terraform variable on the cluster module, so no cluster is ever created without a conscious choice:
- dev, staging and sandbox recreated with
ADVANCED_DATAPATH(DPv2 / Cilium). - production kept explicitly on
LEGACY_DATAPATH; migrating a live production cluster is a separate project that needs a real maintenance window.
The lesson generalizes: any immutable infrastructure field should be an explicit variable in code, never an inherited default. Defaults change out from under you.
The silver lining: a GitOps bootstrap
Before this, bringing a cluster up meant a pile of manual kubectl apply and ad-hoc Helm installs: cert-manager, two ingress controllers, the autoscaler, cluster issuers. That knowledge lived in people's heads. Because a destroy-and-recreate would have lost all of it, I converted it into a versioned bootstrap directory plus a CI workflow, so bringing a cluster up from zero is now a deterministic, reviewed pipeline run. The recreate also let me upgrade the autoscaler a few minor versions and pin exact versions of cert-manager and the rest.
Each recreated cluster then re-registers with the central GitOps controller, since the cluster's API endpoint, token and CA certificate all change on recreate.
Validate the data plane, do not assume it
Switching the data plane can change subtle behaviour (NetworkPolicy enforcement, source IPs, connectivity). I validated each recreated cluster against an 18-point plan: NetworkPolicies, external secrets with Workload Identity, GitOps sync, Prometheus scraping, cert-manager issuance, and egress through Cloud NAT, among others. Green across the board before anything depended on it.
What I would tell someone starting this
- Make immutable fields explicit in your IaC. If changing a field means recreating the resource, it should be a required, reviewed variable, not a silent default.
- Use the empty-cluster window. The cheapest time to do a destructive migration is before anything is running on it.
- Let a forced recreate pay down debt. If you must rebuild, rebuild as code: turn manual bootstrap into a repeatable GitOps pipeline.
- Keep production for last, with its own window. Prove the pattern on the empty clusters first.
- Validate the data plane explicitly. A networking swap is exactly where "it probably still works" bites you.