Building a Multi-Region EKS Platform with Crossplane, FluxCD, and GitOps
Originally published on Medium on 18 June 2026.

Multi-region Kubernetes sounds simple until you try to operate it. At first, the problem looks like this: “We need to deploy the same application to multiple EKS clusters.”
But very quickly, the real problem becomes much bigger:
- Who creates the AWS infrastructure?
- Who owns the EKS clusters?
- How are applications onboarded?
- How do we avoid configuration drift?
- How do we promote changes safely?
- How do we know what version is running in each region?
- How do we recover when one region fails?
In this series, I want to describe a platform architecture for managing multi-region deployments on AWS EKS using Crossplane, FluxCD, GitOps, and GitHub Deployments.
This is not meant to be the only correct architecture. It is a practical pattern for building a platform where infrastructure, cluster configuration, application onboarding, and promotion are all declarative and auditable.
The main idea is simple:
- Crossplane provisions the infrastructure.
- FluxCD reconciles the clusters.
- A platform orchestration app connects application repositories to target namespaces.
- A promotion engine uses GitHub Deployments as the deployment context.
- Git remains the source of truth.
The problem with multi-region platforms

A single EKS cluster is usually easy to reason about. You have one cluster, one ingress layer, one deployment pipeline, one set of Helm values, and one place to check when something breaks. Multi-region changes the shape of the problem.
Now we may have clusters like:
- prod-eu-west-1
- prod-eu-central-1
- prod-us-east-1
Each region needs similar platform components:
- aws-load-balancer-controller
- external-dns
- cert-manager
- observability
- gateway layer
- application namespaces
- networking policies
- secrets integration
But each region may also need slightly different configuration:
- AWS account
- AWS region
- cluster name
- domain name
- replica count
- availability zones
- traffic weight
- failover role
- regional dependencies
If we manage this manually, drift is almost guaranteed.
- One cluster ends up with a different controller version.
- Another cluster has a missing namespace policy.
- Another cluster is still running an old application version.
- Another region was patched manually during an incident and never reconciled back.
This is exactly the type of problem GitOps helps with.
The platform model
The architecture I like separates the platform into four major layers:
- Infrastructure provisioning
- Cluster reconciliation
- Application onboarding
- Promotion and rollout control
Each layer has a clear responsibility.
- Crossplane
- Owns cloud infrastructure.
- FluxCD
- Owns reconciliation inside Kubernetes clusters.
- Platform orchestration app
- Owns onboarding and generated GitOps configuration.
- Promotion engine
- Owns release progression across environments and regions.
The important design choice is that none of these systems should secretly bypass Git. Even if an internal app generates configuration, the output should still be committed to Git. Even if a promotion engine decides that a version is ready for production, the promotion should still result in a Git change. Even if Crossplane provisions infrastructure, the desired state should still be declared as Kubernetes resources.
This keeps the whole platform reviewable, auditable, and easier to debug.
High-level architecture
At a high level, the system looks like this:

The management cluster runs the platform control-plane components. Crossplane runs there and provisions AWS infrastructure. The platform orchestration app also runs there and generates Flux configuration for applications and namespaces.
Each target EKS cluster runs FluxCD.
Flux reads from Git and reconciles the desired state into the cluster.
The promotion engine watches GitHub deployment context, decides what can move forward, and updates Git accordingly.
Why Crossplane?
In this model, Crossplane is responsible for AWS infrastructure.
That can include:
- VPCs
- subnets
- EKS clusters
- IAM roles
- security groups
- Route 53 records
- S3 buckets
- regional dependencies
Instead of using a separate workflow where infrastructure is created outside the Kubernetes control plane, Crossplane lets the platform expose infrastructure as Kubernetes-style APIs.
The platform team can define higher-level abstractions like:
- CompositeEKSCluster
- CompositeRegionalNetwork
- CompositeApplicationDNS
- CompositeTenantNamespace
Behind those abstractions, Crossplane compositions can create the lower-level AWS resources. Application teams do not need to understand all the AWS details. They request a platform capability. The platform translates that request into cloud resources.
Why FluxCD?

FluxCD is responsible for reconciliation inside the target clusters. Each EKS cluster runs Flux. Flux watches Git sources and applies the desired Kubernetes state.
For example, a cluster may reconcile:
- platform controllers
- tenant namespaces
- RBAC
- network policies
- application manifests
- Helm releases
- Kustomize overlays
A key benefit is that every cluster reconciles independently. If one AWS region has a problem, the other clusters do not need to depend on it. Each cluster has its own Flux controllers and its own reconciliation loop. That makes the architecture more resilient and easier to reason about.
Why an orchestration app?

Flux is powerful, but asking every application team to manually create Flux resources is not a great developer experience.
For every application, we may need to generate:
- Namespace
- ServiceAccount
- Role
- RoleBinding
- GitRepository
- Kustomization
- optional HelmRelease
- network policies
- default resource quotas
Now multiply that by:
- many applications
- many clusters
- many regions
- many environments
This becomes repetitive and error-prone. The platform orchestration app solves this by providing a higher-level interface.
An application team might only need to provide:
application: payment-service
repository: github.com/example/payment-service
namespace: payment-service
targets:
- environment: staging
cluster: staging-eu-west-1
path: ./deploy/overlays/staging
- environment: production
cluster: prod-eu-west-1
path: ./deploy/overlays/prod-eu-west-1
- environment: production
cluster: prod-eu-central-1
path: ./deploy/overlays/prod-eu-central-1
The platform app then generates the lower-level GitOps configuration. The important part is that the app should not directly mutate the cluster. It should generate declarative configuration and commit it to the fleet repository.
Flux then reconciles that configuration.
Why GitHub Deployments?

The missing piece is promotion. Flux is excellent at reconciling desired state, but it does not decide business-level rollout policy.
For example:
- Can this version move from staging to production?
- Did the deployment pass smoke tests?
- Was it approved?
- Is this version already running in another region?
- Did prod-eu-west-1 succeed before prod-eu-central-1 starts?
- Should we pause promotion because error rate increased?
This is where GitHub Deployments are useful.
A GitHub Deployment represents a request to deploy a specific ref, branch, SHA, or tag to an environment.
Deployment statuses then provide the lifecycle context: queued, in_progress, success, failure, inactive
The promotion engine can use this context to understand what happened, what is currently running, and what can happen next.
In this architecture, GitHub Deployments become the shared language between application repositories, CI workflows, and the platform promotion engine.
The most important design rule
The promotion engine should not become a hidden deployment system. It should not secretly run kubectl apply. It should not mutate production clusters directly. It should use GitHub Deployments as context, evaluate promotion rules, and then create Git changes.
For example:
- App repository creates a deployment for staging.
- CI builds and tests the application.
- Promotion engine sees staging succeeded.
- Promotion engine creates a pull request to update the fleet repo for prod-eu-west-1.
- Flux applies the change in prod-eu-west-1.
- Promotion engine observes the result and updates GitHub deployment status.
- If healthy, the engine promotes to the next region.
Git remains the source of truth. The promotion engine makes decisions. Flux applies desired state. That separation is what keeps the architecture clean.
Final thoughts
A multi-region EKS platform is not just a collection of clusters. It is a control plane.
- Crossplane gives us infrastructure as APIs.
- FluxCD gives us continuous reconciliation.
- The orchestration app gives us a better developer experience.
- The promotion engine gives us controlled rollout progression.
- GitHub Deployments give us deployment context and history.
- Git gives us auditability.
In the next article, we will go deeper into the infrastructure layer and look at how Crossplane can provision the AWS foundation for a multi-region EKS platform.
Next in this series: Provisioning Multi-Region AWS Infrastructure with Crossplane.