MET : GitOps & CI/CD (6/6)

| Digital

What if every Git change automatically triggered the right deployment, with instant rollback and zero manual intervention? That’s exactly what we’ve achieved with GitLab CI/CD connected directly to our Kubernetes cluster.

We’ve reached the final article in this series. We’ve seen how Terraform creates infrastructure, Ansible configures it, Kubernetes orchestrates applications, and Helm packages them. The final piece: automated deployment and full application lifecycle management.

The problem: manual deployment

Even with Helm, deployment remains a manual action:

  1. Connect to the cluster
  2. Run helm upgrade
  3. Check that it works
  4. Hope you didn’t forget something

The problems:

  • No traceability: Who deployed what, when?
  • Risk of error: Wrong namespace, wrong version…
  • No easy rollback: “What was the config before?”
  • Complex operations not automated: volume resizing, migrations…

The GitOps philosophy

GitOps is an approach where Git becomes the single source of truth for infrastructure and applications:

“The desired state is in Git. Every change goes through a commit. Deployment is triggered from the pipeline.”

  • All changes go through a Git commit
  • Git history = complete deployment history
  • Rollback = return to a previous commit
  • Zero manual connection to the cluster in production
GitOps flow: Git commit → GitLab CI/CD pipeline → Kubernetes

GitLab CI/CD: our deployment engine

We use GitLab CI/CD connected directly to the Kubernetes cluster via the GitLab Agent. Each client application has its own Git repository with an integrated pipeline. The agent runs inside the cluster, allowing pipelines to interact with Kubernetes securely without exposing the API server externally.

Every project follows the same two-stage structure:

stages:
  - deploy   # Helm deployment (manual trigger)
  - resize   # Automatic volume resizing

Deploy stage: deploying with Helm

The deploy stage runs helm upgrade --install using our versioned wordpress-stack chart from the shared templates-helm repository.

deploy:production:
  stage: deploy
  before_script:
    - kubectl config use-context ${KUBE_CONTEXT}
    - git clone --depth 1 --branch wordpress-stack-v3.10.9
        https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.cluster.../templates-helm.git
    - cp -r /tmp/templates-helm/charts/wordpress-stack ./wordpress-stack
  script:
    - |
      helm upgrade --install ${RELEASE_NAME} ./wordpress-stack \
        --namespace ${NAMESPACE} \
        --values values-prod.yaml \
        --set credentials.mariadbPassword=${MARIADB_PASSWORD} \
        --set credentials.wordpressAdminPassword=${WP_ADMIN_PASSWORD} \
        --wait --timeout 10m
  only:
    - prod
  when: manual

Credentials are never in the code — stored in GitLab CI/CD variables, encrypted and injected only at execution time.

Resize stage: automatic volume management

Kubernetes doesn’t allow reducing a volume’s size live. Our automatic resize pipeline detects any size change in the configuration file and runs the full procedure:

  1. Create a temporary PVC with the current size
  2. Copy data via a Kubernetes Job
  3. Delete the old PVC (removing finalizers if necessary)
  4. Create the new PVC with the desired size
  5. Copy data back from the temporary PVC
  6. Delete the temporary PVC
resize:production:
  stage: resize
  script:
    - |
      LITESPEED_SIZE=$(awk '/^litespeed:/{f=1} f && /storage:/{g=1}
        f && g && /size:/{print $2; exit}' values-prod.yaml)
      MARIADB_SIZE=$(awk '/^mariadb:/{f=1} f && /storage:/{g=1}
        f && g && /size:/{print $2; exit}' values-prod.yaml)

      export PVC_NAME=vitrine-wordpress-stack-litespeed-data
      export NEW_SIZE=${LITESPEED_SIZE}
      sh resize-volume.sh

      kubectl scale statefulset vitrine-wordpress-stack-mariadb --replicas=0
      export PVC_NAME=data-vitrine-wordpress-stack-mariadb-0
      export NEW_SIZE=${MARIADB_SIZE}
      sh resize-volume.sh
      kubectl scale statefulset vitrine-wordpress-stack-mariadb --replicas=1
  rules:
    - if: '$CI_COMMIT_BRANCH == "prod"'
      when: always

The script compares the current PVC size with the desired size. If identical, it does nothing. The resize only triggers when the size has actually changed.

Secrets: never in Git

All credentials are stored in GitLab CI/CD variables, encrypted and injected only at pipeline execution time — never visible in logs, never committed to the repository.

Rollback: revert in seconds

Option 1: Via Git (recommended)

git revert HEAD
git push origin prod
# → Pipeline restarts with the old config

Option 2: Via Helm

helm history <release> -n <namespace>
helm rollback <release> -n <namespace>

Our best practices

📁 One Git repo per client

Each client has its own repository with values-prod.yaml and .gitlab-ci.yml. The shared chart is versioned in templates-helm and cloned at each pipeline run.

🔐 Credentials in CI/CD variables

  • No secrets in committed files
  • “Masked” variables: never visible in logs
  • “Protected” variables: accessible only on protected branches

🔄 Protected branches

  • The prod branch is protected: direct push forbidden
  • All changes go through a merge request
  • Deployment stays manual (when: manual) to validate before pushing to production

📦 Versioned chart

  • Chart cloned from a specific Git tag (wordpress-stack-v3.10.9)
  • Controlled upgrades: change the tag in CI to update
  • No :latest — every deployment is reproducible

The final result

With our complete stack (Terraform + Ansible + Kubernetes + Helm + GitLab CI/CD), we’ve achieved our initial goal:

Goal Achieved
Reproducible infrastructure ✅ Terraform + Ansible = recreation in 1h
Controlled deployment ✅ Push to prod → manual pipeline trigger
High availability ✅ Kubernetes + Longhorn replication
Complete traceability ✅ Git history = deployment history
Instant rollback ✅ git revert or helm rollback
Automated volume management ✅ Automatic resize without manual intervention
Cost savings ✅ 20+ apps on 2 servers, optimized storage

Series conclusion

Throughout these 6 articles, we’ve covered our entire modern infrastructure stack:

  1. Vision: Why build your own infrastructure
  2. Terraform: Provision infrastructure as code
  3. Ansible: Automate configuration
  4. Kubernetes & Rancher: Orchestrate applications
  5. Helm: Package and deploy easily
  6. GitOps & GitLab CI/CD: Automate deployments and lifecycle management

Most importantly: everything is in Git. Each client’s configuration, the deployed chart version, volume sizes — all versioned, auditable, and reproducible.

🚀 Ready to modernize your infrastructure?

Whether you want to:

  • Migrate to Kubernetes
  • Implement GitOps with GitLab CI/CD
  • Automate your deployments and volume management
  • Train your team

We can guide you through every step. Our production experience helps you avoid common pitfalls and accelerate your transition.

Let’s discuss your project →