Aller au contenu

Le langage HCL

100 Comprendre ⏱ 1 h terraformopentofuhcl

À la fin, vous saurez

  • Reconnaître dans un fichier .tf les blocs, leurs étiquettes, les arguments et les blocs imbriqués
  • Choisir le bon type (string, number, bool, list, set, map, object, tuple) et prévoir les conversions automatiques
  • Faire référence à l'attribut d'une autre ressource, et comprendre la dépendance que cela crée
  • Construire des valeurs avec l'interpolation, les heredocs, les conditions et les expressions for
  • Utiliser les fonctions intégrées courantes, dont cidrsubnet, templatefile et jsonencode
  • Expérimenter avec terraform console, et formater avec terraform fmt

Prérequis

Testé avec opentofu 1.13.1 random 3.9.1 terraform 1.16.1 , vérifié le 5 octobre 2026

Pourquoi

Les deux premières leçons ont utilisé du HCL sans l'expliquer : des accolades, des signes égal, une expression ${random_pet.suffixe.id} au milieu d'une chaîne. Cela suffit pour recopier un exemple. Cela ne suffit plus le jour où il faut calculer quatre sous-réseaux à partir d'un bloc, générer le cloud-init de chaque instance à partir d'un modèle, ou comprendre pourquoi une liste de zones contient soudain la chaîne "true".

HCL (HashiCorp Configuration Language) a été conçu pour être lu par des humains autant que par une machine : pas de point-virgule, peu de ponctuation, une structure visible à l'indentation. Mais c'est un vrai langage, avec des types, des conversions implicites et des fonctions, et ses pièges sont ceux de tout langage. Cette leçon en fait le tour, avec terraform console pour vérifier chaque affirmation.

Tout ce qui suit vaut pour OpenTofu, qui a hérité du même langage.

Les concepts

Blocs, étiquettes et arguments

La spécification de HCL décrit un fichier comme un corps (body) qui contient deux sortes d'éléments :

  • des attributs (la documentation de Terraform dit arguments) : un nom, un signe égal, une valeur. Un nom n'apparaît qu'une fois par corps ;
  • des blocs : un type, zéro ou plusieurs étiquettes entre guillemets, puis un corps entre accolades, qui contient à son tour attributs et blocs.
resource "scaleway_instance_server" "sig_app_1" {   # bloc de type resource, deux étiquettes
  name  = "sig-app-1"                               # argument
  type  = "PRO2-XXS"
  zone  = "fr-par-1"

  root_volume {                                     # bloc imbriqué, sans étiquette
    size_in_gb = 20
  }
}

Le type de bloc dit à Terraform ce que c'est : terraform, provider, resource, data, variable, output, locals, module, import, moved. Le nombre d'étiquettes dépend du type : resource en prend deux (le type de ressource et le nom local), variable une, locals aucune.

Les blocs imbriqués (comme root_volume) sont définis par le fournisseur, dans le schéma de chaque ressource. Certains peuvent se répéter (plusieurs règles dans un groupe de sécurité), d'autres non. La différence entre un argument de type objet (etiquettes = { ... }, avec un signe égal) et un bloc imbriqué (root_volume { ... }, sans signe égal) n'est pas une affaire de goût : c'est le schéma du fournisseur qui décide, et la documentation de chaque ressource le précise.

Les commentaires

# commentaire sur une ligne (la forme recommandée)
// commentaire sur une ligne, accepté
/* commentaire
   sur plusieurs lignes */

Le guide de style de HashiCorp recommande #, et demande d'écrire un code assez clair pour que les commentaires ne servent qu'à expliquer ce qui ne se voit pas : une contrainte du fournisseur, une décision d'architecture, un ticket.

Les types

La documentation de Terraform distingue trois types primitifs et des types complexes :

TypeExempleRemarque
string"fr-par-1"du texte Unicode, entre guillemets doubles
number20, 0.5entiers et décimaux, un seul type
booltrue, falsesans guillemets
list(...)["fr-par-1", "fr-par-2"]une suite ordonnée, d'éléments du même type, indexée à partir de 0
set(...)toset(["fr-par-1", "fr-par-2"])des éléments uniques, sans ordre ni index
map(...){ app = "signalements", env = "prod" }des valeurs du même type, repérées par des clés
object({...}){ nom = "sig-app-1", vcpu = 2 }des attributs nommés, chacun avec son type
tuple([...])["a", 1]une suite dont chaque position a son type

Et une valeur spéciale, null, qui représente, selon la documentation, « l'absence ou l'omission ». Donner null à un argument revient à ne pas l'écrire : le fournisseur applique sa valeur par défaut, ou signale une erreur si l'argument est obligatoire.

La différence entre list et tuple, ou map et object, se voit dans la console. Un littéral entre crochets avec des éléments de types différents est un tuple :

> type(["a", 1])
tuple([
    string,
    number,
])
> type({a = 1, b = "x"})
object({
    a: number,
    b: string,
})

Vous n'aurez presque jamais à écrire tuple ou object vous-même : Terraform convertit ces littéraux vers le type attendu (une list(string), une map(string)) quand c'est possible. C'est pratique, et c'est la source du piège suivant.

Les conversions automatiques

Terraform convertit sans prévenir entre string, number et bool quand le contexte l'exige : true devient "true", 15 devient "15", et l'inverse si la chaîne s'y prête. Dans la console :

> 1 + "2"
3
> "10" == 10
false

L'addition a converti la chaîne en nombre. L'égalité, elle, ne convertit pas : une chaîne n'est jamais égale à un nombre. La documentation le signale comme la seule exception, et c'est un piège réel dans les conditions.

Plus sournois, une liste déclarée list(string) accepte des éléments d'un autre type et les convertit. Avec une variable (leçon 5) déclarée ainsi :

variable "zones" {
  type    = list(string)
  default = ["fr-par-1", true]
}

terraform validate répond Success!, et la console montre ce que vaut réellement la variable :

> var.zones
tolist([
  "fr-par-1",
  "true",
])

Une faute de frappe devient une zone nommée "true", que seul le fournisseur refusera, au moment du plan ou, pire, de l'apply. Les variables avec validation (leçon 5) servent à attraper ce genre d'erreur tôt.

Les références

Une expression peut désigner une autre valeur de la configuration :

FormeDésigneExemple
<type>.<nom>.<attribut>un attribut d'une ressourcescaleway_instance_ip.sig_app_1.id
data.<type>.<nom>.<attribut>un attribut d'une source de donnéesdata.scaleway_account_project.signalements.id (leçon 4)
var.<nom>une variable d'entréevar.zones (leçon 5)
local.<nom>une valeur localelocal.etiquettes (leçon 5)
module.<nom>.<sortie>une sortie de modulecours Terraform avancé
path.modulele répertoire du module courant"${path.module}/signalements.yaml"

Une référence ne se contente pas de copier une valeur : elle crée une dépendance. En écrivant ip_id = scaleway_instance_ip.sig_app_1.id, vous dites à Terraform que l'instance a besoin de l'IP, donc qu'il doit créer l'IP d'abord et la détruire après. C'est ce graphe de références qui ordonne tout le travail (leçon 8), et c'est pourquoi l'ordre des blocs dans les fichiers n'a aucune importance.

Une faute dans une référence est détectée tôt, avec une suggestion :

Error: Unsupported attribute

  on erreur.tf line 6, in output "nom":
   6:   value = random_pet.nom.idd

This object has no argument, nested block, or exported attribute named "idd".
Did you mean "id"?

Chaînes, interpolation et heredocs

Dans une chaîne entre guillemets doubles, ${ ... } insère le résultat d'une expression :

> "Zone : ${upper("fr-par-1")}"
"Zone : FR-PAR-1"

Une chaîne sur plusieurs lignes s'écrit en heredoc, une syntaxe héritée des shells : <<EOT, le texte, puis EOT seul sur sa ligne (le mot est libre, EOT est l'usage). La forme <<-EOT, avec un tiret, retire l'indentation commune des lignes, ce qui permet d'indenter le texte avec le code :

output "motd" {
  value = <<-EOT
    Bienvenue sur sig-app-1.
      Zone : fr-par-1
    EOT
}

donne, après apply :

motd = <<EOT
Bienvenue sur sig-app-1.
  Zone : fr-par-1

EOT

La première ligne a perdu ses quatre espaces, la seconde n'a gardé que les deux de plus. Sans le tiret, les quatre espaces auraient été conservés, ce qui suffit à rendre invalide un fichier YAML ou un script.

Opérateurs et conditions

Les opérateurs sont ceux de la plupart des langages : arithmétiques (+, -, *, /, %), comparaisons (==, !=, <, >=...), logiques (&&, ||, !). L'expression conditionnelle a la forme condition ? si_vrai : si_faux :

> true ? "prod" : "preprod"
"prod"

Les deux branches doivent avoir un type compatible : var.prod ? 2 : "un" est une erreur. La leçon 9 en fait un usage intensif, avec count et for_each.

Les expressions for

Une expression for transforme une collection en une autre, comme une compréhension de liste en Python :

> [for i in range(1, 3) : format("sig-app-%d", i)]
[
  "sig-app-1",
  "sig-app-2",
]

Entre crochets, elle produit une liste (un tuple, converti au besoin) ; entre accolades, avec =>, une map : {for z in ["fr-par-1", "fr-par-2"] : z => "PRO2-XXS"}. Une clause if filtre les éléments. On la retrouve à la leçon 9.

Les fonctions intégrées

HCL n'a pas de fonctions définies par l'utilisateur (Terraform et OpenTofu acceptent en revanche des fonctions fournies par les fournisseurs). Il en a une centaine d'intégrées, rangées par familles dans la documentation : chaînes, collections, encodage, fichiers, dates, réseau, conversion de types. Celles qui reviennent le plus souvent :

> format("sig-app-%d", 1)
"sig-app-1"
> join(", ", ["fr-par-1", "fr-par-2"])
"fr-par-1, fr-par-2"
> merge({app = "signalements"}, {env = "prod"}, {env = "preprod"})
{
  "app" = "signalements"
  "env" = "preprod"
}
> lookup({"fr-par-1" = "PRO2-XXS"}, "fr-par-2", "DEV1-S")
"DEV1-S"
> toset(["fr-par-2", "fr-par-1", "fr-par-2"])
toset([
  "fr-par-1",
  "fr-par-2",
])
> coalesce(null, "defaut")
"defaut"
  • merge fusionne des maps ; la dernière l'emporte en cas de clé commune : env vaut preprod. C'est le comportement voulu pour des étiquettes par défaut surchargées par environnement.
  • lookup lit une clé avec une valeur de repli si elle manque.
  • toset retire les doublons et trie : l'ordre de départ est perdu.
  • coalesce renvoie la première valeur non nulle.

Les fonctions réseau méritent une mention particulière, parce qu'elles calculent un plan d'adressage comme le cours Le modèle TCP/IP l'a fait à la main. cidrsubnet(préfixe, bits_ajoutés, numéro) découpe un bloc :

> cidrsubnet("172.16.20.0/22", 2, 0)
"172.16.20.0/24"
> cidrsubnet("172.16.20.0/22", 2, 3)
"172.16.23.0/24"
> cidrhost("172.16.20.0/24", 11)
"172.16.20.11"

Emprunter deux bits à un /22 donne quatre /24, numérotés de 0 à 3 ; cidrhost donne la onzième adresse d'un bloc, celle de sig-app-1 dans le scénario. Un plan d'adressage écrit ainsi ne contient plus d'adresse recopiée à la main.

Les fonctions d'encodage produisent des formats que d'autres outils lisent :

> jsonencode({nom = "sig-app-1", zones = ["fr-par-1"]})
"{\"nom\":\"sig-app-1\",\"zones\":[\"fr-par-1\"]}"
> yamlencode({nom = "sig-app-1", ports = [8000]})
<<EOT
"nom": "sig-app-1"
"ports":
- 8000

EOT

jsonencode sert à écrire une politique de bucket ou un document de configuration sans risquer une virgule manquante ; yamlencode produit un YAML valide, avec des clés entre guillemets, que l'on préfère parfois à un modèle écrit à la main.

Enfin, les fonctions de fichiers lisent depuis le répertoire du module : file(chemin) renvoie le contenu brut, templatefile(chemin, variables) le contenu d'un modèle dans lequel les ${ ... } sont remplacés. C'est ainsi que l'on génère le cloud-init de chaque instance à partir d'un seul fichier. Avec un modèle tpl/cloud-init.yaml.tftpl :

#cloud-config
hostname: ${nom}
write_files:
  - path: /etc/signalements/env
    content: |
      APP_VERSION=${version}

l'expression templatefile("${path.module}/tpl/cloud-init.yaml.tftpl", { nom = "sig-app-1", version = "1.2.0" }) produit :

#cloud-config
hostname: sig-app-1
write_files:
  - path: /etc/signalements/env
    content: |
      APP_VERSION=1.2.0

L'extension .tftpl est la convention recommandée pour les modèles ; elle n'a pas d'effet sur le traitement.

En pratique

Expérimenter avec terraform console

terraform console ouvre une invite où l'on tape des expressions, évaluées dans le contexte du répertoire courant : les variables, les valeurs locales et l'état y sont accessibles. C'est l'outil pour comprendre une fonction avant de l'écrire dans un fichier, ou pour inspecter la valeur d'un attribut après un apply. Toutes les sorties de cette leçon en viennent.

$ cd ~/signalements-iac
$ terraform console
> cidrsubnet("172.16.20.0/22", 2, 1)
"172.16.21.0/24"
> exit

On peut aussi lui passer une expression sur l'entrée standard, ce qui est utile dans un script :

$ echo 'cidrhost("172.16.20.0/24", 12)' | terraform console
"172.16.20.12"

Une erreur s'affiche comme dans un plan :

> 1 + null
│ Error: Operation failed
│ 
│   on <console-input> line 1:
│   (source code not available)
│ 
│ Error during operation: argument must not be null.

La console ne modifie jamais rien. Elle lit l'état, et pose un verrou dessus quand il est partagé (leçon 7) : ne la laissez pas ouverte pendant qu'un collègue doit appliquer.

Écrire le plan d'adressage de Signalements

Mettons ensemble types, fonctions et expressions. Dans un fichier adressage.tf :

locals {
  bloc_vpc = "172.16.20.0/22"

  # Quatre /24 : applications, bases, administration, réserve.
  sous_reseaux = {
    applications   = cidrsubnet(local.bloc_vpc, 2, 0)
    bases          = cidrsubnet(local.bloc_vpc, 2, 1)
    administration = cidrsubnet(local.bloc_vpc, 2, 2)
    reserve        = cidrsubnet(local.bloc_vpc, 2, 3)
  }

  instances = {
    for i in range(1, 3) : format("sig-app-%d", i) => cidrhost(local.sous_reseaux.applications, 10 + i)
  }
}

output "plan_adressage" {
  value = local.sous_reseaux
}

output "adresses_instances" {
  value = local.instances
}

Un terraform apply (qui ne crée rien : il n'y a pas de ressource) affiche les sorties calculées : les quatre /24 de 172.16.20.0/24 à 172.16.23.0/24, puis sig-app-1 = "172.16.20.11" et sig-app-2 = "172.16.20.12". Changez bloc_vpc en 10.42.0.0/22 : tout le plan suit. La leçon 10 utilisera ces valeurs pour créer les réseaux privés.

Formater avec terraform fmt

terraform fmt réécrit les fichiers .tf du répertoire dans le style standard. Sur ce fichier mal aligné :

locals {
  zones = ["fr-par-1","fr-par-2"]
  etiquettes = {
    app = "signalements"
      env= "prod"
  }
}

terraform fmt -diff montre et applique la correction :

main.tf
--- old/main.tf
+++ new/main.tf
@@ -1,7 +1,7 @@
 locals {
-  zones = ["fr-par-1","fr-par-2"]
+  zones = ["fr-par-1", "fr-par-2"]
   etiquettes = {
     app = "signalements"
-      env= "prod"
+    env = "prod"
   }
 }

terraform fmt -check ne modifie rien et sort avec un code non nul si un fichier n'est pas formaté : c'est la commande à mettre dans la CI (leçon 12). -recursive descend dans les sous-répertoires.

Les conventions du guide de style

fmt règle l'indentation (deux espaces par niveau) et l'alignement des signes égal. Le reste relève de conventions que le guide de style de HashiCorp énonce et que les relecteurs vérifient :

  • Noms : des noms communs, en minuscules, avec des tirets bas (sig_app_1, pn_signalements), sans répéter le type de ressource (scaleway_vpc_private_network.signalements, pas scaleway_vpc_private_network.private_network_signalements). Les tirets sont permis dans les noms locaux, mais incommodes dans les références : préférez les tirets bas.
  • Fichiers : terraform.tf pour le bloc terraform, providers.tf, variables.tf et outputs.tf (blocs par ordre alphabétique), locals.tf, main.tf pour les ressources ; quand la configuration grandit, un fichier par domaine (reseau.tf, donnees.tf, calcul.tf).
  • Ordre dans une ressource : count ou for_each en premier, puis les arguments, puis les blocs imbriqués, puis lifecycle, et depends_on en dernier, chaque groupe séparé par une ligne vide.

Pour Terraform, l'ordre des fichiers et des blocs n'a aucune importance : il lit tout le répertoire et reconstruit le graphe. Ces conventions servent aux humains, qui, eux, lisent dans l'ordre.

Sous le capot

Une seule grammaire, deux syntaxes. La spécification de HCL définit un modèle d'information (corps, attributs, blocs) indépendant de la syntaxe. Terraform accepte donc aussi des fichiers .tf.json, où la même configuration est écrite en JSON. Personne ne les écrit à la main ; ils servent aux programmes qui génèrent une configuration, parce qu'un générateur de JSON ne se trompe pas d'accolade. OpenTofu ajoute les extensions .tofu et .tofu.json, lues à la place d'un fichier .tf du même nom.

L'évaluation est paresseuse et typée. Terraform ne calcule pas les expressions dans l'ordre du fichier : il construit le graphe des références, puis évalue chaque nœud quand ses dépendances sont connues. Une valeur qui ne sera connue qu'après la création d'une ressource reste inconnue pendant le plan ((known after apply)), et toute expression qui en dépend l'est aussi. C'est pourquoi certaines constructions (un count qui dépend d'un identifiant pas encore créé) sont refusées au plan : Terraform doit savoir combien d'objets créer avant de créer quoi que ce soit (leçon 9).

Les fonctions sont pures. À part celles qui lisent des fichiers ou l'horloge, une fonction intégrée rend toujours le même résultat pour les mêmes arguments. C'est ce qui permet à Terraform de recalculer un plan et d'obtenir le même résultat. timestamp() ou uuid(), qui changent à chaque appel, provoquent une modification à chaque plan si on les met dans un argument de ressource : on les évite, ou on fige leur valeur avec ignore_changes (leçon 8).

Pièges courants

Les conversions silencieuses. "10" == 10 est faux ; une list(string) accepte true et en fait "true". Contrôlez les types aux frontières, avec des variables typées et validées (leçon 5).

Le heredoc sans tiret. <<EOT conserve l'indentation du code dans le texte produit. Pour un cloud-init en YAML, c'est un fichier invalide, et une instance qui démarre sans configuration. Utilisez <<-EOT, ou templatefile avec un fichier séparé.

merge dans le mauvais ordre. La dernière map l'emporte. merge(local.etiquettes_environnement, local.etiquettes_par_defaut) fait gagner les valeurs par défaut, l'inverse de ce que l'on voulait.

toset qui change l'ordre. Un ensemble est trié et sans doublon. Si l'ordre compte (une liste de serveurs DNS par priorité), gardez une liste.

Un argument écrit comme un bloc, ou l'inverse. root_volume = { size_in_gb = 20 } au lieu de root_volume { size_in_gb = 20 } donne une erreur Unsupported argument ou An argument named ... is not expected here. Did you mean to define a block. La documentation de la ressource dit lequel des deux est attendu.

null dans une opération. 1 + null échoue, "${null}" aussi. coalesce et try donnent une valeur de repli.

Sécurité

  • Pas de secret dans une chaîne littérale. Un mot de passe écrit dans un .tf part dans Git, dans l'historique de tous les clones. Les secrets arrivent par des variables marquées sensitive (leçon 5), par un gestionnaire de secrets, ou sont générés par Terraform (random_password) et rangés ailleurs.
  • templatefile et l'injection. Un modèle insère des valeurs telles quelles. Si une valeur vient d'une source que vous ne contrôlez pas (un nom fourni par un client, par exemple), elle peut casser la structure du fichier produit, ou y injecter une ligne. Pour produire du JSON ou du YAML, préférez jsonencode et yamlencode, qui échappent correctement les valeurs.
  • file lit n'importe quoi. Une configuration qui fait file("~/.ssh/id_ed25519") met une clé privée dans l'état et peut-être dans les sorties. Ne lisez que des fichiers du dépôt, par des chemins relatifs à path.module.
  • La console lit l'état. Avec un état partagé, terraform console donne accès à tous les attributs, secrets compris : c'est un outil d'administration, pas d'exploration anodine.

En production

  • fmt -check et validate en CI, à chaque demande de fusion : ils coûtent quelques secondes et éliminent les discussions de style et les fautes de référence (leçon 12).
  • Des valeurs locales pour nommer les choses, plutôt que des expressions répétées : un bloc d'adressage, des étiquettes communes, un préfixe de nom. Un changement se fait alors à un seul endroit.
  • Un linter, comme TFLint, complète validate : il connaît les règles propres à certains fournisseurs et les mauvaises pratiques courantes.
  • Lisez le schéma plutôt que des exemples trouvés en ligne. terraform providers schema -json donne, pour la version installée du fournisseur, tous les arguments, leurs types, et s'ils sont obligatoires : c'est ce qui a servi à vérifier les exemples Scaleway de ce cours.

Exercices

1. Lire un bloc (niveau 100). Dans la ressource scaleway_instance_server.sig_app_1 de la leçon 1, identifiez : le type de bloc, ses étiquettes, les arguments, les blocs imbriqués, et la référence qui crée une dépendance.

Solution

Type de bloc : resource. Étiquettes : "scaleway_instance_server" (type de ressource) et "sig_app_1" (nom local). Arguments : name, type, image, zone, ip_id, user_data, tags. user_data est un argument de type map (il s'écrit avec =), pas un bloc imbriqué ; il n'y a pas de bloc imbriqué dans cet exemple. La référence scaleway_instance_ip.sig_app_1.id crée la dépendance : l'IP sera créée avant l'instance. L'appel file("${path.module}/signalements.yaml") lit un fichier, mais ne crée pas de dépendance entre ressources.

2. Prédire la console (niveau 100). Sans l'exécuter, donnez le résultat de chaque expression, puis vérifiez avec terraform console : (a) length(toset(["a", "b", "a"])) ; (b) "5" == 5 ; (c) merge({env = "prod"}, {env = "preprod", app = "sig"}) ; (d) cidrsubnet("172.16.20.0/22", 4, 15).

Solution

(a) 2 : l'ensemble retire le doublon. (b) false : l'égalité ne convertit pas les types. (c) { "app" = "sig", "env" = "preprod" } : la dernière map l'emporte, et les clés sont affichées triées. (d) Quatre bits empruntés à un /22 donnent seize /26 de 64 adresses ; le numéro 15 est le dernier : "172.16.23.192/26".

3. Un cloud-init par instance (niveau 100). Écrivez un modèle tpl/cloud-init.yaml.tftpl qui reçoit nom, version et une liste paquets, et produit un cloud-config avec hostname, la liste packages et le fichier /etc/signalements/env. Affichez-le pour sig-app-2, version 1.3.0, avec les paquets docker.io et jq, dans une sortie. Indice : les modèles acceptent des directives %{ for ... }.

Solution

Un modèle possible :

#cloud-config
hostname: ${nom}
packages:
%{ for p in paquets ~}
  - ${p}
%{ endfor ~}
write_files:
  - path: /etc/signalements/env
    content: |
      APP_VERSION=${version}

et la sortie value = templatefile("${path.module}/tpl/cloud-init.yaml.tftpl", { nom = "sig-app-2", version = "1.3.0", paquets = ["docker.io", "jq"] }). Le ~ retire le saut de ligne qui suit la directive, pour ne pas laisser de ligne vide entre les paquets. Vérifiez le résultat avec cloud-init schema comme à la leçon 4 du cours cloud, ou générez la liste avec yamlencode pour éviter toute erreur d'indentation.

Récapitulatif

  • Un fichier HCL est un corps fait d'arguments (nom = valeur) et de blocs (type, étiquettes, corps). Le schéma du fournisseur dit ce qui est un argument et ce qui est un bloc imbriqué.
  • Types : string, number, bool, list, set, map, object, tuple, et null pour « absent ». Les conversions entre primitifs sont automatiques, sauf pour l'égalité.
  • Une référence (type.nom.attribut, var., local., data.) copie une valeur et crée une dépendance ; l'ordre des blocs est indifférent.
  • ${ ... } interpole, <<-EOT écrit un texte multiligne sans l'indentation du code, ? : choisit, for transforme une collection.
  • Une centaine de fonctions intégrées : format, join, merge (la dernière l'emporte), lookup, toset (trie et dédoublonne), cidrsubnet, cidrhost, jsonencode, yamlencode, file, templatefile.
  • terraform console pour expérimenter ; terraform fmt pour le style, -check en CI ; le guide de style pour les noms et l'organisation des fichiers.

Pour aller plus loin

  • La spécification de HCL, pour le modèle d'information et la syntaxe native.
  • Les pages Types and Values, Expressions et Functions de la documentation du langage Terraform (ou leurs équivalents chez OpenTofu).
  • Le guide de style de HashiCorp, à faire lire à toute l'équipe avant la première relecture.
  • La leçon suivante, qui fait entrer le fournisseur Scaleway : configuration, ressources et sources de données.
Voir ma constellation →

Sources