Installation de Rundeck
Pour ceux qui ne connaissent pas Rundeck, Rundeck est un logiciel libre permettant l’automatisation de l’administration de serveurs appelés nodes. Cette automatisation se fait sur des projets via des taches qui sont composées de jobs.
Ces taches sont composées de workflows permettant de gérer les conditions d’exécution et les erreurs. Les jobs Rundeck peuvent être exécuté sur les nodes via une connection SSH ou WinRM. Les jobs peuvent être des scripts, des programmes écrits dans le langage de votre choix, mais aussi des playbooks Ansible. C’est donc une alternative à Ansible Tower ou AWX. La gestion d’Ansible est native, mais il faut tout de même l’installer, ainsi que toutes les collections.
Rundeck permet une gestion très fine des droits de chacun des
utilisateurs via un système RBAC
et peut être connecté à un AD/LDAP.
Démarche d’automatisation de l’installation de Rundeck
Toujours le même principe. Je pars d’une feuille vierge avec comme objectifs :
- gestion des upgrades/downgrades facilement
- Utiliser une simple base H2
- Ajouter de la persistance sur les données via un simple partage Nfs.
- 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
Rundeck ↗.
J’ai fait le choix d’utiliser les packages war
pour le déploiement pour sa
simplicité de mise en oeuvre.
Ecriture du Vagrantfile
Du classique :
# -*- 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 = "2048" end config.vm.define "rundeck" do |rundeck| rundeck.vm.network "forwarded_port", guest: 443, host: 443 rundeck.vm.network :private_network, ip: "192.168.3.10" rundeck.vm.hostname = "rundeck" rundeck.vm.provision "ansible" do |a| a.verbose = "v" a.playbook = "deploy_rundeck.yml" a.host_vars = { "rundeck" => { "server_address" => "localhost", "rundeck_xmx" => "1024m", "rundeck_xms" => "256m", "rundeck_maxmetaspacesize" => "256m" } } end endend
Ecriture du playbook
On est très proche de celui utilisé que j’ai utilisé pour
Nexus. Le versioning est géré
par un lien symbolique rundeck
pointant sur le déploiement d’une version
spécifique. Cela permet de gérer l’upgrade et le rollback facilement. Le
package est au format war, qui est en fait un fichier zip pris en
charge par java.
---- hosts: all gather_facts: false become: true vars: #datasource=github-releases depName=rundeck/rundeck rundeck_version: 3.4.10-20220118 #datasource=github-tags depName=ansible-community/ansible-build-data ansible_version: 5.5.0 rundeck_os_group: rundeck rundeck_os_user: rundeck timezone: Europe/Paris rundeck_installation_dir: /opt rundeck_data_dir: /data/rundeck nfs_path: devbox1:/data force_install: true rundeck_version_running: "" ansible_python_interpreter: /usr/libexec/platform-python server_address: "rundeck.robert.local" rundeck_xmx: "1024m" rundeck_xms: "256m" rundeck_maxmetaspacesize: "256m"
## Nginx nginx_version: 1.18 nginx_fqdn: rundeck.robert.local cert_file: "{{ nginx_fqdn }}+3.pem" cert_key: "{{ nginx_fqdn }}+3-key.pem" tasks: - name: Wait 600 seconds for target connection to become reachable/usable wait_for_connection: - name: Get ansible_facts ansible.builtin.setup: - name: "Check rundeck-latest link stat in {{ rundeck_installation_dir }}" ansible.builtin.stat: path: "{{ rundeck_installation_dir }}/rundeck" register: running_version tags: version - name: Register current running version if any ansible.builtin.set_fact: rundeck_version_running: >- {{ running_version.stat.lnk_target | regex_replace('^.*rundeck-(\d*\.\d*\.\d*-\d*)', '\1') }} when: - running_version.stat.exists | default(false) - running_version.stat.islnk | default(false) tags: version # - name: debug # debug: # var: "{{ item }}" # with_items: # - running_version # - rundeck_version_running # tags: version - name: create group rundeck ansible.builtin.group: name: "{{ rundeck_os_group }}" state: present - name: create user rundeck ansible.builtin.user: name: "{{ rundeck_os_user }}" groups: "{{ rundeck_os_group }}" append: yes - name: install packages ansible.builtin.package: state: present name: - glibc-common - glibc-langpack-en - glibc-langpack-fr - java - tar - unzip - epel-release - python3-libsemanage - python3-pip - policycoreutils-python-utils - python3-libselinux - name: Install tools for debug ansible.builtin.package: name: - htop - net-tools - python39 state: present - name: Correct python version selected community.general.alternatives: name: python3 path: /usr/bin/python3.9 - name: set as default locale ansible.builtin.command: localectl set-locale LANG=en_US.UTF-8 - name: Set timezone community.general.timezone: name: "{{ timezone }}" - name: mount nfs /data ansible.posix.mount: src: "{{ nfs_path }}" path: /data state: mounted fstype: nfs - name: mount /dev/shm ansible.posix.mount: fstype: tmpfs name: "/dev/shm" opts: "defaults,nodev,nosuid,noexec" src: tmpfs state: mounted - name: Create rundeck directory ansible.builtin.file: path: "{{ item }}" state: "directory" owner: "{{ rundeck_os_user }}" group: "{{ rundeck_os_group }}" mode: 0755 with_items: - "{{ rundeck_installation_dir }}" - "{{ rundeck_data_dir }}" - name: get list of services ansible.builtin.service_facts: - name: Stop rundeck service ansible.builtin.service: name: rundeck enabled: true state: stopped when: (rundeck_version_running | length == 0 or rundeck_version != rundeck_version_running or force_install) - name: Create directory ansible.builtin.file: path: "{{ rundeck_installation_dir }}/rundeck-{{ rundeck_version }}" state: directory owner: "{{ rundeck_os_user }}" group: "{{ rundeck_os_group }}" mode: 0755 when: (rundeck_version_running | length == 0 or rundeck_version != rundeck_version_running or force_install) - name: install rundeck become_user: rundeck ansible.builtin.get_url: url: "https://packagecloud.io/pagerduty/rundeck/packages/java/org.rundeck/rundeck-{{ rundeck_version }}.war/artifacts/rundeck-{{ rundeck_version }}.war/download" dest: "{{ rundeck_installation_dir }}/rundeck-{{ rundeck_version }}/rundeck.war" owner: "{{ rundeck_os_user }}" when: (rundeck_version_running | length == 0 or rundeck_version != rundeck_version_running or force_install) - name: uncompress war become_user: "{{ rundeck_os_user }}" ansible.builtin.command: cmd: "java -jar rundeck.war --installonly" chdir: "{{ rundeck_installation_dir }}/rundeck-{{ rundeck_version }}" environment: RDECK_BASE: "{{ rundeck_installation_dir }}/rundeck-{{ rundeck_version }}" when: (rundeck_version_running | length == 0 or rundeck_version != rundeck_version_running or force_install) - name: Update symlink rundeck ansible.builtin.file: path: "{{ rundeck_installation_dir }}/rundeck" src: "{{ rundeck_installation_dir }}/rundeck-{{ rundeck_version }}" owner: "{{ rundeck_os_user }}" group: "{{ rundeck_os_group }}" state: link when: (rundeck_version_running | length == 0 or rundeck_version != rundeck_version_running or force_install) - name: Update configuration ansible.builtin.template: src: templates/rundeck-config.properties dest: "{{ rundeck_installation_dir }}/rundeck/server/config" owner: "{{ rundeck_os_user }}" group: "{{ rundeck_os_group }}" mode: 0644 when: (rundeck_version_running | length == 0 or rundeck_version != rundeck_version_running or force_install) - name: Upgrade pip ansible.builtin.pip: name: - pip - name: Install ansible ansible.builtin.pip: name: - ansible version: "{{ ansible_version }}" - name: get list of services ansible.builtin.service_facts: - name: Create systemd service configuration ansible.builtin.template: src: "rundeck.service" dest: "/etc/systemd/system" mode: 0755 - name: Reload systemd service configuration ansible.builtin.service: name: rundeck enabled: true state: restarted daemon_reload: yes when: (rundeck_version_running | length == 0 or rundeck_version != rundeck_version_running or force_install) - name: Add /usr/local/bin to path ansible.builtin.copy: dest: /etc/profile.d/rundeck.sh content: "PATH=$PATH:/usr/local/bin" owner: root group: root mode: 0644# Deploy Nginx - name: install nginx dnf: name: '@nginx:{{ nginx_version }}' state: present - name: template nginx configuration ansible.builtin.template: src: rundeck.conf dest: /etc/nginx/conf.d/rundeck.conf mode: 0640 notify: reload_nginx - name: copy certificate ansible.builtin.copy: src: "certificats/{{ 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
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.
Installation sur une machine du home Lab avec Terraform
La aussi nous sommes très proches de ce que j’ai utilisé pour nexus. Mis à part le nom de la vm, la mac address le nombre de cpu et de mémoire. Changez-les en fonction de votre besoin.
terraform { required_providers { libvirt = { source = "dmacvicar/libvirt" } powerdns = { source = "pan-net/powerdns" } }}
// instance the providerprovider "libvirt" { // uri = "qemu:///system" uri = "qemu+ssh://admuser@devbox3/system"}
provider "powerdns" { api_key = "${var.pdns_api_key}" server_url = "https://pdns.robert.local"}
// variables that can be overridenvariable "hostname" { default = "rundeck" }variable "domain" { default = "robert.local" }variable "ip_type" { default = "dhcp" } # dhcp is other valid typevariable "memoryMB" { default = 1024*2 }variable "cpu" { default = 2 }
// fetch the latest ubuntu release image from their mirrorsresource "libvirt_volume" "os_image" { name = "${var.hostname}-os_image" pool = "devbox" 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 = "devbox" 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-alma" { # domain name in libvirt, not hostname name = "${var.hostname}" memory = var.memoryMB vcpu = var.cpu autostart = true qemu_agent = true timeouts { create = "20m" }
disk { volume_id = libvirt_volume.os_image.id } network_interface { network_name = "bridged-network" mac = "52:54:00:36:14:e8" wait_for_lease = true }
cloudinit = libvirt_cloudinit_disk.commoninit.id
console { type = "pty" target_port = "0" target_type = "serial" }
graphics { type = "spice" listen_type = "address" autoport = "true" } provisioner "local-exec" { command = "ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -u admuser -i ${self.network_interface.0.addresses.0}, --private-key ${var.private_key_path} deploy_rundeck.yml" }}
terraform { required_version = ">= 0.12"}
output "ips" { value = libvirt_domain.domain-alma.network_interface.0.addresses.0}
resource "powerdns_record" "test" { zone = "robert.local." name = "rundeck.robert.local." type = "A" ttl = 300 records = [libvirt_domain.domain-alma.network_interface.0.addresses.0]}
Nouveauté : j’ai trouvé la solution pour récupérer l’adresse IP. Il faut
ajouter qemu_agent = true
dans la déclaration de la machine.
Pour obtenir l’adresse IP, il faut pour le moment relancer la commande
terraform refresh
.
terraform refresh vagrant@devbox 08:24:26libvirt_cloudinit_disk.commoninit: Refreshing state... [id=/data/images/rundeck-commoninit.iso;825c8076-d07d-497b-846b-1db197896c34]libvirt_volume.os_image: Refreshing state... [id=/data/images/rundeck-os_image]libvirt_domain.domain-alma: Refreshing state... [id=1adad1e7-0075-4fc3-b157-d81919ac562d]
Outputs:
ips = "192.168.1.143"
Démarrage de la VM
Le playbook se lance directement depuis terraform :
terraform initterraform apply -auto-approve
Il faudra attendre quelques minutes (une dizaine). Mais tout devrait être configuré ! Pour éviter d’attendre j’ai écris ce billet sur le temps de perdu en build
Ouvrez votre navigateur sur l’adresse que vous avez défini : https://rundeck.robert.local ↗. Les identifiants de connexion sont admin/admin.
Plus loin
Le code source de l’ensemble est disponible sur gitlab ↗
Reste à faire :
- Installation des collections Ansible
- Créer les inventaires et les playbooks Ansible dans le répertoire /data/ansible (à créer)
Je documenterai dans les prochains temps comment utiliser rundeck avec Ansible et voir aussi comment l’intégrer dans un CI gitlab. Ca semble être faisable, il y a la gestion des webhooks et une CLI est disponible.
Pour les impatients je vous laisse parcourir la documentation ↗ qui est plutôt complète.