Aller au contenu principal

Maîtriser les Expressions Terraform

Dans l'univers de Terraform, les expressions transforment les configurations statiques en solutions dynamiques et automatisées. Elles permettent aux administrateurs systèmes DevOps d'effectuer des répétitions, de conditionner la création de ressources et de réaliser des calculs. Ces capacités rendent les infrastructures plus adaptables et réactives aux besoins spécifiques d'une entreprise.

Utiliser les Boucles dans Terraform

Les boucles dans Teraform simplifient la création et la gestion de multiples ressources. Trois méthodes peuvent être utilisées : count, for_each et la compréhension de liste avec for.

La Boucle avec count**

La directive count est idéale pour créer un nombre défini d'instances d'une ressource. Elle est souvent utilisée lorsque vous avez besoin de plusieurs copies identiques d'une ressource, avec seulement de légères variations.

Prenons l'exemple de la création de plusieurs instances de serveurs dans un cloud :

resource "aws_instance" "server" {
  count         = 5  # Crée 5 instances
  instance_type = "t2.micro"
  # autres configurations
}

Dans cet exemple, Teraform crée cinq instances EC2 identiques. Vous pouvez accéder à chaque instance individuellement en utilisant aws_instance.server[0], aws_instance.server[1], etc.

La Boucle avec for_each

for_each est utilisé lorsque chaque ressource doit être configurée de manière unique, basée sur un ensemble de données, comme une liste ou un map. Cela est utile pour des configurations où chaque ressource a ses propres paramètres spécifiques.

Par exemple, la création de sous-réseaux dans différents emplacements pourrait se faire comme suit :

resource "aws_subnet" "example" {
  for_each = var.subnets

  availability_zone = each.key
  cidr_bloc        = each.value
}

Ici, var.subnets est un map avec des zones de disponibilité comme clés et des blocs CIDR comme valeurs. for_each crée un sous-réseau pour chaque paire clé-valeur dans ce map.

Les boucles for

Les boucles avec for permet de transformer et de filtrer des listes et des maps. Elle est idéale pour créer de nouvelles structures de données à partir de données existantes.

Par exemple, pour transformer une liste d'identifiants en une liste de noms de serveurs :

locals {
  instance_names = [for id in var.instance_ids : "server-${id}"]
}

Dans cet exemple, pour chaque id dans var.instance_ids, un nouveau nom de serveur est généré et ajouté à la liste instance_names.

Une autre moyen de simplifier son écriture fait appel à l'opérateur splat [*]

Supposons que vous ayez défini vos instances EC2 comme suit dans Terraform :

resource "aws_instance" "example" {
  count = 5
  // autres configurations
}

Ici, count permet de créer 5 instances. Si vous voulez récupérer les adresses IP de toutes ces instances, vous pouvez utiliser le splat operator de cette manière :

output "ip_addresses" {
  value = aws_instance.example[*].public_ip
}

Dans cet exemple, aws_instance.example fait référence à toutes les instances EC2 créées. Le [*] est le splat operator, qui est utilisé pour itérer sur chaque instance et récupérer l'attribut public_ip. Ainsi, aws_instance.example[*].public_ip renvoie une liste des adresses IP publiques de toutes les instances EC2 créées par cette ressource.

C'est un moyen efficace de gérer les données de multiples ressources sans avoir à écrire des boucles ou des références individuelles pour chaque ressource. Le splat operator simplifie le code et le rend plus lisible, surtout lorsque vous travaillez avec un grand nombre de ressources similaires.

Les blocs dynamiques

Les bloc dynamiques de Terraform permettent de créer des configurations répétables et modulables. Ils sont particulièrement utiles lorsque vous devez créer plusieurs blocs de configuration similaires avec seulement quelques variations. Utiliser des blocs dynamiques rend votre code Terraform plus propre, plus organisé et plus facile à maintenir.

Supposons que vous avez une liste de règles de sécurité que vous souhaitez appliquer à un groupe de sécurité AWS. Au lieu de déclarer chaque règle individuellement, vous pouvez utiliser un bloc dynamique pour les créer toutes à partir d'une liste structurée.

Avec les blocs dynamiques, vous pouvez simplifier cela. Voici comment cela peut être fait :

variable "ingress_rules" {
  description = "Liste des règles d'entrée pour le groupe de sécurité"
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
    // Autres règles ici si nécessaire
  ]
}

resource "aws_security_group" "example" {
  name        = "example-security-group"
  description = "Exemple de groupe de sécurité utilisant des blocs dynamiques"

  dynamic "ingress" {
    for_each = var.ingress_rules

    content {
      from_port   = ingress.value["from_port"]
      to_port     = ingress.value["to_port"]
      protocol    = ingress.value["protocol"]
      cidr_blocks = ingress.value["cidr_blocks"]
    }
  }
}

Dans cette configuration, la variable ingress_rules est une liste d'objets, chaque objet représentant une règle d'entrée pour le groupe de sécurité. Vous pouvez facilement ajouter ou supprimer des règles en modifiant cette liste, rendant la gestion des règles de sécurité plus flexible et modulaire.

  • dynamic "ingress" indique qu'il s'agit d'un bloc dynamique pour les règles d'entrée (ingress).
  • for_each = var.ingress_rules spécifie que le bloc dynamique doit itérer sur chaque élément de la variable var.ingress_rules.
  • Le bloc content définit la configuration pour chaque règle d'entrée, avec des attributs comme from_port, to_port, protocol et cidr_blocks.

En utilisant des blocs dynamiques, vous pouvez donc gérer efficacement des configurations répétitives et complexes, rendant votre code Terraform plus lisible et facile à gérer.

Implémenter des Conditions avec Terraform

La gestion conditionnelle des ressources dans Teraform est cruciale pour créer des configurations flexibles et adaptatives. Teraform permet d'implémenter des conditions principalement à travers deux méthodes : l'utilisation de la propriété count pour créer conditionnellement des ressources et l'utilisation des expressions conditionnelles avec l'opérateur ternaire.

Création Conditionnelle avec count

La propriété count peut être utilisée pour contrôler la création d'une ressource en fonction d'une condition. Si la condition est remplie, la ressource est créée ; sinon, elle ne l'est pas.

Imaginons que vous souhaitiez créer une instance de base de données seulement si un certain drapeau est activé :

resource "aws_db_instance" "example" {
  count = var.create_db ? 1 : 0
  # Configuration de la base de données
}

Ici, var.create_db est une variable booléenne. Si elle est vraie (true), count devient 1 et la base de données est créée. Si elle est fausse (false), count est 0 et la ressource n'est pas créée.

Expressions Conditionnelles avec l'Opérateur Ternaire

L'opérateur ternaire condition ? true_value : false_value permet d'assigner des valeurs à des propriétés de ressource en fonction d'une condition. C'est une manière concise d'écrire des conditions simples.

Par exemple, supposons que vous vouliez attribuer une taille différente à une instance en fonction de l'environnement :

resource "aws_instance" "example" {
  instance_type = var.environment == "prod" ? "t2.large" : "t2.small"
  # autres configurations
}

Dans cet exemple, si var.environment est égal à "prod", l'instance sera de type t2.large. Sinon, elle sera de type t2.small.

Voici la liste des opérateurs de comparaison :

  • == : Égal à
  • != : Différent de
  • > : Plus grand que
  • < : Moins grand que
  • >= : Plus grand ou égal à
  • <= : Moins grand ou égal à

Et les opérateurs logiques permettant de combiner plusieurs conditions:

  • && : ET logique
  • || : OU logique
  • ! : NON logique (négation)

Voici un exemple de l'utilisation de conditions logiques dans Teraform pour gérer la création et la configuration de ressources en fonction de conditions spécifiques. Imaginons que vous souhaitiez créer une instance AWS EC2, mais le type et la configuration de cette instance doivent varier en fonction de l'environnement de déploiement (par exemple, "production" ou "développement") et d'une condition supplémentaire comme la nécessité d'une instance à haute disponibilité.

variable "environment" {
  description = "L'environnement de déploiement (prod ou dev)"
  type        = string
}

variable "high_availability" {
  description = "Indique si une haute disponibilité est requise"
  type        = bool
}

resource "aws_instance" "example" {
  count = var.environment == "prod" && var.high_availability ? 2 : 1

  instance_type = var.environment == "prod" ? "t2.large" : "t2.micro"
  ami           = "ami-12345678"  # ID d'une AMI spécifique

  tags = {
    Environment = var.environment
    HA          = var.high_availability ? "Enabled" : "Disabled"
  }
}
  • Variables :

    • environment : Une variable pour déterminer l'environnement (production ou développement).
    • high_availability : Une variable booléenne pour indiquer si une haute disponibilité est nécessaire.
  • Ressource AWS EC2 (aws_instance) :

    • count :
      • Si l'environnement est "production" (prod) et que la haute disponibilité (high_availability) est requise, Teraform créera deux instances (pour la redondance).
      • Dans tous les autres cas, une seule instance sera créée.
    • instance_type :
      • Utilise un type d'instance plus grand (t2.large) pour la production et un type plus petit (t2.micro) pour le développement.
    • ami :
      • ID d'une Amazon Machine Image (AMI) spécifique, utilisé ici à titre d'exemple.
    • tags :
      • Des tags pour identifier l'environnement et indiquer si la haute disponibilité est activée ou non.

Cet exemple montre comment les conditions logiques dans Teraform peuvent être utilisées pour créer des configurations dynamiques, en fonction des besoins et des spécificités de l'environnement de déploiement.

Effectuer des Calculs avec Terraform

Teraform permet également d'effectuer de calcul pour manipuler les données de configuration. Ces calculs se font principalement à travers l'utilisation d'opérateurs arithmétiques, de fonctions intégrées et de manipulations de chaînes de caractères.

Opérateurs Arithmétiques dans Terraform

Les opérateurs arithmétiques standards sont utilisés pour effectuer des calculs de base. Ces opérateurs sont utiles pour définir des valeurs dynamiques basées sur des calculs. En voici la liste :

  • + : Addition
  • - : Soustraction
  • * : Multiplication
  • / : Division
  • % : Modulo (reste de la division)

Par exemple, pour ajuster la taille d'une ressource en fonction d'un facteur multiplicateur :

locals {
  base_size = 10
  factor    = 3
  total_size = local.base_size * local.factor
}

Dans cet exemple, total_size sera égal à 30, résultant de la multiplication de base_size par factor.

Utilisation des Fonctions Intégrées

Teraform offre une variété de fonctions intégrées pour effectuer des opérations complexes. Ces fonctions incluent la manipulation de chaînes de caractères, la gestion des listes et des maps, les calculs mathématiques et bien plus.

Par exemple, pour combiner des chaînes de caractères et des nombres :

locals {
  server_name = "server-${var.environment}-${local.base_size + local.factor}"
}

Ici, server_name pourrait être quelque chose comme "server-prod-13", combinant une chaîne de caractères avec des valeurs calculées.

Manipulations de Chaînes de Caractères

Les manipulations de chaînes de caractères sont fréquemment utilisées dans les configurations Teraform pour personnaliser les noms, les tags et d'autres propriétés de ressources. Des fonctions telles que format, replace, ou split permettent de traiter et de formatter les chaînes de caractères de manière dynamique.

Prenons l'exemple de la création d'un nom de ressource basé sur plusieurs variables :

resource "aws_instance" "example" {
  name = format("instance-%s-%d", var.environment, local.total_size)
  # autres configurations
}

Dans cet exemple, format est utilisé pour créer un nom structuré et informatif pour l'instance, basé sur l'environnement et la taille calculée.

Les différentes fonctions intégrées

Voici une listre des principales fonctions disponibles:

Fonctions Numériques

  • abs(number): Renvoie la valeur absolue d'un nombre.
  • ceil(number): Arrondit un nombre à l'entier supérieur.
  • floor(number): Arrondit un nombre à l'entier inférieur.
  • log(number, base): Calcule le logarithme d'un nombre avec une base spécifique.
  • max(...): Renvoie le plus grand nombre d'un ensemble de nombres.
  • min(...): Renvoie le plus petit nombre d'un ensemble de nombres.
  • pow(x, y): Calcule x élevé à la puissance de y.
  • signum(number): Renvoie le signe d'un nombre.

Fonctions sur les Chaînes de Caractères

  • format(format, ...): Formate une chaîne de caractères selon un format spécifié.
  • join(delimiter, list): Joint les éléments d'une liste en une chaîne, séparés par un délimiteur.
  • lower(string): Convertit une chaîne en minuscules.
  • upper(string): Convertit une chaîne en majuscules.
  • replace(string, search, replace): Remplace une sous-chaîne par une autre.
  • split(delimiter, string): Divise une chaîne en une liste de chaînes, séparées par un délimiteur.
  • substr(string, offset, length): Renvoie une sous-chaîne d'une chaîne donnée.

Fonctions sur les Collections (Listes et Maps)

  • length(collection): Renvoie le nombre d'éléments dans une collection.
  • list(...): Crée une liste à partir d'arguments.
  • map(...): Crée un map à partir d'arguments.
  • element(list, index): Renvoie l'élément à un index donné dans une liste.
  • lookup(map, key, default): Renvoie la valeur d'une clé dans un map, ou une valeur par défaut.

Fonctions de Hachage et d'Encodage

  • base64encode(string): Encode une chaîne en Base64.
  • base64decode(string): Décode une chaîne encodée en Base64.
  • md5(string): Calcule le hachage MD5 d'une chaîne.
  • sha256(string): Calcule le hachage SHA256 d'une chaîne.

Fonctions sur les Types de Données

  • tonumber(string): Convertit une chaîne en nombre.
  • tostring(value): Convertit une valeur en chaîne de caractères.
  • tobool(value): Convertit une valeur en booléen.

Fonctions Diverses

  • timestamp(): Renvoie le timestamp actuel.
  • uuid(): Génère un identifiant unique universel (UUID).

Voici un exemple de configuration Teraform utilisant plusieurs fonctions intégrées pour gérer une infrastructure complexe. Imaginons que vous devez créer des sous-réseaux dans AWS et le nombre ainsi que la configuration de ces sous-réseaux dépendent des données fournies par l'utilisateur.

variable "regions" {
  description = "Liste des régions pour déployer les sous-réseaux"
  type        = list(string)
}

variable "cidr_base" {
  description = "Bloc CIDR de base pour les sous-réseaux"
  type        = string
}

locals {
  # Crée une liste de blocs CIDR basée sur le bloc CIDR de base et le nombre de régions
  cidr_blocs = [for i in range(length(var.regions)) : cidrsubnet(var.cidr_base, 8, i)]

  # Associe chaque région à son bloc CIDR correspondant
  region_cidr_map = zipmap(var.regions, local.cidr_blocs)
}

resource "aws_subnet" "example" {
  for_each = local.region_cidr_map

  vpc_id            = "vpc-12345678"  # ID de votre VPC
  cidr_bloc        = each.value
  availability_zone = each.key

  tags = {
    Name = format("subnet-%s-%s", each.key, substr(md5(each.value), 0, 4))
  }
}
  • Variables :

    • regions : Liste des régions AWS où les sous-réseaux doivent être créés.
    • cidr_base : Bloc CIDR de base pour les sous-réseaux.
  • Locals :

    • cidr_blocs : Utilise la fonction cidrsubnet pour créer des blocs CIDR uniques pour chaque région.
    • region_cidr_map : Crée un map associant chaque région à son bloc CIDR correspondant, en utilisant les fonctions zipmap et range.
  • Ressource AWS Subnet (aws_subnet) :

    • Utilise for_each pour itérer sur le map region_cidr_map.
    • Définit les paramètres de chaque sous-réseau, y compris l'ID du VPC, le bloc CIDR et la zone de disponibilité.
    • Les tags de chaque sous-réseau incluent un nom unique généré en utilisant les fonctions format, substr et md5 pour créer une étiquette distinctive basée sur le nom de la région et une partie du hachage MD5 du bloc CIDR.

Cet exemple illustre comment combiner diverses fonctions intégrées de Teraform pour créer une configuration dynamique et flexible, capable de s'adapter à un ensemble varié d'entrées et de besoins spécifiques.

Swich Case (simulé)

Teraform ne dispose pas d'une structure de contrôle switch ou case comme on peut la trouver dans de nombreux langages de programmation traditionnels. Cependant, Teraform offre une fonction intégrée nommée lookup qui peut être utilisée pour obtenir un effet similaire dans certaines situations. De plus, des expressions conditionnelles et la fonction map peuvent être combinées pour imiter le comportement d'un switch.

Pour simuler un switch, vous pouvez utiliser un map pour définir les correspondances clé-valeur et ensuite utiliser lookup ou une compréhension de liste pour sélectionner la valeur en fonction d'une clé donnée. Voici un exemple simple :

Exemple Simulant un Switch avec lookup.

variable "environment" {
  description = "L'environnement de déploiement"
  type        = string
}

locals {
  instance_sizes = {
    prod    = "t2.large"
    staging = "t2.medium"
    dev     = "t2.small"
  }

  instance_size = lookup(local.instance_sizes, var.environment, "t2.micro")
}

resource "aws_instance" "example" {
  instance_type = local.instance_size
  ami           = "ami-12345678"
  # autres configurations
}

Dans cet exemple, local.instance_sizes est un map qui associe chaque environnement à une taille d'instance EC2. La fonction lookup est ensuite utilisée pour obtenir la taille de l'instance basée sur la valeur de la variable environment. Si la clé n'est pas trouvée dans le map, lookup retournera la valeur par défaut spécifiée, ici "t2.micro".

Cela permet une certaine flexibilité similaire à un switch, permettant de sélectionner des valeurs en fonction de clés définies dans un map.

Conclusion

À travers ce guide, nous avons exploré comment les expressions Teraform, notamment les boucles, les conditions et les calculs, peuvent être utilisées pour créer des configurations dynamiques.

En utilisant judicieusement les boucles avec count, for_each et la compréhension de liste avec for, vous pouvez gérer de manière efficace des ensembles complexes de ressources. Les conditions, bien qu'elles n'offrent pas de structure switch native, peuvent être habilement simulées avec des fonctions comme lookup pour adapter vos configurations à différents contextes. De plus, les calculs avec les opérateurs arithmétiques et les fonctions intégrées permettent une manipulation fine des données, rendant vos configurations encore plus puissantes.