Blog
Warum Sie YAML aufgeben und stattdessen Jsonnet verwenden sollten

Ich weiß nicht genau, wann die Erkenntnis kam, aber wenn ich mir das YAML-Format und seine Verwendung ansehe: Es muss eines der Meisterwerke des Teufels gewesen sein. Ähnlich wie Fliegen, die von einem riesigen Haufen Kuhmist angezogen werden, werden Entwickler, mich eingeschlossen, von einer unsichtbaren Kraft zu diesem Format getrieben.
Es hätte so einfach sein können. Ich meine, es gibt viele Alternativen, getippt und ungetippt, um das Konfigurationsdurcheinander zu bändigen. Aber NOOOOOOOOO, verwenden wir YAML. Ich glaube, die Erkenntnis kam, als ich zum ersten Mal ein Helm-Diagramm sah. Es hat eine Weile gedauert, bis ich es kapiert habe:
- Helm ist der Paketmanager für Kubernetes
- Ich schreibe Helm-Diagramme als "Pakete", die von anderen wiederverwendet werden können
- Niemand verwendet das Diagramm jemals wieder, und wenn doch, wird es zu dieser Art von extrem enger Kopplung, vor der jeder Clean-Code-Blog warnt
- Versuch und Irrtum, bis es funktioniert
- Sie haben es fast geschafft, Sie müssen nur noch die Einrückung richtig hinbekommen.
Oder es handelt sich um eine so genannte"Arbeitsbeschaffungsmaßnahme", ein Weg, die Arbeitslast zu erweitern, um mehr Entwickler von der Straße zu holen.
Ich weiß es nicht.
YAML hat eine (zu) komplexe Spezifikation, leidet unter unbeabsichtigten Parsing-Effekten, wie dem Norwegen-Problem, kann unsicher sein und wird oft missbraucht, um Probleme zu lösen, die eigentlich nicht in YAML gelöst werden sollten.
Was sind die Alternativen?
Ich habe angefangen, mit Jsonnet und HOCON zu spielen. HOCON ist außerhalb der Java/Scala-Blase nicht sehr bekannt und die Unterstützung in anderen Sprachen ist daher"mau".
Und Jsonnet? Ich persönlich finde, es hat die richtige Balance zwischen Leistung und Einfachheit.
Einführung Jsonnet
Was ist Jsonnet? Kurz und bündig:
Eine Datenvorlagensprache für App- und Tool-Entwickler
Denken Sie an erweitertes JSON. Technisch gesehen handelt es sich um eine Obermenge von JSON, in die einige Programmiersprachenkonstrukte eingemischt sind. Das bedeutet, dass Jsonnet "kompiliert" oder transformiert werden muss. Die Ausgabe ist normalerweise JSON.
Jsonnet = JSON + Variablen, Konditionale, Arithmetik, Funktionen, Importe und Fehlerfortpflanzung
Beispiel:
local person3 = import 'person3.libsonnet';
{
person1: {
name: "Alice",
welcome: "Hello " + self.name + "!",
},
person2: self.person1 { name: "Bob" },
person3: person3
}
Der Ablauf ist einfach: Sie beginnen mit einer Jsonnet-Datei (der Hauptdatei), in die Sie andere Jsonnet-Dateien importieren können (Abbildung 1). Die Hauptdatei können Sie zu JSON kompilieren.
Abbildung 1: Fluss von Jsonnet zu JSON (a) Jsonnet verfügt über eine Importfunktion. Sie können andere Dateien, die oft mit .libsonnet als Erweiterung benannt sind, über die Import-Direktive importieren, z.B. local martinis = import 'martinis.libsonnet';. (b) Die importierende Konfigurationsdatei (die Hauptdatei) fasst alle Importe zu einem Konfigurationsobjekt zusammen und (c) schreibt es in die JSON-Ausgabe.
Weitere Informationen finden Sie in der offiziellen Dokumentation.
Macht vs. Einfachheit
Ein Machtvakuum wird gefüllt, ob Sie es wollen oder nicht. Im Falle von YAML wäre das z.B. Jinja. Aber was passiert im Kopf des Entwicklers, wenn Sie Jinja verwenden können? Die Schritte sind einfach:
- Ich habe Jinja
- Ich kann meine eigenen Python-Funktionen erstellen und
- integrieren Sie sie in Jina.
- Jetzt kann ich sie in der YAML-Vorlage verwenden
- Ich lasse auch die Unit-Tests weg, denn es geht ja um die Konfiguration, nicht wahr?
Alternativ kann ich auch versuchen, alles in Jinja zu machen und YAML als "Glue"-Code zu verwenden.
Ergebnis: Spaghetti, Schlangenstücke und Kotelettstäbchen.
Jsonnet hat eine natürliche Obergrenze: Wenn Sie es nicht in Jsonnet lösen können, ist Ihr Problem möglicherweise zu komplex für eine Konfiguration. Wechseln Sie lieber zu einer echten Programmiersprache und abstrahieren Sie die Komplexität in ein separates Modul mit Tests. So bleibt die Konfiguration schlank. Meiner Meinung nach hat Jsonnet eine gute "Korrelation" zwischen sauber aussehen und sauber sein. Warum ist das wichtig, werden Sie sich fragen? Ähnlich wie der Code ist die Konfiguration ein Dokument. Die Entwickler lesen sie, erweitern sie, verbessern sie oder bringen sie durcheinander. Sie müssen sie pflegen. Deshalb ist es besser, falschen Code falsch aussehen zu lassen.
Werkzeugbau
Allerdings muss ich zugeben, dass Jsonnet eine Nischenkonfigurationssprache ist und das Tooling ein wenig, sagen wir mal, "schrullig" ist. Es gibt eine natürliche "Beule", die andere von einer einfachen Übernahme abhält.
Zunächst haben Sie das Paket jsonnet, das von jsonnet.org/ stammt. Leider gibt es keine echte Installationsanleitung. github.com/google/jsonnet ist aufschlussreicher:
brew install jsonnet
apt install jsonnet
<your favourite pkg manager> install jsonnet
sollte funktionieren.
Zusätzlich gibt es go-jsonnet, die offizielle Go-Implementierung(GitHub Repo). Sie ist ein direkter Ersatz für die C++-Implementierung. Sie wird mit einem Formatierer (jsonnetfmt) und einem Linter (jsonnet-lint) geliefert. Praktisch! (wenn Sie es kennen)
brew install go-jsonnet
Das Go Repo enthält einige Installationsanweisungen.
vscode-Erweiterungen sind ebenfalls verfügbar (ich habe sie jedoch nicht verwendet).
Python
Ich mache es sehr kurz (weil es nicht viel zu erzählen gibt):
pip install jsonnet
# or you can also use the go version
# pip install gojsonnet
Sagen wir es so:
import _jsonnet
# in case of the go-based pkg use
# import _gojsonnet
import json
res = json.loads(_jsonnet.evaluate_file("info.jsonnet"))
oder das offizielle Beispiel:
import json
import _jsonnet
jsonnet_str = '''
{
person1: {
name: "Alice",
welcome: "Hello " + self.name + "!",
},
person2: self.person1 {
name: std.extVar("OTHER_NAME"),
},
}
'''
json_str = _jsonnet.evaluate_snippet(
"snippet", jsonnet_str,
ext_vars={'OTHER_NAME': 'Bob'})
json_obj = json.loads(json_str)
for person_id, person in json_obj.items():
print('%s is %s, greeted by "%s"' % (
person_id,
person['name'],
person['welcome']))
Weitere Dokumentation finden Sie auf der Seite Bindungen, scrollen Sie nach unten zu "Python API" und den Tests des Python-Moduls. Die Tests zeigen, dass es eine ganze Reihe von Argumenten für
static PyObject* evaluate_snippet(PyObject* self, PyObject* args, PyObject *keywds)
{
...
static char *kwlist[] = {
"filename", "src", "jpathdir",
"max_stack", "gc_min_objects", "gc_growth_trigger", "ext_vars",
"ext_codes", "tla_vars", "tla_codes", "max_trace", "import_callback",
"native_callbacks",
NULL
};
...
}
Undokumentiert, leider. Die nützlichen sind wahrscheinlich ext_vars, ext_codes, tla_vars, tla_codes, denn sie werden auch in der offiziellen Dokumentation erwähnt. Wenn Sie verstehen wollen, wie das Argument import_callback funktioniert, werfen Sie einen Blick auf den Thread in der Github-Ausgabe.
Als allgemeine Anmerkung: Umgebungsvariablen müssen über die Funktionsargumente übergeben werden. Das ist ein Vorteil: keine Nebeneffekte.
Syntax
Hier bin ich faul, schauen Sie einfach in den offiziellen Dokumentationen nach (Tutorial, Getting Started, ...) . Sie sind recht gut, wenn es um die Syntax geht. Hilfe finden Sie auf StackOverflow.
Arbeitsabläufe und Pipelines
Die Vorbereitung der Konfiguration für mehrere Umgebungen in einer CI/CD-Pipeline ist heutzutage eine der Standardaufgaben für jeden Entwickler. Ich versuche, so viel Code wie möglich aus der YAML-Pipeline-Beschreibung in ein Makefile oder Bash-Skript zu verschieben. Ich tue das, weil es mir die Möglichkeit gibt, Teile der Pipeline zu Debugging-Zwecken lokal auszuführen. Um eine Vorstellung davon zu bekommen, sehen Sie hier ein Beispiel Makefile:
.PHONY: build fmt clean
SHELL=bash
ENV?=acc
build:
mkdir -p out
# Emit multiple files
# If you use multi-file output, the keys are the file names,
# `app-config-file-1.json` in this case.
jsonnet --ext-str env=$(ENV) -m out app-config-files.jsonnet
# Emit single file to stdout
jsonnet --ext-str env=$(ENV) app-config.jsonnet >out/app.json
<out/app.json jq -r '.server' >out/server.txt
fmt:
jsonnetfmt -i *.jsonnet
clean:
rm -rf out/
(Falls erforderlich, extrahiere ich manchmal Teile der Ergebnisse in eine andere Datei mit
jq
.)
Das Makefile verwendet diese jsonnet-Dateien:
// jsonnet/app/app-config-files.jsonnet
local env = std.extVar('env');
local config = {
dev: import 'dev.jsonnet',
acc: import 'acc.jsonnet',
prd: import 'prd.jsonnet',
}[env];
{
'app-config-file-1.json': config,
}
// jsonnet/app/app-config.jsonnet
local env = std.extVar('env');
{
dev: import 'dev.jsonnet',
acc: import 'acc.jsonnet',
prd: import 'prd.jsonnet',
}[env]
// jsonnet/app/prd.jsonnet
{
server: 'prd.bargsten.org',
}
// jsonnet/app/dev.jsonnet
{
server: 'dev.bargsten.org',
}
// jsonnet/app/acc.jsonnet
{
server: 'acc.bargsten.org',
}
Sie können natürlich auch eine Datei pro Umgebung verwenden und die erforderlichen Konfigurationseinstellungen dorthin importieren und so den obigen Ansatz umkehren:
jsonnet $ENV.jsonnet >out/app.json
Schlussfolgerungen
- YAML hat eine Menge Probleme und eine übermäßig komplexe Spezifikation.
- Die Spezifikation von Jsonnet ist sehr einfach.
- Jsonnet hat genug Power, um Dinge zu erledigen, aber eine natürliche "Obergrenze", um all die Komplexität zu verhindern, die mit einer echten Programmiersprache oder einer Hybridlösung wie YAML & Jinja einhergeht.
- Die meisten Programmiersprachen werden unterstützt und im Zweifelsfall können Sie sie jederzeit in JSON umwandeln.
- Jsonnet wurde als rein funktionale Sprache entwickelt und ist frei von Nebeneffekten.
Viel Spaß!
(Sie finden diesen Beitrag auch in meinem persönlichen Blog)
Verfasst von

Joachim Bargsten
Unsere Ideen
Weitere Blogs
Contact


