Hello les amis,
Aujourd’hui, j’ai décidé de faire du Terraform de la façon la plus flinguée possible pour vous montrer les trucs qui peuvent potentiellement casser votre infra, ainsi que les façons de réparer les bêtises.
Parce que je suis à peu près certain qu’à un moment, même avec toute la bonne volonté du monde, il arrive de faire des giga conneries avec Terraform. Je veux vous éviter quelques gouttes de sueur en vous donnant des clés pour résoudre les problèmes et des moyens de réparer les erreurs.
Et aussi parce que ça servira sûrement à mon moi du futur et à certains d’entre vous qui travaillent comme des margoulins ou qui ont malencontreusement ripé sur leur clavier et cherchent des solutions à leurs problèmes.
Sans plus attendre, amorçons le grand tour des conneries à ne pas faire avec Terraform. Pas forcément dans l’ordre, ni avec les trucs les plus probables en premier.

Couper la connexion alors qu’un apply / destroy est en cours
Excuses “valables” :
- J’ai subi une déconnexion alors que j’étais en SSH sur une machine distante.
- J’ai eu une coupure imprévue de courant ou d’Internet.
Excuses de merde :
- J’ai approuvé, mais j’étais pressé, je n’ai pas vérifié que j’allais péter des trucs, alors j’ai coupé à l’arrache pour limiter les dégâts.
- J’ai un alias avec auto-approve parce que ça me gonfle de taper “yes” à chaque fois. J’apply et destroy comme un Viking : je mets le feu au bateau et je pille les villages.
- Oh bah, merde hein
Un grand classique quand on travaille comme un sauvage. “Boh, zut ! Je ne voulais pas apply / destroy finalement, je coupe rapido ni vu ni connu.” Je refais un apply un peu plus tard et, paf ! État inconsistant de mon Terraform.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
│ Error: Error creating service account: googleapi: Error 409: Service account service-account-id already ex [...]
│ Details:
│ [
│ {
│ "@type": "type.googleapis.com/google.rpc.ResourceInfo",
│ "resourceName": "projects/tf-on-casse-tout/serviceAccounts/[email protected][...]
│ }
│ ]
│ , alreadyExists
│
│ with google_service_account.default,
│ on main.tf line 15, in resource "google_service_account" "default":
│ 15: resource "google_service_account" "default" {
│
╵
╷
│ Error: googleapi: Error 409: Already exists: projects/tf-on-casse-tout/locations/us-central1/clusters/my-[...]
│ Details:
│ [
│ {
│ "@type": "type.googleapis.com/google.rpc.RequestInfo",
│ "requestId": "0xdda6524170e3676c"
│ }
│ ]
│ , alreadyExists
│
│ with google_container_cluster.primary,
│ on main.tf line 20, in resource "google_container_cluster" "primary":
│ 20: resource "google_container_cluster" "primary" {
|
Dans ce cas précis, j’ai activé des API et créé un cluster GKE. Malheureusement, j’ai un conflit entre le compte de service et le cluster GKE parce que l’état de Terraform ne s’est pas mis à jour avant que je coupe la connexion comme un bon sauvageon. Résultat : Terraform essaye lamentablement de créer des ressources qui existent déjà.

Réparation rapide
Solution brutale et rapide :
- Supprimer chaque ressource en conflit manuellement, puis relancer un apply.
Solution conservatrice et un peu plus longue :
- Faire des import pour chaque ressource en conflit manuellement afin de remettre l’état de Terraform en cohérence avec l’infra.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
google_service_account.default: Importing from ID "[email protected]"
google_service_account.default: Import prepared!
Prepared google_service_account for import
google_service_account.default: Refreshing state... [id=projects/tf-on-casse-tout/serviceAccoun[...]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
~/projects/terraform_on_casse_tout/tf_coupe_connection » terraform import google_container_cluster.primary
https://container.googleapis.com/v1/projects/tf-on-casse-tout/locations/us-central1/clusters/my-gke-cluster
google_container_cluster.primary: Importing from ID "https://container.googleapis.com/v1/projects
/tf-on-casse-tout/locations/us-central1/clusters/my-gke-cluster"...
google_container_cluster.primary: Import prepared!
Prepared google_container_cluster for import
google_container_cluster.primary: Refreshing state... [id=projects/tf-on-casse-tout/locations/us-central1/[...]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
terraform import google_container_node_pool.primary_preemptible_nodes
https://container.googleapis.com/v1/projects/tf-on-casse-tout/locations/us-central1/clusters/my-gke-cluster/[...]
google_container_node_pool.primary_preemptible_nodes: Importing from ID "https://container.googleapis.com/v1
/projects/tf-on-casse-tout/locations/us-central1/clusters/my-gke-cluster/nodePools/default-pool"...
google_container_node_pool.primary_preemptible_nodes: Import prepared!
Prepared google_container_node_pool for import
google_container_node_pool.primary_preemptible_nodes:
Refreshing state... [id=projects/tf-on-casse-tout/locations/us-central1/clusters/my-gke-cluster/nodePools/[...]]
Import successful!
|
Bon, là, j’ai pris un cluster GKE. Il va m’embêter parce que le node pool par défaut a été supprimé. Il créera un nouveau node pool lors de l’apply.
À vous de voir en fonction de la complexité des ressources et de l’impact de la reconstruction.
- Utiliser un CI/CD chargé de faire les apply et destroy, plutôt que d’exécuter ces commandes directement depuis la machine de dev.
- Assumer qu’on est imparfait et arrêter d’utiliser des alias dangereux sur des environnements de production.
- Utiliser tmux ou un équivalent pour persister la connexion SSH, même en cas de coupure (à coupler avec mosh pour un shell capable de reprendre la connexion).
- Bien examiner le plan avant d’exécuter une action pour éviter de paniquer et de tout couper à l’arrache.
- Prendre une pause pour avoir les idées claires avant d’agir sur des apply importants.
J’ai un lock sur le bucket et il ne part pas
Excuses “valables” :
- Mon processus Terraform a été coupé accidentellement et le lock n’a jamais été relâché.
Excuses de merde :
- Je n’ai pas communiqué avec mes collègues, et je ne savais pas qu’ils travaillaient sur le même Terraform que moi.
- Je n’ai pas eu le temps de chercher.
- Je n’ai pas envie de chercher.
- “Mais ça marchait hier !”
Alors, tu es content de ton code, tu veux balancer ton apply avant d’aller manger, et paf ! Tu te prends un lock dans les dents !
Bon, tu vas manger, un peu chafouin, tu prends ton café, deux heures plus tard tu relances ton Terraform… et paf, un lock !
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
│ Error: Error acquiring the state lock
│
│ Error message: writing "gs://tf-on-casse-tout/coupe-connection/default.tflock"
failed: googleapi: Error 412: At least one of the pre-conditions you specified did not hold.,
│ conditionNotMet
│ Lock Info:
│ ID: 1737715198197146
│ Path: gs://tf-on-casse-tout/coupe-connection/default.tflock
│ Operation: OperationTypeApply
│ Who: punkachien@machinededevnulle
│ Version: 1.5.2
│ Created: 2025-01-24 10:32:44.638750707 +0000 UTC
│ Info:
│
│
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.
|
Manifestement, c’est monsieur Punkachien sur la machine machinededevnulle qui tient le lock. Ah ça tombe bien, c’est moi.
J’ai encore dû lancer un Terraform à l’arrache en background, qui a mal fini, et le lock n’a jamais été relâché !

Réparation rapide
Vérifier que personne n’est en train de faire un apply.
Supprimer le lock avec gsutil rm et réparer manuellement les dégâts :
1
|
gsutil rm gs://tf-on-casse-tout/coupe-connection/default.tflock
|
solution plus adapté à terraform
- Communiquer lorsque vous travaillez à plusieurs.
- Éviter de lancer des Terraform en background.
- Réduire le nombre de ressources par dossier Terraform pour être plus modulaire et permettre un travail plus efficace en équipe.
Excuses “valables” :
- Je viens d’arriver en astreinte et je ne savais pas que du code TF pilotait l’infra.
- J’ai enchaîné plusieurs nuits d’astreinte compliquées, c’est juste un oubli.
Excuses de merde :
- J’avais apéro, j’ai plié l’incident vite fait.
- Roh, on s’en fout, le TF ne change plus de toute façon.
- C’est pas si grave, il faut juste faire attention.
- Le périmètre d’intervention est flou, j’ai jawadé l’incident (je rends service, monsieur).
- Hey, on a des backups, hein !

Mettons que j’ai créé une instance Compute Engine avec Terraform, mais au bout d’un moment, le disque de données attaché déborde.
En astreinte, on resize ce disque très vite manuellement et on augmente la taille de la partition système.
Dommage, la modification n’a pas été répercutée dans le code Terraform…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
google_compute_disk.seconddisk: Refreshing state... [id=projects/tf-on-casse-tout/zones/us-central1-a[...]
google_compute_instance.default: Refreshing state... [id=projects/tf-on-casse-tout/zones/us-central1-a[...]
google_compute_attached_disk.default: Refreshing state... [id=projects/tf-on-casse-tout/zones/us-central1-a[...]
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement
Terraform will perform the following actions:
# google_compute_attached_disk.default must be replaced
-/+ resource "google_compute_attached_disk" "default" {
~ device_name = "persistent-disk-2" -> (known after apply)
~ disk = "projects/tf-on-casse-tout/zones/us-central1-a/disks/seconddisk"
# forces replacement -> (known after apply) # forces replacement
~ id = "projects/tf-on-casse-tout/zones/us-central1-a/instances/my-instance/
seconddisk" -> (known after apply)
# (4 unchanged attributes hidden)
}
# google_compute_disk.seconddisk must be replaced
-/+ resource "google_compute_disk" "seconddisk" {
+ access_mode = (known after apply)
~ creation_timestamp = "2025-01-24T07:54:02.036-08:00" -> (known after apply)
~ disk_id = "4426197581000739190" -> (known after apply)
~ enable_confidential_compute = false -> (known after apply)
~ id = "projects/tf-on-casse-tout/zones/us-central1-a
/disks/seconddisk" -> (known after apply)
~ label_fingerprint = "vezUS-42LLM=" -> (known after apply)
- labels = {} -> null
~ last_attach_timestamp = "2025-01-24T07:54:16.057-08:00" -> (known after apply)
+ last_detach_timestamp = (known after apply)
~ licenses = [] -> (known after apply)
name = "seconddisk"
~ physical_block_size_bytes = 4096 -> (known after apply)
~ provisioned_iops = 0 -> (known after apply)
~ provisioned_throughput = 0 -> (known after apply)
~ self_link = "https://www.googleapis.com/compute/v1/projects/
tf-on-casse-tout/zones/us-central1-a/disks/seconddisk" -> (known after apply)
~ size = 11 -> 10 # forces replacement
+ source_disk_id = (known after apply)
+ source_image_id = (known after apply)
+ source_snapshot_id = (known after apply)
~ users = [
- "https://www.googleapis.com/compute/v1/projects/
tf-on-casse-tout/zones/us-central1-a/instances/my-instance",
] -> (known after apply)
# (5 unchanged attributes hidden)
}
# google_compute_instance.default will be updated in-place
~ resource "google_compute_instance" "default" {
id = "projects/tf-on-casse-tout
/zones/us-central1-a/instances/my-instance"
name = "my-instance"
tags = [
"bar",
"foo",
]
# (20 unchanged attributes hidden)
- attached_disk {
- device_name = "persistent-disk-2" -> null
- mode = "READ_WRITE" -> null
- source = "https://www.googleapis.com/compute/v1/projects/tf-on-casse-tout
/zones/us-central1-a/disks/seconddisk" -> null
}
# (6 unchanged blocks hidden)
}
Plan: 2 to add, 1 to change, 2 to destroy.
|
Si un collègue passe ensuite et fait un apply sans faire attention, c’est la destruction de disque assurée !
Réparation rapide
solution bof
Faire en sorte que le code match les changements manuels, faire très attention au directive “replace” dans le plan
dans mon exemple, matcher la taille de disque data permet de résoudre le problème.
solution idéaliste (mais pas toujours possible en prod)
détruire et reconstruire les ressources qui on changé
- Encadrer fortement les modifications manuelles (opérations d’urgence seulement)
- Donner des accès read only pour les taches courantes des utilisateurs
Mauvaise gestion du renommage de ressource
Excuses “valables” :
- Je bosse comme un crassous mais c’est du dev (par contre je met au propre ensuite en prod )
Excuses de merde :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# google_service_account.default will be destroyed
# (because google_service_account.default is not in configuration)
- resource "google_service_account" "default" {
- account_id = "my-custom-sa" -> null
- disabled = false -> null
- display_name = "Custom SA for VM Instance" -> null
- email = "[email protected]" -> null
- id = "projects/tf-on-casse-tout/serviceAccounts/
[email protected]" -> null
- member = "serviceAccount:[email protected]" -> null
- name = "projects/tf-on-casse-tout/serviceAccounts/
[email protected]" -> null
- project = "tf-on-casse-tout" -> null
- unique_id = "106429948722620898065" -> null
}
# google_service_account.my-sa will be created
+ resource "google_service_account" "my-sa" {
+ account_id = "my-custom-sa"
+ disabled = false
+ display_name = "Custom SA for VM Instance"
+ email = "[email protected]"
+ id = (known after apply)
+ member = "serviceAccount:[email protected]"
+ name = (known after apply)
+ project = "tf-on-casse-tout"
+ unique_id = (known after apply)
}
Plan: 1 to add, 0 to change, 1 to destroy.
|
J’ai copié une ressource dans l’exemple de la doc, j’ai fait un apply, j’aimerais renommer ma ressource mais tf me propose un recréation.
en fait le problème viens du fait de terrafrom se base sur le nom de la ressource pour traquer les changements.
y’a une commande qui permet de faire comprendre à terraform que la ressource à été renommée.

réparation rapide
solution bof:
j’men fous, ma ressource n’est pas importante, destruction et recreation c’est ok, balek
solution propre
j’utilise la commande terraform state mv qui permet de refleter les changements de nom de la ressource.
pour mon exemple de compte de service
1
2
3
|
terraform state mv google_service_account.default google_service_account.my-sa
Move "google_service_account.default" to "google_service_account.my-sa"
Successfully moved 1 object(s).
|
terraform plan pour checker l’état de mon state et terminé bonsoir
## comment réduire ce genre de problèmes
renommer les ressources proprement avant de les apply pour eviter d’être embeter plus tard.
bien checker le code avant le passage en prod pour eviter de faire des state mv sur des ressources importantes en prod.
ma ressource existe déja, erreur 419
Excuses “valables” :
- je travaille sur des des ressources immuable et j’ai cassé mon state accidentellement
- j’ai migré mon state et ça s’est mal passé
- j’essaye de reproduire une ressource complexe provisionnée à la main dans mon code terraform
- je convertis mes ressources créees manuellement en infra as code
Excuses de merde :
- mon état est inconsistant parceque j’ai fait des modif à la fois manuellement et avec terraform sans reflexion
Bon j’essaye de faire des ressources un peu relou en terraform mais facile en clickou.
Je veux bien faire et exporter ça dans mon code tf. Donc je vais y aller au talent et importer la ressource pour faire matcher le code associé.
Je viens de créer un Cloudrun avec l’image de base et deux trois variables en faisant mon meilleur clickou.
Je balance le code d’exemple de la ressource et je match le nom de mon cloudrun et la localisation que j’ai sur GCP.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
resource "google_cloud_run_v2_service" "default" {
name = "hello"
location = "europe-west1"
deletion_protection = false
ingress = "INGRESS_TRAFFIC_ALL"
template {
containers {
image = "us-docker.pkg.dev/cloudrun/container/hello"
resources {
limits = {
cpu = "2"
memory = "1024Mi"
}
}
}
}
}
|
J’apply ça et
1
2
3
4
5
|
│ Error: Error creating Service: googleapi: Error 409: Resource 'hello' already exists.
│
│ with google_cloud_run_v2_service.default,
│ on main.tf line 1, in resource "google_cloud_run_v2_service" "default":
│ 1: resource "google_cloud_run_v2_service" "default" {
|
Parfait, c’est exactement ce que je recherche, c’est le strict minimum qu’il me faut pour importer ma ressource.
je balance ma commande d’import sans lire la doc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
tf import google_cloud_run_v2_service.default hello
google_cloud_run_v2_service.default: Importing from ID "hello"...
╷
│ Error: Import id "hello" doesn't match any of the accepted formats:
[^projects/(?P<project>[^/]+)/locations/(?P<location>[^/]+)/services/(?P<name>[^/]+)$
^(?P<project>[^/]+)/(?P<location>[^/]+)/(?P<name>[^/]+)$ ^(?P<location>[^/]+)/(?P<name>[^/]+)$]
│
le message d'erreur me donne le format attendu
tf import google_cloud_run_v2_service.default projects/tf-on-casse-tout/locations/europe-west1/services/hello
google_cloud_run_v2_service.default: Importing from ID
"projects/tf-on-casse-tout/locations/europe-west1/services/hello"...
google_cloud_run_v2_service.default: Import prepared!
Prepared google_cloud_run_v2_service for import
google_cloud_run_v2_service.default: Refreshing state...
[id=projects/tf-on-casse-tout/locations/europe-west1/services/hello]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
|
et à partir de là, le plan me donne le manquant pour matcher exactement ma ressource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
# google_cloud_run_v2_service.default will be updated in-place
~ resource "google_cloud_run_v2_service" "default" {
- client = "cloud-console" -> null
~ deletion_protection = true -> false
id = "projects/tf-on-casse-tout/locations/europe-west1/services/hello"
name = "hello"
# (27 unchanged attributes hidden)
~ template {
# (6 unchanged attributes hidden)
~ containers {
- name = "hello-1" -> null
# (4 unchanged attributes hidden)
- env {
- name = "ENV" -> null
- value = "devprod" -> null
}
- env {
- name = "QUOIQUOIQUOI" -> null
- value = "quoicoubeh" -> null
}
- env {
- name = "SUPER" -> null
- value = "cool" -> null
}
~ resources {
- cpu_idle = true -> null
~ limits = {
~ "cpu" = "1000m" -> "2"
~ "memory" = "512Mi" -> "1024Mi"
}
- startup_cpu_boost = true -> null
}
# (2 unchanged blocks hidden)
}
# (1 unchanged block hidden)
}
# (1 unchanged block hidden)
}
|
du coup je peux modifier mon code tf en copier coller, glue chaude et ça donne un truc comme ça
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
resource "google_cloud_run_v2_service" "default" {
name = "hello"
location = "europe-west1"
deletion_protection = true
ingress = "INGRESS_TRAFFIC_ALL"
template {
containers {
image = "us-docker.pkg.dev/cloudrun/container/hello"
env {
name = "ENV"
value = "devprod"
}
env {
name = "QUOIQUOIQUOI"
value = "quoicoubeh"
}
env {
name = "SUPER"
value = "cool"
}
resources {
cpu_idle = true
limits = {
"cpu" = "1000m"
"memory" = "512Mi"
}
startup_cpu_boost = true
}
}
}
}
|
Il me reste un peu quelques conneries résiduelles mais si je fais un apply, je managed désormais ma ressource en terraform.
Pour être bien propre, je peux faire destroy et apply après avoir apply le flag delete protection à false et j’aurai une ressource terraform 100 identique à mon code.
ça peux être utile dans le cas de ressource avec des API un peu relou qui on besoin de chaines de caractère avec des escapes string, par exemple les customs metrique ou les alertes sur les logs.
Conclusion
On peux faire un sacret paquets de conneries avec Terraform si on ne fait pas attention, c’est un outil assez sensible, mais une fois qu’on à bien compris sont fonctionnement, on peut rattraper plus simplement les erreurs et moins en avoir peur.
