Sixteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack.
The Environment Problem
Any non-trivial application needs multiple environments: development, test, production. In a large enterprise application, feature branches ideally each get an isolated environment so developers can share a live preview without blocking one another.
Configuration is where the real complexity hides. Three environments × four services × dozens of environment variables × secrets × scaling rules = hundreds of values that must be correct for every combination. Managing this by hand inevitably leads to deployment failures from miscopied connection strings or wrong environment variables.
The Architecture: Three Environments on Azure Container Apps
A typical setup runs three distinct environment types, all on Azure Container Apps (ACA):
flowchart LR
subgraph ACA[Azure Container Apps Environment]
direction LR
subgraph FE[Feature Branches]
direction TB
FE_Title[Per-branch:]
FE_SPA[SPA]
FE_API[API]
FE_Proxy[Proxy]
FE_Redis[Redis]
FE_Iso[Isolated per branch]
end
subgraph TEST[Test]
direction TB
T_Title[Shared:]
T_SPA[SPA]
T_API[API]
T_Proxy[Proxy]
T_Redis[Redis]
T_Notes[Stable integration]
end
subgraph PROD[Production]
direction TB
P_Title[Shared:]
P_SPA[SPA]
P_API[API]
P_Proxy[Proxy]
P_Redis[Redis]
P_Notes[Live traffic]
end
end
Feature Environments
In many teams, every feature branch gets its own fully isolated deployment: SPA, API, proxy, and Redis containers. The CI/CD pipeline provisions on git push and tears everything down when the branch is deleted.
- Developers share a live URL within minutes of pushing
- No shared test environment lock — multiple features can be tested in parallel
- Full isolation — one branch’s bugs never impact another
flowchart LR
subgraph Dev[Developer Workflow]
direction LR
A[git push to feature branch]
B["CI/CD: provision\nFeature Environment\n(SPA, API, Proxy, Redis)"]
C["Share live URL\nfor review & QA"]
D[Branch merged\nand deleted]
E[CI/CD: teardown\nFeature Environment]
A --> B --> C --> D --> E
end
Per-Branch Redis
Each feature branch gets its own Redis container. The pipeline rewrites Redis connection strings in the manifests at deploy time, preventing any cross-branch cache pollution.
flowchart LR
subgraph BranchA[Feature Branch A]
A_API[API A]
A_R[Redis A]
A_API --> A_R
end
subgraph BranchB[Feature Branch B]
B_API[API B]
B_R[Redis B]
B_API --> B_R
end
style BranchA fill:#e8f5e9,stroke:#2e7d32
style BranchB fill:#e3f2fd,stroke:#1565c0
The Configuration Generator
Manually managing hundreds of configuration values does not scale. A YAML-based configuration system can generate all deployment artifacts from a single source of truth.
Configuration Merge Order
Configuration values are defined in layers, where later layers override earlier ones:
flowchart TB
L1["Layer 1:\nvalues.yaml\n(base + test defaults)"]
L2["Layer 2:\nenvironments/production.yaml\n(production overrides)"]
L3["Layer 3:\nplatforms/container-apps.yaml\n(platform defaults)"]
L4["Layer 4:\nplatforms/container-apps.production.yaml\n(platform × env)"]
M[⭣\nMerged configuration object]
O1[Container Apps\nJSON manifests]
O2[Azure Bicep\nparameter files]
O3[Pipeline\nvariable files]
L1 --> L2 --> L3 --> L4 --> M
M --> O1
M --> O2
M --> O3
To add a new environment variable, define it once in values.yaml with a default. If production needs a different value, override it in environments/production.yaml. The generator merges all layers and emits the final artifacts.
Generated Artifacts
The configuration generator produces three kinds of outputs:
| Artifact | Purpose | Example |
|---|---|---|
| Container Apps manifests | Complete container spec (env vars, secrets, scaling) | container-apps/spa.test.json |
| Bicep parameter files | Infrastructure parameters (environment name, region) | container-apps/my-app.test.bicepparam |
| Pipeline variable files | CI/CD variables (image tags, resource names) | variables/common.yml |
flowchart LR
SRC["Single source of truth\n(YAML config)"]
GEN[Configuration generator]
MAN[Container Apps\nJSON manifests]
BICEP[Bicep parameter files]
VARS[Pipeline variable files]
SRC --> GEN
GEN --> MAN
GEN --> BICEP
GEN --> VARS
Separation of Infrastructure and Application Configuration
A critical design choice in large systems: infrastructure and application configuration are treated as separate concerns.
flowchart LR
subgraph Infra["Infrastructure (Bicep)"]
I1[Container Apps Environment]
I2[Application Insights]
I3[Other platform resources]
I_Mgr["Managed by:\nInfrastructure pipeline\n(runs rarely)"]
end
subgraph AppCfg["Application (JSON Manifests)"]
A1[Container image + tag]
A2[Environment variables]
A3["Secrets (Key Vault refs)"]
A4[Scaling rules]
A5["Resource limits (CPU/RAM)"]
A6[Ingress configuration]
A_Mgr["Managed by:\nBuild/deploy pipeline\n(runs every deployment)"]
end
Infra -->|"Provides infrastructure\nendpoints & resources"| AppCfg
Infrastructure — the Container Apps Environment, monitoring, and other platform resources — changes rarely and is defined with Bicep. Application configuration — environment variables, secrets, scaling rules, resource limits — changes with each deployment and lives in generated JSON manifests.
Risky, infrequent infrastructure changes are decoupled from routine application releases that run multiple times per day.
Secret Management
Sensitive values — connection strings, API keys, encryption keys — never live in Git. Secrets are stored in Azure Key Vault and referenced by name in the manifests:
Manifest (in Git):
env:
- name: NUXT_REDIS_CONNECTION_STRING
secretRef: redis-connection-string ← reference, not value
Key Vault:
redis-connection-string = "redis://host:6379,password=..."
← actual value
flowchart LR
subgraph Git[Git Repo]
M[Manifest\nsecretRef: redis-connection-string]
end
subgraph KV[Azure Key Vault]
S[Secret:\nredis-connection-string\n= actual value]
end
subgraph ACA[Azure Container Apps Runtime]
R[Container\nat startup]
end
M -. reference name .-> R
S -. value resolution .-> R
The Container Apps runtime resolves these references at startup. Secret values never show up in CI/CD logs, Git history, or committed manifests.
Runtime Placeholders
Some values are only known at deploy time — for example, the image tag (from the build) or the Application Insights connection string (from infrastructure). Placeholders handle these late-bound values:
Manifest template:
image: myregistry.azurecr.io/spa:__IMAGE_TAG__
env:
- name: APPLICATIONINSIGHTS_CONNECTION_STRING
value: __APPINSIGHTS_CONNECTION_STRING__
Deploy pipeline substitution:
jq '.properties.template.containers[0].image |=
gsub("__IMAGE_TAG__"; "20260602.3")' manifest.json
sequenceDiagram
participant B as Build
participant P as Deploy Pipeline
participant M as Manifest Template
participant ACA as Azure Container Apps
B->>P: Produce image tag\n(e.g. 20260602.3)
P->>M: Load manifest template\nwith __IMAGE_TAG__ / __APPINSIGHTS_CONNECTION_STRING__
P->>P: Use jq to substitute\nplaceholders with real values
P->>ACA: Apply concrete manifest
ACA->>ACA: Run container with\nresolved image & settings
The pipeline replaces placeholders at deploy time using jq. Manifests remain deterministic — the same manifest plus different placeholder values yields different environments.
Blue-Green Deployments
Test and production environments commonly use blue-green deployments: the new version is deployed alongside the old one, validated, and then traffic is switched.
flowchart TB
subgraph Before[Before]
BO["Old Revision\n(v20260601)\n100% traffic"]
end
subgraph During[During Deploy]
DO["Old Revision\n(v20260601)\n100% traffic"]
DN["New Revision\n(v20260602)\n0% traffic\n(warming up)"]
end
subgraph After[After Validation]
AO["Old Revision\n(v20260601)\n0% traffic\n(standby)"]
AN["New Revision\n(v20260602)\n100% traffic"]
end
subgraph Rollback[Rollback]
RO["Switch traffic back\nto old revision\n(instant)"]
end
The old revision remains deployed at 0% traffic. Rolling back is a single traffic flip — no new deployment, effectively sub-second rollback.
Fully Isolated Chains
Both versions run side by side during the transition. To avoid mixed-version states (for example, a new SPA calling an old API), environment variables are rewritten at deploy time to point to revision-specific hostnames:
Old Chain: Old Proxy → Old SPA → Old API (all on main hostnames)
New Chain: New Proxy → New SPA → New API (all on revision hostnames)
flowchart LR
subgraph Old["Old Chain\n(main hostnames)"]
OP[Old Proxy]
OS[Old SPA]
OA[Old API]
OP --> OS --> OA
end
subgraph New["New Chain\n(revision hostnames)"]
NP[New Proxy]
NS[New SPA]
NA[New API]
NP --> NS --> NA
end
style Old fill:#fff3e0,stroke:#fb8c00
style New fill:#e3f2fd,stroke:#1565c0
Traffic is switched at the proxy level — a single switch moves all requests to the new chain in one shot.
Production Migration: Front Door Traffic Switching
For an initial cutover from a legacy system to a new stack, Azure Front Door enables zero-downtime traffic switching:
Before Go-Live:
Front Door → Old App Service (100% traffic)
During Migration:
Front Door → Old App Service (100%)
New Container Apps (0%, ready and warmed)
Go-Live:
Front Door → New Container Apps (100%)
Old App Service (0%, still running)
If issues:
Front Door → Old App Service (100%) ← instant rollback
flowchart TB
subgraph Before[Before Go-Live]
FD1[Azure Front Door]
OA1[Old App Service\n100% traffic]
FD1 --> OA1
end
subgraph Migration[During Migration]
FD2[Azure Front Door]
OA2[Old App Service\n100% traffic]
NC2["New Container Apps\n0% traffic\n(ready & warmed)"]
FD2 --> OA2
FD2 -. monitoring .- NC2
end
subgraph GoLive[Go-Live]
FD3[Azure Front Door]
NC3[New Container Apps\n100% traffic]
OA3["Old App Service\n0% traffic\n(still running)"]
FD3 --> NC3
end
subgraph Issue[If issues]
FD4[Azure Front Door]
OA4["Old App Service\n100% traffic\n(instant rollback)"]
FD4 --> OA4
end
Both systems run in parallel. The switch is a Front Door configuration change — no DNS propagation delays, no cold starts. Rollback is likewise instant.
Lessons Learned
Generate configuration, don’t manage it
Manual configuration management across environments does not scale. Treat it as a code generation problem: define values once, override per environment, and let a generator produce the final artifacts. This removes entire classes of deployment bugs.
Separate infrastructure from application deployment
Infrastructure changes are rare, high-risk, and require planning. Application deployments are frequent and should be low-friction. Coupling the two means either infrastructure changes slow everything down or every deploy becomes risky.
Feature branch environments reshape the workflow
When every branch has its own live URL, code review turns into live review. Stakeholders can exercise features before they merge. QA can work in parallel with development. The infrastructure cost is trivial compared to the productivity gain.
Blue-green is worth the complexity
Being able to deploy a new version, validate it under real traffic conditions, and then flip traffic with instant rollback changes the risk profile of releases. Deployments become uneventful.
What’s Next
- Article 12: Security in a Nuxt SSR App — CSRF, Azure AD, CSP, and More — The security layers that protect a server-rendered application.
- Article 13: Observability and Distributed Tracing — Application Insights End-to-End — How every request is traced across all layers.
- Article 14: AI-Assisted Development — MCP, Debug Chatbot, and the Shared Language of the Codebase — Making AI assistants genuinely useful for live debugging.
Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.







Leave a Reply