Exemple d’un projet Iac avec Terraform et Gitlab
Aperçu
Le code présenté ci-dessous est issue d’un projet dont le but était de déployer plusieurs plateformes semblables (qualif, preprod, prod) comportant chacune plusieurs centaines de VM, reparties dans différente VAPP et connecté a plusieurs réseaux.
Les réseaux, VAPP et VM sont définis dans des fichiers d’entrés au format csv, avec un fichier pour chaque type d’objet. Ils seront gérés dans un mode Infra as code (IaC) au travers des outils Terraform et Gitlab.

Par exemple chaque VM est défini par la VAPP qui la contient, la quantité de CPU et de RAM qui lui est alloué, le template utilisé, la taille du disque système, le nombre et la taille des disques supplémentaires, la liste des réseaux connectés à la VM.
Le code contient également un exemple de configuration Gitlab CI, ce qui permet de déployer automatiquement l’infrastructure à chaque fois que le code ou les fichiers d’entré sont modifiés.
La configuration Gitlab CI défini un pipeline qui va être exécuter dans un container docker, ce pipeline va lancer l’exécution de l’ensemble des commandes Terraform nécessaire (init, plan, apply) en utilisant certaine variables géré par le Gitlab (par exemple les identifiants de connexion a Cloud Avenue, l’environnement de déploiement en fonction de la branche git modifié, etc.
Organisation du projet
L’ensemble d’une configuration Terraform est constitué d’un ou plusieurs fichiers Terraform (extension : .tf), en général on découpe la configuration en plusieurs fichier pour une meilleurs lisibilité.
Si la configuration utilise des variables il faudra également un fichier de variables (extension : .tfvars). On pourra avoir un fichier de variables par environnement.
terraform_ngp/
.gitlab-ci.yml
backend.tf
inputs.tf
network.tf
output.tf
output.tmpl
provider.tf
exemple.tfvars
terraform.tf
vapps.tf
variables.tf
vms.tf
inputs/
networks.csv
vapps.csv
vms.csv
vm_modules/
main.tf
outputs.tf
terraform.tf
variables.tf
Fichier Terraform mandatory
Les 4 fichiers ci-dessous se retrouve dans la plupart des configurations Terraform.
Le fichier terraform.tf
Contient la définition des providers Terraform utilisés : le provider VCD de Vmware pour utiliser l’API VCD et le provider local pour lire et écrire les fichiers csv
terraform {
required_providers {
# list provider used by the configuration.
vcd = {
source = "vmware/vcd"
version = ">=3.5.1"
}
local = {
source = "hashicorp/local"
version = ">=2.1.0"
}
}
}
Le fichier backend.tf
Contient la définition du backend ou sera stocké le fichier d’état, dans cet exemple on utilise la fonctionnalité de Gitlab pour stocker le fichier d’état.
# using gitlab registry to store tfstate
terraform {
# all parameters are defined in environment by the gitlab CI/CD
backend "http" {
}
}
Le fichier provider.tf
Contient la définition des paramètres pour le ou les providers utilisés : information de connexion a l’API VCD, les paramètres sont définis sous forme de variable.
provider "vcd" {
# parameteres of provider vmware/vcd
user = var.vcd_user
password = var.vcd_password
org = var.vcd_org
url = var.vcd_url
vdc = var.edgegw_vdc
allow_unverified_ssl = var.vcd_allow_unverified_ssl
}
# the provider local has no parameter
Le fichier variables.tf
Défini les variables utilisées par la configuration Terraform, toutes les variables doivent être déclaré pour pouvoir être utilisé dans la configuration et défini dans le fichier tfvars, ou dans l’environnement a lors de l’exécution de Terraform.
variable "vcd_user" {
type = string
description = "VCD User"
sensitive = true
}
variable "vcd_password" {
type = string
description = "VCD Password"
# sensitive variable are not displayed in standard output
sensitive = true
}
variable "vcd_url" {
type = string
description = "VCD URL"
}
variable "vcd_allow_unverified_ssl" {
type = bool
description = "skip ssl certificate verification"
default = false
}
variable "vcd_org" {
type = string
description = "VCD Organization name"
}
variable "catalog_name" {
type = string
description = "name of the catalog that contains the templates"
}
variable "edgegw_vdc" {
type = string
description = "Name of vdc that contains the edge gateway"
}
variable "edgegw_name" {
type = string
description = "Name of the edge gateway"
}
variable "net_csv" {
type = string
description = "filename of networks definition"
}
variable "vapp_csv" {
type = string
description = "filename of vapps definition"
}
variable "vm_csv" {
type = string
description = "filename of vms definition"
}
variable "output_csv" {
type = string
description = "finename of output data"
}
Fichiers spécifiques au projet
Les fichiers suivant sont spécifique a ce projet terraform
Le fichier inputs.tf
Lecture des fichiers d’entré. Le code contenu dans ce fichier lit les fichiers csv d’entré présents dans le répertoire « inputs » et les transforme en variable utilisé dans le reste de la configuration.
# read input data from csv file
data "local_file" "input_network" {
# read the list of network in csv format
filename = "${path.root}/inputs/${var.net_csv}"
}
data "local_file" "input_vapp" {
# read the list of vapp in csv format
filename = "${path.root}/inputs/${var.vapp_csv}"
}
data "local_file" "input_vm" {
# read the list of vm in csv fornat
filename = "${path.root}/inputs/${var.vm_csv}"
}
# transform the files content to a list of map for each input file
locals {
net_data = csvdecode(data.local_file.input_network.content)
vapp_data = csvdecode(data.local_file.input_vapp.content)
vm_data = csvdecode(data.local_file.input_vm.content)
}
Le fichier networks.tf
Construction des objets de type réseau. Ce fichier contient 2 type de ressources « vcd_network_routed_v2 » et « vcd_network_isolated_v2 », il crée les réseaux routé et isolé, les réseaux routés sont reliés à une edge gateway.
data "vcd_nsxt_edgegateway" "edgegateway1" {
# get the data for the edge gateway,
# edge gateway id is needed for router network
name = var.edgegw_name
org = var.vcd_org
vdc = var.edgegw_vdc
}
resource "vcd_network_routed_v2" "rnetwork" {
# create a network for each network in the list with a network type of 'routed'
for_each = {for x, net in local.net_data : net.NETWORK_NAME => net
if lower(net.NETWORK_TYPE) == "routed" &&
net.ORG_NAME == var.vcd_org }
org = each.value.ORG_NAME
vdc = each.value.VDC_NAME
name = each.key
# use the vcd_nsxt_edgegateway data to find the gateway id
edge_gateway_id = data.vcd_nsxt_edgegateway.edgegateway1.id
# define the ip address used by the gateway in the network
# and define the network prefix. input is in format : x.x.x.x/y
gateway = split("/", each.value.GATEWAY)[0]
prefix_length = split("/", each.value.GATEWAY)[1]
# define the ip pool address for the network,
# input format is x.x.x.x|y.y.y.y
static_ip_pool {
start_address = split("|", each.value.STATIC_ADDRESS_POOL)[0]
end_address = split("|", each.value.STATIC_ADDRESS_POOL)[1]
}
}
resource "vcd_network_isolated_v2" "inetwork" {
# create a network for each network in the list with a network type of 'isolated'
for_each = {for x, net in local.net_data : net.NETWORK_NAME => net
if lower(net.NETWORK_TYPE) == "isolated" &&
net.ORG_NAME == var.vcd_org }
org = each.value.ORG_NAME
vdc = each.value.VDC_NAME
name = each.key
# even if the network is not routed it need a ip address
# for the gateway in the network
# and define the network prefix. input is in format : x.x.x.x/y
gateway = split("/", each.value.GATEWAY)[0]
prefix_length = split("/", each.value.GATEWAY)[1]
static_ip_pool {
start_address = split("|", each.value.STATIC_ADDRESS_POOL)[0]
end_address = split("|", each.value.STATIC_ADDRESS_POOL)[1]
}
}
Le fichier vapps.tf
Construction des objets Vapps et association des réseaux à chacune des Vapp.
resource "vcd_vapp" "vapp1" {
# define a vapp for each line in input file
for_each = {for x, vapp in local.vapp_data : vapp.VAPP_NAME => vapp if vapp.ORG_NAME == var.vcd_org }
name = each.key
org = each.value.ORG_NAME
vdc = each.value.VDC_NAME
}
locals {
# transform data format since each vapp has a variable number of networks
vapp_net = flatten([
for vapp in local.vapp_data : [
for net in split("|",vapp.ORG_NETWORK) : {
org = vapp.ORG_NAME
vdc = vapp.VDC_NAME
vapp = vapp.VAPP_NAME
netname = net
}
] if vapp.ORG_NAME == var.vcd_org
])
}
resource "vcd_vapp_org_network" "network1" {
# attach org network into each vapp
depends_on = [vcd_vapp.vapp1,vcd_network_routed_v2.rnetwork, vcd_network_isolated_v2.inetwork]
for_each = {for x, net in local.vapp_net : "${net.vapp}-${net.netname}" => net }
vapp_name = each.value.vapp
org = each.value.org
vdc = each.value.vdc
org_network_name = each.value.netname
}
Le fichier vms.tf
Construction des objets VM. Ce fichier utilise un « module » Terraform car chaque VM est constitué de plusieurs ressource : une VM et un ou plusieurs additionnels. voir ci dessous la section « Utilisation de Module Terraform »
module "vm_instance" {
depends_on = [vcd_vapp.vapp1, vcd_vapp_org_network.network1]
source = "./vm_modules"
for_each = {for x,vm in local.vm_data : vm.HOSTNAME => vm if
vm.ORGANIZATION == var.vcd_org }
vm_name = each.key
organization = each.value.ORGANIZATION
vdc = each.value.VDC_NAME
vapp_name = each.value.VAPP
catalog_name = var.catalog_name
template_name = each.value.TEMPLATE
memory = tonumber(each.value.RAM_QUANTITY)
cpus = tonumber(each.value.CPU_QUANTITY)
vm_storage_profile = each.value.STORAGE_POLICY
networks = [
{name = each.value.NET1, ip = each.value.IP1},
{name = each.value.NET2, ip = each.value.IP2},
{name = each.value.NET3, ip = each.value.IP3},
]
osdisk = each.value.DISK0
disks = [each.value.DISK1, each.value.DISK2, each.value.DISK3]
}
# the module returns the ip assigned to each interface and the admin password
output "out_vm" {
value = module.vm_instance
sensitive = true
}
Les fichiers output.tf et output.tmpl
Le fichier output.tf crée un fichier de résultats au format csv qui contient les même informations que le fichier d’entrée des VM avec en plus l’information des adresse IP assigné a la VM et le mot de passe admin défini lors de la création de la VM. Le fichier output.tmpl est un template pour la création de ce fichier de résultat.
locals {
# transform the output of module vm_instance in a variable usable by the template
out_data = {for k,v in module.vm_instance : k => {ip = v.vm_ip, admin = v.admin_password}}
}
resource "local_file" "output" {
# create the output file form input vm data et output variable
filename = var.output_csv
file_permission = 0644
content = templatefile("output.tmpl", {input = local.vm_data, out = local.out_data})
}
Terraform peut utiliser des templates pour generer des fichiers, les directives utilisables dans un template sont documentées ici
HOSTNAME,ORGANIZATION,VAPP,TEMPLATE,VDC_NAME,CPU_QUANTITY,RAM_QUANTITY,STORAGE_POLICY,DISK0,DISK1,DISK2,DISK3,NET1,IP1,NET2,IP2,NET3,IP3,ADMIN
%{ for host in input ~}
${host.HOSTNAME},${host.ORGANIZATION},${host.VAPP},${host.TEMPLATE},${host.VDC_NAME},${host.CPU_QUANTITY},${host.RAM_QUANTITY},${host.STORAGE_POLICY},${host.DISK0},${host.DISK1},${host.DISK2},${host.DISK3},${host.NET1},${out[host.HOSTNAME]["ip"][0]},${host.NET2},%{ if length(out[host.HOSTNAME]["ip"]) > 1 }${out[host.HOSTNAME]["ip"][1]}%{ endif },${host.NET3},%{ if length(out[host.HOSTNAME]["ip"]) > 2 }${out[host.HOSTNAME]["ip"][2]}%{ endif },${out[host.HOSTNAME]["admin"]}
%{ endfor ~}
Le fichier exemple.tfvars
Fichier d’exemple de définition des variables. Toutes les variables utilisé par la configurations doivent être défini lors de l’exécution de Terraform, elles peuvent l’être dans ce fichier, ou sous forme de variable d’environnement, sinon Terraform demandera de façon interactive la valeur de chacune des variables non défini. Pour les variables sensibles (login, mot de passe) il est préférable de ne pas les mettre dans un fichier, mais plutôt de les définir dans l’environnement ce que peut faire automatiquement le pipeline de CI/CD
vcd_org = "cav01xxxxxxxxxxxx" vcd_url = "https://console2.cloudavenue.orange-business.com/api" vcd_allow_unverified_ssl = true edgegw_name = "tn01xxxxxxxxxxx" edgegw_vdc = "your_vdc" catalog_name = "your_catalog" net_csv = "networks.csv" vapp_csv = "vapps.csv" vm_csv = "vms.csv" output_csv = "output.csv"
Les fichiers d’entrées
placés dans le repertoire « inputs » leur noms sont définis dans le fichiers de variables.
Définition des réseaux
chaque réseau est défini par le vdc qui le contient, son nom, le type de réseau isolé ou routé, l’adresse ip de la gateway et le mask en notation CIDR, la definition du static address pool avec son ip de debut et celle de fin.
ORG_NAME,VDC_NAME,NETWORK_NAME,NETWORK_TYPE,GATEWAY,STATIC_ADDRESS_POOL cav01xxxxxxxxxxxx,your_vdc,rtr_01,routed,192.168.0.1/24,192.168.0.10|192.168.0.100 cav01xxxxxxxxxxxx,your_vdc,iso_02,isolated,192.168.2.1/24,192.168.2.10|192.168.2.100 cav01xxxxxxxxxxxx,your_vdc,iso_03,isolated,192.168.3.1/24,192.168.3.10|192.168.3.100
Définitions des vapps
chaque vapp est défini par le vdc qui la contient, son nom, et la liste des réseaux qui lui sont attaché séparé par le caractère « | »
ORG_NAME,VDC_NAME,VAPP_NAME,ORG_NETWORK cav01xxxxxxxxxxxx,your_vdc,vapp_01,rtr_01|iso_02|iso_03 cav01xxxxxxxxxxxx,your_vdc,vapp_02,rtr_01|iso_03 cav01xxxxxxxxxxxx,your_vdc,vapp_03,rtr_01|iso_03
Définitions des vms
chaque vm est défini par son nom, la vapp et le vdc qui la contient, le nombre de cpu, la quantité de memoire, la politique de stockage, la taille des disques système (disk0) et additionnels (disk1 a disk3), le nom des réseaux connectés et eventuellement une ip si on veut la fixé sinon elle est assigné dans le pool.
HOSTNAME,ORGANIZATION,VAPP,TEMPLATE,VDC_NAME,CPU_QUANTITY,RAM_QUANTITY,STORAGE_POLICY,DISK0,DISK1,DISK2,DISK3,NET1,IP1,NET2,IP2,NET3,IP3 vm-01,cav01xxxxxxxxxxxx,vapp_01,ubuntu-20,your_vdc,2,4,gold,4,0,0,0,rtr_01,,iso_02,,iso_03, vm-02,cav01xxxxxxxxxxxx,vapp_01,ubuntu-20,your_vdc,2,2,gold,10,10,20,0,rtr_01,,iso_03,,, vm-03,cav01xxxxxxxxxxxx,vapp_03,ubuntu-20,your_vdc,2,2,gold,15,10,0,0,rtr_01,,iso_02,,,
Utilisation de Module Terraform
Pour factoriser le code on peut définir des modules Terraform qui se présente sous forme d’un répertoire contenant également des fichiers Terraform.
Ici on utilisera un module pour définir les VM car elles sont constitué de plusieurs ressources (une VM et des disques additionnels).
Le répertoire vm_module
Module Terraform pour créer une vm avec des disques supplémentaire, ce module est lui-même constitué de 4 fichiers :
Le fichier vm_module/main.tf
Contient la définition d’une VM et de ses disques additionnel.
# create a vm, most argument are variables
resource "vcd_vapp_vm" "vapp_vm" {
name = var.vm_name
computer_name = var.vm_name
org = var.organization
vdc = var.vdc
vapp_name = var.vapp_name
catalog_name = var.catalog_name
template_name = var.template_name
memory = var.memory * 1024
cpus = var.cpus
storage_profile = var.vm_storage_profile
cpu_hot_add_enabled = true
memory_hot_add_enabled = true
dynamic "network" {
for_each = [for net in var.networks : net if net.name != ""]
content {
adapter_type = "VMXNET3"
type = "org"
name = network.value.name
ip_allocation_mode = network.value.ip == "" ? "POOL" : "MANUAL"
ip = network.value.ip
}
}
override_template_disk {
bus_type = "paravirtual"
size_in_mb = var.osdisk * 1024
bus_number = 0
unit_number = 0
}
customization {
allow_local_admin_password = true
auto_generate_password = true
enabled = true
force = false
}
}
resource "vcd_vm_internal_disk" "diskadd" {
depends_on = [vcd_vapp_vm.vapp_vm]
for_each = {for idx, disk in var.disks : idx => disk if disk != "0"}
org = var.organization
vdc = var.vdc
vapp_name = var.vapp_name
vm_name = var.vm_name
size_in_mb = each.value * 1024
bus_type = var.disk_bus
bus_number = 0
unit_number = each.key < 6 ? each.key + 1 : each.key + 2
}
Le fichier vm_module/terraform.tf
Défini le provider utilisé par le module, ici comme dans la configuration principale le provider Vmware VCD.
terraform {
required_providers {
vcd = {
source = "vmware/vcd"
version = "3.5.1"
}
}
}
Le fichier vm_module/output.tf
Défini les valeurs retournées par le module, ici les adresse IP assigné à la VM et le mot de passe admin défini lors de la construction de la VM.
output "vm_ip" {
value = flatten([
for network in resource.vcd_vapp_vm.vapp_vm.network : [
network.ip
]
])
}
output "admin_password" {
value = resource.vcd_vapp_vm.vapp_vm.customization[0].admin_password
sensitive = true
}
Le fichier vm_module/variables.tf
Déclare les variables utilisées par le module ces variables sont les paramètres d’appel du module, et doivent être défini lors de l’appel du module.
variable "vm_name" {
type = string
description = "name of vm (hostname)"
}
variable "organization" {
type = string
description = "name of organization"
}
variable "vdc" {
type = string
description = "name of vdc"
}
variable "vapp_name" {
type = string
description = "name of the vapp containing the vm"
}
variable "catalog_name" {
type = string
description = "name of catalog"
}
variable "template_name" {
type = string
description = "name of vm template"
}
variable "memory" {
type = number
description = "vm memory in GB"
}
variable "cpus" {
type = number
description = "number of cpus"
}
variable "vm_storage_profile" {
type = string
description = "storage profile for vm disks"
}
variable "networks" {
type = list(any)
description = "network description"
}
variable "osdisk" {
type = number
description = "size of system disk"
}
variable "disks" {
type = list(any)
description = "list of additional disks size"
}
variable "disk_bus" {
type = string
description = "type of disk bus"
default = "paravirtual"
}
Déploiement à l’aide de Gitlab CI
Le fichier .gitlab-ci.yml
Définition du pipeline de CI/CD.
Dans cet exemple le pipepline exécute automatiquement une tache Terraform plan, puis une tache Terraform apply, il utilise une image docker fournie par gitlab.com qui contient terraform plus un script gitlab-terraform qui simplifie la configuration du pipeline.
Les variables sensibles (utilisateur et mot de passe de l’API: QUALIF_VCD_USER et QUALIF_VCD_PASSWORD) sont stockées dans Gitlab (procédure pour définir des variables dans Gitlab). Et le pipeline les défini dans l’environnement, ainsi que les paramètres pour stocker le fichier d’état dans le Gitlab a l’aide des variables prédéfinies par la CI (Listes des variables prédéfinies). Le fichier d’output est sauvegardé dans les artifacts gitlab.
image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest
variables:
TF_ROOT: ${CI_PROJECT_DIR}
cache:
paths:
- ${TF_ROOT}/.terraform
before_script:
- cd ${TF_ROOT}
- terraform --version
stages:
- qualif-plan
- qualif-apply
.qualif:
variables:
TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/qualif
TF_VAR_vcd_user: ${QUALIF_VCD_USER}
TF_VAR_vcd_password: ${QUALIF_VCD_PASSWORD}
tags:
- private
- prod
- docker
qualif-plan:
extends: .qualif
stage: qualif-plan
script:
- gitlab-terraform init -backend-config=skip_cert_verification=true
- gitlab-terraform validate
- gitlab-terraform plan -var-file qualif.tfvars
- gitlab-terraform plan-json
artifacts:
name: qualif-plan
paths:
- ${TF_ROOT}/plan.cache
reports:
terraform: ${TF_ROOT}/plan.json
qualif-apply:
extends: .qualif
stage: qualif-apply
script:
- gitlab-terraform apply
dependencies:
- qualif-plan
artifacts:
name: qualif-output
paths:
- ${TF_ROOT}/output.csv
Notes à propos des identifiants
Ce projet utilise des variables Gitlab pour stocker les identifiants utilisés, pour une plus grande sécurité il est recommandé d’utiliser un gestionnaire de secret externe, la documentation de Gitlab décrit comment utiliser par exemple Hashicorp Vault.