Nexus le gestionnaire d'artefacts
**S’il est bien un outil qui est indispensable dans une démarche Devops c’est bien le gestionnaire de dépôts. C’est dans cet outil qu’on va stocker les artefacts, les paquets et les métadonnées produites par les pipelines CI/CD.
J’ai fait le choix d’installer Nexus dans mon Home Lab Devops pour les raisons suivantes :
- Nexus offre dans sa version opensource la gestion de toute une série de repositories en natif dont : APT, Yum, Docker, Maven, Pypi, Nuget, npm, R, RubyGems, Go, Helm, Composer*, Cpan*, … et RAW.
- C’est un très bon exercice pour se former à Ansible, Terraform, Vagrant
Ma démarche pour l’automatisation de l’installation de Nexus
Mes ressources étant limitées, j’ai commencé par développer le playbook Ansible sur une machine virtuelle créé avec Vagrant. Une fois le playbook fonctionnel, je l’ai transposé dans une VM instanciée avec du Terraform sur une de mes machines de mon Homelab.
Les objectifs que je me suis fixés et les contraintes imposées :
- Pouvoir faire les upgrades facilement
- Ne pas utiliser de Bdd externes (pas assez de ressources)
- Ajouter de la persistance sur les données via un simple partage Nfs. Avant j’utilisais un cluster glusterfs sur 3 Raspberry Pi. Je rebasculerais dessus dans les prochaines semaines après l’avoir reconstruit.
- Pas d’installation via Docker.
Pour écrire le playbook Ansible, j’ai tout simplement transposé la procédure d’installation disponible sur le site de la documentation de Nexus ↗.
Ecriture du Vagrantfile
Maintenant je peux dire que je suis à l’aise avec Vagrant. Donc voici la configuration que j’ai utilisée :
# -*- mode: ruby -*-# vi: set ft=ruby :
Vagrant.configure("2") do |config| config.vm.box = "almalinux/8" config.vm.synced_folder '.', '/vagrant', disabled: true config.vm.provider "libvirt" do |hv| hv.cpus = "2" hv.memory = "3072" end config.vm.define "nexus" do |nexus| nexus.vm.network "forwarded_port", guest: 443, host: 8443 nexus.vm.network :private_network, ip: "192.168.3.10" nexus.vm.hostname = "nexus" nexus.vm.provision "ansible" do |a| a.verbose = "v" a.playbook = "deploy_nexus.yml" end endend
Comme vous pouvez le voir j’utilise un VM avec 2 CPU et 3 Go de RAM. C’est le minimum requis. Je ne partage que le port 443, car c’est nginx qui portera la partie SSL et se chargera de faire le lien vers les différents types de repository. J’ai utilisé une box AlmaLinux dans sa version 8.
Mise en place du serveur NFS
Pour stocker les données persistantes j’ai fait le choix d’utiliser des montages NFS. Sur mon PC de travail et sur un de mes serveurs du Home Lab j’ai installé le serveur NFS, accompagné du service statd (pour la gestion du remote locking).
sudo apt updatesudo apt install nfs-kernel-serversudo systemctl enable rpc-statd.service --nowsudo systemctl enable nfs-server.service --now
sudo mkdir /home/datasudo chown nobody:nogroup /home/dataecho "/home/data *(rw,insecure,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exportssudo exportfs -raexportfs
/home/data <world>
Comme je n’ai pas créé de FS dédié, je le crée sur le FS /home
qui a le plus
de place à disposition. Je n’ai pas non plus mis de restriction sur la plage
d’IP qui peut y accéder (vous pouvez le faire.)
Ecriture du playbook
Je me suis forcé de surtout pas reprendre de l’existant et de l’écrire à la volée avec les objectifs que je me suis fixé.
---- hosts: all gather_facts: true become: true vars: ## Nexus nexus_version: 3.38.0-01 force_install: false nexus_version_running: 1.0 nexus_package: "nexus-{{ nexus_version }}-unix.tar.gz" nexus_installation_dir: /opt/sonatype nexus_data_dir: /data/nexus nexus_os_group: nexus nexus_os_user: nexus nexus_tmp_dir: '/tmp/nexus' nexus_default_port: 8081 nexus_default_context_path: '/' nexus_timezone: Europe/Paris nexus_os_max_filedescriptors: 65536 nfs_path : 192.168.1.101:/data memory: 1536 # nfs_path: 192.168.122.1:/home/vagrant/Projets/nexus/data
## Nginx nginx_version: 1.18 nginx_fqdn: artefacts.robert.local cert_file: "{{ nginx_fqdn }}+4.pem" cert_key: "{{ nginx_fqdn }}+4-key.pem"
tasks: - name: "Check nexus-latest link stat in {{ nexus_installation_dir }}" ansible.builtin.stat: path: "{{ nexus_installation_dir }}/nexus" register: running_version - name: Register current running version if any ansible.builtin.set_fact: nexus_version_running: >- {{ running_version.stat.lnk_target | regex_replace('^.*nexus-(\d*\.\d*\.\d*-\d*)', '\1') }} when: - running_version.stat.exists | default(false) - running_version.stat.islnk | default(false) - name: create group nexus ansible.builtin.group: name: "{{ nexus_os_group }}" state: present - name: create user nexus ansible.builtin.user: name: "{{ nexus_os_user }}" groups: "{{ nexus_os_group }}" append: yes - name: create /data mount ansible.builtin.file: path: /data state: directory mode: 0755 - name: mount nfs /data ansible.posix.mount: src: "{{ nfs_path }}" path: /data # opts: vers=4,udp state: mounted fstype: nfs - name: Create Nexus directory ansible.builtin.file: path: "{{ item }}" state: "directory" owner: "{{ nexus_os_user }}" group: "{{ nexus_os_group }}" mode: 0755 with_items: - "{{ nexus_tmp_dir }}" - "{{ nexus_installation_dir }}" - "{{ nexus_data_dir }}" - "{{ nexus_data_dir }}/log" - "{{ nexus_data_dir }}/tmp" - name: get list of services ansible.builtin.service_facts: - name: Stop nexus service ansible.builtin.service: name: nexus enabled: true state: stopped when: (nexus_version != nexus_version_running or force_install) and "nexus.service" in ansible_facts.services - name: Delete lock file ansible.builtin.file: path: /data/nexus/lock state: absent when: nexus_version != nexus_version_running or force_install - name: install packages ansible.builtin.package: state: present name: - glibc-common - glibc-langpack-en - glibc-langpack-fr - java - rsync - tar - unzip - epel-release - python3-libsemanage - policycoreutils-python-utils - name: set as default locale ansible.builtin.command: localectl set-locale LANG=en_US.UTF-8 - name: Get path to default settings ansible.builtin.set_fact: nexus_default_settings_file: "{{ nexus_installation_dir }}/nexus/etc/nexus-default.properties" - name: install nexus become_user: nexus ansible.builtin.unarchive: src: "http://download.sonatype.com/nexus/3/{{ nexus_package }}" dest: "{{ nexus_installation_dir }}" remote_src: yes owner: nexus when: nexus_version != nexus_version_running or force_install - name: Update symlink nexus ansible.builtin.file: path: "{{ nexus_installation_dir }}/nexus" src: "{{ nexus_installation_dir }}/nexus-{{ nexus_version }}" owner: "{{ nexus_os_user }}" group: "{{ nexus_os_group }}" state: link register: nexus_latest_version when: nexus_version != nexus_version_running or force_install - name: Setup Nexus tmp directory ansible.builtin.lineinfile: dest: "{{ nexus_installation_dir }}/nexus/bin/nexus.vmoptions" regexp: "^-Djava.io.tmpdir=.*" line: "-Djava.io.tmpdir={{ nexus_tmp_dir }}" when: nexus_version != nexus_version_running or force_install - name: Setup Nexus data directory ansible.builtin.lineinfile: dest: "{{ nexus_installation_dir }}/nexus/bin/nexus.vmoptions" regexp: "^-Dkaraf.data=.*" line: "-Dkaraf.data={{ nexus_data_dir }}" when: nexus_version != nexus_version_running or force_install - name: Setup JVM logfile directory ansible.builtin.lineinfile: dest: "{{ nexus_installation_dir }}/nexus/bin/nexus.vmoptions" regexp: "^-XX:LogFile=.*" line: "-XX:LogFile={{ nexus_data_dir }}/log/jvm.log" when: nexus_version != nexus_version_running or force_install - name: Setup Nexus default timezone ansible.builtin.lineinfile: dest: "{{ nexus_installation_dir }}/nexus/bin/nexus.vmoptions" regexp: "^-Duser.timezone=.*" line: "-Duser.timezone={{ nexus_timezone }}" when: nexus_version != nexus_version_running or force_install - name: Set nexus user ansible.builtin.lineinfile: dest: "{{ nexus_installation_dir }}/nexus/bin/nexus.rc" regexp: ".*run_as_user=.*" line: "run_as_user=\"{{ nexus_os_user }}\"" when: nexus_version != nexus_version_running or force_install - name: Set nexus port ansible.builtin.lineinfile: dest: "{{ nexus_default_settings_file }}" regexp: "^application-port=.*" line: "application-port={{ nexus_default_port }}" when: nexus_version != nexus_version_running or force_install - name: Set nexus context path ansible.builtin.lineinfile: dest: "{{ nexus_default_settings_file }}" regexp: "^nexus-context-path=.*" line: "nexus-context-path={{ nexus_default_context_path }}" when: nexus_version != nexus_version_running or force_install - name: Configure Memory Usage ansible.builtin.lineinfile: dest: /opt/sonatype/nexus/bin/nexus.vmoptions regexp: "^-{{ item }}.*" line: "-{{ item }}{{ memory }}" with_items: - "Xms" - "Xmx" - "XX:MaxDirectMemorySize=" tags: memory - name: Create systemd service configuration ansible.builtin.template: src: "nexus.service" dest: "/etc/systemd/system" mode: 0755 when: nexus_version != nexus_version_running or force_install - name: Reload systemd service configuration ansible.builtin.service: name: nexus enabled: true state: restarted daemon_reload: yes when: nexus_version != nexus_version_running or force_install - name: Install tools for debug ansible.builtin.package: name: - htop - net-tools state: present# Deploy Nginx - name: install nginx ansible.builtin.dnf: name: '@nginx:{{ nginx_version }}' state: present - name: copy nginx config ansible.builtin.copy: src: files/nginx.conf dest: /etc/nginx mode: 0644 - name: template nginx configuration ansible.builtin.template: src: artefacts.conf dest: /etc/nginx/conf.d/artefacts.conf mode: 0640 notify: reload_nginx - name: copy certificate ansible.builtin.copy: src: "files/{{ item }}" dest: "/etc/ssl/{{ item }}" mode: 0640 with_items: - "{{ cert_file }}" - "{{ cert_key }}" notify: reload_nginx - name: set sebool httpd can network connect to on ansible.posix.seboolean: name: httpd_can_network_connect state: yes persistent: yes - name: enable & start nginx ansible.builtin.service: name: nginx enabled: yes state: started handlers: - name: reload_nginx ansible.builtin.service: name: nginx state: reloaded
L’installation est faite dans le dossier /opt/sonatype
en y déposant le
contenu du tar.gz téléchargé. Ensuite un lien est créé vers la version désirée.
Si la version n’existe pas, on arrête Nexus et on fait l’installation, dans le
cas contraire, on bypass pour ne faire que la partie configuration.
Si l’installation s’est mal passée vous pouvez la relancer en mettant
force_install à true
. Ne pas oublier de le remettre à false par la suite.
La gestion de la version se fait avec la variable nexus_version
. Les
versions disponibles ↗.
Ensuite on procède à l’installation de nginx dont les certificats ont été générés avec mkcert.
La configuration de Nginx se fait via l’utilisation d’un template Ansible :
server_tokens off;
server { listen 80; server_name {{ nginx_fqdn }}; return 301 https://$server_name$request_uri; } server { listen *:443 ssl http2; server_name {{ nginx_fqdn }};
# allow large uploads of files for docker client_max_body_size 2G;
ssl_certificate /etc/{{ cert_file }}; ssl_certificate_key /etc/{{ cert_key }}; ssl_verify_client off; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:MozSSL:10m; ssl_session_timeout 1d; ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=63072000; allways;";
location /v2/ { proxy_pass http://127.0.0.1:8082; proxy_set_header Host $host:$server_port; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto "https"; }
location / { # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto "https"; } }
Il y a redirections dont une qui permet de définir l’accès à une registry docker. Nous verrons comment la créer plus tard.
Test de la VM avec vagrant
Vous pouvez télécharger le code en clonant le repository que je vous ai mis à disposition :
git clone
N’oubliez pas de changer les variables du fqdn, des certificats et du montage nfs.
Pour lancer la création de la VM :
vagrant up --no-destroy-on-error --provision
Dans la phase de développement, je vous conseille d’ajouter l’option
--no-destroy-on-error
qui permet de garder la vm même si le playbook plante.
Pour le relancer l’option --provision
permet de spécifier qu’il ne faut que
refaire la partie provisioning.
Ensuite, il suffit de se rendre sur l’url https://192.168.3.10 ↗ dans votre navigateur. Si vous le faites depuis visual code, vous pouvez ajouter le port 192.168.3.10:443 et dans le navigateur https://localhost ↗.
Pour se connecter la première fois, il faut récupérer le mot de passe que Nexus
a généré. Le user est admin
:
cat /data/nexus/admin.password
Nexus vous demandera de le changer de suite ! Activer l’accès en mode
anonymous
.
Installation sur une machine du home Lab avec Terraform
Maintenant que cela fonctionne je l’installe sur une des machines Ubuntu du Home Lab. Pour cela je crée le partage NFS comme ci-dessus.
[Maj] La configuration automatique des machines du home lab est décrite ici
Installation de libvirt
Il faut installer et configurer libvirt pour qu’il utilise le pont réseau monté sur la carte ethernet. Cela permet d’exposer les VM sur le même réseau que la machine hôte et donc bénéficier du serveur dhcp (ma box).
sudo apt install libvirt-daemon-system libvirt-clients libvirt-dev qemu-kvm cockpit
J’installe cockpit pour faciliter la configuration. En effet, la création du pont réseau se fait en deux clics.
Pour y accéder https://ip-de-votre-machine:9090 ↗.
Création du pont réseau
Une fois identifié, il suffit de se rendre dans la partie réseau et de cliquer sur [Ajouter un Pont]. Donnez-lui un nom, br0 par exemple et cliquer sur le nom de la carte réseau à associer (eth0, enp1s0, …). [Appliquez]
On configure ensuite libvirt pour qu’il utilise ce pont. Sur la machine
hôte créer un fichier se nommant bridged-network.xml
et y mettre ce contenu :
<network> <name>bridged-network</name> <forward mode="bridge" /> <bridge name="br0" /></network>
(attention mettre le nom du pont que vous avez créé précédement !
Et on applique :
sudo virsh net-define bridged-network.xmlsudo virsh net-start bridged-networksudo virsh net-autostart bridged-network
Création du code Terraform
Je vous fournis le code que j’ai utilisé :
terraform { required_providers { libvirt = { source = "dmacvicar/libvirt" } }}
// instance the providerprovider "libvirt" { // uri = "qemu:///system" uri = "qemu+ssh://root@devbox/system"}
// variables that can be overridenvariable "hostname" { default = "artefacts" }variable "domain" { default = "robert.local" }variable "ip_type" { default = "dhcp" } # dhcp is other valid typevariable "memoryMB" { default = 1024*3 }variable "cpu" { default = 3 }
// fetch the latest ubuntu release image from their mirrorsresource "libvirt_volume" "os_image" { name = "${var.hostname}-os_image" pool = "default" source = "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2" format = "qcow2"}
// Use CloudInit ISO to add ssh-key to the instanceresource "libvirt_cloudinit_disk" "commoninit" { name = "${var.hostname}-commoninit.iso" pool = "default" user_data = data.template_cloudinit_config.config.rendered network_config = data.template_file.network_config.rendered}
data "template_file" "user_data" { template = file("${path.module}/cloud_init.cfg") vars = { hostname = var.hostname fqdn = "${var.hostname}.${var.domain}" public_key = file("~/.ssh/id_ed25519.pub") }}
data "template_cloudinit_config" "config" { gzip = false base64_encode = false part { filename = "init.cfg" content_type = "text/cloud-config" content = "${data.template_file.user_data.rendered}" }}
data "template_file" "network_config" { template = file("${path.module}/network_config_${var.ip_type}.cfg")}
// Create the machineresource "libvirt_domain" "domain-ubuntu" { # domain name in libvirt, not hostname name = "${var.hostname}" memory = var.memoryMB vcpu = var.cpu
disk { volume_id = libvirt_volume.os_image.id } network_interface { network_name = "bridged-network" mac = "52:54:00:36:14:e9" }
cloudinit = libvirt_cloudinit_disk.commoninit.id
# IMPORTANT # Ubuntu can hang is a isa-serial is not present at boot time. # If you find your CPU 100% and never is available this is why console { type = "pty" target_port = "0" target_type = "serial" }
graphics { type = "spice" listen_type = "address" autoport = "true" }}
terraform { required_version = ">= 0.12"}
output "ips" { #value = libvirt_domain.domain-ubuntu #value = libvirt_domain.domain-ubuntu.*.network_interface # show IP, run 'terraform refresh' if not populated value = libvirt_domain.domain-ubuntu.*.network_interface}
Il faut modifier :
- la variable
domain
(au début du script) - dans network_interface changer de mac addresse si besoin. Je la fixe pour que l’ip utilise un bail statique sur ma box Orange.
Ce code fait appel à du cloud-init pour configurer la VM.
#cloud-config# https://cloudinit.readthedocs.io/en/latest/topics/modules.htmltimezone: Europe/Paris
fqdn: artefacts.robert.localmanage_etc_hosts: trueresize_rootfs: true
users: - name: admuser sudo: ALL=(ALL) NOPASSWD:ALL groups: users, wheel home: /home/admuser shell: /bin/bash lock_passwd: false ssh-authorized-keys: - ${public_key}
# only cert auth via ssh (console access can still login)## debug - ssh_pwauth: truedisable_root: falsessh_pwauth: truechpasswd: list: | root:passwd admuser:123456 expire: falsegrowpart: mode: auto devices: ['/']packages: - qemu-guest-agentwrite_files: - path: /etc/sysctl.d/10-disable-ipv6.conf permissions: 0644 owner: root content: | net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1# every bootbootcmd: - [ sh, -c, 'echo $(date) | sudo tee -a /root/bootcmd.log' ]# run once for setup
runcmd: - sed -i 's/#UseDNS yes/UseDNS no/' /etc/ssh/sshd_config - systemctl restart sshd - sysctl --load /etc/sysctl.d/10-disable-ipv6.conf - localectl set-keymap fr - localectl set-locale LANG=fr_FR.UTF8 - domainname robert.local
Modifier le nom de domain !!
Et pour la partie réseau :
version: 2ethernets: eth0: dhcp4: true nameservers: addresses: [192.168.1.1] search: [robert.local]
Modifier le nom de domain et la gateway !!
Démarrage de la VM
Maintenant que tout est prêt allez on démarre le tout :
terraform initterraform apply -auto-approve
Allez dans cockpit et dans machine virtuelle cliquez sur artefacts.
Ouvrez la console série et entrez root
, passwd
pour vous connecter.
cloud-init statusstatus: done
C’est bon elle est configurée !
On peut lancer le playbook Ansible.
ansible-playbook -i inventory deploy_nexus.yml
Ça va mouliner un certain temps. Tout dépend des ressources de votre machine hôte. Moi, c’est un Atom x5-Z8350 et il faut pas moins de 10 minutes avant que Nexus soit opérationel. J’ai commandé un miniforums UM250 : RYZEN 5 PRO 2500U + 16Go de RAM + 256 Gb de SSD upgradable.Y a eu une belle promo sur Amazon.fr ↗ à 390€.
Dans la fenêtre console il suffit de taper la commande suivante pour vérifier que nexus est démarré :
ss -tlLISTEN 0 128 0.0.0.0:sunrpc 0.0.0.0:*LISTEN 0 128 0.0.0.0:ssh 0.0.0.0:*LISTEN 0 128 [::]:sunrpc [::]:*LISTEN 0 128 [::]:ssh [::]:*
Si le port 8081 apparaît, c’est que Nexus a démarré. Vous pouvez aussi vérifier
les logs dans /data/nexus/log/nexus.log
.
sudo tail -f /data/nexus/log/nexus.log....2022-02-21 15:41:32,390+0100 INFO [jetty-main-1] *SYSTEM org.sonatype.nexus.bootstrap.jetty.JettyServer --------------------------------------------------
Started Sonatype Nexus OSS 3.37.3-02
-------------------------------------------------
Enfin !
Ouvrez votre navigateur sur l’adresse que vous avez défini : https://artefacts.robert.local ↗
Configuration d’une registry Docker
Pour stocker vos images de container, vous pouvez activer le plugin docker de Nexus. Identifiez-vous avec le compte admin. Et cliquez en haut à gauche sur la roue dentée, puis [repository], puis [create repositories]. Choissisez docker (hosted) et renseignez les paramêtres comme dans l’image ci-dessous:
Dans la partie finissez par [Create repository].
Test de la registry docker
On va tester le stockage d’image. On va faire simple :
Sur votre poste de travail, taguer une image avec le nom de domaine de votre nexus :
docker pull alpine:3.15.0docker tag alpine:3.15 artefacts.robert.local/alpine:3.15.0docker login -u admin artefacts.robert.localPassword:WARNING! Your password will be stored unencrypted in /home/vagrant/.docker/config.json.Configure a credential helper to remove this warning. Seehttps://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
docker push artefacts.robert.local/alpine:3.15The push refers to repository [artefacts.robert.local/alpine]8d3ac3489996: Pushed
Un petit tour dans nexus. [Browse] [test] ->
On a une registry docker sécurisée ! C’est pas merveilleux ?
Si Nexus ne démarre pas
Connectez-vous avec le compte nexus :
sudo su - nexus
On va lancer la commande de démarrage manuellement :
sh -x /opt/sonatype/nexus/bin/nexus start
Si vous ne voyez pas d’erreur recopier la dernière commande affichée. Et lancer là :
java.lang.NumberFormatException
J’ai eu ce problème qui était dû à l’absence du service rpc.statd
sur la
machine hébergeant le partage NFS. Il suffit donc de l’installer et de le
démarrer (voir au début du billet).
Pas assez de ressources.
Par défaut la jvm Nexus est configurée avec les paramètres -Xms2703m
,
-Xmx2703m
et -XX:MaxDirectMemorySize=2703m
. Vous pouvez essayer de les
réduire à 1536 ça devrait démarrer. Ca se passe dans le fichier
/opt/sonatype/nexus/bin/nexus.vmoptions
Attention à chaque nouvelle installation il faudra le refaire, à moins que vous l’ajoutiez dans le playbook ansible (le temps que je le fasse).
Plus loin
Le code source de l’ensemble est disponible sur gitlab ↗
Il y a encore un peu de travail pour rendre le playbook tip-top au petits oignons. Mais je vous laisse jouer avec pour apprendre :
- gérer la configuration mémoire avec des
lineinfile
. Je vous déconseille le template car les fichiers de configuration nexus - …