Un environnement de développement Puppet
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-node0sudo supuppet 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.master0sudo supuppetserver 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 endend
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.