HCL : Maîtriser le Langage de Configuration de Terraform

Maîtrisez HCL, le langage de configuration de Terraform. Syntaxe, types de données, blocs resource/data/variable, fonctions built-in, boucles count/for_each et expressions conditionnelles.

Si Terraform est le moteur de votre Infrastructure as Code, alors HCL (HashiCorp Configuration Language) en est le carburant. Ce langage déclaratif, conçu spécifiquement par HashiCorp, est à la fois suffisamment simple pour être accessible aux débutants et suffisamment puissant pour exprimer des configurations d'infrastructure complexes. Dans cet article, nous allons plonger en profondeur dans la syntaxe, les types de données, les blocs, les fonctions et les expressions avancées de HCL. À la fin de cette lecture, vous maîtriserez les fondamentaux du langage et serez capable d'écrire des configurations Terraform expressives, maintenables et élégantes.

Structure du langage HCL en un coup d'oeil

Diagramme - HCL : Maîtriser le Langage de Configuration de Terraform

Qu'est-ce que HCL ?

HCL (HashiCorp Configuration Language) est un langage de configuration créé par HashiCorp pour ses outils (Terraform, Vault, Consul, Nomad, Packer, etc.). Il a été conçu avec trois objectifs principaux :

  • Lisibilité humaine : HCL est plus lisible que JSON et plus structuré que YAML.
  • Interopérabilité machine : HCL peut être converti en JSON et vice-versa, facilitant l'intégration avec des outils existants.
  • Expressivité : HCL supporte les variables, les fonctions, les expressions conditionnelles et les boucles, tout en restant déclaratif.

Les fichiers HCL utilisent l'extension .tf pour les configurations Terraform et .tfvars pour les fichiers de variables.

La syntaxe de base : blocs, arguments et expressions

La syntaxe HCL repose sur trois éléments fondamentaux : les blocs, les arguments et les expressions.

Les blocs

Un bloc est un conteneur qui regroupe une configuration. Il est défini par un type, zéro ou plusieurs labels, et un corps entre accolades :

# Syntaxe générale d'un bloc
type_de_bloc "label_1" "label_2" {
  # corps du bloc
  argument1 = valeur1
  argument2 = valeur2

  bloc_imbriqué {
    argument3 = valeur3
  }
}

Voici un exemple concret avec une ressource AWS :

# "resource" est le type de bloc
# "aws_instance" est le premier label (type de ressource)
# "web_server" est le second label (nom logique)
resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    Name = "WebServer"
  }
}

Les arguments

Un argument assigne une valeur à un nom à l'intérieur d'un bloc. La syntaxe est simple : nom = valeur. Les arguments sont spécifiques à chaque type de bloc et de ressource.

resource "aws_s3_bucket" "mon_bucket" {
  # Arguments de la ressource
  bucket = "mon-bucket-unique-12345"

  tags = {
    Environment = "production"
    Team        = "devops"
  }
}

Les expressions

Les expressions représentent des valeurs, soit littérales, soit calculées. HCL supporte de nombreux types d'expressions :

# Expression littérale (string)
name = "mon-serveur"

# Expression littérale (nombre)
count = 3

# Expression littérale (booléen)
enabled = true

# Référence à une autre ressource
subnet_id = aws_subnet.main.id

# Interpolation de chaîne
name = "serveur-${var.environment}"

# Expression conditionnelle
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"

# Appel de fonction
upper_name = upper(var.name)

Les types de données en HCL

HCL supporte un ensemble riche de types de données, divisés en types primitifs et types complexes.

Types primitifs

Les trois types primitifs de HCL sont :

# String (chaîne de caractères)
nom = "mon-serveur"

# Number (nombre entier ou décimal)
port     = 8080
cpu_load = 0.75

# Bool (booléen)
actif  = true
debug  = false

String multilignes (Heredoc)

Pour les chaînes de caractères sur plusieurs lignes, HCL utilise la syntaxe Heredoc :

# Heredoc standard (conserve l'indentation)
description = <<EOT
Ceci est une description
sur plusieurs lignes.
Elle conserve l'indentation exacte.
EOT

# Heredoc avec suppression de l'indentation (recommandé)
user_data = <<-EOF
  #!/bin/bash
  apt-get update -y
  apt-get install -y nginx
  systemctl start nginx
EOF

La syntaxe <<- (avec le tiret) supprime automatiquement l'indentation commune à toutes les lignes, ce qui permet de garder un code bien indenté.

Types complexes : list (tuple)

Une list est une séquence ordonnée de valeurs du même type :

# Liste de strings
availability_zones = ["eu-west-3a", "eu-west-3b", "eu-west-3c"]

# Liste de nombres
ports = [80, 443, 8080]

# Accéder à un élément par son index
first_az = var.availability_zones[0]  # "eu-west-3a"

# Longueur d'une liste
nb_zones = length(var.availability_zones)  # 3

Types complexes : map (object)

Un map est une collection de paires clé-valeur :

# Map de strings
tags = {
  Name        = "WebServer"
  Environment = "production"
  Team        = "devops"
  Project     = "ecommerce"
}

# Accéder à une valeur par sa clé
env = var.tags["Environment"]  # "production"
# Ou avec la notation point
env = var.tags.Environment     # "production"

# Map de nombres (tarifs par région)
instance_prices = {
  "eu-west-1" = 0.0116
  "eu-west-3" = 0.0118
  "us-east-1" = 0.0104
}

Types complexes : object

Un object est un type structuré avec des attributs nommés et typés :

variable "serveur" {
  type = object({
    nom           = string
    instance_type = string
    port          = number
    actif         = bool
    tags          = map(string)
  })

  default = {
    nom           = "web-01"
    instance_type = "t3.micro"
    port          = 443
    actif         = true
    tags = {
      Environment = "dev"
    }
  }
}

Types complexes : set et tuple

# Set : comme une liste mais sans doublons et sans ordre garanti
variable "allowed_ips" {
  type    = set(string)
  default = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
}

# Tuple : liste avec des types hétérogènes
variable "config" {
  type    = tuple([string, number, bool])
  default = ["serveur", 8080, true]
}

Les blocs fondamentaux de Terraform

Terraform utilise plusieurs types de blocs HCL, chacun ayant un rôle spécifique dans la configuration de l'infrastructure.

Le bloc terraform

Le bloc terraform configure le comportement global de Terraform lui-même :

terraform {
  # Version minimale de Terraform requise
  required_version = ">= 1.5.0"

  # Providers requis avec contraintes de version
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.0"
    }
  }

  # Configuration du backend pour le state
  backend "s3" {
    bucket         = "mon-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-west-3"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Le bloc provider

Le bloc provider configure un fournisseur de services :

# Provider AWS avec configuration
provider "aws" {
  region  = "eu-west-3"
  profile = "production"

  default_tags {
    tags = {
      ManagedBy = "Terraform"
      Project   = "mon-projet"
    }
  }
}

# Vous pouvez avoir plusieurs instances du même provider
# en utilisant des alias
provider "aws" {
  alias  = "us_east"
  region = "us-east-1"
}

# Utiliser un provider avec alias
resource "aws_s3_bucket" "cdn_bucket" {
  provider = aws.us_east
  bucket   = "mon-bucket-cdn"
}

Le bloc resource

Le bloc resource définit un composant d'infrastructure à créer et gérer :

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "vpc-principale"
  }
}

resource "aws_subnet" "publique" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "eu-west-3a"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-publique"
  }
}

# Référencer des attributs d'autres ressources
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.publique.id  # Référence !

  tags = {
    Name = "web-server"
  }
}

Le bloc data

Le bloc data permet de lire des informations depuis un provider sans créer de ressource. C'est utile pour référencer des ressources existantes qui ne sont pas gérées par votre configuration Terraform :

# Récupérer l'AMI Ubuntu la plus récente
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Utiliser la data source dans une ressource
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id  # Référence au data source
  instance_type = "t3.micro"
}

# Récupérer les AZs disponibles
data "aws_availability_zones" "available" {
  state = "available"
}

# Récupérer le VPC par défaut
data "aws_vpc" "default" {
  default = true
}

Le bloc variable

Le bloc variable déclare une variable d'entrée qui paramètre votre configuration :

variable "environment" {
  description = "L'environnement de déploiement (dev, staging, prod)"
  type        = string
  default     = "dev"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "L'environnement doit être dev, staging ou prod."
  }
}

variable "instance_count" {
  description = "Nombre d'instances à créer"
  type        = number
  default     = 1
}

variable "enable_monitoring" {
  description = "Activer le monitoring détaillé"
  type        = bool
  default     = false
}

variable "allowed_cidrs" {
  description = "Liste des CIDR autorisés"
  type        = list(string)
  default     = ["10.0.0.0/8"]
}

variable "instance_config" {
  description = "Configuration des instances par environnement"
  type = map(object({
    instance_type = string
    volume_size   = number
    monitoring    = bool
  }))
  default = {
    dev = {
      instance_type = "t3.micro"
      volume_size   = 20
      monitoring    = false
    }
    prod = {
      instance_type = "t3.large"
      volume_size   = 100
      monitoring    = true
    }
  }
}

# Variable sensible (masquée dans les logs)
variable "db_password" {
  description = "Mot de passe de la base de données"
  type        = string
  sensitive   = true
}

Les variables peuvent être fournies de plusieurs manières (par ordre de priorité croissante) : valeur par défaut, fichier terraform.tfvars, fichiers *.auto.tfvars, variables d'environnement (TF_VAR_nom), option -var en ligne de commande.

Le bloc output

Le bloc output exporte des valeurs après l'application de la configuration. Ces valeurs peuvent être affichées à l'écran, utilisées par d'autres configurations Terraform, ou consommées par des scripts :

output "instance_ip" {
  description = "Adresse IP publique de l'instance"
  value       = aws_instance.web.public_ip
}

output "instance_url" {
  description = "URL du serveur web"
  value       = "http://${aws_instance.web.public_ip}"
}

output "database_endpoint" {
  description = "Endpoint de la base de données"
  value       = aws_db_instance.main.endpoint
  sensitive   = true  # Masquer la valeur dans les logs
}

# Output conditionnel
output "lb_dns_name" {
  description = "DNS du load balancer (si activé)"
  value       = var.enable_lb ? aws_lb.main[0].dns_name : "N/A"
}

Le bloc locals

Le bloc locals définit des valeurs locales calculées qui peuvent être réutilisées dans la configuration. C'est l'équivalent des constantes ou des variables intermédiaires :

locals {
  # Valeur simple
  project_name = "ecommerce"

  # Valeur calculée
  name_prefix = "${var.environment}-${local.project_name}"

  # Tags communs
  common_tags = {
    Project     = local.project_name
    Environment = var.environment
    ManagedBy   = "Terraform"
    CreatedAt   = timestamp()
  }

  # Calcul conditionnel
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

  # Transformation de données
  subnet_cidrs = [for i in range(3) : cidrsubnet("10.0.0.0/16", 8, i)]
  # Résultat : ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"]
}

# Utilisation des locals
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.instance_type

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-web"
    Role = "webserver"
  })
}

Les fonctions built-in de HCL

Terraform fournit un ensemble riche de fonctions built-in pour manipuler les données. Voici les plus utilisées, classées par catégorie.

Fonctions sur les chaînes de caractères

locals {
  # upper / lower : changer la casse
  majuscule = upper("terraform")      # "TERRAFORM"
  minuscule = lower("TERRAFORM")      # "terraform"

  # title : première lettre en majuscule
  titre = title("hello world")        # "Hello World"

  # format : formatage de chaîne (comme printf)
  message = format("Serveur %s sur le port %d", "web", 8080)
  # "Serveur web sur le port 8080"

  # join : concaténer une liste en string
  zones = join(", ", ["eu-west-3a", "eu-west-3b", "eu-west-3c"])
  # "eu-west-3a, eu-west-3b, eu-west-3c"

  # split : découper une string en liste
  parties = split(",", "a,b,c")       # ["a", "b", "c"]

  # replace : remplacement dans une chaîne
  clean = replace("hello-world", "-", "_")  # "hello_world"

  # trimspace : supprimer les espaces en début et fin
  propre = trimspace("  hello  ")     # "hello"

  # substr : extraction de sous-chaîne
  extrait = substr("terraform", 0, 5)  # "terra"

  # regex / regexall : expressions régulières
  match = regex("[0-9]+", "serveur-42")  # "42"
}

Fonctions sur les collections

locals {
  # length : nombre d'éléments
  nb_items = length(["a", "b", "c"])  # 3

  # lookup : chercher une valeur dans un map avec valeur par défaut
  region_ami = lookup({
    "eu-west-3" = "ami-123"
    "us-east-1" = "ami-456"
  }, "eu-west-3", "ami-default")  # "ami-123"

  # element : accéder à un élément avec index cyclique
  az = element(["a", "b", "c"], 4)  # "b" (index 4 % 3 = 1)

  # contains : vérifier la présence d'un élément
  has_prod = contains(["dev", "staging", "prod"], "prod")  # true

  # concat : fusionner des listes
  all_cidrs = concat(
    ["10.0.0.0/16"],
    ["172.16.0.0/12"],
    ["192.168.0.0/16"]
  )

  # merge : fusionner des maps
  all_tags = merge(
    { Environment = "prod" },
    { Team = "devops" },
    { ManagedBy = "Terraform" }
  )
  # { Environment = "prod", Team = "devops", ManagedBy = "Terraform" }

  # flatten : aplatir des listes imbriquées
  flat = flatten([["a", "b"], ["c"], ["d", "e"]])
  # ["a", "b", "c", "d", "e"]

  # keys / values : extraire les clés ou valeurs d'un map
  tag_keys   = keys({ Name = "web", Env = "prod" })    # ["Env", "Name"]
  tag_values = values({ Name = "web", Env = "prod" })   # ["prod", "web"]

  # distinct : supprimer les doublons
  unique = distinct(["a", "b", "a", "c", "b"])  # ["a", "b", "c"]

  # sort : trier une liste
  sorted = sort(["banana", "apple", "cherry"])  # ["apple", "banana", "cherry"]

  # zipmap : créer un map à partir de deux listes
  server_map = zipmap(
    ["web", "api", "db"],
    ["10.0.1.1", "10.0.1.2", "10.0.1.3"]
  )
  # { web = "10.0.1.1", api = "10.0.1.2", db = "10.0.1.3" }
}

Fonctions numériques

locals {
  # min / max
  minimum = min(5, 12, 3, 9)  # 3
  maximum = max(5, 12, 3, 9)  # 12

  # abs : valeur absolue
  absolu = abs(-42)  # 42

  # ceil / floor : arrondir
  plafond  = ceil(4.3)   # 5
  plancher = floor(4.7)  # 4

  # signum : signe d'un nombre
  signe = signum(-15)  # -1

  # parseint : convertir une chaîne en entier
  hex_value = parseint("FF", 16)  # 255
}

Fonctions de fichiers

locals {
  # file : lire le contenu d'un fichier
  ssh_key = file("~/.ssh/id_rsa.pub")

  # fileexists : vérifier si un fichier existe
  has_config = fileexists("${path.module}/config.json")

  # templatefile : lire un fichier template avec variables
  user_data = templatefile("${path.module}/templates/user_data.sh", {
    hostname    = "web-01"
    environment = var.environment
    packages    = ["nginx", "htop", "curl"]
  })

  # filebase64 : lire un fichier en base64
  logo_base64 = filebase64("${path.module}/files/logo.png")

  # jsondecode / jsonencode
  config = jsondecode(file("${path.module}/config.json"))
  json_output = jsonencode({
    name = "test"
    port = 8080
  })

  # yamldecode / yamlencode
  k8s_config = yamldecode(file("${path.module}/config.yaml"))
}

Fonctions réseau

locals {
  # cidrsubnet : calculer un sous-réseau
  subnet_1 = cidrsubnet("10.0.0.0/16", 8, 0)   # "10.0.0.0/24"
  subnet_2 = cidrsubnet("10.0.0.0/16", 8, 1)   # "10.0.1.0/24"
  subnet_3 = cidrsubnet("10.0.0.0/16", 8, 255) # "10.0.255.0/24"

  # cidrhost : calculer une adresse IP dans un sous-réseau
  gateway = cidrhost("10.0.1.0/24", 1)  # "10.0.1.1"

  # cidrnetmask : obtenir le masque de sous-réseau
  masque = cidrnetmask("10.0.0.0/16")  # "255.255.0.0"
}

Expressions conditionnelles

HCL supporte les expressions conditionnelles ternaires, similaires à celles de nombreux langages de programmation :

# Syntaxe : condition ? valeur_si_vrai : valeur_si_faux

# Choix du type d'instance selon l'environnement
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

  # Activer le monitoring uniquement en production
  monitoring = var.environment == "prod" ? true : false

  # Nombre de sous-réseaux selon l'environnement
  # (les conditionnelles marchent aussi avec count)
  count = var.create_instance ? 1 : 0
}

# Conditionnel dans les locals
locals {
  # Choix de la taille du volume
  volume_size = var.environment == "prod" ? 100 : var.environment == "staging" ? 50 : 20

  # Valeur par défaut si variable vide
  bucket_name = var.custom_bucket_name != "" ? var.custom_bucket_name : "default-bucket-${var.environment}"

  # Fusionner des tags conditionnellement
  tags = merge(
    local.common_tags,
    var.environment == "prod" ? { CriticalLevel = "high" } : {}
  )
}

Les boucles en HCL

HCL propose plusieurs mécanismes pour créer des ressources multiples ou transformer des données : count, for_each et les expressions for.

count : créer N ressources identiques

count est le mécanisme le plus simple pour créer plusieurs instances d'une même ressource :

# Créer 3 instances EC2
resource "aws_instance" "web" {
  count = 3

  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"

  tags = {
    Name = "web-${count.index}"  # web-0, web-1, web-2
  }
}

# Accéder aux instances créées
output "instance_ips" {
  value = aws_instance.web[*].public_ip  # Splat expression
}

# count conditionnel : créer ou non une ressource
resource "aws_eip" "web" {
  count    = var.assign_elastic_ip ? 1 : 0
  instance = aws_instance.web[0].id
}

# count depuis une variable
variable "server_names" {
  default = ["web", "api", "worker"]
}

resource "aws_instance" "servers" {
  count         = length(var.server_names)
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"

  tags = {
    Name = var.server_names[count.index]
  }
}

for_each : créer des ressources à partir d'un map ou set

for_each est plus flexible que count car chaque ressource est identifiée par une clé unique plutôt que par un index numérique :

# for_each avec un map
variable "instances" {
  default = {
    web = {
      instance_type = "t3.micro"
      ami           = "ami-123456"
    }
    api = {
      instance_type = "t3.small"
      ami           = "ami-789012"
    }
    worker = {
      instance_type = "t3.medium"
      ami           = "ami-345678"
    }
  }
}

resource "aws_instance" "servers" {
  for_each = var.instances

  ami           = each.value.ami
  instance_type = each.value.instance_type

  tags = {
    Name = each.key  # "web", "api", "worker"
  }
}

# Accéder à une instance spécifique
output "web_ip" {
  value = aws_instance.servers["web"].public_ip
}

# for_each avec un set de strings
resource "aws_iam_user" "users" {
  for_each = toset(["alice", "bob", "charlie"])
  name     = each.value
}

# for_each avec un map pour les Security Group rules
variable "ingress_rules" {
  default = {
    http = {
      port        = 80
      description = "HTTP"
    }
    https = {
      port        = 443
      description = "HTTPS"
    }
    ssh = {
      port        = 22
      description = "SSH"
    }
  }
}

resource "aws_security_group_rule" "ingress" {
  for_each = var.ingress_rules

  type              = "ingress"
  from_port         = each.value.port
  to_port           = each.value.port
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.main.id
  description       = each.value.description
}

Expression for : transformer des collections

L'expression for permet de transformer des listes et des maps, similaire aux list comprehensions en Python :

locals {
  # Transformer une liste : mettre en majuscules
  names = ["alice", "bob", "charlie"]
  upper_names = [for name in local.names : upper(name)]
  # ["ALICE", "BOB", "CHARLIE"]

  # Filtrer une liste
  long_names = [for name in local.names : name if length(name) > 3]
  # ["alice", "charlie"]

  # Transformer une liste en map
  name_map = { for name in local.names : name => upper(name) }
  # { alice = "ALICE", bob = "BOB", charlie = "CHARLIE" }

  # Itérer sur un map
  servers = {
    web    = "10.0.1.1"
    api    = "10.0.1.2"
    worker = "10.0.1.3"
  }

  server_urls = { for name, ip in local.servers : name => "http://${ip}:8080" }
  # { web = "http://10.0.1.1:8080", api = "http://10.0.1.2:8080", ... }

  # Transformer avec index
  indexed_names = [for i, name in local.names : "${i}: ${name}"]
  # ["0: alice", "1: bob", "2: charlie"]

  # Grouper par valeur (avec le symbole ...)
  users = {
    alice   = "admin"
    bob     = "dev"
    charlie = "admin"
    david   = "dev"
  }

  users_by_role = {
    for name, role in local.users : role => name...
  }
  # { admin = ["alice", "charlie"], dev = ["bob", "david"] }
}

Les commentaires en HCL

HCL supporte trois styles de commentaires :

# Commentaire sur une ligne (style shell)
// Commentaire sur une ligne (style C) - également valide

/*
  Commentaire sur
  plusieurs lignes
  (style C)
*/

resource "aws_instance" "web" {
  ami           = "ami-123456"     # Commentaire en fin de ligne
  instance_type = "t3.micro"       // Aussi valide en fin de ligne

  /*
  Cette section est temporairement désactivée
  monitoring = true
  ebs_optimized = true
  */
}

Le style # est le plus utilisé dans la communauté Terraform et est considéré comme le standard idiomatique.

Les dynamic blocks

Les dynamic blocks permettent de générer dynamiquement des blocs imbriqués à l'intérieur d'une ressource. Ils sont utiles quand le nombre de blocs imbriqués dépend d'une variable :

variable "ingress_rules" {
  default = [
    {
      port        = 80
      description = "HTTP"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      port        = 443
      description = "HTTPS"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      port        = 22
      description = "SSH"
      cidr_blocks = ["10.0.0.0/8"]
    }
  ]
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Security group pour le serveur web"

  # Bloc dynamic qui génère un bloc "ingress" pour chaque règle
  dynamic "ingress" {
    for_each = var.ingress_rules

    content {
      description = ingress.value.description
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Les expressions de type avancées

Splat expressions

Les splat expressions ([*]) permettent d'extraire un attribut de tous les éléments d'une liste de ressources :

resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-123456"
  instance_type = "t3.micro"
}

# Splat expression : récupérer toutes les IPs
output "all_ips" {
  value = aws_instance.web[*].public_ip
  # ["52.1.2.3", "52.4.5.6", "52.7.8.9"]
}

# Équivalent avec for
output "all_ips_for" {
  value = [for instance in aws_instance.web : instance.public_ip]
}

Opérateurs

locals {
  # Arithmétique
  somme     = 5 + 3      # 8
  produit   = 5 * 3      # 15
  division  = 10 / 3     # 3.333...
  modulo    = 10 % 3     # 1

  # Comparaison
  egal         = 5 == 5    # true
  different    = 5 != 3    # true
  superieur    = 5 > 3     # true
  inferieur    = 3 < 5     # true

  # Logique
  et  = true && false    # false
  ou  = true || false     # true
  non = !true             # false

  # Combinaison
  is_prod_large = var.environment == "prod" && var.instance_count > 10
}

Bonnes pratiques pour écrire du HCL propre

Pour conclure, voici les bonnes pratiques essentielles pour écrire du HCL maintenable et professionnel :

Formatage automatique

Utilisez toujours terraform fmt pour formater votre code de manière cohérente. Intégrez-le dans vos hooks Git ou votre CI/CD :

# Formater tous les fichiers du répertoire courant
terraform fmt

# Formater récursivement
terraform fmt -recursive

# Vérifier le formatage sans modifier (utile en CI)
terraform fmt -check

Validation

Validez toujours la syntaxe de votre configuration avant d'exécuter un plan :

# Valider la syntaxe et la cohérence
terraform validate

Conventions de nommage

  • Utilisez le snake_case pour les noms de ressources, variables et outputs : web_server, instance_count.
  • Utilisez des noms descriptifs et significatifs : préférez aws_instance.web_server à aws_instance.this.
  • Ajoutez des descriptions à toutes les variables et outputs.
  • Préfixez les noms de ressources cloud avec l'environnement ou le projet.

Organisation du code

  • Séparez les variables (variables.tf), les outputs (outputs.tf) et les locals (locals.tf) dans des fichiers dédiés.
  • Regroupez les ressources liées dans le même fichier (par exemple, toutes les ressources réseau dans networking.tf).
  • Utilisez les modules pour encapsuler la logique réutilisable.
  • Ne codez jamais les valeurs en dur : utilisez des variables pour tout ce qui peut changer entre les environnements.

Conclusion

Vous disposez maintenant d'une compréhension solide du langage HCL et de ses capacités. De la syntaxe de base (blocs, arguments, expressions) aux fonctionnalités avancées (boucles for_each, dynamic blocks, fonctions built-in), HCL offre un équilibre remarquable entre simplicité et expressivité.

La clé pour maîtriser HCL est la pratique. Commencez par des configurations simples, puis explorez progressivement les fonctionnalités avancées au fur et à mesure que vos besoins se complexifient. N'hésitez pas à consulter la documentation officielle de Terraform qui détaille chaque fonction et chaque expression disponible.

Dans les prochains articles de cette série, nous aborderons des sujets plus avancés comme les modules Terraform, la gestion du state et les bonnes pratiques en équipe. En attendant, ouvrez votre éditeur et commencez à expérimenter avec les exemples de cet article !

Cet article fait partie de la série Terraform sur CodeClan. Partagez vos configurations et vos questions en commentaire, et restez connectés pour les prochains épisodes de cette série !

Vous vous êtes abonné avec succès à CodeClan
Parfait ! Ensuite, complétez le paiement pour obtenir un accès complet à tout le contenu premium.
Erreur ! Impossible de s'inscrire. Lien invalide.
Bienvenue ! Vous vous êtes connecté avec succès.
Erreur ! Impossible de se connecter. Veuillez réessayer.
Succès ! Votre compte est entièrement activé, vous avez maintenant accès à tout le contenu.
Erreur ! Le paiement Stripe a échoué.
Succès ! Vos informations de facturation sont mises à jour.
Erreur ! La mise à jour des informations de facturation a échoué.