Aller au contenu principal

Un environnement de développement Puppet

· 8 minutes de lecture
Stéphane ROBERT
Consultant DevOps

La semaine passée je vous ai proposé de découvrir les bases de l'écriture de manifests puppet. Je vous propose aujourd'hui de configurer un environnement de développement complet sur votre machine. Cet environnement est composé d'un serveur et de n nodes. Votre code puppet est monté directement sur le serveur via un partage NFS. On peut ainsi utiliser son éditeur de code favori et testé le déploiement sur un ou plusieurs nodes de tests.

Utilisation de l'environnement de développement puppet

Installation de l'environnement

Il faut au préalable avoir installé Vagrant mais aussi le plugin hostmanager. Rappel, ce plugin complète les fichiers /etc/hosts sur votre machine et sur les VM instanciées, avec les adresses IP de toutes les VM créés par Vagrant. Très pratique par la suite : on peut se connecter directement ensuite sur les VM en SSH sans passer par la commande vagrant login <vm>.

Pour l'installer :

vagrant plugin install vagrant-hostmanager

Autre bénéfice, on peut lancer un playbook ansible directement, à installer également. D'ailleurs, c'est comme cela qu'est configuré l'environnement puppet.

Tout est prêt on clone le projet :

git clone https://github.com/stephrobert/vagrant-puppet-master-agent.git

Pour instancier l'environnement, il suffit de lancer, depuis le répertoire où se trouve le fichier Vagrantfile, la commande :

vagrant up

Cela va provisionner le master et les n node(s) et configurer la partie SSH pour qu'il accepte les connexions depuis votre machine.

Ensuite pour configurer l'environnement, on lance en local le playbook init-cluster.yml. Pour simplifier les choses Vagrant met à disposition un provisionner local (local-exec) qui peut être appeler par la commande :

vagrant push

Voilà tout est prêt !

Quand vous terminé vous pouvez détruire l'environnement avec la classique commande vagrant :

vagrant destroy -f

Application des manifests sur les nodes

Comme dis plus haut, il suffit de se connecter au noeud en SSH et de lancer la commande puppet agent -t :

ssh vagrant@puppet-node0
sudo su
puppet agent -t

Connexion au master puppet

Pour contrôler par exemple que le(s) node(s) sont bien enregistrés sur le master. Comme pour les nodes, on se connecte en SSH, on passe root pour lancer les commandes :

ssh vagrant@puppet.master0
sudo su
puppetserver ca list --all

Ecrire et appliquer des manifests

L'utilisation est assez simple : dans le dépôt se trouve un répertoire puppet qui est monté via un partage NFS sur le master. Dedans, on retrouve, tous les fichiers du précédent billet, complété par le fichier site.pp dans lequel on indique les classes à appliquer sur les noeuds.

Quelques explications sur le Vagrantfile et le playbook

Vagrant est un outil que j'utilise très souvent pour instancier des environnements de tests. Cette fois donc je vous propose un Vagrantfile qui provisionne un serveur et une ou plusieurs machines clientes. Et comme ansible n'a pas besoin d'agent déployé pour fonctionner, c'est lui qui est à la manœuvre pour installer le serveur Puppet et configurer les clients. Résultat, vous avez un environnement de test, prêt à l'emploi : même les nœuds clients sont déjà enregistrés. J'ai utilisé la dernière version de Puppet disponible au moment de l'écriture de ce billet. Je vais configurer le dépôt pour qu'il se mette à jour seul lors des sorties de nouvelles versions de Puppet.

Vous pouvez cloner le dépôt source pour l'adapter à votre besoin. Par exemple pour changer l'image de base des clients. Pour le moment cela ne fonctionne qu'avec des clients avec des images de base debian et ses dérivées. D'ailleurs, je vais l'inclure dans ma liste de todo pour que chaque client puisse prendre en charge d'autres images de base, comment celle à base de redhat.

Le vagrantfile

J'ai déjà pas mal documenté Vagrant et je vous propose ici un Vagrantfile complet qui se charge de construire l'environnement de développement en une seule opération. Merci Vagrant qui fournit un inventaire ansible dynamique 👍 qui simplifie pas mal l'écriture.

Le code :

# -*- mode: ruby -*-
# vi: set ft=ruby :
ENV['VAGRANT_NO_PARALLEL'] = 'yes'
Vagrant.configure("2") do |config|
  base_ip_str = "10.240.0.1"
  number_master = 1 # Number of master
  cpu_master = 2
  mem_master = 3072
  number_node = 1 # Number of nodes
  cpu_node = 1
  mem_node = 1024

  # Compute nodes
  number_machines = number_master + number_node - 1

  nodes = []
  (0..number_machines).each do |i|
    case i
      when 0..number_master - 1
        nodes[i] = {
          "name" => "master#{i}",
          "ip" => "#{base_ip_str}#{i}",
          "image" => "generic/ubuntu2204"
        }
      when number_master..number_machines
        nodes[i] = {
          "name" => "node#{i-number_master}",
          "ip" => "#{base_ip_str}#{i}",
          "image" => "generic/ubuntu2204"
        }
    end
  end

# Provision VM
  nodes.each do |node|
    config.hostmanager.enabled = true
    config.hostmanager.manage_host = true
    config.vm.allow_fstab_modification = true
    config.vm.define node["name"] do |machine|
      machine.vm.hostname = "puppet.%s" % node["name"]
      machine.vm.provider "libvirt" do |lv|
        lv.driver = "kvm"
        if (node["name"] =~ /master/)
          lv.cpus = cpu_master
          lv.memory = mem_master
        else
          lv.cpus = cpu_node
          lv.memory = mem_node
        end
      end
      machine.vm.network "private_network", ip: node["ip"]
      config.vm.box = node["image"]
      if (node["name"] =~ /master/)
          config.vm.synced_folder "puppet", "/etc/puppetlabs/code/environments/developpement/", type: "nfs", nfs_udp: false, mount_options: ['actimeo=2']
      else
        machine.vm.synced_folder '.', '/vagrant', disabled: true
      end
      machine.vm.provision "ansible" do |ansible|
        ansible.playbook = "playbooks/provision.yml"
        ansible.groups = {
          "masters" => ["master[0:#{number_master-1}]"],
          "nodes" => ["node[0:#{number_node-1}]"],
          "puppet:children" => ["masters", "nodes"],
          "all:vars" => {
            "base_ip_str" => "#{base_ip_str}",
            "number_master" => "#{number_master-1}"
          }
        }
      end
    end
  end
  config.push.define "local-exec" do |push|
    push.inline = <<-SCRIPT
      ansible-playbook -i .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory playbooks/init-puppet.yml -u vagrant
    SCRIPT
  end
end

Dans la première partie du Vagrantfile, je constitue un tableau de nodes composé d'un master et de n clients. Le nombre de node(s) est configuré par la variable number_node = 1. Dans les paramètres des nodes on retrouve la capacité et l'image de base. C'est ici, l'image des clients, si vous voulez utiliser d'autres images c'est ici qu'il faudra intervenir.

Ensuite vient la partie provisioning dans lequel je constitue les groupes d'inventaire ansible: master et nodes. Ces groupes sont ensuite utilisés dans le playbook init-cluster.yml pour lancer les bonnes actions sur le serveur et le client.

attention

Attention : Ne modifiez pas le répertoire où est monté le code puppet à production. En effet, cela aura pour effet de bloquer l'installation du serveur. Vous pouvez le changer à une autre valeur, mais pas production.

Le playbook init-cluster

Pour chacun de mes projets, je décide d'écrire plutôt un playbook que d'utiliser des rôles (je les écris ensuite). Voici le code :

---
- name: Install puppet server
  hosts: masters
  vars:
    puppet_package_url: 'http://apt.puppet.com/puppet7-release-focal.deb'
    puppet_package_name: puppet7-release-focal
  tasks:
    - name: Install dependencies
      ansible.builtin.apt:
        name:
          - apt-transport-https
          - ca-certificates
          - gnupg2
          - xz-utils
        state: present
        update_cache: true
      become: true
    - name: Check if puppet package is installed
      ansible.builtin.command:
        cmd: dpkg-query -W puppet
      register: puppet_package_check_deb
      failed_when: puppet_package_check_deb.rc > 1
      changed_when: puppet_package_check_deb.rc == 1
    - name: Download puppet repo package
      ansible.builtin.get_url:
        url: '{{ puppet_package_url }}'
        dest: '/tmp/{{ puppet_package_name }}.deb'
        owner: '{{ ansible_env.USER }}'
        group: '{{ ansible_env.USER }}'
        mode: 0755
      when: puppet_package_check_deb.rc == 1
    - name: Install puppet repo package
      ansible.builtin.apt:
        deb: '/tmp/{{ puppet_package_name }}.deb'
      become: true
      when: puppet_package_check_deb.rc == 1
    - name: Install puppetserver
      ansible.builtin.apt:
        name: puppetserver
        state: present
        update-cache: true
      become: true
    - name: Configure server
      ansible.builtin.lineinfile:
        path: /etc/puppetlabs/puppet/puppet.conf
        regex: '{{ item.regex }}'
        line: '{{ item.line }}'
      with_items:
        - {regex: '^server.*', line: 'server = puppet.master0'}
        - {regex: '^ca_server.*', line: 'ca_server = puppet.master0'}
        - {regex: 'dns_alt_names.*', line: 'dns_alt_names = puppet'}
      become: true
    - name: Enable and start service puppetserver
      ansible.builtin.service:
        name: puppetserver
        state: restarted
        enabled: true
      become: true
    - name: Add puppet to path
      ansible.builtin.lineinfile:
        path: /etc/environment
        regexp: 'PATH=(["])((?!.*?/opt/puppetlabs/server/bin).*?)(["])$'
        line: 'PATH=\1\2:/opt/puppetlabs/server/bin/\3'
        backrefs: true
        state: present
      become: true
- name: Install puppet agent on Nodes
  hosts: nodes
  vars:
    puppet_package_url: 'http://apt.puppet.com/puppet7-release-focal.deb'
    puppet_package_name: puppet7-release-focal
  tasks:
    - name: Change hostname
      ansible.builtin.hostname:
        name: 'puppet.{{ inventory_hostname }}'
        use: systemd
      become: true
    - name: Check if puppet package is installed
      ansible.builtin.command:
        cmd: dpkg-query -W puppet
      register: puppet_package_check_deb
      failed_when: puppet_package_check_deb.rc > 1
      changed_when: puppet_package_check_deb.rc == 1
    - name: Download puppet repo package
      ansible.builtin.get_url:
        url: '{{ puppet_package_url }}'
        dest: '/tmp/{{ puppet_package_name }}.deb'
        owner: '{{ ansible_env.USER }}'
        group: '{{ ansible_env.USER }}'
        mode: 0755
      when: puppet_package_check_deb.rc == 1
    - name: Install puppet repo package
      ansible.builtin.apt:
        deb: '/tmp/{{ puppet_package_name }}.deb'
      become: true
      when: puppet_package_check_deb.rc == 1
    - name: Install puppet-agent
      ansible.builtin.apt:
        name: puppet-agent
        state: present
        update-cache: true
      become: true
    - name: Add master to puppet conf
      ansible.builtin.lineinfile:
        regex: '{{ item.regex }}'
        line: '{{ item.line }}'
        path: /etc/puppetlabs/puppet/puppet.conf
      become: true
      with_items:
        - {regex: '^server =.*', line: 'server = puppet.master0' }
        - {regex: '^environment =.*', line: 'environment = developpement' }
    - name: Start puppet agent service
      ansible.builtin.service:
        name: puppet
        enabled: true
        state: restarted
      become: true
    - name: Ask to generate certificate for node on master
      ansible.builtin.command:
        cmd: /opt/puppetlabs/bin/puppet ssl bootstrap
      register: test_node
      become: true
      async: 120
      poll: 0
    - name: Sign certificate node on master
      ansible.builtin.command:
        cmd: /opt/puppetlabs/bin/puppetserver ca sign --certname puppet.{{ inventory_hostname }}
      delegate_to: puppet.master0
      register: register_node
      changed_when: register_node.rc in [0, 1, 130]
      failed_when: register_node.rc not in [0, 1, 130]
      become: true
      until: "register_node is not failed"
      delay: 120
      tags: test
    - name: Add puppet to path
      ansible.builtin.lineinfile:
        path: /etc/environment
        regexp: 'PATH=(["])((?!.*?/opt/puppetlabs/bin).*?)(["])$'
        line: 'PATH=\1\2:/opt/puppetlabs/bin/\3'
        state: present
        backrefs: true
      become: true

Il est décomposé en deux parties :

  • La première qui configure le serveur maître puppet hosts : master.
  • La seconde sur les nodes : hosts: nodes.

Dans le second stage, celui sur les nodes j'utilise une tâche asynchrone, async et un delegate_to pour l'enregistrement du certificat du node sur le master. Pourquoi ? Tout simplement la commande de l'enregistrement du node attends qu'en parallèle le master accepte cet enregistrement.