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