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.
definition des reseaux
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
definitions 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
definitions 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" }
Deployement a 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 a 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.