Skip to content

feat: Azure Container Apps deployment for tools/httpserver - #306

Merged
gerardrecinto merged 6 commits into
masterfrom
feat/azure-container-apps-production
Sep 10, 2026
Merged

gerardrecinto merged 6 commits into
masterfrom
feat/azure-container-apps-production

Conversation

@gerardrecinto

Copy link
Copy Markdown
Collaborator

Summary

Deploys tools/httpserver (the Data Manager UI + commercial Stripe billing surface) to Azure Container Apps, built around one constraint: keep it least-cost, since this runs on a personal account.

  • Fixed a real bug found along the way: subscriptions, the webhook idempotency map, the customer-tenant map, and enterprise inquiries all lived in plain in-process maps. A restart or redeploy silently wiped every paying customer's subscription. Backed DefaultBillingService with a new governance.JoltrinBillingStore — this repo's own embedded B-Tree engine — instead of adding Redis or another external dependency. See TestDefaultBillingService_SurvivesRestart for the regression test.
  • Wired the existing, previously-unused governance.TierRateLimiter into the checkout/portal endpoints (keyed by client IP), and added bounded retry-with-backoff on Stripe 429/5xx responses.
  • Added a Bicep stack under infra/azure/ (Container Registry, Key Vault, Log Analytics, Container Apps environment/app, cost budget with 50/75/90/100% alerts, Azure Monitor metric alerts).
  • Added .github/workflows/deploy-azure.yml, OIDC-authenticated (no stored client secret), gated by the same Trivy critical/high check as security.yml.
  • Added make deploy-check / make lint-infra.

Architecture decisions (made with the repo owner before implementing)

  • Container App pinned to minReplicas = maxReplicas = 1. joltrin's embedded engine has no documented multi-process write-safety guarantee. It does ship a Redis-backed distributed lock (adapters/redis) that would make horizontal scaling safe, but that's a new billed Azure resource, so it's deliberately not wired in — least cost over replica fan-out.
  • IaC is Bicep, not Terraform — a deliberate choice to build real ARM/Bicep experience rather than more Terraform.
  • Alerting is native Azure Monitor metric alerts, not Managed Prometheus/Grafana, for the same cost reason (that stack has real monthly ingestion/workspace cost).
  • Idempotency/subscription persistence backs onto joltrin's own store, not Redis or an external DB — no new paid resource, dogfoods the product.

Release sequence

This merges to master first; v5.5.0 gets tagged after this is in and CI is green, covering both the DevSecOps pipeline (already on master via #305) and this Azure Container Apps work together.

Test plan

  • go build ./...
  • go vet ./governance/... ./tools/httpserver/...
  • go test ./governance/... ./tools/httpserver/... -count=1 (full suite, including new persistence/retry/rate-limit tests)
  • make deploy-check (Bicep syntax validated in CI where az is available; YAML validated locally)
  • CI: ci.yml / go.yml / security.yml / codeql.yml / e2e.yml all green
  • First deploy-azure.yml run on merge (provisions real Azure infra — needs AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_SUBSCRIPTION_ID repo variables and STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET / ALERT_EMAIL repo secrets configured first; see infra/azure/README.md)

…rin's own store

subscriptions, the customer-tenant map, webhook idempotency keys, and
enterprise inquiries lived only in in-process maps, so a restart or
redeploy silently wiped every paying customer's subscription. Back
DefaultBillingService with a new JoltrinBillingStore (this repo's own
embedded B-Tree engine) instead of adding an external dependency; the
in-memory maps stay as a read cache hydrated from the store at startup.
- Point the billing store at the server's own data path so it survives
  restarts, and isolate httpserver tests from it via TestMain (they were
  about to leak billing state across separate go test runs through a
  relative _billing path in the source tree).
- Wire the existing, previously-unused governance.TierRateLimiter into
  the checkout/portal endpoints, keyed by client IP, so a flood can't
  run up the Stripe API bill. The webhook endpoint stays unthrottled by
  IP deliberately: Stripe's webhook senders share IP ranges, and
  signature verification plus the 1MB body cap already guard it.
- Retry Stripe API calls with bounded backoff on 429/5xx so a single
  Stripe blip doesn't fail a customer's checkout outright; 4xx errors
  other than 429 still fail immediately.
Resource group scoped stack: Container Registry (Basic, admin disabled,
pull via managed identity only), Key Vault (RBAC, holds the Stripe
secret/webhook keys), Log Analytics (30-day retention), a Container
Apps environment, the Container App itself, a monthly cost budget with
50/75/90/100% alerts, and Azure Monitor metric alerts on CPU/memory/
restart-loop.

Pinned to minReplicas=maxReplicas=1: joltrin's embedded B-Tree engine
has no documented multi-process write-safety guarantee, and this
deployment optimizes for lowest cost over horizontal scale (it does
ship a Redis-backed distributed lock in adapters/redis that would make
scaling safe, deliberately not wired in here). Alerting uses native
Azure Monitor metric alerts rather than Managed Prometheus/Grafana for
the same cost reason.
Deploys only on push to master (never on a PR, since it provisions
real billed infra), authenticated via GitHub OIDC federated
credentials rather than a stored client secret. Flow: deploy the
Bicep stack first (it has a public placeholder image so it stands up
before a real image exists), build and Trivy-scan the runtime image
using the same critical/high gate as security.yml, push it to the
now-existing ACR, then roll the Container App onto it with
az containerapp update, which blocks until the new revision's probes
pass.
Mirrors the existing security-scan target's style: validate Bicep
syntax locally when az is installed (skip with a hint otherwise), plus
build/vet/test the billing persistence code path and an optional
az deployment what-if dry run, so problems surface before a push
instead of after deploy-azure.yml runs.
…eaks

The AcrPull and Key Vault Secrets User role definition IDs added in
infra/azure/modules/*.bicep are public, Microsoft-documented constants,
not credentials, but tripped the generic-api-key high-entropy rule.
@gerardrecinto
gerardrecinto merged commit e5e1cf5 into master Sep 10, 2026
19 checks passed
@gerardrecinto
gerardrecinto deleted the feat/azure-container-apps-production branch September 10, 2026 08:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant