Multi-Environment Infrastructure — Azure Container Apps and the Configuration System

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:

ArtifactPurposeExample
Container Apps manifestsComplete container spec (env vars, secrets, scaling)container-apps/spa.test.json
Bicep parameter filesInfrastructure parameters (environment name, region)container-apps/my-app.test.bicepparam
Pipeline variable filesCI/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.

Categories:

Leave a Reply

Your email address will not be published. Required fields are marked *