AZ-104 · Skill Domain

Compute & Storage

Windows and Linux VMs, managed disks, App Service with blue/green deployment slots, Azure Blob Storage with private endpoints, Container Registry, and Container Apps with traffic splitting and scale-to-zero — the full compute and data tier.

4Projects
0Stored credentials
2Container images
P4–P12Phase 2 + 3 + 4
01

Projects

04
Blob Storage · SAS · Lifecycle · Private Endpoint
Azure Blob Storage
LRS + GRS redundancy · SAS tokens · lifecycle policies · static website · private endpoint lockdown

Deployed two storage accounts demonstrating different redundancy tiers — LRS for active workloads, GRS for disaster recovery — then built out the full access control surface: SAS tokens, stored access policies, lifecycle management rules, and a static website. Finished by removing the storage account from the public internet entirely using a private endpoint and split-horizon DNS, confirming that the same hostname resolves to a private IP inside the VNet and returns a 404 (not 403) from the public internet.

Two SAS token types with fundamentally different security postures:

Type Auth basis Revocable? Use case
Ad-hoc SAS Account key No — must rotate account key Never in production
Policy-linked SAS Stored access policy Yes — delete the policy All production use

Ad-hoc SAS is like cash — once issued, it cannot be recalled before expiry. Policy-linked SAS is like a credit card — compromise means deleting the policy, not disrupting all other applications sharing the account key.

Storage accounts & access control — project4-storage-accounts.svg Lifecycle policy & redundancy — project4-lifecycle-redundancy.svg
  • Storage account names — no hyphens, globally unique — Names must be lowercase, 3–24 chars, no hyphens, unique across all of Azure. st-az104-dev-wus3-01 is invalid. staz104devwus301 is correct. Include an owner prefix to avoid collisions with other tenants.
  • Confirm-then-lockdown sequence for private endpoints — Deploy endpoint → verify private IP resolution via nslookup → disable public access → verify public returns 404. Re-enable before teardown — CLI commands from Cloud Shell cannot reach a storage account with public access disabled.
  • 404 is the stronger security posture — When public access is disabled, Azure Storage returns 404, not 403. A 403 reveals the resource exists. A 404 reveals nothing. This is intentional Azure behavior.
  • Soft delete — re-enable before testing deletion — With soft delete enabled, deleted blobs are retained for the retention period and can be undeleted. Demonstrated by deleting a blob, viewing it in the soft-deleted state, and restoring it via the undelete operation.
⚠ Gotcha — Private DNS zone must link to ALL VNets

Linking only to vnet-hub means vnet-spoke VMs query Azure DNS without the private zone override and receive the public IP. DNS resolution and network routing are independent — peering establishes the route; VNet DNS zone links determine which IP is returned. Both must be configured.

Blob Storage LRS / GRS SAS Tokens Stored Access Policies Lifecycle Management Static Website Private Endpoint Soft Delete
SAS token expiry
SAS token expiry
Public access disabled
Public access disabled
Soft deleted blob
Soft deleted blob
Undelete blob
Blob undelete operation
07
VM · Extensions · Disk Management · Availability
Virtual Machines & Extensions
Windows + Linux · Custom Script Extension · BGInfo · disk attach/resize/snapshot · availability zones

Deployed a cross-zone pair of VMs — Windows Server 2025 Azure Edition in Zone 1, Ubuntu 24.04 in Zone 2 — demonstrating zone-redundant architecture. Both VMs deployed with no public IPs; all access via Bastion or az vm run-command. Extensions automated post-deployment configuration without requiring interactive access: Custom Script Extension installed IIS on Windows, BGInfo wrote system metadata to the desktop wallpaper. Disk operations covered attach, UUID-based mount (never device name), online resize, and snapshot.

Extensions are agents that run inside the VM after deployment. The Azure platform delivers them without requiring any interactive session:

  • Custom Script Extension — Fetches a script from a storage blob and executes it. Used to install IIS and configure a test page. The CSE agent makes an anonymous HTTP fetch — use service SAS (account key auth), not user delegation SAS, or the download returns 403.
  • BGInfo — Writes hostname, IP address, OS version, and uptime to the desktop wallpaper. No longer in the portal marketplace; deploy via CLI only: az vm extension set --name BGInfo --publisher Microsoft.Compute.
  • MDE.Windows / MDE.Linux — Microsoft Defender for Endpoint deploys automatically on Visual Studio subscriptions via Defender for Cloud. Leave in place — it provides security telemetry.
View architecture diagram — project7-architecture.svg
  • Windows computerName — 15 character limit — Azure resource name can be any length, but Windows OS hostname is hard-limited to 15 characters. vm-win-dev-wus3-01 is 18 chars and fails at provisioning. Pass computerName as a separate explicit parameter in Bicep — do not rely on take(vmName, 15) for multi-VM deployments, which produces duplicate names.
  • Linux disk mount — UUID, never device name — Device names (/dev/sda, /dev/sdb) can shift on reboot or when another disk is added. UUID never changes. Always use UUID in fstab with the nofail flag — without it, a missing disk prevents the VM from booting.
  • Disk resize is two separate stepsaz disk update --size-gb expands the block device at the Azure layer. sudo resize2fs expands the filesystem inside the OS to use the new space. One without the other leaves the disk unusable at the expanded size.
  • Never mount data disks at /mnt — Azure Linux agent mounts the temporary disk at /mnt by default. Mounting a data disk there creates a conflict — after reboot the temp disk wins. Use /data or any other path.
⚠ Gotcha — Windows Server 2025 hotpatch settings

patchMode: 'AutomaticByOS' fails with InvalidParameter on Windows Server 2025 Azure Edition. Required config: patchMode: 'AutomaticByPlatform', enableHotpatching: true inside patchSettings, with rebootSetting: 'IfRequired'. The enableHotpatching property belongs inside patchSettings, not directly in windowsConfiguration.

Virtual Machines Availability Zones Custom Script Extension BGInfo Managed Disks Disk Snapshots Azure Bastion Run Command
08
App Service · Deployment Slots · Container Instances
App Service & Container Instances
P0v3 plan · staging slot · blue/green swap · slot-sticky settings · ACI nginx · zero-downtime deployment

Deployed an App Service plan with a Python 3.11 web app and a staging deployment slot, demonstrating blue/green deployment: deploy a new version to staging, validate it, then swap to production in under 30 seconds with zero downtime. Slot-sticky app settings stay with the slot during swaps — so the ENVIRONMENT variable always reflects the correct environment regardless of which code revision is running. Also deployed an Azure Container Instance running nginx to demonstrate the lightest-weight compute option in Azure: no VM, no plan, a running container in under 60 seconds.

App Service is like a managed apartment building — Azure handles infrastructure; you move in your app. A deployment slot is a second apartment in the same building. Swapping exchanges the front doors: staging becomes production and production becomes staging. No one notices the move happened.

Swap staging → production
az webapp deployment slot swap \
-g rg-az104-dev-wus3-01 \
-n app-az104-dev-wus3-01 \
--slot staging \
--target-slot production
View architecture diagram — project8-architecture.svg
  • P0v3 minimum for deployment slots — Deployment slots require Standard tier or higher. P0v3 is the smallest non-legacy tier that supports slots (~$41/mo). Free and Basic tiers are shown as "Legacy" in the current portal — Standard is the correct starting point for any production-pattern deployment.
  • Slot-sticky settings stay with the slot — The ENVIRONMENT app setting is marked as a slot setting so it doesn't swap with the code. Production always shows ENVIRONMENT=production and staging always shows ENVIRONMENT=staging regardless of which version is deployed where. This is how you track what's running where without hardcoding it in the application.
  • Managed identity at creation time--assign-identity "[system]" at az webapp create time. Assigning after creation introduces a timing window where an RBAC assignment might be attempted before the identity GUID exists in Entra ID.
  • KV reference strings — use Cloud Shell, not PowerShell — The @ symbol and parentheses in Key Vault reference values cause parsing failures in PowerShell even inside quotes. Use Cloud Shell Bash for any az webapp config appsettings set commands containing KV references.
Container Instances vs App Service vs VMs

ACI — Single container, job-style or short-lived workloads. No persistent storage, no autoscale. Fastest cold start. App Service — Platform-managed PaaS. Ideal for web apps and APIs where you want deployment slots, autoscale, and built-in CI/CD without managing infrastructure. VMs — Full OS control required, legacy software, non-HTTP workloads, or when you need to configure the network stack directly.

App Service Deployment Slots Blue/Green Deploy Slot-Sticky Settings Container Instances Managed Identity Key Vault References
App Service slots
Production + staging slots
Slot sticky setting
Slot-sticky env variable
App after slot swap
Post-swap — production promoted
ACI nginx logs
ACI nginx access logs
12
ACR · Container Apps · Traffic Splitting · Scale-to-Zero
Container Registry & Container Apps
ACR · CAE VNet-injected · managed identity · 80/20 traffic split · min 0 replicas · Bicep throughout

Deployed a complete container workload using infrastructure-as-code throughout — no portal clicks for resource creation. The stack: Azure Container Registry (acrdevwus301) storing two image versions built via different methods, a VNet-injected Container Apps Environment on a dedicated /23 subnet delegated to Microsoft.App/environments, and a Container App running nginx with a system-assigned managed identity, scale-to-zero configuration, and an 80/20 canary traffic split between revisions. No credentials stored anywhere in the deployment chain.

  • Local Docker build + push (v1) — Build happens on the local machine. Docker Engine stamps the image, tags it with the full registry path, pushes each layer to ACR. Layers from prior pushes are reused. Requires Docker Desktop running locally.
  • ACR Tasks cloud build (v2)az acr build uploads the Dockerfile and context to Azure. ACR spins up a build agent, executes the Dockerfile, pushes the result — all in the cloud. No local Docker daemon required. This is how CI/CD pipelines build images.
View architecture diagram — project12-architecture.svg
  • Scale to zero — minReplicas: 0 — The Container App runs zero instances when idle. Cost drops to zero. First request cold-starts a new instance in seconds. For workloads with uneven demand — internal tools, APIs called during business hours — this is a significant cost advantage over App Service, which idles at a fixed price regardless of traffic.
  • Canary deployment via traffic splitting — 80% of requests route to the stable v1 revision, 20% to the new v2 revision. If v2 shows problems, shift 100% back to v1 without redeployment. Traffic weights are managed via CLI post-deploy (az containerapp ingress traffic set) — not in Bicep, because revision names are auto-generated at deploy time and unknown at template authoring time.
  • Two-pass deployment for role assignment — System-assigned managed identity only exists after the Container App is created, so the AcrPull role assignment cannot happen in the same deployment pass. Production-grade solution: user-assigned managed identity created as infrastructure before the app, with the role pre-assigned. The identity exists independently of any app.
  • Internal CAE is immutable — The Container Apps Environment was declared internal at creation time and cannot be changed afterward. Redeploy required to change to external. This was a deliberate design choice following the no-public-IP posture established across all lab resources.
⚠ Gotcha — subnet delegation is a prerequisite

The target subnet must be delegated to Microsoft.App/environments before the Container Apps Environment can be created. Minimum subnet size is /23 (512 addresses). Delegation belongs in the subnet creation command and bootstrap script — not discovered at deploy time from a failed provisioning attempt.

Azure Container Registry ACR Tasks Container Apps Container Apps Environment Managed Identity Traffic Splitting Scale-to-Zero VNet Injection Bicep
ACR overview
ACR overview
ACR v1 and v2 images
ACR — v1 + v2 images
Traffic split 80/20
80/20 traffic split
Scale to zero
Scale — min 0, max 3
CAE overview
Container Apps Environment
Container App overview
Container App overview
02

Lessons Learned

Ad-hoc SAS cannot be revoked
A compromised ad-hoc SAS token cannot be cancelled — it remains valid until expiry. The only remediation is rotating the storage account key, which breaks every other application using that key. Policy-linked SAS ties to a stored access policy that can be deleted instantly, revoking all tokens derived from it without touching the account key.
Device names shift; UUIDs never do
Linux device names (/dev/sda, /dev/sdb) are assigned at boot time and can change when disks are added or removed. A fstab entry using a device name can cause a VM to fail to boot after adding a second disk. UUID-based mounts are stable across reboots and disk topology changes. Always use UUID with the nofail flag.
CSE agent cannot use user delegation SAS
The Custom Script Extension agent makes an anonymous HTTP fetch to download script blobs. User delegation SAS tokens require an Entra ID token to be presented — the CSE agent has no AD context and returns 403. Use service SAS (account key auth) for CSE downloads. Production pattern: managed identity with Storage Blob Data Reader role.
Slot-sticky settings are independent of code
Deployment slot swaps move code between slots. Slot-sticky settings stay with their slot regardless of which code version is running. This is how you maintain environment-aware configuration — ENVIRONMENT=production always lives in the production slot, not in the application code. Forgetting to mark a setting as slot-sticky causes it to follow the code into production during a swap.
Container Apps traffic weights belong in CLI, not Bicep
Bicep traffic weight configuration requires hardcoding the current revision name, which is auto-generated by Azure at deploy time. This means you cannot know the name when authoring the template. Traffic splitting is an operational concern that changes frequently — it belongs in az containerapp ingress traffic set post-deploy, not in the infrastructure definition.
Verify installation; never trust exit codes alone
apt install nginx can exit 0 while silently failing to download packages when there's no outbound internet. The Standard Load Balancer SNAT issue in Project 5 was only discovered via systemctl status nginx — the install appeared successful. This pattern applies everywhere: always verify service state, not just package manager exit status, after installing software on a newly provisioned VM.