I keep meeting platform leads who run Kubernetes on two or three clouds and maintain a separate playbook for each. A policy change ships to EKS on Monday, GKE on Wednesday, and the AKS port gets forgotten until an audit finds a prod cluster missing the mandatory-probes rule.
That is not a Kubernetes problem. It is a boundary problem. Teams let cloud-plane details leak into the delivery pipeline until the pipeline itself becomes cloud-specific. Fix the boundary and the three playbooks collapse into one repo plus a handful of per-cluster overlays.
Here is the full blueprint: what to standardize, what has to stay cloud-specific, and the exact tools, tables and YAML that make it work.
Why do teams end up with three separate Kubernetes playbooks in the first place?
Teams end up with three playbooks because each cloud forces a different answer to the same four questions - workload identity, ingress, storage and node lifecycle - and those cloud-plane answers leak upward into the delivery pipeline until the pipeline itself is cloud-specific. It is a missing boundary, not a missing tool.
The four leak points are concrete:
- Identity: EKS IRSA or EKS Pod Identity, versus GKE Workload Identity Federation, versus AKS Microsoft Entra Workload ID.
- Ingress: AWS Load Balancer Controller, versus GKE Gateway and Google Cloud Load Balancing, versus Application Gateway Ingress Controller.
- Storage: gp3 with the EBS CSI driver, versus pd-balanced with the PD CSI driver, versus managed-csi with the Azure Disk CSI driver.
- Node lifecycle: Karpenter, versus GKE node auto-provisioning, versus AKS node pools with the Cluster Autoscaler.
The second cause is tooling order. Each cloud team reaches for the native CI/CD first - CodePipeline, Cloud Build, Azure Pipelines - so delivery is coupled to the cloud from day one, and the coupling only becomes visible at cloud number two.
The third cause is drift. Clusters land on different Kubernetes minor versions, different CNIs and different ingress controller versions, so the "same" manifest quietly behaves differently in each place.
This is mainstream, not an edge case. Organizations run an average of 2.4 public clouds according to the Flexera 2025 State of the Cloud Report, and most now mix public and private environments. If you run Kubernetes at all, you probably run it in more than one place.
The compounding cost is the real killer. Every policy or pipeline change has to be ported N times, and the Nth port is the one that gets forgotten.
The thesis for the rest of this article: draw a portability boundary at the Kubernetes API, parameterize everything below it, and the three playbooks become one repo plus N overlays.
What can actually be standardized across EKS, GKE and AKS - and what cannot?
Everything expressed as a Kubernetes API object is portable across EKS, GKE and AKS, because all three are Certified Kubernetes offerings running the same upstream API. Everything touching the cloud control plane is not portable and must become a per-cluster value, not a second playbook.
The portability claim is not hand-waving. The CNCF Certified Kubernetes Conformance Program now lists more than 100 certified distributions and platforms, and certification guarantees the complete Kubernetes API behaves identically across them. That is the contract you build on.
Portable layer: Deployments and StatefulSets, Services, Helm charts, kustomize bases, admission policies, NetworkPolicies, RBAC, ResourceQuotas, PodDisruptionBudgets, container images, build steps, rollout strategy, and observability agents like the OpenTelemetry Collector and Prometheus.
Non-portable layer: workload identity binding, LoadBalancer and Ingress annotations, StorageClass names and default-class behavior, node pool shapes and instance families, managed database provisioning, VPC and subnet and private-endpoint layout, and registry authentication.
The operating rule, worth pinning above your desk: if it is not portable, it becomes a value in a per-cluster values file or a kustomize overlay - one chart plus N overlays, never N repos.
Watch for false portability. The same manifest can still misbehave because of defaults you never set:
- Default StorageClass and
volumeBindingMode differ per cloud, so a PVC that binds immediately in one place waits for a pod in another.
- LoadBalancer provisioning time, spot and preemption eviction behavior, and PVC reclaim policies are not uniform.
- Maximum pods per node differ sharply. GKE Standard defaults to 110 pods per node, AKS defaults to 30 pods per node with Azure CNI (up to a hard ceiling of 250), and EKS is bounded by ENI-based limits that depend on the instance type unless you enable prefix delegation. A DaemonSet-heavy manifest that schedules fine on GKE can starve a small EKS node.
One test you can apply today: if rolling a change out to all clusters requires editing more than one repository, the boundary is in the wrong place.
| Concern | AWS / EKS | GCP / GKE | Azure / AKS | Verdict |
|---|
| Workload identity | IRSA / EKS Pod Identity | Workload Identity Federation | Microsoft Entra Workload ID | Cloud-specific by nature |
| Ingress / load balancer | AWS Load Balancer Controller | GKE Gateway / Cloud Load Balancing | Application Gateway Ingress Controller | Parameterize as a value (annotations) |
| Default StorageClass + CSI | gp3 + EBS CSI | pd-balanced + PD CSI | managed-csi + Azure Disk CSI | Parameterize as a value |
| Node autoscaling | Karpenter | Node auto-provisioning | Node pools + Cluster Autoscaler | Cloud-specific by nature |
| Managed database | RDS / Aurora | Cloud SQL / AlloyDB | Azure Database | Cloud-specific by nature |
| Admission policy engine | Kyverno / Gatekeeper | Kyverno / Gatekeeper | Kyverno / Gatekeeper | Portable as-is |
| Secrets backend | Secrets Manager | Secret Manager | Key Vault | Parameterize (one ESO API, per-cluster SecretStore) |
| Container registry | ECR | Artifact Registry | ACR | Parameterize as a value |
| CI build step | Any runner | Any runner | Any runner | Portable as-is |
| Deploy mechanism | Argo CD / Flux | Argo CD / Flux | Argo CD / Flux | Portable as-is |
| Observability agent | OTel Collector / Prometheus | OTel Collector / Prometheus | OTel Collector / Prometheus | Portable as-is |
| Max pods per node | ENI-based, instance-dependent | 110 default (up to 512) | 30 default w/ Azure CNI (250 max) | Cloud-specific by nature |
How do you enforce one Kubernetes policy set across AWS, GCP and Azure?
Install the same policy-as-code admission controller - Kyverno or OPA Gatekeeper - on every cluster, keep the policy bundle in one git repository, and deliver it with the same GitOps controller that delivers your apps. Handle cloud differences with narrowly scoped exceptions keyed on a cluster label, never by forking the policy repo.
Both engines are mature and credible. Kyverno graduated within CNCF in March 2026 and uses Kubernetes-native YAML with mutation, generation and image verification built in, with a shallower learning curve. OPA Gatekeeper builds on the graduated Open Policy Agent project and uses Rego, which wins on complex, data-driven logic and existing OPA investment. Pick one and run it fleet-wide.
Start from the free floor that behaves identically everywhere. Pod Security Admission graduated to stable in Kubernetes 1.25, ships in-tree, costs nothing, and enforces the baseline and restricted standards the same way on all three clouds. Use PSA as the baseline, then layer Kyverno or Gatekeeper for what PSA cannot express.
If your needs are simple, ValidatingAdmissionPolicy reached GA in Kubernetes 1.30 and runs CEL inside the API server with zero dependencies. Be honest about where it stops: no mutation, no image signature verification, and no cross-resource data lookups. For "does this namespace have a NetworkPolicy," you still need Kyverno or Gatekeeper.
Standardize these categories first:
- Allowed image registries, and image signature verification with Sigstore and cosign.
- Required owner labels, resource requests and limits, mandatory liveness and readiness probes.
- No privileged containers, no hostPath, no hostNetwork.
- A NetworkPolicy per namespace.
Scope the cloud deltas with labels. Label every cluster cloud=aws|gcp|azure plus env, region and tier, then match or except on those labels. Here is a Kyverno policy that requires probes, paired with a cluster-label-scoped exception for one legacy workload:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-probes
spec:
validationFailureAction: Enforce
rules:
- name: liveness-readiness-required
match:
any:
- resources: { kinds: ["Deployment"] }
validate:
message: "liveness and readiness probes are required"
pattern:
spec:
template:
spec:
containers:
- livenessProbe: { "{}": "?*" }
readinessProbe: { "{}": "?*" }
---
apiVersion: kyverno.io/v2
kind: PolicyException
metadata:
name: legacy-batch-exempt
spec:
match:
any:
- resources:
namespaces: ["batch-legacy"]
selector:
matchLabels: { cloud: "azure" } # scoped to AKS only
exceptions:
- policyName: require-probes
ruleNames: ["liveness-readiness-required"]
Roll out with discipline. Run in Audit mode first, measure violations for a fixed window, then flip to Enforce. Keep policy unit tests in CI - kyverno test or Gatekeeper's gator - so a bad policy can never reach a cluster.
The payoff is real. In the Red Hat State of Kubernetes Security Report, 90% of organizations reported at least one Kubernetes security incident in the prior year, 67% said they had delayed or slowed a deployment over security concerns, and misconfiguration drove a large share of incidents. Policy-as-code is how you stop shipping those misconfigurations N times.
Close the loop with provisioning: a new cluster is not "ready" until the policy bundle has reconciled and reported healthy in the GitOps controller.
| Engine | Policy language | Mutation | Image signature verification | Identical on all 3 clouds | Multi-cluster distribution | CNCF maturity | Best fit |
|---|
| Kyverno | Kubernetes YAML | Yes | Yes (built-in) | Yes | GitOps (Argo/Flux) | Graduated (2026) | Teams wanting native YAML + image verification |
| OPA Gatekeeper | Rego | Limited (mutation beta) | Via external tooling | Yes | GitOps (Argo/Flux) | OPA graduated | Complex, data-driven logic; existing OPA investment |
| Pod Security Admission | Namespace labels | No | No | Yes | Labels via GitOps | In-tree, stable since 1.25 | Free baseline floor on every cluster |
| ValidatingAdmissionPolicy | CEL | No | No | Yes | Manifests via GitOps | In-tree, GA since 1.30 | Simple validations, zero dependencies |
| Cloud-native fleet policy | Varies (CEL/config) | Varies | Varies | No (per-vendor) | Vendor console/API | N/A (vendor) | Single-cloud shops already all-in on one vendor |
How do you build one CI/CD pipeline that deploys to EKS, GKE and AKS?
Split the pipeline in two. CI builds, scans and signs one immutable image and commits a new tag to an environment repository. Then a GitOps controller inside each cluster pulls that commit and applies it. With that split, CI holds no kubeconfig and no cloud credentials for any of the three clouds, and adding a fourth cluster is a config entry rather than a new pipeline.
Push versus pull is the decision that removes the three playbooks. Push-based CI needs cluster credentials for every cloud - three credential sets to rotate, three blast radii. Pull-based GitOps needs none, because the agent lives inside the cluster and reconciles outward.
The reference architecture, numbered and concrete:
- Build one multi-arch image.
- Scan it with Trivy or Grype, and sign it with cosign.
- Push to one registry, or replicate per cloud.
- Update the image digest in the environment repo.
- An Argo CD ApplicationSet cluster generator fans the app out to every matching cluster.
- Progressive rollout per cluster with Argo Rollouts or Flux Flagger.
Step 5 is the whole trick, and it is officially documented. The Argo CD ApplicationSet cluster generator turns one definition into one Application per registered cluster, selected by label:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: checkout-service
spec:
generators:
- clusters:
selector:
matchLabels:
tier: prod # every cluster labeled tier=prod, any cloud
template:
metadata:
name: "checkout-{{name}}"
spec:
project: default
source:
repoURL: https://git.example.com/env-repo.git
targetRevision: main
path: "overlays/{{metadata.labels.cloud}}" # per-cloud overlay
destination:
server: "{{server}}"
namespace: checkout
syncPolicy:
automated: { prune: true, selfHeal: true }
Argo CD versus Flux, fairly: Argo CD ships ApplicationSets and a UI that suit multi-cluster fan-out and app teams. Flux is lighter, controller-composable, and suits GitOps-native platform teams. Both are CNCF Graduated projects, so you are not betting on a hobby tool either way.
A few rules that keep this clean:
- Keep CI cloud-agnostic. GitHub Actions or GitLab CI runners only need registry push plus git push, and should use OIDC federation to each cloud instead of long-lived access keys.
- Promote the same digest. Move one image digest from dev to staging to prod across clouds, never rebuild per environment. Digest equality is what makes a cross-cloud incident reproducible.
- One secrets API. The External Secrets Operator presents a single
ExternalSecret resource while a per-cluster SecretStore points at AWS Secrets Manager, Google Secret Manager or Azure Key Vault.
- What stays per-cluster, honestly: ingress annotations, StorageClass name, node selectors and tolerations. All values in the overlay, all reviewed in the same pull request.
Does the split pay off? The 2025 DORA State of DevOps Report still ties small, frequent, reproducible changes to better delivery performance, and it also found that AI-assisted development can increase delivery instability unless the delivery system is disciplined. A single pull-based pipeline with digest promotion is exactly that discipline.
| Approach | Cloud creds held by CI | Pipelines maintained | Drift risk | Developer self-service | Effort to add a 4th cluster | Who owns upgrades |
|---|
| Native per-cloud CI (CodePipeline / Cloud Build / Azure Pipelines) | All three | 3+ | High | Low | New pipeline + creds | Each cloud team |
| One CI, three push-based deploy jobs | All three | 1 CI, 3 deploy paths | Medium | Low | New job + kubeconfig | Platform team |
| Pull-based GitOps (Argo CD or Flux) | None | 1 CI + 1 env repo | Low | Medium | One cluster entry (label) | Platform team |
| Internal developer platform (Qovery) | None (platform-managed) | 0 to maintain | Low | High | Add a cluster in console/API | Platform-managed |
How do you provision and upgrade the clusters themselves without three Terraform codebases?
You will always have cloud-specific infrastructure code, but you can reduce it to one interface: a thin wrapper module per cloud exposing an identical variable contract and identical outputs, or a Cluster API/Crossplane composition exposing one declarative Cluster resource per provider. Day-2 upgrades, not day-1 provisioning, are where the three-playbook problem comes back.
The Terraform route is the pragmatic one. Wrap the community modules - terraform-aws-modules/eks, terraform-google-modules/kubernetes-engine, Azure/terraform-azurerm-aks - in your own module with a shared variable contract: name, version, node shapes, network, add-on bundle. The calling code then looks identical across clouds even though the module internals differ.
The abstraction route goes further. Cluster API (CAPA, CAPG, CAPZ) or Crossplane (graduated in CNCF in October 2025) give you one Cluster object per cluster. The honest trade-off is an extra control plane and operator set that you now have to run and upgrade.
Day-2 is the real recurring cost, and the numbers are unforgiving. Kubernetes ships about three minor releases a year and supports each for roughly 14 months (12 months plus a 2-month upgrade window), per the Kubernetes patch release policy, and you can skip at most one minor version per upgrade. On top of that, each provider enforces its own window:
- EKS: 14 months of standard support at $0.10 per cluster-hour, then extended support at $0.60 per cluster-hour - a jump from roughly $73 to $438 per cluster per month, per Amazon EKS pricing.
- GKE: roughly 14 months of standard support, extendable to about 24 months total on the Extended channel, per GKE release channels.
- AKS: an N-2 supported window (the latest minor plus the two previous), with a short platform-support tail after that.
The rule to state plainly: pin one target minor version fleet-wide and upgrade in lockstep rather than per cloud, because a mixed-version fleet turns every policy and manifest change into a three-way compatibility test.
Then standardize the add-on bundle as one Helm umbrella chart on every cluster: ingress controller, cert-manager, external-dns, External Secrets Operator, metrics-server, policy engine, observability agent. Pin versions centrally and test upgrades on a sacrificial cluster first. Label every cluster consistently (cloud, region, kubernetes-version, env, tier), because those labels are what your ApplicationSets, PolicyExceptions and inventory dashboard all key on. And treat an unreconciled Argo CD Application, or a cluster off the pinned version, as a paging-worthy condition, not a backlog item.
| Route | Single declarative interface | Who runs the extra control plane | Upgrade automation | Add-on bundle management | Learning curve | Team size it suits |
|---|
| Per-cloud Terraform modules (raw) | No | None | Manual per cloud | Manual per cloud | Low | Any, but high toil |
| Wrapper modules, shared variable contract | Partial (same inputs) | None | Scripted per cloud | One umbrella chart | Low-medium | 1-3 platform engineers |
| Cluster API (CAPA/CAPG/CAPZ) | Yes (Cluster object) | You (management cluster) | Declarative | Via add-on providers | High | Dedicated platform team |
| Crossplane compositions | Yes (one composition) | You (Crossplane control plane) | Declarative | Via compositions | High | Dedicated platform team |
| Managed IDP (Qovery) | Yes (console/API) | Platform-managed | Managed upgrades | Platform-managed | Low | Small or no platform team |
What does this cost in headcount, and when is a platform cheaper than building it?
Building the cross-cloud GitOps stack takes a few engineer-weeks. The recurring cost is what breaks teams: control-plane upgrades on three clouds every cycle, an add-on compatibility matrix, policy maintenance, secrets rotation, and on-call for the platform itself. Below roughly two dedicated platform engineers, a managed internal developer platform is usually the cheaper answer.
Price the recurring work in engineer-days before you commit. Per cycle you are signing up for three control-plane upgrades, add-on compatibility testing, CNI and ingress regression hunting, policy maintenance and exception review, CI runner maintenance, secrets rotation, and platform on-call. None of that ships a feature. All of it has to happen three times unless you have collapsed the boundary correctly.
The headcount math is not hypothetical. Gartner predicted that by 2026, 80% of software engineering organizations would have platform teams, up from 45% in 2022, and the 2025 DORA research found the large majority of organizations have adopted at least one internal platform. A platform engineer's loaded cost runs well into six figures in most markets (Levels.fyi tracks the current ranges), so two of them is a real line item you should weigh against a subscription.
When DIY genuinely wins:
- You already have a platform team with spare capacity.
- You run custom controllers or unusual compliance requirements.
- Your workloads need cluster-level control that an abstraction would hide.
When a managed internal developer platform wins:
- You have small or no dedicated DevOps/SRE headcount.
- You need identical behavior across clouds quickly.
- You have a hard requirement to keep workloads, data and the cloud bill inside your own accounts.
Be fair about the alternatives, because they are good at different things. Google Anthos/GKE Enterprise and Azure Arc do real fleet-level policy and config management across clouds, but they anchor the control plane in one vendor's ecosystem and give developers no self-service deployment experience. Backstage is a portal, not a delivery engine. Humanitec is a platform orchestrator you still assemble around. Rafay is fleet management aimed at cluster operators, not developers. Northflank is a strong app platform with a bring-your-own-cloud option.
| Route | Who runs the control plane | Where workloads run | Clouds supported | Policy enforcement | Developer self-service | Assumed platform team | Time to first prod deploy |
|---|
| DIY GitOps (Argo CD + Kyverno + Terraform) | You | Your accounts | AWS/GCP/Azure/on-prem | Yes (you build it) | Medium (you build it) | 2+ engineers | Weeks |
| Backstage portal + Argo CD | You | Your accounts | Any | Via Argo/Kyverno | Portal UX, no delivery engine | 2+ engineers | Weeks to months |
| Fleet managers (Anthos/GKE Enterprise, Azure Arc, Rafay) | Vendor-anchored | Your clusters | Multi, vendor-centric | Strong (fleet policy) | Low (operator-focused) | 1-2 engineers | Days to weeks |
| Humanitec | You (orchestrator) | Your accounts | Multi | Via your tooling | Medium (assemble it) | 1-2 engineers | Weeks |
| Northflank | Vendor or BYOC | Vendor or your cloud | Multi (BYOC) | Platform-level | High | Small | Hours to days |
| Qovery | Platform-managed | Your own accounts (BYOC) | AWS/GCP/Azure/Scaleway + your K8s | Platform-level, keep your own Kyverno | High | Small or none | Minutes to hours |
How does Qovery give you one pipeline and one policy model across AWS, GCP, Azure and your own clusters?
Qovery installs and manages the same Kubernetes layer, deployment pipeline, environment model and RBAC inside your own AWS, GCP, Azure or Scaleway account, or on a Kubernetes cluster you already run, so the developer-facing contract is identical on every cloud while the clusters, the data and the cloud bill stay in your accounts under your existing discounts.
This is the bring-your-own-cloud model, and the ordering matters. The cluster runs in your own account, so your Savings Plans, committed-use discounts and data-residency commitments stay intact. That is the opposite of shipping your workloads into a vendor's account and losing both the discounts and the control.
The same primitives show up on every cloud:
- Git-push deployments.
- Preview and ephemeral environments per pull request.
- Environment auto-stop for non-production.
- Managed cluster upgrades.
- Per-environment RBAC.
- Databases backed by managed cloud services.
Because Qovery can also run on a Kubernetes cluster you already operate - self-managed or on-prem - this stays a consistency answer rather than a lock-in answer.
Map it onto the DIY stack from earlier in this article. Qovery replaces the per-cluster add-on bundle, the pipeline plumbing, the environment model and the upgrade treadmill. Teams that want their own Kyverno or Gatekeeper policies on top keep running them. 🚀
The honest boundary: Qovery standardizes the application delivery layer. Cloud-plane specifics - VPC design, IAM, which managed database you pick - still exist. The difference is you configure them once per cluster instead of re-implementing them in a third playbook. For the current feature set, the Qovery docs are the source of truth rather than any claim I make here.
How do I set this up from Claude Code, Cursor or Codex?
Install the Qovery Agent Skills once and your coding agent can provision a cluster, wire the pipeline and deploy into your own AWS, GCP, Azure or Scaleway account, or an existing Kubernetes cluster, from plain-language instructions - with an approval step before anything runs.
- Install globally for every compatible agent (Claude Code, Cursor, Codex, OpenCode, Gemini CLI):
curl -fsSL https://skill.qovery.com/install.sh | bash - installs the skill bundle; your agent asks for approval before running it.
- Project-scoped instead of global:
curl -fsSL https://skill.qovery.com/install.sh | bash -s -- --project - keeps the skills inside a single repository.
- Add the MCP Server to Claude Code (OAuth, read-only by default):
claude mcp add --transport http qovery https://mcp.qovery.com/mcp --callback-port 4242 - lets the agent inspect existing clusters and environments; you approve the OAuth flow in the browser.
- Enable write access when you want the agent to change things (also requires enabling write access in the Qovery Console):
claude mcp add --transport http qovery "https://mcp.qovery.com/mcp?read_write=true" --callback-port 4242.
The skills deploy (qovery-onboard, qovery-deploy, qovery-troubleshoot, qovery-optimize, qovery-speedup, qovery-preview, qovery-terraform); the MCP Server operates what already exists. Then ask in plain language, for example: "install a Qovery cluster in my GCP account and deploy this service with a preview environment per pull request."
Start points: the AI agent quickstart and the Claude Code integration.
Frequently asked questions
Can I really use one CI/CD pipeline for EKS, GKE and AKS?
Yes. Use one CI job that builds, scans with Trivy or Grype, signs with cosign, and commits an image digest to a git environment repo, then let a pull-based GitOps controller (Argo CD or Flux) inside each cluster reconcile that commit. CI never holds a kubeconfig for any cloud, and adding a fourth cluster is one labeled entry an Argo CD ApplicationSet picks up.
Should I use Kyverno or OPA Gatekeeper for multi-cloud policy enforcement?
Use Kyverno if you want Kubernetes-native YAML policies with built-in mutation and image signature verification and a shorter learning curve; use OPA Gatekeeper if you need complex, data-driven logic in Rego or already run OPA. Both are credible fleet-wide choices - Kyverno graduated within CNCF in 2026 and Gatekeeper builds on the graduated OPA project. Run Pod Security Admission (stable since Kubernetes 1.25) underneath either one as a free baseline.
Is Argo CD or Flux better for deploying to multiple clouds?
Both are CNCF Graduated and both do pull-based GitOps well. Choose Argo CD for its ApplicationSet cluster generator and UI, which fit multi-cluster fan-out and app teams; choose Flux for a lighter, controller-composable setup that suits GitOps-native platform teams. The multi-cloud mechanism is identical: one definition fans out to every registered cluster by label.
How do I handle IAM differences (EKS IRSA, GKE Workload Identity, Azure Workload Identity) in a single pipeline?
Keep workload identity out of the pipeline entirely and treat it as a per-cluster value. Bind each cloud's identity (IRSA or EKS Pod Identity, GKE Workload Identity Federation, Microsoft Entra Workload ID) to a Kubernetes ServiceAccount at cluster bootstrap, then reference that ServiceAccount by name in a portable manifest. The same Deployment YAML works everywhere because the cloud-specific binding lives in the overlay, not the app.
How do I keep Kubernetes versions and add-ons in sync across AWS, GCP and Azure?
Pin one target minor version fleet-wide and upgrade every cluster in lockstep, because Kubernetes supports each minor for about 14 months and lets you skip at most one version per upgrade. Package your add-ons as one Helm umbrella chart with centrally pinned versions, test upgrades on a sacrificial cluster first, and watch each provider's clock: EKS extended support jumps to $0.60 per cluster-hour, GKE's Extended channel reaches about 24 months, and AKS supports N-2.
Do I need a dedicated platform team to run Kubernetes on three clouds?
Not necessarily, and the honest break-even is around two dedicated platform engineers. The DIY GitOps stack takes a few engineer-weeks to build but a continuous stream of engineer-days to run - three control-plane upgrades per cycle, add-on compatibility testing, policy maintenance, secrets rotation and on-call. Below that headcount, a managed internal developer platform that runs in your own accounts is usually cheaper than building and staffing the platform yourself.
How is Qovery different from Anthos/GKE Enterprise, Azure Arc or Rafay for multi-cloud Kubernetes?
Anthos/GKE Enterprise and Azure Arc do genuine fleet-level policy and config across clouds, but they anchor the control plane in one vendor's ecosystem and give developers no self-service deployment experience, and Rafay targets cluster operators rather than developers. Qovery is developer-facing: it gives your team git-push deployments, per-PR preview environments and per-environment RBAC that behave identically on AWS, GCP, Azure, Scaleway or your own Kubernetes cluster, while the clusters and the cloud bill stay in your accounts.
The lesson I keep relearning: the three-playbook problem is never solved by a smarter tool, only by moving the boundary back to the Kubernetes API and refusing to let cloud-plane details leak above it. Do that once and your fourth cloud costs a config entry, not a new team.