Background
Sections
IntroductionFoundations1. Resource Hierarchy2. Resource Manager3. Identity and RBAC4. Regions and Availability5. Naming and TaggingVirtual Machines1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetVirtual Network1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetBlob Storage1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure SQL Database1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Kubernetes Service1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Container Registry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetMicrosoft Entra ID1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure RBAC1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Functions1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAPI Management1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure App Configuration1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Machine Learning1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Monitor1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure AI Foundry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and Cheatsheet

4. Getting Started

7 min read

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 login on az storage commands. 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 default StorageV2 kind. 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 assignmentStorage 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 demoUpload → 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:

  1. azurerm_storage_container is a data-plane operation. Your logged-in identity needs Storage Blob Data Contributor (or shared-key access left enabled) and network reachability to the blob endpoint. Contributor alone is not enough once you disable shared keys.
  2. The storage_account_id argument replaced the older storage_account_name in recent azurerm versions. If you're on an older provider, use storage_account_name instead ⚠️ 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 delete is 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