Aller au contenu

Un environnement de développement Puppet

logo 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 :

Terminal window
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 :

Terminal window
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 :

Terminal window
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 :

Terminal window
vagrant push

Voilà tout est prêt !

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

Terminal window
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 :

Terminal window
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 :

Terminal window
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.

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.