Your first GitOps deployment: manifest repository structure, creating the GitRepository source and Kustomization, deploying your first application, then testing auto-sync by changing the configuration through Git.

In episode 5 you mastered the Flux CLI — flux check, flux get, flux reconcile, up to flux create. Now we put all those tools to real use: your first GitOps deployment. Starting with this episode, Git becomes the only source of truth; the cluster merely follows what's in the repository.
We'll set up the repository structure, the GitRepository source, and the Kustomization, then deploy the first application and test the GitOps loop.
The manifest repository (fleet repo) is where all cluster configuration is stored:
gitops-cluster/
├── clusters/production/
│ └── flux-system/
├── apps/webapp/
│ ├── deployment.yaml
│ └── service.yaml
└── infrastructure/
└── ingress-nginx/clusters/ — cluster-specific configuration, including the bootstrap files from flux bootstrap in episode 4.apps/ — business application configuration: one folder per application.infrastructure/ — platform components like the ingress controller, monitoring, or storage.Tip
There are no rigid rules; what matters is consistency. Some teams put everything under apps/, others separate infrastructure/ because its PR permissions differ. Choose what fits your team's size.
The source is the starting point of every GitOps flow. Flux doesn't apply manifests directly from the repo; it copies the repo into a local cache, then renders and applies those manifests. The object that manages this is GitRepository:
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: webapp
namespace: flux-system
spec:
interval: 1m
ref:
branch: main
url: https://github.com/arman/webapp-manifests.gitinterval — how often Flux checks the repo. For production, usually 5m to 10m; for a lab, 1m so changes are detected quickly.ref — can be branch, tag, commit, or semver. Details are covered in episode 9.Authentication is configured via spec.secretRef:
| Method | URL | Secret | Best for |
|---|---|---|---|
| HTTPS + PAT | https://github.com/... | username + password | GitHub, GitLab |
| HTTPS + token | https://github.com/... | token | Token without a username |
| SSH | git@github.com:... | identity + known_hosts | Private repos, strict security |
Public repos don't need a secret at all. Private HTTPS repos use basic auth with a personal access token as the password:
flux create secret git webapp-auth \
--url=https://github.com/arman/webapp-manifests.git \
--username=arman \
--password=$GITHUB_TOKENImportant
Never put a token in a manifest. Always store it in a Kubernetes Secret, and never commit that Secret to Git. For production needs, episode 11 covers SOPS and the External Secrets Operator.
Verify the source syncs successfully:
flux get sources gitThe flux get sources git output shows the READY and STATUS columns. If READY is True, the repo has been successfully copied into the Flux cache.
GitRepository only provides the contents of the repo. The Kustomization is what determines what gets applied — this is a Kustomize Controller CRD, not to be confused with the kustomization.yaml file covered in episode 7:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: webapp
namespace: flux-system
spec:
interval: 5m
path: ./webapp
prune: true
sourceRef:
kind: GitRepository
name: webapp
targetNamespace: webappsourceRef — points to which source to use, here the GitRepository named webapp.path — the directory inside the repo to render. It can be a folder containing a kustomization.yaml, or plain YAML files directly.targetNamespace — the default namespace for resources that don't specify one.prune — removes resources that are no longer in Git (garbage collection).Now let's populate the repo with the webapp application manifests in the webapp/ directory:
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
namespace: webapp
spec:
replicas: 2
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: nginx:1.27
ports:
- containerPort: 80Add a namespace.yaml file to create the webapp namespace, then commit everything:
git add webapp/
git commit -m "feat: add webapp manifests"
git push origin main
flux reconcile source git webappflux reconcile source git webapp forces Flux to fetch the latest version without waiting for the interval.
Wait a few seconds, then check:
flux get kustomizations
kubectl get deployment webapp -n webapp
kubectl get pods -n webappCheck the latest events if there are any problems:
kubectl describe kustomization webapp -n flux-system
kubectl get events -n flux-system --sort-by=.lastTimestamp | tail -20Warning
If flux get kustomizations shows READY as False, the message in the STATUS column is your best clue. The most common causes: a wrong path, the namespace not created yet, or sourceRef not yet Ready. Also check kubectl describe and events before panicking.
This is where the heart of GitOps is tested. Change the replica count from 2 to 4, then commit and push — without touching kubectl apply:
git add webapp/deployment.yaml
git commit -m "chore: scale webapp to 4 replicas"
git push origin main
flux reconcile kustomization webapp
kubectl get pods -n webappFlux detects the change in the repo, re-renders the manifests, and adjusts the cluster. No manual apply, no drift — Git and the cluster stay aligned.
Note
Compare this with the old way: log into the cluster, change the deployment manually, forget to record the change. With GitOps, every change has an audit trail in Git and can be rolled back with just a git revert followed by a push.
Your first GitOps deployment is running and tested:
clusters/, apps/, infrastructure/ structure keeps the manifest repo tidy and easy to navigate.GitRepository provides the repo contents to Flux; authentication via HTTPS or SSH is handled through Secrets.Kustomization determines the path, source, target namespace, and prune behavior.flux get, kubectl get, and events before trusting the status.Everything still uses plain manifests. In episode 7 we'll work with Kustomize: reusable base templates, overlays per environment, patch strategies, and Kustomization CRD features that the standalone Kustomize binary doesn't have. See you!