Terraform : exemple de code

By Denis Fabien
2023-05-01

Amis de l'infrastructure As Code, je vous présente Terraform (par Hashicorp).

Bon ok, je suis certain que vous connaissez déjà ! Mais si vous êtes comme moi, parfois quand vous avez besoin d'un truc un peu plus complexe, vous êtes probablement en recherche d'exemple.

Je vous propose ici une série d'exemples allant du plus basique au un peu plus complexe (avec Module).

Dans cette série d'exemples, nous n'utiliserons pas les outputs (cela fera l'objet d'une article différent si j'ai le temps).

Quels outils avez-vous besoin ?

Comme je suis un gars pragmatique, j'ai monté ces exemples sur des cas réels. Vous allez avoir besoin de deux choses :

  • Hashicorp Vault : J'ai préparé une version Docker avec du code Terraform déjà existant au besoin ici :(https://gitlab.com/changendevops/gitopstoolkit/vaultterraformdocker)
  • Hashicorp Consul : J'ai là aussi un petitt docker-compose exemple qui monte 3 noeuds (https://gitlab.com/changendevops/gitopstoolkit/docker-consul-for-test)

 

Liste des exemples disponibles

Suivez le code ici : https://gitlab.com/changendevops/gitopstoolkit/terraformsample.git

Configuration d'un simple provider (step01)

Le but ici est fort simple et s'adresse au débutant : C'est quoi un provider et comment on l'utilise de base (voir aussi : https://registry.terraform.io/providers/hashicorp/consul/latest/docs)

# provider.tf
terraform {
  required_providers {
    vault = {
      source = "hashicorp/vault"
      version = "3.15.0"
    }
  }
}

provider "vault" {
  address = "http://localhost:8200"
  token = "anytokenxyz"
}

Mes premières variables (step02)

Retirez les variables du provider pour les mettre hors du fichier .tf

# provider.tf
terraform {
  required_providers {
    vault = {
      source = "hashicorp/vault"
      version = "3.15.0"
    }
  }
}

provider "vault" {
  address = var.vault_url
  token = var.vault_token
}
# variables.tf
variable "vault_url" {
  type        = string
  description = "Url of Vault"
}

variable "vault_token" {
  type        = string
  description = "Token to connect to vault"
}

Variable auto.tfvars (step03)

Utilisez un fichier terraform.auto.tfvars pour stocker les variables (dans un pipeline, ce fichier sera généré dynamiquement et ne sera pas stocké dans git).

# variables.auto.tfvars
vault_url="http://localhost:8200"
vault_token="token"

Utiliser un remote backend comme Consul (step04)

En utilisant le docker Consul défini préalablement, stockez les states files dans un remote service.

# backend.tf
terraform {
  backend "consul" {
    address       = "localhost:8500"
    scheme        = "http"
    path          = "terraform-demo/vault/main"
    access_token  = "super token"
  }
}

Configuration d'un premier KV dans Hashicorp Vault (step05)

Utilisez simplement la ressource vault_mount 

# variables.tf
...
variable "kv_path" {
  type        = string
  description = "Path of the kv"
}
# variables.auto.tfvars
...
kv_path="kv-test"
# kv.tf
resource "vault_mount" "this" {
  path        = var.kv_path
  type        = "kv"
  options     = { version = "2" }
  description = "kv for ${var.kv_path}"
}

Ma première loop Terraform (step06)

Comment utiiser une liste depuis une variable et "looper" sur cette liste.

# variables.tf
...
variable "kv_path" {
  type        = list
  description = "Path of the kv"
}
# variable.auto.tfvars
...
kv_path=["kv-loop1", "kv-loop2"]
# kv.tf
resource "vault_mount" "this" {
  for_each = toset( var.kv_path )
  path        = each.key
  type        = "kv"
  options     = { version = "2" }
  description = "kv for ${each.key}"
}

Une loop basée sur un count (step07)

Cette fois, la loop va etre basée sur un count et un simple index, plus une liste définie. Avec cette façon, chaque object se nommera ...1...2...3...

# variables.tf
...
variable "kv_path" {
  type        = string
  description = "Path of the kv"
}
...
kv_path="kv-count"
# kv.tf
resource "vault_mount" "this" {
  count = 4
  path        = "${var.kv_path}-${count.index}"
  type        = "kv"
  options     = { version = "2" }
  description = "description for ${var.kv_path}-${count.index}"
}

Une loop sur une liste d'objet (step08)

On complique un peu la sauce, on ajoute cette fois une liste d'objet.

# variables.tf
...
variable "kvs" {
  type = list(object({
      name = string
      description = string
    }))
}
# variable.auto.tfvars
...
kvs=[
  {
    name          = "step08-1"
    description   = "Description kv2"
  },
  {
    name          = "step08-2"
    description   = "Description kv2"
  },
  {
    name          = "step08-3"
    description   = "Description kv3"
  },
  {
    name          = "step08-4"
    description   = "Description kv5"
  }
]
# kv.tf
resource "vault_mount" "this" {
  count          = length(var.kvs)
  path        = "${element(var.kvs, count.index).name}"
  type        = "kv"
  options     = { version = "2" }
  description = "${element(var.kvs, count.index).description}"
}

Mon premier module (step09)

Ok maintenant, utilisons un module local. Le module recevra 2 variables puis créera la ressource qui va bien.

# modules/kv/main.tf
resource "vault_mount" "this" {
  path        = "${var.name}"
  type        = "kv"
  options     = { version = "2" }
  description = "${var.description}"
}
# modules/ks/variables.tf
variable "name" {
  type        = string
  description = "Name"
}

variable "description" {
  type        = string
  description = "Description"
}
# version.tf
terraform {
  required_providers {
    vault = {
      source = "hashicorp/vault"
      version = "3.15.0"
    }
  }
}
# kv.tf
module "kv2_denis" {
  source          = "./modules/kv"
  name            = var.kv.name
  description     = var.kv.description
}
# variables.auto.tf.vars
...
kv={
  name          = "super kv 1"
  description   = "Description kv2"
}

Un module avec un objet (step10)

Et maintenant, au lieu de lui passer 2 variables, passons-lui 1 variable de type objet qui contient 2 valeurs.

# modules/kv/main.tf
resource "vault_mount" "this" {
  path        = "${var.kv.name}"
  type        = "kv"
  options     = { version = "2" }
  description = "${var.kv.description}"
}
# modules/kv/variables.tf
variable "kv" {
  type = object({
    name = string
    description = string
  })
}
# kv.tf
module "kv2_denis" {
  source = "./modules/kv"
  kv   = var.kv
}

Un module avec une liste d'objet (step11)

Dernier exemple de la liste, cette fois on va utiliser une liste d'objet, comme pour le step08, mais via un module. Dans un monde idéal, le module permet d'isoler certaines actions et de les rendre indépendantes du reste du code.

# modules/kv/main.tf
resource "vault_mount" "this" {
  path        = "${var.kv.name}"
  type        = "kv"
  options     = { version = "2" }
  description = "${var.kv.description}"
}
# modules/kv/variables.tf
variable "kv" {
  type = object({
    name = string
    description = string
  })
}
# kv.tf
module "kv2" {
  count          = length(var.kvs)
  source = "./modules/kv"
  kv   = element(var.kvs, count.index)
}

Pour finir

Ceci représente que quelques possibilités que vous offre le langage HCL de terraform. Cependant, avant de mettre plus de complexité, je vous suggère d'adjoindre un second langage (tel que Python par exemple), car le HCL n'est pas un langage très flexible. Comme les SDK de AWS qui passent par un code Python qui génère des Terraform simples.

Pour aider à générer du terraform depuis Python, j'ai créé une librairie CndScaffold (https://gitlab.com/changendevops/gitopstoolkit/cndscaffold) qui permet de générer du code à la volée suivant certains templates définis (il permet de remplacer autant de fichiers que vous le souhaitez.) Potentiellement, il pourrait même vous générer tout votre projet, à partir de variables stockées dans des fichiers YAML par exemple.

Ami du As Code, je vous souhaite une bonne soirée !