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.

Meme des Simpson illustrant la différence entre ‘terraform plan’ (propre et bien organisé) et ‘terraform apply’ (chaotique et raté).

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à.

La citée de la peur, chérie, ça vas coupé.

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.

Comment réduire ce genre de problèmes

  • 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é !

cadena master lock , ouvert par lock picking lawyer

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

1
terraform force-unlock

Comment réduire ce genre de problèmes

  • 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.

Changer les ressources manuellement sans adapter le code Terraform

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 !

camping 2 , apéro pastis

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é

Comment réduire ce genre de problèmes

  • 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 :

  • Je ne sais pas faire
 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.

American Horror story, asylium, the name game

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.

Jennifer lawrence avec sa doublure sur le tournage d’x-men

 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.

patrick bob l’éponge, mom come pick me up , i’am scared, some ugly terraform code is following me