AZ-104 Refresher Chapter 8 Β· Containers & App Service

Chapter 8

Containers & App Service

πŸ“– Theory 35 minπŸ›  Lab 25 minCompute Β· 20–25%

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.

Part A

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.

SKUAdds
BasicCheapest, for learning and small teams. Lower storage and throughput.
StandardMore storage and throughput. Most production use.
PremiumGeo-replication (one registry in several regions), private endpoints, content trust, highest throughput.

Ways to authenticate to ACR, from best to worst:

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).

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.

Scaling in Container Apps

You set min replicas and max replicas and one or more scale rules. Scaling is driven by KEDA:

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?

ACIContainer AppsAKSApp Service (containers)
Best forSimple, short-lived or batch containersMicroservices, APIs, event-driven appsFull Kubernetes controlWeb apps and APIs
AutoscaleNoYes, incl. to zeroYes (you configure)Yes (plan level)
Kubernetes APINoNoYesNo
You manageAlmost nothingAlmost nothingCluster, upgrades, node poolsThe 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.

TierMax instancesAutoscaleSlotsNotable
Free (F1) / Shared (D1)1 (shared hardware)No–Dev/test only, CPU minutes per day, no TLS for custom domains
Basic (B1–B3)3No, manual only–Custom domains + TLS, VNet integration, dedicated VMs
Standard (S1–S3)10Yes5The minimum for autoscale and slots
Premium v3 / v430Yes, plus automatic scaling20Zone redundancy, faster hardware
Isolated v2100Yes20Runs in an App Service Environment (single tenant, in your VNet)

Two ways to scale:

Exam trap

"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:

  1. Deploy the new version to the staging slot and test it there.
  2. Swap staging with production. Azure first warms up the staging instances, then switches the routing, so there's no downtime and no cold start.
  3. If something's wrong, swap back: the previous version is now in the staging slot.
Before swap productioncode v1 Β· setting COLOR=bluesticky: ENV=prod stagingcode v2 Β· setting COLOR=greensticky: ENV=staging swapwarm-up, then switch routing After swap productioncode v2 Β· setting COLOR=greensticky: ENV=prod (stayed) stagingcode v1 Β· setting COLOR=bluesticky: ENV=staging (stayed)
Normal settings move with the code. Settings marked "deployment slot setting" (sticky) stay with the slot.

Custom domains and TLS

Every app gets <app>.azurewebsites.net with HTTPS. To use your own name:

  1. Prove ownership with a TXT record asuid.<subdomain> containing the app's custom domain verification ID.
  2. Point the name at the app: a CNAME to <app>.azurewebsites.net for a subdomain like www, or an A record to the app's IP for the apex domain.
  3. 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:

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:

DirectionFeatureWhat it does
InboundAccess restrictionsAllow/deny rules by IP range, service endpoint (subnet), service tag or X-Forwarded-* headers (e.g. only via Front Door).
Private endpointGives the app a private IP in your VNet, so clients reach it privately. Public access can then be disabled.
OutboundVNet integrationThe 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 ConnectionsReach a specific host:port on-premises through a relay agent, without a VPN.
Exam trap

"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 link

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.

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.

Part B

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
  1. Task

    Create a registry and get an image into it

    Create a Basic container registry acraz104xx. Copy the public image mcr.microsoft.com/azuredocs/aci-helloworld:latest into it as aci-helloworld:v1, without using Docker on your machine.

    Done when
    • The registry's Repositories show aci-helloworld with tag v1.
    Hint

    There's an az acr command 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
  2. Task

    Run it in Container Instances

    Run your image from your registry in ACI as aci-hello, reachable on port 80 at a public name aci-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
    1. Registry β†’ Settings β†’ Access keys β†’ enable Admin user.
    2. Create a resource β†’ Container Instances: name aci-hello, image source Azure Container Registry, registry/image/tag, size 1 vCPU, 1 GiB.
    3. Networking: Public, DNS name label aci-hello-xx, port 80 TCP. Restart policy On failure (Advanced tab) or leave Always.
    4. 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 tsv
    Cost

    ACI bills per second while running. Delete aci-hello after you've seen it: az container delete -g $RG -n aci-hello -y.

  3. 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 workspace log-az104 from chapter 4. In it, create ca-hello from 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
    1. Create a resource β†’ Container App: name ca-hello, create new environment cae-az104 (Consumption only; Monitoring tab: Log Analytics β†’ log-az104).
    2. Container: image source Azure Container Registry, your image, CPU/memory 0.25 / 0.5Gi.
    3. Ingress: enabled, Accepting traffic from anywhere, target port 80.
    4. 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).
    5. 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 table

    In production you'd use a managed identity with AcrPull instead of the admin password (--registry-identity system).

  4. Task

    Create a web app that can use slots

    Create a Linux App Service plan asp-az104 in the cheapest tier that supports deployment slots, and a web app app-az104-xx running the container mcr.microsoft.com/azuredocs/aci-helloworld:latest. Make it HTTPS only with minimum TLS 1.2.

    Done when
    • https://app-az104-xx.azurewebsites.net shows the hello page (the first start can take a minute).
    • http:// redirects to https://.
    Solution Β· Portal
    1. Create a resource β†’ Web App: name app-az104-xx, publish Container, OS Linux, region, new plan asp-az104, pricing plan Standard S1.
    2. Container tab: image source Other container registries, public, server https://mcr.microsoft.com, image azuredocs/aci-helloworld:latest.
    3. 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
  5. 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 setting ENV_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_NAME in production is still production.
    Solution Β· Portal
    1. Web app β†’ Deployment β†’ Deployment slots β†’ Add slot: name staging, clone settings from production.
    2. Open the staging slot β†’ Deployment Center: change the image to azuredocs/containerapps-helloworld:latest, Save.
    3. In each slot: Settings β†’ Environment variables β†’ App settings β†’ Add ENV_NAME, tick Deployment slot setting.
    4. Deployment slots β†’ Swap: source staging, target production. 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
  6. 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
  7. Task

    Connect the app to your network, and restrict who can reach it

    • Let the web app make outbound calls into vnet-spoke through snet-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
    1. Web app β†’ Settings β†’ Networking β†’ Outbound traffic configuration β†’ Virtual network integration β†’ Add β†’ vnet-spoke / snet-app.
    2. 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.
    3. 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
  8. 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.com with a valid certificate that costs nothing and renews itself. Create the DNS records in your Azure DNS zone lab.yourdomain.com from chapter 3.

    Done when
    • Your browser opens https://www.lab.yourdomain.com with 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 called lab.yourdomain.com, the record names are relative, so it's asuid.www and www.

    Solution Β· Portal
    1. 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.
    2. In another tab, DNS zone lab.yourdomain.com β†’ + Record set: name www, type CNAME, alias app-az104-xx.azurewebsites.net. Another: name asuid.www, type TXT, value = the verification ID.
    3. 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 -1

    Also 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.

Cost

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
Checkpoint
  • Registry acraz104xx with aci-helloworld:v1.
  • Container Apps environment cae-az104 (logging to log-az104) with ca-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.