Concepts avancés de Terraform
Constructions et usages avancés
Ce chapitre traite des constructions de code et notions avancées de Terraform.
Ces éléments permettent de rendre le code plus expressif, d’implémenter des conditions, de créer plusieurs ressources similaires, ou encore d’indiquer qu’une ressource a été renommée.
Une partie de ces opérations est implémentée avec des méta-arguments. Les méta-arguments sont des arguments disponibles sur toutes les ressources et modules.
Terraform propose deux moyens d’implémenter la création de ressources multiples à partir d’un seul bloc resource, le count et le for_each. Les expressions for et splat permettent de manipuler des collections et sont utilisées dans le contexte de ressources multiples utilisant count ou for_each.
Ce chapitre présente également l’utilisation des blocs dynamiques, des méta-arguments de cycle de vie des ressources, la sélection des providers, ainsi que la gestion des valeurs sensibles.
L’argument count
Le méta-argument count permet de préciser le nombre de ressources similaires devant être créées par un bloc resource ou module.
Cet argument attend une valeur de type number. La valeur peut être calculée par une expression, avec par exemple un appel de fonction.
Une bonne pratique est de déclarer le méta-argument count dans la toute première ligne d’une resource ou d’un module.
L’exemple ci-dessous crée deux instances de serveur, en utilisant le provider Scaleway :
resource "scaleway_instance_server" "web_instances" {
count = 2
type = "DEV1-S"
image = "ubuntu_jammy"
}
Lors de l’exécution d’un terraform plan, Terraform détecte la création de deux ressources pour le même bloc de code :
$ terraform plan
Terraform will perform the following actions:
# scaleway_instance_server.web_instances[0] will be created
# scaleway_instance_server.web_instances[1] will be created
À noter que les ressources sont numérotées dans la sortie de terraform plan.
Le nom affiché scaleway_instance_server.web_instances[0] s’appelle l’Adresse de la ressource. Cette notion est expliquée dans le chapitre La gestion du state - Adresses des ressources et modules.
1. Utilisation de l’index
Pour chaque ressource devant être créée par un count, Terraform met à disposition une variable count.index. Cette variable représente le numéro de la ressource à créer. Les index de count.index commencent à la valeur 0, et sont également ceux représentés dans l’adresse de state générée lors d’un plan. L’adresse de state contient alors [count.index].
Cette variable count.index...
for_each
Le méta-argument for_each est le deuxième moyen pour créer de multiples ressources à partir d’un seul bloc de code resource.
for_each permet d’itérer sur le contenu d’un set(string) ou d’une map, et crée une ressource pour chacun des éléments.
for_each peut aussi être utilisé sur un module pour en créer plusieurs instances. On peut aussi plus rarement le retrouver sur un bloc data pour requêter plusieurs ressources existantes.
Lorsqu’un set(string) est passé en paramètre, le comportement de for_each est assez similaire à celui du count.
Comme pour le count, une bonne pratique consiste à déclarer le méta-argument for_each dans la toute première ligne d’une resource ou d’un module.
Lorsqu’une map() est passée en paramètre, la map peut alors contenir n’importe quel type de données (les clés d’une map sont toujours des string). Cette flexibilité rend le for_each beaucoup plus expressif que le count lorsqu’il est utilisé avec une map.
Utilisation de each
De manière similaire au count, l’utilisation de for_each introduit dans la ressource des nouvelles variables : each.key et each.value.
Avec une map() passée en paramètre au for_each, la variable each.key contient la clé de la map()...
Expression for
Les expressions for permettent de transformer un type complexe en un autre, en itérant sur chacun des éléments.
Les cas d’usage sont multiples, comme transformer une liste d’objets en map ou inversement, modifier tous les éléments d’une liste ou en filtrer certains.
Les expressions for sont très puissantes et ont une très forte expressivité. Elles sont cependant compliquées à lire. C’est une bonne idée d’accompagner ces expressions d’un commentaire décrivant le but de l’expression.
Le résultat d’une expression for est une liste ou une map. On retrouve souvent les expressions for dans des variables locales, dans des outputs, ou dans un argument for_each.
1. Expression for pour créer des listes
Une expression for ayant pour résultat une liste s’écrit de la manière suivante :
[ for <IT> in <COLL> : <EXPRESSION> ]
Les crochets [ et ] permettent de définir une liste. Le mot-clé for est suivi d’un nom de variable d’itération <IT>. La structure sur laquelle itérer est définie par le mot-clé in, suivi de la référence vers la structure. Le caractère : sépare la déclaration de la boucle for de l’expression à évaluer.
Pour illustrer, voici un exemple d’expression...
Expressions splat
Les expressions splat sont un moyen de créer des listes, au même titre que le ferait une expression for.
Au même titre que les expressions for, les expressions splat sont souvent utilisées dans des for_each, output ou des variables local.
Il n’est pas possible de filtrer des éléments dans une expression splat, mais il est possible de combiner une expression for et une expression splat pour arriver à ce but.
Les expressions splat utilisent un accesseur d’index de liste [*].
Voici un exemple d’expression splat, utilisée dans une variable locale student_names qui retourne une liste de noms d’étudiants à partir d’une liste d’objets :
locals {
students = [
{ name = "luke", age = 19 },
{ name = "leia", age = 19 },
{ name = "han", age = 32 },
]
student_names = local.students[*].name
}
Cette expression splat est équivalente à l’expression for [ for s in local.students : s.name ].
Il est aussi possible d’utiliser une expression splat sur une ressource sur laquelle un méta-argument count a été positionné, pour construire des outputs contenant l’ensemble des valeurs d’un...
Les dynamic blocks
Certains éléments de configuration de ressources sont des blocs.
Un bloc se distingue d’un argument par le fait qu’il soit répétable, et n’utilise pas le caractère =.
Voici un exemple de ressource scaleway_iam_policy. Cette ressource contient quelques arguments, et un bloc rule :
resource "scaleway_iam_policy" "object_read_only" {
name = "policy"
description = "gives app readonly access to object storage"
application_id = local.application_id
rule {
project_ids = [local.project_id]
permission_set_names = ["ObjectStorageReadOnly"]
}
}
Les blocs sont répétables, et peuvent donc être spécifiés plusieurs fois, comme dans ce deuxième exemple :
resource "scaleway_iam_policy" "object_and_secrets_read_only" {
name = "policy"
description = "gives app readonly access"
application_id = local.application_id
rule {
project_ids = [local.project_id]
permission_set_names = ["ObjectStorageReadOnly"]
}
rule { ...
Le bloc lifecycle et l’attribut depends_on
Le bloc lifecycle est un méta-argument qui permet d’indiquer à Terraform comment il doit gérer la ressource. Il est possible de demander à Terraform de modifier son comportement par défaut si la ressource doit être recréée, ou de protéger une ressource d’une destruction accidentelle.
L’attribut depends_on permet de forcer la dépendance entre deux ressources ou data déclarées dans le code. Cela permet principalement d’indiquer à Terraform l’ordre de création ou de modification de ressources qui ne dépendent pas l’une de l’autre a priori.
1. create_before_destroy
Lorsqu’une ressource doit être modifiée, il arrive que Terraform soit contraint de la recréer. C’est le cas lorsque certains éléments d’une infrastructure ne peuvent pas être changés à chaud, par exemple un nom de bucket, une image de base d’une VM, ou un type de base de données.
Ce sont les providers qui implémentent les règles permettant de décider si une ressource doit être recréée quand un de ses arguments est modifié, ou si une mise à jour est possible. La documentation de chaque provider indique souvent quels paramètres occasionnent une recréation.
Par défaut, la commande terraform plan indique que les ressources doivent être recréées.
Voici un exemple d’un plan indiquant qu’une ressource bucket doit être recréée, suite à un changement de nom :
# scaleway_object_bucket.this must be replaced
-/+ resource "scaleway_object_bucket" "this" {
~ api_endpoint = "https://s3.fr-par.scw.cloud" ->
(known after apply)
~ endpoint = "https://codekaio-terraform-book-bucket.s3.
fr-par.scw.cloud" -> (known after apply)
~ id = "fr-par/codekaio-terraform-book-bucket" ->
(known after apply)
~ name = "codekaio-terraform-book-bucket" ->
"codekaio-terraform-book-bucket-new" # forces replacement
~ project_id = "d2f60dce-f716-4e45-96cb-3837fe56f0d9"...
Validation des variables
Lorsque l’on écrit un module réutilisable, il peut parfois être intéressant de vouloir valider la valeur de certaines variables en entrée.
Une validation avant toute exécution peut s’avérer utile, pour éviter que l’exécution d’un module échoue lors de la phase apply, ou pour forcer l’utilisation de certaines valeurs acceptables.
On peut souhaiter, par exemple, limiter la longueur d’une variable utilisée comme nom d’objet, limiter les valeurs possibles pour une variable utilisée comme environnement ou taille de machine, etc.
Les variables peuvent être validées en entrée avec un bloc validation. Ce bloc comprend une condition à évaluer, qui doit être une expression booléenne. Un error_message à afficher à l’utilisateur, dans le cas où l’évaluation de la condition est false, doit également être fourni.
Voici un exemple d’une variable region, qui n’autorise que quelques valeurs possibles :
variable "zone" {
type = string
description = "the zone to use"
validation {
condition = contains(["europe-west9-a", "europe-west9-b",
"europe-west9-c"], var.zone)
error_message...
L’argument provider
Un méta-argument provider est disponible sur l’ensemble des blocs resource et data. Cet argument permet de préciser quel provider doit être utilisé, au cas où plusieurs instances d’un même provider seraient configurées dans le code. Il est possible d’avoir plusieurs instances d’un même provider pour utiliser des clés d’accès différentes, ou utiliser des paramètres par défaut (région, projet, etc.) différent.
Voici un exemple de configuration comportant plusieurs instances d’un même provider google. Cet exemple est issu de la documentation officielle de Terraform :
# default configuration
provider "google" {
region = "us-central1"
}
# alternate configuration, whose alias is "europe"
provider "google" {
alias = "europe"
region = "europe-west1"
}
resource "google_compute_instance" "example" {
# This "provider" meta-argument selects the google provider
# configuration whose alias is "europe", rather than the
# default configuration.
provider = google.europe
# ...
}
Pour pouvoir...
Attributs, variables et outputs sensitives
Certains éléments d’une infrastructure peuvent comporter des éléments sensibles : mots de passe, clés d’API, tokens d’authentification.
Ces éléments doivent faire l’objet d’une attention particulière.
Terraform permet de protéger des variable et des output marquées sensitive. Lorsqu’une variable ou un output est marquée sensitive, les logs d’exécution des différentes commandes Terraform n’affichent pas la valeur de la variable ou de l’output.
La plupart des providers implémentent également des attributs sensitive dans leurs définitions de ressources et data.
Attention cependant, les variables et output sensitive sont tout de même stockées en clair dans le fichier de state.
1. Attributs sensitives
Voici un exemple d’une ressource postgresql_role, provenant du provider doctolib/postgresql. Cette ressource permet de créer un rôle postgresql (user) dans une base de données déjà existante :
variable "user_name" {
type = string
}
variable "user_password" {
type = string
}
resource "postgresql_role" "my_role" {
name = var.user_name
login = true
password = var.user_password
}
L’exécution de la commande terraform plan a pour effet d’afficher le plan d’exécution de cette ressource :
Terraform used the selected providers to generate the following
execution plan. Resource actions are indicated with the following
symbols:
+ create
Terraform will perform the following actions:
# postgresql_role.my_role will be created
+ resource "postgresql_role" "my_role" {
+ login = true
+ name = "yoda"
+ password = (sensitive value)
}
On peut déjà constater ici que le password de notre utilisateur n’est pas affiché sur la sortie du plan et qu’il est remplacé par le message (sensitive value).
Ce premier niveau de protection a lieu du côté du code du provider....
Applications partielles
Dans certains cas d’utilisation, en particulier lorsqu’on travaille sur de grands projets composant beaucoup de ressources, il est parfois souhaitable de pouvoir appliquer des modifications uniquement à une ou plusieurs ressources, sans impacter le reste de l’infrastructure. C’est le cas lorsque l’on souhaite recréer une ressource particulière, ou que l’exécution d’un plan complet serait trop longue.
L’option -target de terraform plan permet de calculer un plan d’exécution qui ne cible qu’une ressource particulière. Lors de l’exécution de -target, les ressources dépendant de la ressource ciblée sont également prises en compte dans le plan d’exécution calculé.
L’option -target prend en paramètre l’adresse d’une ressource ou d’un module.
Voici un exemple de son utilisation :
$ terraform plan -target 'aws_subnet.public_subnet[“eu-west-3c”]'
-out=tfplan
# aws_subnet.public_subnet["eu-west-3c"] will be created
+ resource "aws_subnet" "public_subnet" {
+ availability_zone = "eu-west-3c"
+ cidr_block = "10.200.3.0/24"
} ...
Conclusion
Ce chapitre a présenté toutes les structures de code avancées de Terraform.
L’impasse a été faite volontairement sur une notion : les provisioners. Ils permettent d’exécuter des commandes sur l’infrastructure après sa création, via une connexion SSH qui est alors ouverte sur la machine distante, ou via des commandes ou scripts exécutés localement.
L’utilisation des provisioners est considérée par l’ensemble de la communauté comme une mauvaise pratique, car elle supprime l’aspect immutable de l’infrastructure. Terraform n’est pas l’outil qui excelle dans la configuration de machines, il vaut mieux vous tourner vers un outil comme Ansible, Chef ou Puppet pour cela. Néanmoins, si vous êtes curieux, je vous invite à lire la documentation de Terraform sur les provisioners disponible à cette adresse : https://developer.hashicorp.com/terraform/language/resources/provisioners/syntax
Le chapitre suivant va aborder une notion tout aussi dense que ce chapitre : la gestion du state. Le state est un des éléments centraux de Terraform, et il nécessite une gestion particulière. Nous allons donc aborder la notion de state avec son fonctionnement interne, le stockage du state sur un backend, et l’ensemble des opérations qui peuvent être...