GitOps Architecture¶
Design for the BRAC POC's GitOps control plane — how the openshift-platform-gitops repo, OpenShift GitOps (ArgoCD), and RHACM cooperate to drive all four OpenShift clusters.
Grounded in: Red Hat Advanced Cluster Management GitOps docs (RHACM 2.13) and Red Hat OpenShift GitOps 1.18 documentation (verified 2026-04-24).
The decisions that shape this¶
Per DECISION #025 — GitOps-only operational model:
- All cluster configuration + workload deployment flows through Git
oc apply/kubectl applyare reserved for bootstrap-only cases (initial GitOps operator install, break-glass situations)- Everything else: commit → push → ArgoCD reconciles → cluster updated
Per DECISION #024 — Red Hat operators only: OpenShift GitOps is the Red Hat build of ArgoCD (not community Argo CD).
Pattern: App-of-Apps + ApplicationSets + Pull Mode¶
Three Red Hat-recommended patterns combined for the right trade-offs:
1. App-of-Apps (governance)¶
A single root Application on hub-dc points at the bootstrap/ folder in the Git repo. That folder contains child Applications/ApplicationSets — which in turn manage everything else. One bootstrap command, one Git repo, everything else self-manages.
2. ApplicationSet (multi-cluster scaling)¶
ApplicationSet dynamically generates individual Application resources from a generator (cluster generator, list, git, matrix). One YAML definition → N target clusters. This is how we deploy the same workload to both spokes.
3. Pull mode via RHACM (scalability + isolation)¶
Instead of hub-dc's ArgoCD directly pushing manifests to spoke-dc, the hub propagates a ManifestWork that tells the spoke's own ArgoCD to pull from Git. This means:
- Hub controller load stays low (no N-way sync fan-out)
- Spoke is the source of truth for its own state (fits the DR model)
- Spoke continues reconciling even if hub is briefly unreachable
- Status aggregates back via
MulticlusterApplicationSetReport
Status (as of 2026-04-24): pull model via ApplicationSet + ACM annotations is GA in RHACM 2.13. The GitOps add-on (auto-installing the GitOps operator on spokes) is Technology Preview — we'll install manually via Subscription CRs in the bootstrap repo to avoid TP risk.
Hub-spoke wiring (RHACM integration)¶
Three CRs connect ArgoCD on hub-dc to the managed clusters:
flowchart LR
CS["ManagedClusterSet<br/>'brac-poc-clusters'"] -->|binds to namespace| CSB["ManagedClusterSetBinding<br/>in openshift-gitops"]
CSB --> PL["Placement<br/>selects managed clusters"]
PL --> GOC["GitOpsCluster<br/>registers selected clusters<br/>to ArgoCD's cluster list"]
GOC --> AS["ApplicationSet<br/>uses clusterDecisionResource<br/>generator → fans out"]
AS -->|ManifestWork<br/>pull mode| spoke1["spoke-dc ArgoCD"]
AS -->|ManifestWork<br/>pull mode| spoke2["spoke-dr ArgoCD"]
AS -->|direct sync| hubDR["hub-dr ArgoCD<br/>(for hub mgmt workloads)"]
Example GitOpsCluster (lives in bootstrap/acm-integration/):
```yaml apiVersion: cluster.open-cluster-management.io/v1beta2 kind: ManagedClusterSet metadata: name: brac-poc-clusters spec: clusterSelector: selectorType: ExclusiveClusterSetLabel
apiVersion: cluster.open-cluster-management.io/v1beta2 kind: ManagedClusterSetBinding metadata: name: brac-poc-clusters namespace: openshift-gitops spec: clusterSet: brac-poc-clusters
apiVersion: cluster.open-cluster-management.io/v1beta1 kind: Placement metadata: name: brac-poc-placement namespace: openshift-gitops spec: clusterSets: - brac-poc-clusters tolerations: - key: cluster.open-cluster-management.io/unreachable operator: Exists
apiVersion: apps.open-cluster-management.io/v1beta1 kind: GitOpsCluster metadata: name: brac-poc-gitops namespace: openshift-gitops spec: argoServer: cluster: local-cluster argoNamespace: openshift-gitops placementRef: kind: Placement apiVersion: cluster.open-cluster-management.io/v1beta1 name: brac-poc-placement ```
Then each spoke is labeled with its set membership + its role:
hub-dr: cluster.open-cluster-management.io/clusterSet=brac-poc-clusters
brac.poc/role=hub brac.poc/site=dr
spoke-dc: cluster.open-cluster-management.io/clusterSet=brac-poc-clusters
brac.poc/role=spoke brac.poc/site=dc
spoke-dr: cluster.open-cluster-management.io/clusterSet=brac-poc-clusters
brac.poc/role=spoke brac.poc/site=dr
These labels drive which ApplicationSet targets which cluster.
Repo structure — openshift-platform-gitops¶
Host: GitLab HA deployed on spoke-dc (POC Issue #4). Repo visibility: internal to the BRAC team; mirrored to comptech-lab/openshift-platform-gitops on GitHub for review.
openshift-platform-gitops/
├── README.md
├── bootstrap/
│ ├── 00-namespace-rbac.yaml # openshift-gitops ns + cluster-admin binding
│ ├── 01-argocd-instance.yaml # ArgoCD CR (Red Hat GitOps operator CRD)
│ ├── 02-root-application.yaml # Points at bootstrap/ itself (self-managing)
│ └── acm-integration/
│ ├── managed-cluster-set.yaml
│ ├── managed-cluster-set-binding.yaml
│ ├── placement-all.yaml
│ └── gitops-cluster.yaml
│
├── applicationsets/
│ ├── hub-platform.yaml # ACM, ACS, GitOps, Compliance, COO → hubs
│ ├── hub-keycloak.yaml # Keycloak + CNPG Postgres → hubs
│ ├── spoke-platform.yaml # Compliance Op, PSA, defaults → spokes
│ ├── spoke-workloads.yaml # 9 POC components → spokes
│ └── all-clusters-baseline.yaml # sysctl, audit rules, allowed-registries
│
├── components/ # Kustomize bases (reused across ApplicationSets)
│ ├── _base/
│ │ ├── acm/
│ │ ├── acs-central/
│ │ ├── openshift-gitops/
│ │ ├── compliance-operator/
│ │ ├── cluster-observability-operator/
│ │ ├── keycloak-rhbk/
│ │ └── keycloak-realm-brac-poc/
│ │
│ └── workloads/
│ ├── kafka/
│ ├── redis-sentinel/
│ ├── otel-signoz/
│ ├── wso2-apim/
│ ├── wso2-is/
│ ├── nginx-middleware/
│ ├── open-liberty-sample/
│ ├── trivy/
│ ├── jboss-domain/
│ └── sample-app-otel/
│
├── overlays/ # Per-cluster customizations
│ ├── hub-dc/
│ ├── hub-dr/
│ ├── spoke-dc/
│ └── spoke-dr/
│
├── policies/ # ACM Policy CRs (compliance baseline)
│ ├── etcd-encryption.yaml
│ ├── audit-profile.yaml
│ ├── allowed-registries.yaml
│ ├── default-deny-network-policy.yaml
│ ├── pod-security-restricted.yaml
│ └── placement-bindings.yaml
│
└── projects/ # ArgoCD AppProjects for RBAC
├── hub-project.yaml
├── spoke-project.yaml
└── platform-baseline.yaml
Kustomize layering¶
flowchart LR
Base["components/_base/acm"] --> OV["overlays/hub-dc"]
Base --> OV2["overlays/hub-dr"]
OV --> App["ApplicationSet (hub-platform.yaml)<br/>targets clusters labeled role=hub"]
OV2 --> App
App --> ArgoCD["hub-dc ArgoCD"]
ArgoCD -->|ManifestWork| HubDC[hub-dc cluster]
ArgoCD -->|ManifestWork| HubDR[hub-dr cluster]
Each overlay is a small kustomization.yaml that patches the base with cluster-specific values (hostnames, replica counts, resource sizing).
Example: deploying RHACM via GitOps¶
File: applicationsets/hub-platform.yaml
yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: hub-platform
namespace: openshift-gitops
spec:
generators:
- clusterDecisionResource:
configMapRef: ocm-placement-generator
labelSelector:
matchLabels:
cluster.open-cluster-management.io/placement: brac-poc-placement
matchExpressions:
- key: brac.poc/role
operator: In
values: [hub]
template:
metadata:
name: '{{name}}-hub-platform'
annotations:
apps.open-cluster-management.io/ocm-managed-cluster: '{{name}}'
apps.open-cluster-management.io/ocm-managed-cluster-app-namespace: openshift-gitops
argocd.argoproj.io/skip-reconcile: "true"
labels:
apps.open-cluster-management.io/pull-to-ocm-managed-cluster: "true"
spec:
project: platform-baseline
source:
repoURL: https://gitlab.apps.brac-poc.comptech-lab.com/brac-poc/openshift-platform-gitops.git
targetRevision: main
path: overlays/{{name}}/hub-platform
destination:
server: https://kubernetes.default.svc
namespace: openshift-gitops
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
Critical annotations for pull-mode:
| Annotation | Purpose |
|---|---|
apps.open-cluster-management.io/ocm-managed-cluster: '{{name}}' |
Identifies which cluster gets this app |
argocd.argoproj.io/skip-reconcile: "true" |
Hub ArgoCD does NOT sync this Application itself |
apps.open-cluster-management.io/pull-to-ocm-managed-cluster: "true" |
Label → ACM propagates via ManifestWork |
Status comes back via the auto-generated MulticlusterApplicationSetReport in the same namespace.
Compliance enforcement: ACM Policy vs ArgoCD¶
Both tools + different jobs. Per Red Hat guidance:
| Tool | Job | Example |
|---|---|---|
| ArgoCD ApplicationSet | Continuous delivery — "install this Deployment on those clusters" | RHACM operator, Kafka StatefulSet, sample apps |
| ACM Policy | Governance — "these clusters MUST have this config; auto-remediate drift" | restricted PSA on every namespace · audit profile = WriteRequestBodies · allowed registries list · default-deny NetworkPolicy · FIPS mode verification |
Why both? Policy enforces state; ApplicationSet deploys apps. A cluster can be policy-compliant (baseline) without having workloads, or have workloads without being compliant. We want both.
Example policy (policies/audit-profile.yaml):
```yaml apiVersion: policy.open-cluster-management.io/v1 kind: Policy metadata: name: require-audit-writerequestbodies namespace: openshift-gitops annotations: policy.open-cluster-management.io/standards: PCI-DSS policy.open-cluster-management.io/categories: "AC Access Control, AU Audit Accountability" policy.open-cluster-management.io/controls: "PCI-DSS 10.2, 10.3" spec: remediationAction: enforce policy-templates: - objectDefinition: apiVersion: policy.open-cluster-management.io/v1 kind: ConfigurationPolicy metadata: name: audit-writerequestbodies spec: object-templates: - complianceType: musthave objectDefinition: apiVersion: config.openshift.io/v1 kind: APIServer metadata: name: cluster spec: audit: profile: WriteRequestBodies
apiVersion: policy.open-cluster-management.io/v1 kind: PlacementBinding metadata: name: require-audit-writerequestbodies namespace: openshift-gitops placementRef: apiGroup: cluster.open-cluster-management.io kind: Placement name: brac-poc-placement subjects: - apiGroup: policy.open-cluster-management.io kind: Policy name: require-audit-writerequestbodies ```
All 4 clusters get the WriteRequestBodies audit profile automatically; any drift is auto-remediated.
Bootstrap sequence — the only oc commands we run¶
Step 1 (manual, once per hub): install the OpenShift GitOps operator on hub-dc and hub-dr via oc apply on these three files:
```yaml
00-namespace.yaml¶
apiVersion: v1 kind: Namespace metadata: name: openshift-gitops-operator
01-operatorgroup.yaml¶
apiVersion: operators.coreos.com/v1 kind: OperatorGroup metadata: name: openshift-gitops-operator namespace: openshift-gitops-operator
02-subscription.yaml¶
apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: openshift-gitops-operator namespace: openshift-gitops-operator spec: channel: gitops-1.18 # ← verify in Red Hat catalog before applying installPlanApproval: Automatic name: openshift-gitops-operator source: redhat-operators sourceNamespace: openshift-marketplace ```
Wait for the openshift-gitops namespace to appear + the ArgoCD Route to be ready (~2 min).
Step 2 (manual, once): apply the root Application that points at the repo:
```yaml
02-root-application.yaml¶
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: bootstrap-root namespace: openshift-gitops finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://gitlab.apps.brac-poc.comptech-lab.com/brac-poc/openshift-platform-gitops.git path: bootstrap/ targetRevision: main destination: server: https://kubernetes.default.svc namespace: openshift-gitops syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - ServerSideApply=true ```
From this point on, nothing else uses oc. The root Application syncs bootstrap/ which contains the ACM integration CRs + ApplicationSets, which in turn deploy every operator, every workload, and every policy.
Operations — how to change anything¶
| Want to… | Do |
|---|---|
| Deploy a new workload | Add components/workloads/<name>/ + reference in an ApplicationSet; commit + push |
| Change Kafka replicas on spoke-dc only | Edit overlays/spoke-dc/kafka/kustomization.yaml; commit + push |
| Enforce a new compliance control cluster-wide | Add a Policy in policies/; commit + push |
| Roll out a change to all spokes | Edit the base in components/_base/…; commit + push |
| Urgently break-glass a cluster | oc is acceptable — then reflect the change back in Git within 24h |
| Drift an ArgoCD-managed resource manually | Don't — ArgoCD selfHeal: true will revert it |
No one has write access to clusters without going through Git + PR review. Audit trail = git log.
PR workflow¶
flowchart LR
Dev["Engineer"] -->|branch: feat/xyz| Change["Edit YAML<br/>in openshift-platform-gitops"]
Change -->|git push| PR["GitLab MR"]
PR -->|CI: kubeconform +<br/>policy lint +<br/>yamllint| Review["Human review"]
Review -->|approve + merge| Main["main branch"]
Main -->|webhook / poll| ArgoCD["hub-dc ArgoCD"]
ArgoCD -->|" ManifestWork<br/>(pull mode)"| Spoke["target cluster(s)"]
Spoke -->|status| Report["MulticlusterAppSetReport"]
Report --> ACM["RHACM UI on hub-dc"]
The GitLab CI validates every merge request before it can be merged:
| Check | Tool |
|---|---|
| YAML syntax | yamllint |
| Kubernetes schema validity | kubeconform |
| Kustomize builds cleanly | kustomize build |
| ACM Policy structure | policy-generator-plugin --lint |
| ArgoCD AppProject ownership | custom check — App destinations must match the AppProject's allowlist |
| Secret leak scan | gitleaks |
What we lose vs full cluster-scoped freedom¶
To set expectations honestly:
- Urgent hotfixes still require a git commit. Mitigation: MR auto-merge for
hotfix/*branches after CI passes. Still auditable. - Experimentation requires branching, not live-edit on cluster. Mitigation: dev namespace on spoke-dc with relaxed policy; experiments there don't need PRs.
- Initial debugging curve — team members who are used to
oc applyneed a week to adjust. Mitigation: paired sessions during Phase 1.
What we gain: every cluster change is logged in git log with who, when, why. Full audit trail. Reproducibility. DR is "deploy the same Git state to new clusters". This is exactly what BRAC's compliance audit would want to see.
References¶
- RHACM GitOps Overview — primary source for pull-mode + GitOpsCluster
- OpenShift GitOps 1.18 docs — ArgoCD operator CR specifics
- ApplicationSet generator reference
- ACM Policy framework — policy-based cluster governance
Created: 2026-04-24 · Owner: DevOps + Platform Leads · Status: Ready for execution