lid's Wiki

Prérequis

Idéalement:

Dès que nos deux VMs "hôtes" sont prêtes, nous allons installer le paquet sudo et ajouter notre utilisateur (ici, l'utilisateur /username/) au groupe sudo, puis nous allons créer un snapshot de nos VMs, qui nous servira à /rollback/ rapidement pendant nos différents tests:

su -
apt install sudo
usermod -aG sudo username
poweroff
# puis on prend un snapshot de notre VM.
# et on fait la même sur la deuxième VM. 

Gestion de Configuration

Les différents outils

Puppet

Puppet est un logiciel libre de gestion de configuration, écrit en Ruby et existant de puis 2005. Il permet via un agent installé sur chaque machine du parc de déployer, configurer et faire évoluer notre parc, soit de manière automatique (l'agent interroge le serveur puppet à intervalle régulier pour mettre à jour sa recette) soit de façon manuelle (l'administrateur lance la commande puppet agent -t pour exécuter la recette sur la machine ciblée).

Il est très puissant, mais peut sembler assez complexe au premier abord, exemple ici avec un serveur de mail d'environnement de préproduction:

class demomail {
  node /ob-demo-mail-all-*/ {
    include 'common::demo::base'

    class { 'postfix':
      master_smtp => 'smtp inet n - - - - smtpd',
      master_entries => [ 'slowsmtp unix - - - - 3 smtp', ]
    }

    file { '/etc/postfix/transport':
      notify => Service['postfix'],
      owner => 'root',
      group => 'root',
      mode => '644',
      source => 'puppet:///modules/common/postfix/transport_mailers',
      ensure => 'present',
    }

    file { '/etc/postfix/virtual':
      notify => Service['postfix'],
      owner => 'root',
      group => 'root',
      mode => '644',
      source => 'puppet:///modules/common/postfix/virtual',
      ensure => 'present',
    }

    postfix::config {
      'mailbox_size_limit': value => '0';
      'smtpd_error_sleep_time': value => '0';
      'maximal_queue_lifetime': value => '1d';
      'transport_maps': value => 'hash:/etc/postfix/transport';
      'mynetworks': value => '127.0.0.0/8, 10.90.0.0/24';
      'smtpd_recipient_restrictions': value => 'permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination';
      'local_header_rewrite_clients': value => 'permit_mynetworks';
      'slowsmtp_destination_concurrency_limit': value => '2';
      'slowsmtp_destination_rate_delay': value => '2s';
      'slowsmtp_destination_recipient_limit': value => '6';
    }
  }
}

Il dispose d'énormément de modules qui permettent d'étendre ses fonctionnalités et de gérer une large gamme de logiciels et services. Il y a des modules pour à peu près tout ce qui existe 😃, par exemple dans l'exemple ci-dessus, on utilise un module dédié à Postfix, un serveur de mail très utilisé.

Il gère les OS suivants:

Un logiciel de gestion de configuration très puissant, et adapté à la gestion de parcs composés de centaines voire de milliers de machine (bien évidemment, le ou les serveurs puppet doivent être à l'avenant pour pouvoir "suivre" les demandes de centaines d'agents 😃)

Chef

Chef a d'abord été écrit en Ruby - comme Puppet - en 2009, puis a été réécrit en Erlang en 2013. Sa syntaxe est très proche de celle de Ruby. C'est un logiciel libre mais pas gratuit: son utilisation est gratuite jusqu'à 5 machines, puis payante ensuite:

Il fonctionne sur le même principe client-serveur que Puppet, et nécessite donc l'installation d'un agent sur les machines gérées par le serveur Chef.

Il gère les OS suivants:

Votre serviteur n'ayant jamais utilisé ce logiciel... il ne peut guère vous fournir d'autres informations sur le sujet 😃

SaltStack

SaltStack est un logiciel de gestion de configuration libre et gratuit, apparu en 2011, écrit en Python, et dont les recettes sont écrites en YAML, Jinja2 (pour le templating de fichiers de configuration) et Python (pour les tâches plus complexes). Il se compose de deux services:

Il est notamment utilisé par OpenSuse Kubic pour joindre et provisionner les noeuds d'un cluster Kubernetes.

Il gère les OS suivants:

Ansible

Ansible est apparu en 2012. Contrairement aux autres solutions, Ansible est une solution /agentless/: le seul "agent" requis pour déployer nos configurations est SSH. Ansible a été racheté par RedHat en 2015, et est utilisé en France notamment par:

Les /recettes/ d'Ansible sont écrites en YAML, et on dispose de nombreux modules permettant de gérer une vaste gamme de logiciels différents. Il permet de gérer la configuration de machines sous:

Mise en place: Ansible

Terminologie

Ansible utilise une terminologie particulière. Tout d'abord nos machines sont référencées dans /l'inventaire/, en fonction de leurs /rôle/, rôles auxquels sont attachées les /tâches/ nécessaire à l'installation et la configuration (via des /templates/ de fichiers de configuration) des différents applicatifs. Pour résumer:

Paquets prérequis

C'est très simple, il nous suffit d'installer les paquets /ansible/ et /sshpass/:

sudo apt install ansible sshpass whois

Le paquet ansible se passe de commentaire, sshpass nous permettra de fournir le mot de passe de notre utilisateur déjà précréé, et whois arrive avec l'utilitaire mkpasswd, qui nous permettra de créer et hasher le mot de passe de notre utilisateur "bob" (ou "alice", ou "michel", vous voyez le topo)

Nos machines sont référencées dans un fichier /hosts/ dont la syntaxe est de la forme:

[proxies]
b3-lb-1 ansible_host=192.168.1.3 ansible_ssh_port=22

[webservers]
b3-web-1 ansible_host=192.168.1.4 ansible_ssh_port=22
b3-web-2 ansible_host=192.168.1.5 ansible_ssh_port=22

[bdd]
b3-bdd-1 ansible_host=192.168.1.6 ansible_ssh_port=22

On voit que les machines sont ici organisées par /rôles/. Par exemple le rôle "proxies", ici, installerait le logiciel HAProxy et son copain hatop sur la machine b3-lb-1 et copierait les fichiers de configurations requis au bon fonctionnement d'HAProxy. Le fichier YAML permettant le déploiement d'un rôle sur un ensemble de machines s'appelle un /playbook/.

Si je dois déployer un deuxième proxy identique, il me suffit d'ajouter une ligne dans mon fichier hosts dans la section rôle correspondante et de jouer le /playbook/ voulu.

Création de notre fichier hosts

On va donc créer, dans un répertoire ansible créé pour l'occasion sur notre VM Desktop, un fichier hosts référençant nos deux VMs b3-web et b3-bdd:

[webservers]
b3-web ansible_host=192.168.1.4 ansible_ssh_port=22

[bdd]
b3-bdd ansible_host=192.168.1.6 ansible_ssh_port=22

Création de notre premier "playbook"

Ici, on va créer notre premier playbook. Ce playbook, par défaut, va s'appliquer à tous nos /hôtes/ et aura pour tâches:

Finie la tambouille manuelle à coup de /scp/, /chmod/, /nano/!

On obtiendrait un playbook ressemblant à ça:

---
- hosts: all
  vars:
    - bob_password: '$6$hj9ri4u8t/dWrqD6$jQzAyAfUFVr8K/Q8YcZAqS4ITVtnhBJj6LakZHaZFsyFtnsX5bcj.rQLm41cYYgyOYv8yiDTX2a3jB7x4r.uO1'
  become: yes
  become_method: sudo
  tasks:
  - name: Add user Bob
    user:
      name=bob
      password={{ bob_password }}
  - name: Add bob to sudoers
    copy:
      dest: "/etc/sudoers.d/bob"
      content: "bob ALL=(ALL:ALL) ALL"
  - name: deploy bob ssh keys
    authorized_key: user=bob
                    key="{{ lookup('file', '/home/lidstah/.ssh/id_ed25519.pub') }}"
                    state=present
  - name: deploy ssh keys
    authorized_key: user=lidstah
                    key="{{ lookup('file', '/home/lidstah/.ssh/id_rsa.pub') }}"
                    state=present
                    key="{{ lookup('file', '/home/lidstah/.ssh/id_ed25519.pub') }}"
                    state=present
  - name: check SSHD configuration
    lineinfile:
      path: /etc/ssh/sshd_config
      regexp:  '#PasswordAuthentication yes'
      line: 'PasswordAuthentication no'
    notify:
      - restart sshd

  handlers:
    - name: restart sshd
      ansible.builtin.service: 
        name: sshd
        state: restarted

On va détailler un peu plus ce que fait notre recette, mais comme vous pouvez le constater c'est assez clair.

Pour exécuter notre playbook, nous allons utiliser la commande /ansible-playbook/:

ansible-playbook -K -k -i hosts sshplaybook.yml

Que font nos différentes options:

Enfin, l'argument final est le nom du playbook.

On peut aussi vouloir limiter l'exécution du playbook sur un seul hôte:

ansible-playbook -K -i hosts --limit b3-bdd sshplaybook.yml

Pas à pas: un playbook pour une mini-infra

Ici, nous allons installer, sur deux petites VMs, Apache, PHP sur la première et MariaDB sur la seconde, et les configurer.

Dès lors nous aurons deux rôles:

Notre playbook devra donc installer et configurer Apache et PHP sur la VM dont le rôle est "web", et MariaDB sur la machine dont le rôle est "bdd".

Étape 0: Créons notre arborescence

Tout d'abord, on va créer nos répertoires et sous répertoires de notre playbook:

[lidstah@eink k3scluster]$ tree
.
├── inventory
│   └── hosts.ini
├── roles
│   ├── all
│   │   └── tasks
│   │       └── main.yml
│   ├── web
│   │   └── tasks
│   │       └── main.yml
│   └── bdd
│       └── tasks
│           └── main.yml
└── site.yml

Étape 1: Notre fichier d'inventaire

Notre fichier d'inventaire va contenir les adresses de nos machines en fonction de leur rôle. On va donc éditer le fichier inventory/hosts.ini (mais on pourrait le placer ailleurs. Là c'est par convention)

Dans mon cas:

  [web]
  192.168.1.80

  [bdd]
  192.168.1.81

Ça reste donc très simple.

Étape 2: définissons les tâches s'appliquant à chaque rôle (all)

Pour définir les tâches s'appliquant à chaque rôle (en l'occurence la configuration SSH), nous allons modifier le fichier roles/all/tasks/main.yml.

Quelles sont ces tâches?

Une tâche Ansible est généralement atomique, c'est à dire qu'elle ne fait qu'une seule chose (installer un paquet, modifier un fichier de configuration, copier un fichier de configuration depuis les templates, exécuter une commande spécifique, ajouter ou modifier une ligne dans un fichier, créer ou supprimer un utilisateur du système, etc).

Une tâche Ansible est toujours définie par un nom (name). Par exemple, une tâche ansible pour installer un paquet "vim" sera de la forme (sous Debian, donc apt):

- name: Install Vim, the best text editor in the world
  apt:
    name: vim
    state: latest

Dans notre cas (SSH), nous avons donc trois tâches à effectuer:

Notre première tâche va donc s'écrire:

- name: copy my SSH key
  authorized_key:
    user: votre_username
    state: present
    key: "ssh-ed25519 AAAAC3Nza.....CZa lidstah@arcturus"

Et voilà!

Notre seconde tâche va remplacer la ligne "#PasswordAuthentication yes" par la ligne "PasswordAuthentication no" dans le fichier /etc/ssh/sshd_config, en utilisant la commande lineinfile:

- name: configure SSHD
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '#PasswordAuthentication yes'
    line: 'PasswordAuthentication no'

Et voilà! Il ne nous reste plus qu'à redémarrer sshd, notre troisième tâche, qui va juste exécuter une commande shell:

- name: restart SSHd
  command: systemctl restart sshd

Notre fichier roles/all/tasks/main.yml devrait donc ressembler à ça - faites attention, on voudra toujours commencer nos fichiers yml par "---", c'est un séparateur en YAML

---
- name: copy my SSH key
  authorized_key:
    user: votre_username
    state: present
    key: "ssh-ed25519 AAAAC3Nza.....CZa lidstah@arcturus"

- name: configure SSHD
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '#PasswordAuthentication yes'
    line: 'PasswordAuthentication no'

- name: restart SSHd
  command: systemctl restart sshd

On voit ici que nos trois tâches vont s'enchaîner: d'abord la copie de la clef SSH, puis la modification de la configuration SSH et enfin le restart du daemon SSHd.

Le fichier site.yml

Avant de s'attaquer à nos fichier de tâches pour notre rôle Web et notre rôle BDD, on va faire un petit détour par le fichier site.yml à la racine de notre projet ansible. Pour le moment il est vide, mais il va nous permettre de dire quels rôles appliquer à nos hôtes. "hosts: all" signifie que le rôle s'applique à tous les hôtes, quels que soit leur position dans le fichier d'inventaire. Par exemple, pour notre rôle "all" configurant notre serveur SSH proprement:

---
- hosts: all
  gather_facts: yes
  become: yes
  roles:
    - role: all

Ici:

On va pouvoir jouer notre playbook une première fois:

  ansible-playbook -K -k -i inventory/hosts.ini site.yml

Lorsque nous ajouterons nos rôles web et bdd, nous ajouterons aussi un bloc "hosts" similaire dans notre fichier site.yml.

Notre rôle web

De la même façon que pour notre rôle "all", notre rôle web (roles/web/tasks/main.yml) va exécuter des tâches très simples:

Commençons par installer Apache2 via ansible:

---
- name: Install Apache2
  apt:
    name: apache2
    state: latest

Alors, on pourrait s'amuser à rédiger autant de blocs "apt" que voulu pour nos paquets PHP mais ce serait fastidieux, non? Même avec un bon éditeur de texte! Ansible à la rescousse: on peut utiliser un système nous permettant de boucler sur une liste!

- name: install PHP packages
  apt: name={{ item }} state=latest
  loop: [ 'php', 'php7.4-mysql', et caetera, cf liste ]

Et enfin, relancer Apache2 de la même manière que sshd précedemment:

- name: restart apache2
  command: systemctl restart apache2

Idéalement, on voudrait aussi que notre rôle nous copie le bon vhost au bon endroit, et qu'il installe notre applicatif au bon endroit, etc. Mais pour le moment on reste dans le simple.

Notre rôle bdd

Maintenant, il ne nous reste plus qu'à écrire notre rôle base de données (roles/bdd/tasks/main.yml). De quoi avons nous besoin?

Ça tombe bien, on sait déjà faire tout ça! Dès lors, notre fichier main.yml devrait ressembler à ça:

---
- name: install MariaDB packages
  apt: name={{ item }} state=latest
  loop: [ 'mariadb-server', 'mariadb-client' ]

- name: modify mariadb server configuration
  lineinfile:
    path: /etc/mysql/mariadb.conf.d/50-server.cnf
    regexp: '^.*bind-address.*'
    line: 'bind-address = 0.0.0.0'

- name: restart MariaDB server
  command: systemctl restart mariadb

Et voilà, rien de compliqué en fait! Alors on pourrait vouloir ajouter des utilisateurs directement, créer des bases de données, injecter un dump dans une base, etc... Et bien sûr il existe un module MariaDB pour ansible:

https://mariadb.com/kb/en/existing-ansible-modules-and-roles-for-mariadb/

https://docs.ansible.com/ansible/2.9/modules/list_of_database_modules.html

Finalisation: notre fichier site.yml

Il ne nous reste plus qu'à finaliser notre fichier site.yml pour pouvoir jouer notre playbook sur notre "infrastructure":

---
- hosts: all
  gather_facts: yes
  become: yes
  roles:
    - role: all

- hosts: web
  gather_facts: yes
  become: yes
  roles:
    - role: web

- hosts: bdd
  gather_facts: yes
  become: yes
  roles:
    - role: bdd

Et enfin jouer notre playbook, sans l'option '-k' vu qu'on l'a normalement déjà jouée:

  ansible-playbook -K -i inventory/hosts.ini site.yml

Allons plus loin: Un petit cluster k3s

Une meilleure organisation de nos playbooks, "directory-based"

Plutot que d'écrire une énorme recette imbuvable, Ansible nous permet de découper notre playbook dans des répertoires et sous répertoires, et de stocker les taches individuelles de chaque rôle dans ces sous-répertoires. Par exemple (car une représentation graphique vaut toutes les explications magistrales):

[lidstah@eink k3scluster]$ tree
.
├── inventory
│   ├── group_vars
│   │   └── all.yml
│   └── hosts.ini
├── roles
│   ├── all
│   │   └── tasks
│   │       └── main.yml
│   ├── master
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── nfs.yaml.j2
│   └── node
│       └── tasks
│           └── main.yml
└── site.yml

k3s: kubernetes allégé

K3S est une distribution de Kubernetes orientée "IoT", plus légère qu'une distribution Kubernetes classique du fait de certains choix techniques (etcd remplacé par sqlite, par exemple) tout en conservant les fonctionnalités habituelles d'un cluster Kubernetes. Elle est tout à fait adaptée à des équipements peu puissants du type Raspberry Pi ou petites VMs.

De plus, son installation est triviale 😃 (cf cours maintenance opérationnelle pour le détail)

Détail de la recette

Notre recette est ici subdivisée dans plusieurs sous-répertoires dont les noms indiquent l'utilisation:

Je pars du principe que vous avez déjà joué votre playbook ssh sur vos 2 ou 3 VMs k3s. Vous devrez adapter les fichiers suivants à votre setup:

Ensuite, il vous suffit de taper:

  ansible-playbook -K -i inventory/hosts.ini site.yml

Pour déployer un joli petit cluster k3s qui va nous permettre d'expérimenter un peu avec Kubernetes dans la suite du module.

Une fois les /playbooks/ joués, sur le node master (/control-plane/ en terminologie Kubernetes), en root (on verra ensuite comment utiliser le client /kubectl/ depuis notre machine personnelle):

  kubectl get nodes

qui devrait nous donner quelque chose du style:

NAME             STATUS   ROLES                  AGE     VERSION
k3s-m1   Ready    control-plane,master   7d22h   v1.24.6+k3s1
k3s-w2   Ready    <none>                 7d22h   v1.24.6+k3s1
k3s-w1   Ready    <none>                 7d22h   v1.24.6+k3s1

Indiquant le bon fonctionnement des nodes de notre petit cluster 😃

Terraform

Installation

Suivant votre setup, vous pouvez télécharger Terraform pour linux, windows, macOS, FreeBSD, OpenBSD et Solaris ici:

https://www.terraform.io/downloads

Il suffit ensuite de suivre les instructions. Pour Linux, vous pouvez soit ajouter le dépot terraform (Debian et consorts), soit télécharger le standalone et le copier dans votre PATH (/usr/local/bin par exemple)

On peut ensuite vérifier la version de terraform:

[lidstah@eink ~]$ terraform version
Terraform v1.3.2
on linux_arm64

Concepts

Organisation de nos recettes terraform

Idéalement, on séparera configuration, variables et ressources dans différents fichiers ".tf".

Par exemple, je sépare mes fichiers de la sorte:

Bien sur, comme disent nos amis anglophones, YMMV /Your Mileage May Vary/ 😃

Terraform utilise un vrai langage de programmation, mais axé "gestion d'infratructure" - on ne codera pas un moteur 3D avec ce langage - appelé HCL pour Hashicorp Configuration Language. HashiCorp est l'éditeur de Terraform, et une entreprise influente dans le milieu du logiciel libre.

Comme tout langage de programmation, on peut utiliser:

Providers

Terraform peut communiquer avec pléthore de solutions: Virtualisation (VSphere, Proxmox, XCP-NG...) mais aussi IPAM/DCIM (netbox, phpIPAM...), serveurs DNS (PowerDNS, Bind9...) afin d'y créer les ressources désirées. Cependant, pour cela, il a besoin de /providers/ lui permettant de dialoguer avec ces solutions.

Il y a donc des providers pour Proxmox, VSphere, VMWare Workstation, Virtualbox, Kubernetes, netbox, etc.

Cependant il faut les configurer, et notamment la partie authentification - on y revient dans la section sécurité plus bas - permettant à Terraform de créer les ressources voulues.

Exemple de mon fichier main.tf:


terraform {
  backend "pg" {
    conn_str = "postgres://terraform:supermotdepasse@192.168.1.136/terraform_backend?sslmode=disable"
  }
  required_providers {
    proxmox = {
      source  = "telmate/proxmox"
      version = "2.9.11"
    }
    netbox = {
      source = "e-breuninger/netbox"
      version = "~> 2.0.1"
    }
    powerdns = {
      source = "pan-net/powerdns"
      version = "1.5.0"
    }
  }
}

provider "proxmox" {
  pm_tls_insecure = true
  pm_api_url = "https://polaris.lidstah.info:8006/api2/json"
  pm_parallel = "4"
  pm_debug = true
}

provider "netbox" {
  server_url = "https://netbox.lidstah.info"
}

provider "powerdns" {
  server_url = "https://pdns-api.lidstah.info"
}

Comme vous pouvez le constater, hormis sur la backend pg, les identifiants ne sont pas renseignés dans la configuration.

En effet, je les charge depuis un fichier d'environnement chiffré en AES-256-CBC. En production on signe ce fichier avec nos clefs GPG. Dans notre cas, pour le moment, on renseignera directement ces valeurs dans notre main.tf, vu qu'on ne commitera rien sur des dépots externes (github, gitlab, whatever)

Bien entendu, les différentes options sont disponibles dans la documentation de chaque provider, sur le /registry/ officiel de Terraform.

Lorsque nous initialiserons terraform dans notre répertoire de travail via la commande:

terraform init

Terraform téléchargera automatiquement nos providers (cool!)

Variables

Terraform permet d'utiliser des variables, par exemple pour stocker une liste d'adresses IP à assigner à nos VMs, ou une liste des hyperviseurs où déployer ces VMs, et ainsi de suite. Par exemple:

variable "node_names" {
  description = "proxmox cluster nodes"
  type = list(string)
  default = [
    "polaris",
    "procyon",
  ]
}

variable "master_vmids" {
  description = "VM IDs for masters"
  type = list(string)
  default = [
    "200",
  ]
}

variable "worker_vmids" {
  description = "VM IDs for workers"
  type = list(string)
  default = [
    "201",
    "202",
    "203",
  ]
}

Ici, je définis des VMIDs ainsi que les noeuds de mon cluster Proxmox où déployer mes VMs. Pour les IPs, j'utilise une source de données en provenance de netbox qui signifie, en gros: "assigne à la VM la première IP disponible dans l'IPAM dans le bloc voulu". Sinon j'aurai pu faire une liste d'IPs à attribuer aux VMs de façon séquentielle.

Ressources

Le gros morceau: les ressources sont les éléments que Terraform va créer sur notre infrastructure. Il peut s'agir de VMs, mais aussi d'enregistrements DNS, d'enregistrements dans un IPAM, de containers docker ou de pods Kubernetes, de tasses de café ou de pizzas à commander (presque!)

Alors, si on avait 10 VMs identiques à créer, on pourrait tout à fait décrire une ressource par VM, mais ce serait fastidieux. On peut utiliser une boucle et nos variables précedemment créées pour simplifier notre recette:

resource "proxmox_vm_qemu" "k3s-master" {
  count = 1
  vmid = var.master_vmids[count.index]
  name = "k3s-master${count.index + 1}"
  desc = "k3s cluster control plane"
  target_node = "procyon"

  full_clone = false
  clone = "deb-cloudinit-template"

  pool = "Testing"

  cpu     = "host"
  cores   = 2
  memory  = 2048
  os_type = "cloud-init"
  ipconfig0 = "ip=${netbox_available_ip_address.k3smaster_ip[count.index].ip_address},gw=192.168.1.254"
}

data "netbox_ip_range" "k3snetbox" {
  contains = "192.168.1.1/24"
}

resource "netbox_available_ip_address" "k3smaster_ip" {
  count = 1
  ip_range_id = data.netbox_ip_range.k3snetbox.id
  status = "active"
  dns_name = "k3s-master${count.index + 1}.lidstah.info"
  description = "k3s test cluster control plane"
}

resource "powerdns_record" "k3s-master" {
  count = 1
  zone = "lidstah.info."
  name = "k3s-master${count.index + 1}.lidstah.info."
  type = "A"
  ttl = 300
  records = [replace("${netbox_available_ip_address.k3smaster_ip[count.index].ip_address}", "/\\/.*$/", "")]
}

resource "proxmox_vm_qemu" "k3s-worker" {
  count = 3
  vmid = var.worker_vmids[count.index]
  name = "k3s-worker${count.index + 1}"
  desc = "k3s cluster worker"
  target_node = var.node_names[count.index % 2]

  full_clone = false
  clone = "deb-cloudinit-template"

  pool = "Testing"

  cpu     = "host"
  cores   = 2
  memory  = 4096
  os_type = "cloud-init"
  ipconfig0 = "ip=${netbox_available_ip_address.k3sworker_ip[count.index].ip_address},gw=192.168.1.254"
}

resource "netbox_available_ip_address" "k3sworker_ip" {
  count = 3
  ip_range_id = data.netbox_ip_range.k3snetbox.id
  status = "active"
  dns_name = "k3s-worker${count.index + 1}.lidstah.info"
  description = "k3s test cluster worker"
}

resource "powerdns_record" "k3s-worker" {
  count = 3
  zone = "lidstah.info."
  name = "k3s-worker${count.index + 1}.lidstah.info."
  type = "A"
  ttl = 300
  records = [replace("${netbox_available_ip_address.k3sworker_ip[count.index].ip_address}", "/\\/.*$/", "")]
}

Ici notre boucle est simplement définie, pour chaque ressource, via le paramètre /count/, et nos variables utilisent // pour la numérotation voulue (k3s-worker1,2,3... etc). le + 1 est ici présent uniquement car count.index, comme tout index de liste qui se respecte, débute à zéro 😃. Et oui, on peut faire des calculs avec Terraform, notamment le modulo /%/ très important pour boucler sur une liste!

Workspaces et synchronisation d'état

Alors, vous allez travailler en solo pour commencer, et Terraform par défaut stocke l'état des ressources qu'il a déployées en local sur votre machine. C'est suffisant pour une utilisation simple, mais lorsqu'on travaille en équipe, cela peut causer des soucis d'inconsistance de l'état des ressources déployées entre les différents postes de travail de vos collaborateurs et collaboratrices.

Dès lors, on va avoir besoin d'une solution de centralisation de ces informations. Ça tombe bien, Terraform supporte plein de backends différentes pour stocker ces informations:

Dans mon cas, chez moi comme en prod, j'utilise la backend PostgreSQL qui fait très bien le boulot.

De plus, différentes équipes peuvent travailler sur différents projets, il est donc nécessaire de pouvoir séparer les états terraform de ces différents projets afin d'éviter des conflits, voire des catastrophes entre les états terraform de tous ces projets. Pour pallier à ce problème, Terraform propose des espaces de travail, /workspaces/, pour isoler nos projets les uns des autres:

  terraform workspace list
  terraform workspace new projet1
  terraform workspace new projet2
  terraform workspace select projet2
  terraform state pull
  terraform workspace delete projet2  

Terraform plan: Planifier et vérifier

Une fois nos recettes Terraform pretes, on va vouloir les appliquer à nos providers, pour créer - enfin - nos ressources, ou pour les détruire lorsqu'elles ne sont plus nécessaires. On va commencer par créer un plan:

  terraform plan -out test.tfplan

Terraform va alors nous indiquer les ressources qu'il va créer et leur configuration. Une fois qu'on a vérifié que notre plan est bien en accord avec les ressources qu'on désire créer, on peut l'appliquer:

  terraform apply test.tfplan

Enfin, une fois que nos ressources ne sont plus nécessaires, on peut détruire ces ressources en créant un plan de destruction - qu'on vérifiera 42 fois avant de l'appliquer 😉 - et l'appliquer:

  terraform plan -destroy -out destroy.tfplan
  terraform apply destroy.tfplan

Et voilà!

Mise en place

Installation du provider VSphere

Note sur la sécurité

Création de notre première ressource sur le cluster VSphere de l'EPSI

Destruction de notre première ressource