At Backbase, we operate a Banking-as-a-Service platform that serves 150+ banks and credit unions. That means running Kubernetes clusters for each customer — across Azure AKS and AWS EKS — and deploying to all of them reliably, repeatedly, and safely.
This post covers how we do it with ArgoCD and GitHub Actions, and the lessons we’ve learned along the way.
The Core Idea
GitOps is simple in principle: Git is the single source of truth. Every change to infrastructure or application config goes through a pull request. The cluster reconciles itself to match what’s in Git.
ArgoCD is our reconciliation engine. It watches a set of Git repositories and continuously ensures the cluster state matches the declared state. If someone manually patches a Kubernetes resource, ArgoCD drifts it back.
GitHub Actions handles the CI side: building images, running tests, bumping image tags in the config repo, and opening PRs.
Our Repository Structure
We use a two-repo model:
app-repo/ # Application source code + Dockerfile
.github/
workflows/
ci.yml # Build, test, push image
config-repo/ # Kubernetes manifests (Helm values + Kustomize overlays)
clusters/
customer-a/
values.yaml
customer-b/
values.yaml
base/
deployment.yaml
The CI pipeline in app-repo builds the image, pushes it to the registry, and opens a PR in config-repo to bump the image tag. ArgoCD picks up the merged config change and syncs the cluster.
Handling Scale: App-of-Apps Pattern
With 100+ tenants, maintaining individual ArgoCD Application manifests gets unwieldy. We use the App-of-Apps pattern: a parent Application that generates child Application objects from a template.
# parent-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: tenant-apps
spec:
source:
repoURL: https://github.com/org/config-repo
targetRevision: HEAD
path: clusters/
destination:
server: https://kubernetes.default.svc
namespace: argocd
Each subdirectory under clusters/ becomes its own Application, scoped to its tenant namespace.
The Gotchas
1. Sync waves matter. Deploy CRDs before the resources that depend on them. ArgoCD sync waves let you order deployments within a single sync.
2. Secrets don’t belong in Git. We use External Secrets Operator to pull secrets from Azure Key Vault at sync time, not at build time.
3. Don’t auto-sync production. Staging clusters auto-sync on every commit. Production requires a manual promotion step — a PR from staging config to prod config, reviewed by a second engineer.
4. Health checks save you. ArgoCD’s custom health checks let you define what “healthy” means for your custom resources. Don’t skip this step for CRDs.
Key Takeaways
- Two-repo model cleanly separates CI (build) from CD (deploy)
- App-of-Apps scales well to 100+ tenants without duplication
- External Secrets Operator keeps secrets out of Git without compromising the GitOps model
- Manual promotion gates for production give you confidence without slowing down dev
GitOps at scale isn’t magic — it’s boring consistency. And boring consistency is exactly what production systems need.
Have questions or a different approach? Reach me on LinkedIn or by email.