Terraform - Fondamentaux

Concernant les supports de cours

Supports de cours

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

Licence Creative Commons BY-SA 4.0
Licence Creative Commons BY-SA 4.0

Introduction

Qu’est-ce que l’Infrastructure as code (IaC) ?

  • 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

Quelques outils permettant de faire du IaC

Timeline des différents outils de IaC par rapport aux technos cloud

Avantages principaux de l’IaC

  • Versionning

  • Réutilisabilité

  • Reproductibilité

  • Automatisation

  • Gain de temps

  • Idempotence

  • Prédictibilité

  • Intégration aux outils de CI/CD

Cycle de vie de l’Infrastructure

  • T0 : Phase de Setup
    • Provisionner l’infrastructure
    • Configurer l'infrastructure
    • Installation initiale des logiciels
    • Configuration initiale des logiciels
  • T1 : Phase de maintenance Post Initialisation
    • Ajustement de l'infrastructure
    • Suppression ou ajout de composants
    • Mise à jour des logiciels
    • Reconfiguration des logiciels

Terraform

  • 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...

Quelques faits

  • 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

Outils de Programmation Procedurale versus Déclarative

  • Déclaratif
    • J'énonce ce que je veux obtenir
  • Procédural
    • J'énonce comment faire pour obtenir ce que je veux

Exemple :

  • Déclaratif :
    • Je veux une fusée avec 3 étages
  • Procédural
      1. Mettre en place le premier étage sur le sol
      1. Mettre le deuxième étage sur le premier étage
      1. Mettre le troisième étage sur le deuxième étage

Procedural vs Déclaratif

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

Impératif vs Déclaratif

Installer Terraform CLI

terraform version
Terraform v1.2.1
on darwin_amd64

Architecture et Concepts

Terraform Core

  • Binaire é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)

Terraform Provider

  • 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

Terraform Core & Terraform plugins

Initialisation

Initialiser le projet Terraform

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"
  }
}

Le workflow Terraform

Workflow principal Terraform

Devops Loop

  • 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.

Les états et les backends

Qu’est-ce que le fichier d’état ?

  • Utilisé par Terraform pour y stocker l’état de l’infrastructure et de la configuration qu’il gère
  • Stocké par défaut dans un fichier local terraform.tfstate
  • Pour être stocké à distance
  • Utile pour créer des plans et apporter des modifications à l’infrastructure
{
  "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": "/",
...

Qu’est-ce qu’un backend ?

  • Un backend sert à définir :
    • Où les opérations sont effectuées
    • Où les fichiers d’état sont stockés
  • Une opération est une requête API pour créer, lire, mettre à jour ou supprimer une ressource

  • Deux types de backends
    • Local
    • Distant

Backend local

  • 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

Backend distant (1)

🟢 À privilégier

  • Permet l’utilisation d’un espace de stockage distant pour stocker le fichier d’état

  • Facilite le travail en équipe

  • Exemples de backend distants :
    • Etcd
    • Consul
    • HTTP
    • S3
    • Azure File
    • GCS
    • AliCloud

Backend distant (2)

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"
  }
}

State locking (Verrouillage d’état)

  • 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

Gestion des secrets dans le fichier d’état

  • Cas du fichier d’état local :
    • Données stockées dans des fichiers JSON en texte brut
  • Cas fichier d’état distant :
    • L’état n’est conservé en mémoire que lorsqu’il est utilisé par Terraform
    • Possibilité de chiffrement sur le répertoire distant selon le backend utilisé

Les workspaces

Qu’est-ce qu'un workspace

  • Simplement des fichiers d'état (terraform.tfstate) gérés de manière indépendante
  • Un workspace contient tout ce dont terraform à besoin pour gérer un configuration d'infrastructure
  • les workspaces fonctionnent comme des répertoires de travail
  • Les workspaces pemettent de gérer différents environnements sans modifier les configurations

  • Chaque environnement à son prope état
  • Les ressources peuvent être nommées dynamiquement avec le nom du workspace
  • Les espaces de travail garantissent que les environnements sont isolés et mis en miroir.
  • Les espaces de travail sont le successeur des anciens Terraform Environments

Cas d'usage pour les workspaces

  • Environnements
    • Production
    • Staging
    • Dev
    • ...
  • Zones
    • northeurope
    • westeurope
    • us-east-1
    • eu-west-2
  • Différentes subscriptions/account
  • arn
  • sub
  • ...
  • Clients
  • Usine à sites
  • ...

Comment manipuler les workspace

# Créer un workspace
terraform workspace new Production
terraform workspace new Staging

# Sélectionner un workspace
terraform workspace select Production

# Lister les workspaces
terraform workspace list

# Créer un plan
terraform plan -out prod.tfplan

# Appliquer le plan
terraform apply  prod.tfplan

# Sur quel workspace suis-js
terraform workspace show

# Supprimer un workspace
terraform workspace Staging

Les workspaces (vue interne : local state )

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

Les workspaces avec les backend distants (1)

Les workspaces avec les backend distants (2)

Les commandes de base

terraform 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

  • Elle permet :
    • D’initialiser les backends
    • D’installer les modules
    • D’installer les plugins

🟢 Bonne pratique : l’exécuter souvent

terraform plan

  • Gé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

terraform apply (1)

  • Exécute les changements proposés par le plan

terraform apply (2)

Fichiers

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_x5

terraform destroy (1)

  • Supprime les ressources définies dans le plan (gérées par terraform)

  • Utile surtout pour les environnements éphémères

terraform destroy (2)

Configuration Terraform

Qu'est-ce que c'est ?

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).

resources

  • Les ressources sont les éléments de base d'une configuration Terraform.

  • Lorsque vous définissez une configuration, vous définissez une ou plusieurs (généralement plusieurs) ressources.
  • 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

  • Plusieurs méta-arguments
    • count
    • depends_on
    • for_each
    • provider
    • lifecycle
    • provisioner et connection

Resources :Exemple


resource "aws_instance" "web" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
}

Data Sources (1)

  • 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
  // ...
}

Data Sources : Filtrer les data sources avec 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"]
  }
}

Data Sources : Filtrer les data sources avec 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-*"]
  }
}

Data Sources : Filtrer les data sources avec filter (3)


data "aws_ec2_transit_gateway" "tgw" {
  filter {
    name   = "tag:Name"
    values = ["wahlnetwork-tgw-prod"]
  }
}

Créer une configuration - Exercice

  • 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

Commentaires

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
}

Variables d’entrée (1)

12 factor app: Il doit y avoir une séparation stricte entre la configuration et le code

  • Paramètres pour personnaliser le code source

  • Définition des valeurs à l’aide :
    • Des options CLI
    • Des variables d’environnement
  • Possibilité de définir des règles de validation personnalisées

Variables d’entrée (2)

  • Types :
    • string
    • number
    • bool
    • list/tuple : ["us-west-1a", "us-west-1c"] - ["c", "b", "b"]
    • map/object : {name = "Mabel", age = 52}
    • set : ["c", "b"] (Pas de duplication possible de valeurs) (fonction toset())

Variables d’entrée (3)

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-03

Variables d’entrée (4)

Fichier de variables:

  • Automatiques
    • terraform.tfvars
    • *.auto.tfvars
  • Fichiers
terraform apply -auto-approve -var-file=file-var.tfvars
  • Variables d'environment
TF_VAR_<var-name>="a-value" terraform plan
# eg.
TF_VAR_rg_name="a-terraform-training-10" terraform plan

Variables ordre de lecture

  1. dans le fichier tf
  2. Variables d'environnement
  3. terraform.tfvars
  4. *.auto.tfvars
  5. cmd line file vars (-var-file)
  6. cmd line file var (-var)

Exemple de fichier de variables en fichier

Simples

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

  }))
}

Variables : string par défaut

variable "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"
}

Variables : string heredoc

variable "app_description" {
 type        = string
 description = "application description"
 default     = <<EOT
Welcome !
Application version is %%app_version
EOT
}

Variables : number

variable "the_counter" {
 type        = number
 description = "# iteration"
 default     = 4
}

Variables : bool

variable "trueOrFalse" {
 type        = bool
 description = "true or false"
 default     = true
}

Variables : list

variable "eu_locations" {
 type        = list
 description = "location list"
 default     = ["westeurope","northeurope"]
}

Variables : map

variable "hel" {
 type        = map
 description = "hel person"
 default     = {
   surname = "Hervé"
   name    = "Leclerc"
   role    = "CTO"
  }
}

Variables : object

variable "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"
            ]
  }
}

Variables - Données sensibles



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 pratiques

Variables - Exercice

  • Créer le fichier variables.tf

  • Créer le fichier formation.tfvars

  • Variabiliser :
    • la région
    • l’instance type
    • tag Name

Meta-arguments

  • depends_on :
    • Dépendance Explicite vs Dépendance Implicite
    • depends_on = [“resource list”]
    • eg. depends_on = [ "azurerm_network_interface.nic1", "azurerm_managed_disk.dd1" ]
  • count :
    • loop count.index
    • Utile pour créer une simple condition if then avec count = var.something si quelque-chose = 0 la ressource ne sera pas créée/modifiée
  • provider
    • Spécifie le fournisseur à utiliser pour une ressource.
    • Ceci est utile lorsque on utilise plusieurs fournisseurs, ce qui est généralement utilisé lorsque on crée des ressources multirégionales. Pour différencier ces fournisseurs, vous utilisez un champ d'alias.
  • for_each :
    • loop sur une liste ou map
      • chaque instance de for_each a un identifiant unique lors de la création ou de la modification de la configuration
  • lifecycle :
    • Sur n'importe quel bloc
    • 3 arguments :
      • 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)

Meta-arguments Exemple (1)


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,]
 }
}

Meta-arguments Exemple (2)


### 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
}

Outputs

  • 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."
}

Outputs - Exercice

  • 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

Valeurs locales

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)
}

Valeurs locales exemples


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"
  }
}

Fonctions intégrées (built-in) (1)

  • Terraform inclut un nombre de fonctions intégrées

  • Il n’est pas possible de créer ses propres fonctions

  • Familles de fonction
    • Numérique
    • Chaîne de caractères
    • Collection
    • Encodage
    • Système de fichier
    • Date et heure
    • Hachage et chiffrement
    • Réseau
    • Conversion de types

Fonctions intégrées (built-in) (2) : Quelques exemples

  • numeric
    • abs, ceil, floor, log, max, min, parseint, pow, signum
  • string
    • chomp, format, formatlist, indent, join, lower, regex, regexall, replace
    • split, strrev, substr, title, trim, trimprefix, trimsuffix, trimspace, upper
  • collection
    • chunklist, coalesce, coalescelist, compact, concat, contains, distinct, element, flatten
    • index, keys, length, list, lookup, map, matchkeys, merge, range reverse, setintersection
    • setproduct, setsubtract, setunion, slice, sort, sum, transpose, values, zipmap
  • encoding
    • base64decode, base64encode, base64gzip, csvdecode, jsondecode, jsonencode
    • urlencode, yamldecode, yamlencode
  • filesystem
    • abspath, dirname, pathexpand, basename, file, fileexists, fileset, filebase64, templatefile
  • date and time
    • formatdate, timeadd, timestamp
  • hash and crypto
    • base64sha256,base64sha512, bcrypt, filebase64sha256, filebase64sha512, filemd5, filesha1, filesha256
    • filesha512, md5, rsadecrypt, sha1, sha256, sha512, uuid, uuidv5
  • ip network
    • cidrhost, cidrnetmask, cidrsubnet, cidrsubnets
  • type conversion
    • can, tobool, tolist, tomap, tonumber, toset, tostring, try

Dynamic block

  • Utilisé pour construire dynamiquement des blocs imbriqués reproductibles
  • Fonctionne comme une boucle for

Pour 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ère
resource "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"
    }
  }
}
  • (Voir les exemples dans le chapitre loops)

Graphe des ressources

  • 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

Modules

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 :

  • Organisation des configurations : facilite la compréhension des configurations
  • Encapsulation : Permet de masquer l'implémentationb interne de l'infrastructure et de se prévenir de changement non voulus
  • Ré-utilisation : Permet de créer des modules qui sont utilisés dans plusieurs configurations
  • Consistence : Permet de gérer facilement plusieurs environnements (staging, production, dev…)

Interpolation

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 }!"

Les Conditions

Exemples ternaires


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"
}

Exemples de conditions avec built-in functions


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"
}

Exemples de conditions avec count


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
}

Les Loops

Loops avec count et for_each

count (1)


resource "null_resource" "simple" {
  count = 2
}
output "simple" {
  value = null_resource.simple
}

count (2)


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
}

count (3)


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]}"
 }
}

for_each (1)

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
}

for_each (2)


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
}

for_each (3)

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}"]
}

Loops avec Blocs Dynamiques (1)

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"]
  }
}

Loops avec Blocs Dynamiques (2)

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"]
    }
  }
}

Loops avec Blocs Dynamiques (3)

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
}

Nested Loops (1)

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"],
      }]
    }
  }
}

Nested Loops (2)


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
}

Nested Loops (3)

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"],
    }]
  }
}

Nested Loops (4)


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
}

For In Loop Basics (1)

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 ]
}

For In Loop Basics (2)

# 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 }
}

For In Loop Basics (3)

# 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") ]
}

For In Loop Basics (4)

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
}

For In Loop Basics (5)

# 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...}
}

Lookups, Keys, Contains

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") ]
}

Provisioners

local-exec provisioner

  • 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"]
  }
}

remote-exec provisioner

  • 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",
    ]
  }
}

file provisioner

  • Le provisioner file est utilisé pour copier des fichiers ou des répertoires de la machine exécutant Terraform vers la ressource nouvellement créée.

resource "aws_instance" "web" {
  # ...

  # Copies the myapp.conf file to /etc/myapp.conf
  provisioner "file" {
    source      = "conf/myapp.conf"
    destination = "/etc/myapp.conf"
  }

Provisionners all in one


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"
  }
}

Autres Commandes utiles

terraform validate

  • Validation 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

terraform validate - Exercice

  • 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

terraform fmt

  • Formate 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
  }

terraform import

  • Sert à 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.

Verbosité des logs

  • TF_LOG

  • TF_LOG_CORE

  • TF_LOG_PROVIDER

  • Niveaux :
    • TRACE
    • DEBUG
    • INFO
    • WARN
    • ERROR

Verbosité des logs - Exercice

  • 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

terraform workspace

  • Les données stockées dans un backend appartiennent à un workspace

  • Le backend par défaut se nomme “default”

  • Possibilité d’avoir plusieurs backends

terraform state

  • Commande utilisée pour gérer l’état Terraform

  • Elle a plusieurs sous-commandes

terraform state - Exercice

Exécuter les commandes suivantes :

terraform state list

terraform state show aws_instance.web

terraform taint untaint

La 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.

terraform show

  • Affiche l’état de manière lisible pour un humain

terraform refresh

Terraform 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.

Modules Terraform

Qu’est-ce qu’un module Terraform ?

  • 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

Types de modules

  • Local

  • Distant
    • Terraform registry : https://registry.terraform.io/browse/modules
    • GitLab, Github…
    • Terraform Cloud
    • Terraform Enterprise private module registries
    • URLs HTTP

Modules invocation

# 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"
}

Sources

Modules - Exercice

  • 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

Bonnes Pratiques

Structure d’un projet Terraform

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.tf
  • Le fichier main.tf qui est le fichier principal d’un projet terraform
  • Le fichier provider.tf pour y définir les fournisseurs
  • Le fichier variables.tf pour les variables principales
  • Le fichier terraform.tfvars pour les variables secrètes qui ne sera pas stocké dans votre repository git
  • Le fichier de variables *.auto.tfvars variables qui sont lues automatiquement
  • Le fichier outputs.tf pour y définir tout ce qui sera affiché
  • Les fichiers 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.
  • Les modules
  • Le fichier .gitignore voir slide suivante

Fichier .gitignore


# 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

Plus de bonnes pratiques

https://git.alterway.fr/dt/terraform/best-practices

Terraform Tests

tfsec

  • Outil d’analyse statique

  • Détection de potentiels problèmes de sécurité
  • Vérifie si
    • Des données sensibles sont incluses dans la configuration Terraform
    • Les bonnes pratiques sont respectées
    • Analyse les modules locaux
    • Évalue les expressions ainsi que les valeurs littérales
    • Évalue les fonctions Terraform
    • https://github.com/aquasecurity/tfsec

checkov

  • 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é

  • https://github.com/bridgecrewio/checkov

checkov

terrascan

Un outil avec des compétences multiples

  • 500+ Polices pour les bonnes pratiques de sécurité
  • Scanning de Terraform (HCL2)
  • Scanning des templates AWS CloudFormation (CFT)
  • Scanning des Azure Resource Manager (ARM)
  • Scanning de Kubernetes (JSON/YAML), Helm v3, and Kustomize
  • Scanning de Dockerfiles
  • Support pour AWS, Azure, GCP, Kubernetes, Dockerfile, and GitHub
  • S'intègre avec les scanner de vulnérabilités des images docker image pour les registries AWS, Azure, GCP, Harbor.
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                :   0

terratest

  • Librairie Go

  • Facilite l’écriture de tests

  • Fournit des fonctions et des patterns

  • Fonctionnalités
    • Test de code Terraform
    • Test de templates Packer
    • Test d’images Docker
    • Prise en charge d’API AWS
    • Prise en charge de l’API Kubernetes
    • Requêtes HTTP

Terraform compliance

  • Framework de test

  • Permet de tester son code avant de le déployer

  • Behavior Driven Development (BDD)

Terraform compliance (1)

Terraform compliance (2)

Terraform compliance (3)

Terraform tests (All In One)

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:
    - master

Aller plus loin

CDKTF (CDK for Terraform)

Le 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

CDKTF (Exemple)

#!/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()

Importer des ressources dans Terraform

  1. terraform import https://www.terraform.io/cli/import
  2. terraformer: multi providers https://github.com/GoogleCloudPlatform/terraformer
  3. aztfy: spécialisé Azure https://github.com/Azure/aztfy

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

Cheat Sheet Commandes

Command Line tricks


# 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

Conclusion

HashiCorp Certified Terraform Associate

A vous de Jouer !