Glückskekse als systemd-Service

Mit fortune(6) als Go-Server

Wer schon unter grösserer Langweile gelitten hat, dürfte mit dem “Spiel” fortune(6), das sich auf Debian folgendermassen installieren lässt, bekannt sein:

# apt install fortunes

$ fortune
Debug is human, de-fix divine.
$ fortune
Abraham Lincoln didn't die in vain.  He died in Washington, D.C.
$ fortune
You work very hard.  Don't try to think as well.

Doch ist diese Art der Bedienung noch angemessen für das dritte Jahrtausend?

Excuse me, wir haben 2024!

Sollte man daraus nicht eine Web-Anwendung machen, die dann mit systemd gestartet wird?

Ein Go-Server

Zunächst benötigen wir einmal Go, damit wir damit eine Anwendung schreiben können:

# apt install golang

Womit wir folgende randgruppengerechte Implementierung bereitstellen (fortune1.go):

package main

import (
    "math/rand"
    "net/http"
)

var quotes = []string{
    "Kommet mer am beschta glei nochem Mittagessa, no sendr zom Veschpara wieder drhoim.",
    "Sieba Johr hen se mer verseggelt, aber i hans glei gmerkt.",
    "Der putzt da Arsch vor er gschissa hot – no ka-ner s' Babieer zwoamol braucha.",
    "A bissle domm isch jedr, abbr so domm wia manchr isch koinr.",
    "A guads Gwissa kommd bloß vom a schlechda Gedächdnis.",
    "A Schwob wird nedd reich durch viel vrdiena, sondern durch wenig ausgäba!",
    "Drei Bier senn au a Veschbr, ond no hasch erschd no nix gessa.",
    "Wo Geld isch, isch au dr Deifl, wo kois isch, do isch er zwoimol.",
    "Schlof naggich, no vrsoichsch koi Hemmad.",
    "Mr ko au ogschaffd veschbara, abbr ohne Veschbr ko mr nedd schaffa!",
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        i := rand.Intn(len(quotes))
        w.Write([]byte(quotes[i] + "\n"))
    })
    http.ListenAndServe("0.0.0.0:2024", nil)
}

Das Programm wird folgendermassen kompiliert und gestartet:

$ go build fortune1.go
$ ./fortune1

Die Weisheit soll per curl(1) aufgesogen werden:

# apt install curl

$ curl localhost:2024
A guads Gwissa kommd bloß vom a schlechda Gedächdnis.
$ curl localhost:2024
A bissle domm isch jedr, abbr so domm wia manchr isch koinr.
$ curl localhost:2024
Mr ko au ogschaffd veschbara, abbr ohne Veschbr ko mr nedd schaffa!

Ein systemd-Service

Damit uns die Weisheit immer gleich beim Systemstart zur Verfügung steht, soll der Server von einer systemd-Service-Unit gestartet werden.

Zunächst soll das ausführbare Programm an einen Ort verschoben werden, wo es alle Benutzer des Systems verwenden können:

# mv fortune1 /usr/local/bin/

Dann erstellen wir die Unit-Datei (fortune1.service):

[Unit]
Name=fortune1: Weisheiten aus dem Schwarzwald
Documentation=https://www.paedubucher.ch/articles/dfde-systemd
After=network.target

[Service]
ExecStart=/usr/local/bin/fortune1
Type=simple
Restart=always

[Install]
WantedBy=multi-user.target

Die einzelnen Abschnitte und Direktiven haben folgende Bedeutung:

Diese verschieben wir ins Verzeichnis mit den übrigen systemd-Unit-Dateien:

# mv fortune1.service /etc/systemd/system/

Der systemd-Daemon muss neu geladen werden, damit er die Unit erkennt:

# systemctl daemon-reload

Anschliessend starten wir den Service:

# systemctl start fortune1.service

Wir überprüfen noch, ob das wirklich geklappt hat:

$ systemctl is-active fortune1.service
active

Und ergötzen uns von Neuem der schwäbischen Weisheiten:

$ curl localhost:2024
A Schwob wird nedd reich durch viel vrdiena, sondern durch wenig ausgäba!

Der Service soll beim nächsten Systemstart gleich mitgestartet werden:

# systemctl enable fortune1.service

Hierdurch wird ein symbolischer Link vom Verzeichnis des multi-user.target auf unsere Unit-Datei erstellt. Sobald das System beim Aufstarten in den Mehrbenutzerbetrieb wechselt, wird auch unser Server gestartet.

Einige Verbesserungen

Die Anwendung und ihr Deployment weisen noch einige Schwächen auf:

  1. Die Anwendung läuft mit root-Rechten. Das sollte zwar bei der aktuellen Implementierung kein Problem darstellen, ist aber trotzdem unnötig.
  2. Die Weisheiten sind hartkodiert und können nicht so einfach erweitert werden.
  3. Der Systemadministrator erfährt nicht, wie oft und von wem der Service verwendet wird.

Diese Mängel sollen behoben werden: Auf Anwendungs- und Konfigurationsebene!

Verbesserter Go-Server

Der Server soll die Weisheiten aus einer Textdatei lesen, welche über ein Kommandozielenargument mitgegeben werden soll. Ausserdem soll er die Aufrufe loggen. Diese Änderungen wurden hier vorgenommen (fortune2.go):

package main

import (
    "fmt"
    "math/rand"
    "net/http"
    "os"
    "strings"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr, "usage: %s [file]\n", os.Args[0])
        os.Exit(1)
    }
    data, err := os.ReadFile(os.Args[1])
    if err != nil {
        fmt.Fprintf(os.Stderr, "reading file %s: %s\n", os.Args[1], err)
        os.Exit(1)
    }

    var quotes []string
    lines := strings.Split(string(data), "\n")
    for _, line := range lines {
        quote := strings.TrimSpace(line)
        if len(quote) > 0 {
            quotes = append(quotes, quote)
        }
    }

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(os.Stderr, "wisdom requested from %s\n", r.RemoteAddr)
        i := rand.Intn(len(quotes))
        w.Write([]byte(quotes[i] + "\n"))
    })
    http.ListenAndServe("0.0.0.0:2024", nil)
}

Anstelle hartkodierter Zitate besteht der Code nun v.a. aus Fehlerbehandlung, dem Einlesen der Dateien und dem Herausfiltern leerer Zeilen.

Damit der neue Server getestet werden kann, muss erst einmal der alte beendet (und beim Systemstart nicht wieder aufgestartet) werden:

# systemctl disable --now fortune1.service

Die Weisheiten wurden aus dem Code in die Datei schwaebische-weisheiten.txt verschoben:

Kommet mer am beschta glei nochem Mittagessa, no sendr zom Veschpara wieder drhoim.
Sieba Johr hen se mer verseggelt, aber i hans glei gmerkt.
Der putzt da Arsch vor er gschissa hot – no ka-ner s' Babieer zwoamol braucha.
A bissle domm isch jedr, abbr so domm wia manchr isch koinr.
A guads Gwissa kommd bloß vom a schlechda Gedächdnis.
A Schwob wird nedd reich durch viel vrdiena, sondern durch wenig ausgäba!
Drei Bier senn au a Veschbr, ond no hasch erschd no nix gessa.
Wo Geld isch, isch au dr Deifl, wo kois isch, do isch er zwoimol.
Schlof naggich, no vrsoichsch koi Hemmad.
Mr ko au ogschaffd veschbara, abbr ohne Veschbr ko mr nedd schaffa!

Anschliessend wird der neue Service kompiliert und getestet:

$ go build fortune2.go
$ ./fortune2 schwaebische-weisheiten.txt

Getestet wird erneut mit curl:

$ curl localhost:2024
Wo Geld isch, isch au dr Deifl, wo kois isch, do isch er zwoimol.

Was serverseitig folgende Ausgabe erzeugt:

wisdom requested from 127.0.0.1:39948

Der verbesserte Service wird sogleich allen Benutzern des Systems zugänglich gemacht:

# mv fortune2 /usr/local/bin/

Verbesserte Service-Konfiguration

Zunächst erstellen wir einen neuen Benutzer mit $HOME-Verzeichnis, eigener Benutzergruppe und der Bash als Shell:

# useradd -m -d /home/fortune -U -s /usr/bin/bash fortune

Die Weisheiten werden ins $HOME-Verzeichnis dieses neuen Benutzers verschoben, der sogleich zum neuen Besitzer davon gemacht wird:

# mv schwaebische-weisheiten.txt /home/fortune/
# chown fortune:fortune /home/fortune/schwaebische-weisheiten.txt

Die neue Service-Unit soll auf der alten basieren:

# cp /etc/systemd/system/fortune1.service /etc/systemd/system/fortune2.service

Doch sollen einige Änderungen daran vorgenommen werden:

[Unit]
Name=fortune2: Weisheiten aus aller Welt
Documentation=https://www.paedubucher.ch/articles/dfde-systemd
After=network.target

[Service]
ExecStart=/usr/local/bin/fortune2 schwaebische-weisheiten.txt
Type=simple
Restart=always
User=fortune
Group=fortune
WorkingDirectory=/home/fortune

[Install]
WantedBy=multi-user.target

Es wurden folgende Änderungen vorgenommen:

Der systemd-Daemon muss neu geladen werden, damit er die neue Unit erkennt:

# systemctl daemon-reload

So soll der Service testhalber gestartet werden:

# systemctl start fortune2.service

Läuft der Service?

$ systemctl is-active fortune2.service
active

Er läuft und wird sogleich getestet. Der Admin möchte aber hierzu die Log-Ausgaben mitverfolgen:

# journalctl -fu fortune2.service

Der Test kann starten:

$ curl localhost:2024
Kommet mer am beschta glei nochem Mittagessa, no sendr zom Veschpara wieder drhoim.

Und der Admin sieht folgendes:

Nov 30 10:23:48 bookworm fortune2[3216]: wisdom requested from 127.0.0.1:40670

So soll der verbesserte Service automatisch aufgestartet werden:

# systemctl enable fortune2.service

Fazit

Es wurde zunächst eine einfache Go-Serveranwendung entwickelt, welche zufällige Zitate aus einer hartkodierten Liste via HTTP zurückliefert. Die Serveranwendung wurde als systemd-Service-Unit konfiguriert und ausgeführt.

Anschliessend wurde die Serveranwendung verbessert, sodass sie Weisheiten aus einer Datei anbieten kann und die Aufrufe als Logmeldungen ausgibt. Die Service-Konfiguration wurde dahingehend erweitert, dass der Server von einem Benutzer mit eingeschränkten Rechten ausgeführt wird. Die Logmeldungen der Anwendung können im Journal eingesehen werden.

Übungen

Wer etwas tiefer in Go, systemd oder andere Quellen der Weisheit eintauchen möchte, der kann sich an den folgenden Übungen versuchen:

  1. Der Service horcht auf die Adresse 0.0.0.0:2024, d.h. akzeptiert Aufrufe von überall her auf Port 2024. Diese beiden Angaben könnte man per Kommandozeilen-Flag (Go-Package flag) oder mithilfe von Umgebungsvariablen (FORTUNE_ADDR, FORTUNE_PORT) konfigurierbar machen. Hierzu muss sicher das Go-Programm, aber je nach Lösungsweg auch die Unit-Konfiguration angepasst werden.
  2. Da die Datei mit den Weisheiten nun einfach ersetzbar ist, könnten ein paar weitere Beispiele aus anderen Regionen für Abwechslung sorgen.