In the previous parts of this series, we focused on instrumentation, the OpenTelemetry Operator, and the Collector. Now we shift gears and build the GitOps architecture that will manage everything, platform components, workloads, and environment‑specific configuration in a clean, scalable, production‑ready way.
This is where the project becomes a real platform.
All manifests, ApplicationSets, and configuration used in this series are available in the companion GitHub repository
🎯 What We’re Building in Part 4
By the end of this part, you will have:
- A multi‑environment GitOps structure
- A clean separation between:
- Platform components (cert-manager, OTel Operator, Collector)
- Application workloads (demo-dotnet)
- A split ApplicationSet model:
- One ApplicationSet for Helm‑based platform components
- One ApplicationSet for plain‑YAML platform components
- One ApplicationSet for application workloads
- Matrix generators that produce environment named instances e.g.
dev-cert-manager,dev-collector,dev-demo-dotnet, if another environment was added e.g. staging then they would bestaging-cert-manager, staging-collector, staging-demo-dotnet - Sync waves to enforce deterministic ordering
- Namespace isolation per environment
- Environment‑specific overrides via
environments/{{.environment}}/values/
This is the architecture used by real platform teams running GitOps at scale.
📁 Repository Structure
A clean repo structure makes GitOps easier. This series uses:
argocd/
app-of-apps.yaml
applicationset-platform-helm.yaml
applicationset-platform.yaml
applicationset-apps.yaml
platform/
cert-manager/
opentelemetry-operator/
collector/
apps/
demo-dotnet/
environments/
dev/
values/
platform-values.yaml
apps-values.yaml
This structure is intentionally simple, scalable, and DRY.
🌱 The App-of-Apps Root
Argo CD starts with a single root Application:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/<your-repo>
targetRevision: main
path: argocd
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: false
selfHeal: false
syncOptions:
- CreateNamespace=true
- PruneLast=true
This Application discovers and applies all ApplicationSets inside argocd/.
🏗️ Splitting Platform Components:
Not all platform components are created equal:
- cert-manager → Helm chart
- opentelemetry-operator → Helm chart
- collector → plain YAML
Trying to force everything through Helm creates errors and unnecessary complexity.
So we split the platform into two ApplicationSets:
1. applicationset-platform-helm.yaml
Manages cert-manager + OTel Operator.
2. applicationset-platform.yaml
Manages the Collector.
This keeps the repo clean and avoids Helm‑related errors
🧬 Matrix Generators: env × component
Each ApplicationSet uses a matrix generator:
- One list defines environments (
dev,staging,prod) - One list defines components (e.g., cert-manager, operator, collector)
This series includes only a dev environment, but the structure supports adding staging and prod with no additional changes
Argo CD multiplies them:
(dev × cert-manager)
(dev × operator)
(dev × collector)
(staging × cert-manager)
...
This produces a clean, predictable set of Applications per environment.
⏱️ Sync Waves: Ordering Matters
Platform components must deploy in the correct order:
| Wave | Component |
| 0 | cert-manager, opentelemetry-operator |
| 1 | collector |
| 3 | workloads |
This ensures:
- CRDs exist before the Operator starts
- The Collector exists before workloads send telemetry
- Workloads deploy last
🌍 Environment-Specific Overrides
Each environment has its own values e.g.
environments/dev/values/platform-values.yaml
environments/staging/values/platform-values.yaml
environments/prod/values/platform-values.yaml
Only dev is included in this series, but the pattern scales to additional environments easily.
This keeps platform definitions DRY while allowing environment‑specific behaviour.
🚀 The Result
By the end of Part 4, you have:
- A fully declarative, multi‑environment GitOps architecture
- Clean separation of platform vs apps
- Deterministic ordering via sync waves
- Environment‑specific overrides
- Namespace isolation
- A scalable pattern for adding new apps or environments
This is the foundation for everything that follows in the series.