Molecule & Terraform#

Nous allons voir ici comment utiliser le driver delegated de Molecule en faisant une implémentation basée sur Terraform.

Pour diminuer la phase de prérequis, l’exemple s’appuie également sur Docker, plutôt que sur un Cloud Provider.

Prérequis#

Initialisation#

  • Créez un rôle pour notre exercice

$ pwd
/home/user/ansible-workspaces/ultimate-training

$ cd roles

$ molecule init role molecule_terraform_demo --driver-name delegated

$ cd molecule_terraform_demo

$ tree -a molecule/
molecule/
└── default
    ├── converge.yml
    ├── create.yml
    ├── destroy.yml
    ├── INSTALL.rst
    ├── molecule.yml
    └── verify.yml

1 directory, 6 files

On peut voir que la commande d’init a créé un répertoire molecule/default/ et plusieurs fichiers :

molecule.yml

fichier de configuration des environnements de test et des options molecule pour ce scénario.

converge.yml

playbook qui sera appliqué aux environnements de test

verify.yml

playbook qui sera joué pour vérifier que converge.yml a bien effectué les modifications attendues.

create.yml

playbook qui sera lancé pour la création des environnements de test

destroy.yml

playbook qui sera lancé pour la destruction des environnements de test

Terraforming#

Nous allons maintenant ajouter le Terraform nécessaire pour simuler un serveur acessible en SSH avec un conteneur Docker local. Il s’agit de techniques dédiées aux tests, à proscrire dans des contexte de déploiements quels qu’ils soient.

Créez un répertoire pour hébergé le code Terraform :

$ pwd 
/home/user/ansible-workspaces/ultimate/training/roles/molecule_terraform_demo

$ mkdir -p molecule/default/terraform

Créez et remplissez les fichiers suivants (les chemins attendus sont en en-tête de chaque bloc) :

  • Un Dockerfile pour notre instance de serveur.

#
# roles/molecule_terraform_demo/molecule/default/terraform/Dockerfile
# 
ARG DEBIAN_TAG=11-slim
FROM debian:$DEBIAN_TAG
ARG DEBIAN_FRONTEND=noninteractive
ARG ROOT_PUBLIC_KEY=to-be-defined
RUN set -eux; \
  apt-get update && apt-get upgrade && apt-get dist-upgrade; \
  apt-get install --no-install-recommends -y apt-utils \
  curl ca-certificates sudo \
  python python3 python3-apt locales \
  systemd systemd-sysv libpam-systemd dbus dbus-user-session openssh-server; \
  localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8; \
  localedef -i fr_FR -c -f UTF-8 -A /usr/share/locale/locale.alias fr_FR.UTF-8
ENV LANG fr_FR.utf8
RUN rm -f /lib/systemd/system/multi-user.target.wants/* \
  /etc/systemd/system/*.wants/* \
  /lib/systemd/system/local-fs.target.wants/* \
  /lib/systemd/system/sockets.target.wants/*udev* \
  /lib/systemd/system/sockets.target.wants/*initctl* \
  /lib/systemd/system/sysinit.target.wants/systemd-tmpfiles-setup* \
  /lib/systemd/system/systemd-update-utmp*; \
  mkdir -p /var/run/sshd /root/.ssh
RUN systemctl enable ssh
RUN echo $ROOT_PUBLIC_KEY > /root/.ssh/authorized_keys
EXPOSE 22
ENTRYPOINT ["/lib/systemd/systemd"]
  • Le strict minimum de code Terraform pour construire le conteneur, le lancer et récupérer les informations de connexion.

#
# roles/molecule_terraform_demo/molecule/default/terraform/main.tf
# 
terraform {
  required_providers {
    docker = { source = "kreuzwerker/docker", version = "2.16.0" }
    tls    = { source = "hashicorp/tls", version = "3.3.0" }
  }
}

locals {
  base_name          = "molecule"
  container_name     = "${terraform.workspace}"
  root_key_algorithm = "ED25519"
  identity_file      = "${abspath(path.module)}/${terraform.workspace}.key"
}

resource "tls_private_key" "root" {
  algorithm = local.root_key_algorithm
}

resource "docker_image" "fake_server" {
  name = local.base_name
  build {
    path      = "."
    tag       = ["${local.base_name}:${local.container_name}"]
    build_arg = { ROOT_PUBLIC_KEY : tls_private_key.root.public_key_openssh }
  }
}

resource "null_resource" "private_key" {
  provisioner "local-exec" {
    command = "cat > ${local.identity_file} <<EOF\n${tls_private_key.root.private_key_openssh}\nEOF"
  }
  provisioner "local-exec" {
    command = "chmod 600 ${local.identity_file}"
  }
}

resource "docker_container" "fake_server" {
  name       = local.container_name
  image      = docker_image.fake_server.latest
  privileged = true
}

output "address" { value = docker_container.fake_server.ip_address }
output "user" { value = "root" }
output "identity_file" { value = local.identity_file }
output "instance" { value = terraform.workspace }
output "port" { value = 22 }

Intégration Molecule#

Maintenant que nous avons de quoi démarrer un serveur local accessible en SSH, il faut l’intégrer dans le cycle de gestion de Molecule.

  • Remplacez le contenu du fichier roles/molecule_terraform_demo/molecule/default/create.yml par :

---
- name: Create
  hosts: localhost
  connection: local
  gather_facts: false
  no_log: "{{ molecule_no_log }}"
  tasks:

    - name: Create the test environment
      terraform:
        project_path: "{{ playbook_dir }}/terraform"
        force_init: true
        workspace: "{{ item.name }}"
        state: present
      register: server
      loop: "{{ molecule_yml.platforms }}"

    - when: server.changed | default(false) | bool
      block:
        - name: Populate instance config dict
          set_fact:
            instance_conf_dict:
              instance: "{{ item.outputs.instance.value }}"
              address: "{{ item.outputs.address.value }}"
              user: "{{ item.outputs.user.value }}"
              port: "{{ item.outputs.port.value }}"
              identity_file: "{{ item.outputs.identity_file.value }}"
          with_items: "{{ server.results }}"
          register: instance_config_dict

        - debug:
            var: instance_conf_dict
        - name: Convert instance config dict to a list
          set_fact:
            instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"

        - name: Dump instance config
          copy:
            content: |
              # Molecule managed

              {{ instance_conf | to_json | from_json | to_yaml }}
            dest: "{{ molecule_instance_config }}"
            mode: 0600
  • Remplacez le contenu du fichier roles/molecule_terraform_demo/molecule/default/destroy.yml par :

---
- name: Destroy
  hosts: localhost
  connection: local
  gather_facts: false
  no_log: "{{ molecule_no_log }}"
  tasks:
    - name: Destroy the test environment
      terraform:
        project_path: "{{ playbook_dir }}/terraform"
        force_init: true
        workspace: "{{ item.name }}"
        state: absent
      register: server
      loop: "{{ molecule_yml.platforms }}"
 
    - name: Populate instance config
      set_fact:
        instance_conf: {}

    - name: Dump instance config
      copy:
        content: |
          # Molecule managed

          {{ instance_conf | to_json | from_json | to_yaml }}
        dest: "{{ molecule_instance_config }}"
        mode: 0600
      when: server.changed | default(false) | bool

Code du rôle#

  • Afin d’avoir quelque chose à tester, remplissez le fichier de tasks du rôles :

---
#
# roles/molecule_terraform_demo/tasks/main.yml
#
- name: Installation de nginx
  apt:
    name: nginx
    update_cache: yes

- name: Activation de nginx
  service:
    name: nginx
    state: started
    enabled: true

Code des tests#

  • Enfin, remplissez le fichier de vérification Molecule :

---
#
# roles/molecule_terraform_demo/molecule/default/verify.yml
#
- name: Verify
  hosts: all
  gather_facts: false
  tasks:
  - name: Installation de nginx
    apt:
      name: nginx
    register: nginx_install

  - name: Activation de nginx
    service:
      name: nginx
      state: started
      enabled: true
    register: nginx_enable

  - assert:
      that:
        - nginx_install is not changed
        - nginx_enable is not changed

Test complet#

Tout est en place, vous pouvez maintenant lancer un test bout en bout avec les commandes suivantes :

$ pwd
/home/user/ansible-workspaces/ultimate/training/roles/molecule_terraform_demo

$ molecule test
INFO     default scenario test matrix: dependency, lint, cleanup, destroy, syntax, create, prepare, converge, idempotence, side_effect, verify, cleanup, destroy
INFO     Performing prerun...
INFO     Set ANSIBLE_LIBRARY=/home/user/.cache/ansible-compat/9c82a6/modules:/home/user/.ansible/plugins/modules:/usr/share/ansible/plugins/modules
INFO     Set ANSIBLE_COLLECTIONS_PATHS=/home/user/.cache/ansible-compat/9c82a6/collections:/home/user/ansible-workspaces/ultimate/training/.direnv:/home/user/ansible-workspaces/ultimate/training/.direnv
INFO     Set ANSIBLE_ROLES_PATH=/home/user/.cache/ansible-compat/9c82a6/roles:/home/user/ansible-workspaces/ultimate/training/roles/molecule_terraform_demo/roles:roles
INFO     Running default > dependency
WARNING  Skipping, missing the requirements file.
WARNING  Skipping, missing the requirements file.
INFO     Running default > lint
INFO     Lint is disabled.
INFO     Running default > cleanup
WARNING  Skipping, cleanup playbook not configured.
INFO     Running default > Destroy
[...]
===============================================================================
Destroy the test environment -------------------------------------------- 2.74s
Dump instance config ---------------------------------------------------- 0.34s
Populate instance config ------------------------------------------------ 0.02s
INFO     Pruning extra files from scenario ephemeral directory

Ligne d’arrivée#

Vous avez maintenant un workflow complet de Molecule qui intègre Terraform comme implémentation du driver delegated. Libre à vous d’adapter le code Terraform pour pouvoir lancer vos tests Molecule directement sur AWS, GCP ou tout autre founisseur d’infrastructure.