Aller au contenu principal

Installation de Rundeck

· 9 minutes de lecture
Stéphane ROBERT
Consultant DevOps

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

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 :

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.

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.