La vie de développeur est faite de plusieurs craintes : devoir gérer un conflit git, régler un problème de cache et gérer un state Terraform à la main par exemple.
Aujourd’hui, on va s’attaquer à ce dernier avec un cas concret.
Pourquoi diable vouloir fusionner des states Terraform ?
Imaginez que vous ayez une application pour laquelle vous devez provisionner une infrastructure, mettons sur AWS. L’application tourne dans un cluster EKS provisionné à part, dans son namespace et utilise un utilisateur dédié dans Rancher. La vie est belle, vos ressources sont provisionnées « à plat » à grand coup de resource x
dans vos scripts terraform.
Mais voilà que l’application devient multiclient et chaque client a besoin de sa « stack », avec son namespace, son bucket etc. Pour faire simple, vous copiez/collez vos ressources en changeant le nom et la configuration.
Tout cela fonctionne très bien, mais le nombre de stack grandit et vous vous retrouvez avec 5 stacks et un code terraform ingérable où la moindre modification doit être répliquée autant de fois.
Ce qui permettait d’aller vite au début devient maintenant un caillou dans la chaussure.
resource "rancher2_namespace" "project_a" {
}
resource "rancher2_namespace" "project_b" {
}
resource "rancher2_namespace" "project_c" {
}
Comme on le voit, chacune des ressources (bucket, user…) est dupliquée.
Armé de votre courage, vous décidez de mettre en place un module terraform, qui vous permettra de définir de nouvelles stacks en trois coups de cuillère à pot.
module "stack" {
source = "./modules/stack"
for_each = local.stacks
name = each.value["name"]
secrets = each.value["secrets"]
}
locals {
stacks = {
"staging" = {
name = "staging"
secrets = merge(var.default_secrets, var.stacks_secrets["staging"])
}
}
}
Génial, c’est simple à maintenir et la complexité est déportée dans le module à un seul endroit. Plus de doublons.
Le problème, c’est que vous avez déjà provisionné 5 stacks avant de vous décider à mettre en place un module.
Idéalement il faut que ces stacks ré-intègrent votre module. Malheureusement les fichiers de states sont non seulement différents (ce sont des fichiers distincts) mais les ressources ne sont pas au même format.
Là où vous aviez rancher2_project_role_template_binding.project_a
vous devriez maintenant avoir module.stack["project_a"].rancher2_project_role_template_binding.stack
, vos stacks étant gérées dans une map.
Nous allons voir comment gérer cette transition.
Sécurisez l’existant
On commence par les précautions d’usage, c’est-à-dire faire un backup des states.
On appellera original
celui qui a été géré sans module et destination
celui avec.
# dans le projet sans module
tf state pull > original.tfstate
# dans le projet avec module
tf state pull > destination.tfstate
Faites un backup en lieu sûr de ces fichiers pour disposer d’exemplaires originaux et travaillez sur une copie.
Les fichiers d’états sont de simples fichiers texte, toutefois je vous déconseille vivement de vous essayer à des modifications manuelles dans ces fichiers.
Commençons par lister le contenu de notre state original.
terraform state list -state=original.tfstate
Nous donne :
rancher2_cluster_role_template_binding.project_a
rancher2_cluster_role_template_binding.project_b
rancher2_cluster_role_template_binding.project_c
rancher2_cluster_role_template_binding.project_d
rancher2_cluster_role_template_binding.project_e
rancher2_cluster_role_template_binding.project_f
rancher2_global_role_binding.project_a
rancher2_global_role_binding.project_b
rancher2_global_role_binding.project_c
rancher2_global_role_binding.project_d
rancher2_global_role_binding.project_e
rancher2_global_role_binding.project_f
rancher2_namespace.project_a
rancher2_namespace.project_b
rancher2_namespace.project_c
rancher2_namespace.project_d
rancher2_namespace.project_e
rancher2_namespace.project_f
rancher2_project.project_a
rancher2_project.project_b
rancher2_project.project_c
rancher2_project.project_d
rancher2_project.project_e
rancher2_project.project_f
rancher2_project_role_template_binding.project_a
rancher2_project_role_template_binding.project_b
rancher2_project_role_template_binding.project_c
rancher2_project_role_template_binding.project_d
rancher2_project_role_template_binding.project_e
rancher2_project_role_template_binding.project_f
rancher2_registry.project_a
rancher2_registry.project_b
rancher2_registry.project_c
rancher2_registry.project_d
rancher2_registry.project_e
rancher2_registry.project_f
rancher2_secret_v2.project_a_administration
rancher2_secret_v2.project_a_aws_s3
rancher2_secret_v2.project_a_database
rancher2_secret_v2.project_a_deepl
rancher2_secret_v2.project_a_froala
rancher2_secret_v2.project_a_google
rancher2_secret_v2.project_a_mailjet
rancher2_secret_v2.project_b_administration
rancher2_secret_v2.project_b_aws_s3
rancher2_secret_v2.project_b_database
rancher2_secret_v2.project_b_google
rancher2_secret_v2.project_c_administration
rancher2_secret_v2.project_c_aws_s3
rancher2_secret_v2.project_c_database
rancher2_secret_v2.project_c_google
rancher2_secret_v2.project_d_administration
rancher2_secret_v2.project_e_administration
rancher2_secret_v2.project_e_aws_s3
rancher2_secret_v2.project_e_database
rancher2_secret_v2.project_e_google
rancher2_secret_v2.project_f_administration
rancher2_secret_v2.project_f_aws_s3
rancher2_secret_v2.project_f_database
rancher2_secret_v2.project_f_google
rancher2_secret_v2.project_f_mailjet
rancher2_user.project_a
rancher2_user.project_b
rancher2_user.project_c
rancher2_user.project_d
rancher2_user.project_e
rancher2_user.project_f
Ouch ! Ce fichier nous liste l’ensemble des ressources dans nos states. Forcément, nous retrouvons chaque ressource démultipliée (1 fois par projet).
Il va falloir déplacer ces ressources une à une avec tf state mv
.
On ne se laisse pas abattre et on va créer un petit script pour nous aider. Celui-ci est très simple et va migrer les ressources d’un fichier de state à un autre.
Ce qu’on souhaite c’est que notre code à base de stack ait connaissance des ressources qui avaient été crées hors stack afin de les ré-intégrer.
#!/usr/bin/env bash
while read state; do
terraform state mv -state=original.tfstate -state-out=destination.tfstate $state $state
done <$(terraform state list -state=original.tfstate)
Une fois fait, on va se retrouver avec notre fichier destination.tfstate
contenant l’intégralité du state. On peut le vérifier avec un terraform state list -state=destination.tfstate
.
À noter que terraform va créer des fichiers de state intermédiaires pour chacune des opérations, permettant un rollback éventuel. Étant donné qu’on travaille sur des fichiers locaux, le risque est ici limité.
En l’état ce qu’on vient de faire s’apparente à un merge des fichiers de state.
Maintenant il faut que nous mettions les nouvelles ressources au bon format. Je vous conseille également d’outiller la démarche avec des scripts, au moins par type de ressources, si le nombre de ressources à migrer est important.
En termes de commandes finales, elles s’apparenteront à cela :
tf state mv -state=destination.tfstate rancher2_user.project_a module.stack\[\"project_a\"\].rancher2_user.stack
Une fois que les ressources ont été migrées, il nous reste à ajouter les instructions dans notre module, comme si les stacks importées avaient été créées avec.
module "stack" {
source = "./modules/stack"
for_each = local.stacks
name = each.value["name"]
secrets = each.value["secrets"]
}
locals {
stacks = {
"staging" = {
name = "staging"
secrets = merge(var.default_secrets, var.stacks_secrets["staging"])
}
"project_a" = {
name = "project_a"
secrets = merge(var.default_secrets, var.stacks_secrets["project_a"])
}
…
}
}
C’est le moment de vérité, il reste à vérifier qu’on a bien fait les choses :
mv destination.tfstate nom_utilise_dans_votre_projet.tfstate
tf state push nom_utilise_dans_votre_projet.tfstate
tf plan
Si tout est en ordre, notre tf plan
ne devrait produire aucun changement (pas de destroy, add ou update). Si vous avez tout cassé, remettez le state original, retirez le code ajouté et recommencez.
À noter qu’ici je donne l’exemple de state différents, si j’avais eu à gérer des mouvements de ressources faisant déjà partie d’un même projet et d’un même state j’aurai sans doute utilisé des instructions moved
.
J’aurai peut-être même utilisé tfautomv que j’ai eu l’occasion de découvrir à Cloud Nord 2023 et dont je vous reparlerai dans l’article de compte rendu des conférences 😊.