Webinar · Oct 20: The migration takes 2 weeks. Deciding to do it takes 6 months.

One Kubernetes Policy and One CI/CD Pipeline Across AWS, GCP and Azure: How to Stop Maintaining Three Playbooks

A practical blueprint for running a single Kubernetes policy set and one CI/CD pipeline across AWS, GCP, Azure and your own clusters: what to standardize, what must stay cloud-specific, and the exact tools, tables and YAML that make it work.

Alessandro Carrano
Head of Product
OCT 17, 2026 · 12 MIN
One Kubernetes Policy and One CI/CD Pipeline Across AWS, GCP and Azure: How to Stop Maintaining Three Playbooks

Key points

  • Yes, one policy set and one pipeline across AWS, GCP and Azure is achievable. Draw the boundary at the Kubernetes API. Manifests, admission policies, RBAC, build steps and rollout strategy are portable. IAM, load balancers, storage classes, node pools and managed databases are not, and they must become per-cluster values, never separate playbooks.
  • The reference stack is four pieces: Kyverno or OPA Gatekeeper for policy-as-code on every cluster, Argo CD or Flux for pull-based GitOps delivery, Terraform wrapper modules or Cluster API/Crossplane for provisioning, and one CI system that only builds, scans, signs and commits an image tag. CI should never hold a kubeconfig for three clouds.
  • Pull-based GitOps is the mechanism that removes the three playbooks. One Application definition fans out to every registered cluster by label, so adding a fourth cluster is one config entry instead of a new pipeline and a new credential set.
  • The recurring cost is drift, not the pipeline. Kubernetes ships about three minor releases a year and each provider enforces its own support window, so pin one target minor version and one add-on umbrella chart fleet-wide before standardizing anything else.
  • If you do not have a dedicated platform team, an internal developer platform is usually the cheaper answer. Qovery installs and manages the same Kubernetes layer, pipeline, environment model and RBAC inside your own AWS, GCP, Azure or Scaleway account, or on a cluster you already run, so the developer contract is identical everywhere while the cloud bill and discounts stay in your accounts.

Qovery · Agentic Infrastructure Platform
A control plane for platform teams and their coding agents
Learn more

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.

ConcernAWS / EKSGCP / GKEAzure / AKSVerdict
Workload identityIRSA / EKS Pod IdentityWorkload Identity FederationMicrosoft Entra Workload IDCloud-specific by nature
Ingress / load balancerAWS Load Balancer ControllerGKE Gateway / Cloud Load BalancingApplication Gateway Ingress ControllerParameterize as a value (annotations)
Default StorageClass + CSIgp3 + EBS CSIpd-balanced + PD CSImanaged-csi + Azure Disk CSIParameterize as a value
Node autoscalingKarpenterNode auto-provisioningNode pools + Cluster AutoscalerCloud-specific by nature
Managed databaseRDS / AuroraCloud SQL / AlloyDBAzure DatabaseCloud-specific by nature
Admission policy engineKyverno / GatekeeperKyverno / GatekeeperKyverno / GatekeeperPortable as-is
Secrets backendSecrets ManagerSecret ManagerKey VaultParameterize (one ESO API, per-cluster SecretStore)
Container registryECRArtifact RegistryACRParameterize as a value
CI build stepAny runnerAny runnerAny runnerPortable as-is
Deploy mechanismArgo CD / FluxArgo CD / FluxArgo CD / FluxPortable as-is
Observability agentOTel Collector / PrometheusOTel Collector / PrometheusOTel Collector / PrometheusPortable as-is
Max pods per nodeENI-based, instance-dependent110 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:

YAML
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.

EnginePolicy languageMutationImage signature verificationIdentical on all 3 cloudsMulti-cluster distributionCNCF maturityBest fit
KyvernoKubernetes YAMLYesYes (built-in)YesGitOps (Argo/Flux)Graduated (2026)Teams wanting native YAML + image verification
OPA GatekeeperRegoLimited (mutation beta)Via external toolingYesGitOps (Argo/Flux)OPA graduatedComplex, data-driven logic; existing OPA investment
Pod Security AdmissionNamespace labelsNoNoYesLabels via GitOpsIn-tree, stable since 1.25Free baseline floor on every cluster
ValidatingAdmissionPolicyCELNoNoYesManifests via GitOpsIn-tree, GA since 1.30Simple validations, zero dependencies
Cloud-native fleet policyVaries (CEL/config)VariesVariesNo (per-vendor)Vendor console/APIN/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:

  1. Build one multi-arch image.
  2. Scan it with Trivy or Grype, and sign it with cosign.
  3. Push to one registry, or replicate per cloud.
  4. Update the image digest in the environment repo.
  5. An Argo CD ApplicationSet cluster generator fans the app out to every matching cluster.
  6. 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:

YAML
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.

ApproachCloud creds held by CIPipelines maintainedDrift riskDeveloper self-serviceEffort to add a 4th clusterWho owns upgrades
Native per-cloud CI (CodePipeline / Cloud Build / Azure Pipelines)All three3+HighLowNew pipeline + credsEach cloud team
One CI, three push-based deploy jobsAll three1 CI, 3 deploy pathsMediumLowNew job + kubeconfigPlatform team
Pull-based GitOps (Argo CD or Flux)None1 CI + 1 env repoLowMediumOne cluster entry (label)Platform team
Internal developer platform (Qovery)None (platform-managed)0 to maintainLowHighAdd a cluster in console/APIPlatform-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.

RouteSingle declarative interfaceWho runs the extra control planeUpgrade automationAdd-on bundle managementLearning curveTeam size it suits
Per-cloud Terraform modules (raw)NoNoneManual per cloudManual per cloudLowAny, but high toil
Wrapper modules, shared variable contractPartial (same inputs)NoneScripted per cloudOne umbrella chartLow-medium1-3 platform engineers
Cluster API (CAPA/CAPG/CAPZ)Yes (Cluster object)You (management cluster)DeclarativeVia add-on providersHighDedicated platform team
Crossplane compositionsYes (one composition)You (Crossplane control plane)DeclarativeVia compositionsHighDedicated platform team
Managed IDP (Qovery)Yes (console/API)Platform-managedManaged upgradesPlatform-managedLowSmall 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.

RouteWho runs the control planeWhere workloads runClouds supportedPolicy enforcementDeveloper self-serviceAssumed platform teamTime to first prod deploy
DIY GitOps (Argo CD + Kyverno + Terraform)YouYour accountsAWS/GCP/Azure/on-premYes (you build it)Medium (you build it)2+ engineersWeeks
Backstage portal + Argo CDYouYour accountsAnyVia Argo/KyvernoPortal UX, no delivery engine2+ engineersWeeks to months
Fleet managers (Anthos/GKE Enterprise, Azure Arc, Rafay)Vendor-anchoredYour clustersMulti, vendor-centricStrong (fleet policy)Low (operator-focused)1-2 engineersDays to weeks
HumanitecYou (orchestrator)Your accountsMultiVia your toolingMedium (assemble it)1-2 engineersWeeks
NorthflankVendor or BYOCVendor or your cloudMulti (BYOC)Platform-levelHighSmallHours to days
QoveryPlatform-managedYour own accounts (BYOC)AWS/GCP/Azure/Scaleway + your K8sPlatform-level, keep your own KyvernoHighSmall or noneMinutes 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.

Alessandro Carrano
About the author
Alessandro Carrano

Alessandro leads product at Qovery. He drives the changelog, roadmap, and product strategy - turning customer feedback into platform capabilities.

Next step

Ship faster on infrastructure you control.

Qovery gives your team self-service deployments on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.