Linux-Packages erstellen aus fertigen Binaries – mit automatischen Updates

Man kennt das Problem: in der Linux-Distribution der Wahl ist eine gewünschte Software nicht als Paket verfügbar, oder nicht in der Version, die man braucht/möchte. 

Früher™ hätte man häufig selbst kompilieren müssen. Heutzutage, und insbesondere mit Programmiersprachen wie Go oder Java, aus denen (fast) statische Binaries/JARs fallen, stellen viele Upstreams eigene Binaries für ihre Programme bereit. Mit etwas Glück auch in Form von DEBs oder RPMs – aber leider nicht immer.

Was bisher geschah

In diesem Fall haben wir bisher die Binaries auf einen internen Webserver bei uns kopiert und von dort per Puppet ausgerollt:

  archive { "/opt/jmx-exporter/jmx_prometheus_javaagent.jar":
    ensure        => present,
    source        => 'https://…/files/prometheus/jmx_prometheus_javaagent-0.15.0.jar',
    checksum      => 'fccad536103503e21df12c6a16e3870924432418e7eef1e5730f9bc81789d7b662f4a8b66baf1fcdc63ba26c171047aab046a7b0b23957def26ca8e1790062cf',
    checksum_type => 'sha512',
    user          => 'root',
    group         => 'root',
  }

Mit unserem Lieblingstool für Dependency-Updates, Renovate, konnten wir leicht dafür sorgen, dass die Versionsangabe automatisch aktualisiert wird.

Leider reichte das noch nicht: wenn wir nur die URL im Puppet-Code ändern, dann zeigen wir damit auf eine Datei, die es nicht gibt, und der Hash ist auch noch falsch. Die externe URL direkt verwenden möchten wir auch nicht, da unsere Maschinen nicht einfach „nach draußen“ ins Internet verbinden können (defense in depth). 

Um die Datei bei neuen Releases auch automatisch auf den internen Webserver zu laden, damit die URL funktionieren würde, haben wir kein passendes Tool parat. Aber wir haben ein passendes Tool parat, um OS-Packages zu bauen.

nfpm

nfpm benutzen wir seit Jahren um DEB- und RPM-Packages zu bauen. Mit nfpm muss man sich nicht in die arkanen Werkzeuge von Debian und RPM einlesen, sondern kann mit einer einfachen Konfigurationsdatei ein Paket schnüren:

name: "babiel-prometheus-jmx-exporter"
arch: "all"
platform: "linux"
version: "${UPSTREAM}"
release: ${EARTHLY_SOURCE_DATE_EPOCH}
section: "default"
priority: "optional"
maintainer: "Babiel Providing <…@babiel.com>"
description: Prometheus jmx exporter
vendor: "Babiel Providing <…@babiel.com>"
license: "Unknown"
contents:
  - src: "./jmx_prometheus_javaagent.jar"
    dst: "/usr/lib/jmx_prometheus_javaagent.jar"
    file_info:
      mode: 0644

Die gleiche Konfigurationsdatei funktioniert für alle Paketformate. Wir erstellen in diesem Beispiel nur ein DEB, aber an anderen Stellen erzeugen wir auch ein RPM. nfpm unterstützt außerdem noch die Paketformate von Alpine und Arch Linux.

Der neue Ansatz

Alles was fehlte, war Renovate und nfpm zu verbinden, um automatisch OS-Packages aus externen Binaries zu erzeugen. Diesen Teil haben wir kürzlich implementiert.

Earthly ist unser Build-Tool der Wahl, daher passiert das Ganze in einem überschaubaren Earthfile:

VERSION 0.7

# Die Upstream-Version die wir runterladen. Wird von Renovate aktualisiert
ARG --global UPSTREAM=0.20.0

binary:
    FROM …     # ein internes Container-Image von uns, u.a. curl
    WORKDIR /build
    RUN curl --fail -L -o jmx_prometheus_javaagent.jar https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/${UPSTREAM}/jmx_prometheus_javaagent-${UPSTREAM}.jar

    # Mit diesem Befehl machen wir Dateien für andere Targets verfügbar
    SAVE ARTIFACT ./jmx_prometheus_javaagent.jar

package:
    FROM goreleaser/nfpm:v2.33.1@sha256:27a3404a83cb33282397ee6015ea81df0ae5232fc6debcc775d17f73c1581853
    WORKDIR /build
    
    # Dieses COPY funktioniert wie im Dockerfile
    # Die Datei wird aus dem Quellverzeichnis kopiert
    COPY nfpm.yaml ./

    # Dieses COPY kopiert eine Datei aus einem anderen Target
    COPY +binary/jmx_prometheus_javaagent.jar .

    ARG EARTHLY_SOURCE_DATE_EPOCH # interne Variable aktivieren
    RUN nfpm pkg --target ./prometheus-jmx.deb
    SAVE ARTIFACT *.deb

# [… weitere Targets für Upload auf unseren Debian-Mirror …]

Das Earthfile befindet sich in einem Git-Repo, dessen Inhalte dann von Renovate erkannt und automatisch aktualisiert werden. Die zugehörige renovate.json:

{
    "$schema": "https://docs.renovatebot.com/renovate-schema.json",
    "packageRules": [
        {
            "matchPackageNames": [
                "prometheus/jmx_exporter"
            ],
            "matchDatasources": [
                "github-releases"
            ],
            "extractVersion": "^parent-|(?<version>\\d.*)$"
        }
    ],
    "regexManagers": [
        {
            "fileMatch": [
                "Earthfile$"
            ],
            "matchStrings": [
                "ARG\\s--global\\sUPSTREAM=(?<currentValue>.*)"
            ],
            "depNameTemplate": "prometheus/jmx_exporter",
            "datasourceTemplate": "github-releases"
        }
    ]
}

Wir haben auch eine elegante Lösung für ein kleines Problem gefunden: Was passiert wenn man am Paketbuild etwas ändert, ohne dass sich das Upstream-Release ändert? Die Paketformate sehen dafür die Revision vor: hinter der Upstream-Version steht noch ein -1 usw. Um diese Revision hochzählen zu können, müsste man natürlich die jeweils vorherige gespeichert haben. Stattdessen gehen wir einen einfacheren Weg: Wir verwenden $EARTHLY_SOURCE_DATE_EPOCH. Diese Variable wird von Earthly bereitgestellt und enthält den Zeitpunkt des letzten Git-Commits als Unix-Timestamp.  Die vollständige Version lautet dann z.B. 0.20.0-1696945516.

Fazit

Unser neuer Ansatz macht es trivial neue Packages zu erstellen, daher haben wir schon einige.

Und Renovate war auch schon fleißig:

Der Rollout der Pakete erfolgt dann wie üblich durch den Automainter.

Falls du auch Interesse hast solche Lösungen zu finden und an diesen zu arbeiten, bewirb dich doch bei uns.

(Header-Bild: Meanwell Packaging, CC BY 2.0, via Wikimedia Commons)