Repository navigation
Replies: 2 comments
Revision 1: the charter's revisions 14 and 15, and what the job and mailer walkthroughs foundThe charter's revisions 14 and 15 change this walkthrough as their tables say: the module revision 15's story made, The job and RabbitMQ processor walkthroughs have since landed, and their code found more that holds here:
The Kafka processor walkthrough's revisions 1 and 2, and the RabbitMQ processor walkthrough's revision 1, made two choices that hold here too:
Open questions 1 to 3 are still open. This revision adds none. Each section below replaces or adds to the part of the same name in the post. Everything else is unchanged. SummaryIn the bullet "It is production-ready by the bar revision 13 sets", its last sentence becomes: "They drain on SIGTERM, send traces, logs and metrics over OTLP, and ship Grafana alert rules and a dashboard." The bullet on the three levels becomes:
This bullet is added:
The applicationThe tree becomes: The module is The codeThis section is added.
The cart's APIThe list under "A dependency that is down is a 503, never a 500" ends:
The catalog
MigrationsThe last bullet becomes:
The settingsIn the bullet "Telemetry is sourced too", "Its resource takes the pod's name and namespace from the downward API" becomes: "Its resource is "Each Trying it by handThis section is added.
It reads the private key from a PEM file. Production-ready, as a checklistThe checklist gains a third column, Where, linking each item to the page and section that shows it, as the job's and the mailer's do. These rows change:
The testsThese rows change, and the last two are added:
The abort cases also spend the retry budget, which a success refills by a tenth of a token. So a later case may see fewer attempts than Against a shared staging environment, the last This case is added, to
The deploymentThis section is replaced.
The kind overlay,
The kind job runs it, and so does a reader. No secret is kept in the repository. The kind job builds the image, runs The Dockerfile builds the cart, the catalog and AlertsThis section is replaced.
The pagesThis section is replaced. They are under
Changes to other filesThis section is replaced.
Design decisionsThe bullet "The kind overlay serves a static key set, not an identity provider" becomes:
These are added:
TestingThis is added: The prototype ran before the charter's revisions 14 and 15. It didn't try WireMock, Open questionsQuestions 1 to 3 are still open. Out of scope"A second version of the schema in the walkthrough's code. The pages describe expand then contract instead." becomes:
These are added:
ReferencesThese are added:
|
Revision 2: the open questions answered, and a catalog over HTTPThe open questions:
The catalog is still a service of the walkthrough's own, as option (c) had it, not WireMock. It is deployed from Each section below replaces or adds to the part of the same name in the post, as revision 1 left it. Everything else is unchanged. Summary"This proposal fixes exactly those, and takes three things #762, #763 and #870 handed to the walkthroughs" becomes: "This proposal fixes exactly those, and takes two things #762 and #870 handed to the walkthroughs: a service calling another end to end, and authentication in a production-ready service." The bullet "Prices come from a catalog, over gRPC" becomes:
The diagram becomes: BackgroundThe walkthrough takes three of the four things waves 4 and 5 handed it: a service calling another end to end, from #762; authentication in a production-ready service, from #870; and failure injection shown in a walkthrough, from #869. #763's, a service that calls gRPC, goes to a gRPC walkthrough. The applicationIn the tree,
The module gains only The codeThe sentence on the The cart's API" In the list under "A dependency that is down is a 503, never a 500", the catalog's item becomes:
The catalogThis section is replaced. An
Calling the catalogThis section is replaced. The cart builds one
It sets
The settings
ReadinessThe catalog's monitor becomes:
"Both latch, as This is added:
Failure injectionIn the table, the cart's points become: In the bullet "A scenario aimed at the catalog's database goes through the cart", "The catalog's query fails, "Open question 3." is dropped from the first bullet. Production-ready, as a checklistIn the row "A rollout loses no request", " The testsIn the Units row, "the catalog's methods" becomes "the catalog's operations, on a fake The Target becomes:
"It does import These cases change:
In the failure injection cases, and in revision 1's The deploymentThe catalog's Deployment probes its own port over HTTPS, as the cart's does. Alerts
The dashboard shows the catalog's requests by status, in place of its calls by code. The pagesPages 5 and 6 become:
Changes to other files
Design decisionsThe bullet "The catalog is a real service, not a stand-in" becomes:
"Open question 2." and "Open question 3." are dropped from their bullets. This is added:
TestingThis is added: The prototype's cart called a gRPC catalog. Its readiness results were The HTTP path the scenario now takes wasn't prototyped. Each half of it is what Failure injection's own test runs (Through a humustest Host): an Open questionsThis section is replaced. None. The three above are answered. Out of scopeThis is added:
"Credentials on the cart's call to the catalog beyond its client certificate, which #870 left to service code and ReferencesProposal: gRPC services and Proposal: outbound gRPC clients are dropped, here and from what the walkthrough builds on. buf is dropped from the tools. This is added: |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Note
Part of the humus re-evaluation: wave 6 of Proposal: humus on the bedrock process model, which plans one proposal per walkthrough. This walkthrough keeps the points every walkthrough keeps, set by its revision 13, and doesn't ask them again: how it's packaged, where its deployed tests run, where its code lives and how its alerts are written are that revision's to decide.
It builds on proposals that have all landed: OpenAPI endpoint DSL, whose shoppingcart it turns into the walkthrough, authentication and authorization, gRPC services, outbound gRPC clients, outbound HTTP clients, exposing health probes, failure injection and humus's own metrics. It depends on no other walkthrough.
Summary
A walkthrough is a production-ready application, kept in this repository, with pages that walk a reader through how it was built and why. This one is an API: the shoppingcart, made production-ready, deployed, and explained.
#842 and #886 built the shoppingcart as humus's reference service: six typed operations on
openapi/v3, on PostgreSQL, behind JWT bearer tokens. #760 handed it to the walkthroughs. What it lacks is everything between a service that passes its tests and one that someone runs:addCartItemstores theunitPriceits body carries.schema.sqlsays so.This proposal fixes exactly those, and takes three things #762, #763 and #870 handed to the walkthroughs: a service calling another end to end, a service that calls gRPC, and authentication in a production-ready service.
grpcserver. The cart asks it for a product's price withgrpcclient, over mTLS, and keeps the price in the cart. The catalog's writes need a token withcatalog:write, throughgrpcauth.Background
The shoppingcart is the reference service, not yet a walkthrough. It shows the pieces of
openapi/v3andauthtogether, with whole-App tests and a container test (app/app.go). It has no deployment, no failure injection and no pages of its own. OpenAPI endpoint DSL said the walkthroughs would build on it.Its gaps are real ones:
AddItemRequesttakesunitPricefrom the client.Store.GetCarttakes only a cart ID, so a token withcarts:writecan delete anyone's cart.schema.sql: "The service applies no migrations".BuildTelemetrykeepstelemetry.DefaultSettings(), so it exports no traces or metrics.DATABASE_URLandTLS_PKCS12_PASSWORDcarry secrets in the environment.v0.15.0 had an orders REST walkthrough, which the reset deleted (its index,
example/rest/orders-walkthrough). It orchestrated three backend services that Wiremock mocked, and ran an observability stack with podman-compose. It was never deployed, and its backends were never real.Waves 4 and 5 handed this walkthrough four things:
Probes, from Proposal: exposing health probes #761, and alert rules on humus's own instruments, from Proposal: humus's own metrics #871, go to every walkthrough.
Failure injection across services needs a service of the end user's on the other side.
ForwardScenariosends a scenario on only to a dependency that honors it with the same codec (Failure injection). A dependency that has points of its own is the only way to show a scenario reaching them.Proposed API
A walkthrough adds no API to humus. What it adds is an application, its manifests, its tests and its pages.
The application
It stays at
example/shoppingcart, in theexamplemodule. The module gainsgithub.com/z5labs/humus/grpc, replaced with../grpcas the core module is, andgithub.com/jackc/tern/v2.The cart's API
The six operations stay, with their paths, codecs and answers. Three things change.
Prices come from the catalog.
AddItemRequestisproductIdandquantity.addCartItemasks the catalog for the product withGetProduct, and stores its price with the item, so a later change of price doesn't change a cart. A product the catalog doesn't have is a 422Invalid.A cart is its owner's.
ClaimsgainsSubject, the token'ssub. EveryStoremethod takes the owner, and a cart whose owner isn't the caller isErrCartNotFound: a 404, so the API never says whose carts exist.A dependency that is down is a 503, never a 500. A new answer,
Unavailable, is a problem detail at 503 that says to try again, given withopenapi.Error:Unavailable,DeadlineExceeded, or a call its breaker rejected;Storewraps asdatabase.ErrUnavailable, which it does for a connection that failed, and for its injection point's abort.The catch-all still answers anything else with a 500, and records it.
The API's handler is wrapped in
http.MaxBytesHandlerat 64 KiB, so a body over it is the API's 413. A cart's bodies are a few dozen bytes.The catalog
A gRPC service,
catalog.v1.Catalog, with two methods:grpcserver, over TLS, withClientAuth: tls.VerifyClientCertIfGivenand the internal CA asClientCAs.grpcauthdecides who may call what:GetProductrequiresauth.MutualTLS: a caller with a certificate the internal CA issued, which is the cart.PutProductrequires a bearer token withcatalog:write, verified byauth/jwtagainst the same identity provider as the cart.grpchealth.NewService, on a plaintext port of its own. The Service takes client certificates, and the kubelet'sgrpcprobes use plain text on most clusters (Health).postgresql:catalog, evaluated before each query, as the docs suggest naming one. An abort fails the query as an unreachable database does, andGetProductendsUnavailable.The generated code is checked in, made by
buf generatefromcatalog/proto, whosebuf.gen.yamlpins protoc-gen-go and protoc-gen-go-grpc. The walkthrough's container job generates it again, and fails on a diff.Calling the catalog
The cart builds one
grpcclient.Clientfor the catalog, fromCATALOG_ADDRandCATALOG_CA_FILE, with its client certificate for mTLS. It keepsDefaultSettings()' retries, budget, breaker andClassify:GetProductis a read, so retrying it onUnavailableis safe;NotFoundis the catalog working, so it counts as a success to the breaker.It sets
ForwardScenario, because the catalog is the walkthrough's own and reads the same codec.Migrations
Each service's schema is a directory of versioned SQL migrations, embedded in its
databasepackage. They replaceschema.sql.migrateis a program of its own: a humus job.pgx.Connthrough the Host.Migratoron the migrations itsMIGRATIONSsetting names,cartsorcatalog.APP_ROLEsetting names, read and write on the schema's tables, and nothing more.The settings
Every setting is sourced with
config.Structand checked, and a bad one exits 78, naming its variable. Every secret is a file, read throughs.Host.FS:DATABASE_URL_FILE. The URL still sets every field pgx uses, so pgx finds nothing in the real environment or~/.pgpass, as the shoppingcart's package doc explains today. The deployment connects withsslmode=verify-full.TLS_CERT_FILEandTLS_KEY_FILE, which is what cert-manager writes to a Secret. They replaceTLS_PKCS12_FILEand its password.internal/certfileserves them throughtls.Config.GetCertificate, and the cart's client certificate throughGetClientCertificate. It reads them again when their files change, and checks at most every 30s. So a renewed certificate is served without a restart.httpserver.CheckTLSandgrpcserver.CheckTLSalready accept a config withGetCertificatealone.Builderreads the specification'sOTEL_*variables thattelemetry.Settingsnames, such asOTEL_TRACES_EXPORTERandOTEL_EXPORTER_OTLP_ENDPOINT, as Telemetry suggests. Its resource takes the pod's name and namespace from the downward API.FAULT_TRUST_SCOPEnames the scope a token needs for the cart to trust its scenario. It is empty by default, so production trusts no one.Each
apppackage's doc lists every variable its service reads, as today's does.Readiness
The cart's probes are attached to its own port, with
httphealth.NewHandlerat/health/, beside the API. The kubelet probes it over HTTPS, and nothing wraps it, so it stays open while the API requires tokens. Its readiness ishealth.Andof two monitors:Client'sConnected.Both latch, as
Connecteddoes, for the reason Calling gRPC services gives:The catalog's readiness latches on its own PostgreSQL the same way.
Failure injection
Both services pass
humus.WithFaults(Faults), withjsonscenariounder the baggage memberacme.fault, the Failure injection page's key, which every walkthrough uses.Verifier, whose scopes holdFAULT_TRUST_SCOPEpostgresql:carts, of its own;grpc:catalog.v1.Catalog/GetProduct, itsgrpcclient;http:<key set host>:<port>, its key set'shttpclientpostgresql:catalog, of its ownForwardScenarioTrustScenarioverifies the token itself, only for a request that carries a scenario. Open question 3.GetProductendsUnavailable, and the cart's client retries it, meets the same fault, and gives up. The cart answers 503. The trace shows every attempt, and the catalog'shumus.fit.evaluationevents.Production-ready, as a checklist
The walkthrough's pages close with this list, and the application meets each item:
config.Structand the packages' checks, in all three programss.Host.FS; no handler logs a tokeninternal/certfile, on cert-manager's SecretsminAvailable: 1, and a rolling update ofmaxUnavailable: 0httpserver.Main's andgrpcserver.Main's 5s drain, andterminationGracePeriodSecondsabove the drain, the grace period and the telemetry's shutdownmigrateinit container, under tern's lock, with backwards-compatible migrationsdeploy/alerts.yamlFAULT_TRUST_SCOPEempty; the OpenAPI document the only open path besides the probesThe tests
go test, in the Build jobStoreandCatalog; the catalog's methods;internal/certfileon a humustest Host, rewriting its files; the cart'sTrustScenario; eachStore's injection pointtestcontainerstagStoreagainst PostgreSQL in a container, with its schema applied bymigrate's ownInit; twomigrateruns at oncego test, in the Build jobtestcontainerstag, in a CI job that isn't requiredhumus.OSHost(), against PostgreSQL in a container, migrated bymigratefunctionaltag, in a CI job that isn't required, and against an end user's stagingThe whole-App level runs twice. The cart's state is a
Storethe walkthrough already fakes in memory, so its cases can run inside synctest in the Build job too, in a few milliseconds, besides against PostgreSQL. Other whole-App tests stay inapp: a bad setting exits 78, readiness waits for both dependencies, and the telemetry carries the evaluations and strips, read withhumustest.Telemetry.The shared cases are in
cases. It imports neither the application nor humustest, so the compiler keeps them black-box. It does importcatalogpb, the catalog's published contract, as any client of it would. Each case takes aTarget:*http.Clientthat trusts it;catalogpb.CatalogClient;They poll with plain loops, which work in both harnesses. Each case makes its own subjects and products, named with
crypto/rand, so the cases can run in parallel and against a shared staging environment. They mutate, so they never run against production. The cases:PutProductchanges it, the item keeps its price and a new item gets the new one.carts:writegets a 403 on a change. No token gets a 401, except for/openapi.json.PutProductwithoutcatalog:writeisPermissionDenied.grpc:catalog.v1.Catalog/GetProduct: adding an item is a 503, the cart is unchanged, and the next add without the scenario succeeds, polled until the breaker, if it opened, lets it through;postgresql:catalog, forwarded to the catalog: adding an item is a 503;grpc:catalog.v1.Catalog/GetProductby 2s: the add still succeeds;postgresql:carts: reading the cart is a 503 that says to try again, and the next read succeeds;The deployment
deploy/is a kustomization:automountServiceAccountToken: false;migrateinit container, probes, a startup probe, resources, a non-root, read-only security context, and the downward API;kubernetes.io/tlsSecrets, the database URLs, and the CA bundles;alerts.yaml, the Prometheus rules.The kind overlay adds what a cluster would provide:
FAULT_TRUST_SCOPEset to the cases' scope.The kind job builds the image, loads it into the cluster, applies the overlay, waits for both rollouts, and runs the cases through NodePorts that the cluster's config maps to the runner.
The Dockerfile builds the three programs as static binaries, onto a distroless, non-root base. Each Deployment runs its program as the container's command.
Alerts
deploy/alerts.yamlholds the services' alert rules, which CI checks with promtool. The pages explain each one, and what to look at first:http.server.request.duration's count, by statushttp.server.request.durationrpc.server.call.duration's count, by status codehumus.circuit.statehumus.circuit.transitionshumus.retry.decisions,throttledThe pages
A walkthrough's pages go in the Walkthroughs section that revision 13's story adds. This one's are
docs/content/walkthroughs/shoppingcart/, in the order it was built:grpcserver, mTLS andgrpcauth, and its probes.grpcclient, its retries and breaker, and turning its failures into answers.Init, the settings, secrets and certificates as files, readiness, telemetry andFaults.Changes to other files
docs/content/openapi.md: its section on the shoppingcart links to the walkthrough, and keeps only what is aboutopenapi/v3. The samples it quotes follow the code..github/workflows/build.yaml:catalogpbagain, failing on a diff;functionaltag too.example/go.mod: thegrpcmodule, replaced with../grpc, and tern.Design decisions
kubectl apply. The services can't change their schemas. tern is pgx's author's migrator: it takes apgx.Conn, which dials through the Host as the pool does, and reads its migrations from anfs.FS, so they're embedded. Open question 2.Storealready exists for its unit tests. Running the cases there too gives the Build job every case in milliseconds, with fake time for the breaker's wait.Testing
The walkthrough is its own test: see The tests. Two of the claims it rests on were prototyped, in a module that replaces humus with this repository at 1bb3bb5:
go test -race -count=3. Anhttpservercart called agrpcservercatalog withgrpcclientover mTLS, with grpc-go's interop service standing in forcatalogpb.jwt.VerifierinsideTrustScenario, reached the catalog. The catalog's ownpostgresql:catalogpoint aborted each of the 3 attempts thatDefaultSettings().Classifymade, and the cart answered 503.ForwardScenariooff, the scenario was stripped at the client. Aborting the cart's owngrpc:…point sent the catalog nothing.health.AndwithConnected, attached withhttphealth.NewHandlerto the cart's port, was 503 until the catalog listened, then 200.Connected's check waits for the connection. In synctest that is a test that never ends, as grpc-go's reconnect backoff keeps fake time moving. The kubelet bounds every probe withtimeoutSeconds, and the tests bound theirs the same way.pgx.Connthat dialed through aDialFuncof the test's. Both succeeded, each migration ran once, and a third run did nothing, under-race -count=3.Migration from today
The shoppingcart changes, and has no clients outside this repository:
addCartItem's body losesunitPrice; the golden document changes with it.TLS_PKCS12_FILEandTLS_PKCS12_PASSWORDbecomeTLS_CERT_FILEandTLS_KEY_FILE, andDATABASE_URLbecomesDATABASE_URL_FILE. It gains the catalog's, the telemetry's and the faults' settings.schema.sqlbecomes the cart's first migration.Open questions
The gRPC dependency.
httpclient. That leaves Proposal: outbound gRPC clients #763's handoff to no walkthrough.Recommendation: (a).
How migrations are applied.
kubectl apply, or a tool that orders them, such as Argo CD's sync waves.Init, under the same lock. It needs no second program, but the service's role must be able to change its schema.Recommendation: (a).
Whom the cart trusts with a scenario.
FAULT_TRUST_SCOPE, as proposed.Recommendation: (a).
Out of scope
createCartandaddCartItem.opts.References
All reactions