Azure · Platform Engineering

The Missing Rung

A Paved-Road Pattern for Azure Container Apps

There's a container adoption ladder that plays out the same way in almost every enterprise I've worked with over the last several years.

Development teams get interested in Docker, and then get excited when they discover Docker Compose. Suddenly local development feels almost civilized. The team can define a multi-service stack in a single file, run it with one command, and onboard a new developer in an afternoon instead of a week.

And then someone says: "Great. Now let's do this in production. We should probably look at Kubernetes." And the room goes quiet because nobody has the bandwidth to think about it.

The Kubernetes conversation doesn't stall because the team is incapable. It stalls because the complexity tax is real, and the team — correctly — is doing a cost-benefit analysis in real time. Helm charts, RBAC, persistent volume claims, operators, custom resource definitions. For a team that just wants to run a handful of containerized APIs and a couple of background workers, this is not a trade they want to make.

So they stay on Docker Compose running on a VM, and the path to something better never gets shorter. Worse, I've seen teams delay cloud migration entirely — not because the business case is weak, but because nobody has the bandwidth to design the landing zone. The business doesn't care where it runs, so it doesn't run anywhere new.

The Missing Rung

The problem isn't the team's ambition or capability. The problem is that the ladder has a missing rung.

What these teams actually need — run my container, give it secrets, scale it when traffic spikes, expose it behind a proper ingress — has a name now: Azure Container Apps. It's the managed, opinionated orchestration layer that sits between "I have a container" and "I need a full Kubernetes cluster." You get environment isolation, managed ingress, autoscaling, native Key Vault integration, managed identity, and a sensible networking model. You do not get, or need to understand, pod scheduling, cluster autoscalers, or CNI plugins.

Container Apps is the rung that was missing. The reason most enterprise teams aren't standing on it isn't the platform. The platform is good. The reason is that Container Apps, like most Azure primitives, doesn't come with a deployment story. It comes with a portal, an ARM API, and the implication that you'll figure the rest out.

Most teams don't figure the rest out. They write one-off deployment scripts. They click through the portal during the first setup and then forget how they did it. They handle secrets inconsistently across services. They accumulate tribal knowledge that lives in one person's head until that person leaves. The platform exists; the road does not.

A Paved Road's Only Job

Here's the design principle I keep coming back to, earned across a lot of platform engineering work:

A paved road's job is to absorb the complexity of the underlying platform, not expose it.

If developers have to understand Bicep to ship a container to your platform, you have not built a paved road. You have built a paved shoulder — technically present, practically unused. Developers will route around it. They'll go back to the deployment script they already understand. They'll stay on Docker Compose.

The right developer interface for deploying a containerized workload is a file that describes the thing they want. Not a file that describes how Azure should be configured to produce the thing they want. The translation from intent to configuration is the platform team's problem. Absorbing that complexity is the whole point.

What This Looks Like in Practice

Let me show you the pattern concretely.

A developer on a team using this approach writes something like this to deploy a new service:

# apps/order-processor/container-app.yaml
name: order-processor
image: myregistry.azurecr.io/order-processor:${IMAGE_TAG}
environment: production

ingress:
  enabled: true
  external: false
  targetPort: 8080

scaling:
  minReplicas: 1
  maxReplicas: 10
  rules:
    - name: http-scaling
      http:
        concurrentRequests: 50

secrets:
  - name: database-connection-string
    keyVaultUri: https://myvault.vault.azure.net/secrets/order-processor-db-conn
  - name: service-bus-connection
    keyVaultUri: https://myvault.vault.azure.net/secrets/service-bus-conn

resources:
  cpu: 0.5
  memory: 1Gi

That's it. That's the entire deployment definition. The developer knows what image they're deploying, what secrets it needs (by reference — the value never appears in the file), how it should scale, and whether it has an ingress. They do not know what a Bicep template looks like. They do not need to.

At pipeline stage, a lightweight transformer reads this YAML and generates a Bicep parameters file. The translation is mechanical — there's nothing clever happening here, and that's the point:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "containerAppName": { "value": "order-processor" },
    "containerImage": { "value": "myregistry.azurecr.io/order-processor:abc123" },
    "targetPort": { "value": 8080 },
    "externalIngress": { "value": false },
    "minReplicas": { "value": 1 },
    "maxReplicas": { "value": 10 },
    "secrets": { "value": [ /* Key Vault references, unchanged */ ] },
    "cpu": { "value": "0.5" },
    "memory": { "value": "1Gi" }
  }
}

The pipeline then runs a standard az deployment group create against a shared Bicep template that the platform team owns. The template knows how to provision a Container App. It handles managed identity, Key Vault access policies, the Container Apps environment reference, the ingress configuration. Developers never see it. They never need to.

Every service on the platform follows the same pattern. The YAML files live in the application repositories, next to the code that produces the container. The Bicep template lives in the platform repository, owned by the platform team. The transformer is a few dozen lines of Python or TypeScript. The pipeline stage is the same across every application — parameterized, not bespoke.

Who Eats the Complexity

The design question underneath all of this is: who eats the complexity of the underlying platform?

The naive answer is "the developers, because they're the ones deploying." The paved-road answer is "the platform team, once, so that every developer after them doesn't have to." The platform team writes the Bicep template. They figure out the Key Vault access pattern. They understand the Container Apps environment model. Then they encapsulate all of that behind a YAML interface that application teams can use without learning any of it.

This is not a new idea. It's what every good platform team does. The specific insight here is that the interface boundary matters enormously, and most platform efforts draw it in the wrong place. They publish Bicep modules and call it a paved road. They build internal developer portals that wrap the portal. They write extensive runbooks. And developers don't use any of it, because the cognitive overhead is still too high. The platform has been documented; it has not been absorbed.

The YAML-at-pipeline-stage pattern draws the boundary correctly. Developers describe what they want. The platform does the work. The road is walkable because the hard parts are under the pavement, not in the developer's face.

The Pattern Generalizes

The specific tools here — Azure Container Apps, Bicep, GitHub Actions — are implementation details. The principle is not Azure-specific.

The same pattern applies to any platform primitive that has a complexity gap between "the thing exists" and "a developer team can safely operate it." The question is always the same: what is the right developer-facing interface, and how do you absorb the translation cost between that interface and the underlying platform? Who eats the complexity?

Get that design choice right, and the road gets used. Get it wrong, and you've spent six months building infrastructure that sits empty while the team stays on Docker Compose on a VM.

John Donaghy

I'm building an open-source accelerator that implements this pattern — Azure Container Apps, Bicep, GitHub Actions — and I'll share it when it's ready.

If this resonates and you'd like to talk through how it applies in your environment — Heuron Technology LLC takes consulting engagements, architecture reviews, and fractional CTO work.

First published on Techne, 8 May 2026, as “The Missing Rung — A Paved-Road Pattern for Azure Container Apps.” This copy is hosted here for the archive; the canonical version remains on Techne. Text matches the published article.