GitOps Framework: How the Fako Cluster Repository is Organized

Infrastructure
Table of Contents

Overview

The Fako Cluster uses a GitOps approach where Git is the single source of truth for all cluster state. Every deployment, configuration change, and infrastructure update flows through Git commits. FluxCD watches the repository and automatically reconciles the cluster to match the desired state defined in code.

This post explains the repository structure, the reasoning behind each directory, and how the pieces work together.

Repository Structure

fako-cluster/
โ”œโ”€โ”€ apps/                    # Application deployments
โ”‚   โ”œโ”€โ”€ base/               # Base Kustomize configurations
โ”‚   โ””โ”€โ”€ staging/            # Environment overlay (what actually deploys)
โ”œโ”€โ”€ clusters/                # Cluster bootstrap and Flux config
โ”‚   โ””โ”€โ”€ staging/            # Flux Kustomizations that orchestrate everything
โ”œโ”€โ”€ infrastructure/          # Platform components
โ”‚   โ”œโ”€โ”€ controllers/        # Operators (Cert-Manager, CloudNative-PG, etc.)
โ”‚   โ””โ”€โ”€ configs/            # Configuration for controllers
โ”œโ”€โ”€ monitoring/              # Observability stack
โ”‚   โ”œโ”€โ”€ controllers/        # Prometheus, Loki, Grafana, etc.
โ”‚   โ””โ”€โ”€ configs/            # Dashboards, alerting rules
โ”œโ”€โ”€ scripts/                 # Deployment and maintenance scripts
โ””โ”€โ”€ notes/                   # Documentation and troubleshooting

The Base/Staging Pattern

The most important concept to understand is the base/staging pattern used throughout the repository.

What is Base?

The base/ directory contains the actual Kubernetes manifests for each application:

apps/base/n8n/
โ”œโ”€โ”€ namespace.yaml          # Namespace definition
โ”œโ”€โ”€ kustomization.yaml      # Kustomize configuration
โ”œโ”€โ”€ deployment.yaml         # The actual deployment
โ”œโ”€โ”€ service.yaml            # Service definition
โ”œโ”€โ”€ ingress.yaml            # External access
โ”œโ”€โ”€ storage.yaml            # Persistent volume claims
โ”œโ”€โ”€ external-secret.yaml    # Secret references
โ””โ”€โ”€ ...

Each application in base/ is a complete, self-contained unit with all resources it needs to run.

What is Staging?

The staging/ directory is an overlay that selects which applications from base/ should be deployed:

# apps/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  # AI/ML stack
  - ollama
  - open-webui
  - whisper
  - piper

  # Workflow Automation
  - n8n

  # Disabled apps (commented out)
  # - linkding
  # - audiobookshelf

Why This Pattern?

  1. Easy enable/disable: Comment out a line to disable an app, uncomment to enable
  2. Clean separation: Base contains the “what”, staging contains the “which”
  3. Future multi-environment: Could add production/ overlay with different selections
  4. No duplication: All manifests live in one place (base), overlays just reference them

The Four Pillars

The repository is organized into four main sections, each with a specific purpose:

1. Infrastructure Controllers

infrastructure/controllers/base/
โ”œโ”€โ”€ cert-manager/           # TLS certificate automation
โ”œโ”€โ”€ cloudnative-pg/         # PostgreSQL operator
โ”œโ”€โ”€ external-secrets/       # AWS Secrets Manager integration
โ”œโ”€โ”€ gpu-operator/           # NVIDIA GPU support
โ”œโ”€โ”€ nfs-storage/            # NFS CSI driver
โ”œโ”€โ”€ smb-csi/                # SMB/CIFS storage driver
โ”œโ”€โ”€ renovate/               # Automated dependency updates
โ””โ”€โ”€ k8s-backup/             # ETCD and PVC backups

Purpose: Install operators and controllers that provide platform capabilities. These must be installed before applications can use features like TLS certificates, external secrets, or GPU acceleration.

2. Infrastructure Configs

infrastructure/configs/base/
โ”œโ”€โ”€ cert-manager/           # ClusterIssuers, certificates
โ”œโ”€โ”€ nfs-config/             # NFS mount configurations
โ””โ”€โ”€ control-plane-taint/    # Node configuration

Purpose: Configure the controllers installed above. For example, after Cert-Manager is installed, this creates the ClusterIssuer that applications use for TLS.

3. Monitoring

monitoring/controllers/base/
โ”œโ”€โ”€ kube-prometheus-stack/  # Prometheus, Grafana, AlertManager
โ”œโ”€โ”€ loki-stack/             # Log aggregation
โ”œโ”€โ”€ alloy/                  # Unified telemetry collection
โ”œโ”€โ”€ tempo/                  # Distributed tracing
โ”œโ”€โ”€ gpu-monitoring/         # NVIDIA GPU metrics
โ””โ”€โ”€ metrics-server/         # Resource metrics

Purpose: Observability stack. Separated from infrastructure because monitoring has its own lifecycle and can be updated independently.

4. Applications

apps/base/
โ”œโ”€โ”€ ollama/                 # LLM inference server
โ”œโ”€โ”€ n8n/                    # Workflow automation
โ”œโ”€โ”€ paperless-ngx/          # Document management
โ”œโ”€โ”€ open-webui/             # AI chat interface
โ”œโ”€โ”€ whisper/                # Speech-to-text
โ”œโ”€โ”€ piper/                  # Text-to-speech
โ””โ”€โ”€ ... (30+ more)

Purpose: The actual workloads that provide business value. These depend on infrastructure being in place.

Flux Orchestration

How Flux Knows What to Deploy

The clusters/staging/ directory contains Flux Kustomization resources that point to the other directories:

# clusters/staging/apps.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps
  namespace: flux-system
spec:
  interval: 1m0s
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./apps/staging      # Points to apps/staging/kustomization.yaml
  prune: true
  decryption:
    provider: sops
    secretRef:
      name: sops-age

The Dependency Chain

Flux deploys resources in a specific order to ensure dependencies are met:

flux-system (bootstrap)
    โ”‚
    โ–ผ
infrastructure-controllers
    โ”‚ (Cert-Manager, External Secrets, GPU Operator installed)
    โ–ผ
infrastructure-configs
    โ”‚ (Certificates created, NFS configured)
    โ–ผ
apps
    โ”‚ (Applications can now use TLS, secrets, GPUs)
    โ–ผ
monitoring-controllers โ†’ monitoring-configs
    (Observability watches everything)

This ordering is critical:

  • Apps can’t get TLS certificates until Cert-Manager is installed and configured
  • Apps can’t pull secrets until External Secrets Operator is running
  • GPU workloads can’t schedule until GPU Operator has configured the nodes

Controllers vs Configs Pattern

Within infrastructure and monitoring, you’ll notice a controllers/configs split:

Controllers

Install the actual operators/software:

# infrastructure/controllers/base/cert-manager/
- Helm release for cert-manager
- CRDs for Certificate, ClusterIssuer, etc.

Configs

Configure the installed software:

# infrastructure/configs/base/cert-manager/
- ClusterIssuer for Let's Encrypt
- Wildcard certificate for *.landryzetam.net

Why separate them?

  1. Different update cycles: Controllers update when new versions release, configs change based on business needs
  2. Dependency clarity: Configs depend on controllers being ready
  3. Troubleshooting: Easy to identify if the operator or the configuration is the problem

Deployment Flow

Adding a New Application

  1. Create base manifests:
mkdir apps/base/myapp/
# Create namespace.yaml, deployment.yaml, service.yaml, etc.
# Create kustomization.yaml listing all resources
  1. Enable in staging:
# apps/staging/kustomization.yaml
resources:
  - myapp  # Add this line
  1. Commit and push:
git add . && git commit -m "feat: add myapp" && git push
  1. Flux deploys automatically:
# Watch it happen
flux get kustomization apps
kubectl get pods -n myapp

Disabling an Application

Simply comment out the line in apps/staging/kustomization.yaml:

resources:
  # - myapp  # Disabled

With prune: true, Flux will automatically delete the resources.

Secret Management

No secrets are stored in Git. Instead:

  1. Secrets live in AWS Secrets Manager:
aws secretsmanager create-secret \
  --name "/fako/myapp/database" \
  --secret-string '{"password":"xxx"}'
  1. ExternalSecret resources reference them:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
spec:
  secretStoreRef:
    name: aws-secrets-store
  target:
    name: database-credentials
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: /fako/myapp/database
        property: password
  1. SOPS for encrypted files (when AWS isn’t suitable):
sops --encrypt --age $AGE_PUBLIC_KEY secret.yaml > secret.enc.yaml

Directory Reference

DirectoryPurposeUpdates When
apps/base/Application manifestsAdding/modifying apps
apps/staging/Which apps to deployEnabling/disabling apps
clusters/staging/Flux orchestrationRarely (bootstrap config)
infrastructure/controllers/Platform operatorsNew operator versions
infrastructure/configs/Platform configurationCertificates, mounts change
monitoring/controllers/Observability toolsPrometheus/Loki updates
monitoring/configs/Dashboards, alertsNew dashboards, alert rules

Benefits of This Structure

1. Self-Documenting

The repository structure tells you what’s deployed. apps/staging/kustomization.yaml is the definitive list of running applications.

2. Git History = Audit Trail

Every change has a commit with author, timestamp, and reason. “Who deployed X and when?” is a git log away.

3. Easy Rollback

Deployment broke something? git revert and push. Flux rolls back automatically.

4. Pull Request Workflow

Changes can be reviewed before deployment:

git checkout -b feature/add-myapp
# Make changes
git push && gh pr create
# Review, approve, merge
# Flux deploys

5. Disaster Recovery

Cluster dies completely? Bootstrap Flux on a new cluster, point it at the same repo, everything rebuilds automatically.

Common Operations

Force Reconciliation

flux reconcile source git flux-system
flux reconcile kustomization apps

Check Status

flux get all -A
flux get kustomization apps

View Logs

flux logs --follow
flux logs --kind=Kustomization --name=apps

Debug Failed Deployment

# See what Flux sees
flux get kustomization apps -o yaml

# Check events
kubectl get events -n flux-system --sort-by='.lastTimestamp'

Lessons Learned

  1. Start with this structure: Retrofitting GitOps onto an existing cluster is painful. Start clean.

  2. Resist the temptation to kubectl apply: Every manual change creates drift. Always go through Git.

  3. Keep base manifests simple: Don’t over-engineer. Start with deployment, service, ingress. Add complexity only when needed.

  4. Use meaningful commit messages: “fix: stuff” doesn’t help when debugging at 2 AM.

  5. Test in a branch first: For risky changes, create a branch and temporarily point Flux at it.



The beauty of GitOps is that your infrastructure becomes as reviewable, testable, and reversible as your application code. The repository structure described here has evolved over months of running production workloads, and represents a balance between simplicity and capability.