Chapter 8
Containers & App Service
Not everything needs a VM. This chapter covers the platform services that run your code or containers for you: Container Registry, Container Instances, Container Apps and App Service. In the lab you run the same image in all of them, and connect the web app to snet-app from chapter 4.
Theory
Azure Container Registry
A container image is a packaged app with everything it needs to run. Images are stored in a registry; Azure Container Registry (ACR) is your private one, at <name>.azurecr.io. Inside it, images are grouped in repositories and identified by tags, like aci-helloworld:v1.
| SKU | Adds |
|---|---|
| Basic | Cheapest, for learning and small teams. Lower storage and throughput. |
| Standard | More storage and throughput. Most production use. |
| Premium | Geo-replication (one registry in several regions), private endpoints, content trust, highest throughput. |
Ways to authenticate to ACR, from best to worst:
- Managed identity with the AcrPull role: the recommended way for Container Apps, App Service, AKS and ACI to pull images.
- Individual Entra login:
az acr login, for people. - Service principal with AcrPull/AcrPush, for CI/CD pipelines.
- Repository-scoped tokens: limited to certain repositories.
- Admin user: one username and two passwords with full push/pull rights. Disabled by default; only for testing.
Useful features: ACR Tasks build images in Azure (az acr build, no local Docker needed) and can rebuild automatically when a base image changes. az acr import copies an image from another registry without pulling it to your machine.
Azure Container Instances
Azure Container Instances (ACI) is the simplest way to run a container: no VMs, no orchestrator, pay per second. The unit of deployment is a container group: one or more containers that are scheduled on the same host and share a lifecycle, IP address, network and storage volumes (similar to a Kubernetes pod).
- Linux or Windows containers. Multi-container groups are Linux only.
- Networking: a public IP with an optional DNS name label (
<label>.<region>.azurecontainer.io), or deploy into a delegated subnet of your VNet for private access. - Restart policy: Always (default, for long-running services), OnFailure or Never (for run-once tasks and batch jobs).
- Volumes: mount an Azure Files share, a secret, an emptyDir or a git repo.
- Sizing: you request CPU and memory per container when you create the group. To change them you redeploy the group.
- No autoscaling. To handle more load you create more container groups yourself (or use Container Apps).
Azure Container Apps
Azure Container Apps (ACA) runs containers on a managed Kubernetes platform without exposing Kubernetes. It adds what ACI lacks: autoscaling (including to zero), HTTPS ingress, revisions and traffic splitting.
- Environment: a secure boundary around a group of container apps. Apps in one environment share a VNet and a Log Analytics workspace. Workload profiles are Consumption (serverless, pay per use) or Dedicated (reserved hardware, e.g. for GPUs or more memory).
- Ingress: external (internet) or internal (environment/VNet only), with a free HTTPS endpoint.
- Revisions: every change to the container template creates an immutable revision. In multiple revision mode you can split traffic, e.g. 90% to v1 and 10% to v2.
- Sizing: CPU and memory per container in fixed combinations, such as 0.25 vCPU / 0.5 GiB up to 4 vCPU / 8 GiB on Consumption.
Scaling in Container Apps
You set min replicas and max replicas and one or more scale rules. Scaling is driven by KEDA:
- HTTP: number of concurrent requests per replica (default 10).
- TCP: number of concurrent connections.
- Custom (KEDA scalers): queue length in Service Bus or Storage queues, Event Hubs lag, Kafka, and many more.
- CPU / memory utilisation (these can't scale to zero).
With min replicas = 0 the app scales to zero when idle, so you pay nothing for compute, at the cost of a cold start on the first request.
Which container service?
| ACI | Container Apps | AKS | App Service (containers) | |
|---|---|---|---|---|
| Best for | Simple, short-lived or batch containers | Microservices, APIs, event-driven apps | Full Kubernetes control | Web apps and APIs |
| Autoscale | No | Yes, incl. to zero | Yes (you configure) | Yes (plan level) |
| Kubernetes API | No | No | Yes | No |
| You manage | Almost nothing | Almost nothing | Cluster, upgrades, node pools | The plan |
Azure App Service
App Service hosts web apps, REST APIs and mobile back ends as code (.NET, Java, Node.js, Python, PHP) or as a container, on Windows or Linux. Microsoft patches the OS and runtime; you deploy your app.
App Service plans
Every app runs in an App Service plan, which defines the region, OS, pricing tier and number of instances. All apps in a plan share those instances: three apps on one plan with two instances means all three apps run on both instances. You pay for the plan, not per app.
| Tier | Max instances | Autoscale | Slots | Notable |
|---|---|---|---|---|
| Free (F1) / Shared (D1) | 1 (shared hardware) | No | β | Dev/test only, CPU minutes per day, no TLS for custom domains |
| Basic (B1βB3) | 3 | No, manual only | β | Custom domains + TLS, VNet integration, dedicated VMs |
| Standard (S1βS3) | 10 | Yes | 5 | The minimum for autoscale and slots |
| Premium v3 / v4 | 30 | Yes, plus automatic scaling | 20 | Zone redundancy, faster hardware |
| Isolated v2 | 100 | Yes | 20 | Runs in an App Service Environment (single tenant, in your VNet) |
Two ways to scale:
- Scale up: change the tier/size (more CPU, memory, features). Apps restart.
- Scale out: change the number of instances, manually or with autoscale rules (metric- or schedule-based, Standard+, just like scale sets in chapter 5). Premium also offers automatic scaling, where the platform scales on HTTP load by itself.
"Choose the cheapest tier that supports deployment slots / autoscale." Answer: Standard. "β¦that supports custom domains with TLS / VNet integration": Basic. "β¦zone redundancy": Premium v3.
Deployment slots
A deployment slot is a live copy of your app with its own hostname, like app-az104-xx-staging.azurewebsites.net. The typical flow:
- Deploy the new version to the staging slot and test it there.
- Swap staging with production. Azure first warms up the staging instances, then switches the routing, so there's no downtime and no cold start.
- If something's wrong, swap back: the previous version is now in the staging slot.
- Swapped: app settings and connection strings (unless sticky), general settings, handler mappings, the code itself.
- Not swapped: settings marked as deployment slot setting, custom domains, TLS bindings, scale settings, VNet integration, managed identities, IP restrictions.
- Swap with preview applies the production settings to staging first, so you can test before completing the swap.
- Auto swap swaps automatically after each deployment to a slot.
- Traffic routing can send a percentage of production traffic to a slot ("testing in production").
Custom domains and TLS
Every app gets <app>.azurewebsites.net with HTTPS. To use your own name:
- Prove ownership with a TXT record
asuid.<subdomain>containing the app's custom domain verification ID. - Point the name at the app: a CNAME to
<app>.azurewebsites.netfor a subdomain likewww, or an A record to the app's IP for the apex domain. - Add the domain in the app (Custom domains), then add a TLS/SSL binding.
Certificate options: the free App Service managed certificate (auto-renewed; no wildcards, not exportable), a certificate imported from Key Vault, an uploaded .pfx, or an App Service certificate you buy. Bindings use SNI SSL (normal) or IP-based SSL. Also set HTTPS Only and a minimum TLS version (1.2 or 1.3).
Backup
App Service can back up the app's content, configuration and (optionally) a linked database:
- Automatic backups: taken by the platform every hour on Basic tier and higher, kept for 30 days, no storage account needed.
- Custom backups: on a schedule you define, to a storage account container you choose, with your own retention. Maximum total size 10 GB (including the database).
You can restore a backup to the same app, another app, or a slot.
Networking
App Service networking is split into inbound (clients reaching the app) and outbound (the app reaching other things). This split is a classic exam question:
| Direction | Feature | What it does |
|---|---|---|
| Inbound | Access restrictions | Allow/deny rules by IP range, service endpoint (subnet), service tag or X-Forwarded-* headers (e.g. only via Front Door). |
| Private endpoint | Gives the app a private IP in your VNet, so clients reach it privately. Public access can then be disabled. | |
| Outbound | VNet integration | The app can call resources in or through your VNet (VMs, private endpoints, on-prem via VPN). Needs an empty, delegated subnet. With Route All, all outbound traffic goes through the VNet (so NSGs and UDRs apply). |
| Hybrid Connections | Reach a specific host:port on-premises through a relay agent, without a VPN. |
"The web app must connect to a SQL VM in a VNet." That's outbound: VNet integration. "Only users on the corporate VNet may open the web app." That's inbound: private endpoint (or access restrictions). VNet integration does not make the app private.
AZ-305's "design compute" area is mostly the decision table in this chapter: App Service vs Container Apps vs AKS vs Functions vs VMs, driven by requirements like "scale to zero", "Kubernetes API needed", "minimal management", or "full OS control". Add the networking split (inbound private endpoint, outbound VNet integration) and you can answer most of them.
- Introduction to Azure Container Registry
- Authenticate with ACR
- What is Azure Container Instances?
- Container groups in ACI
- Azure Container Apps overview
- Scaling in Azure Container Apps
- App Service plans
- Set up staging slots
- Map a custom DNS name
- Secure a custom domain with a TLS binding
- Back up and restore an app
- App Service networking features
Self-check
1. A container must run once every night for 10 minutes to process files, and then stop. Cheapest simple option?
D. Pay per second, no idle cost. (A Container Apps job would also work.)
2. An API must scale out with HTTP load and cost nothing when unused. Which service?
B. Only Container Apps scales to zero here.
3. After a slot swap, production uses the staging database. You wanted each slot to keep its own connection string. What should you have done?
A. Sticky settings stay with the slot.
4. Your web app on a Basic plan needs a staging slot. What do you do?
C. Slots start at Standard.
5. You need the free App Service managed certificate for *.contoso.com. Will that work?
B. Use a Key Vault/App Service certificate or upload a wildcard .pfx.
Lab: one image, four ways to run it
You put an image in your own registry, run it in ACI and Container Apps, and deploy a web app with a staging slot that's connected to snet-app. Paste the variables first:
RG=rg-az104-lab; LOC=westeurope; LOC2=northeurope; INIT=xx
ACR=acraz104${INIT}; APP=app-az104-${INIT}
ZONE=lab.yourdomain.com # only for the optional custom domain task
-
Task
Create a registry and get an image into it
Create a Basic container registry
acraz104xx. Copy the public imagemcr.microsoft.com/azuredocs/aci-helloworld:latestinto it asaci-helloworld:v1, without using Docker on your machine.Done when- The registry's Repositories show
aci-helloworldwith tagv1.
Hint
There's an
az acrcommand that imports images straight from another registry.Solution Β· Portal + CLI
Create a resource β Container Registry: name
acraz104xx, SKU Basic. The import itself is CLI only (see below).Solution Β· Azure CLI
az acr create -g $RG -n $ACR -l $LOC --sku Basic az acr import -n $ACR --source mcr.microsoft.com/azuredocs/aci-helloworld:latest --image aci-helloworld:v1 az acr repository show-tags -n $ACR --repository aci-helloworld -o table - The registry's Repositories show
-
Task
Run it in Container Instances
Run your image from your registry in ACI as
aci-hello, reachable on port 80 at a public nameaci-hello-xx.<region>.azurecontainer.io, with 1 vCPU and 1 GiB memory. For pulling from ACR you may use the registry's admin user this time.Done when- The FQDN shows the "Welcome to Azure Container Instances!" page.
- You know how you'd give it 2 vCPUs. (Can you change it in place?)
Solution Β· Portal
- Registry β Settings β Access keys β enable Admin user.
- Create a resource β Container Instances: name
aci-hello, image source Azure Container Registry, registry/image/tag, size 1 vCPU, 1 GiB. - Networking: Public, DNS name label
aci-hello-xx, port 80 TCP. Restart policy On failure (Advanced tab) or leave Always. - Open the FQDN from Overview. To get 2 vCPUs you delete and recreate the container group.
Solution Β· Azure CLI
az acr update -n $ACR --admin-enabled true PW=$(az acr credential show -n $ACR --query "passwords[0].value" -o tsv) az container create -g $RG -n aci-hello -l $LOC --image $ACR.azurecr.io/aci-helloworld:v1 \ --registry-login-server $ACR.azurecr.io --registry-username $ACR --registry-password $PW \ --cpu 1 --memory 1 --ports 80 --ip-address Public --dns-name-label aci-hello-$INIT --os-type Linux az container show -g $RG -n aci-hello --query ipAddress.fqdn -o tsvCostACI bills per second while running. Delete
aci-helloafter you've seen it:az container delete -g $RG -n aci-hello -y. -
Task
Run it in Container Apps, scaling to zero
Create a Container Apps environment
cae-az104(Consumption) that sends its logs to your existing Log Analytics workspacelog-az104from chapter 4. In it, createca-hellofrom your ACR image with:- external ingress on target port 80;
- 0.25 vCPU / 0.5 GiB;
- 0 to 5 replicas, scaling on 20 concurrent HTTP requests per replica.
Then generate some load and watch the replica count.
Done when- The app URL (
https://ca-hello.β¦.azurecontainerapps.io) shows the hello page. - The Replica Count metric goes up under load and back to 0 after a few idle minutes.
Solution Β· Portal
- Create a resource β Container App: name
ca-hello, create new environmentcae-az104(Consumption only; Monitoring tab: Log Analytics βlog-az104). - Container: image source Azure Container Registry, your image, CPU/memory 0.25 / 0.5Gi.
- Ingress: enabled, Accepting traffic from anywhere, target port 80.
- After creation: Application β Scale (or Scale and replicas) β min 0, max 5, add rule HTTP scaling with concurrent requests 20 β Save (creates a new revision).
- Monitoring β Metrics β Replica Count.
Solution Β· Azure CLI
WSID=$(az monitor log-analytics workspace show -g $RG -n log-az104 --query customerId -o tsv) WSKEY=$(az monitor log-analytics workspace get-shared-keys -g $RG -n log-az104 --query primarySharedKey -o tsv) az containerapp env create -g $RG -n cae-az104 -l $LOC --logs-workspace-id $WSID --logs-workspace-key $WSKEY PW=$(az acr credential show -n $ACR --query "passwords[0].value" -o tsv) az containerapp create -g $RG -n ca-hello --environment cae-az104 \ --image $ACR.azurecr.io/aci-helloworld:v1 \ --registry-server $ACR.azurecr.io --registry-username $ACR --registry-password $PW \ --target-port 80 --ingress external --cpu 0.25 --memory 0.5Gi \ --min-replicas 0 --max-replicas 5 \ --scale-rule-name http-rule --scale-rule-type http --scale-rule-http-concurrency 20 URL=https://$(az containerapp show -g $RG -n ca-hello --query properties.configuration.ingress.fqdn -o tsv) curl -s $URL | head -5 for i in $(seq 300); do curl -s -o /dev/null $URL & done; wait az containerapp replica list -g $RG -n ca-hello -o tableIn production you'd use a managed identity with AcrPull instead of the admin password (
--registry-identity system). -
Task
Create a web app that can use slots
Create a Linux App Service plan
asp-az104in the cheapest tier that supports deployment slots, and a web appapp-az104-xxrunning the containermcr.microsoft.com/azuredocs/aci-helloworld:latest. Make it HTTPS only with minimum TLS 1.2.Done whenhttps://app-az104-xx.azurewebsites.netshows the hello page (the first start can take a minute).http://redirects tohttps://.
Solution Β· Portal
- Create a resource β Web App: name
app-az104-xx, publish Container, OS Linux, region, new planasp-az104, pricing plan Standard S1. - Container tab: image source Other container registries, public, server
https://mcr.microsoft.com, imageazuredocs/aci-helloworld:latest. - After creation: Settings β Configuration β General settings: HTTPS Only On, Minimum inbound TLS 1.2.
Solution Β· Azure CLI
az appservice plan create -g $RG -n asp-az104 -l $LOC --is-linux --sku S1 az webapp create -g $RG -p asp-az104 -n $APP --container-image-name mcr.microsoft.com/azuredocs/aci-helloworld:latest az webapp update -g $RG -n $APP --https-only true az webapp config set -g $RG -n $APP --min-tls-version 1.2 curl -sI http://$APP.azurewebsites.net | head -3 -
Task
Release a new version with a slot swap
Create a staging slot that runs a different image,
mcr.microsoft.com/azuredocs/containerapps-helloworld:latest. Give both slots an app settingENV_NAME(production/staging) that must stay with its slot. Then swap staging into production.Done when- The production URL now shows the Container Apps hello page, and the staging URL shows the old one.
ENV_NAMEin production is stillproduction.
Solution Β· Portal
- Web app β Deployment β Deployment slots β Add slot: name
staging, clone settings from production. - Open the staging slot β Deployment Center: change the image to
azuredocs/containerapps-helloworld:latest, Save. - In each slot: Settings β Environment variables β App settings β Add
ENV_NAME, tick Deployment slot setting. - Deployment slots β Swap: source
staging, targetproduction. Look at the "config changes" preview, then Swap.
Solution Β· Azure CLI
az webapp deployment slot create -g $RG -n $APP --slot staging --configuration-source $APP az webapp config container set -g $RG -n $APP --slot staging \ --container-image-name mcr.microsoft.com/azuredocs/containerapps-helloworld:latest az webapp config appsettings set -g $RG -n $APP --slot-settings ENV_NAME=production az webapp config appsettings set -g $RG -n $APP --slot staging --slot-settings ENV_NAME=staging az webapp deployment slot swap -g $RG -n $APP --slot staging --target-slot production az webapp config appsettings list -g $RG -n $APP --query "[?name=='ENV_NAME']" -o table -
Task
Scale the plan
Add autoscale to
asp-az104: between 1 and 3 instances, +1 when average CPU > 70% for 10 minutes, β1 when < 30%. Then look at what would happen if you scaled up to Premium v3 (don't do it, just look at what features it lists).Done when- The plan's Scale out page shows custom autoscale with two rules.
Solution Β· Portal
asp-az104β Settings β Scale out (App Service plan) β Rules Based β Manage rules based scaling β Custom autoscale: min 1, max 3, default 1, add the scale-out and scale-in rules β Save. Scale up (App Service plan) shows the tiers and features.Solution Β· Azure CLI
az monitor autoscale create -g $RG --resource asp-az104 --resource-type Microsoft.Web/serverfarms \ -n autoscale-asp --min-count 1 --max-count 3 --count 1 az monitor autoscale rule create -g $RG --autoscale-name autoscale-asp --condition "CpuPercentage > 70 avg 10m" --scale out 1 az monitor autoscale rule create -g $RG --autoscale-name autoscale-asp --condition "CpuPercentage < 30 avg 10m" --scale in 1 -
Task
Connect the app to your network, and restrict who can reach it
- Let the web app make outbound calls into
vnet-spokethroughsnet-app(the delegated subnet you made with Bicep). - Allow inbound traffic to the production app only from your browser's public IP. Check from your Linux VM that it's blocked, then remove the restriction again.
Done when- Networking shows Virtual network integration with
snet-app. - The Linux VM gets 403 while your browser still works.
- You can explain why VNet integration didn't make the app private.
Solution Β· Portal
- Web app β Settings β Networking β Outbound traffic configuration β Virtual network integration β Add β
vnet-spoke/snet-app. - Inbound traffic configuration β Public network access β Enabled from select virtual networks and IP addresses, add an Allow rule for your browser's IP (
x.x.x.x/32), unmatched rule action Deny. Save. - On the Linux VM:
curl -s -o /dev/null -w "%{http_code}" https://$APP.azurewebsites.netβ 403. Then set public access back to Enabled with no access restrictions.
VNet integration is outbound only. Making the app private needs a private endpoint (inbound).
Solution Β· Azure CLI
az webapp vnet-integration add -g $RG -n $APP --vnet vnet-spoke --subnet snet-app az webapp access-restriction add -g $RG -n $APP --rule-name allow-me --action Allow \ --ip-address <your-browser-ip>/32 --priority 100 curl -s -o /dev/null -w "%{http_code}\n" https://$APP.azurewebsites.net # 403 from the Linux VM az webapp access-restriction remove -g $RG -n $APP --rule-name allow-me - Let the web app make outbound calls into
-
Task Β· optional, needs the chapter 3 DNS zone
Put the web app on your own domain with TLS
Make the production app reachable at
https://www.lab.yourdomain.comwith a valid certificate that costs nothing and renews itself. Create the DNS records in your Azure DNS zonelab.yourdomain.comfrom chapter 3.Done when- Your browser opens
https://www.lab.yourdomain.comwith a padlock and no certificate warning. - You can name the two DNS records you needed, and why one of them is a TXT record.
- You know whether this custom domain would move to the staging slot on the next swap.
Hint
App Service wants proof that you own the name: a TXT record
asuid.<subdomain>containing the app's custom domain verification ID. In a zone calledlab.yourdomain.com, the record names are relative, so it'sasuid.wwwandwww.Solution Β· Portal
- Web app β Settings β Custom domains β + Add custom domain: domain provider All other domain services, TLS/SSL certificate App Service Managed Certificate, TLS/SSL type SNI SSL, domain
www.lab.yourdomain.com. The wizard shows the CNAME and TXT values; keep it open. - In another tab, DNS zone
lab.yourdomain.comβ + Record set: namewww, type CNAME, aliasapp-az104-xx.azurewebsites.net. Another: nameasuid.www, type TXT, value = the verification ID. - Back in the wizard: Validate β Add. The managed certificate is created and bound automatically (a few minutes).
Custom domains and TLS bindings are not swapped: they stay with the production slot.
Solution Β· Azure CLI
ZONE=lab.yourdomain.com VID=$(az webapp show -g $RG -n $APP --query customDomainVerificationId -o tsv) az network dns record-set txt add-record -g $RG -z $ZONE -n asuid.www -v $VID az network dns record-set cname set-record -g $RG -z $ZONE -n www -c $APP.azurewebsites.net az webapp config hostname add -g $RG --webapp-name $APP --hostname www.$ZONE az webapp config ssl create -g $RG -n $APP --hostname www.$ZONE # free managed certificate THUMB=$(az webapp config ssl list -g $RG --query "[?subjectName=='www.$ZONE'].thumbprint | [0]" -o tsv) az webapp config ssl bind -g $RG -n $APP --certificate-thumbprint $THUMB --ssl-type SNI curl -sI https://www.$ZONE | head -1Also have a quick look at Settings β Backups: automatic backups are already running, and this is where you'd configure a custom backup to a storage account.
- Your browser opens
The Standard S1 plan is the most expensive thing in this lab (about β¬0.09/hour). Delete the web app and asp-az104 at the end of this chapter (later chapters don't need them), and delete aci-hello if you haven't. The Container App scales to zero and the Basic registry costs a few cents a day, so they can stay until chapter 10.
az network dns record-set cname delete -g $RG -z $ZONE -n www --yes # if you did the custom domain task
az network dns record-set txt delete -g $RG -z $ZONE -n asuid.www --yes
az webapp delete -g $RG -n $APP
az appservice plan delete -g $RG -n asp-az104 -y
az container delete -g $RG -n aci-hello -y- Registry
acraz104xxwithaci-helloworld:v1. - Container Apps environment
cae-az104(logging tolog-az104) withca-hello, scaling 0β5. - You built and deleted ACI, an App Service plan with slots, autoscale, VNet integration and (optionally) a custom domain with a managed certificate.