Aller au contenu
Culture DevOps medium

Installer GARM avec LXD sur Ubuntu 24.04

17 min de lecture

Ce guide installe GARM v0.2.1 sur une VM Ubuntu 24.04 qui fait déjà tourner Gitea. À la fin, GARM sera actif en tant que service systemd, le provider LXD sera enregistré, et l'API répondra sur le port 9997.

GARM (GitHub/Gitea Actions Runner Manager) s'installe de la même façon que Gitea : des binaires compilés statiquement, téléchargés depuis les releases GitHub du projet, et un service systemd. Aucun conteneur n'est nécessaire pour faire tourner GARM lui-même.

  • VM Ubuntu 24.04 avec 4 Go de RAM, 30 Go de disque
  • Gitea installé et accessible (voir Installer Gitea)
  • Accès SSH avec sudo sur la VM
  • curl, tar et sha256sum (présents par défaut sur Ubuntu 24.04)

GARM publie trois archives distinctes, chacune accompagnée d'un fichier .sha256 généré par le projet. Le serveur, la CLI et le provider LXD sont versionnés séparément : garm et garm-cli suivent le dépôt cloudbase/garm, alors que garm-provider-lxd vit dans son propre dépôt et porte sa propre numérotation. Comparer les sommes de contrôle avant d'installer garantit que l'archive reçue est bien celle publiée par l'éditeur.

Sur la VM :

Fenêtre de terminal
GARM_VERSION="v0.2.1"
LXD_PROVIDER_VERSION="v0.1.5"
ARCH="linux-amd64" # linux-arm64 sur une VM ARM
cd /tmp
# 1. Serveur GARM + son empreinte
curl -sSLO "https://github.com/cloudbase/garm/releases/download/${GARM_VERSION}/garm-${ARCH}.tgz"
curl -sSLO "https://github.com/cloudbase/garm/releases/download/${GARM_VERSION}/garm-${ARCH}.tgz.sha256"
# 2. CLI GARM + son empreinte
curl -sSLO "https://github.com/cloudbase/garm/releases/download/${GARM_VERSION}/garm-cli-${ARCH}.tgz"
curl -sSLO "https://github.com/cloudbase/garm/releases/download/${GARM_VERSION}/garm-cli-${ARCH}.tgz.sha256"
# 3. Provider LXD + son empreinte (dépôt séparé)
curl -sSLO "https://github.com/cloudbase/garm-provider-lxd/releases/download/${LXD_PROVIDER_VERSION}/garm-provider-lxd-${ARCH}.tgz"
curl -sSLO "https://github.com/cloudbase/garm-provider-lxd/releases/download/${LXD_PROVIDER_VERSION}/garm-provider-lxd-${ARCH}.tgz.sha256"
# 4. Vérifier les trois archives d'un coup
sha256sum --check \
"garm-${ARCH}.tgz.sha256" \
"garm-cli-${ARCH}.tgz.sha256" \
"garm-provider-lxd-${ARCH}.tgz.sha256"

La sortie attendue est une ligne OK par archive :

garm-linux-amd64.tgz: OK
garm-cli-linux-amd64.tgz: OK
garm-provider-lxd-linux-amd64.tgz: OK

Si l'une des lignes affiche FAILED, arrêtez-vous : supprimez les fichiers et recommencez le téléchargement. Un FAILED signifie que le contenu reçu ne correspond pas à ce que le projet a publié.

Chaque archive contient un unique exécutable, sans arborescence ni fichier de configuration. Les deux commandes destinées à l'opérateur (garm, garm-cli) vont dans /usr/local/bin, tandis que le provider externe est isolé dans /opt/garm/providers.d : GARM le lance comme un sous-processus, il n'a aucune raison d'être dans le PATH des utilisateurs.

Fenêtre de terminal
cd /tmp
ARCH="linux-amd64"
# Extraire les trois exécutables
tar -xzf "garm-${ARCH}.tgz"
tar -xzf "garm-cli-${ARCH}.tgz"
tar -xzf "garm-provider-lxd-${ARCH}.tgz"
# Installer le serveur et la CLI
sudo install -o root -g root -m 755 /tmp/garm /usr/local/bin/garm
sudo install -o root -g root -m 755 /tmp/garm-cli /usr/local/bin/garm-cli
# Installer le provider LXD dans un répertoire dédié
sudo mkdir -p /opt/garm/providers.d
sudo install -o root -g root -m 755 /tmp/garm-provider-lxd /opt/garm/providers.d/garm-provider-lxd
# Vérifier
garm --version
# → v0.2.1
garm-cli version
# → garm-cli: v0.2.1

LXD est le provider local qui va créer les conteneurs runners. Sur Ubuntu 24.04, il s'installe via snap.

Épinglez explicitement le canal LTS 5.21/stable plutôt que de laisser snap choisir : le canal latest suit la branche 6.x, qui change de comportement plus souvent et n'est pas la cible testée par le provider.

Fenêtre de terminal
# Installer LXD depuis le canal LTS
sudo snap install lxd --channel=5.21/stable
# Initialiser LXD avec les paramètres par défaut
sudo lxd init --auto
# Vérifier que le socket Unix est présent
test -S /var/snap/lxd/common/lxd/unix.socket && echo "LXD OK"
# Ajouter votre utilisateur au groupe lxd (pour utiliser lxc sans sudo)
sudo usermod -aG lxd ${USER}
newgrp lxd
# Vérifier la version
lxd --version
# → 5.21.5 LTS (ou plus récent sur le canal 5.21)

GARM tourne en tant qu'utilisateur dédié, sans shell, membre du groupe lxd pour accéder au socket LXD.

Fenêtre de terminal
sudo useradd \
--system \
--no-create-home \
--shell /usr/sbin/nologin \
--groups lxd \
garm
# Vérifier
id garm
# → uid=997(garm) gid=997(garm) groups=997(garm),988(lxd)

Étape 5, Créer les répertoires et la configuration

Section intitulée « Étape 5, Créer les répertoires et la configuration »

GARM lit deux fichiers TOML au démarrage. Le premier, /etc/garm/config.toml, décrit le serveur lui-même : écoute HTTP, authentification JWT, base de données, liste des providers. Le second est propre au provider externe LXD et n'est jamais lu par GARM : le serveur se contente de transmettre son chemin au sous-processus via la variable d'environnement GARM_PROVIDER_CONFIG_FILE. Cette séparation explique pourquoi les deux fichiers n'ont ni la même syntaxe ni les mêmes clés.

  1. Créer les répertoires :

    Fenêtre de terminal
    sudo mkdir -p /etc/garm /var/lib/garm
    sudo chown garm:garm /var/lib/garm && sudo chmod 750 /var/lib/garm
    sudo chown root:garm /etc/garm && sudo chmod 750 /etc/garm
  2. Générer les secrets, puis créer /etc/garm/config.toml :

    Fenêtre de terminal
    # 32 caractères hexadécimaux exactement, tirés du générateur du système
    GARM_DB_PASSPHRASE=$(openssl rand -hex 16)
    GARM_JWT_SECRET=$(openssl rand -base64 48)
    sudo tee /etc/garm/config.toml > /dev/null << EOF
    [default]
    [jwt_auth]
    secret = "${GARM_JWT_SECRET}"
    time_to_live = "24h"
    [apiserver]
    bind = "0.0.0.0"
    port = 9997
    use_tls = false
    [logging]
    log_level = "info"
    log_format = "text"
    [database]
    backend = "sqlite3"
    passphrase = "${GARM_DB_PASSPHRASE}"
    [database.sqlite3]
    db_file = "/var/lib/garm/garm.db"
    [[provider]]
    name = "lxd_local"
    description = "LXD local"
    provider_type = "external"
    [provider.external]
    provider_executable = "/opt/garm/providers.d/garm-provider-lxd"
    config_file = "/etc/garm/garm-provider-lxd.toml"
    EOF
  3. Créer /etc/garm/garm-provider-lxd.toml :

    Attention, ce fichier n'a pas de section [lxd] : les clés du provider LXD sont à la racine du document TOML. Le provider refuse de démarrer avec un message trompeur, error validating config: unix_socket or address must be specified, si les clés sont imbriquées dans une table.

    Fenêtre de terminal
    sudo tee /etc/garm/garm-provider-lxd.toml > /dev/null << 'EOF'
    # Socket Unix local, prioritaire sur "url" (connexion réseau + TLS)
    unix_socket_path = "/var/snap/lxd/common/lxd/unix.socket"
    instance_type = "container"
    project_name = "default"
    include_default_profile = false
    secure_boot = false
    # Dépôt d'images Ubuntu officiel. Sans aucun dépôt déclaré, le provider
    # ne peut utiliser que les images déjà présentes sur le serveur LXD.
    [image_remotes]
    [image_remotes.ubuntu]
    addr = "https://cloud-images.ubuntu.com/releases"
    public = true
    protocol = "simplestreams"
    skip_verify = false
    EOF
  4. Fixer les permissions :

    Fenêtre de terminal
    sudo chown garm:garm /etc/garm/config.toml /etc/garm/garm-provider-lxd.toml
    sudo chmod 640 /etc/garm/config.toml /etc/garm/garm-provider-lxd.toml

L'unité ci-dessous utilise Type=exec plutôt que Type=simple. La différence compte au moment du diagnostic : avec Type=simple, systemd considère le service comme démarré dès qu'il a forké, et systemctl start renvoie 0 même si le binaire est absent ou non exécutable. Type=exec attend la réussite de l'appel execve(), ce qui fait remonter immédiatement une erreur 203/EXEC. Les directives ProtectSystem=strict et ReadWritePaths limitent par ailleurs l'écriture au seul répertoire de données, pour qu'une compromission de GARM ne se traduise pas en écriture sur le reste du système de fichiers.

Fenêtre de terminal
sudo tee /etc/systemd/system/garm.service << 'EOF'
[Unit]
Description=GARM - GitHub/Gitea Actions Runner Manager
After=network.target
[Service]
Type=exec
User=garm
Group=garm
WorkingDirectory=/var/lib/garm
ExecStart=/usr/local/bin/garm -config /etc/garm/config.toml
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/garm /etc/garm
ReadOnlyPaths=/opt/garm
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable garm
sudo systemctl start garm

Trois contrôles suffisent à confirmer que le serveur est opérationnel : le service est active, le port 9997 est en écoute, et l'API répond. Le point qui surprend concerne le code HTTP : tant que le compte administrateur n'existe pas, GARM renvoie 409 Conflict sur toutes les routes de l'API, y compris /api/v1/. Ce n'est pas une erreur, c'est la façon dont le serveur signale qu'il attend son initialisation. Le code passera à 401 Unauthorized une fois l'étape 7 terminée.

Fenêtre de terminal
# Vérifier que le service tourne
sudo systemctl status garm --no-pager
# Vérifier que le socket est en écoute
ss -tlnp | grep 9997
# → LISTEN 0 4096 0.0.0.0:9997 0.0.0.0:* users:(("garm",pid=...,fd=16))
# Tester l'API avant initialisation (409 = GARM répond, il attend garm-cli init)
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:9997/api/v1/
# → 409

GARM ne crée pas de compte admin automatiquement. La première commande garm-cli init crée l'administrateur et configure le profil local de garm-cli. Cette opération est à faire une seule fois sur chaque installation.

Fenêtre de terminal
read -r -s -p "Mot de passe admin GARM : " GARM_ADMIN_PASSWORD; echo
garm-cli init \
--name "mon-garm" \
--url "http://localhost:9997" \
--username "garmadmin" \
--password "${GARM_ADMIN_PASSWORD}" \
--email "admin@garm.lab"

La commande affiche deux tableaux, l'un décrivant l'utilisateur créé, l'autre le contrôleur. Retenez le Controller Webhook URL : c'est l'adresse que Gitea devra appeler lors de l'intégration.

Vérification :

Fenêtre de terminal
# Afficher les infos du contrôleur
garm-cli controller show
# → Controller ID, Hostname, Metadata/Callback/Webhook URL, Version v0.2.1
# Lister les providers
garm-cli provider list
# → NAME: lxd_local | DESCRIPTION: LXD local | TYPE: external

Le premier réflexe devant un service failed est de lire le journal plutôt que le statut : GARM écrit la raison exacte du refus sur une seule ligne avant de s'arrêter, et cette ligne identifie la section fautive du TOML. Les deux erreurs les plus fréquentes sur une première installation portent sur la longueur de la passphrase et sur l'accès au socket LXD, qui dépend de l'appartenance de l'utilisateur garm au groupe lxd.

SymptômeCauseSolution
passphrase must be…32 charactersPassphrase trop courte dans [database]Régénérer avec openssl rand -hex 16, qui donne exactement 32 caractères
unix_socket or address must be specifiedClés du provider imbriquées dans une table [lxd]Mettre les clés à la racine de garm-provider-lxd.toml et utiliser unix_socket_path
permission denied sur le socket LXDL'utilisateur garm n'est pas dans le groupe lxdsudo usermod -aG lxd garm puis sudo systemctl restart garm
Port 9997 déjà utiliséAutre processus en écoutess -tlnp | grep 9997 pour identifier le processus
garm-cli: token expiredLe token JWT a dépassé son time_to_livegarm-cli profile login -u garmadmin -p '<mot-de-passe>'
Service en failed au démarrageErreur de configurationjournalctl -u garm -n 20 --no-pager pour lire la ligne d'erreur

Cette installation est fonctionnelle mais volontairement minimale : l'API écoute sur 0.0.0.0 en HTTP sans TLS. Sur une VM de lab isolée, c'est acceptable ; dès que la machine est joignable au-delà de votre réseau d'administration, trois points sont à traiter.

  • Restreindre l'écoute : passer bind = "127.0.0.1" dans [apiserver] et exposer GARM derrière un reverse proxy qui termine le TLS.
  • Activer TLS nativement si vous ne voulez pas de proxy : use_tls = true plus une section [apiserver.tls] avec certificate et key.
  • Protéger les secrets sur disque : /etc/garm/config.toml contient la passphrase de la base et le secret JWT en clair. Le mode 640 avec root:garm de l'étape 5 est le minimum ; une sauvegarde de ce fichier doit être chiffrée au même titre que la base.
  • Les binaires garm, garm-cli et garm-provider-lxd se téléchargent depuis les releases GitHub et se vérifient avec le fichier .sha256 publié à côté de chaque archive.
  • Le provider LXD est versionné indépendamment du serveur : v0.1.5 pour GARM v0.2.1.
  • La passphrase du bloc [database] doit faire exactement 32 caractères (AES-256), d'où openssl rand -hex 16.
  • Le fichier du provider LXD n'a pas de section [lxd] : les clés sont à la racine, et le socket se déclare avec unix_socket_path.
  • L'utilisateur garm doit être dans le groupe lxd pour accéder au socket Unix de LXD.
  • garm-cli init est une opération unique : elle crée l'admin et configure le profil local.
  • Avant initialisation l'API répond 409, après initialisation elle répond 401 : dans les deux cas, GARM tourne.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn