Configuration
Une image, plusieurs environnements — et ce que « Secret » ne veut pas dire.
ConfigMaps et Secrets
Une image de conteneur doit pouvoir être déployée en développement, en préproduction et en production sans être reconstruite. C’est le principe qui justifie toute cette leçon, et il n’est pas théorique : si l’URL de la base de données est inscrite dans l’image, alors l’artefact que vous testez n’est pas celui que vous livrez, et le test ne prouve plus rien.
L’autre moitié du problème est plus désagréable. Un mot de passe placé dans une image y reste définitivement : les couches sont immuables, l’effacer dans une couche ultérieure ne le retire pas de l’historique, et toute personne ayant accès au registre peut le lire. Un secret livré dans une image n’est pas un secret, c’est une fuite avec un délai.
Kubernetes sépare donc deux objets pour deux usages : la ConfigMap porte la configuration non sensible, le Secret porte les données confidentielles. Les deux s’injectent dans les conteneurs de la même façon — variables d’environnement ou fichiers montés — mais leur traitement par le cluster diffère.
Un avertissement à placer immédiatement, avant toute syntaxe, parce que c’est le malentendu le plus répandu de l’écosystème : un Secret Kubernetes n’est pas chiffré par défaut. Il est encodé en base64, ce qui est un format de transport, pas une protection. Nous verrons exactement ce que cela implique, et ce qu’il faut ajouter pour que le mot « secret » soit mérité.
La ConfigMap
Une ConfigMap est un dictionnaire de paires clé-valeur, ou un ensemble de fichiers, stocké dans le cluster.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: atelier
data:
# Valeurs simples : une clé, une chaîne
APP_ENV: "production"
LOG_LEVEL: "info"
API_TIMEOUT: "30"
FEATURE_NOUVEAU_PANIER: "true"
# Fichier entier : la clé devient le nom du fichier si on monte en volume
application.properties: |
server.port=8080
spring.jpa.show-sql=false
logging.level.root=INFO
nginx.conf: |
server {
listen 80;
location /api/ {
proxy_pass http://api:8080/;
}
}
Deux détails de syntaxe qui coûtent du temps quand on les ignore. D’abord, toutes les valeurs de data sont des chaînes : API_TIMEOUT: 30 sans guillemets est refusé par le schéma, et true sans guillemets est interprété comme un booléen YAML. Prenez l’habitude de guillemeter systématiquement. Ensuite, le | du YAML conserve les retours à la ligne, ce qui permet d’embarquer un fichier de configuration tel quel.
Les variantes impératives sont pratiques pour de l’exploration :
# Depuis des valeurs littérales
kubectl create configmap app-config \
--from-literal=APP_ENV=production \
--from-literal=LOG_LEVEL=info
# Depuis un fichier : la clé est le nom du fichier
kubectl create configmap nginx-config --from-file=nginx.conf
# Depuis un répertoire entier : une clé par fichier
kubectl create configmap tous-configs --from-file=./config/
# Depuis un fichier .env : une clé par ligne
kubectl create configmap app-config --from-env-file=.env
kubectl get configmaps
kubectl describe configmap app-config # affiche les valeurs en clair
kubectl get configmap app-config -o yaml
Le Secret
Structurellement identique, avec un champ de plus et un encodage attendu.
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: atelier
type: Opaque
stringData:
# stringData : vous écrivez en clair, Kubernetes encode à l'enregistrement
DB_USER: "app_user"
DB_PASSWORD: "S3cr3t-a-changer"
DATABASE_URL: "postgresql://app_user:S3cr3t-a-changer@postgres:5432/appdb"
stringData est presque toujours le bon choix pour un manifeste écrit à la main : il évite d’encoder soi-même. Le champ data, lui, attend des valeurs déjà en base64 :
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
DB_USER: YXBwX3VzZXI= # echo -n 'app_user' | base64
DB_PASSWORD: UzNjcjN0LWEtY2hhbmdlcg==
# Création impérative : l'encodage est fait pour vous
kubectl create secret generic db-credentials \
--from-literal=DB_USER=app_user \
--from-literal=DB_PASSWORD='S3cr3t-a-changer'
# Depuis un fichier (certificat, clé privée, fichier de credentials)
kubectl create secret generic tls-keys --from-file=./server.key
# Secret dédié aux registres privés, pour imagePullSecrets
kubectl create secret docker-registry registre-prive \
--docker-server=registry.example.com \
--docker-username=deploiement \
--docker-password='jeton-du-registre'
Les types de Secret les plus utilisés :
type | Usage |
|---|---|
Opaque | données arbitraires : le cas général |
kubernetes.io/dockerconfigjson | authentification à un registre d’images privé |
kubernetes.io/tls | certificat et clé privée, consommés par les Ingress |
kubernetes.io/service-account-token | jeton d’un compte de service |
Ce que « Secret » protège réellement
Voici la démonstration à faire une fois pour ne plus jamais l’oublier :
# Le contenu « protégé »
kubectl get secret db-credentials -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
# S3cr3t-a-changer
Aucun mot de passe, aucune clé, aucun déchiffrement. Le base64 n’a jamais été un mécanisme de sécurité — il sert à transporter des octets binaires dans un champ texte. Ce que cela implique concrètement :
- Toute personne autorisée à lire les Secrets d’un namespace lit tous les mots de passe de ce namespace en clair.
- Un Secret validé dans Git est un secret compromis, même encodé en base64.
- Par défaut, les Secrets sont stockés en clair dans etcd. Une sauvegarde d’etcd mal protégée expose tout.
Ce qui rend un Secret réellement confidentiel, ce sont trois mesures ajoutées :
- Le RBAC. C’est la seule protection qui fonctionne par défaut : restreindre qui peut lire les Secrets. Un Secret n’est confidentiel que si les droits de lecture le sont.
- Le chiffrement au repos. Il s’active côté serveur d’API par une
EncryptionConfiguration, avec une clé locale ou un service de gestion de clés (KMS). Ce n’est pas activé d’office sur un cluster autogéré ; les offres managées le proposent souvent, à vérifier au cas par cas. - Un gestionnaire externe. HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, avec un opérateur qui synchronise vers le cluster. La rotation devient possible et l’audit existe, ce que Kubernetes seul ne fournit pas.
Par rapport à une variable d’environnement écrite dans un manifeste de Deployment, le Secret apporte tout de même deux gains réels : la valeur n’apparaît pas dans kubectl describe deployment, et elle est administrable séparément du déploiement. C’est un progrès, pas une garantie.
Injecter par variables d’environnement
La méthode la plus directe, clé par clé :
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: mon-api:1.0.0
ports:
- containerPort: 8080
env:
# Une clé de ConfigMap vers une variable, avec renommage possible
- name: ENVIRONNEMENT
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
# Une clé de Secret vers une variable
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
# Une valeur littérale, pour ce qui n'est ni partagé ni sensible
- name: PORT
value: "8080"
Pour tout importer d’un coup, sans énumérer :
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: db-credentials
envFrom est concis, mais il a une contrepartie qu’il faut mesurer : les noms de variables deviennent implicites. En lisant le Deployment, on ne sait plus quelles variables l’application reçoit. Pour une application dont vous ne maîtrisez pas le code, l’énumération explicite reste préférable.
Ajoutez enfin optional: true quand une clé peut légitimement manquer, faute de quoi le Pod reste bloqué en CreateContainerConfigError :
- name: FONCTION_EXPERIMENTALE
valueFrom:
configMapKeyRef:
name: app-config
key: FEATURE_NOUVEAU_PANIER
optional: true
La limite majeure des variables d’environnement
Elle est structurelle : les variables d’environnement sont fixées au démarrage du processus. Modifier une ConfigMap ne change rien dans les Pods qui tournent déjà, et ne déclenche aucun redéploiement. Vous obtenez alors un cluster où la ConfigMap dit une chose et l’application en fait une autre — une incohérence silencieuse, très difficile à repérer.
Deux réponses possibles, selon le besoin.
Forcer un redéploiement à chaque changement, en faisant dépendre le template d’une empreinte de la configuration :
metadata:
labels:
app: api
annotations:
# À recalculer à chaque livraison : toute modification du template
# déclenche une mise à jour progressive des Pods.
checksum/config: "9f2b7c1e4a"
Ou, plus simplement en exploitation courante :
# Redémarrage progressif du Deployment, sans changer l'image
kubectl rollout restart deployment/api
Injecter par volume
Monter une ConfigMap comme un répertoire donne un fichier par clé. C’est le mode obligatoire pour un fichier de configuration entier, et le seul qui permette une mise à jour sans redémarrage.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
volumeMounts:
# Toutes les clés de la ConfigMap deviennent des fichiers ici
- name: config-volume
mountPath: /etc/app/config
readOnly: true
# Un Secret monté : permissions restrictives, jamais dans un log
- name: secrets-volume
mountPath: /etc/app/secrets
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
items:
# Sans « items », toutes les clés sont montées.
# Avec, on choisit précisément, et on renomme.
- key: nginx.conf
path: default.conf
- name: secrets-volume
secret:
secretName: db-credentials
defaultMode: 0400 # lecture seule pour le propriétaire
Avec ce manifeste, le conteneur voit /etc/app/config/default.conf contenant la valeur de la clé nginx.conf. Le renommage par items est ce qui permet de placer un fichier là où l’application l’attend, sans contorsion.
Pour remplacer un fichier unique dans un répertoire existant sans masquer le reste de son contenu, subPath est la clé :
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf # remplace UN fichier, pas le répertoire
Sans subPath, monter sur /etc/nginx/conf.d/ masquerait tout le contenu d’origine du répertoire. C’est une cause classique de conteneur qui refuse de démarrer après ajout d’une configuration.
La mise à jour à chaud, avec ses conditions
Les ConfigMaps montées en volume sont mises à jour dans le conteneur qui tourne, sans redémarrage. Trois réserves à connaître :
- La propagation prend jusqu’à une minute environ, selon la période de synchronisation du kubelet.
- L’application doit savoir recharger son fichier. nginx a besoin d’un
nginx -s reload, une application Spring Boot ne relit pas ses propriétés spontanément. Le fichier change, le comportement pas. - Un montage avec
subPathn’est jamais mis à jour. C’est le compromis exact de cette option : précision du montage contre perte du rechargement.
# Modifier une ConfigMap en place puis observer le fichier dans le Pod
kubectl edit configmap app-config
kubectl exec -it deploy/web -- cat /etc/app/config/default.conf
Variables d’environnement ou volume : lequel choisir
| Critère | Variables d’environnement | Volume |
|---|---|---|
| Valeurs simples et courtes | adapté | verbeux |
| Fichier de configuration complet | inadapté | seule option |
| Mise à jour sans redémarrage | impossible | possible, avec réserves |
Visibilité dans /proc et les logs de crash | exposée | limitée aux lecteurs du fichier |
Sortie de kubectl describe pod | noms des variables visibles | chemin du montage seulement |
Pour les secrets, le volume est plus sûr, et pour une raison concrète que l’on peut constater soi-même :
# L'environnement du processus principal est lisible dans /proc.
kubectl exec -it deploy/api -- sh -c 'tr "\0" "\n" < /proc/1/environ | grep DB_'
# DB_PASSWORD=S3cr3t-a-changer
Les variables d’environnement se retrouvent aussi dans les traces d’exception, dans les rapports de plantage, et sont héritées par tout processus enfant. Un fichier monté en mode 0400 a une surface d’exposition nettement plus étroite.
Les ConfigMaps immuables
Marquer une ConfigMap ou un Secret comme immuable interdit toute modification ultérieure, et allège au passage la charge de surveillance sur le serveur d’API dans les grands clusters.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v3
immutable: true
data:
APP_ENV: "production"
LOG_LEVEL: "warn"
Cela impose une discipline utile : plutôt que de modifier app-config, vous créez app-config-v4 et vous faites pointer le Deployment dessus. Le changement de configuration devient une modification versionnée du Deployment, donc visible dans kubectl rollout history et annulable par kubectl rollout undo. Une ConfigMap modifiée en place, elle, ne laisse aucune trace exploitable.
À retenir
- Une image doit être indépendante de son environnement. La ConfigMap porte la configuration, le Secret les données sensibles, et les deux s’injectent par variables d’environnement ou par volume.
- Un Secret n’est pas chiffré : il est encodé en base64, lisible d’un
base64 -d, et stocké en clair dans etcd par défaut. Ce qui le protège vraiment, c’est le RBAC, le chiffrement au repos configuré côté serveur d’API, et un gestionnaire externe. - Les variables d’environnement sont figées au démarrage : modifier une ConfigMap ne change rien aux Pods en cours. Forcez la bascule par une annotation de checksum ou un
kubectl rollout restart. - Les volumes sont propagés à chaud (à une minute près), à condition que l’application recharge son fichier — et jamais avec
subPath, qui gèle le montage. - Préférez le volume pour les secrets : les variables d’environnement se retrouvent dans
/proc, dans les traces d’erreur et chez tous les processus enfants.
Conseil : versionnez vos ConfigMaps (
app-config-v3) et rendez-les immuables. Un changement de configuration devient alors une révision du Deployment, donc traçable et réversible comme n’importe quel déploiement.
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion