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:
[Unit]
: Allgemeine Informationen über die UnitName
: Ein sprechender Name für die UnitDocumentation
: Ein Verweis auf eine Manpage oder Online-DokumentationAfter
: Abhängigkeiten; für unser Beispiel sollte das Netzwerk verfügbar sein
[Service]
: Spezifische Direktiven für Service-Unit (systemd.service(5)
)ExecStart
: Der Befehl, mit dem der Service gestartet wirdType
: Wie der Service gestartet werden soll (Rückmeldung über Starterfolg)Restart
: Ob und wie (oft) der Service im Fehlerfall neugestartet werden soll
[Install]
: Direktiven für die Installation der UnitWantedBy
: Welche Target-Unit den Service aufstarten soll
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:
- Die Anwendung läuft mit
root
-Rechten. Das sollte zwar bei der aktuellen Implementierung kein Problem darstellen, ist aber trotzdem unnötig. - Die Weisheiten sind hartkodiert und können nicht so einfach erweitert werden.
- 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:
Name
: Die Unit heisst nunfortune2
und nicht mehrfortune1
.ExecStart
: Das neue Programmfortune2
wird mit einem Argument gestartet.User
undGroup
: Der Service wird mit dem neuen Benutzer gestartet.WorkingDirectory
: Das Arbeitsverzeichnis ist das$HOME
-Verzeichnis desfortune
-Benutzers, welches auch die Datei mit den Weisheiten enthält.
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:
- 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-Packageflag
) 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. - Da die Datei mit den Weisheiten nun einfach ersetzbar ist, könnten ein paar weitere Beispiele aus anderen Regionen für Abwechslung sorgen.