Scrie partea din configurația Envoy care e a ta și generează restul
Serviciile Transferify stau în spatele unui singur gateway, un proxy Envoy. Acesta verifică cererile cu serviciul nostru de autentificare, admite browserele după origine și trimite căile și metodele RPC către serviciile din spatele lui. Configurația lui era un singur fișier Envoy lung. Acum oamenii editează un fișier scurt, iar un program scrie din el fișierul pentru Envoy.
Programul se numește proxyfile. L-am construit la Transferify pentru propriul nostru gateway. E open source sub licența Apache-2.0, e scris în Go și se află la github.com/kitecorp/proxyfile. Articolul acesta explică de ce l-am scris, ce scrii tu și ce scrie el pentru tine, și cum am mutat propriul nostru gateway pe el fără să schimbăm ce face Envoy. Pentru una dintre părțile pe care le scrie, puntea de la Connect la gRPC, vezi Cum transformă Envoy apelurile Connect ale browserului în gRPC.
De ce crește mereu fișierul Envoy al unui gateway
Configurația Envoy e explicită prin design. Fiecare listener, cluster, rută, filtru și regulă de antet e scrisă complet. Asta i se potrivește lui Envoy, care o citește. E greu pentru oamenii care o schimbă, pentru că un gateway real se repetă: fiecare rută în fiecare mediu, fiecare cluster, fiecare regulă de antet.
Al nostru ajunsese la 552.000 de octeți în 799 de linii. Servea trei medii, iar virtual host-ul fiecărui mediu conținea aceleași 205 rute RPC. O copie era YAML. Celelalte două erau fiecare câte o singură linie de JSON, lungă de circa 167.000 și 169.000 de caractere. Un generator scria acele părți între comentarii de marcaj, în același fișier pe care oamenii îl editau de mână.
Faptele care erau ale noastre erau puține: serviciile și porturile lor, timeout-urile, verificarea de autentificare, originile browserelor pentru fiecare mediu, rutele după cale sau după metoda RPC, eșantionarea jurnalelor și tracing-ul. Erau risipite printre numele de tipuri ale Envoy, iar ca să schimbi unul trebuia mai întâi să-l găsești. Nimeni nu putea citi sau revizui fișierul.
Voiam să scriem doar ce e al nostru, cu motivul lângă fiecare valoare, și să lăsăm o unealtă să scrie restul, la fel de fiecare dată.
Cel mai mic proxyfile
Un proxyfile descrie un singur gateway. Doar patru chei sunt obligatorii: listen, services, origins și routes. Acesta e cel mai mic, din exemplele repository-ului:
# yaml-language-server: $schema=https://raw.githubusercontent.com/kitecorp/proxyfile/v1.2.0/schema/proxyfile.schema.json
# The smallest proxyfile: one service, one route, and the browser origins it admits.
listen: 8000
services:
app: {http: 3000}
origins:
all: {hosts: [localhost], origins: ["http://localhost:3000"]}
routes:
- {name: app, prefix: /, to: app}
Același gateway în HOCON:
# The smallest proxyfile, in HOCON: one service, one route, and the browser origins it admits.
listen = 8000
services {
app { http = 3000 }
}
origins {
all { hosts = [localhost], origins = ["http://localhost:3000"] }
}
routes = [
{ name = app, prefix = /, to = app }
]
Se citește cum sună: un listener public pe portul 8000, un serviciu cu un port HTTP, un mediu care răspunde pentru localhost și admite originea de browser http://localhost:3000, și o rută care trimite fiecare cale către serviciu. Ambele fișiere dau același envoy.yaml, octet cu octet: 119 linii și 5.592 de octeți. Tot ce conține și nu spune proxyfile-ul e o valoare implicită, aceeași în fiecare gateway pe care îl scrie proxyfile.
render și check
render scrie fișierul Envoy. check generează în memorie și compară rezultatul cu fișierul de pe disc. Nu scrie nimic.
proxyfile render gateway.yaml -o envoy.yaml
proxyfile check gateway.yaml -o envoy.yaml
Niciuna nu afișează nimic, și amândouă se încheie cu 0. După o editare manuală a envoy.yaml sau o schimbare în gateway.yaml fără un nou render, check se încheie cu 1 și spune:
envoy.yaml differs from what proxyfile render writes
Aceeași intrare dă mereu aceiași octeți, și de aceea funcționează perechea asta. Pui fișierul generat într-un commit lângă sursa lui, le revizuiești pe amândouă în același pull request și lași check din build-ul tău să eșueze când cineva editează de mână rezultatul sau uită să ruleze render. Fișierul generat spune asta pe prima linie:
# Generated by proxyfile. Do not edit: change the proxyfile beside it and run proxyfile render; proxyfile check fails on a hand edit.
render își scrie fișierele doar când toată intrarea e fără probleme, așa că o greșeală nu lasă niciodată în urmă un fișier scris pe jumătate. Codul de ieșire 2 înseamnă o eroare de utilizare. Fără un nume de fișier, proxyfile citește Proxyfile, proxyfile.conf, proxyfile.yaml sau proxyfile.yml din directorul de lucru, și trebuie să existe exact unul dintre ele.
Cum arată o greșeală
Fiecare cheie e verificată. Scrie greșit listen în cel mai mic fișier, și render refuză, nu scrie nimic și numește fișierul, linia și coloana:
gateway.yaml:3:1: "listen" is required here
gateway.yaml:3:1: unknown key "liste"; known here: admin, auth, cors, limits, listen, log, namespace, origins, remove_from_clients, routes, rpc, services, timeouts, tracing
O cheie necunoscută, oriunde ar fi, e o problemă, iar mesajul enumeră cheile cunoscute în acel loc. O rută către un serviciu care nu e declarat dă:
gateway.yaml:9:32: route "app": the service "shop" is not declared
Verificările rulează în etape: citirea și sintaxa, apoi formatul fiecărei chei, apoi regulile care compară cheile între ele. O rulare raportează toate problemele primei etape care găsește vreuna, așa că repari câte un lot o dată, nu câte o problemă la fiecare rulare.
În HOCON, și o eroare de sintaxă are o linie și o coloană. Celelalte probleme numesc în schimb calea cheii, pentru că cititorul HOCON nu păstrează o linie pentru o valoare. Dacă scrii portul ca http = "3000", obții:
gateway.conf: services.app.http: "http" must be a whole number from 1 to 65535, not quoted or block text
În HOCON o valoare între ghilimele e text, iar un port trebuie să fie un număr.
Valori implicite decise o singură dată
Ce nu are o cheie în format e la fel în fiecare gateway pe care îl scrie proxyfile. Configurarea Envoy stă în fragmente YAML în interiorul programului, iar fiecare fragment are comentarii care spun de ce e scris așa. Câteva valori implicite la care ținem:
- Nicio cale în vreo linie de jurnal. Gateway-ul scrie trei feluri de linii de jurnal JSON pe ieșirea standard, și niciuna nu conține o cale, un query, o adresă, un user agent sau un referrer, pentru că o cale poate purta linkuri și tokenuri. Cu
log, fiecare răspuns de lastatus_fromîn sus e înregistrat, la fel fiecare răspuns cu un response flag Envoy, plus o parte din rest, aleasă după primele opt cifre hexazecimale dinx-request-id. - Reîncercări doar când cererea n-a ajuns niciodată la serviciu. Valoarea
retriesa unei rute se aplică doar condițiilorconnect-failureșireset-before-requestdin Envoy, așa că o cerere pe care serviciul a văzut-o nu e trimisă niciodată de două ori. - Un 502 gol când un serviciu nu poate fi contactat. Răspunsul propriu al Envoy e un 503, sau un 502 cu un corp care descrie eșecul. Cu proxyfile, clientul primește un 502 cu corpul gol, iar detaliile ajung în linia de jurnal
gateway_upstream_failure. x-request-idpe fiecare răspuns, cel al clientului, dacă l-a trimis, așa că un apelant poate cita id-ul unui apel eșuat. Erequest_iddin jurnalul cererilor.- Nicio rescriere a căii. Envoy nici nu normalizează o cale, nici nu comasează slash-urile, așa că regulile de cale ale serviciului decid ce înseamnă o cale.
- Originile străine se opresc la gateway. O origine de browser care nu e admisă primește
403înainte ca CORS, verificarea de autentificare sau vreun serviciu s-o vadă: la fel pentru preflight-uri, cereri și upgrade-uri WebSocket. Pentru o origine admisă, gateway-ul răspunde singur la fiecare preflight. - Corectura de status pentru puntea Connect. Fiecare rută RPC transformă un apel Connect în gRPC. Pentru patru coduri, puntea din Envoy încă scrie statusul HTTP dintr-un tabel mai vechi al codurilor de eroare Connect, așa că un mic filtru Lua îl setează pe cel actual:
canceled499,deadline_exceeded504,failed_precondition400 șiunimplemented501. Articolul despre punte explică de ce contează asta pentru jurnale și alarme.
Nu există nicio cale de a trece configurație Envoy brută printr-un proxyfile. Când un gateway are nevoie de altă valoare pentru una dintre aceste valori implicite, e nevoie de o cheie nouă.
Verificarea de autentificare
Cu auth, gateway-ul întreabă unul dintre serviciile tale despre fiecare cerere, înainte ca cererea să ajungă la un serviciu. Întreabă în modul HTTP, așa că serviciul de autentificare e un endpoint HTTP obișnuit și nu are nevoie de niciun tip specific Envoy. Iată un gateway cu o verificare, cu numele inventate din repository:
# yaml-language-server: $schema=https://raw.githubusercontent.com/kitecorp/proxyfile/v1.2.0/schema/proxyfile.schema.json
listen: 8000
timeouts:
upstream_idle: 45s # under the accounts service's own idle timeout, so the gateway closes first
services:
shop: {http: 8080}
accounts: {http: 7070}
auth:
ask: accounts /auth/check
timeout: 1500ms
enabled: true
when_unavailable: allow # if accounts cannot answer, let the request through and log it
only_with_header: authorization # a request without a token goes through unchecked
send: [authorization]
accept: [x-user-*] # the answer headers that reach the service
origins:
live:
hosts: [api.example.com]
origins: ["https://example.com"]
routes:
- {name: accounts-webhooks, path: /api/webhooks, method: POST, to: accounts, rewrite: /hooks/payments, auth: off}
- {name: shop, prefix: /, to: shop, retries: 1}
log: {status_from: 400, sample: 1%}
Comentariul de lângă upstream_idle e genul de motiv pe care îl voiam lângă fiecare valoare. O verificare trimisă pe o conexiune pe care serviciul de autentificare tocmai o închide eșuează, așa că gateway-ul ar trebui să închidă o conexiune inactivă înaintea serviciului.
Ce primește verificarea și ce face răspunsul ei:
- Primește o cerere către calea din
ask, niciodată calea sau query-ul clientului, pentru că o cale poate purta linkuri și tokenuri. Primește antetele pe care le numeștesend, iar modul HTTP adaugăHostal clientului, metoda,AuthorizationșiContent-Length: 0. x-gateway-timeout-msconține termenul-limită, aici 1500, ca serviciul de autentificare să-și poată ține propriile apeluri în interiorul lui.- Un
200lasă cererea să treacă. Antetele răspunsului pe care le numeșteacceptsunt setate pe cererea către serviciu și înlocuiesc orice valoare de acolo. Un5xxcontează ca apel eșuat, despre care decidewhen_unavailable. Orice alt status se întoarce la client ca refuz. - Un client nu poate falsifica ce spune verificarea. Gateway-ul elimină din cererile clienților fiecare antet
accept, și antetele cu care întreabă verificarea, înainte de rutare și înainte de orice filtru. - Cu
when_unavailable: allow, o cerere care n-a putut fi verificată trece cux-envoy-auth-failure-mode-allowedsetat, iar gateway-ul scrie pentru ea o linie de jurnalgateway_auth_unavailable. auth: offpe o rută, ca ruta de webhook de mai sus, își trimite cererile mai departe fără verificare.
Rute RPC, dintr-o listă de metode
Pentru serviciile gRPC și Connect nu scrii câte o rută pentru fiecare metodă. Pui o intrare rpc în routes și dai lista de metode. În HOCON lista poate sta într-un fișier separat:
include required("rpc-methods.conf") # written by the build; required: a missing file is an error
listen = 8000
services {
web { http = 3000 }
backend { grpc = 5000 }
}
origins {
only { hosts = [gateway.example.net], origins = ["https://app.example.net"] }
}
routes = [
{ rpc { to { "*" = backend } } }
{ name = web, prefix = /, to = web }
]
Iar rpc-methods.conf lângă el:
rpc {
methods = [
{ service = "example.backend.v1.ReportService", method = ListReports, action = read, reports = [export, share] }
]
}
proxyfile scrie câte o rută pentru fiecare metodă, potrivită pe calea exactă pe care o poartă un apel, aici /example.backend.v1.ReportService/ListReports, cu puntea Connect pornită pentru ea. Rutele RPC sunt sortate după cale și scrise acolo unde stă intrarea rpc, așa că o rută simplă de deasupra intrării e încercată prima, iar una de dedesubt după ele. În to, un nume exact de serviciu câștigă, apoi cel mai lung model de pachet care se potrivește, iar * vine ultimul. Cu o verificare de autentificare, fiecare rută RPC îi spune verificării și action și reports ale metodei sale, în x-gateway-action și x-gateway-reports.
Cuvântul required contează. Un include HOCON simplu sare peste un fișier care lipsește. include required(...) îl refuză și îl numește:
rpc-methods.conf: cannot read include file: open rpc-methods.conf: no such file or directory
--rpc-table scrie în plus rutele rezolvate ca JSON, câte un obiect pe linie, cu serviciul, metoda, acțiunea, rapoartele și clusterul, pentru ca un alt program să le citească. În propriul nostru repository, un pas al build-ului scrie rpc-methods.conf din contractele noastre protobuf, iar Proxyfile-ul îl include.
YAML sau HOCON
Ambele forme au aceleași chei, sensuri și reguli și dau aceiași octeți. Alege-o pe cea pe care echipa ta o editează mai ușor.
YAML vine cu un JSON Schema, așa că un editor completează și verifică cheile în timp ce scrii. Pune linia yaml-language-server de mai sus prima în fișier pentru editoarele care folosesc YAML language server, cum e VS Code cu extensia YAML. Într-un IDE JetBrains, adaugă o asociere JSON Schema pentru același URL. proxyfile schema afișează schema, iar fiecare release o conține ca proxyfile.schema.json. Schema descrie doar forma YAML.
HOCON îți dă include-uri și substituții. Un include e citit relativ la fișierul care îl conține. O substituție ca accept = ${auth.send} copiază o valoare a proxyfile-ului în alta. Citește fișierele, niciodată mediul, așa că aceleași fișiere dau aceiași octeți pe orice mașină. Trei reguli de știut: routes e un array, pentru că prima rută care se potrivește câștigă, iar HOCON nu cere unui obiect să-și păstreze ordinea cheilor; o cheie care conține . sau * se pune între ghilimele, ca în "*" = backend; iar un număr între ghilimele e text.
Fișierul gateway-ului nostru e HOCON.
Cum am trecut la proxyfile fără să schimbăm ce face Envoy
Sursa scrisă de mână a gateway-ului nostru e acum un Proxyfile de 169 de linii de HOCON. Dintre ele, 49 sunt comentarii, iar alte 10 au un comentariu lângă valoare. Lângă el stă rpc-methods.conf cu cele 205 metode ale noastre, pe care îl scrie build-ul.
Trecerea nu avea voie să schimbe nicio rută și niciun antet. Așa că a venit cu o dovadă: un test care rulează versiunea publicată proxyfile v1.2.0 pe Proxyfile, parsează în arbori fișierul generat și fișierul pe care îl înlocuiește și cere ca cei doi arbori să fie egali, inclusiv ordinea listelor. Cere și ca tabelul RPC generat să fie cel vechi, octet cu octet. Dovada e temporară. Fișierul vechi a rămas fixture de test până la primul deploy în producție al celui nou, iar acum, când producția rulează fișierul generat, o schimbare ulterioară elimină fixture-ul și testul.
Ce rămâne e verificarea din build. Build-ul descarcă release-ul proxyfile fixat, îl verifică cu fișierul de checksum-uri al release-ului, scrie fișierul de metode și rulează proxyfile render. Un pas al task-ului check din build rulează proxyfile check și eșuează la o editare manuală a envoy.yaml, la o schimbare a Proxyfile-ului fără render și la un fișier de metode editat. Build-ul imaginii rulează apoi envoy --mode validate pe fișierul pe care îl livrează.
Fișierul generat e tot mare: 522.056 de octeți în 1.112 linii, pentru că Envoy are în continuare nevoie de fiecare rută în fiecare mediu. Dar nimeni nu îl editează. Fiecare rută RPC e acum o linie pe mediu, iar cea mai lungă linie are 959 de caractere, așa că un diff al fișierului generat arată rutele care s-au schimbat. Atributele Git îl marchează ca generat, așa că GitHub îl restrânge într-un pull request.
Envoy 1.40 încarcă fiecare exemplu
proxyfile scrie pentru Envoy 1.40. Verificarea obligatorie a repository-ului rulează testele Go. Ele compară arborele scris pentru fiecare exemplu cu unul scris de mână, inclusiv ordinea listelor, țin fișierele din commit egale cu ce scrie render și cer ca formele YAML și HOCON ale fiecărui exemplu să dea aceleași fișiere. Apoi verificarea rulează envoy --mode validate din imaginea Envoy 1.40.2 pe fiecare exemplu generat. Testele dovedesc că fiecare fișier e cel intenționat. Doar Envoy dovedește că se încarcă.
Împreună, exemplele acoperă formatul: examples/full scrie fiecare cheie în afară de address al unui serviciu și de tracing, examples/partial ia cealaltă variantă a fiecărei chei opționale, iar examples/tracing scrie tracing.
Instalare
Fiecare release are un fișier de program pentru Linux și macOS pe amd64 și arm64, cu checksums.txt și proxyfile.schema.json alături. Descarcă-l pe cel pentru sistemul tău, verifică-l și pune-l în path:
curl -fsSLO https://github.com/kitecorp/proxyfile/releases/download/v1.2.0/proxyfile-linux-amd64
curl -fsSLO https://github.com/kitecorp/proxyfile/releases/download/v1.2.0/checksums.txt
shasum -a 256 -c --ignore-missing checksums.txt
install -m 0755 proxyfile-linux-amd64 /usr/local/bin/proxyfile
Pe un Mac cu Apple silicon, fișierul e proxyfile-darwin-arm64. Într-un Dockerfile, copiază programul din imagine, care conține /proxyfile și nimic altceva:
COPY --from=ghcr.io/kitecorp/proxyfile:1.2.0 /proxyfile /usr/local/bin/proxyfile
Cu Go instalat:
go install github.com/kitecorp/proxyfile/cmd/proxyfile@v1.2.0
Apoi proxyfile version afișează proxyfile v1.2.0.
Ce nu face proxyfile
- Scrie configurație pentru Envoy și pentru niciun alt proxy.
- Scrie fișiere. Nu are un pas de deploy și nu e un control plane, așa că nimic nu se schimbă într-un Envoy care rulează până când build-ul tău nu livrează fișierul nou.
- Nu primește configurație Envoy brută, cum am văzut mai sus.
- Presupune că în fața gateway-ului stă un proxy sau un load balancer. Un gateway expus direct internetului are nevoie de o cheie care nu există încă.
- Un serviciu e o adresă IP,
127.0.0.1dacă nu scrii alta, nu un nume de host, pentru că Envoy nu caută niciun nume pentru un cluster static.
README-ul din repository conține tot formatul: fiecare cheie, fiecare regulă, fiecare valoare implicită și motivul fiecăreia.
Întrebări frecvente
Face proxyfile deploy la configurație sau reîncarcă Envoy?
Nu. Scrie fișiere. Pui envoy.yaml generat într-un commit lângă proxyfile, iar build-ul tău îl livrează ca pe orice alt fișier. proxyfile nu are un pas de deploy și nu e un control plane.
Pot pune configurație Envoy brută într-un proxyfile?
Nu. O cheie necunoscută, oriunde ar fi, e o problemă, iar mesajul enumeră cheile cunoscute în acel loc. Tot ce nu are o cheie în format e o valoare implicită, aceeași în fiecare gateway pe care îl scrie proxyfile, iar o altă valoare cere o cheie nouă.
Formele YAML și HOCON dau același rezultat?
Da. Fiecare cheie are același nume, același sens și aceleași reguli în ambele, iar fișierul generat e același, octet cu octet. Numele fișierului decide forma, niciodată conținutul. Un fișier numit Proxyfile sau terminat în .conf e HOCON, iar unul terminat în .yaml sau .yml e YAML.
Depinde rezultatul de mașina pe care rulează?
Nu. Aceeași intrare dă mereu aceiași octeți. Substituțiile HOCON citesc doar proxyfile-ul și fișierele incluse de el, niciodată o variabilă de mediu. Un tag de tracing care numește o variabilă de mediu e citit de Envoy când pornește, nu de proxyfile.
Pentru ce versiune de Envoy scrie proxyfile?
Envoy 1.40. Verificarea obligatorie a repository-ului rulează envoy --mode validate din imaginea Envoy 1.40.2 pe fiecare exemplu pe care îl generează.