GitLab CI for an infra repo: lint, validate, and flux reconcile
Published: 2026-04-03
The .gitlab-ci.yml for the infra repository has one job for each thing that can go wrong: YAML syntax errors, Kustomize render failures, Terraform plan errors, and drift between git and the running cluster. Here's how the pipeline is structured.
Pipeline stages
validate:yaml ← yamllint on fluxcd/ and ansible/
validate:kustomize ← kubectl kustomize for every env
validate:terraform ← terraform validate for every env
validate:security ← Trivy SARIF + SonarQube (manual trigger)
reconcile:flux ← flux reconcile source git (auto, default branch only)
reconcile:envs ← per-env Kustomization reconcile (manual)
notify ← Telegram + Mattermost webhook
YAML lint
yamlvalidate:yaml:
stage: validate:yaml
image: registry.example.com/pipelinecomponents/yamllint:latest
script:
- yamllint -c .yamllint.yaml fluxcd/ ansible/
.yamllint.yaml:
yamlextends: default
rules:
line-length:
max: 200 # SealedSecret base64 values need room
truthy:
allowed-values: ['true', 'false'] # prevent yes/no booleans
comments:
min-spaces-from-content: 1
document-start: disable
Fails fast before any Kubernetes validation.
Kustomize validation
One job per environment:
yamlkustomize-validate-dev:
stage: validate:kustomize
image: registry.example.com/bitnami/kubectl:latest
script:
- kubectl kustomize fluxcd/projects/dev/kustomization/hub/
- kubectl kustomize fluxcd/projects/dev/kustomization/spoke/
- kubectl kustomize fluxcd/projects/dev/kustomization/routes/
- kubectl kustomize fluxcd/projects/dev/kustomization/spoke/monitoring/
This catches:
- Missing
kustomization.yamlfiles - Bad
resources:references - Patch selector mismatches
- Invalid YAML in HelmRelease values
Terraform validate
yamlterraform-validate-dev:
stage: validate:terraform
image: registry.example.com/hashicorp/terraform:1.x
script:
- cd terraform/dev
- terraform init -backend=false
- terraform validate
-backend=false skips state backend connection. Validation only checks syntax and provider schema.
Flux reconcile (auto on main)
yamlflux-sync:
stage: reconcile:flux
image: registry.example.com/fluxcd/flux-cli:v2.8.3
interruptible: false
script:
- flux reconcile source git flux-system -n flux-system --context=infra-k8s
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Runs only on pushes to main. The KUBECONFIG points to .kubeconfigs/infra.yaml committed to the repo (hub cluster endpoint only, cluster is VPN-only network).
Per-env manual reconcile
yamlreconcile-dev:
stage: reconcile:envs
when: manual
image: registry.example.com/fluxcd/flux-cli:v2.8.3
script:
- flux reconcile ks dev-apps -n dev --context=infra-k8s --timeout=5m
- flux reconcile ks dev-spoke -n dev --context=infra-k8s --timeout=5m
- flux reconcile ks dev-monitoring-spoke -n dev --context=infra-k8s --timeout=5m
- flux reconcile ks dev-routes -n dev --context=infra-k8s --timeout=5m
Useful after a large batch of changes when you don't want to wait for the 1-minute Flux polling interval.
Workflow rules
yamlworkflow:
auto_cancel:
on_new_commit: interruptible
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- when: never
auto_cancel: on_new_commit: interruptible — lint and kustomize-validate jobs are cancelled and restarted on new commit. flux-sync is not interruptible.
Notification jobs
yamlnotify:success:
stage: notify
when: on_success
variables:
STATUS_ICON: "✅"
STATUS_LABEL: SUCCESS
notify:failure:
stage: notify
when: on_failure
variables:
STATUS_ICON: "❌"
STATUS_LABEL: FAILED
Both extend .notify which sends to Telegram and Mattermost. The message includes pipeline ID, branch, commit SHA, author, and first line of the commit message.
Telegram goes through an HTTP proxy because the CI runner is on-prem and can't reach api.telegram.org directly. The script tries the proxy first, then falls back to direct.
Runner setup
All jobs use the office-dind tag — an on-prem GitLab Runner with Docker-in-Docker. Images pulled from registry.example.com (internal Artifactory), not Docker Hub.
GIT_DEPTH: "1" on most jobs. SonarQube overrides with GIT_DEPTH: "0" for git blame analysis.
Full job dependency graph
yamllint
└─> kustomize-validate-*
└─> flux-sync (auto)
└─> reconcile-* (manual)
└─> notify
If yamllint fails, Kustomize validation is skipped. This prevents noisy failures from cascading.