Un cluster de production en code
Pourquoi
Les sept leçons précédentes ont décrit un cluster à coups de commandes scw et de manifestes. Cela suffit pour comprendre ; cela ne suffit pas pour exploiter. Un cluster de production décrit à la main a trois défauts. On ne sait pas le refaire : si la région est perdue, ou si l'on veut une préproduction identique, il faut retrouver de mémoire le type de nœud, la fenêtre de maintenance, la politique de remplacement. On ne sait pas le relire : qui a changé la taille du pool, et pourquoi ? Et on ne sait pas le tester : le cluster de préproduction de la leçon 6 doit se créer par le même code que la production, sinon il ne prouve rien.
Terraform règle ces trois points, au prix de deux difficultés propres à Kubernetes. D'abord, un cluster est un objet dont l'intérieur (Argo CD, Traefik, les applications) ne se décrit pas dans le même outil que son enveloppe : il faut décider où s'arrête Terraform et où commence le GitOps. Ensuite, le cluster crée des ressources que Terraform ne connaît pas (répartiteurs, volumes) et le fournisseur Scaleway expose un kubeconfig qui finit dans l'état, avec un secret dedans. Ces deux points, qui font les incidents, occupent la moitié de la leçon.
On suppose acquis Terraform : les fondamentaux (ressources, état distant, variables, for_each) et on renvoie à Terraform avancé pour les modules et les couches.
Les concepts
Ce que Terraform possède
flowchart TB
subgraph TF["Possédé par Terraform"]
N["Réseau privé<br/>pn-signalements"] --> C["Cluster<br/>signalements-prod"]
C --> P["Pools de nœuds"]
C --> A["Liste d'adresses autorisées"]
C --> H["Amorçage : Argo CD"]
end
subgraph K["Créé par le cluster"]
L["Répartiteurs (Service LoadBalancer)"]
V["Volumes (PersistentVolumeClaim)"]
end
subgraph G["Possédé par Git, via Argo CD"]
X["Traefik, External Secrets,<br/>Signalements"]
end
C -. "crée" .-> L
C -. "crée" .-> V
H --> X
Trois propriétaires, et la règle est de ne pas en mélanger. Terraform possède ce qui existe avant le premier pod : réseau, cluster, pools, accès. Le cluster possède ce qu'il crée lui-même en réponse aux objets Kubernetes : un répartiteur apparaît parce qu'un Service le demande, un volume parce qu'un claim le demande ; Terraform ne doit pas essayer de les décrire. Git, par Argo CD, possède tout ce qui s'exécute dans le cluster.
Les arguments qui comptent
Le schéma du fournisseur (version 2.84.0, lu avec terraform providers schema -json) décrit les ressources de cette leçon. Les arguments utiles :
scaleway_k8s_cluster. Obligatoires : name, version, cni, delete_additional_resources. On y ajoute type (kapsule pour le plan de contrôle mutualisé, kapsule-dedicated-4, -8 ou -16), private_network_id (obligatoire en pratique : la documentation du fournisseur précise que les réseaux privés sont désormais obligatoires pour Kapsule), tags, les blocs auto_upgrade (activation, jour, heure de début), autoscaler_config (les mêmes réglages que ceux de scw k8s cluster update), open_id_connect_config (leçon 5) et les listes feature_gates et admission_plugins. upgrade_pools (vrai par défaut) décide si la montée du cluster entraîne celle des pools.
scaleway_k8s_pool. Obligatoires : cluster_id, node_type, size. On y ajoute zone, min_size et max_size (utilisés par l'autoscaling), autoscaling, autohealing, root_volume_size_in_gb (20 Go au minimum), root_volume_type, labels (étiquettes Kubernetes appliquées aux nœuds et réconciliées), des blocs taints et startup_taints, public_ip_disabled (pour l'isolation complète, qui exige une passerelle publique), et le bloc upgrade_policy (max_surge, max_unavailable).
scaleway_k8s_acl. Un bloc acl_rules par règle, soit avec ip, soit avec scaleway_ranges = true (jamais les deux dans la même règle), ou no_ip_allowed pour isoler complètement l'API.
Ce qui recrée et ce qui change en place
Le fournisseur documente les arguments dont la modification détruit puis recrée la ressource. Les connaître avant d'écrire du code évite les mauvaises surprises :
| Ressource | Recrée | Se modifie en place |
|---|---|---|
| Cluster | cni, private_network_id (sauf migration d'un cluster ancien sans réseau privé), pod_cidr, service_cidr, service_dns_ip une fois posés à une valeur personnalisée, skip_nodes_with_local_storage et log_level de l'autoscaler | version (montée), auto_upgrade, tags, autoscaler_config (le reste), open_id_connect_config, feature_gates |
| Pool | name, node_type, zone, public_ip_disabled, container_runtime, placement_group_id | size, min_size, max_size, autoscaling, autohealing, upgrade_policy, labels, tags, version |
Recréer un cluster, c'est perdre tout ce qu'il contient ; recréer un pool, c'est remplacer tous ses nœuds d'un coup. Pour le pool, le fournisseur indique la bonne méthode : créer le nouveau pool sous un autre nom, vider l'ancien avec kubectl drain, puis le retirer du code. Avec create_before_destroy, le nom doit être généré, sans quoi Terraform tente de créer le nouveau pool sous le même nom, que l'API refuse.
Le kubeconfig dans l'état
L'attribut kubeconfig de scaleway_k8s_cluster expose config_file, host, cluster_ca_certificate et token. Le fournisseur le marque sensible : Terraform ne l'affichera pas dans le plan. Sensible ne veut pas dire absent de l'état. L'état stocke la valeur en clair (voir L'état) : quiconque peut lire le fichier d'état lit ce jeton.
Ce jeton est un secret de plein droit. Il n'est pas votre clé IAM personnelle ; c'est celui que l'API retourne pour le cluster, et la documentation de Scaleway comme le code de la CLI montrent qu'à côté de l'authentification par IAM subsiste une authentification par jeton propre au cluster (legacy, voir leçon 5). Sur un cluster d'essai, kubectl auth whoami avec ce jeton vous dit quelles permissions il porte : vérifiez-le plutôt que de le supposer. Dans tous les cas, protégez l'état comme on protège un accès d'administrateur : état distant dans un bucket privé à accès restreint, versionnage, chiffrement, une application IAM dédiée à Terraform, et personne d'autre en lecture. Les cours Terraform : les fondamentaux, leçon 7 et Les secrets hors de l'état détaillent ces protections ; ici, ne pas aggraver : ne déclarez jamais de output qui expose le kubeconfig, même marqué sensible.
Amorcer, puis céder la main
Argo CD est à la fois un composant du cluster et ce qui déploie tout le reste. Il faut donc que quelqu'un l'installe en premier. Terraform peut le faire avec le fournisseur Helm : un helm_release du chart argo-cd, qui s'exécute après la création des pools (il faut des nœuds pour accueillir les pods). Ensuite, on crée l'application racine d'Argo CD, qui pointe vers le dépôt GitOps, et Argo CD prend la suite : il installe Traefik, External Secrets, l'application, et même se gère lui-même.
Le risque est une double propriété : Terraform et Argo CD veulent tous deux gérer la version d'Argo CD. L'amorçage ne doit donc servir qu'une fois. Deux garde-fous : lifecycle { ignore_changes = [version, values] } sur le helm_release pour que Terraform ne touche plus à l'installation après la première fois, ou, plus radical, le retrait de la ressource de l'état (terraform state rm) une fois Argo CD autonome.
Des couches, pas un monolithe
Une configuration unique qui crée le réseau, le cluster, et installe Argo CD par un fournisseur dont les paramètres dépendent du cluster pose un problème connu : le fournisseur Helm doit connaître l'adresse du cluster avant qu'il n'existe, ce que Terraform gère mal (d'où l'astuce null_resource de la documentation du fournisseur Scaleway). La bonne réponse est de séparer (voir Composer une infrastructure en couches et Découper et refactoriser les états) :
- cluster : réseau, cluster, pools, accès. Son état contient le secret ; il change rarement.
- amorçage : Argo CD, installé avec le jeton d'un compte de service IAM, pas avec celui du cluster.
- applications : tout le reste, dans Git, géré par Argo CD, pas par Terraform.
En pratique
La configuration est validée contre le schéma réel du fournisseur (terraform init, fmt et validate avec Scaleway 2.84.0 et Helm 3.3.0). Elle n'a pas été appliquée : on ne crée pas de cluster pour écrire un cours.
1. Versions et fournisseurs
# versions.tf
terraform {
required_version = ">= 1.14"
required_providers {
scaleway = {
source = "scaleway/scaleway"
version = "~> 2.84"
}
helm = {
source = "hashicorp/helm"
version = "~> 3.3"
}
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
}
}
provider "scaleway" {
project_id = var.project_id
region = "fr-par"
zone = "fr-par-1"
}Le fournisseur est lu par la variable d'environnement SCW_ACCESS_KEY et SCW_SECRET_KEY d'une application IAM dédiée à Terraform (pas d'une personne), dont la politique se limite au projet signalements-prod et aux produits utilisés : Kubernetes et VPC. Les clés ne sont jamais écrites dans le code (voir la leçon 5).
2. Variables
# variables.tf
variable "project_id" {
type = string
description = "Identifiant du projet Scaleway signalements-prod"
}
variable "kubernetes_version" {
type = string
description = "Version mineure de Kubernetes (x.y) ; la correction suit la mise à jour automatique"
default = "1.36"
validation {
condition = can(regex("^1\\.[0-9]+$", var.kubernetes_version))
error_message = "Indiquer une version mineure, par exemple 1.36."
}
}
variable "cluster_type" {
type = string
description = "Type du plan de contrôle : kapsule (mutualisé) ou kapsule-dedicated-4, -8, -16"
default = "kapsule"
validation {
condition = contains(["kapsule", "kapsule-dedicated-4", "kapsule-dedicated-8", "kapsule-dedicated-16"], var.cluster_type)
error_message = "Type de cluster inconnu."
}
}
variable "zones_applications" {
type = list(string)
description = "Zones des pools d'applications, un pool par zone"
default = ["fr-par-1", "fr-par-2"]
}
variable "api_allowed_cidrs" {
type = map(string)
description = "Adresses autorisées à joindre l'API : description => CIDR"
}
variable "argocd_chart_version" {
type = string
description = "Version du chart argo-cd, à épingler"
}La version est donnée en x.y parce que la mise à jour automatique est active : le fournisseur l'exige. Aucune valeur par défaut pour argocd_chart_version : le choix d'une version épinglée est une décision à prendre et à relire, pas une valeur à hériter.
3. Le réseau privé
# reseau.tf
resource "scaleway_vpc" "principal" {
name = "vpc-signalements"
tags = ["signalements", "prod"]
}
resource "scaleway_vpc_private_network" "app" {
name = "pn-signalements"
vpc_id = scaleway_vpc.principal.id
tags = ["signalements", "prod"]
ipv4_subnet {
subnet = "172.16.20.0/22"
}
}C'est le réseau du cours Signalements sur Scaleway, en code ; la leçon 2 a dit comment le choisir (taille, chevauchement avec les réseaux de pods et de services).
4. Le cluster
# cluster.tf
resource "scaleway_k8s_cluster" "apps" {
name = "signalements-prod"
description = "Signalements, production"
type = var.cluster_type
version = var.kubernetes_version
cni = "cilium"
private_network_id = scaleway_vpc_private_network.app.id
tags = ["signalements", "prod"]
# Ne pas laisser Scaleway supprimer à notre place les répartiteurs,
# volumes et le réseau privé que le cluster a fait créer.
delete_additional_resources = false
# Les pools se montent séparément, avec leur politique de remplacement.
upgrade_pools = false
auto_upgrade {
enable = true
maintenance_window_day = "tuesday"
maintenance_window_start_hour = 7
}
autoscaler_config {
balance_similar_node_groups = true
expander = "least_waste"
scale_down_unneeded_time = "10m"
scale_down_utilization_threshold = 0.5
}
lifecycle {
prevent_destroy = true
}
}Chaque choix se justifie :
cni = "cilium": modifier ce champ recrée le cluster, il se choisit une fois (leçon 2).delete_additional_resources = false: voir la section « En production » ; c'est un choix de prudence, et il se discute.upgrade_pools = false: la montée du cluster ne touche pas aux nœuds ; les pools se montent avec leur propreupgrade_policy.auto_upgrade: correctifs automatiques dans une fenêtre de deux heures, le mardi à 7 heures (UTC, d'après la documentation du fournisseur). Le mardi matin est un jour où l'équipe est là pour regarder.autoscaler_config:least_wastechoisit, parmi les pools capables d'accueillir un pod, celui qui gaspille le moins de ressources ;balance_similar_node_groupsrépartit entre les pools semblables (nos pools d'applications des deux zones).prevent_destroy = true: unterraform destroyou une suppression accidentelle du bloc échoue avec un message. Pour supprimer vraiment, on retire d'abord cette ligne, dans un changement relu.
feature_gates et admission_plugins ne sont pas utilisés : ils activent des fonctions optionnelles du serveur d'API, et la liste des valeurs acceptées est fixée par Scaleway. N'activez une porte de fonctionnalité que pour un besoin précis, jamais une fonction alpha en production.
5. Les pools
# pools.tf
resource "scaleway_k8s_pool" "systeme" {
cluster_id = scaleway_k8s_cluster.apps.id
name = "systeme"
node_type = "PRO2-S"
zone = "fr-par-1"
size = 2
min_size = 2
max_size = 3
autoscaling = true
autohealing = true
root_volume_size_in_gb = 50
labels = {
role = "systeme"
}
upgrade_policy {
max_surge = 1
max_unavailable = 0
}
}
resource "scaleway_k8s_pool" "applications" {
for_each = toset(var.zones_applications)
cluster_id = scaleway_k8s_cluster.apps.id
name = "applications-${each.key}"
node_type = "PRO2-M"
zone = each.key
size = 1
min_size = 1
max_size = 4
autoscaling = true
autohealing = true
labels = {
role = "applications"
}
upgrade_policy {
max_surge = 1
max_unavailable = 0
}
lifecycle {
ignore_changes = [size]
}
}Le pool systeme accueille Traefik, Argo CD et External Secrets ; les deux pools applications (un par zone) accueillent Signalements. Un pool est limité à une zone : la répartition sur deux zones s'obtient donc avec deux pools, ici par for_each. Le min_size de 1 dans chaque zone, avec les nœuds du pool système, donne les trois nœuds au moins que recommande la documentation sur deux zones. Les labels permettent aux déploiements de choisir leurs nœuds (nodeSelector). Les types PRO2 sont à vCPU partagés (voir Instances) : on passe à des vCPU dédiés si les mesures de la leçon 7 montrent du temps volé.
Trois détails :
ignore_changes = [size]: avec l'autoscaling, l'autoscaler change la taille réelle du pool ; sans cette ligne, chaqueplanvoudrait la ramener àsize. D'après la documentation du fournisseur, quand l'autoscaling est actif la modification desizen'est de toute façon pas prise en compte.upgrade_policy: surcapacité d'un nœud, aucun nœud indisponible : la capacité ne baisse jamais pendant une montée (leçon 6).- Un pool réservé aux environnements éphémères (leçon 7) s'ajoute par une ressource de plus, avec
size = 0,min_size = 0et un bloctaints(clé, valeur, effetNoSchedule) pour que seuls les pods qui le tolèrent y arrivent.
6. La liste d'adresses autorisées
# acces.tf
resource "scaleway_k8s_acl" "api" {
cluster_id = scaleway_k8s_cluster.apps.id
dynamic "acl_rules" {
for_each = var.api_allowed_cidrs
content {
ip = acl_rules.value
description = acl_rules.key
}
}
acl_rules {
scaleway_ranges = true
description = "Plages Scaleway (nœuds, services)"
}
}Les adresses viennent d'une variable (le réseau de Lyneko, le bastion), dans un fichier terraform.tfvars versionné : ce sont des adresses publiques, pas des secrets. D'après la documentation du fournisseur, cette ressource remplace la règle 0.0.0.0/0 créée avec le cluster, et la règle est recréée si la ressource disparaît : on ne se retrouve pas sans protection par accident, mais on se retrouve ouvert après un destroy de la seule ACL.
7. Les sorties, sans le kubeconfig
# sorties.tf
output "cluster_id" {
description = "Identifiant régional du cluster, pour scw k8s kubeconfig install"
value = scaleway_k8s_cluster.apps.id
}
output "apiserver_url" {
value = scaleway_k8s_cluster.apps.apiserver_url
}
output "cluster_ca_certificate" {
description = "Certificat public de l'autorité du cluster (base64), non secret"
value = nonsensitive(scaleway_k8s_cluster.apps.kubeconfig[0].cluster_ca_certificate)
}On sort ce qui est public : l'identifiant, l'adresse de l'API, le certificat de l'autorité (c'est ce que tout client doit connaître). Pas de config_file, pas de token. Les personnes se connectent par scw k8s kubeconfig install avec leur propre identité (leçon 5).
8. L'amorçage d'Argo CD en une étape
C'est la version « tout en un », avec l'astuce documentée par le fournisseur : le cluster est créé avec le statut pool_required et son kubeconfig est déjà disponible, ce qui permet de configurer le fournisseur Helm. La ressource null_resource fige les valeurs utilisées :
# amorcage.tf
resource "null_resource" "kubeconfig" {
depends_on = [scaleway_k8s_pool.systeme]
triggers = {
host = scaleway_k8s_cluster.apps.kubeconfig[0].host
token = scaleway_k8s_cluster.apps.kubeconfig[0].token
cluster_ca_certificate = scaleway_k8s_cluster.apps.kubeconfig[0].cluster_ca_certificate
}
}
provider "helm" {
kubernetes = {
host = null_resource.kubeconfig.triggers.host
token = null_resource.kubeconfig.triggers.token
cluster_ca_certificate = base64decode(null_resource.kubeconfig.triggers.cluster_ca_certificate)
}
}
resource "helm_release" "argocd" {
name = "argocd"
namespace = "argocd"
repository = "https://argoproj.github.io/argo-helm"
chart = "argo-cd"
version = var.argocd_chart_version
create_namespace = true
atomic = true
timeout = 600
depends_on = [scaleway_k8s_pool.applications]
lifecycle {
ignore_changes = [version, values]
}
}Dans le fournisseur Helm 3, le bloc kubernetes devient un argument (kubernetes = { ... }), alors qu'il s'écrivait comme un bloc dans les versions 2 : une configuration trouvée dans une ancienne documentation ne se valide pas telle quelle. atomic = true annule l'installation si elle échoue ; timeout donne dix minutes au chart ; ignore_changes laisse Argo CD, une fois installé, gérer sa propre version.
Cette étape a un défaut : le jeton du cluster est copié une deuxième fois dans l'état, par le null_resource. C'est la raison pour laquelle on la limite à un premier démarrage, et pourquoi on préfère la version en couches de l'étape 10.
L'application racine d'Argo CD (qui pointe vers le dépôt GitOps de lyneko-apps) s'applique ensuite par une commande unique, ou par Terraform avec un second helm_release d'un chart d'applications ; peu importe lequel, à condition que ce soit la dernière chose que Terraform fait dans le cluster. Voir Installer Argo CD et déployer une application.
9. Plan, application, vérification
$ terraform fmt -check -recursive
$ terraform validate
$ terraform plan -out=cluster.tfplan
Lisez le plan ligne à ligne la première fois : un réseau privé, un cluster, trois pools, une ACL, un null_resource, un helm_release. Aucune destruction. Vérifiez que le sous-réseau est 172.16.20.0/22, que les deux pools d'applications sont dans deux zones différentes, et que la liste d'adresses ne contient pas 0.0.0.0/0. Puis terraform apply cluster.tfplan : l'application d'un fichier de plan garantit qu'on applique exactement ce qu'on a relu.
$ scw k8s kubeconfig install $(terraform output -raw cluster_id)
$ kubectl get nodes -o wide
$ kubectl -n argocd get pods
Trois nœuds au moins, répartis sur deux zones, tous prêts ; les pods d'Argo CD en exécution. La création d'un cluster et de ses pools prend plusieurs minutes ; wait_for_pool_ready (vrai par défaut) fait attendre Terraform.
10. La version en couches
Séparez amorcage.tf dans un second répertoire, avec son propre état. Il lit les sorties publiques de la couche cluster et s'authentifie avec une clé IAM d'un compte de service dédié, passée en variable d'environnement :
# amorcage/main.tf
data "terraform_remote_state" "cluster" {
backend = "s3"
config = {
bucket = "lyneko-etats"
key = "signalements/prod/cluster.tfstate"
region = "fr-par"
endpoints = { s3 = "https://s3.fr-par.scw.cloud" }
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_s3_checksum = true
}
}
variable "scw_secret_key" {
type = string
sensitive = true
}
provider "helm" {
kubernetes = {
host = data.terraform_remote_state.cluster.outputs.apiserver_url
cluster_ca_certificate = base64decode(data.terraform_remote_state.cluster.outputs.cluster_ca_certificate)
token = var.scw_secret_key
}
}Le jeton porteur est la clé secrète d'une application IAM disposant de KubernetesFullAccess (ou, plus étroit, d'un groupe lié par un RoleBinding à cluster-admin pour le seul temps de l'amorçage). Elle arrive par TF_VAR_scw_secret_key, issue du coffre de la CI ; les valeurs de configuration d'un fournisseur ne sont pas écrites dans l'état. Le jeton d'administrateur du cluster n'est donc plus recopié, et la couche d'amorçage n'a plus besoin de la lecture de l'état du cluster pour autre chose que trois valeurs publiques. Le reste du fichier (le helm_release) est identique à l'étape 8.
Sous le capot
Ce que le fournisseur appelle. scaleway_k8s_cluster appelle l'API Kubernetes de Scaleway (CreateCluster), qui répond tout de suite : le cluster est en état pool_required tant qu'aucun pool n'existe. Le fournisseur le sait, d'où la remarque de sa documentation sur le kubeconfig disponible avant les nœuds. À l'application, il attend l'état stable ; la création d'un pool attend l'arrivée des nœuds prêts (wait_for_pool_ready). Une montée de version modifie la ressource en place : le fournisseur appelle l'opération de montée, et, si upgrade_pools est vrai, l'API monte aussi les pools, hors de Terraform : l'état des pools ne reflète plus leur version. C'est une des raisons de upgrade_pools = false.
Ce que le cluster crée sans le dire à Terraform. Un Service LoadBalancer fait créer un répartiteur par le cloud controller manager ; un PersistentVolumeClaim fait créer un volume par le pilote CSI (leçon 4). Ces ressources portent des étiquettes qui les rattachent au cluster, mais Terraform ne les a pas dans son état. Il ne peut donc ni les supprimer proprement, ni les détecter comme dérive ; seul delete_additional_resources permet à Scaleway de les emporter avec le cluster.
Pièges courants
Error: Instance cannot be destroyed (prevent_destroy). C'est voulu. Pour supprimer le cluster, retirez la ligne dans un changement relu, appliquez, puis détruisez. N'utilisez pas -target pour contourner.
Un plan qui veut recréer le cluster. Un seul champ en cause, souvent cni ou private_network_id modifié par mégarde. Lisez la ligne marquée # forces replacement ; si ce n'est pas voulu, remettez la valeur. Ne pas appliquer un plan qui détruit un cluster sans avoir lu pourquoi.
Un plan qui veut ramener la taille du pool. Il manque ignore_changes = [size], ou l'autoscaling n'est pas déclaré. Sans cela, chaque application de Terraform écrase le travail de l'autoscaler.
Error: Unsupported block type sur le fournisseur Helm. Vous utilisez la syntaxe de la version 2 (kubernetes { ... }) avec la version 3 : c'est kubernetes = { ... }.
Error: Kubernetes cluster unreachable pendant l'amorçage. Le fournisseur Helm tente de se connecter avant que les nœuds soient prêts, ou l'ACL exclut l'adresse du poste de CI. Vérifiez que depends_on pointe vers les pools, et que l'adresse de sortie de votre CI figure dans api_allowed_cidrs.
Sécurité
- L'état du cluster est un secret d'administrateur. Bucket privé, accès restreint à la CI et à deux personnes, versionnage, chiffrement, journal d'accès. Un état qui fuit donne la main sur le cluster, indépendamment de l'IAM.
- Aucun
outputne porte le kubeconfig ni le jeton ;sensitive = truemasque l'affichage, il ne protège pas la valeur. - Une application IAM par couche. La couche cluster a besoin de Kubernetes et du VPC ; l'amorçage, d'un accès au cluster seulement ; le déploiement applicatif n'a besoin d'aucune clé (c'est Argo CD qui tire).
- La liste d'adresses est du code. Un changement d'ACL passe en revue comme un changement de pare-feu : c'est ce qu'il est.
- Épingler les versions. Fournisseur (
~> 2.84) et chart (argocd_chart_version) : une version flottante change l'infrastructure sans qu'un commit ne le dise. Le fichier.terraform.lock.hclse versionne.
En production
Un module, plusieurs environnements. Le même module « cluster Kapsule » est appelé par la production (cluster_type = "kapsule-dedicated-4", deux zones, fenêtre le mardi) et par la préproduction (plan mutualisé, une zone, un seul pool, fenêtre le lundi, pour essayer les montées en premier). Un fichier de variables par environnement, un état par environnement, et un dépôt qui dit sans ambiguïté quelle version du module chacun utilise.
Qui applique. Le plan se produit en CI à chaque proposition de changement, et l'application se fait après revue, depuis la CI, jamais depuis un poste. Terraform ne tourne pas à chaque commit de l'application : la couche cluster change quelques fois par an.
Détruire un cluster. Avant terraform destroy, supprimez ce que le cluster a créé : kubectl delete svc -A --field-selector spec.type=LoadBalancer (ou plutôt, laissez Argo CD supprimer les applications), puis les PersistentVolumeClaim, puis attendez que les répartiteurs et volumes disparaissent (scw lb lb list, scw block volume list). Avec delete_additional_resources = false, ce qui reste après la suppression est orphelin et facturé. Avec true, Scaleway supprime ces ressources et, d'après la documentation du fournisseur, le réseau privé du cluster s'il est vide : un choix pratique pour un cluster d'essai, dangereux pour un cluster de production où le réseau est géré ailleurs, et dont les volumes portent des données.
Reconstruire. Le vrai test d'un cluster en code est de le détruire et le recréer en préproduction : si Argo CD réinstalle tout à l'identique depuis Git, sans intervention manuelle, le travail est fait. Si une étape manuelle est nécessaire (un secret créé à la main, comme la clé racine d'External Secrets de la leçon 5), notez-la : c'est le dernier maillon à automatiser.
Exercices
1. Lire un plan (niveau 300). Un collègue change cni = "cilium" en cni = "calico" « pour essayer ». Que montre terraform plan ? Que se passerait-il à l'application, et que lui répondez-vous ?
Solution
Le plan indique que le cluster doit être détruit puis recréé (# forces replacement sur cni), avec les pools qui en dépendent. À l'application, le cluster et tout son contenu seraient supprimés ; prevent_destroy = true ferait d'abord échouer l'opération, ce qui est son rôle. Le CNI se choisit à la création (leçon 2) ; pour essayer l'autre, on crée un cluster d'essai par le module, on ne modifie pas la production.
2. Changer le type de nœud (niveau 300). Le pool applications-fr-par-1 passe de PRO2-M à STANDARD3-X4C-16G. Que fait le plan, et quelle procédure suivre pour ne pas couper Signalements ?
Solution
node_type recrée le pool : tous ses nœuds disparaissent d'un coup. La procédure indiquée par le fournisseur : ajouter un nouveau pool (nom différent, nouveau type), attendre ses nœuds prêts, vider les nœuds de l'ancien pool avec kubectl drain --ignore-daemonsets --delete-emptydir-data (les PodDisruptionBudget protègent le service, leçon 6), vérifier que les pods ont migré, puis retirer l'ancien pool du code. Les volumes persistants restent liés à leur zone : le nouveau pool doit être dans la même zone que les volumes. Vérifiez aussi que le type est disponible dans la zone.
3. Un état qui fuit (niveau 300). Le bucket d'état est lu par erreur par une personne de l'équipe applicative. Que peut-elle faire avec ? Quelles actions de remédiation, dans l'ordre ?
Solution
Elle lit le kubeconfig du cluster, donc un jeton d'accès d'administrateur indépendant de l'IAM, et elle peut s'en servir tant qu'il est valable. Dans l'ordre : retirer l'accès au bucket ; réinitialiser le jeton du cluster (scw k8s cluster reset-admin-token) ; relancer un refresh ou un apply pour que l'état contienne le nouveau jeton ; rechercher dans le journal d'audit (si le plan de contrôle est dédié) d'éventuels appels suspects ; revoir les droits du bucket et, si l'état contenait d'autres secrets (clés d'API), les faire tourner aussi.
4. Détruire proprement (niveau 300). Rédigez la séquence pour supprimer un cluster d'essai qui porte un Service LoadBalancer et un volume de 10 Gio, avec delete_additional_resources = false. Qu'est-ce qui reste facturé si l'on oublie la première étape ?
Solution
- Laisser Argo CD (ou
kubectl delete) supprimer l'application, donc leServiceet le claim. 2. Attendre la disparition du répartiteur (scw lb lb list) et du volume (scw block volume list) ; lareclaimPolicyde la classe de stockage décide du volume. 3. Retirerprevent_destroy, puisterraform destroy. Si l'on oublie l'étape 1, le répartiteur et le volume survivent au cluster : orphelins et facturés, hors de l'état Terraform, et à supprimer à la main. Avecdelete_additional_resources = true, ils auraient été supprimés avec le cluster, le volume et ses données compris.
Récapitulatif
- Terraform possède le réseau privé, le cluster, les pools et la liste d'adresses ; le cluster possède les répartiteurs et volumes qu'il crée ; Git (Argo CD) possède ce qui s'exécute dedans.
- La configuration se valide contre le schéma réel du fournisseur :
cni,private_network_id,name,node_typeetzonedu pool recréent,version,auto_upgrade,sizeetupgrade_policyse modifient en place. - Une zone par pool : deux zones se décrivent par deux pools (
for_each) ;ignore_changes = [size]laisse l'autoscaler propriétaire de la taille. - Le kubeconfig est dans l'état, jeton compris, même marqué sensible : protégez l'état comme un accès d'administrateur et ne sortez jamais le
kubeconfigenoutput. - L'amorçage d'Argo CD par le fournisseur Helm ne sert qu'une fois (
ignore_changes), idéalement dans une couche séparée authentifiée par une clé IAM ; ensuite, GitOps. delete_additional_resourcesdécide du sort des répartiteurs et volumes : àfalse, supprimez-les d'abord ; àtrue, ils partent avec le cluster, données comprises.- Un module appelé par chaque environnement rend la préproduction reproductible, condition des montées sans surprise.
Pour aller plus loin
- Les pages du fournisseur pour
scaleway_k8s_cluster,scaleway_k8s_pooletscaleway_k8s_acl, qui listent aussi les champs recréants. - Composer une infrastructure en couches et Les secrets hors de l'état.
- GitOps avec Argo CD, pour l'application racine et l'exploitation d'Argo CD une fois autonome.
- La documentation de Scaleway sur les clusters multi-zones et sur l'isolation complète des nœuds (passerelle publique).
- Les cours Helm et Kubernetes : administrer un cluster, pour packager ce qui s'installe dans le cluster et exploiter les montées de version par bascule.
Sources
- Fournisseur Terraform de Scaleway, ressource scaleway_k8s_cluster
- Fournisseur Terraform de Scaleway, ressource scaleway_k8s_pool
- Fournisseur Terraform de Scaleway, ressource scaleway_k8s_acl
- Scaleway, Multi-AZ Kubernetes clusters (exemple Terraform, limites)
- Scaleway, Kubernetes Kapsule : sécuriser un cluster avec un réseau privé (isolation contrôlée et complète)
- Scaleway, Kubernetes version support policy (mise à jour automatique)
- Fournisseur Helm de HashiCorp 3.x, configuration du fournisseur kubernetes
- Terraform, schéma réel des ressources (terraform providers schema -json, fournisseur scaleway 2.84.0)