Title: Instancier une machine virtuelle Debian dans Proxmox avec OpenTofu et Cloud-Init
Category: sysadmin
Tags: OpenTofu, Debian, Cloud-Init
Date: 2024-11-08 0:04
Cover: assets/backgrounds/turris_ssh_cle.jpg
status: published

Installer une machine virtuelle dans [Proxmox VE][proxmox] n'est pas une tâche
très sympa. Mais heureusement il est possible de l'automatiser en
utilisant [OpenTofu][opentofu] et [Cloud-Init][cloudinit]. C'est ce que nous
allons faire dans cet article en installant une machine Debian.

## Préparer OpenTofu

Je pars du principe que vous avez déjà une installation fonctionnelle de 
[Proxmox VE][proxmox] et d'OpenTofu.

Pour permettre à OpenTofu de communiquer avec notre instance, nous utiliserons
le provider écrit par *Pavel Boldyrev* dont le code est disponible [sur
GitHub][bgp_pve_terraform]

Commençons par créer un dossier pour notre projet OpenTofu et à l'intérieur un 
fichier `provider.tf` avec le contenu suivant:

```terraform
{! content/articles/2024/pve_opentofu_debian_cloudinit/files/provider.tf !}
```

Le premier bloc nous permet de déclarer les *providers* utilisés, bon ici nous
en avons un seul...

Le second est là pour configurer notre *provider Proxmox* en lui donnant les
informations utiles pour qu'il se connecte à l'API. Nous utilisons des variables
que nous définirons... Maintenant!

## Définir nos variables

Maintenant que notre *provider* est configuré, nous pouvons passer à la
définition de nos variables dans le fichier `variables.tf`:

```terraform
{! content/articles/2024/pve_opentofu_debian_cloudinit/files/variables.tf !}
```

Afin de donner des valeurs à nos variables, ajoutons le fichier `
terraform.tfvars` qui sera lu automatiquement par *OpenTofu*:

```terraform
# PVE informations
pve_endpoint = "https://192.168.0.200"
pve_username = "root"
pve_password = "Mysup3rPassW0rd"
pve_node     = "pve01"
pve_insecure = true

# Debian images
debian_image_url = "https://cloud.debian.org/images/cloud/bookworm/20241004-1890/debian-12-generic-amd64-20241004-1890.qcow2"
debian_image_checksum = "da84d609d7ec5645dae1df503ea72037b2a831401d1b42ce2e7ec2a840b699f07ca8aea630853a3d5430839268c2bd337be45d89498264c36a9b5e12872c59ee"
```

Pour la partie informations d'identifications au serveur PVE, remplacez les
valeurs données par les vôtres.

Pour l'image, il suffit de regarder sur le dépôt [Debian relatif aux images
cloud][debian-cloudimages]. Pour le choix de la *somme de contrôle* à utiliser,
il suffit de regarder dans le fichier `SHA512SUMS` situé dans le dossier des
images et de choisir la bonne ligne.

## Petit détour par Cloud-Init

Laisson de côté *OpenTofu* un moment pour passer un peu de temps sur *Cloud
Init*. Cloud-Init permet **d'automatiser l'initialisation d'instance cloud**. Il
est utilisé par de nombreuses distribution Linux et FreeBSD.

Cette phase d'initialisation comprend entre autres la création d'utilisateur, la
mise à jour des dépôts logiciels, l'installation de paquets. Les ordres sont
donnés au service *Cloud-Init* (lancé dans notre machine virtuelle) par
l'intermédiaire de fichiers disponibles dans un lecteur amovible à l'intérieur
de notre machine virtuelle.

Pour notre exemple, nous allons:
 
* créer un utilisateur et installer sa clé publique SSH
* définir le nom d'hôte de la machine
* mettre à jour les dépôts logiciel
* installer `qemu-guest-agent` et activer le service associé

Le dernier point nous permettra s'activer les intégrations de notre machine
virtuelle de test avec notre hyperviseur.

### Vendor config

Cloud-Init utilise (entre autres) des fichers YAML pour gérer les configurations
à appliquer. Le fichier `vendor-config` sert à appliquer les configurations
relative **au fournisseur de l'infrastructure**: dans notre cas *Proxmox VE*.

Ce `vendor-config` nous permettra d'installer et configurer *l'agent Qemu*. Cet
agent permet une meilleure intégration de  notre machine virtuelle dans
l'hyperviseur. Par exemple *Terraform* sera capable de récupérer et afficher
l'adresse IP de la machine créée.

Pour cela, ajoutons le répertoire `cloudinit` à notre projet et à l'intérieur le
fichier `vendor-config.yaml` avec le contenu suivant:

```yaml
{! content/articles/2024/pve_opentofu_debian_cloudinit/files/cloudinit/vendor-config.yaml !}
```

Ce fichier parle de lui même:

* mise a jour des dépôts;
* installation du paquet `qemu-guest-agent`;
* activation du service associé via la directive `runcmd`;

### User config

Le fichier `user-config` est destiné à appliquer les configurations voulues
par l'utilisateur **qui instancie la machine**. Nous allons nous en servir pour
ajouter un utilisateur à notre machine.

Ajoutons le fichier `user-config.yaml` dans le répertoire `cloudinit/` avec le
contenu suivant:

```yaml
{!content/articles/2024/pve_opentofu_debian_cloudinit/files/cloudinit/user-config.yaml!}
```

La plupart des éléments (si ce n'est tous) doivent vous parler, `hostname`
permet de définir le nom de la machines et `users` les comptes utilisateurs. Il
faut bien entendu personnaliser ce fichier en fonction de vos besoins.

La documentation de Cloud-Init donne un [exemple complet][cloudinit_doc] des
paramètres disponibles.

## Notre script OpenTofu

### Téléchargement de l'image Debian

Il nous reste maintenant notre script `main.tf` à créer. Commençons par le
téléchargement de l'image Debian:

```terraform
{! content/articles/2024/pve_opentofu_debian_cloudinit/files/main.tf!lines=1-11}
```

Ce bloc permet le téléchargement de l'image dans le stockage défini par
`datastore_id` dans le fichier donné par le paramètre `file_name`.

Le paramètres `overwrite` défini le comportement de PVE si le fichier image
existe déjà dans `datastore_id`:

* `false`: aucune action se sera effectuée, le fichier reste tel quel;
* `true`: si la taille du fichier local (sur PVE) et celle du fichier distant 
  (donnée par l'entête HTTP`Content-Length`) sont différentes, alors l'image 
  sera téléchargée à nouveau et le fichier local écrasé.

`overwrite_unmanaged` permet de remplacer l'image disque si elle existe sur
`datastore_id` et n'est pas gérée par OpenTofu.

### Les fichiers Cloud-Init

Pour permettre à PVE de stocker les fichiers Cloud-Init envoyés par OpenTofu,
nous devons activer le stockage des *snippets* depuis l'interface web de Proxmox 
VE. Il nous suffit d'aller dans *Datacenter* puis *Stockage* et de double
cliquer sur *local*.

![Capture d'écran de l'interface PVE montrant les paramètres du stockage local]({attach}./imgs/pve_snippets.png)

Ouvrons ensuite la liste déroulante *Content* et ajoutons *Snippets* puis
validons avec *OK*.

Une fois le stockage des snippets activés, ajoutons les instructions suivantes à
notre fichier `main.tf`:

```terraform
{!content/articles/2024/pve_opentofu_debian_cloudinit/files/main.tf!lines=13-33}
```

Nous utilisons pour cela la ressource `proxmox_virtual_environment_file`: 

* `content_type` permet de définir le type d'objet à stocker (images disque,
 image de conteneur, snippet);
* `source_raw` permet de définir la source, nous aurions pu utiliser la notation
  suivante pour l'option `data`[^data_option]:

        raw {
          data = <<-EOF
          #cloud-config
          users:
            - name: ephase
              # [...]
          EOF
        }

[^data_option]:mais personnellement je préfère la fonction `file()` permettant
    de lire le contenu depuis un fichier externe. Il est ainsi plus aisé - par
    exemple - de passer un linter sur le fichier YAML.

### Définition de notre machine virtuelle

Maintenant nous pouvons définir notre machine virtuelle par la définition d'une
ressource `proxmox_virtual_environment_vm`.

```terraform
{! content/articles/2024/pve_opentofu_debian_cloudinit/files/main.tf!lines=35-80 }
```

Première chose importante, l'option `depends_on` qui permet à OpenTofu
d'attendre que les définitions des deux fichiers *Cloud-Init* soient effectuée
avant de définir notre machine. Sans cette option, il arrive que la machine
démarre sans que les fichiers Cloud-Init ne soient prêt, ainsi les
personnalisations ne sont pas appliquées.

Ensuite nous trouvons un ensemble d'options qui définissent les paramètres de
notre machine:

* les blocs `cpu {}` et `memory {}` pour les options relative au calcul comme le
  nombre de cœurs CPU ou la taille de la mémoire;
* le bloc `disk {}` permet de définir un périphérique de stockage;
* le bloc `network_device` permet de définir un périphérique réseau.

Pour `disk` et `network_device`, il est possible de définir **plusieurs blocs**
afin d'instancier plusieurs périphériques. Je vous invite à aller voir la
documentation [du module][bgp-pve-vm-doc] sur son dépôt GitHub.

#### Agent Qemu

**Le bloc `agent` est important**, il permet d'activer le **service
d'intégration du côté *Proxmox VE***. Invité et hôte pourront échanger des
informations et activer des fonctionnalités avancées.

Côté invité, c'est les instructions données dans le fichier `vendor-config` qui
s'occupe de l'installation de l'agent comme nous l'avons vu.

#### Paramétrage de Cloud-Init

C'est dans le bloc `initialization` que se font les paramétrages des éléments de
*Cloud-Init* relatifs à notre machine virtuelle.

Le bloc `ip_config` permet de définir les paramètres réseau. Le paramétrage se
fait comme les autres blocs, via des valeurs affectées à des options. Mais il
est aussi possible d'utiliser des fichiers YAML comme ce que nous avons définis
plus haut. Pour `ip_config`, nous aurions pu utiliser `network_data_file_id` et
donner un identifiant de fichier.

C'est ce que nous faisons pour `vendor_data_file_id` et `user_data_file_id` qui
prennent comme valeurs les identifiants des fichiers définis dans les blocs
`proxmox_virtual_environment_file`:

```terraform
{!content/articles/2024/pve_opentofu_debian_cloudinit/files/main.tf!lines=77-79}
```

Nous passons en paramètre les *id* de fichiers.

### Output

Afin de connaitre l'adresse IP de la machine qui va être créée, ajoutons un bloc
`output` à la racine de notre fichier `main.tf`:

```terraform
{!content/articles/2024/pve_opentofu_debian_cloudinit/files/main.tf!lines=83-85}
```
## Application de notre script OpenTofu

Maintenant que tout est en place nous pouvons nous lancer dans l'exécution de
notre script. D'abord il faut initialiser Opentofu, c'est à cette étape qu'il va
télécharger les modules:

```text
tofu init
Initializing the backend...

Initializing provider plugins...
- Reusing previous version of bpg/proxmox from the dependency lock file
- Installing bpg/proxmox v0.64.0...
- Installed bpg/proxmox v0.64.0 (signed, key ID F0582AD6AE97C188)

Providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://opentofu.org/docs/cli/plugins/signing/

OpenTofu has been successfully initialized!

You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure. All OpenTofu commands
should now work.

If you ever set or change modules or backend configuration for OpenTofu,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

Vous pouvez lancer `tofu plan` afin de vérifier les actions effectuées par notre
script, mais  passons ici directement à l'action pour notre configuration:

```shell
tofu apply
# [...]
Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + debian_12_test_ip_address = (known after apply)

Do you want to perform these actions?
  OpenTofu will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:
```

Puis entrer *yes* afin de valider, les actions seront alors:

```text
proxmox_virtual_environment_file.vendor_config: Creating...
proxmox_virtual_environment_file.user_config: Creating...
proxmox_virtual_environment_download_file.debian_12: Creating...
proxmox_virtual_environment_file.vendor_config: Creation complete after 1s [id=local:snippets/vendor-config.yaml]
proxmox_virtual_environment_file.user_config: Creation complete after 1s [id=local:snippets/user-config.yaml]
proxmox_virtual_environment_download_file.debian_12: Creation complete after 8s [id=local:iso/debian-12-generic-amd64.img]
proxmox_virtual_environment_vm.debian_12_test: Creating...
proxmox_virtual_environment_vm.debian_12_test: Still creating... [10s elapsed]
proxmox_virtual_environment_vm.debian_12_test: Still creating... [20s elapsed]
proxmox_virtual_environment_vm.debian_12_test: Still creating... [30s elapsed]
proxmox_virtual_environment_vm.debian_12_test: Still creating... [40s elapsed]
proxmox_virtual_environment_vm.debian_12_test: Still creating... [50s elapsed]
proxmox_virtual_environment_vm.debian_12_test: Still creating... [1m0s elapsed]
proxmox_virtual_environment_vm.debian_12_test: Still creating... [1m10s elapsed]
proxmox_virtual_environment_vm.debian_12_test: Creation complete after 1m15s [id=100]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

debian_12_test_ip_address = tolist([
  tolist([
    "127.0.0.1",
  ]),
  tolist([
    "192.168.0.112",
  ]),
])
```

Notre machine est en place avec l'adresse `192.168.0.112`, c'est grâce à
**l'intégration de l'agent Qemu** qu'OpenTofu obtient cette information et nous
l'affiche. Maintenant vérifions que notre utilisateur existe et que la clé
publique que nous avons fournie dans `user-config.yaml` fonctionne:

```shell
ssh ephase@192.168.0.112 -i ~/.ssh/id_ed25519
# [...]
ephase@debian-12-test:~$ hostnamectl hostname
test-debian-12
ephase@debian-12-test:~$ sudo id
uid=0(root) gid=0(root) groups=0(root)

```

Notre connexion fonctionne, notre utilisateur existe bien, le nom d'hôte est
bien celui voulu et le paramétrage de `sudo` fonctionne aussi.

## En conclusion

En partant d'une image Debian minimale, nous avons réussi à créer une machine
virtuelle paramétrée en automatisant autant que possible les actions à effectuer
à l'aide de briques logicielles open-sources. Bien sûr nous avons juste effleuré
les possibilités offertes par tous ces éléments, mais j'espère vous avoir donné
l'envie d'aller plus loin.


Merci à [Heuzef][heuzef] et [RavenRamirez][alois] pour les relectures, 
corrections et conseils.

[heuzef]:https://heuzef.com/
[alois]:https://aloisbouny.fr/

[opentofu]: https://opentofu.org/
[cloudinit]: https://cloud-init.io/
[cloudinit_doc]: https://cloudinit.readthedocs.io/en/latest/reference/examples.html
[proxmox]: https://www.proxmox.com/
[bgp_pve_terraform]: https://github.com/bpg/terraform-provider-proxmox
[srobert]: https://blog.stephane-robert.info/docs/virtualiser/type1/proxmox/terraform/#configuration-de-proxmox
[debian-cloudimages]: https://cloud.debian.org/images/cloud/
[bgp-pve-vm-doc]: https://github.com/bpg/terraform-provider-proxmox/blob/main/docs/resources/virtual_environment_vm.md
