Prérequis
Idéalement:
- Une VM Debian Desktop, 2 coeurs, 2 Go RAM
- Deux VMs Debian SANS GUI, 1 coeur, 512Mo à 1Go de RAM, avec des /hostnames/ différents et le paquet /openssh-server/ installé (Option serveur SSH lors de l'install). Pas besoin de configurer l'accès SSH par clefs, Ansible le fera pour nous
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:
- GNU/Linux
- BSD
- macOS
- Windows
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:
- 120$ par mois pour 20 serveurs
- 600$ par mois pour 100 serveurs. Donc, 6000$ par mois pour 1000 machines.
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:
- GNU/Linux
- BSD
- Windows
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:
- le service /salt-master/ qui s'exécute sur le serveur de gestion de configuration
- le service /salt-minion/ qui s'exécute sur les machines gérées par le serveur de gestion de configuration
Il est notamment utilisé par OpenSuse Kubic pour joindre et provisionner les noeuds d'un cluster Kubernetes.
Il gère les OS suivants:
- GNU/Linux
- BSD
- Windows
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:
- Airbus
- La Poste
- La Société Générale
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:
- GNU/Linux
- BSD
- Windows
- macOS
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:
- Dans l'inventaire on renseigne nos machines
- En fonction de leur rôle
- À ces rôles sont attaché des ensembles de tâches permettant de décrire l'état final voulu
- Ces tâches peuvent utiliser des templates de fichiers de configuration (par exemple un vhost apache, ou la configuration d'HAProxy, etc) au format jinja2
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:
- D'ajouter un utilisateur "bob" (ou "alice", ou "michel", bref...) et de le rendre /sudoer/
- D'ajouter sa clef SSH à l'utilisateur bob
- dans la foulée, d'ajouter sa clef SSH à notre utilisateur
- Et enfin de configurer SSH pour désactiver l'authentification login/mot de passe SSH, puis de relancer le service /sshd/ pour prendre en compte la modification de la configuration.
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:
- -K: demander le mot de passe sudoer (ou su, suivant la méthode become choisie)
- -k: demander le mot de passe SSH (la recette va automatiquement configurer clef SSH et daemon SSHD pour ne plus jamais utiliser ces vilains logins/mots de passe)
- -i: indiquer le fichier "hosts" à utiliser
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:
- Un rôle "web"
- Un rôle "bdd"
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?
- nous voulons ajouter notre clef SSH publique au fichier .ssh/authorized_keys de notre utilisateur
- nous voulons configurer SSH pour qu'il n'autorise pas les connexions via login/mot de passe.
- nous voulons relancer le /daemon/ ssh pour qu'il prenne en compte notre modification de configuration.
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:
- copier notre clef publique dans le fichier authorized_keys, via le module "ansible.posix.authorized_key", qu'on va pouvoir abréger "authorized_key" car il s'agit d'un module /builtin/
- modifier notre configuration SSH, via la commande /linefile/
- Et enfin relancer notre daemon SSH, en exécutant une commande shell via la commande ansible /command/ qui nous permettra d'exécuter /systemctl restart sshd/. (note: il y a plus propre, mais on va rester dans le simple)
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:
- hosts indique sur quels hôtes définis dans notre inventaire on va appliquer le(s) rôle(s) définis dans la liste yaml "roles", ici, /all/, c'est à dire tous
- gather_facts: yes signifie qu'on va d'abord vérifier que les tâches définies dans le rôle n'ont pas déjà été jouées
- become: yes signifie qu'on va devoir effectuer une élévation de privilège (par défaut sudo)
- roles: liste le ou les rôles qu'on veut appliquer aux hôtes sélectionnés, ici notre rôle 'all'
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:
- installer Apache2 (bon je préfère nginx mais on va rester dans le connu)
- installer plusieurs paquets PHP:
- php (ofc)
- php7.4-mysql
- libapache2-mod-php
- php7.4-gd
- php7.4-imagick
- relancer apache2
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?
- d'installer Mariadb server et client
- de configurer Mariadb pour qu'elle écoute sur toutes ses interfaces réseau
- et enfin de relancer mariadb pour qu'elle prenne en compte les modifications de notre configuration.
Ç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:
- inventory pour notre fichier hosts et nos variables globales (répertoire group_vars)
- rôles, subdivisé en sous-répertoires indiquant les groupes de machines de l'inventaire (all, master, node)
- les taches (/tasks/) et /templates/ (de fichiers de configuration) à appliquer sur le rôle dans chaque sous-répertoire tasks et templates d'un sous-répertoire rôle
- Et enfin un fichier 'site.yml' à la racine de notre playbook pour dire à ansible de lancer tout ce beau monde
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:
- inventory/hosts.ini
- inventory/group_vars/all.yml
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:
- main.tf : configuration des providers et synchro d'état
- vars.tf : variables utilisées par mes recettes
- vms.tf : configuration des ressources à créer (VMs, enregistrements DNS et DCIM/IPAM, etc)
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:
- Des variables, de types classique: entiers, string, listes...
- Des boucles
- Des conditions (if..then..else..)
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:
- S3
- Minio
- MongoDB
- PostgreSQL
- etcd
- et j'en passe
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à!