Storage & Networking
.mdVerified against Azure CLI 2.87.0, flags verified via `az storage account management-policy create · official docs
What it is and where it fits 🎯#
Storage accounts and Blob Storage are Azure's object-storage primitive — the direct analogue of AWS S3 or
GCS buckets — and Azure's networking stack (VNets, subnets, NSGs, load balancers, private endpoints) is
what everything from page 02's VMs and AKS clusters actually runs inside. This page assumes the identity
and resource-group basics from page 01.
Storage accounts#
az storage account create \
--resource-group my-rg --name mystorageaccount \
--sku Standard_LRS --location eastus \
--kind StorageV2 --https-only true --min-tls-version TLS1_2
az storage account list --resource-group my-rg --output table
az storage account show --name mystorageaccount --query "primaryEndpoints"A storage account name must be globally unique across all of Azure (not just your subscription) — a
create failing with a name-already-taken error is common and not a permissions problem. --sku sets the
replication strategy: Standard_LRS (locally redundant, cheapest, single datacenter), Standard_ZRS
(zone-redundant within a region), Standard_GRS/Standard_RAGRS (geo-redundant to a paired region, the
latter with read access to the secondary). --https-only true and --min-tls-version TLS1_2 are worth
setting explicitly rather than trusting the account default, especially for anything holding regulated
data.
Blob containers#
az storage container create --account-name mystorageaccount --name my-container --auth-mode login
az storage container list --account-name mystorageaccount --auth-mode login
az storage container show --account-name mystorageaccount --name my-container --auth-mode login--auth-mode login uses your az login identity (with the right RBAC role, e.g. Storage Blob Data Contributor) instead of a storage account key — prefer it over the legacy key-based auth mode for anything
beyond a quick local test, since account keys are long-lived, full-access secrets with no per-user audit
trail.
Uploading and downloading blobs#
az storage blob upload --account-name mystorageaccount --container-name my-container --name file.txt --file ./file.txt --auth-mode login
az storage blob download --account-name mystorageaccount --container-name my-container --name file.txt --file ./file.txt --auth-mode login
az storage blob list --account-name mystorageaccount --container-name my-container --auth-mode login --output table
az storage blob delete --account-name mystorageaccount --container-name my-container --name file.txt --auth-mode loginTip
For bulk transfers (many files, or large files), reach for AzCopy instead of az storage blob upload/download in a loop — it's Microsoft's dedicated high-throughput data-movement tool (parallel
chunked transfers, resumable on failure) and is what az storage itself shells out to internally for some
operations. az storage blob is fine for one-off files and scripting glue; AzCopy is the right tool once
you're moving real volume.
Static website hosting#
az storage blob service-properties update --account-name mystorageaccount \
--static-website --index-document index.html --404-document 404.html
az storage blob upload-batch --account-name mystorageaccount \
--destination '$web' --source ./out --auth-mode login
az storage account show --name mystorageaccount --query "primaryEndpoints.web" -o tsvEnabling --static-website creates a special $web container and gives the account a dedicated static-site
endpoint — the same pattern a Next.js output: "export" build (or any static-export site) targets when
hosting directly on Azure instead of behind a CDN-fronted bucket on another cloud. upload-batch uploads a
whole local directory tree in one call, preserving the relative path structure as blob names.
Blob lifecycle management policies#
az storage account management-policy create \
--account-name mystorageaccount --resource-group my-rg \
--policy @lifecycle-policy.json
az storage account management-policy show --account-name mystorageaccount --resource-group my-rgThe --policy argument takes a JSON document (rules for tiering to cool/archive or deleting blobs after N
days, matched by name prefix or blob index tags) — there's no flag-based way to define individual rules,
you always author the full policy document and apply it in one call:
{
"rules": [
{
"name": "archive-old-logs",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["logs/"] },
"actions": {
"baseBlob": {
"tierToCool": { "daysAfterModificationGreaterThan": 30 },
"tierToArchive": { "daysAfterModificationGreaterThan": 90 },
"delete": { "daysAfterModificationGreaterThan": 365 }
}
}
}
}
]
}Generating a SAS (Shared Access Signature) token#
az storage blob generate-sas \
--account-name mystorageaccount --container-name my-container --name file.txt \
--permissions r --expiry 2026-09-01T00:00Z --auth-mode login --as-user --full-uri
az storage account generate-sas \
--account-name mystorageaccount \
--services b --resource-types sco --permissions rwl --expiry 2026-09-01T00:00Z--auth-mode login --as-user generates a user delegation SAS, signed with your Entra ID identity
instead of a long-lived storage account key — the recommended approach, since the resulting token is scoped
to your own RBAC permissions and stops working the moment the underlying delegation key is rotated (Azure
rotates it automatically). az storage account generate-sas mints an account-level SAS instead, signed with
the account key directly — broader in scope and, because it isn't tied to any Entra identity, still valid
until its stated expiry even if the person who generated it loses all their RBAC roles in the meantime.
Scope --permissions/--resource-types as tightly as the use case allows either way.
Virtual networks and subnets#
az network vnet create --resource-group my-rg --name my-vnet --address-prefixes 10.0.0.0/16
az network vnet subnet create --resource-group my-rg --vnet-name my-vnet --name my-subnet --address-prefixes 10.0.1.0/24
az network vnet subnet create --resource-group my-rg --vnet-name my-vnet --name aks-subnet \
--address-prefixes 10.0.2.0/23 --service-endpoints Microsoft.Storage Microsoft.KeyVault
az network vnet list --resource-group my-rg --output table--service-endpoints extends a subnet's identity into Azure's backbone network for the named services —
traffic to a service endpoint-enabled resource (storage, Key Vault) stays on Microsoft's network instead of
traversing the public internet, and lets you restrict that resource's firewall to "only this VNet/subnet"
rather than an IP allowlist. Private Endpoints (below) are the more complete, newer version of this idea.
VNet peering#
az network vnet peering create \
--resource-group my-rg --name hub-to-spoke \
--vnet-name hub-vnet --remote-vnet spoke-vnet \
--allow-vnet-access --allow-forwarded-traffic
az network vnet peering create \
--resource-group my-rg --name spoke-to-hub \
--vnet-name spoke-vnet --remote-vnet hub-vnet \
--allow-vnet-access --allow-forwarded-traffic
az network vnet peering list --resource-group my-rg --vnet-name hub-vnet --output tablePeering is not transitive and must be created on both sides — peering VNet A to VNet B does not
automatically let a third VNet C (peered separately to B) reach A. This is the standard trap in hub-and-
spoke topologies: two spokes peered only to the hub cannot talk to each other unless the hub actively
forwards traffic between them (--allow-forwarded-traffic on the hub's peerings, plus a route table or
Azure Firewall doing the actual forwarding) or the spokes are peered directly to each other too.
Network security groups (NSGs)#
az network nsg create --resource-group my-rg --name my-nsg --location eastus
az network nsg rule create \
--resource-group my-rg --nsg-name my-nsg --name allow-https \
--priority 100 --access Allow --direction Inbound --protocol Tcp \
--destination-port-ranges 443 --source-address-prefixes '*'
az network nsg rule create \
--resource-group my-rg --nsg-name my-nsg --name deny-all-inbound \
--priority 4096 --access Deny --direction Inbound --protocol '*' \
--destination-port-ranges '*' --source-address-prefixes '*'
az network vnet subnet update --resource-group my-rg --vnet-name my-vnet --name my-subnet --network-security-group my-nsgAn NSG is attached to a subnet or a NIC, not to a VNet directly — the Azure equivalent of an AWS security
group, but rule priority (lower number = evaluated first, first match wins, 100–4096 range) is explicit
and load-bearing here in a way AWS security groups don't require. There is also an implicit, unremovable
DenyAllInBound/AllowVnetInBound/AllowInternetOutBound rule set at priority 65000+ that always applies
last — an explicit low-priority Allow rule is what actually opens traffic, not the absence of a Deny.
Public IPs and Load Balancer#
az network public-ip create --resource-group my-rg --name my-pip --sku Standard --allocation-method Static
az network lb create \
--resource-group my-rg --name my-lb --sku Standard \
--public-ip-address my-pip --frontend-ip-name my-frontend --backend-pool-name my-backend-pool
az network lb rule create \
--resource-group my-rg --lb-name my-lb --name http-rule \
--protocol Tcp --frontend-port 80 --backend-port 80 \
--frontend-ip-name my-frontend --backend-pool-name my-backend-poolStandard SKU is the current recommended default for both the public IP and the load balancer (Basic is
being phased out and lacks availability-zone support) — mixing SKUs between the IP and the LB is not
allowed, both must match.
Private Endpoints#
az network private-endpoint create \
--resource-group my-rg --name my-storage-pe \
--vnet-name my-vnet --subnet my-subnet \
--private-connection-resource-id $(az storage account show --name mystorageaccount --query id -o tsv) \
--group-id blob --connection-name my-storage-connectionA Private Endpoint gives a PaaS resource (storage account, Key Vault, SQL Database) a private IP address directly inside your VNet — traffic never touches the public internet or even Azure's public backbone the way a service endpoint's traffic does, and the resource's firewall can be set to reject all public access entirely. This is the strongest-isolation option of the three networking-to-PaaS patterns on this page (public with IP allowlist → service endpoint → private endpoint, in increasing order of isolation).
Real-world scenario: hosting a static site on Blob Storage behind a CDN#
az storage account create --resource-group my-rg --name mysitestorage --sku Standard_LRS --kind StorageV2
az storage blob service-properties update --account-name mysitestorage --static-website --index-document index.html
az storage blob upload-batch --account-name mysitestorage --destination '$web' --source ./out --auth-mode login
az cdn profile create --resource-group my-rg --name my-cdn --sku Standard_Microsoft
az cdn endpoint create --resource-group my-rg --profile-name my-cdn --name my-site \
--origin mysitestorage.z13.web.core.windows.net --origin-host-header mysitestorage.z13.web.core.windows.netThis is the Azure-native equivalent of the S3-plus-CloudFront pattern many static sites use on AWS —
$web's own endpoint serves directly, but fronting it with Azure CDN adds edge caching, a custom domain,
and (with the CDN's managed certificate) HTTPS on that domain without provisioning your own cert.
Real-world scenario: locking down a storage account to a specific VNet#
az storage account update --resource-group my-rg --name mystorageaccount \
--default-action Deny
az storage account network-rule add --resource-group my-rg --account-name mystorageaccount \
--vnet-name my-vnet --subnet my-subnetCaution
Setting --default-action Deny takes effect immediately and blocks the Azure Portal's own blob browser,
az storage CLI calls without --auth-mode login from an allowed network, and any integration that
reaches the account over the public internet — including, easy to forget, GitHub Actions runners and
most local developer machines. Add every network rule (VNet/subnet, specific public IP ranges) the
account genuinely needs reachable before flipping the default action to Deny, and test from an
allowed network before considering the change done.
CI/CD integration recipe: deploy a static site to Blob Storage from GitHub Actions#
# .github/workflows/deploy-static-site.yml
name: Deploy static site
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- run: az storage blob upload-batch --account-name mysitestorage --destination '$web' --source ./out --overwrite --auth-mode login
- run: az cdn endpoint purge --resource-group my-rg --profile-name my-cdn --name my-site --content-paths '/*'The CDN purge step matters for the same reason this site's own make deploy invalidates CloudFront — a
static-site deploy that only updates origin storage without purging the CDN edge cache can serve stale
content for the cache's full TTL.
Common pitfalls#
- Storage account name collisions — the namespace is global across all of Azure, not per-subscription.
- Treating VNet peering as transitive — it isn't; see the peering section above.
- Forgetting NSG rule priority ordering — a broad low-priority Deny above a more specific Allow silently wins; higher-priority (lower-numbered) rules are evaluated first.
- Locking a storage account's network rules before testing reachability — see the CAUTION above; this can lock out CI and the Portal simultaneously.
- Mixing
BasicandStandardSKUs between a public IP and its load balancer — not allowed, and the error message doesn't always make the SKU mismatch obvious at a glance.
Exit codes#
0 success · non-zero on any API/validation error — a storage operation with --auth-mode login failing
with an authorization error usually means the RBAC role (Storage Blob Data Contributor, etc.) hasn't
propagated yet or wasn't assigned at the right scope; re-check with az role assignment list --scope <storage-account-resource-id>.
When to reach for something else#
For a CDN with a broader global edge network and finer cache-control tooling than Azure CDN's Standard
tier, teams sometimes front Azure origins with Cloudflare or another third-party CDN instead — evaluate
against Azure Front Door (Microsoft's more full-featured CDN/WAF product) before reaching outside Azure
entirely. For network topology as reviewable code rather than a sequence of az network calls, model VNets,
peerings, and NSGs in Bicep/Terraform, consistent with the same declarative-IaC guidance on pages 01/02.