Deployment (current state)¶
Companion:
azure(CLI setup),prod-launch-runbook(one-time WC26 launch fixes),prod-deployment-runbook(staging → prod promotion flow).Last verified against measured infra: 2026-06-11. Replaces the legacy May-2026 deployment doc which referenced a
developmentbranch that no longer exists.
1. Branch flow¶
gitGraph
commit id: "release/staging"
branch dev-amal-integration-staging-<date>
checkout dev-amal-integration-staging-<date>
commit id: "cut from release/staging --no-track"
branch feat/your-feature
checkout feat/your-feature
commit id: "work"
checkout dev-amal-integration-staging-<date>
merge feat/your-feature id: "PR → integration"
checkout release/staging
merge dev-amal-integration-staging-<date> id: "PR → release/staging"
commit id: "CI: migrate + roll + smoke"
branch staging-to-prod-release-<date>
checkout staging-to-prod-release-<date>
commit id: "cut from release/staging"
checkout release/staging
branch prod-integration-<date>
checkout prod-integration-<date>
commit id: "cut from release/prod --no-track"
merge staging-to-prod-release-<date> id: "PR 1"
checkout release/prod
merge prod-integration-<date> id: "PR 2 — gated by reviewer"
commit id: "CI: migrate + approval + roll prod"
| Branch | Cut from | Convention |
|---|---|---|
feat/<slug> |
integration branch | new features |
bug/<slug> |
integration branch | bug fixes |
dev-amal-integration-staging-<date> |
release/staging |
weekly integration branch |
release/staging |
(long-lived) | CI integration target |
staging-to-prod-release-<date> |
release/staging |
promotion-only, no own commits |
prod-integration-<date> |
release/prod |
review seam for prod cutover |
release/prod |
(long-lived) | production deploy target |
main is NOT the integration branch
main is unused for day-to-day work. Cut feature branches off the dated integration branch (which itself is cut off release/staging with --no-track). Never branch off main.
Rules:
- Cut every
feat//bug/branch off the current dated integration branch, never offrelease/stagingormain. --no-trackon the integration cut so the branch can never accidentallygit pushontorelease/staging. Pushing requires an explicitgit push -u origin <branch>.- One branch = one scope. Branch again if scope expands.
- Push to
originimmediately after the first meaningful commit. - Every PR description includes
Author: @<github-handle>.
For the staging → prod flow specifically, see prod-deployment-runbook.
2. Services¶
Three Go microservices on Azure Container Apps in centralindia. Replica counts below are measured 2026-06-11 on prod (ftl-prd-rg-cin).
| Service | Container App | vCPU / Memory | min / max replicas | Ingress |
|---|---|---|---|---|
| api-server | ftl-prd-api |
0.5 vCPU / 1 GiB | 1 / 2 | external |
| ws-server | ftl-prd-ws |
1.0 vCPU / 2 GiB | 1 / 2 | external |
| flusher | ftl-prd-flusher |
0.5 vCPU / 1 GiB | 1 / 1 | none (internal) |
Staging (ftl-stg-rg-cin):
| Service | Container App | vCPU / Memory | min / max | Ingress |
|---|---|---|---|---|
| api-server | ftl-stg-api |
0.5 vCPU / 1 GiB | 1 / 2 | external |
| ws-server | ftl-stg-ws |
0.5 vCPU / 1 GiB | 1 / 2 | external |
| flusher | ftl-stg-flusher |
0.25 vCPU / 0.5 GiB | 1 / 1 | none |
| whatsapp-bot | ftl-stg-whatsapp-bot |
0.25 vCPU / 0.5 GiB | 1 / 3 | external |
| frontend | ftl-stg-frontend |
0.25 vCPU / 0.5 GiB | 1 / 1 | external |
No frontend / whatsapp-bot Container Apps on prod today — the frontend is served by Azure Static Web App
ftl-prd-frontend(Free SKU). Seeprod-snapshot-2026-06-11.
Decision: Custom Go WebSocket server on Container Apps rather than Azure Web PubSub — 15× cheaper at 50K concurrent connections.
Decision: Redis Lua atomic scripts over a message queue — single-threaded execution, zero lock contention, no Service Bus cost.
3. Ingress endpoints¶
Public DNS names (custom domains) and Container Apps system FQDNs:
| Service | Custom domain (use this) | Azure FQDN (fallback) |
|---|---|---|
| api | https://prd-api.ftljeta.cloud |
https://ftl-prd-api.livelywave-a872f0a8.centralindia.azurecontainerapps.io |
| ws | wss://prd-ws.ftljeta.cloud |
wss://ftl-prd-ws.livelywave-a872f0a8.centralindia.azurecontainerapps.io |
| frontend (SWA) | https://ftl.jetafutures.com |
https://blue-grass-01f012000.7.azurestaticapps.net |
| Service | Custom domain (use this) | Azure FQDN (fallback) |
|---|---|---|
| api | https://api.ftljeta.cloud |
https://ftl-stg-api.kindfield-0bb54ed4.centralindia.azurecontainerapps.io |
| ws | wss://ws.ftljeta.cloud |
wss://ftl-stg-ws.kindfield-0bb54ed4.centralindia.azurecontainerapps.io |
| frontend (SWA) | https://yellow-sand-0d6f5a100.7.azurestaticapps.net |
same |
Prod uses the
ftljeta.cloudapex; staging shares it without theprd-prefix. Domain shape is different per environment — using the prod URL against staging or vice versa will hit the wrong system. Smoke and runbook calls must use the env-correct domain.
4. Resource group + Container Apps Environment¶
| Environment | Resource group | Container Apps Environment |
|---|---|---|
| Staging | ftl-stg-rg-cin |
ftl-stg-cae-cin |
| Production | ftl-prd-rg-cin |
ftl-prd-cae-cin |
Both environments use the Consumption workload profile; not zone-redundant. Same centralindia region.
5. Single-revision mode requirement¶
Always confirm Single-revision mode before scaling
Running scale-down.sh / scale-up.sh (or any az containerapp update) against an app in Multiple revision mode creates a new revision on every call. Old revisions keep their original minReplicas and bill forever.
2026-05-02 incident reference: ftl-stg-api accumulated 9 revisions (10 always-warm replicas) after repeated az containerapp update calls. Actual burn was ₹1,234/day vs the ₹150/day plan. See azure-cost-audit-2026-05-02.md for the post-mortem.
Verify on every app:
for app in ftl-prd-api ftl-prd-ws ftl-prd-flusher; do
AZURE_CONFIG_DIR=~/.azure-ftl az containerapp show \
-g ftl-prd-rg-cin -n "$app" \
--query 'properties.configuration.activeRevisionsMode' -o tsv
done
# Expected: 'Single' on every line.
If Multiple, fix it:
AZURE_CONFIG_DIR=~/.azure-ftl az containerapp revision set-mode \
-g <resource-group> -n <app-name> --mode Single
Then deactivate non-traffic revisions:
# List
AZURE_CONFIG_DIR=~/.azure-ftl az containerapp revision list \
-g <resource-group> -n <app-name> \
--query "[?properties.active].name" -o tsv
# Deactivate each non-live
AZURE_CONFIG_DIR=~/.azure-ftl az containerapp revision deactivate \
-g <resource-group> -n <app-name> --revision <revision-name>
6. Scale-to-zero overnight (STAGING ONLY)¶
Staging runs scale-down.sh / scale-up.sh to cap idle burn. These scripts MUST NOT be pointed at prod — they stop the Postgres flex server and set all Container Apps to minReplicas=0, which would kill prod.
# Night — staging only
AZURE_CONFIG_DIR=~/.azure-ftl bash ftl-infra/scripts/scale-down.sh
# Morning — staging only
AZURE_CONFIG_DIR=~/.azure-ftl bash ftl-infra/scripts/scale-up.sh
What they do:
scale-down.sh— stops the Postgres Flex server and sets--min-replicas=0on staging Container Apps.scale-up.sh— starts Postgres, waits 60 s, then sets--min-replicas=1onftl-stg-api,ftl-stg-ws,ftl-stg-flusher, andftl-stg-frontend.
Azure Flex auto-restart
Azure auto-restarts a stopped Flex server after 7 days. The daily night-time stop resets that clock — the 7-day limit is never reached during active development.
7. Staging cost reference (measured)¶
Snapshot of the staging RG cost over the last 11 days (June 1–11, 2026):
| Service | MTD INR | Share |
|---|---|---|
| Redis Standard C1 | 1 253 | 36 % |
| PostgreSQL D2ds_v5 | 1 021 | 29 % |
| Container Apps | 1 017 | 29 % |
| ACR Basic | 158 | 5 % |
| Everything else (KV, IPs, etc.) | 35 | 1 % |
| Total MTD | ~3 484 | 100 % |
Detailed cost model + per-tier projections: cost-estimate-audit-2026-06-11.
8. CI pipeline overview¶
flowchart LR
A[push to release/staging] --> B[lint + test]
B --> C[build + push 4 images]
C --> D[migrate job]
D --> E[roll api / ws / flusher]
E --> F[health smoke]
F --> G[staging live]
H[push to release/prod] --> I[lint + test]
I --> J[build + push 4 images]
J --> K[production env approval gate]
K --> L[migrate job]
L --> M[roll api / ws / flusher]
M --> N[health smoke]
N --> O[prod live]
Each release/staging push fires deploy-staging.yml; each release/prod push fires deploy-prod.yml and pauses at the production GitHub Environment approval gate (see github-env-audit-2026-06-11 — required reviewers must be enabled before the gate has any effect).
After approval, CI runs migrations against prod Postgres, then rolls the three Container Apps in parallel, then smokes /health.
9. What's next (action items)¶
For the immediate WC26 launch sequence see today-2026-06-11. For one-off prod fixes (PG SKU, Redis tier, env vars) see prod-launch-runbook. For the staging → prod promotion flow see prod-deployment-runbook.