Dans cet article, je vous propose de vous présenter Terraform, un outil que j’utilise depuis 2018 afin de provisionner les infrastructures que je monte pour mes clients.
Voyons d’abord à quelle problématique répond un outil tel que Terraform.

Problème à résoudre

Foo vient de créer mon infrastructure sur AWS, cool 😎

À coup, de clique/clique tout est allée super vite, en une journée tout est terminée, nous sommes vendredi, Foo peut partir en vacances l’esprit tranquille.
Lundi matin, Bar revient de vacances et prend la relève de Foo. Malheureusement, il fait une mauvaise manipulation ; et ne sait pas comment recréer les ressources AWS que Foo avait créé la semaine dernière.

C’est cette problématique à laquelle répond le principe d’Infrastructure as Code (IaC). Terraform est un outil parmi d’autre permettant de faire du IaC dans la partie “Provisioning”.

J’ai personnellement utilisé Terraform pour monter l’ensemble des briques sur AWS pour le site de Clever Tech Ware 😉

Solution

On utilisera Terraform pour décrire l’infrastructure que l’on souhaite créer. Terraform va ensuite se charger de créer les ressources sur AWS. On y rajoutera une petite brique d’intégration continue pour automatiser le déploiement de notre infrastructure à chaque merge sur la branche principale de notre repository GIT.

Prérequis

  • Un compte AWS
  • une CLI AWS configurée

Présentation du principe de Terraform

Terraform est un outil, écrit en Go, qui permet de décrire l’infrastructure que l’on souhaite créer. On décrit dans un langage, appelé HCL (HashiCorp Configuration Language), les ressources que l’on souhaite créer. Terraform va ensuite se charger de créer les ressources sur AWS, Azure, GCP, etc.

Pour ce faire, il se base sur le principe de réconciliation d’état. Il va créer un fichier représentant l’état (state) qui va permettre de garder une trace de l’infrastructure créée.

Terraform state principle

⚠️Ce fichier d’état est très important, il permet à Terraform de savoir quelles ressources il doit créer, mettre à jour ou supprimer.

Au final, avec Terraform comme tout outil de réconciliation d’état (ex : Kubernetes), il faut retenir une seule commande : terraform apply.

Installation de Terraform

Pour installer Terraform, il suffit de télécharger l’archive correspondant à votre système d’exploitation sur le site de Terraform.

Une fois l’archive téléchargée, il suffit de la décompresser et de placer l’exécutable dans votre PATH.

curl -LO https://releases.hashicorp.com/terraform/1.6.1/terraform_1.6.1_linux_amd64.zip
unzip terraform_1.6.1_linux_amd64.zip
sudo chmod +x terraform
sudo mv terraform /usr/local/bin/

Exemple simple - Création d’une instance EC2

Pour cet exemple, nous allons créer un bucket S3 sur AWS. Pour ce faire, nous allons créer le fichier main.tf suivant :

provider "aws" {
  region = "eu-west-3"
}

# Allow the instance to receive requests on port 8080.
resource "aws_security_group" "instance" {
  
  # Allow inbound traffic from port 8080.
  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
  
  # Allow all outbound traffic.
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

# Deploy an EC2 Instance.
resource "aws_instance" "example" {
  # Run Amazon Linux 2023 on the EC2 instance.
  ami                    = "ami-0a4b7ff081ca1ded9"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.instance.id]

  # When the instance boots, start a web server on port 80 that responds with "Hello, World from xxxxx".
  user_data = <<EOF
#!/bin/bash
# Use this for your user data (script from top to bottom)
# install httpd (Linux 2 version)
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello World from $(hostname -f)</h1>" > /var/www/html/index.html
EOF
  
  tags = {
    Name = "Terraform instance"
  }
}

# Output the instance's public IP address.
output "public_ip" {
  value = aws_instance.example.public_ip
}

Ensuite, nous allons initialiser Terraform :

terraform init

Cette commande va télécharger les plugins nécessaires à l’exécution de notre fichier main.tf.

terraform apply

Cette commande va créer le bucket S3 sur AWS et un fichier terraform.tfstate qui va contenir l’état de notre infrastructure.

On peut tester que notre instance EC2 est bien accessible en allant sur l’URL suivante : http://<public_ip>.

Vous ne devriez pas pouvoir accéder à l’instance EC2 car nous n’avons pas ouvert le port 80 dans le Security Group, mais 8080. Erreur volontaire 😉

Pour corriger votre erreur, modifier désormais le SG créé en éditant la section resource "aws_security_group" "instance" dans le fichier main.tf de la manière suivante :

# Allow the instance to receive requests on port 80.
resource "aws_security_group" "instance" {
  
  # Allow inbound traffic from port 8080.
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
  
  # Allow all outbound traffic.
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

Pour mettre à jour notre infrastructure, il suffit de relancer la commande terraform apply. Terraform va détecter que le Security Group a été modifié et va mettre à jour l’infrastructure en conséquence.

terraform apply
...
...
Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_security_group.instance: Modifying... [id=sg-xxxxxxxxxxxxxxxxx]
aws_security_group.instance: Modifications complete after 1s [id=sg-xxxxxxxxxxxxxxxxx]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Désormais vous devriez voir le nom de votre instance lors de l’appel curl:

curl http://<public_ip>
<h1>Hello World from ip-xx-xx-xx-xx.eu-west-3.compute.internal</h1>

Pour supprimer l’infrastructure, il suffit de lancer la commande :

terraform destroy

Cette commande va supprimer l’ensemble des resources AWS créées précédemment et mettre à jour le fichier terraform.tfstate en supprimant l’instance EC2 et le security group.

Remarque - script cloud init

Si vous préférez séparer vos scripts cloud init de votre code de déploiement Terraform, il est totalement possible de le faire en utilisant le paramètre user_data de la manière suivante :

resource "aws_instance" "example" {
  # Run Amazon Linux 2023 on the EC2 instance.
  ami                    = "ami-0a4b7ff081ca1ded9"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.instance.id]

  # When the instance boots, start a web server on port 80 that responds with "Hello, World from xxxxx".
  user_data = file("user_data.sh")
  
  tags = {
    Name = "Terraform instance"
  }
}

Et vous mettrez dans le fichier user_data.sh le script cloud init que vous souhaitez exécuter:

#!/bin/bash
# Use this for your user data (script from top to bottom)
# install httpd (Linux 2 version)
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello World from $(hostname -f)</h1>" > /var/www/html/index.html

Pour aller plus loin

Un projet Terraform représente en général une infrastructure complète. Il est donc nécessaire de pouvoir découper son code en plusieurs fichiers. Pour ce faire, Terraform propose plusieurs autres concepts telle que les modules, les variables ou encore les outputs.

Utilisation de variables

Il faut dans un premier temps déclarer les variables dans un fichier variables.tf :

variable "instance_type" {
  description = "Instance type"
  type        = string
  default     = "t2.micro"
}   

Pour utiliser les variables, il faut les referencer via le préfixe var dans le fichier main.tf de la manière suivante :

# Deploy an EC2 Instance.
resource "aws_instance" "instance" {
  # Run Amazon Linux 2023 on the EC2 instance.
  ami                    = "ami-0a4b7ff081ca1ded9"
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.instance.id]

  # When the instance boots, start a web server on port 80 that responds with "Hello, World from xxxxx".
  user_data = <<EOF
#!/bin/bash
# Use this for your user data (script from top to bottom)
# install httpd (Linux 2 version)
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello World from $(hostname -f)</h1>" > /var/www/html/index.html
EOF
  
  tags = {
    Name = "Terraform instance"
  }
}

Appliquer désormais le script Terraform :

terraform apply

Si vous souhaitez modifier ou surcharger les valeurs par défaut des variables, il faut les définir dans un fichier <xxxxx>.tfvars. Créer le fichier variables.tfvars de la manière suivante :

instance_type = "t2.small"

Ensuite, il suffit de lancer la commande terraform apply en précisant le fichier de variables à utiliser :

terraform apply -var-file="variables.tfvars"

Utilisation de data

Les data permettent de récupérer des informations sur des ressources spécifiques à un provider telle qu’AWS. Pour utiliser des data, il suffit de les définir dans un fichier data.tf de la manière suivante :

data "aws_ami" "amazon_linux" {
  most_recent = true

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

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

  owners = ["amazon"]
}

Pour utiliser les datas, il suffit ensuite d’y faire référence dans le fichier main.tf de la manière suivante :

# Deploy an EC2 Instance.
resource "aws_instance" "instance" {
  # Run Amazon Linux 2023 on the EC2 instance.
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.instance.id]

  # When the instance boots, start a web server on port 80 that responds with "Hello, World from xxxxx".
  user_data = <<EOF
#!/bin/bash
# Use this for your user data (script from top to bottom)
# install httpd (Linux 2 version)
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello World from $(hostname -f)</h1>" > /var/www/html/index.html
EOF
  
  tags = {
    Name = "Terraform instance"
  }
}

Utilisation des outputs

Un output permet de récupérer des informations sur des ressources créées par Terraform. Pour utiliser des outputs, il suffit de les definir de la manière suivante :

output "public_ip" {
  value = aws_instance.example.public_ip
}

C’est notamment ce que nous avons utilisé dans l’exemple précédent pour récupérer l’IP publique de notre instance EC2.

Terraform et son système de fichiers

Il faut savoir qu’un projet terraform n’est pas limité à un seul fichier main.tf. Il est possible de découper son code en plusieurs fichiers.

Je ne connais pas de convention officielle pour découper son code Terraform.

Cela étant dit, à titre personnel, je découpe mon code Terraform de la manière suivante :

  • main.tf : contient la définition du provider que je vais utiliser, la définition du backend ainsi que les resources globale à mon projet,

Ex : récupérer l’identifiant AWS de mon compte.

  • <ressource_fonctionnelle>.tf : contient l’ensemble des resources AWS dont j’ai besoin pour répondre à un de mes besoins fonctionnelles,

Ex : buckets.tf contient l’ensemble des resources de type bucket S3 dont j’ai besoin pour mon projet.

  • variables.tf : contient l’ensemble des variables
  • outputs.tf : contient l’ensemble des outputs

Création d’un module

Un module Terraform permet de découper son code en plusieurs parties. Cela permet de réutiliser du code et de le rendre plus lisible.

Pour créer un module, on peut créer un dossier contenant les fichiers suivants :

mkdir -p modules/ec2
touch modules/ec2/main.tf
touch modules/ec2/variables.tf
touch modules/ec2/outputs.tf

Dans le fichier main.tf, on va décrire les resources AWS de l’infrastructure que le module aura pour but de créer :

# Allow the instance to receive requests on port ingress_port_allowed.
resource "aws_security_group" "instance" {
  ingress {
    from_port        = var.ingress_port_allowed
    to_port          = var.ingress_port_allowed
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

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

# Deploy an EC2 Instance.
resource "aws_instance" "instance" {
  # Run Amazon Linux 2023 on the EC2 instance.
  ami                    = var.ami
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.instance.id]

  # When the instance boots, start a web server on port 80 that responds with "Hello, World from xxxxx".
  user_data = var.user_data
}

Dans le fichier variables.tf, on va définir les variables que l’on souhaite pouvoir paramétrer :

variable "ami" {
  description = "AMI to use for the instance"
  type = string
}

variable "instance_type" {
  description = "Instance type"
  type        = string
}

variable "user_data" {
  description = "User data to use for the instance"
  type = string
}

variable "ingress_port_allowed" {
  description = "Ingress port from where income traffic is allowed"
  type = number
}

Dans le fichier outputs.tf, on va définir les outputs que l’on souhaite récupérer :

output "public_ip" {
  value = aws_instance.instance.public_ip
}

Utilisation d’un module

Pour utiliser un module, il suffit de l’appeler dans le fichier main.tf de la manière suivante :

provider "aws" {
  region = "eu-west-3"
}

module "ec2_instance" {
  source = "./modules/ec2"

  ami                  = "ami-0a4b7ff081ca1ded9"
  instance_type        = "t2.micro"
  ingress_port_allowed = 80
  user_data            = file("user_data.sh")
}

# Output the instance's public IP address.
output "public_ip" {
  value = module.ec2_instance.public_ip
}

Dans cet exemple, on choisit d’utiliser un script cloud init stocké dans un fichier user_data.sh. Il faut donc créer le fichier user_data.sh à la racine du projet Terraform :

#!/bin/bash
# Use this for your user data (script from top to bottom)
# install httpd (Linux 2 version)
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello World from $(hostname -f)</h1>" > /var/www/html/index.html

Il ne reste plus qu’à créer l’infrastructure :

terraform init  # Permet d'initialiser Terraform en téléchargeant les plugins et modules nécessaires. 
terraform apply

Conclusion

Dans cet article, nous avons vu comment utiliser Terraform pour créer une infrastructure sur AWS. Nous avons vu comment créer un module Terraform et comment utiliser des variables, des data et des outputs.

Resources utiles