Skip to content

26 — Deploying a Multi-Microservice Application (Pattern)

The reusable pattern for onboarding a whole application made of many microservices onto the cluster using the 3-layer App of Apps hierarchy — one scoped AppProject per group, one Application per service, traceable names modgpt.<app>.<service>.

Worked example → ASK

For a concrete, fill-in-the-blanks onboarding of a real app, see 27 — ASK Application Onboarding. The creation runbook is in How-To → Create a New App Group.


The Pattern at a Glance
LAYER 1 — modgpt (root, shared across all apps)
  AppProject: modgpt
  Application: modgpt  ──→  cluster-addons/root-bootstrap/

LAYER 2 — one entry per application group
  AppProject: modgpt.<app>           ← scopes this app's repos + namespaces
  Application: modgpt.<app>  ──→  <app>/bootstrap/

LAYER 3 — one entry per microservice
  Application: modgpt.<app>.postgres   ← Helm chart, wave 0
  Application: modgpt.<app>.api        ← Helm chart, wave 1
  Application: modgpt.<app>.worker     ← Helm chart, wave 1
  Application: modgpt.<app>.frontend   ← Helm chart, wave 2

Each Application is a plain YAML file in bootstrap/ of its app's git repo. No ApplicationSet, no generators, no templates. Every deployed service is explicit, auditable, and independently rollback-able.


Key Design Decisions
Question Answer Rationale
One Application per microservice or umbrella chart? One per microservice Independent sync/health/rollback — umbrella charts hide which service is broken
ApplicationSet or App of Apps? Pure App of Apps Explicit YAML per service — no auto-discovery surprises, full audit trail
Per-service AppProject? No — reuse Layer 2 modgpt.<app> Layer 2 AppProject already scopes sources + destinations; per-service AppProjects add overhead with no RBAC benefit for a single operator
New AppProject per app group? Yes — one modgpt.<app> AppProject Each application group gets its own scoped boundary
Naming convention? modgpt.<app>.<service> Traceable from L1 → L3 in the ArgoCD portal

Repository Layout (per application)

Each application has its own GitLab repo (gitlab.cl1.sq4.aegis.internal/modgpt/<app>):

<app>/
├── bootstrap/                        ← Layer 2 app points here
│   ├── application-postgres.yaml     ← Layer 3: data store (wave 0)
│   ├── application-api.yaml          ← Layer 3: API server (wave 1)
│   ├── application-worker.yaml       ← Layer 3: background worker (wave 1)
│   └── application-frontend.yaml     ← Layer 3: frontend (wave 2)
├── charts/                           ← in-house Helm chart (if no public chart)
│   └── <app>-service/
│       ├── Chart.yaml
│       └── templates/
└── values/                           ← Helm values per service
    ├── postgres/<env>.yaml
    ├── api/<env>.yaml
    ├── worker/<env>.yaml
    └── frontend/<env>.yaml

The cluster-addons repo (root-bootstrap/) holds the Layer 2 pair that points to this repo:

cluster-addons/root-bootstrap/
  appproject-<app>.yaml     ← Layer 2 AppProject (sync-wave: -1)
  application-<app>.yaml    ← Layer 2 Application (sync-wave:  0)

Layer 3 Application YAML (template)

Each service is one Application file. The structure is identical for every service — only name, namespace, chart, and values differ:

# bootstrap/application-api.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: modgpt.<app>.api
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "1"       # after data stores (wave 0)
spec:
  project: modgpt.<app>                      # Layer 2 AppProject
  destination:
    server: https://kubernetes.default.svc
    namespace: <app>
  syncPolicy:
    automated: { prune: true, selfHeal: true }
    syncOptions: [CreateNamespace=true]
  sources:
    - repoURL: https://gitlab.cl1.sq4.aegis.internal/api/v4/projects/<PID>/packages/helm/stable
      chart: <app>-service
      targetRevision: "1.0.0"
      helm:
        releaseName: api                      # short name — no dots
        valueFiles: ["$values/values/api/api-stg.yaml"]
    - repoURL: https://gitlab.cl1.sq4.aegis.internal/modgpt/<app>.git
      targetRevision: HEAD
      ref: values

Sync wave ordering

Wave Services Examples
0 Data stores PostgreSQL, Redis, Kafka
1 Orchestrators + backend Hatchet, core-service, worker
2 Consumers + frontend Web UI, ingress

Cross-Cutting Concerns
Concern Implementation
Secrets Vault KV v2 (secret/<app>/*) → VSO VaultStaticSecret renders into app namespace. Never in values files.
Images Harbor container registry (harbor.cl1.sq4.aegis.internal/ask/<image>). Pull secret in every app namespace.
Ingress + TLS cert-manager issues the <app>.mod.auh1.dev.dir cert from the internal CA. One ingress on the frontend/gateway service.
Resource limits Every service sets requests + limits. No burstable/BestEffort pods in production.
Health checks Liveness + readiness probes on every Deployment so ArgoCD health is meaningful.
Stateful services HA + PDB + OnDelete update strategy. Roll one pod at a time. See KB-01 — Raft / Quorum Clusters.
Rollback Per-service: argocd app rollback modgpt.<app>.<svc> — rolls back only the broken microservice.

Adding a New Microservice to an Existing App

No changes to cluster-addons. Only the app's own repo:

cd ~/<app>
mkdir -p values/<newsvc>
vi values/<newsvc>/<newsvc>-stg.yaml

cat > bootstrap/application-<newsvc>.yaml <<'EOF'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: modgpt.<app>.<newsvc>
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec:
  project: modgpt.<app>
  destination:
    server: https://kubernetes.default.svc
    namespace: <app>
  syncPolicy:
    automated: { prune: true, selfHeal: true }
    syncOptions: [CreateNamespace=true]
  sources:
    - repoURL: https://gitlab.cl1.sq4.aegis.internal/api/v4/projects/<PID>/packages/helm/stable
      chart: <newsvc>
      targetRevision: "1.0.0"
      helm:
        releaseName: <newsvc>
        valueFiles: ["$values/values/<newsvc>/<newsvc>-stg.yaml"]
    - repoURL: https://gitlab.cl1.sq4.aegis.internal/modgpt/<app>.git
      targetRevision: HEAD
      ref: values
EOF

git add bootstrap/ values/
git commit -m "feat: add <newsvc> microservice"
git push origin main
# modgpt.<app> creates modgpt.<app>.<newsvc> automatically

Full Platform View
ArgoCD portal after onboarding two apps:

modgpt                               L1 — root
  modgpt.cluster-addons              L2 — platform add-ons
    modgpt.cluster-addons.vault      L3
    modgpt.cluster-addons.vso        L3
    modgpt.cluster-addons.reloader   L3
    modgpt.cluster-addons.pdb        L3
  modgpt.ask                         L2 — ASK application
    modgpt.ask.postgres              L3 — wave 0
    modgpt.ask.core-service          L3 — wave 1
    modgpt.ask.hatchet               L3 — wave 1
    modgpt.ask.frontend              L3 — wave 2

➡️ Next: apply this pattern to a real app — 27 — ASK Application Onboarding