4. Getting Started
One task, three ways: create a storage account, make a container, upload a file, read it back, and delete everything. That's it. This page is deliberately throwaway — hard-coded names, no variables, no remote state, no pipeline. Everything production-shaped lives in Deployment.
Two rules that make cleanup trivial and are worth adopting permanently:
- Always create a throwaway resource group first, and delete that at the end. Deleting a resource group removes everything inside it in one call. It is genuinely cleaner than anything AWS gives you, and it's the reason a forgotten Azure demo costs less than a forgotten AWS one.
- Always use
--auth-mode loginonaz storagecommands. It authenticates as you via Entra ID instead of silently fetching the account key. It's the same auth model you'll use in production, and it will fail loudly if your RBAC is wrong — which is exactly what you want while learning.
[Image Prompt: 2D minimalistic diagram comparing three provisioning paths, Azure Portal, Azure CLI, and Terraform, converging on the same Azure storage account and container, flat design, clean vector art style, white background]
Before you start
You need an Azure subscription, az version 2.50 or later, and the ability to create role assignments
(Owner or User Access Administrator on the resource group) — because you're going to grant yourself a
data-plane role, which is the whole lesson of this page.
Storage account names are globally unique, 3–24 characters, lowercase letters and digits only — no
hyphens, no uppercase. Every example below uses stdemo$RANDOM; substitute your own and keep it
consistent.
Path 1 — the Azure Portal
Fast to grasp, not repeatable. Blade names are stable; exact button labels are not.
- Create a resource group. Search "Resource groups" → Create → name it
rg-blob-demo, pick a region near you (uksouth,eastus, whatever). - Create the storage account. Search "Storage accounts" → Create → select
rg-blob-demo, give it a globally unique lowercase name, choose Standard performance, Locally-redundant storage (LRS) for a demo, and leave the defaultStorageV2kind. On the Advanced tab, note where hierarchical namespace lives — leave it off, and register that this is the one setting you can never change later. - Grant yourself data access. On the account, open the Access Control (IAM) blade → Add role assignment → Storage Blob Data Contributor → assign to your own user. Being Owner is not enough, and discovering that here is the point.
- Create a container. On the account, open the Containers blade → + Container → name it
demo, leave public access set to Private. - Upload and read. Open
demo→ Upload → pick any small file. Click the blob to see its properties, tier, and URL. Change the Authentication method at the top of the container blade between "Access key" and "Microsoft Entra user account" and watch the difference — that toggle is the control-plane/data-plane split made visible.
Path 2 — Azure CLI
Copy-pasteable, repeatable, scriptable. This is the version to actually run.
# --- variables -------------------------------------------------------------
RG=rg-blob-demo
LOC=uksouth
ACCT=stdemo$RANDOM # must be globally unique, lowercase alphanumeric, 3-24 chars
CONTAINER=demo
# --- 1. throwaway resource group ------------------------------------------
az group create -n $RG -l $LOC
# --- 2. storage account ----------------------------------------------------
az storage account create \
--name $ACCT \
--resource-group $RG \
--location $LOC \
--sku Standard_LRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--allow-blob-public-access false
# --- 3. give YOURSELF a data-plane role -----------------------------------
# Control-plane Owner does not grant blob access. This is the lesson.
ACCT_ID=$(az storage account show -n $ACCT -g $RG --query id -o tsv)
ME=$(az ad signed-in-user show --query id -o tsv)
az role assignment create \
--assignee-object-id $ME \
--assignee-principal-type User \
--role "Storage Blob Data Contributor" \
--scope $ACCT_ID
# Role assignments take a moment to propagate. If the next command 403s, wait and retry.
sleep 30
# --- 4. container ----------------------------------------------------------
az storage container create \
--account-name $ACCT \
--name $CONTAINER \
--auth-mode login
# --- 5. upload -------------------------------------------------------------
echo "hello from blob storage" > hello.txt
az storage blob upload \
--account-name $ACCT \
--container-name $CONTAINER \
--name hello.txt \
--file hello.txt \
--auth-mode login
# --- 6. list and read back -------------------------------------------------
az storage blob list \
--account-name $ACCT \
--container-name $CONTAINER \
--auth-mode login \
-o table
az storage blob download \
--account-name $ACCT \
--container-name $CONTAINER \
--name hello.txt \
--file downloaded.txt \
--auth-mode login
cat downloaded.txt
Three optional experiments worth five minutes each
# (a) Change the access tier of a single blob, and see it reflected immediately.
az storage blob set-tier \
--account-name $ACCT -c $CONTAINER -n hello.txt \
--tier Cool --auth-mode login
# (b) Mint a user-delegation SAS — a time-limited link signed by Entra ID, not by the account key.
EXPIRY=$(date -u -d "1 hour" '+%Y-%m-%dT%H:%MZ') # macOS: date -u -v+1H '+%Y-%m-%dT%H:%MZ'
SAS=$(az storage blob generate-sas \
--account-name $ACCT -c $CONTAINER -n hello.txt \
--permissions r --expiry $EXPIRY \
--as-user --auth-mode login -o tsv)
curl "https://$ACCT.blob.core.windows.net/$CONTAINER/hello.txt?$SAS"
# (c) Prove the plane split: turn off shared-key access and watch key-based auth stop working.
az storage account update -n $ACCT -g $RG --allow-shared-key-access false
az storage blob list --account-name $ACCT -c $CONTAINER -o table # no --auth-mode login → fails
az storage blob list --account-name $ACCT -c $CONTAINER --auth-mode login -o table # still works
Experiment (c) is the one to actually run. It's the setting that turns "control plane vs. data plane" from a diagram into something you've felt.
Path 3 — Terraform (minimal)
The smallest declarative version of the same thing. The parameterised, production-shaped module — with remote state, environments, and a pipeline — is in Deployment.
# main.tf — hello-world only. Do not ship this.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "demo" {
name = "rg-blob-demo"
location = "uksouth"
}
resource "azurerm_storage_account" "demo" {
name = "stdemouniquename" # change me — globally unique, lowercase, no hyphens
resource_group_name = azurerm_resource_group.demo.name
location = azurerm_resource_group.demo.location
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = false
}
resource "azurerm_storage_container" "demo" {
name = "demo"
storage_account_id = azurerm_storage_account.demo.id
container_access_type = "private"
}
output "blob_endpoint" {
value = azurerm_storage_account.demo.primary_blob_endpoint
}
terraform init
terraform plan
terraform apply
Two things that will bite you here, and both are the plane split again:
azurerm_storage_containeris a data-plane operation. Your logged-in identity needsStorage Blob Data Contributor(or shared-key access left enabled) and network reachability to the blob endpoint.Contributoralone is not enough once you disable shared keys.- The
storage_account_idargument replaced the olderstorage_account_namein recentazurermversions. If you're on an older provider, usestorage_account_nameinstead ⚠️ verify against the provider version you've pinned.
Teardown — do this now, not later
# Terraform path
terraform destroy
# CLI path — one command removes the account, the container, and every blob
az group delete -n rg-blob-demo --yes --no-wait
Cleanup note.
az group deleteis usually the whole story, but not always. If you enabled an immutability policy or a legal hold, the delete will fail until the policy expires or the hold is released — by design. If the account had soft delete enabled at the account level, the name may stay reserved for a retention period after deletion. And half of all surprise cloud bills come from forgotten demo resources, so run this before you close the terminal.
Next: Deployment →
← Back to the Blob Storage overview · ← Previous: Architecture