🎯 The Problem: Manifest Sprawl

Kubernetes manifests start simple — a Deployment YAML and a Service YAML. Then a ConfigMap, then a Secret reference, then you need dev and prod variants, then a second service, then onboarding a new team... Without intentional structure, you end up with a flat folder of 200 YAML files where nobody knows what owns what or how to promote a change safely.

This lesson establishes the mental models and concrete patterns for organizing manifests at every scale. The key insight: the right structure is not universal — it scales with your team size and deployment complexity.

The three-scale evolution

SMALL 1–5 devs

One repo, flat structure, environment differences as value overrides. Simplicity wins. Optimize later.

MEDIUM 5–20 devs

Separate infra repo, Helm charts per service, overlays per environment, GitOps pipeline with ArgoCD or Flux.

ENTERPRISE 20+ devs

Platform team owns base charts + golden paths. Product teams own value files only. Tenant model with guardrails.

SMALL Pattern 1 — App-in-Repo (Co-located Manifests)

For a small team with one or a few services, keep manifests in the same repository as the application code. No separate infra repo, no Helm overhead. Fast to iterate, easy to understand.

my-app/
├── src/                        # application source code
├── Dockerfile
├── k8s/                        # ALL manifests live here
│   ├── base/
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   ├── configmap.yaml
│   │   └── kustomization.yaml  # ties base together
│   ├── overlays/
│   │   ├── dev/
│   │   │   ├── kustomization.yaml
│   │   │   └── patch-replicas.yaml  # dev: 1 replica
│   │   └── prod/
│   │       ├── kustomization.yaml
│   │       └── patch-replicas.yaml  # prod: 3 replicas, HPA
└── .github/workflows/
    └── deploy.yaml             # CI: build image → kustomize build → kubectl apply
💡 Why Kustomize (not raw YAML) even at small scale Raw YAML duplication between dev and prod is brittle — one edit to add a label must be made in N places. Even with a single service, Kustomize's base/overlay pattern eliminates duplication from day one and makes promotion trivially safe.

Small-scale Kustomize base

# k8s/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
images:
- name: my-app
  newTag: latest   # CI overrides: kustomize edit set image my-app=ghcr.io/org/app:v1.2.3

# k8s/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: patch-replicas.yaml
namePrefix: prod-   # optional: prefix all resource names
namespace: production

ENTERPRISE Pattern 3 — Platform Team + Tenant Model

At 20+ developers across multiple teams, the medium pattern breaks down: teams fight over the charts/ directory, base charts accumulate too many feature flags, and onboarding a new team takes days. The enterprise pattern separates platform concerns from product concerns.

platform-infra/                         # owned by Platform Team
├── base-charts/                        # golden path charts (opinionated)
│   ├── web-service/                    # any HTTP service
│   ├── worker-service/                 # any background worker
│   ├── cronjob/                        # any scheduled task
│   └── postgres-instance/             # managed DB provisioning
│
├── platform/                           # cluster-level config
│   ├── clusters/
│   │   ├── prod-eu-west-1/
│   │   │   ├── platform-apps.yaml      # ArgoCD AppOfApps
│   │   │   └── cluster-config/
│   │   └── prod-us-east-1/
│   ├── addons/                         # cert-manager, external-dns, etc.
│   └── policies/                       # Kyverno/OPA baseline policies
│
└── tenants/                            # one directory per product team
    ├── team-payments/
    │   ├── namespace.yaml
    │   ├── rbac.yaml
    │   └── apps/
    │       ├── checkout-api.yaml       # ArgoCD Application
    │       └── payment-worker.yaml
    └── team-catalog/
        ├── namespace.yaml
        └── apps/
product-team-payments/                  # owned by Payments Team
├── services/
│   ├── checkout-api/
│   │   ├── src/
│   │   ├── Dockerfile
│   │   └── deploy/                     # THIS is all a product team owns
│   │       ├── values.yaml             # base values for their service
│   │       ├── values.dev.yaml
│   │       ├── values.staging.yaml
│   │       └── values.prod.yaml        # PR to this = prod deploy
│   └── payment-worker/
│       └── deploy/
└── .github/
    └── workflows/
        └── update-image-tag.yaml       # CI: PR to platform-infra on merge

The App-of-Apps pattern with ArgoCD

In large clusters, you don't want to manually create ArgoCD Application objects. The App-of-Apps pattern uses one root ArgoCD Application that watches a directory of other Application manifests and deploys them automatically — self-bootstrapping the entire cluster:

# platform/clusters/prod-eu-west-1/platform-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: platform-apps
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "-1"   # deploy infra before apps
spec:
  source:
    repoURL: https://github.com/myorg/platform-infra
    path: tenants/                  # watches ALL team application files
    directory:
      recurse: true
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

📐 Naming Conventions — The Rules That Save You

ResourceConventionExampleWhy
Namespaceteam-env or teampayments-prod, platformTeam ownership clear; RBAC maps cleanly
Helm releaseservice-envcheckout-api-prodUnique across namespace; easy to grep
Image tagv{semver} or {branch}-{sha:7}v2.4.1, main-abc1234Traceability from deploy back to commit
Config labelsapp.kubernetes.io/* standard labelsapp.kubernetes.io/name: checkout-apiInteroperates with Prometheus, Grafana, ArgoCD
Values filesvalues.{env}.yamlvalues.prod.yamlAlphabetically sorted; glob-friendly
Chart versionSemVer; bump on breaking change1.0.0 → 1.1.0 → 2.0.0Allows pinning; changelog driven

Standard labels — always include these

# In every Deployment/Pod template (Helm helper or Kustomize commonLabels)
labels:
  app.kubernetes.io/name:       checkout-api
  app.kubernetes.io/instance:   checkout-api-prod   # Helm release name
  app.kubernetes.io/version:    v2.4.1              # image tag
  app.kubernetes.io/component:  api                 # api / worker / web
  app.kubernetes.io/part-of:    payments-platform  # owning system
  app.kubernetes.io/managed-by: Helm               # or Kustomize

MEDIUM Pattern 2 — Dedicated Infra Repo + Helm Charts

Once you have 3+ services and a real CI/CD pipeline, the manifests outgrow the app repo. A dedicated infrastructure repository becomes the single source of truth for what is deployed where. This is also the natural home for a GitOps controller like ArgoCD or Flux.

infra/                              # separate git repo: "my-org/infra"
├── charts/                         # Helm charts owned by this team
│   ├── frontend/
│   │   ├── Chart.yaml
│   │   ├── values.yaml             # defaults
│   │   └── templates/
│   ├── api/
│   │   ├── Chart.yaml
│   │   ├── values.yaml
│   │   └── templates/
│   └── database/
│       ├── Chart.yaml
│       └── templates/
│
├── environments/                   # what is deployed in each env
│   ├── dev/
│   │   ├── frontend.values.yaml    # dev-specific overrides
│   │   ├── api.values.yaml
│   │   └── argocd-apps.yaml        # ArgoCD Application manifests
│   ├── staging/
│   │   ├── frontend.values.yaml
│   │   └── api.values.yaml
│   └── prod/
│       ├── frontend.values.yaml
│       └── api.values.yaml
│
├── platform/                       # cluster-level resources
│   ├── namespaces.yaml
│   ├── rbac/
│   ├── network-policies/
│   └── monitoring/
│
└── scripts/
    └── deploy.sh                   # helm upgrade --install wrapper

Key discipline: values files are the interface

The chart (in charts/) is shared infrastructure. Each environment's *.values.yaml is what changes between environments. A PR changing a production value file is the deployment artifact — reviewable, auditable, rollbackable via git revert.

# environments/prod/api.values.yaml
replicaCount: 3
image:
  repository: ghcr.io/myorg/api
  tag: v2.4.1   # ← CI bumps this via PR on merge to main
resources:
  requests: { cpu: 200m, memory: 256Mi }
  limits:   { memory: 512Mi }
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
ingress:
  host: api.example.com
env:
  LOG_LEVEL: warn
  DB_HOST:   postgres.production.svc.cluster.local
# environments/dev/api.values.yaml
replicaCount: 1
image:
  tag: main-abc123   # latest commit on main branch
resources:
  requests: { cpu: 50m, memory: 64Mi }
autoscaling:
  enabled: false
ingress:
  host: api.dev.example.com
env:
  LOG_LEVEL: debug
  DB_HOST:   postgres.dev.svc.cluster.local
🔵 The app repo's only job: update the image tag When CI builds a new image, its only job in the infra repo is to open a PR (or directly commit to dev) that bumps the image.tag in the right values file. The infra repo owns what is deployed; the app repo owns what the image contains. This clean separation is the foundation of GitOps.

ArgoCD Application per environment

# environments/prod/argocd-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-prod
  namespace: argocd
spec:
  project: production
  source:
    repoURL: https://github.com/myorg/infra
    targetRevision: HEAD
    path: charts/api
    helm:
      valueFiles:
      - ../../environments/prod/api.values.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true   # revert manual kubectl changes

🧠 Knowledge Check

Q1. A startup has one service. Should they use a dedicated infra repo from day one?

A) Yes — always separate infra from app code for clean boundaries
B) No — co-located manifests in k8s/ with Kustomize base/overlays are sufficient and simpler
C) Only if using ArgoCD
D) Yes — GitOps requires a dedicated infra repo

Q2. In the medium-scale pattern, what is CI's only responsibility when a new image is built?

A) Run kubectl apply with the new image tag directly
B) Tag the Docker image and restart the Deployment
C) Open a PR in the infra repo bumping the image tag in the environment values file
D> Build the Helm chart and push it to a chart registry

Q3. What problem does the App-of-Apps pattern solve at enterprise scale?

A) It improves application performance by distributing load across multiple ArgoCD instances
B) It enables parallel deployments of the same application
C) It eliminates manual ArgoCD Application creation — adding a team is a PR to a directory, fully GitOps
D> It ensures all applications share the same resource limits

Q4. In the enterprise model, a product team wants to add a new environment variable to their service. What do they change and where?

A) Modify the base chart in the platform-infra repo and open a platform team PR
B) Add it to their own values.prod.yaml in their product repo — no platform team involvement needed
C) Add it directly to the Deployment YAML in the cluster via kubectl edit
D> Create a new Kustomize patch in the platform-infra tenants/ directory