The previous lesson ended with an implicit question: if having an available, elastic API means building a scale set, a balancer, probes, autoscale rules, operating system patches and certificates by hand… isn't there something that does all of that for you? There is, and it is called Azure App Service.
Contoso Airlines has two new applications that carry no debt from the past: Contoso Bookings, the public website where customers search for flights and buy their tickets, and the Availability API, the internal service that queries seats and prices. Both are modern code (Java for the API, Node.js for the website) that Diego Salas can package and deploy without depending on the operating system. For them, maintaining virtual machines would be unpaid work: patches, agents, certificates and manual deployments that do not add a cent to the business.
In this lesson you will learn what App Service is, how its plans work and why the plan is billed, not the application, how to deploy code with Azure CLI, how to configure the application with app settings and connection strings, how to put Contoso's own domain in place with a managed certificate, and how to publish new versions without interrupting the service using deployment slots. Remember that module 1 already announced the app-contoso-reservas-pro application: here we create it for real.
Cost warning: the Free (F1) tier costs nothing and is enough for almost all the exercises. The Basic, Standard and Premium tiers are billed by the hour as long as the plan exists, even if the application never receives a single request. The cleanup is at the end.
Contents
- What App Service is and why Contoso chooses it
- App Service plans: tiers, Linux and Windows
- Supported runtime stacks
- Creating the plan and the applications with Azure CLI
- Deploying code: ZIP deploy and deployment from Git
- Configuration: app settings and connection strings
- Custom domains and managed TLS certificates
- Deployment slots and swap with warm-up
- Vertical, horizontal and automatic scaling
- Diagnostics: logs, log stream and SSH console
- Virtual network integration (an introduction)
- Cleanup
- Common Mistakes and Tips
- Exercises
- Conclusion
- What App Service is and why Contoso chooses it
Azure App Service is a platform as a service (PaaS) for hosting web applications, APIs and mobile back-end applications. You hand over the code or the artifact; Azure takes care of the operating system, the web server, the runtime, the balancing between instances and the TLS certificate.
Compared with what you built in the previous lesson:
| Task | With VMs and VMSS | With App Service |
|---|---|---|
| Patching the operating system | Yours | Azure's |
| Installing and updating the runtime (JDK, Node) | Yours | Azure's (you pick the version) |
| Balancing between instances | A Load Balancer you build and pay for | Included |
| Autoscaling | Rules over the scale set | Rules over the plan (simpler) |
| TLS certificate | Manual purchase, installation and renewal | Managed and renewed by Azure |
| Zero-downtime deployment | You design it | Slots with swap, included |
| Logs and console | Agents you install | Built into the service |
| Operating system control | Full | None |
The last row is the trade-off: in App Service there is no administrator access to the server, you cannot install arbitrary daemons or depend on paths outside your application's space. That is why the legacy availability engine stays on a VM (lesson 02-01) and the new applications go here.
Contoso Airlines' recorded decision:
app-contoso-reservas-pro: the public sales website, Node.js on Linux.app-contoso-api-disponibilidad-pro: the internal seats-and-prices API, Java on Linux.- Both on the same production plan to begin with; the API moves to its own plan when the season-opening peak justifies it (you will see this in exercise 1).
- App Service plans: tiers, Linux and Windows
The most important concept, and the one that causes the most surprise bills: the App Service plan is the set of machines your applications run on. The plan is billed, not the application.
Direct consequences:
- Ten applications on one plan cost the same as one: what you pay for is the plan.
- A stopped application on a paid plan still costs money, because the plan still exists. To stop paying you have to delete the plan (or drop it to Free).
- The applications on the same plan share CPU, memory and instances. If one eats the CPU, the others feel it.
| Tier | Intended use | Instances | Slots | Custom domain and TLS | Autoscaling | Notes |
|---|---|---|---|---|---|---|
| Free (F1) | Testing and learning | 1 shared, daily CPU quota | No | No | No | Free, no SLA, it goes to sleep |
| Basic (B1–B3) | Development and small applications | Up to 3, dedicated | No | Yes | Manual | No slots: no zero-downtime deployment |
| Standard (S1–S3) | General production | Up to 10 | 5 | Yes | Yes | The first tier genuinely fit for production |
| Premium v3 (P0v3–P5v3) | Demanding production | Up to 30 | 20 | Yes | Yes | More CPU and memory per instance, availability zones, higher network throughput |
| Isolated v2 | Strict isolation and compliance | Up to 100 | 20 | Yes | Yes | Runs inside your own network (App Service Environment). Expensive |
What each jump unlocks, put bluntly:
- From Free to Basic: dedicated instances, your own domain and TLS, and an application that does not fall asleep.
- From Basic to Standard: deployment slots and autoscaling. This is the jump that turns a toy into a production service.
- From Standard to Premium v3: faster hardware, more instances, zone redundancy and better network limits. It is the tier Contoso's policy of keeping critical components in two zones requires.
Linux versus Windows
| Aspect | Linux plan | Windows plan |
|---|---|---|
| Stacks | Java, Node, Python, PHP, .NET, Go (container) | .NET Framework, .NET, Java, Node, PHP |
| Price | Slightly lower at equivalent tiers | Slightly higher |
| Classic .NET Framework (4.x) | No | Yes (that is its reason to exist) |
| Custom containers | Yes (detail in 06-01) | Limited |
| Mixing Linux and Windows in the same plan | Not possible: they are separate plans |
Contoso chooses Linux for both applications: there is no classic .NET Framework anywhere, the price is lower and the Node and Java stacks are perfectly supported.
- Supported runtime stacks
App Service runs your code on a runtime stack that you choose when creating the application and can change later. Check the available ones with:
# Every stack available for web applications on Linux.
az webapp list-runtimes --os-type linux --output tableAn abbreviated output and how to read it:
[
"JAVA:21-java21",
"JAVA:17-java17",
"NODE:22-lts",
"NODE:20-lts",
"PYTHON:3.12",
"PHP:8.3",
"DOTNETCORE:8.0"
]The format is STACK:VERSION. Two important warnings:
- Versions reach end of support: when a Node or Java version leaves community support, Azure removes it from the list, with prior notice. Plan your upgrades; do not discover the end of support on the day a deployment fails.
- If your stack or version is not on the list (for example, Go or one very specific old version), the way out is a custom container, and that is covered in lesson 06-01.
- Creating the plan and the applications with Azure CLI
We start with the development environment, on the free tier, so that you can follow the lesson at no cost.
#!/usr/bin/env bash
set -euo pipefail
GROUP="rg-contoso-reservas-dev"
REGION="westeurope"
PLAN="plan-contoso-reservas-dev"
APP_WEB="app-contoso-reservas-dev"
APP_API="app-contoso-api-disponibilidad-dev"
TAGS=(entorno=desarrollo proyecto=contoso-reservas centro-coste=CC-1042
[email protected])
# 1. App Service plan: Linux, free tier.
az appservice plan create \
--resource-group "${GROUP}" \
--name "${PLAN}" \
--location "${REGION}" \
--is-linux \
--sku F1 \
--tags "${TAGS[@]}" \
--output table
# 2. The bookings website on Node.js 22.
az webapp create \
--resource-group "${GROUP}" \
--plan "${PLAN}" \
--name "${APP_WEB}" \
--runtime "NODE:22-lts" \
--tags "${TAGS[@]}" \
--output table
# 3. The Availability API on Java 21, on the same plan.
az webapp create \
--resource-group "${GROUP}" \
--plan "${PLAN}" \
--name "${APP_API}" \
--runtime "JAVA:21-java21" \
--tags "${TAGS[@]}" \
--output tableDetails that matter:
--is-linuxdefines the operating system of the plan, not of the application. It cannot be changed afterwards: if you get it wrong, you have to create another plan.- The application name is globally unique, because it generates the
app-contoso-reservas-dev.azurewebsites.nethost name. If somebody in the world has taken it, it fails. That is why Contoso uses an organization-specific prefix. - The two applications share a plan: the cost does not change by adding the second one.
- On the F1 tier you cannot turn on
--https-only… actually you can, and you should:
# Force HTTPS: any HTTP request is redirected to HTTPS.
az webapp update \
--resource-group "${GROUP}" \
--name "${APP_WEB}" \
--https-only true \
--output none
# Require TLS 1.2 as a minimum (the recommended value; 1.3 where available).
az webapp config set \
--resource-group "${GROUP}" \
--name "${APP_WEB}" \
--min-tls-version 1.2 \
--output noneThe *.azurewebsites.net domain already comes with a valid TLS certificate, so your application is reachable over HTTPS from the very first second.
- Deploying code: ZIP deploy and deployment from Git
ZIP deploy: the direct method
It is the most used one in scripts and pipelines: you package the application and upload it.
# A minimal Node application that returns the status of the bookings website.
mkdir -p contoso-reservas && cd contoso-reservas
cat > index.js <<'EOF'
const http = require('http');
const port = process.env.PORT || 8080;
const version = process.env.VERSION_APP || 'unknown';
http.createServer((req, res) => {
if (req.url === '/salud') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ service: 'contoso-reservas', status: 'ok', version }));
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`<h1>Contoso Bookings</h1><p>Version ${version}</p>`);
}).listen(port);
EOF
cat > package.json <<'EOF'
{
"name": "contoso-reservas",
"version": "1.0.0",
"main": "index.js",
"scripts": { "start": "node index.js" }
}
EOF
zip -r ../contoso-reservas.zip . && cd ..
# Deploying the package.
az webapp deploy \
--resource-group rg-contoso-reservas-dev \
--name app-contoso-reservas-dev \
--src-path contoso-reservas.zip \
--type zipThree fundamental things about this code, because they are what trips everybody up the first time:
- The port comes in
process.env.PORT. App Service injects that variable and expects your application to listen there. If you listen on a fixed port, you will see the "Application Error" message with no further clue. - The application must listen on every interface, not only on
127.0.0.1. - App Service needs to know how to start it. With Node it uses
npm startorindex.js; if your entry point is something else, define it:
az webapp config set \
--resource-group rg-contoso-reservas-dev \
--name app-contoso-reservas-dev \
--startup-file "node index.js" \
--output noneFor Java, the artifact is a .jar or a .war:
az webapp deploy \
--resource-group rg-contoso-reservas-dev \
--name app-contoso-api-disponibilidad-dev \
--src-path api-disponibilidad.jar \
--type jarDeployment from Git
App Service includes a local Git repository you can push code to; each git push triggers a build and a deployment.
# 1. Enable the local Git repository and get the target URL.
GIT_URL=$(az webapp deployment source config-local-git \
--resource-group rg-contoso-reservas-dev \
--name app-contoso-reservas-dev \
--query url --output tsv)
# 2. User-scope publishing credentials (once per account).
az webapp deployment user set --user-name contoso-despliegue
# 3. Add the remote and push.
git remote add azure "${GIT_URL}"
git push azure main:masterIt can also connect to GitHub to deploy on every commit to a branch. That is convenient for getting started, but for production the right path is Azure Pipelines, with its environments and approvals: that is module 5, and we do not develop it here.
Check the result:
- Configuration: app settings and connection strings
A well-built application does not carry its configuration inside the code. In App Service there are two mechanisms, and both reach your process as environment variables.
| Mechanism | What for | How the application sees it |
|---|---|---|
| App settings | Any parameter: URLs, modes, timeouts, versions | An environment variable with the same name |
| Connection strings | Database connections, with a type (SQLAzure, MySQL, PostgreSQL, Custom) | A variable with a type-dependent prefix, for example SQLAZURECONNSTR_ |
# App settings for the bookings website.
az webapp config appsettings set \
--resource-group rg-contoso-reservas-dev \
--name app-contoso-reservas-dev \
--settings VERSION_APP=1.4.0 \
ENTORNO=desarrollo \
URL_API_DISPONIBILIDAD="https://app-contoso-api-disponibilidad-dev.azurewebsites.net" \
TIEMPO_ESPERA_MS=3000 \
--output table
# Connection string to the bookings database (module 3).
az webapp config connection-string set \
--resource-group rg-contoso-reservas-dev \
--name app-contoso-reservas-dev \
--connection-string-type SQLAzure \
--settings ReservasDb="Server=tcp:sql-contoso-reservas-dev.database.windows.net;Database=db-reservas;Authentication=Active Directory Default;" \
--output noneKey points to remember:
- Changing a setting restarts the application. It is neither instantaneous nor transparent: group them and apply them together.
- Settings override the ones in your application's configuration file with the same name. It is the mechanism that lets you use the same artifact in development and production.
- Each deployment slot can have its own settings; you can mark them as slot settings so that they do not travel on a swap (section 8).
Why secrets do NOT go here in production
App settings are encrypted at rest and do not appear in the code, which is already better than the usual alternative. But:
- Anyone with Contributor permission on the application can read them in clear text from the portal or with
az webapp config appsettings list. - They are exposed in template exports and configuration dumps.
- There is no automatic rotation, no expiry and no per-secret access audit.
The correct solution in Azure is Azure Key Vault, with references from the application that point to the secret instead of containing it:
# A preview of lesson 04-03: the value is a reference, not the secret.
az webapp config appsettings set \
--resource-group rg-contoso-reservas-pro \
--name app-contoso-reservas-pro \
--settings CLAVE_PASARELA_PAGO="@Microsoft.KeyVault(SecretUri=https://kv-contoso-seguridad.vault.azure.net/secrets/clave-pasarela-pago/)" \
--output noneFor this to work, the application needs a managed identity with read permission on the vault. Managed identities and RBAC are lesson 04-02; Key Vault, lesson 04-03. Contoso keeps the payment gateway key and the production connection string there, in rg-contoso-seguridad-pro. Here you only need to take away the rule: in development, settings; in production, Key Vault references.
- Custom domains and managed TLS certificates
Nobody sells tickets at app-contoso-reservas-pro.azurewebsites.net. Contoso uses the contosoairlines.example domain and wants the website at www.contosoairlines.example.
The process has three steps:
Step 1: prove that the domain is yours. Two DNS records are created at your provider (Azure DNS is covered in lesson 02-06):
| Type | Name | Value | What for |
|---|---|---|---|
CNAME |
www |
app-contoso-reservas-pro.azurewebsites.net |
Routing the traffic |
TXT |
asuid.www |
The application's verification identifier | Proving ownership |
# The application's custom domain verification identifier.
az webapp show \
--resource-group rg-contoso-reservas-pro \
--name app-contoso-reservas-pro \
--query customDomainVerificationId \
--output tsvStep 2: associate the domain with the application.
az webapp config hostname add \
--resource-group rg-contoso-reservas-pro \
--webapp-name app-contoso-reservas-pro \
--hostname www.contosoairlines.exampleStep 3: a managed TLS certificate, free and automatically renewed.
# 1. Create the App Service managed certificate for that host name.
az webapp config ssl create \
--resource-group rg-contoso-reservas-pro \
--name app-contoso-reservas-pro \
--hostname www.contosoairlines.example
# 2. Get its thumbprint and bind it with SNI.
THUMBPRINT=$(az webapp config ssl list \
--resource-group rg-contoso-reservas-pro \
--query "[?subjectName=='www.contosoairlines.example'].thumbprint" \
--output tsv)
az webapp config ssl bind \
--resource-group rg-contoso-reservas-pro \
--name app-contoso-reservas-pro \
--certificate-thumbprint "${THUMBPRINT}" \
--ssl-type SNIWhat you should know about the managed certificate: it is free, it renews itself, it requires the Basic tier or above, and it does not cover some cases (wildcards in certain configurations, domains without public verification). For those cases you upload your own certificate or use an App Service Certificate. The operational advantage is enormous: outages caused by expired certificates simply stop happening.
- Deployment slots and swap with warm-up
Here is, probably, the feature that most justifies paying for the Standard tier.
A deployment slot is an additional instance of the application, within the same plan, with its own host name and its own configuration. The swap exchanges the contents of two slots… without restarting the process or interrupting traffic.
graph LR
subgraph BEFORE["Before the swap"]
P1["Production slot<br/>version 1.4.0<br/>← real traffic"]
S1["preproduccion slot<br/>version 1.5.0<br/>← internal testing"]
end
subgraph AFTER["After the swap"]
P2["Production slot<br/>version 1.5.0<br/>← real traffic"]
S2["preproduccion slot<br/>version 1.4.0<br/>← ready to roll back"]
end
BEFORE -->|"az webapp deployment slot swap"| AFTER
GROUP="rg-contoso-reservas-pro"
APP="app-contoso-reservas-pro"
# 1. Create the pre-production slot by cloning the production configuration.
az webapp deployment slot create \
--resource-group "${GROUP}" \
--name "${APP}" \
--slot preproduccion \
--configuration-source "${APP}" \
--output none
# 2. Deploy the new version ONLY to the slot.
az webapp deploy \
--resource-group "${GROUP}" \
--name "${APP}" \
--slot preproduccion \
--src-path contoso-reservas-1.5.0.zip \
--type zip
# 3. Test the slot on its own URL, without affecting customers.
curl -s "https://${APP}-preproduccion.azurewebsites.net/salud"
# 4. Swap: the tested version goes to production.
az webapp deployment slot swap \
--resource-group "${GROUP}" \
--name "${APP}" \
--slot preproduccion \
--target-slot productionWhat the swap does exactly (and why there is no downtime)
The swap does not copy files: it changes the routing between two sets of workers that are already running. Before switching over, Azure runs a warm-up:
- It applies the destination slot's settings (the ones not marked as slot settings) to the source slot.
- It restarts the slot's workers with that configuration and waits for them to respond.
- Only when they respond correctly does it switch the routing.
This eliminates the cold start the first customer would suffer after a classic deployment. You can control the warm-up with specific settings:
az webapp config appsettings set \
--resource-group "${GROUP}" --name "${APP}" --slot preproduccion \
--settings WEBSITE_SWAP_WARMUP_PING_PATH="/salud" \
WEBSITE_SWAP_WARMUP_PING_STATUSES="200" \
WEBSITE_WARMUP_PATH="/salud" \
--output noneSlot settings
Some values must not travel on the swap: the test database connection string cannot end up in production. They are marked as slot settings:
az webapp config appsettings set \
--resource-group "${GROUP}" --name "${APP}" --slot preproduccion \
--slot-settings ENTORNO=preproduccion \
--output noneRollback and progressive deployment
- Rolling back means swapping again: the previous version is still alive in the other slot. It is the fastest rollback there is.
- Progressive (canary) deployment: you can send a percentage of real traffic to the slot before swapping.
# 10% of real traffic goes to the pre-production slot.
az webapp traffic-routing set \
--resource-group "${GROUP}" --name "${APP}" \
--distribution preproduccion=10
# Remove the split when the test finishes.
az webapp traffic-routing clear --resource-group "${GROUP}" --name "${APP}"One important warning: slots share the plan, meaning they share CPU and memory with production. A load test against the slot affects real customers. For serious load testing, use a separate plan.
- Vertical, horizontal and automatic scaling
In App Service you scale the plan, not the application.
# Vertical scaling: change tier (more CPU and memory per instance).
az appservice plan update \
--resource-group rg-contoso-reservas-pro \
--name plan-contoso-reservas-pro \
--sku P1v3
# Manual horizontal scaling: the number of instances.
az appservice plan update \
--resource-group rg-contoso-reservas-pro \
--name plan-contoso-reservas-pro \
--number-of-workers 4Autoscaling, with the same rule mechanics you saw in 02-02 but with no scale set to administer:
PLAN_ID=$(az appservice plan show \
-g rg-contoso-reservas-pro -n plan-contoso-reservas-pro --query id -o tsv)
az monitor autoscale create \
--resource-group rg-contoso-reservas-pro \
--resource "${PLAN_ID}" \
--name autoescala-plan-reservas \
--min-count 2 --max-count 10 --count 2 --output none
az monitor autoscale rule create \
--resource-group rg-contoso-reservas-pro \
--autoscale-name autoescala-plan-reservas \
--condition "CpuPercentage > 70 avg 5m" --scale out 2 --cooldown 5 --output none
az monitor autoscale rule create \
--resource-group rg-contoso-reservas-pro \
--autoscale-name autoescala-plan-reservas \
--condition "CpuPercentage < 30 avg 10m" --scale in 1 --cooldown 10 --output noneThe same golden rules apply: scale out fast, scale in slowly, a wide dead band and a scheduled profile for the season opening. And the same requirement: the application cannot hold local state. Since every instance on a plan shares the application's file system (mounted from storage), writing to it in the hot path is slow and fragile: the boarding passes go to Blob Storage (lesson 02-04) and the data to the database (module 3).
Two other plan features worth knowing:
- Always On: keeps the application awake. On Free and Basic the application is unloaded after a while without traffic and the next request suffers a cold start. Turn it on in production (
--always-on true, requires Basic or above). - Zone redundancy: available on Premium v3, it distributes the instances across availability zones. It is the option that satisfies Contoso's policy for critical components.
- Diagnostics: logs, log stream and SSH console
GROUP="rg-contoso-reservas-dev"
APP="app-contoso-reservas-dev"
# 1. Turn on application and web server logging to the file system.
az webapp log config \
--resource-group "${GROUP}" --name "${APP}" \
--application-logging filesystem \
--web-server-logging filesystem \
--detailed-error-messages true \
--failed-request-tracing true \
--level information \
--output none
# 2. Watch the log stream in real time (Ctrl+C to exit).
az webapp log tail --resource-group "${GROUP}" --name "${APP}"
# 3. Download the logs to analyze them.
az webapp log download --resource-group "${GROUP}" --name "${APP}" --log-file logs.zipaz webapp log tail is App Service's most profitable diagnostic tool: it shows live whatever your application writes to standard output. If your application does not start, that is where you will see the real reason (a missing dependency, the wrong port, a non-existent environment variable).
A console inside the application's container:
# SSH session against the instance (Linux plans only).
az webapp ssh --resource-group "${GROUP}" --name "${APP}"Inside you will find your code in /home/site/wwwroot. Remember: whatever you write outside /home is lost on a restart or when the application moves instance; it is the equivalent of a VM's temporary disk.
Other tools of the service, so that you know they exist:
- Diagnose and solve problems in the portal: automatic detectors for crashes, restarts, memory usage and HTTP errors.
- Kudu (
https://<app>.scm.azurewebsites.net): the service's advanced console, with a file explorer, processes and deployment logs. - Application Insights: the application's real telemetry (requests, dependencies, exceptions, performance). It is lesson 07-03 and it is what Contoso ends up using daily.
- Virtual network integration (an introduction)
By default, a Web App lives outside your virtual network: it receives traffic from the internet and goes out to the internet. Contoso needs the opposite in two directions:
- Outbound: the website and the API must reach the
sql-contoso-reservas-prodatabase without going through the internet. That is solved with virtual network integration, which connects the application's outbound traffic to a delegated subnet (snet-app). - Inbound: the Availability API should not be reachable from the internet, only from the network. That is solved with access restrictions or with a private endpoint.
# Outbound integration with a subnet delegated to App Service.
az webapp vnet-integration add \
--resource-group rg-contoso-reservas-pro \
--name app-contoso-reservas-pro \
--vnet vnet-contoso-pro \
--subnet snet-appWe deliberately leave it here: the design of vnet-contoso-pro, its subnets, the delegation, the network security groups and the private endpoints are the complete content of lesson 02-05, which comes in two lessons' time.
- Cleanup
# Delete the applications and the plan (the plan is what bills).
az webapp delete --resource-group rg-contoso-reservas-dev --name app-contoso-reservas-dev
az webapp delete --resource-group rg-contoso-reservas-dev --name app-contoso-api-disponibilidad-dev
az appservice plan delete --resource-group rg-contoso-reservas-dev --name plan-contoso-reservas-dev --yes
# Check that no plans are left billing in the subscription.
az appservice plan list --query "[].{Plan:name, Tier:sku.name, Instances:sku.capacity, Group:resourceGroup}" --output tableWe insist because it is the typical cost mistake: deleting the application does not delete the plan. A forgotten P1v3 plan costs more than a hundred euros a month without serving a single request.
Common Mistakes and Tips
- Believing that the application is billed. The plan is billed. Stopping the application saves nothing; deleting the plan (or dropping it to Free) does.
- Deleting applications and leaving the plan alive. It is App Service's most common phantom bill.
- Not listening on
process.env.PORT. It produces an unexplained "Application Error". It is the number one failure when deploying Node or Python for the first time. - Choosing the wrong operating system for the plan. Linux and Windows do not mix and a plan cannot be converted: you have to create another one.
- Putting secrets in app settings in production. Any contributor reads them in clear text. Use Key Vault references (04-03) with a managed identity (04-02).
- Changing settings one at a time in production. Each change restarts the application. Group them.
- Trying zero-downtime deployment on the Basic tier. There are no slots until Standard. It is the main reason to move up a tier.
- Forgetting to mark environment-specific settings as slot settings. On the swap, the pre-production configuration ends up in production. It happens, and it hurts.
- Load testing against a slot on the production plan. They share CPU: you harm real customers.
- Leaving Always On turned off in production. The first customer after a quiet spell pays for the cold start.
- Tip: turn on
--https-only trueand--min-tls-version 1.2on every application, including development ones. It costs two commands and it avoids a finding in the module 4 security review. - Tip: name your applications with an organization prefix (
app-contoso-…). The name is global and the generic ones are already taken.
Exercises
Exercise 1: deciding on plans and tiers
Contoso has four applications: the Contoso Bookings website (public, high and seasonal traffic), the Availability API (internal, high traffic at the peak), the ground staff's operations dashboard (internal, 40 users, office hours) and a test portal that Diego uses for demos.
- How many App Service plans would you create and which applications would you put on each one? Justify it.
- Which tier would you assign to each plan and what does that tier unlock?
- What configuration would you add to the production plan to comply with Contoso's policy on critical components?
Exercise 2: zero-downtime deployment
Write the complete sequence of Azure CLI commands to publish version 1.5.0 of Contoso Bookings without any customer seeing an error:
- Create the
preproduccionslot by cloning the production configuration. - Mark
ENTORNOas a setting that does not travel on the swap. - Configure the warm-up against
/salud. - Deploy the ZIP to the slot and verify it.
- Send 10% of real traffic to the slot during the validation.
- Swap, and explain how you would roll back if something goes wrong.
Exercise 3: diagnosing an application that does not start
Diego deploys the Availability API and https://app-contoso-api-disponibilidad-dev.azurewebsites.net returns "Application Error". List, in order, the steps and commands you would use to find the cause, and mention at least three probable causes with their fix.
Solutions
Solution 1:
- Three plans:
plan-contoso-reservas-pro: the bookings website. It is kept separate because its traffic is the highest and the most seasonal and we do not want its peak to affect anything else.plan-contoso-api-pro: the Availability API. It scales on a different profile (the API's peak is sharper than the website's) and it is worth isolating so that a load test or a website failure does not degrade it.plan-contoso-interno-pro: the operations dashboard and the test portal. Low, predictable traffic; they share a plan because the cost is the cost of the plan and this way only one is paid for.
- Tiers:
- The two public plans, Premium v3 (P1v3): deployment slots, autoscaling, zone redundancy and better per-instance performance.
- The internal plan, Standard (S1): enough for 40 users and it already includes slots and autoscaling.
- Zone redundancy on the production plans (available on Premium v3), with at least 2 instances, to comply with the "critical components in at least two availability zones" policy. On top of that,
always-onenabled andhttps-onlyon all of them.
Solution 2:
#!/usr/bin/env bash
set -euo pipefail
GROUP="rg-contoso-reservas-pro"
APP="app-contoso-reservas-pro"
# 1. Slot cloning the production configuration.
az webapp deployment slot create -g "${GROUP}" -n "${APP}" \
--slot preproduccion --configuration-source "${APP}" --output none
# 2. A setting that does NOT travel on the swap.
az webapp config appsettings set -g "${GROUP}" -n "${APP}" --slot preproduccion \
--slot-settings ENTORNO=preproduccion --output none
# 3. Warm-up against the health endpoint.
az webapp config appsettings set -g "${GROUP}" -n "${APP}" --slot preproduccion \
--settings WEBSITE_SWAP_WARMUP_PING_PATH="/salud" \
WEBSITE_SWAP_WARMUP_PING_STATUSES="200" --output none
# 4. Deploy and verify the slot.
az webapp deploy -g "${GROUP}" -n "${APP}" --slot preproduccion \
--src-path contoso-reservas-1.5.0.zip --type zip
curl -s "https://${APP}-preproduccion.azurewebsites.net/salud"
# 5. Progressive deployment: 10% of real traffic.
az webapp traffic-routing set -g "${GROUP}" -n "${APP}" --distribution preproduccion=10
# ... watch errors and latency ...
az webapp traffic-routing clear -g "${GROUP}" -n "${APP}"
# 6. Swap.
az webapp deployment slot swap -g "${GROUP}" -n "${APP}" \
--slot preproduccion --target-slot productionRollback: run the same swap command again. After the first swap, version 1.4.0 was left in preproduccion, alive and warmed up, so going back takes seconds and requires no redeployment.
Solution 3:
# 1. First of all, always: the live log stream.
az webapp log config -g rg-contoso-reservas-dev -n app-contoso-api-disponibilidad-dev \
--application-logging filesystem --level information --output none
az webapp log tail -g rg-contoso-reservas-dev -n app-contoso-api-disponibilidad-dev
# 2. Check the startup configuration and the stack.
az webapp config show -g rg-contoso-reservas-dev -n app-contoso-api-disponibilidad-dev \
--query "{Startup:appCommandLine, Stack:linuxFxVersion, AlwaysOn:alwaysOn}" -o json
# 3. Verify that the artifact is where it should be.
az webapp ssh -g rg-contoso-reservas-dev -n app-contoso-api-disponibilidad-dev
# inside: ls -la /home/site/wwwroot
# 4. Review the settings (is the application missing a variable it requires?).
az webapp config appsettings list -g rg-contoso-reservas-dev \
-n app-contoso-api-disponibilidad-dev -o tableThree probable causes and their fix:
| Cause | Symptom in the log | Fix |
|---|---|---|
| The application listens on a fixed port | The container does not respond and App Service restarts it in a loop | Listen on process.env.PORT (or server.port=${PORT} in Java) |
| The startup command is missing or the artifact name is not the expected one | "entry point not found" | az webapp config set --startup-file "java -jar /home/site/wwwroot/app.jar" |
| A mandatory environment variable is missing (for example, the connection string) | An exception while initializing the application context | Add it with az webapp config appsettings set (or a Key Vault reference in production) |
Conclusion
You now know how to publish applications in Azure without administering servers. You understand what App Service is and why Contoso puts Contoso Bookings and the Availability API there while the legacy engine stays on a VM. You have mastered the concept that costs the most money to ignore: the plan is billed, not the application, with its table of tiers and what each jump unlocks — Basic for a custom domain and TLS, Standard for slots and autoscaling, Premium v3 for zone redundancy. You know how to choose a runtime stack, create the plan and the applications with the CLI, and deploy code with ZIP deploy and Git, with the three first-deployment traps solved (port, interface and startup command). You configure the application with app settings and connection strings as environment variables, and you know why production secrets go to Key Vault by reference. You have set up a custom domain with a managed, auto-renewing certificate, you have published with no downtime using slots, warm-up, progressive deployment and immediate rollback, you know how to scale the plan vertically, horizontally and automatically, and you have the diagnostic tools (live log stream, log download, SSH and Kudu) for when something does not start.
There is one loose end left, and it has a name. Your application cannot store anything locally: not the session, not the files, and not the boarding passes in PDF that Contoso generates every time somebody buys a ticket. In module 1 you created sttarjetascontosodev and its tarjetas-embarque container, and you left one decision pending: which redundancy to use in development and which one in production.
The next lesson, Azure Storage: Blobs, Files, Queues and Tables, picks up that loose end and settles it completely: the four services of a storage account, the blob types and the Hot, Cool, Cold and Archive access tiers with their lifecycle rules — applied to boarding passes that get downloaded a lot in the first week and almost never afterwards — the full LRS, ZRS, GRS, GZRS and RA-GRS redundancy table with Contoso's final decision, and how to grant secure access to a file through temporary shared access signatures instead of handing out the account key.
Azure Course
Module 1: Introduction to Azure
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
