Jsonnet: poznámky z praxe

Pár měsíců zpět jsem na Twitteru mlel o Jsonnetu o jeho kráse. V minulosti jsem takhle psal o spoustě věcí a na něco jsem si za pár dní ani nevzpomněl. Tentokrát to ale mělo trochu jiný vývoj. U jednoho zákazníka jsme se pustili do odřezávání z obřího Helm chartu a pro kompletní přepis těch pár věcí jsme zkusili Jsonnet. Jdeme na to!

Proč jsem ho vybral

V minulosti jsem byl spíše zastáncem Kustomize a různého překrývání a patchování. To funguje dobře pro 1, 2 nebo 3 prostředí, ale když se do toho dostane nějaký dynamický prvek, jako jsou třeba review prostředí, tak narazíte na problém, který lze nejlépe popsat jako "narovnák na vohejbák." Kustomize na tyhle věci nefunguje dobře. Alespoň ne pro mě.

A teď se absolutně nemám chuť bavit o tom, jestli jsou a nebo nejsou potřeba review prostředí. Úplně jinej soudek.

Takže zpět k dynamickým věcičkám. Na tohle dobře fungoval Helm, ten umí vzít parametr A, B, C a zkompilovat z toho nějaký string, který při troše štěstí bude uznán jako validní YAML nebo JSON. Ale typický Helm chart se nedá rozumně číst a ani udržovat. Takže při hledání průniku opravdového jazyka a možnosti parametrizace jsem se dostal k následujícím výsledkům:

Dal jsem do kupy seznam nástrojů, kterými bych si v budoucnu rád pomohl, a Jsonnet mi z toho vyšel jako jasný vítěz kvůli jeho nativní podpoře v Argo CD. A teď se půjdeme mrknout na pár věcí, na které jsem přišel, když jsem si hrál pravě s kombinací Jsonnet a ArgoCD.

Top-Level Argumenty!

V dokumentaci je to nazvané TLAs, jsou to argumenty, které můžete nacpat funkci v hlavním souboru. 

function(
    imageRepository="ubuntu",
    imageTag='latest',
) {
    image: imageRepository,
    tag: imageTag,
}

Tahle kravina při běhu vrátí prostě obyčejný objekt s image a tag. Tyhle parametry jdou ale přetížit z příkazové řádky:

jsonnet --tla-str "imageTag=v1.0.0" main.jsonnet

A když to překlopím do Argo CD příkladu, tak to vypadá nějak takhle:

Najednou tedy máme manifest, který se na produkci může volat jen tak bez parametrů a vypadne z toho nějaká sada manifestů pro produkční prostředí, ale zrovna tak můžeme říct Argo CD, ať mi tu verzi obrazu přetíží něčím jiným, protože chci nasadit review prostředí a omrknout novou funkcionalitu, na které makám.

Knihovny na všechno

Jsonnet umožňuje rozložení aplikačního kódu do knihoven. Tady vás asi napadne, že to použijete na nějaké opakující se věci, jako jsou kontejnery, porty atd. Ale tady naše možnosti nekončí. V aplikacích složených z více prvků je vcelku běžné, že sdílí nějakou konfiguraci. Takže proč nehodit nějaká konfigurační data do knihovny?

local datadog(serviceName = '', environment = '', imageTag = '') = {
    DD_AGENT_HOST: 'nejaka.servica.co.ja.vim.tvl',
    DD_AGENT_PORT: '8126',
    DD_TAGS: 'app:' + serviceName + ',release:' + imageTag,
    DD_ENV: environment,
    DD_TRACE_ENABLED: 'true',
};

{
    datadog: datadog,
    necoDalsiho: {
        BLA: 'bla',
        BLABLA: 'blabla',
    },
}

Tuhle věc pak můžeme snadno injectnout někam do ConfigMap a tu pak načíst direktivou envFrom přímo do podu.

local appka = import "../appka.libsonnet";
local config = import "../config.libsonnet";

local customConfigData = {
    URL_NECEHO: "http://neco.co/",
};

appka.new(
    congigData = config.datadog(
        serviceName='neco',
        environment='stodola',
        imageTag='neni',
    ) + customConfigData,
)

Tady je i krásně vidět, jak jde spojovat různé objekty. Nic, co byste neznali z jiných jazyků.

Asserty

Když píšete nějakou knihovnu, tak do jejího těla můžete klidně vložit i nějakou tu validaci. V zásadě klidně stačí podobné jednoduché výrazy:

assert cpu != '' : "cpu can't be empty!";

Takhle můžu pomoci nějakému jinému uživateli a nebo i sobě za pár týdnů, až se budu snažit pomocí sdílené knihovny sestavit nějakou nefungující blbost. Jsonnet kromě toho umí i vyhazovat chyby klíčovým slovem error, ale zatím jsem to na nic nepotřeboval.

Externí knihovny nejsou potřeba. Zatím.

Původně jsem zvažoval použití k8s-libs, o kterých jsem kdysi psal na Twitteru. Ale pak jsem prošel upravovanou codebase a zjistil jsem, že používáme tak málo Kubernetes objektů, že se mi časově nevyplatí zkoumat použití nějaké externí knihovny a zkoušet jí naroubovat na můj stávající mentální model, který u Kubernetes manifestů používám.

Takže jsem aplikační manifesty překlopil do JSON, přidal komentáře a parametrizaci a udělal z nich knihovny, které potom můžu krmit konfiguračními daty pro dané prostředí viz sekce "knihovny na všechno." 

Testování

V minulosti jsem si kolikrát říkal, že bych fakt chtěl ty svoje nechutné Helm charty nějak testovat, abych dokázal najít nějaké případy, kdy se to celé vysype a deploy neprojde. Dokonce na to i nějaké nástroje existovaly nebo existují, ale jejich použití je z mého pohledu poněkud kostrbaté (jako všechno v Helmu). 

V Jsonnetu to trochu víc připomíná klasické programování. Funkcionalitu zkrátka rozložíte na nějaké menší celky, pošlete do toho data a pak už jen někam hodíte a assert a ten vám zahlásí, jestli je to tak či onak. Není to tak příjemné, jako testovat v Go, ale dost to pomůže!

Styleguide. Nebo ... ?

Na začátků jsem jel podle téhle příručky. Ale pak jsem si všiml, že v balíku s jsonnet-go je i jsonnetfmt, který má --in-place parametr. No a jelikož většinou kódím v Go, tak nemám žádný duchovní problém s automatickým formátováním kódu. Zkrátka to pouštím před každým commitem a kašlu na styleguide.

find . -type f -name "*.jsonnet" -o -name "*.libsonnet" -exec jsonnetfmt -i "{}" \;

Tuhle věc pak pouštím i v automatizaci a když mi to zašpiní git repozitář, tak commit neprojde dál 😂Rozkaz zněl jasně - nesmí projet.

YAML stream

Každý jsonnet file by v sobě měl mít alespoň {}. Ale jde to změnit parametrem -y, který pak očekává pole věcí a vygeneruje třeba něco takového.

{}
---
{}
---
{}

No a to se skvěle hodí pro naše Kubernetes manifesty, ne? Já osobně věci pro jednu app konsoliduji právě do jednoho takového pole, abych si mohl prohlížet všechny vygenerované manifesty najednou. A Argo CD s tím taky nemá problém. Cajk.

Závěr

Co dodat na závěr? Po všech eskapádách jsem asi fakt našel nástroj, který mi skutečně pomáhá nějak elegantně řešit problémy, které mívám při psaní Kubernetes manifestů. Má dobrou podporu v pro mě klíčovém nástroji Argo CD a do budoucna si ho dokážu představit jako velkého pomocníka při psaní CloudFormation, Azure ARM a nebo třeba i GitLab / GitHub pipelines.

Všechno z toho bych samozřejmě mohl napsat v nějakém plnohodnotném jazyku, jako je Go nebo ECMAScript. Ale nezvrhlo by se to na nějaký ultra komplikovaný framework řešící základní otázku lidstva? 😁