Aller au contenu

Installation de Rundeck

logo ansible

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.

homelab rundeck interface

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
end
end

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 provider
provider "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 overriden
variable "hostname" { default = "rundeck" }
variable "domain" { default = "robert.local" }
variable "ip_type" { default = "dhcp" } # dhcp is other valid type
variable "memoryMB" { default = 1024*2 }
variable "cpu" { default = 2 }
// fetch the latest ubuntu release image from their mirrors
resource "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 instance
resource "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 machine
resource "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.

Terminal window
terraform refresh vagrant@devbox 08:24:26
libvirt_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 :

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

homelab rundeck login

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.