Archiboard

Set C · Cloud & Operations

App Service, Container Apps or AKS: hosting a .NET web application then and now

In 2014 the choice was a Cloud Services role, the new Azure Websites, or a virtual machine you patched yourself. Container Apps eventually put a rung in the middle of the ladder, and that is where most small teams should stand.

In September 2014 I sat in a meeting room with a team of four and a product that was about to get its first paying tenants. One of the four wanted virtual machines, because that was what he knew how to reason about, and he had a deployment script that worked on his laptop. A second wanted a Cloud Services web role plus a worker role, which was the respectable Azure answer at the time and came with a deployment cycle measured in coffee breaks. I wanted Azure Websites, which was newer, cheaper and slightly embarrassing to suggest in front of people who had run servers. We argued for an hour and put it on Websites. It stayed there, under its later name, for nine years.

The argument was never really about technology. It was about how many hours a week a team of four could afford to spend on the thing that runs the code rather than on the code. That question has not changed since 2014. What changed is that Azure now has an answer in the middle, and the middle is where most of you belong.

What the ladder looked like when I drew this

In 2014 the rungs were close together and none of them was very high. Azure Websites gave you a box you did not have to patch, a deployment from source control, a staging slot if you paid for it, and an autoscale rule that fired on CPU. Cloud Services gave you web and worker roles, a proper VIP swap between staging and production, and a packaging step slow enough that you stopped deploying on Fridays. Virtual machines gave you everything and took a person. For a .NET application that was one web front end, one background job and one SQL Azure database, Websites was hard to beat, and it stayed hard to beat when it became App Service in 2015.

Then AKS arrived in 2018 and the drawing changed shape. Suddenly there was a rung far above the others, and a gap where nothing was. AKS gave you a Kubernetes API and a bill for the nodes, and left the rest to you: ingress controller, certificate rotation, cluster upgrades, node image patching, a way to see logs. All yours. I watched a team spend roughly a third of their engineering time for a year on their cluster. They were competent people. The cluster was simply a second product, and nobody had budgeted for it.

So between 2018 and 2022 the drawing had two rungs with a hole in the middle. Most small teams stood on the bottom rung and felt vaguely guilty about it.

Three hosting options as a ladder of control against operational effort operational effort control where a team of five should land App Service PaaS, code or container pays: plan hours Container Apps KEDA, scale to zero pays: vCPU seconds AKS the cluster is yours pays: nodes, always on this rung did not exist until 2022; before it you climbed or you stayed
Fig. 1. The hosting ladder as I draw it today. The hatched area is the gap that stood open from 2018 to 2022; a small team either stayed at the bottom or climbed too high.

The rung that arrived in 2022

Container Apps went generally available in May 2022 and it is Kubernetes with the cluster hidden. You give it a container image, a CPU and memory size, an ingress setting and a scale rule, and it gives you revisions, traffic splitting between them, internal DNS between apps in the same environment and a managed certificate. Underneath there is KEDA, so a worker can scale on the length of a Service Bus queue rather than on CPU, and can scale to zero when the queue is empty. That last property is what makes it interesting for a product with uneven tenants: the month-end run needs twelve workers and the third of the month needs none.

The networking is closer to AKS than to App Service. An environment can sit in your own VNet with internal-only ingress, and Front Door reaches it over Private Link. The cost model is either consumption, billed per vCPU second and memory with a free grant, or a dedicated workload profile where you rent a fixed slice and pack apps onto it. On one product I moved, the consumption plan came out cheaper than the App Service plan it replaced, mostly because the workers were idle twenty-six days a month and I had been paying for them anyway.

Here is the worker's scale rule in Bicep. In 2014 the same job was a WebJob polling a queue on a plan that never scaled down, and I would have told you that was fine, because it was.

resource worker 'Microsoft.App/containerApps@2025-01-01' = {
  name: 'ca-render-worker'
  location: location
  identity: { type: 'SystemAssigned' }
  properties: {
    environmentId: env.id
    template: {
      containers: [{
        name: 'worker'
        image: '${acr}/render-worker:${imageTag}'
        resources: { cpu: json('0.5'), memory: '1Gi' }
      }]
      scale: {
        minReplicas: 0
        maxReplicas: 12
        rules: [{
          name: 'render-queue'
          custom: {
            type: 'azure-servicebus'
            metadata: { queueName: 'renders', messageCount: '50', namespace: sbNamespace }
            identity: 'system'
          }
        }]
      }
    }
  }
}

What each rung gives you and takes from you

App Service gives you the least to think about. Scaling is per plan, by instance count, on a metric or a schedule; networking is a public endpoint unless you pay for integration; cost is a flat plan rate; the operational burden is deployment slots and the occasional stuck instance. It runs .NET 10 on Linux or Windows without a Dockerfile, which still matters to teams who do not want to own a container image.

Container Apps gives you scale rules per app, scale to zero, revision-based rollouts and VNet integration by default, and it takes from you the ability to see or touch the cluster. There is no kubectl. If a pod is stuck you have the revision's logs and the console, and that is it. You also cannot run a DaemonSet, a custom CNI, Windows containers or an operator that wants cluster-wide permissions.

AKS gives you all of that and takes a platform engineer. AKS Automatic, which arrived in 2024, closes a good part of the gap: managed node provisioning, upgrades on the stable channel, managed ingress, Prometheus and Grafana on by default. It is a much better cluster than the one I argued against in 2018. It is still a cluster, and the person who owns it will still be the one who is paged when a node pool fails to upgrade on a Saturday.

Where I would put a small team today

If the product is one web application and one database, and nobody on the team wants to write a Dockerfile, App Service is still the right answer and will be for years. Do not let anyone make you feel behind.

If the product is an API plus one or more workers, if tenants are uneven, if you already build a container image in the pipeline, start on Container Apps. Put the API and the workers in one environment, scale the workers on the queue, and keep the environment in a VNet from day one because moving it later is a redeploy. This is the default I now draw for every new .NET web application.

Climb to AKS when you can name the thing Container Apps cannot do and you have tried it. A particular operator, a service mesh you actually use, GPU node pools, a compliance regime that demands you see the nodes. Then use AKS Automatic unless you have a reason not to, and budget a person for it, because the cluster is a product and products need an owner.

The trade-off I would make today

I would take Container Apps over App Service for any product with a worker, and accept that I cannot see the cluster in exchange for not having to run it. I would take App Service over Container Apps for a single web app with no queue, because a Dockerfile you do not need is a Dockerfile you have to maintain. And I would take AKS only when a specific need drags me there, with a name and a date on the ticket that explains why.

Drawn from