Logo
Overview
Un Talos européen de qualité - Part IX - Flux

Un Talos européen de qualité - Part IX - Flux

October 31, 2025
14 min read
part-09

Objectif 🎯

L’essentiel de l’infrastructure étant enfin finalisée, il ne reste plus qu’à mettre en place le déploiement continu (CD) avec Flux.

Pour rappel, Flux est un outil GitOps qui permet de synchroniser l’état d’un cluster Kubernetes avec un dépôt Git. Ainsi, toute modification dans le dépôt Git sera automatiquement appliquée au cluster Kubernetes.

Installation Flux

Créer votre repo git vide flux source sur GitHub ou tout autre VCS, l’URL ici sera https://github.com/ohmytalos/flux-source.git.

clusters/dev-kube/module-delivery.tf
module "kube_delivery" {
source = "../../modules/kube/delivery"
flux_git_repository = "https://github.com/ohmytalos/flux-source.git"
flux_git_password = var.flux_git_password
flux_bootstrap_git_path = "clusters/dev"
flux_sops_age_secret_key = var.flux_sops_age_secret_key
}
Explanation

Cette configuration du module de delivery Kubernetes initialise Flux avec le dépôt Git spécifié. Le chemin clusters/dev indique où se trouvent les configurations spécifiques à ce cluster de développement.

Enfin, GitOps oblige, nous utiliserons sops avec une clé age pour chiffrer les secrets dans notre dépôt Git via la clé publique. Stocker la clé privée dans Bitwarden pour une utilisation par Flux au sein du cluster pour le déchiffrement.

Plus qu’à déployer avec terraform apply pour lancer le bootstrap de Flux dans le cluster Kubernetes. Une fois le processus terminé, Flux commencera à synchroniser l’état du cluster avec le dépôt Git. Lancer kgp -n flux-system pour vérifier l’état des 6 composants flux :

NAME READY STATUS RESTARTS AGE
helm-controller-58854b78db-x7jc9 1/1 Running 0 19s
image-automation-controller-854587f458-zx5sz 1/1 Running 0 19s
image-reflector-controller-745b9fffd6-6wjsb 1/1 Running 0 19s
kustomize-controller-cc4f4cbbf-tznk5 1/1 Running 0 19s
notification-controller-c58679c45-jfd8l 1/1 Running 0 19s
source-controller-86968b9cb8-b9fz7 1/1 Running 0 18s
NomTâche
source-controllerSynchronise les sources Git et défini l’état courant de réconciliation.
kustomize-controllerApplique les configurations Kustomize fournies par source.
helm-controllerDéploie les releases Helm fournies par source.
image-reflector-controllerSurveille les registres d’images pour les nouvelles versions.
image-automation-controllerAutomatise les mises à jour des images dans les manifests.
notification-controllerGère les notifications basées sur les événements dans Flux.

Nous en avons enfin fini pour l’installation de l’infrastruture Kubernetes côté Terraform. La suite concernera la partie applicative géré par Flux, la 3ème brique de ce guide.

Monitoring Flux

La 1ère chose utile serait d’installer de quoi monitorer notre instance de flux. Pour cela, nous allons nous appuyer sur le repo flux-monitoring-example qui contient déjà tout ce qu’on veut.

clusters/dev/monitoring/flux-monitoring.yaml
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: flux-monitoring
namespace: flux-system
spec:
interval: 30m0s
ref:
branch: main
url: https://github.com/fluxcd/flux2-monitoring-example
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: monitoring-config
namespace: flux-system
spec:
interval: 1h0m0s
path: ./monitoring/configs
prune: true
sourceRef:
kind: GitRepository
name: flux-monitoring

Commiter ce fichier et vous devriez observer le déploiement automatique de :

  • Un PodMonitor, permettant de scrapper les métriques de flux via Prometheus, disponible via k get pmon -n monitoring -l app.kubernetes.io/part-of=flux.
  • Des dashboards Grafana, contenant des dashboards préconfigurés pour visualiser les métriques de flux, disponibles via k get cm -n monitoring -l app.kubernetes.io/part-of=flux.

Dashboard Flux

Tip

Pour éviter d’attendre une minute et forcer la synchronisation de l’état avec votre dépôt Git, vous pouvez utiliser la commande flux reconcile kustomization flux-system --with-source.

Cas pratique avec n8n

Pour illustrer l’utilisation de Flux pour gérer les applications dans notre cluster Kubernetes, nous allons déployer n8n, l’outil de prédilection d’automatisation de flux de travail.

Storage n8n

Commencer par créer une base de données PostgreSQL pour n8n, directement sur pgAdmin, avec un utilisateur n8n (n’oubliez pas de mettre le privilège Can login?) et de l’associer en Owner sur une même base de données n8n.

Nous voulons aussi lui dédié un volume Longhorn pour la persistance des données. Créer un volume n8n-data de 8Gi via l’interface Longhorn.

Longhorn create volume n8n

Ensuite, créer un PersistentVolume (et uniquement un PV, pas de PVC) pour ce volume Longhorn. Laissez les paramètres par défaut. Il devrait prendre le StorageClass longhorn-crypto par défaut (précisé lors de l’installation de longhorn).

Longhorn create pvc n8n

Enfin, n’oubliez pas de rajouter le groupe backup à ce volume pour qu’il soit inclus dans les backups réguliers.

Longhorn recurring jobs schedule

Tip

L’intérêt principal de passer par l’interface Longhorn pour créer le volume est de faciliter la restauration en s’assurant que le nom du volume et du backup restent consistants.

Secrets n8n

Nous allons maintenant créer les secrets nécessaires pour n8n (clé encryption n8n, mdp postgres, accès smtp), en utilisant SOPS pour chiffrer les informations sensibles. Créer les fichiers suivants dans clusters/dev/tools/ :

clusters/dev/tools/secret-n8n-encryption-key.yaml
apiVersion: v1
kind: Secret
metadata:
name: encryption-key-secret
namespace: n8n
type: Opaque
data:
N8N_ENCRYPTION_KEY: <openssl rand -hex 16 | base64>
clusters/dev/tools/secret-n8n-database.yaml
apiVersion: v1
kind: Secret
metadata:
name: database-secret
namespace: n8n
type: Opaque
data:
postgres-password: <votre-mot-de-passe-postgres | base64>
clusters/dev/tools/secret-n8n-smtp.yaml
apiVersion: v1
kind: Secret
metadata:
name: smtp-secret
namespace: n8n
type: Opaque
data:
N8N_SMTP_USER: <guid | base64>
N8N_SMTP_PASS: <guid | base64>

Pour faciliter le chiffrement des fichiers secrets avec SOPS, créer un fichier .sops.yaml à la racine du dépôt Git :

.sops.yaml
creation_rules:
- path_regex: clusters/dev/.*/secret-.*.yaml
encrypted_regex: ^(data|stringData)$
age: <votre clé publique age créée lors de l'installation de flux>

Plus qu’à chiffrer les fichiers secrets avec la commande sops.

Terminal window
sops -e -i clusters/dev/tools/secret-n8n-database.yaml
sops -e -i clusters/dev/tools/secret-n8n-encryption-key.yaml
sops -e -i clusters/dev/tools/secret-n8n-smtp.yaml

Sur vscode, l’extension https://github.com/kabolt/vscode-sops marche très bien.

Créer le namespace n8n puis ajouter vos secrets dans le fichier kustomization.yaml pour qu’ils soient appliqués par flux.

clusters/dev/tools/deployment-n8n.yaml
apiVersion: v1
kind: Namespace
metadata:
name: n8n
clusters/dev/tools/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment-n8n.yaml
- secret-n8n-database.yaml
- secret-n8n-smtp.yaml
- secret-n8n-encryption-key.yaml

Puis commiter le tout et lancer une synchronisation avec la commande flux reconcile kustomization flux-system --with-source.

Vous devriez voir vos secrets déchiffrés via kgsec -n n8n.

Déploiement n8n

Plus qu’à déployer n8n via le fichier deploy-n8n.yaml.

clusters/dev/tools/deployment-n8n.yaml
# ...
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: n8n
namespace: flux-system
spec:
interval: 1h0m0s
url: https://community-charts.github.io/helm-charts
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: n8n
namespace: flux-system
spec:
chart:
spec:
chart: n8n
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: n8n
version: ">=1.15.16"
interval: 1m
releaseName: n8n
targetNamespace: n8n
maxHistory: 2
values:
strategy:
type: Recreate
main:
persistence:
enabled: true
existingClaim: n8n-data
volumeName: n8n-data
extraEnvVars:
N8N_EMAIL_MODE: smtp
N8N_SMTP_HOST: smtp.tem.scw.cloud
N8N_SMTP_PORT: "465"
N8N_SMTP_SENDER: n8n@ohmytalos.io
extraSecretNamesForEnvFrom:
- smtp-secret
existingEncryptionKeySecret: encryption-key-secret
db:
type: postgresdb
externalPostgresql:
host: ohmytalos-dev-rw.postgres
username: n8n
database: n8n
existingSecret: database-secret
serviceMonitor:
enabled: true
webhook:
url: https://n8n.ohmytalos.io/
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: n8n-data
namespace: n8n
spec:
resources:
requests:
storage: 8Gi
volumeName: n8n-data
storageClassName: longhorn-crypto
accessModes:
- ReadWriteOnce
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: n8n
namespace: n8n
spec:
routes:
- match: Host(`n8n.ohmytalos.io`)
kind: Rule
services:
- name: n8n
port: http
Explanation

On crée d’abord un HelmRepository pour référencer le dépôt Helm officiel de n8n, puis un HelmRelease pour déployer n8n dans le namespace n8n, en utilisant les secrets précédemment créés pour la configuration.

La configuration de la base de données PostgreSQL externe est spécifiée via externalPostgresql, en utilisant le secret database-secret pour les informations d’authentification.

Enfin, on crée un PersistentVolumeClaim pour le volume Longhorn n8n-data, et une IngressRoute Traefik pour exposer n8n publiquement via le domaine n8n.ohmytalos.io. Si uniquement à vocation interne, utiliser n8n.dev.ohmytalos.io.

Et voilà, une fois le commit poussé et la synchronisation effectuée, n8n devrait être opérationnel dans votre cluster Kubernetes.

n8n dashboard

Outils DevOps

Déploiement Gitea

Allons plus loin avec l’installation de notre propre solution self-hosted VCS + CI légère, Gitea. Le processus est en tout point similaire à celui de n8n.

  1. Création de la base de données PostgreSQL gitea avec un utilisateur gitea sur pgAdmin.
  2. Création du volume Longhorn gitea-data chiffré de 10Gi + configuration backup, mais avec le tag volume pour stocker les données dans les disques attachés aux nœuds storage.
  3. Création du PersistentVolume gitea-data chiffré dans Kubernetes.
  4. Création des secrets SOPS pour Gitea (mot de passe postgres, clé secrète Gitea).
  5. Déploiement de Gitea via HelmRelease Flux.
clusters/dev/build/secret-gitea-admin.yaml
apiVersion: v1
kind: Secret
metadata:
name: gitea-admin-secret
namespace: gitea
type: Opaque
data:
password: <password-admin-gitea | base64>
username: <admin-gitea | base64>
clusters/dev/build/secret-gitea-app-ini.yaml
apiVersion: v1
kind: Secret
metadata:
name: gitea-app-ini-secret
namespace: gitea
type: Opaque
data:
cache: HOST=redis://:<redis-password>@dragonfly.dragonfly:6379/0
database: PASSWD=<postgres-password>
mailer: PASSWD=<smtp-password>
USER=<smtp-username>
queue: CONN_STR=redis://:<redis-password>@dragonfly.dragonfly:6379/0
session: PROVIDER_CONFIG=redis://:<redis-password>@dragonfly.dragonfly:6379/0
storage: MINIO_ACCESS_KEY_ID=<access-key>
MINIO_SECRET_ACCESS_KEY=<secret-access-key>
Important

La construction de la config Gitea app.ini est un peu particulière, référez-vous à la documentation officielle pour plus de détails sur les différentes options de configuration. Le secret gitea-app-ini-secret ne concerne que les valeurs sensibles nécessaires pour configurer Gitea, telles que les informations de connexion à la base de données, les paramètres SMTP pour l’envoi d’e-mails, et la configuration du cache et de la session avec Redis, le stockage S3 externe.

N’oubliez pas de remplacer les valeurs entre chevrons par les valeurs réelles de vos secrets puis réencoder le tout en base64.

Nous allons rajouter également en frontal de Gitea Anubis pour s’éviter les ennuis liés au scraping IA sur ce genre d’instance.

clusters/dev/build/secret-gitea-admin.yaml
apiVersion: v1
kind: Secret
metadata:
name: gitea-anubis-key
namespace: gitea
type: Opaque
data:
ED25519_PRIVATE_KEY_HEX: <openssl rand -hex 32 | base64>

On chiffre tout ça :

Terminal window
sops -e -i clusters/dev/build/secret-gitea-admin.yaml
sops -e -i clusters/dev/build/secret-gitea-app-ini.yaml
sops -e -i clusters/dev/build/secret-gitea-anubis-key.yaml

Le point d’entrée pour flux :

clusters/dev/build/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment-gitea.yaml
- secret-gitea-admin.yaml
- secret-gitea-app-ini.yaml
- secret-gitea-anubis-key.yaml

Le déploiement Gitea complet :

clusters/dev/build/deployment-gitea.yaml
apiVersion: v1
kind: Namespace
metadata:
name: gitea
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: gitea
namespace: flux-system
spec:
interval: 1h0m0s
url: https://dl.gitea.io/charts
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: gitea
namespace: flux-system
spec:
chart:
spec:
chart: gitea
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: gitea
version: ">=12.4.0"
interval: 1m
releaseName: gitea
targetNamespace: gitea
maxHistory: 2
values:
image:
tag: "1.25.3"
gitea:
admin:
existingSecret: gitea-admin-secret
email: admin@ohmytalos.io
metrics:
enabled: true
serviceMonitor:
enabled: true
additionalConfigSources:
- secret:
secretName: gitea-app-ini-secret
config:
server:
DOMAIN: gitea.ohmytalos.io
SSH_DOMAIN: ssh.ohmytalos.io
ROOT_URL: https://gitea.ohmytalos.io
database:
DB_TYPE: postgres
HOST: ohmytalos-dev-rw.postgres
NAME: gitea
USER: gitea
storage:
STORAGE_TYPE: local
MINIO_ENDPOINT: s3.gra.io.cloud.ovh.net
MINIO_LOCATION: gra
MINIO_BUCKET: ohmytalos-dev
MINIO_USE_SSL: true
storage.attachments:
STORAGE_TYPE: local
MINIO_BASE_PATH: attachments/
storage.actions_log:
STORAGE_TYPE: local
MINIO_BASE_PATH: actions_log/
storage.packages:
STORAGE_TYPE: minio
MINIO_BASE_PATH: gitea/packages/
indexer:
REPO_INDEXER_ENABLED: true
mailer:
ENABLED: true
FROM: gitea@ohmytalos.io
SMTP_ADDR: smtp.tem.scw.cloud
SMTP_PORT: "587"
cache:
ADAPTER: redis
session:
PROVIDER: redis
queue:
TYPE: redis
service:
DISABLE_REGISTRATION: true
repository:
DEFAULT_BRANCH: main
metrics:
ENABLED_ISSUE_BY_REPOSITORY: true
ENABLED_ISSUE_BY_LABEL: true
webhook:
ALLOWED_HOST_LIST: "*"
postgresql-ha:
enabled: false
valkey-cluster:
enabled: false
persistence:
create: false
claimName: gitea-data
strategy:
type: Recreate
resources:
requests:
cpu: 100m
memory: 1Gi
limits:
cpu: 2000m
memory: 1Gi
tolerations:
- key: node-role.kubernetes.io/storage
operator: Exists
nodeSelector:
node.kubernetes.io/role: storage
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-data
namespace: gitea
spec:
resources:
requests:
storage: 10Gi
volumeName: gitea-data
storageClassName: longhorn-crypto
accessModes:
- ReadWriteOnce
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: anubis
namespace: gitea
spec:
selector:
matchLabels:
app: anubis
template:
metadata:
labels:
app: anubis
spec:
containers:
- name: anubis
image: ghcr.io/techarohq/anubis:latest
env:
- name: BIND
value: :8080
- name: DIFFICULTY
value: "4"
- name: ED25519_PRIVATE_KEY_HEX
valueFrom:
secretKeyRef:
name: gitea-anubis-key
key: ED25519_PRIVATE_KEY_HEX
- name: METRICS_BIND
value: :9090
- name: SERVE_ROBOTS_TXT
value: "true"
- name: TARGET
value: http://gitea-http.gitea.svc:3000
- name: OG_PASSTHROUGH
value: "true"
- name: OG_EXPIRY_TIME
value: 24h
resources:
limits:
cpu: 750m
memory: 256Mi
requests:
cpu: 250m
memory: 256Mi
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
---
apiVersion: v1
kind: Service
metadata:
name: anubis
namespace: gitea
spec:
selector:
app: anubis
ports:
- name: http
port: 8080
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: gitea-ssh
namespace: gitea
spec:
entryPoints:
- ssh
routes:
- match: HostSNI(`*`)
services:
- name: gitea-ssh
port: ssh
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-http
namespace: gitea
spec:
routes:
- match: Host(`gitea.ohmytalos.io`) && PathRegexp(`^/(api|v2)`)
kind: Rule
services:
- name: gitea-http
port: http
- match: Host(`gitea.ohmytalos.io`) && !PathRegexp(`^/(api|v2)`)
kind: Rule
services:
- name: anubis
port: http
Explanation

Ça commence à devenir assez velu, mais le principe reste le même que pour n8n. On crée un namespace gitea, puis on déploie Gitea via un HelmRelease, en utilisant les secrets SOPS.

On s’assure de configurer l’accès au postgresql, au SMTP et au S3 OVH externe (n’oublier de configurer des accès gitea dédiés). Le stockage S3 n’est activé que sur la partie packages, le plus volumineux (artifacts, registry docker), le reste notamment les repos git restant sur le volume local longhorn.

Nous déployons Gitea sur les nœuds storage, nécessaire pour se brancher au volume Longhorn créé précédemment sur les disques externes via le tag volume.

Côté Ingress, on rajoute l’entrée ssh pour le git over ssh, et on place Anubis en frontal HTTP sauf pour les routes API et docker registry pour éviter de se faire bloquer côté client docker cli ou runner.

Commiter et espérer. Déboguer avec kgp -n gitea en cas de problème et vérifier les logs des pods Gitea et Anubis.

Et voilà, c’est magnifique, tester immédiatement la connexion admin puis la section dashboard.

Gitea Home

Gitea Dashboard

Déploiement SonarQube

Allons encore plus loin avec l’installation de SonarQube pour l’analyse de la qualité du code. Le processus reste similaire à celui de n8n et Gitea, en plus simple sans nécessité de persistance.

clusters/dev/build/secret-sonarqube.yaml
apiVersion: v1
kind: Secret
metadata:
name: sonarqube-secret
namespace: sonarqube
type: Opaque
data:
db-password: <mot-de-passe-postgres | base64>
monitoring-passcode: <mot-de-passe-monitoring | base64>
clusters/dev/build/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- #...
- deployment-sonarqube.yaml
- secret-sonarqube.yaml
clusters/dev/build/deployment-sonarqube.yaml
apiVersion: v1
kind: Namespace
metadata:
name: sonarqube
labels:
pod-security.kubernetes.io/enforce: "privileged"
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: sonarqube
namespace: flux-system
spec:
interval: 1h0m0s
url: https://SonarSource.github.io/helm-chart-sonarqube
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: sonarqube
namespace: flux-system
spec:
chart:
spec:
chart: sonarqube
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: sonarqube
version: ">=2025.6.0"
interval: 1m
releaseName: sonarqube
targetNamespace: sonarqube
maxHistory: 2
values:
community:
enabled: true
buildNumber: "26.1.0.118079"
monitoringPasscode: null
monitoringPasscodeSecretName: sonarqube-secret
monitoringPasscodeSecretKey: monitoring-passcode
resources:
requests:
cpu: 100m
memory: 3Gi
limits:
cpu: 1000m
memory: 3Gi
prometheusMonitoring:
podMonitor:
enabled: true
namespace: sonarqube
jdbcOverwrite:
enable: true
jdbcUrl: jdbc:postgresql://ohmytalos-dev-rw.postgres/sonarqube
jdbcUsername: sonarqube
jdbcSecretName: sonarqube-secret
jdbcSecretPasswordKey: db-password
postgresql:
enabled: false
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: sonarqube
namespace: sonarqube
spec:
routes:
- match: Host(`sonarqube.ohmytalos.io`)
kind: Rule
services:
- name: sonarqube-sonarqube
port: http

Pour le coup bien plus simple, rien de nouveau par rapport à ce qu’on a déjà vu. Commité et admirer (attention les yeux le dark mode est uniquement présent sur la version payante, les filous).

SonarQube Home

Conclusion

Voilà, nous avons mis en place un système de déploiement continu (CD) avec Flux dans notre cluster Kubernetes. Nous avons également déployé plusieurs applications pratiques telles que n8n, Gitea et SonarQube, avec support des mises à jour automatique des charts Helm.

Il est temps de tester notre infrastructure sur toute la chaîne, sur un cas concret d’application type, du développement jusqu’à la production, suite dans la prochaine section.