Sari la conținut
Danube Labs

Fișierul de configurare este un API. Tratează-l ca atare

Un fișier de configurare are clienți, un contract și un mod de eșec. Validează-l înainte de scriere, înlocuiește-l atomic, controlează scriitorii concurenți și explică fiecare cheie.

De Alexandru Secureanu9 min de citit
Un fișier de configurare cu schema, lacăt și istoric în jurul lui

Un fișier de configurare arată ca un fișier text, așa că este editat ca un fișier text. Cineva îl deschide, schimbă o valoare, salvează, repornește serviciul și află abia atunci dacă modificarea a fost bună. Nu am accepta asta de la nicio altă interfață. Un endpoint pe care jumătate din echipă îl apelează cu date nevalidate, care poate fi rescris în timp ce este citit și pe care nimeni nu l-a documentat ar fi considerat o problemă. Fișierul de configurare stă exact în această poziție, de obicei cu mai puține garanții.

Fișierul are clienți, intrări și un mod de eșec

Fiecare cheie dintr-un fișier de configurare este un câmp într-o interfață. Are un tip, un set de valori permise, o valoare implicită și un efect de care depinde cineva. Clienții ei sunt oamenii și programele care o editează: un script de deploy, un panou de administrare, un coleg la unsprezece noaptea, un agent. Modul de eșec diferă de cel al unui API, și tocmai de aceea disciplina se pierde. O cerere greșită primește 400 și cel care a trimis-o o corectează. O cheie greșită este acceptată, procesul pornește, iar problema apare mai târziu ca un URL greșit, o coadă care nu se golește niciodată sau un serviciu care nu ajunge la baza de date.

Comparația nu este decorativă. Ea spune ce trebuie construit: validare, un contract versionat, scrieri atomice, control al concurenței, istoric și documentație. Sunt cerințe obișnuite pentru un endpoint. La un fișier de configurare sunt rare, pentru că fișierul se poate edita de mână și nimic nu impune disciplina.

Validează după schemă înainte de scriere

Majoritatea echipelor validează configurația la pornire, dacă o fac. Procesul citește fișierul, verifică câteva chei obligatorii și se oprește cu eroare când lipsește ceva. Este mai bine decât nimic, dar verificarea cade în cel mai prost moment: după ce modificarea a fost salvată, în timpul unei reporniri, când procesul vechi a dispărut deja.

Validarea își are locul în momentul scrierii. Editorul, CLI-ul sau formularul din admin ar trebui să verifice tot documentul după aceeași schemă pe care o folosește aplicația și să refuze salvarea unuia care nu trece. O schemă JSON este o variantă: tipuri, câmpuri obligatorii, enumerări și constrângeri într-un fișier pe care ambele părți îl pot citi. Formatul contează mai puțin decât aranjamentul. Trebuie să existe o singură definiție canonică, nu o funcție de validare în aplicație și un alt set de reguli în formular.

Trei decizii fac validarea utilă în practică.

  • Chei necunoscute. Respinge-le sau semnalează-le clar. O greșeală de tastare precum max_retires, ignorată în tăcere, este felul în care o setare ajunge fără efect și fără explicație.
  • Valori implicite. Ține-le în schemă, nu împrăștiate prin codul care citește fiecare valoare.
  • Versiune. Schema are o versiune, iar aplicația refuză o configurație scrisă pentru una mai nouă decât ce înțelege.

Validarea la scriere duce eroarea la persoana care a făcut modificarea, cât timp își mai amintește ce a vrut să facă. Verificarea la pornire o duce la cine repornește serviciul, adesea altcineva, adesea peste câteva ore.

Scrie atomic: fișier temporar, apoi redenumire

O scriere nu este o singură operație. Editorul trunchiază fișierul, scrie octeții noi și îl închide. Dacă procesul moare între trunchiere și închidere, dacă discul se umple sau dacă mașina pierde curentul, fișierul de pe disc nu este nici versiunea veche, nici cea nouă. La pornirea următoare, serviciul citește un JSON trunchiat sau un YAML pe jumătate scris, eșuează, iar configurația care funcționa a dispărut.

Remediul este vechi și portabil: scrie conținutul complet într-un fișier temporar din același director, golește bufferul, apoi redenumește-l peste original. Pe sistemele Unix, rename(2) înlocuiește destinația atomic, așa că alt proces vede fie fișierul vechi, fie pe cel nou, niciodată unul parțial. Windows are echivalentul în ReplaceFile, care păstrează și atributele fișierului original și listele de control al accesului. Fișierul temporar trebuie să fie pe același sistem de fișiere cu ținta, pentru că o redenumire nu traversează volume.

Două detalii se uită ușor. Golește bufferul și sincronizează înainte de redenumire dacă mașina poate pierde curentul, pentru că operația este atomică față de alte procese, dar nu neapărat durabilă la o cădere de tensiune. Și păstrează permisiunile originale. Un fișier temporar creat cu valorile implicite moștenește umask-ul celui care scrie, deci o configurație citibilă doar de contul serviciului poate ajunge citibilă de toată lumea. Bibliotecile rezolvă ambele, de exemplu write-file-atomic din npm, care scrie sub un nume temporar, aplică proprietarul și modul existente, redenumește și serializează scrierile concurente din interiorul unui proces.

Stabilește cine are voie să scrie

Înlocuirea atomică protejează împotriva unei căderi. Nu protejează împotriva a doi scriitori. Dacă un panou de administrare și un CLI editează amândouă fișierul, a doua salvare o anulează în tăcere pe prima. Este problema actualizării pierdute, aceeași pe care HTTP a rezolvat-o pentru resurse.

If-Match can be used to prevent the lost update problem.

Fraza vine din descrierea header-ului pe MDN. Când configurația este expusă prin HTTP, trimite versiunea citită și cere serverului să respingă scrierea dacă fișierul s-a schimbat între timp. Pentru un fișier local, echivalentul este un lacăt. flock(2) este consultativ pe Unix: un proces care nu cere lacătul poate scrie în continuare, deci toți scriitorii trebuie să colaboreze. Windows se comportă altfel și impune regulile de partajare la nivel de handle deschis, ceea ce aduce alte moduri de eșec atunci când un cititor ține fișierul.

Regula simplă bate ambele mecanisme: un singur scriitor. Dacă o interfață și un demon trebuie să schimbe setări, lasă demonul să dețină fișierul și pune interfața să îl apeleze. Lacătul este ce adaugi când mai mulți scriitori sunt inevitabili, nu un substitut pentru decizia cine deține fișierul.

Păstrează un istoric pe care îl poți citi și anula

Orice analiză de incident ajunge la întrebarea ce s-a schimbat. Fără istoric, răspunsul vine din memorie, iar memoria înșală. Cu istoric, răspunsul este o diferență, iar remediul este o revenire.

Diferența trebuie să fie structurală, nu textuală. O diferență pe linii a unui fișier JSON raportează că s-a mutat o acoladă și s-au schimbat trei linii, când de fapt s-a schimbat o singură valoare. O diferență structurală raportează că queue.retries a trecut de la 3 la 0 și că queue.backoff a fost eliminată. A doua se citește la două noaptea și se poate anula cheie cu cheie. Stochează documentul parsat pentru fiecare versiune, nu textul randat, și păstrează destul context cât să răspunzi cine a schimbat, când și din ce unealtă.

Dacă fișierul stă într-un repository Git, ai deja aproape tot. Fiecare salvare este un commit, fiecare commit are autor și mesaj, iar revenirea este o operație Git. Din același motiv ținem conținutul site-ului ca fișiere JSON în Git, nu într-o bază de date, cum am descris în articolul despre un CMS fără bază de date. Configurația care stă în afara unui repository, într-un director de date al aplicației sau pe un volum montat, are nevoie de același istoric construit în ce o editează, altfel nu va avea niciunul.

Documentează cheia, nu doar numele ei

O cheie nedocumentată este o cheie pe care nimeni nu o va seta corect. Numele sugerează un lucru, codul face altul, iar singura sursă de încredere este cel care a scris-o. În practică, oamenii copiază valoarea dintr-un articol sau din alt proiect și nu află niciodată ce face.

Pentru fiecare cheie, o referință utilă răspunde la cinci întrebări.

  • Ce face, într-o propoziție pe care o înțelege cineva care nu a scris codul.
  • Ce tip are și ce valori acceptă, inclusiv ce se întâmplă la limite.
  • Care este valoarea implicită și care este valoarea efectivă acum.
  • De unde vine valoarea efectivă: din fișier, dintr-o variabilă de mediu, dintr-un flag de linie de comandă sau din valoarea implicită.
  • Ce afectează o modificare și dacă e nevoie de repornire.

Al patrulea punct lipsește din majoritatea referințelor. Diferența dintre „fișierul zice 3” și „procesul folosește 5 pentru că o variabilă de mediu are prioritate” este adesea tot bug-ul. Eseul twelve-factor definește configurația ca tot ce tinde să varieze între deploy-uri și recomandă variabilele de mediu pentru că nu ajung cu ușurință în versionare și nu depind de un limbaj. Sfatul este bun, dar ascute întrebarea priorității. Odată ce o valoare poate veni din fișier, din mediu, dintr-un flag și dintr-o valoare implicită, cine citește doar una dintre surse nu poate spune ce va folosi procesul.

Deriva este diferența dintre ce crezi și ce rulează

Deriva de configurare apare când staging și producția pornesc de la același fișier și se despart prin mici modificări manuale. Un mediu are o cheie pe care celălalt nu a primit-o niciodată. O valoare implicită s-a schimbat într-o versiune nouă. Cineva a pus o suprascriere într-un panou și a uitat de ea. Nimic din toate acestea nu se vede în repository, pentru că repository-ul ține fișierul, nu starea de la rulare. Kubernetes spune același lucru când descrie un ConfigMap ca mod de a decupla configurația specifică mediului de imaginea containerului, ca aceeași imagine să ruleze în ambele locuri.

„Merge pe staging” este de obicei propoziția care precede descoperirea unei chei nesetate. Staging are gazda SMTP în mediu, producția nu, iar codul cade pe valoarea implicită localhost. Aplicația pornește, verificarea de sănătate trece, iar prima resetare de parolă eșuează. Nimic din diferență nu apare în diff, pentru că diferența nu este în diff.

Remediul este să compari configurația efectivă, nu fișierele. Rezolvă valorile în fiecare mediu, după valori implicite și suprascrieri, apoi compară cheie cu cheie. Rulează comparația ca parte dintr-o lansare, nu ca anchetă. Rezultatul ar trebui să numească cheia, valoarea din fiecare mediu și sursa fiecărei valori. O cheie lipsă și o cheie setată explicit la valoarea implicită sunt fapte diferite, iar amândouă contează.

Două reguli țin deriva mică. Păstrează o singură sursă de adevăr pentru fiecare cheie, ca o valoare să nu fie setată într-un fișier și suprascrisă în alte trei locuri. Și tratează o modificare de configurație ca pe o lansare: validată, revizuită, înregistrată și reversibilă.

O listă de verificare pentru o configurație de încredere

  • O singură schemă, folosită de aplicație și de fiecare editor.
  • Validare la scriere, cu cheile necunoscute respinse sau semnalate.
  • Înlocuire atomică printr-un fișier temporar și o redenumire, cu păstrarea modului și a proprietarului.
  • Un singur proprietar al fișierului sau un lacăt ori o verificare de versiune când există mai mulți scriitori.
  • Istoric de versiuni cu diferențe structurale și revenire dintr-un pas.
  • Documentație pentru fiecare cheie, inclusiv valoarea implicită și cea efectivă.
  • O comparație a valorilor efective între medii înainte de fiecare lansare.

Unde ajută o unealtă

Nimic din toate acestea nu este greu, dar totul se poate sări când fișierul de configurare aparține unui proiect secundar. Am construit GSD Config Manager exact în jurul acestor probleme, pentru config.json-ul GSD, un fișier cu peste două sute de chei imbricate. Unealta explică fiecare cheie pe înțeles, arată valoarea efectivă lângă cea implicită, validează după schemă înainte de salvare, scrie atomic cu blocarea fișierului și păstrează instantanee cu diferențe structurale și restaurare dintr-un click. Pornește cu o singură comandă, nu are componentă de server și are licență MIT.

Danube Labs este un studio de software și AI condus de fondator, în București. Aceeași disciplină se aplică uneltelor interne, panourilor de administrare și scripturilor de deploy; vezi software personalizat, parcurge portofoliul sau programează o discuție introductivă gratuită.

Întrebări frecvente

Ce înseamnă să tratezi un fișier de configurare ca pe un API?
Înseamnă că fiecare cheie este un câmp cu tip, valori permise, valoare implicită și un efect de care depinde cineva, iar cei care editează fișierul sunt clienții lui. De aici vin aceleași cerințe ca la un endpoint: validare după un contract, scrieri sigure, control al concurenței, istoric și documentație.
De ce ar trebui validat un fișier de configurare înainte de salvare?
Validarea la pornire rulează abia după ce modificarea este salvată și serviciul repornește, deci eroarea ajunge la cine îl repornește, adesea mai târziu și adesea la altcineva. Verificarea documentului după schemă în momentul scrierii refuză un fișier stricat cât timp persoana care l-a modificat este încă acolo și îl poate corecta.
De ce nu este atomică scrierea unui fișier de configurare?
O salvare obișnuită trunchiază fișierul și apoi scrie octeții noi, deci o cădere, un disc plin sau o pană de curent între pași lasă un fișier parțial. Scrierea întregului document într-un fișier temporar și redenumirea lui peste original evită asta, pentru că redenumirea înlocuiește destinația într-un singur pas.
Cum evită două procese să își suprascrie reciproc modificările de configurare?
Răspunsul cel mai simplu este să lași un singur scriitor și să ceri fiecărei alte unelte să îi transmită modificarea. Când asta nu se poate, folosește un fișier de blocare respectat de toți scriitorii sau o verificare de versiune, de exemplu un ETag cu If-Match prin HTTP, care respinge o scriere bazată pe o copie veche cu 412 Precondition Failed.
Ce este deriva de configurare și cum o detectezi?
Deriva de configurare este diferența dintre configurația despre care crezi că rulează într-un mediu și cea care rulează de fapt, acumulată din modificări manuale, suprascrieri și valori implicite schimbate. O detectezi rezolvând valoarea efectivă a fiecărei chei în fiecare mediu, inclusiv valorile implicite și suprascrierile, și comparând acele valori, nu fișierele.
Ce ar trebui să conțină documentația pentru o cheie de configurare?
Ce face cheia, tipul și valorile acceptate, valoarea implicită, valoarea efectivă actuală, de unde vine acea valoare și dacă o modificare cere repornire. Sursa valorii efective contează cel mai mult, pentru că o setare suprascrisă de o variabilă de mediu explică multe eșecuri pe care fișierul singur nu le arată.
Distribuie

Articole similare

  • 9 min de citit

    Pagini cu voce: audio ca accesibilitate, nu ca extra

    O versiune audio a unei pagini lungi costă acum câteva minute, deci devine măsură de accesibilitate, nu funcție premium. Cui ajută, ce cere legea și cum o construiești.

    • Accesibilitate
    • Dezvoltare web