Les cours sont maintenus et donnés par : https://www.alterway.fr/cloud-consulting

Utilisation de code combiné à des API pour créer, gérer et supprimer des ressources
Appliquer les bonnes pratiques du développement dans la gestion de l’infrastructure (tests, revues de code…)
🟢 Objectif : automatiser la gestion de l’infrastructure
Versionning
Réutilisabilité
Reproductibilité
Automatisation
Gain de temps
Idempotence
Prédictibilité
Intégration aux outils de CI/CD
Outil d’IaC open source publié par HashiCorp
Utilisé pour gérer les infrastructures grâce à des configurations déclaratives
Cloud agnostic (Amazon, Google, Azure, ...) (Via des providers)
Utilise le langage HCL
Permet de bénéficier des avantages de l’IaC
Gère tout type de ressources : stockage, réseau, entrées DNS, Vms, PaaS...
En 2022 : ~2098 providers (35 Officiels, 206 Vérifiés, 1857 Communautaires )
Terraform utilise un DSL spécifique : HCL (HashiCorp Configuration Language)
Terraform est outils orienté plugin
Terraform a un support natif pour les modules et les remote states
Terraform fournit une abstraction de haut niveau de l'infrastructure au travers des resources
Terraform peut gérer du IAAS, PAAS, SAAS
Terraform peut faire du dry-run (plan vs apply)
Terraform peut gérer tout type de resources qui a une api
Exemple :
| Procedural | Déclaratif | |
|---|---|---|
| Chef | CFEngine | |
| Ansible | Salstack | |
| Pulumi | Terraform | |
| CloudFormation | ||
| CDKTF | Pulumi |
Pulumi : Interface impérative avec un moteur déclaratif
Terraform : Idem via CDKTF : Interface impérative avec un moteur déclaratif
Ouvrir le lien https://learn.hashicorp.com/tutorials/terraform/install-cli
Suivre les instructions pour installer Terraform CLI selon le système d’exploitation
Vérifier la version installée avec la commande : terraform version
terraform version
Terraform v1.2.1
on darwin_amd64Binaire écrit en Go, open source
Ligne de commande qui communique avec des plugins Terraform au travers du protocole RPC (Remote Procedure Call)
Interface commune qui supporte différents fournisseurs cloud et fournisseurs de services
Gère les fichiers d’état des ressources (states)
Construit le graphe des ressources (Dépendances entre les ressources)
Binaire Go invoqué par Terraform Core
Communication avec les API des fournisseurs de services (ex: AWS, GCP, Azure, Docker, K8s, etc.)
Possibilité d’en utiliser plusieurs
Initialise et installe les dépendances nécessaires
Gère l’authentification
Exécute les commandes et les scripts
Liste des différents providers disponibles : https://registry.terraform.io/browse/providers
Les fichiers ayant l'extension .tf ou tf.json sont considérés comme des fichiers de configuration et donc traités par Terraform.
Terraform s'execute toujours dans un contexte de single root module.Une configuration complète est donc constituée d'un root module et de plusieurs child modules.
Fichier providers.tf :
terraform {
required_version = ">=1.0.1"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">=3.65.0"
}
}
}
provider "aws" {
region = "us-east-1"
access_key = "localstacktest"
secret_key = "localstacktestkey"
skip_credentials_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
s3_use_path_style = true
endpoints {
ec2 = "http://192.168.64.11:31566"
iam = "http://192.168.64.11:31566"
}
}
init : initialise l'environnement Terraform (local). Habituellement exécuté une seule fois par session.
plan : compare l'état de Terraform avec l'état tel quel dans le cloud, créé et affiche un plan d'exécution. Cela ne change pas le déploiement (lecture seule)
apply : appliquer le plan de la phase de planification. Cela modifie potentiellement le déploiement (lecture et écriture).
destroy : Détruire toutes les ressources régies par cet environnement de terraformation spécifique.
terraform.tfstate{
"version": 4,
"terraform_version": "1.1.9",
"serial": 56,
"lineage": "0b723a72-0399-a30e-7933-514a923eed6f",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_dynamodb_table",
"name": "main",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": []
},
{
"mode": "managed",
"type": "aws_iam_instance_profile",
"name": "ec2_profile",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"arn": "arn:aws:iam::000000000000:instance-profile/ec2_profile",
"create_date": "2022-05-17T17:14:27Z",
"id": "ec2_profile",
"name": "ec2_profile",
"name_prefix": null,
"path": "/",
...Une opération est une requête API pour créer, lire, mettre à jour ou supprimer une ressource
Backend par défaut
Ne requiert aucune configuration
Le fichier d’état est stocké dans un fichier texte dans le répertoire courant
🔴 À proscrire en production
🟢 À privilégier
Permet l’utilisation d’un espace de stockage distant pour stocker le fichier d’état
Facilite le travail en équipe
terraform {
backend "gcs" {
bucket = "tf-state-prod"
prefix = "terraform/state"
}
}
terraform {
backend "s3" {
bucket = "mybucket"
key = "path/to/my/key"
region = "us-east-1"
}
}
terraform {
backend "azurerm" {
resource_group_name = "StorageAccount-ResourceGroup"
storage_account_name = "abcd1234"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}Se produit à chaque opération qui écrit dans le fichier d’état
Évite la corruption de l’état
Évite que plusieurs opérations d’écriture s’exécutent en simultanées
Possibilité de désactiver le verrouillage (non recommandé)
🔴 Tous les backends ne prennent pas en compte le verrouillage du fichier d’état
terraform.tfstate) gérés de manière indépendanteLes workspaces pemettent de gérer différents environnements sans modifier les configurations
Les espaces de travail sont le successeur des anciens Terraform Environments
resource "azurerm_resource_group" "rg_1" {
name = "a-herlec-rg-${terraform.workspace}-01"
location = "northeurope"
tags = {
createdBy = "herlec"
BU = "DT"
}
}
❯ tree -a terraform.tfstate.d
terraform.tfstate.d
├── Production
│ └── terraform.tfstate
└── Staging
├── terraform.tfstate
└── terraform.tfstate.backup
2 directories, 3 files
initÀ exécuter avant toute autre commande ou après l’ajout de nouvelles ressources
Prépare le répertoire pour l’utilisation de Terraform
🟢 Bonne pratique : l’exécuter souvent
planGénère un plan d’exécution
Met à jour le fichier d’état avec l’état courant des ressources
Compare la configuration à l’état des ressources dans le fichier d’état
Affiche les changements qui vont intervenir
apply (1)
apply (2)
terraform init
# Voir les fichiers / répertoires créés dans le répertoire courant
.terraform
└── providers
└── registry.terraform.io
└── hashicorp
└── aws
└── 4.14.0
└── darwin_amd64
└── terraform-provider-aws_v4.14.0_x5destroy (1)Supprime les ressources définies dans le plan (gérées par terraform)
Utile surtout pour les environnements éphémères
destroy (2)
Une configuration Terraform est un fichier texte qui contient les définitions des ressources d'infrastructure.
Il est possible d'écrire des configurations Terraform au format HCL (avec l'extension .tf) ou au format JSON (avec l'extension .tf.json).
Les ressources sont les éléments de base d'une configuration Terraform.
Les ressources sont spécifiques au provider, de sorte qu'une ressource pour le provider AWS est différente d'une ressource pour OpenStack.
Élément le plus important dans Terraform
Chaque ressource décrit un ou plusieurs éléments d’infrastructure
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
}Permettent à Terraform d’utiliser des informations qu’il n’a pas définit
🤔 Chaque provider Terraform peut offrir ses propres data sources
variable "vpc_id" {}
data "aws_vpc" "selected" {
id = var.vpc_id
}
resource "aws_subnet" "example" {
vpc_id = data.aws_vpc.selected.id
availability_zone = "us-west-2a"
cidr_block = cidrsubnet(data.aws_vpc.selected.cidr_block, 4, 1)
}
data "github_repository_pull_requests" "pull_requests" {
base_repository = "example-repository"
base_ref = "main"
state = "open"
}
module “preview-environment” {
for_each = data.github_repository_pull_requests.pull_requests.results
name = each.value.title
commit_sha = each.value.head_sha
// ...
}filter (1)AWS :
Les filtres permettent de faire le tri et de récupérer les informations nécessaires, utiles pour les données externes.
data "aws_ami" "example" {
executable_users = ["self"]
most_recent = true
name_regex = "^myami-\\d{3}"
owners = ["self"]
filter {
name = "name"
values = ["myami-*"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}filter (2)
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "image-type"
values = ["machine"]
}
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*"]
}
}filter (3)
data "aws_ec2_transit_gateway" "tgw" {
filter {
name = "tag:Name"
values = ["wahlnetwork-tgw-prod"]
}
}Trouver la page de documentation Terraform de la ressource : aws_instance
Copier le premier exemple dans un fichier main.tf
Lancer la commande : terraform init
Lancer la commande : terraform plan
Lancer la commande terraform apply
Voir le nouveau fichier créé terraform.tfstate
Aller sur la console AWS ou Azure et vérifier que la ressource est créée
Une ligne : # or //
Multiple ligne : /* ... */
/*
This module creates a new AWS instance.
If a VPC ID is not specified, a default VPC will be created.
...
*/
locals {
# Common tags to be assigned to all resources
common_tags = {
Service = local.service_name
Owner = local.owner
}
}
resource "aws_instance" "example" {
# ...
tags = local.common_tags
}12 factor app: Il doit y avoir une séparation stricte entre la configuration et le code
Paramètres pour personnaliser le code source
Possibilité de définir des règles de validation personnalisées
["us-west-1a", "us-west-1c"] - ["c", "b", "b"]{name = "Mabel", age = 52}["c", "b"] (Pas de duplication possible de valeurs) (fonction toset())dans fichier tf :
variable "rg_name" {
type = string
description = "Resource Group Name - Must be unique in a subscription"
default = "a-terraform-training-01"
}Command line
terraform plan/apply -var rg_name=a-terraform-training-03Fichier de variables:
terraform.tfvars*.auto.tfvarsterraform apply -auto-approve -var-file=file-var.tfvarsTF_VAR_<var-name>="a-value" terraform plan
# eg.
TF_VAR_rg_name="a-terraform-training-10" terraform plantfSimples
number_of_servers = 10
prefix = "dev"Maps, Objects etc
# fichier project.auto.tfvars
project_data = {
drupal-test = {
harbor_public = "false"
harbor_vulnerability_scanning = "true"
harbor_enable_content_trust = "false"
gitlab_visibility_level = "internal"
gitlab_namespace_id = 27
},
}
# Déclaration de la la variable
variable "project_data" {
type = map(object({
harbor_public = string
harbor_vulnerability_scanning = string
harbor_enable_content_trust = string
gitlab_visibility_level = string
gitlab_namespace_id = number
}))
}string par défautvariable "rg_name" {
type = string
description = "Resource Group Name"
default = "a-terraform-training-01"
}est également équivalent à
variable "rg_name" {
description = "Resource Group Name"
default = "a-terraform-training-01"
}string heredocvariable "app_description" {
type = string
description = "application description"
default = <<EOT
Welcome !
Application version is %%app_version
EOT
}numbervariable "the_counter" {
type = number
description = "# iteration"
default = 4
}boolvariable "trueOrFalse" {
type = bool
description = "true or false"
default = true
}listvariable "eu_locations" {
type = list
description = "location list"
default = ["westeurope","northeurope"]
}mapvariable "hel" {
type = map
description = "hel person"
default = {
surname = "Hervé"
name = "Leclerc"
role = "CTO"
}
}objectvariable "whoishel" {
type = object ({
surname = string
name = string
kids = number
skills = list (string)
}
)
description = "who is hel"
default = {
surname = "Hervé"
name = "Leclerc"
kids = 3
skills = [
"kubernetes",
"docker",
"terraform"
]
}
}
variable "db_password" {
description = "Database administrator password."
type = string
sensitive = true
}
- Suppression des output console et journal
- Permet d'éviter la divulgation accidentel de valeurs sensibles
- Mais ce n'est pas suffisant pour sécuriser les configurations terraform
🟢 Bonnes pratiquesCréer le fichier variables.tf
Créer le fichier formation.tfvars
depends_on :
count :
provider
for_each :
lifecycle :
create_before_destroy (par défaut terraform détruire puis crée)prevent_destroy (terraform lancera une erreur si une ressource est détruite !! impossible d'utiliser terraform destroy)ignore_changes (si quelque chose est modifié en externe, terraform ne modifiera pas la ressource)
resource "azurerm_resource_group" "rg_1" {
name = var.rg_01
location = var.location
tags = var.tags
lifecycle {
create_before_destroy = true
}
}
resource "azurerm_resource_group" "rg_2" {
name = var.rg_02
location = var.location
tags = var.tags
lifecycle {
prevent_destroy = true
}
}
resource "azurerm_resource_group" "rg_3" {
name = var.rg_03
location = var.location
tags = var.tags
lifecycle {
ignore_changes = [ tags,]
}
}
### Default Provider
provider "google" {
region = "us-central1"
}
### Another Provider
provider "google" {
alias = "europe"
region = "europe-west1"
}
### Referencing the other provider
resource "google_compute_instance" "example" {
provider = google.europe
}Valeurs de retour
Utile pour exposer un sous-ensemble d’attributs de ressource à un module parent.
Utile pour afficher certaines valeurs dans la sortie CLI après avoir exécuté terraform apply
Dans le cas de backend distant, les outputs du module racine sont accessibles par d'autres configurations via une source de données terraform_remote_state
### Output variable which will store the arn of instance
### and display after terraform apply command.
output "ec2_arn" {
## Value depends on resource name and type (same as that of main.tf)
value = aws_instance.my-machine.arn
}
### Output variable which will store instance public IP
### and display after terraform apply command
output "instance_ip_addr" {
value = aws_instance.my-machine.public_ip
description = "The public IP address of the main server instance."
}Créer le fichier outputs.tf
Créer l’ouput “public_dns”
Exposer la valeur de l’attribut public_dns
Exécuter la commande : terraform init
Exécuter la commande : terraform plan
Exécuter la commande : terraform apply
Quelle commande affiche la valeur de l’output
Quelle est la différence entre les variables et les locals dans Terraform
Variable : si vous souhaitez transmettre une valeur à Terraform depuis l'extérieur, utilisez des variables.
Variables locales : Elles sont internes au fichier Terraform. on ne peut pas les passer depuis l'extérieur.
Que peut on faire avec variables locales ?
Assigne un nom à une expression
Réutilisable dans la configuration
Permet d’éviter la répétition
locals {
service_name = "redis"
resilient = true
}
locals {
# Common tags to be assigned to all resources
common_tags = {
Service = local.service_name
Owner = local.owner
}
}
locals {
# Ids for multiple sets of EC2 instances, merged together
instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}
variable "project_name" {
type = string
}
variable "environment" {
type = string
}
locals {
name-prefix = "${var.project_name}-${var.environment}"
}
resource "aws_s3_bucket" "default" {
bucket = "${local.name-prefix}-bucket"
acl = "private"
tags = {
Name = "${local.name-prefix}-bucket"
}
}Terraform inclut un nombre de fonctions intégrées
Il n’est pas possible de créer ses propres fonctions
forPour faire des blocs dynamiques il faut :
Des "Collections" : list, map, set
Des "Iterator" (optionnel) : variable temporaire qui représente un élément de la collection
Un "Content" : Un bloc sur lequel on itèreresource "aws_security_group" "example" {
name = "example" # can use expressions here
dynamic "ingress" {
for_each = var.service_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
}
}
}
resource "aws_security_group" "example" {
name = "example" # can use expressions here
dynamic "ingress" {
for_each = var.service_ports
iterator = "service_port"
content {
from_port = service_port.value
to_port = service_port.value
protocol = "tcp"
}
}
}loops)Graphe des dépendances
Généré par Terraform Core
Construit à partir des fichiers de configuration
Utilisé par Terraform pour : gérer les dépendances entre les ressources, la gestion du fichier d’état…
terraform graph
terraform graph | dot -Tsvg > graph.svg
Un module est un "conteneur" pour plusieurs ressources qui sont utilisées ensemble
Un chapitre est entièrement consacré aux un modules
Avantanges des modules :
Une syntaxe terraform spécifique pour référencer les attributs ou les arguments d'autres ressources (ou de soi-même)
Une séquence ${ ... } est une interpolation, qui évalue l'expression donnée entre les marqueurs, convertit le résultat en une chaîne si nécessaire, puis l'insère dans la chaîne finale :
"Hello, ${var.name}!"Un peu plus complexe avec des directives %{if <BOOL>}/%{else}/%{endif} :
"Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"
output "a1" {
value = true ? "is true" : "is false"
}
output "a2" {
value = false ? "is true" : "is false"
}
output "a3" {
value = 1 == 2 ? "is true" : "is false"
}
output "b1" {
value = contains(["a","b","c"], "d") ? "is true" : "is false"
}
output "b2" {
value = keys({a: 1, b: 2, c: 3}) == ["a","b","c"] ? "is true" : "is false"
}
output "b3" {
value = contains(keys({a: 1, b: 2, c: 3}), "b") ? "is true" : "is false"
}
variable "create1" {
default = true
}
resource "random_pet" "pet1" {
count = var.create1 ? 1 : 0
length = 2
}
output "pet1" {
value = random_pet.pet1
}variable "enable_autoscaling" {
description = "If set to true, enable auto scaling"
type = bool
}
resource "aws_autoscaling_schedule" "scale_out_business_hours" {
count = var.enable_autoscaling ? 1 : 0
scheduled_action_name = "scale-out-during-business-hours"
min_size = 2
max_size = 10
desired_capacity = 10
recurrence = "0 9 * * *"
autoscaling_group_name = aws_autoscaling_group.example.name
}count et for_each
resource "null_resource" "simple" {
count = 2
}
output "simple" {
value = null_resource.simple
}
locals {
names = ["bob", "kevin", "stewart"]
}
resource "null_resource" "names" {
count = length(local.names)
triggers = {
name = local.names[count.index]
}
}
output "names" {
value = null_resource.names
}
variable "user_names" {
description = "Matrix name"
type = list(string)
default = ["neo", "trinity", "morpheus"]
}
resource "null_resource" "for" {
# Changes to any instance of the cluster requires re-provisioning
triggers = {always_run = "${timestamp()}"}
count = length(var.user_names)
provisioner "local-exec" {
command = "echo SON NOM EST = ${var.user_names[count.index]}"
}
}locals {
heights = {
bob = "short"
kevin = "tall"
stewart = "medium"
}
}
resource "null_resource" "heights" {
for_each = local.heights
triggers = {
name = each.key
height = each.value
}
}
output "heights" {
value = null_resource.heights
}
variable "car" {
description = "Cars name"
type = list(string)
default = ["bmw", "mercedes", "maserati"]
}
resource "null_resource" "each" {
# Changes to any instance of the cluster requires re-provisioning
triggers = {always_run = "${timestamp()}"}
for_each = toset(var.car)
provisioner "local-exec" {
command = "echo LA VOITURE EST = ${each.value}"
}
}
output "all_cars" {
value = null_resource.each
}ariable "names" {
description = "A list of names"
type = list(string)
default = ["neo", "trinity", "morpheus"]
}
output "upper_names" {
value = [for name in var.names : upper(name)]
}variable "hero_thousand_faces" {
description = "map"
type = map(string)
default = {
neo = "hero"
trinity = "love interest"
morpheus = "mentor"
}
}
output "bios" {
value = [for name, role in var.hero_thousand_faces : "${name} is the ${role}"]
}Comment dynamiser un truc comme ca
resource "aws_security_group" "simple" {
name = "demo-simple"
description = "demo-simple"
ingress {
description = "description 0"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "description 1"
from_port = 81
to_port = 81
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}locals {
ports = [80, 81]
}
resource "aws_security_group" "dynamic" {
name = "demo-dynamic"
description = "demo-dynamic"
dynamic "ingress" {
for_each = local.ports
content {
description = "description ${ingress.key}"
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}locals {
rules = [{
description = "description 0",
port = 80,
cidr_blocks = ["0.0.0.0/0"],
},{
description = "description 1",
port = 81,
cidr_blocks = ["10.0.0.0/16"],
}]
}
resource "aws_security_group" "attrs" {
name = "demo-attrs"
description = "demo-attrs"
dynamic "ingress" {
for_each = local.rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
output "map" {
value = aws_security_group.map
}Tout est dans la structure des données
locals {
groups = {
example0 = {
description = "sg description 0"
rules = [{
description = "rule description 0",
port = 80,
cidr_blocks = ["10.0.0.0/16"],
},{
description = "rule description 1",
port = 81,
cidr_blocks = ["10.1.0.0/16"],
}]
},
example1 = {
description = "sg description 1"
rules = [{
description = "rule description 0",
port = 80,
cidr_blocks = ["10.2.0.0/16"],
},{
description = "rule description 1",
port = 81,
cidr_blocks = ["10.3.0.0/16"],
}]
}
}
}
resource "aws_security_group" "this" {
for_each = local.groups
name = each.key # top-level key is security group name
description = each.value.description
dynamic "ingress" {
for_each = each.value.rules # List of Maps with rule attributes
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
output "security_groups" {
value = aws_security_group.this
}locals {
groups = {
example0 = {
description = "sg description 0"
},
example1 = {
description = "sg description 1"
}
}
rules = {
example0 = [{
description = "rule description 0",
port = 80,
cidr_blocks = ["10.0.0.0/16"],
},{
description = "rule description 1",
port = 81,
cidr_blocks = ["10.1.0.0/16"],
}]
example1 = [{
description = "rule description 0",
port = 80,
cidr_blocks = ["10.2.0.0/16"],
},{
description = "rule description 1",
port = 81,
cidr_blocks = ["10.3.0.0/16"],
}]
}
}
resource "aws_security_group" "this" {
for_each = local.groups
name = each.key # top-level key is security group name
description = each.value.description
dynamic "ingress" {
for_each = local.rules[each.key] # List of Maps with rule attributes
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
output "security_groups" {
value = aws_security_group.this
}Pour faire de la manipulation de données
# List to List
locals {
list = ["a","b","c"]
}
output "list" {
value = [for s in local.list : upper(s)]
}# Map to list
locals {
list = {a = 1, b = 2, c = 3}
}
output "result1" {
value = [for k,v in local.list : "${k}-${v}" ]
}
output "result2" {
value = [for k in local.list : k ]
}# List to Map
locals {
list = ["a","b","c"]
}
output "result" {
value = {for i in local.list : i => i }
}# Map to Map
locals {
list = {a = 1, b = 2, c = 3}
}
output "result" {
value = {for k,v in local.list : k => v }
}# Filtrer une liste de nombre
locals {
list = [1,2,3,4,5]
}
output "list" {
value = [for i in local.list : i if i < 3]
}# Filtrer une Map non consistente
locals {
list = [
{a = 1, b = 5},
{a = 2},
{a = 3},
{a = 4, b = 8},
]
}
output "list" {
value = [for m in local.list : m if contains(keys(m), "b") ]
}variable "hero_thousand_faces" {
description = "map"
type = map(string)
default = {
neo = "hero"
trinity = "love interest"
morpheus = "mentor"
}
}
output "bios" {
value = [for name, role in var.hero_thousand_faces : "${name} is the ${role}"]
}locals {
minions = [{
name: "bob"
},{
name: "kevin",
},{
name: "stuart"
}]
}
output "minions" {
value = local.minions[*].name
}# Filter Map Elements
locals {
list = [
{a = 1, b = 5},
{a = 2, b = 6},
{a = 3, b = 7},
{a = 4, b = 8},
]
}
output "list" {
value = [for m in local.list : values(m) if m.b > 6 ]
}locals {
list = [
"mr bob",
"mr kevin",
"mr stuart",
"ms anna",
"ms april",
"ms mia",
]
}
output "list" {
value = {for s in local.list : substr(s, 0, 2) => s...}
}lookup récupère la valeur d'un seul élément d'une Map, en fonction de sa clé. Si la clé donnée n'existe pas, la valeur par défaut donnée est renvoyée à la place.
locals {
list = [{a = 1}, {b = 2}, {a = 3}]
}
output "list" {
value = [for m in local.list : m if lookup(m, "a", null) != null ]
}locals {
list = [{a = 1}, {b = 2}, {a = 3}]
}
output "list" {
value = [for m in local.list2 : m if contains(keys(m), "a") ]
}Invoque un exécutable local après la création de la ressource
Invoque un processus sur la machine exécutant Terraform et non la ressource
Son utilisation ne doit se fait qu’en dernier recours
variable "owner" {
description = "the owner of this project"
default = "ruan"
}
resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo ${var.owner} > file_${null_resource.this.id}.txt"
interpreter = ["bash", "-c"]
}
}Invoque un script sur la ressource distance, après sa création
Peut être utilisé pour lancer un outil de configuration, une initialisation...
resource "aws_instance" "web" {
# ...
provisioner "remote-exec" {
inline = [
"dnf -y install epel-release",
"dnf -y install htop",
]
}
}
resource "aws_instance" "web" {
# ...
# Copies the myapp.conf file to /etc/myapp.conf
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
}
resource "null_resource" "example_provisioner" {
triggers = {
public_ip = aws_instance.example_public.public_ip
}
connection {
type = "ssh"
host = aws_instance.example_public.public_ip
user = var.ssh_user
port = var.ssh_port
agent = true
}
// copy our example script to the server
provisioner "file" {
source = "files/get-public-ip.sh"
destination = "/tmp/get-public-ip.sh"
}
// change permissions to executable and pipe its output into a new file
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/get-public-ip.sh",
"/tmp/get-public-ip.sh > /tmp/public-ip",
]
}
provisioner "local-exec" {
# copy the public-ip file back to CWD, which will be tested
command = "scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${var.ssh_user}@${aws_instance.example_public.public_ip}:/tmp/public-ip public-ip"
}
}validateValidation statique de la configuration
Ne fait pas d’appels API
Validation syntaxique
Bonne pratique : exécuter cette commande automatiquement dans les systèmes de CI/CD
🔴 Ne peut être exécutée sans initialisation
Tester la commande : terraform validate
Commenter l’instance_type dans le fichier formation.tfvars
Exécuter de nouveau la commande terraform validate
Décommenter la ligne précédemment commentée
fmtFormate les fichiers de configuration
Applique certaines conventions de style Terraform https://www.terraform.io/docs/language/syntax/style.html
🟢 Objectif : Améliorer la lisibilité
Configuration VSCode
"terraform-ls.terraformExecPath": "/usr/local/bin/terraform",
"[terraform]": {
"editor.defaultFormatter": "hashicorp.terraform",
"editor.formatOnSave": true
},
"terraform-ls.experimentalFeatures": {
"validateOnSave": true
}importSert à importer des ressources existantes dans l’état Terraform
Après l’import, Terraform peut les manager
🟠 Il faut saisir tous les paramètres nécessaires pour la ressource. Tester avec un terraform plan jusquà obtenir un plan qui ne contient pas de changement.
TF_LOG
TF_LOG_CORE
TF_LOG_PROVIDER
Dans formation.tfvars, modifier la valeur de l’instance_type
Dans le fichier .env, ajouter : export TF_LOG_PROVIDER=TRACE et lancer un terraform plan
Répéter l’opération en modifiant la valeur de TF_LOG_PROVIDER à DEBUG, INFO, WARN et ERROR
Remettre la valeur de l’instance_type dans le fichier formation.tfvars
Supprimer la dernière ligne ajoute au fichier .env
workspaceLes données stockées dans un backend appartiennent à un workspace
Le backend par défaut se nomme “default”
Possibilité d’avoir plusieurs backends
stateCommande utilisée pour gérer l’état Terraform
Elle a plusieurs sous-commandes
Exécuter les commandes suivantes :
terraform state list
terraform state show aws_instance.web
taint untaintLa commande terraform taint permet de marquer manuellement une ressource comme étant "à problème", ce qui signifie qu'elle sera détruite et recréée lors de la prochaine application de terraform.
terraform untaint permet de supprimer cette condition "à problème" de la ressource.
$ terraform state list
azurerm_network_interface.nic
azurerm_network_security_group.nsg
azurerm_public_ip.pip
azurerm_resource_group.demo-rg
azurerm_subnet.demo-subnet
azurerm_virtual_machine_extension.ext
azurerm_virtual_network.demo-vnet
azurerm_windows_virtual_machine.vm
$ terraform taint azurerm_windows_virtual_machine.myvm
Resource instance azurerm_windows_virtual_machine.myvm has been marked as tainted.
# azurerm_windows_virtual_machine.vm is tainted, so must be replaced
-/+ resource "azurerm_windows_virtual_machine" "myvm" {
~ computer_name = "demo-mytestvm" -> (known after apply)
- encryption_at_host_enabled = false -> null
...
Plan: 2 to add, 0 to change, 2 to destroy.
$ terraform untaint azurerm_windows_virtual_machine.myvm
Resource instance azurerm_windows_virtual_machine.myvm has been successfully untainted.show
refreshTerraform intérroge toutes les resources distantes présentes dans le fichier d'état et le synchronise avec les valeurs des attributs distants. Cette commande ne modifie par les valeurs des attributs distants seul le fichier d'état est modifié (tfstate).
Attention cette commande a été déclarée comme obsolète et sera supprimée dans une future version de terraform car elle n'est pas sûre.
Conteneur qui regroupe plusieurs ressources Terraform ensemble
Peut être appelé à plusieurs endroits et à plusieurs reprises
Évite la duplication de code
Facilite la maintenance
Favorise la réutilisabilité de définitions communes
Local
# Path based
module "service_foo" {
source = "/modules/microservice"
image_id = "ami-12345"
num_instances = 3
}# Terraform registry
module "consul" {
source = "hashicorp/consul/aws"
version = "0.1.0"
}# Public / private module registry
module "rancher" {
source = "https://github.com/objectpartners/tf-modules/rancher/server-standalone-elb-db&ref=9b2e590"
}
Créer un dossier modules/ec2
Créer les fichiers data.tf, instance.tf, variables.tf et outputs.tf
Transférer la configuration du fichier main.tf dans le module
Appeler ce nouveau module créé dans main.tf
Projet simple :
.
├── .gitignore
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── resources.tf
├── provider.tf
├── terraform.tfvars
├── modules/
│ ├── module1/
│ │ ├── README.md
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tfmain.tf qui est le fichier principal d’un projet terraformprovider.tf pour y définir les fournisseursvariables.tf pour les variables principalesterraform.tfvars pour les variables secrètes qui ne sera pas stocké dans votre repository git*.auto.tfvars variables qui sont lues automatiquementoutputs.tf pour y définir tout ce qui sera affichéresources.tf pour un petit projet un simple fichier resources.tf suffira. Il est possible d'en créer d'autre avec des noms explicites.
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
# Exclude all .tfvars files, which are likely to contain sentitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
#
*.tfvars
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
#
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
Outil d’analyse statique
Analyse statique de la configuration
Peut scanner les infrastructures provisionnées avec Terraform pour détecter les erreurs de configuration
Intègre 400 polices qui suivent les bonnes pratiques de sécurité et de conformité
Un outil avec des compétences multiples
Violation Details -
Description : Ensure that Azure Resource Group has resource lock enabled
File : main.tf
Module Name : root
Plan Root : ./
Line : 1
Severity : LOW
-----------------------------------------------------------------------
Scan Summary -
File/Folder : /Users/hleclerc/formation-tf
IaC Type : terraform
Scanned At : 2022-10-03 07:50:45.659396 +0000 UTC
Policies Validated : 1
Violated Policies : 1
Low : 1
Medium : 0
High : 0Librairie Go
Facilite l’écriture de tests
Fournit des fonctions et des patterns
Framework de test
Permet de tester son code avant de le déployer
Behavior Driven Development (BDD)
image:
name: alterway/terraform-azure-cli:1.3
entrypoint:
- "/usr/bin/env"
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
variables:
GITLAB_TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_PROJECT_NAME}
PLAN: plan.tfplan
PLAN_JSON: tfplan.json
TF_ROOT: ${CI_PROJECT_DIR}
CI_DEBUG_TRACE: "false"
cache:
paths:
- .terraform
.before_script_template: &before_script_definition
- apk --no-cache add jq
- cd ${TF_ROOT}
- az login --service-principal -u ${ARM_CLIENT_ID} -p ${ARM_CLIENT_SECRET} --tenant ${ARM_TENANT_ID}
- alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
- terraform --version
- terraform init -backend-config="address=${GITLAB_TF_ADDRESS}" -backend-config="lock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="unlock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="username=gitlab-ci-token" -backend-config="password=${CI_JOB_TOKEN}" -backend-config="lock_method=POST" -backend-config="unlock_method=DELETE" -backend-config="retry_wait_min=5"
stages:
- validate
- build
- compliance
- test
- deploy
validate:terraform:
image:
name: alterway/terraform-azure-cli:1.3
entrypoint:
- "/usr/bin/env"
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
stage: validate
before_script:
- *before_script_definition
script:
- terraform validate
validate:checkov:
image:
name: bridgecrew/checkov
entrypoint:
- "/usr/bin/env"
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
stage: validate
script:
- checkov -d .
validate:tfsec:
image:
name: liamg/tfsec
entrypoint:
- "/usr/bin/env"
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
stage: validate
script:
- if [ -f "terraform.tfvars" ]; then tfsec --tfvars-file terraform.tfvars .; else tfsec .; fi
plan:
stage: build
variables:
ENV: "prod"
before_script:
- *before_script_definition
script:
- terraform plan -var-file=$ENV/$ENV.vars -out=$PLAN
- terraform show -json $PLAN | jq -r '([.resource_changes[]?.change.actions?]|flatten)|{"create":(map(select(.=="create"))|length),"update":(map(select(.=="update"))|length),"delete":(map(select(.=="delete"))|length)}' > $PLAN_JSON
artifacts:
name: plan
paths:
- ${TF_ROOT}/plan.tfplan
reports:
terraform: ${TF_ROOT}/tfplan.json
compliance:terraform:
stage: compliance
image:
name: eerkunt/terraform-compliance
entrypoint:
- "/usr/bin/env"
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
script:
- ls -la
- pwd
- terraform init -backend-config="address=${GITLAB_TF_ADDRESS}" -backend-config="lock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="unlock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="username=gitlab-ci-token" -backend-config="password=${CI_JOB_TOKEN}" -backend-config="lock_method=POST" -backend-config="unlock_method=DELETE" -backend-config="retry_wait_min=5"
- terraform show -json $PLAN > $PLAN.out.json
- terraform-compliance -f features -p $PLAN.out.json
apply:
stage: deploy
environment:
name: prod
variables:
ENV: "prod"
before_script:
- *before_script_definition
script:
- terraform apply -input=false $PLAN
dependencies:
- plan
when: manual
only:
- masterLe kit de développement cloud pour Terraform (CDKTF) permet d'utiliser des langages de programmation très utilisés pour définir et provisionner l'infrastructure.
Cela donne accès à l'ensemble de l'écosystème Terraform sans apprendre le langage de configuration HashiCorp (HCL).
Support actuel : TypeScript, Python, Java, C#, and Go (experimentale).
Pour démarrer : https://learn.hashicorp.com/tutorials/terraform/cdktf-install?in=terraform/cdktf
#!/usr/bin/env python
from constructs import Construct
from cdktf import App, TerraformStack, TerraformOutput, Token
from cdktf_cdktf_provider_azurerm import AzurermProvider, ResourceGroup, VirtualNetwork
class MyConf(TerraformStack):
def __init__(self, scope: Construct, ns: str):
super().__init__(scope, ns)
location="northeurope"
rg_name="hel-cdktf-rg"
vnet_name="hel-cdktf-vnet"
vnet_address_space=["10.0.0.0/16"]
tag = {
"env": "dev",
"projet": "cdktf-test",
"who": "herve leclerc"
}
AzurermProvider(self, "Azurerm",\
features={}
)
hel_cdktf_rg = ResourceGroup(self, 'hel-cdktf-rg',\
name = rg_name,
location = location,
tags = tag
)
hel_cdktf_vnet = VirtualNetwork(self, 'hel-cdktf-vnet',\
depends_on = [hel_cdktf_rg],
name = vnet_name,
location = location,
address_space = vnet_address_space,
resource_group_name =Token().as_string(hel_cdktf_rg.name),
tags = tag
)
TerraformOutput(self, 'vnet_id',
value=hel_cdktf_vnet.id
)
app = App()
MyConf(app, "cdktf")
app.synth()Exemples:
# terraform import
terraform import azurerm_resource_group.importrg \
/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/hel-training-terraform-tobe-imported
# terraformer
terraformer import azure -R awh-terraform-import-labs -o export --resources="*"
# aztfy
aztfy -o terraform awh-terraform-import-labs
# Autocompletion (requière un nouveau login )
terraform -install-autocomplete
# complete -o nospace -C /opt/homebrew/bin/terraform terraform
# Mise à jour des modules
terraform get -update=true
# terraform console pour tester des expressions
echo 'join(",",["foo","bar"])' | terraform console
echo "aws_instance.my_ec2.public_ip" | terraform console
# Source : https://res.cloudinary.com/acloud-guru/image/fetch/c_thumb,f_auto,q_auto/https://acg-wordpress-content-production.s3.us-west-2.amazonaws.com/app/uploads/2020/11/terraform-cheatsheet-from-ACG.pdf
Comment manipuler les workspace