Declare a private storage account in an ARM template and deploy it twice

short · 30 min · Skills measured 3.3

Task

Write a small ARM template, a JSON file that declares a private storage account and a private blob container that depends on it, and let Azure Resource Manager build it. You will see the four sections of a template (parameters, variables, resources, outputs), have Resource Manager validate the file and preview the change before anything exists, read the order it chose from the deployment's operations, and deploy the same template a second time to prove it is idempotent: same result, no duplicates, no errors. You finish by reading the same declaration in Bicep and by exporting a template from what you built.

Steps

  1. Open Cloud Shell (Bash), choose your subscription and create the resource group the template will deploy into:
az account set --subscription 00000000-0000-0000-0000-000000000000
az group create --name az900-lab15-rg --location eastus
  1. Write the template. It is plain JSON: parameters are supplied at deployment, variables are worked out inside it, resources are what to build, and outputs are returned at the end. The container declares that it depends on the account, so Resource Manager must build the account first.
cat > az900-lab15.json << 'EOF'
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageName": { "type": "string", "minLength": 3, "maxLength": 24 },
    "location": { "type": "string", "defaultValue": "[resourceGroup().location]" }
  },
  "variables": {
    "containerName": "notes"
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2023-05-01",
      "name": "[parameters('storageName')]",
      "location": "[parameters('location')]",
      "sku": { "name": "Standard_LRS" },
      "kind": "StorageV2",
      "properties": {
        "allowBlobPublicAccess": false,
        "minimumTlsVersion": "TLS1_2",
        "supportsHttpsTrafficOnly": true
      }
    },
    {
      "type": "Microsoft.Storage/storageAccounts/blobServices/containers",
      "apiVersion": "2023-05-01",
      "name": "[format('{0}/default/{1}', parameters('storageName'), variables('containerName'))]",
      "dependsOn": [ "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]" ],
      "properties": { "publicAccess": "None" }
    }
  ],
  "outputs": {
    "blobEndpoint": { "type": "string", "value": "[reference(parameters('storageName')).primaryEndpoints.blob]" }
  }
}
EOF
  1. Ask Resource Manager to check the file and preview the change before anything is built. Storage account names are global: replace the digits 48213 with four to six digits of your own, and use the same name everywhere below.
az deployment group validate --resource-group az900-lab15-rg --template-file az900-lab15.json --parameters storageName=az900lab1548213 --query properties.provisioningState -o tsv
az deployment group what-if --resource-group az900-lab15-rg --template-file az900-lab15.json --parameters storageName=az900lab1548213

The first prints Succeeded; the second lists the storage account and the container under Create. Nothing exists yet. A template with an error would be rejected here, before any resource was made. 4. Deploy it, then read the output the template returned and the operations Resource Manager ran:

az deployment group create --name az900-lab15-deploy --resource-group az900-lab15-rg --template-file az900-lab15.json --parameters storageName=az900lab1548213
az deployment group show --name az900-lab15-deploy --resource-group az900-lab15-rg --query properties.outputs.blobEndpoint.value -o tsv
az deployment operation group list --name az900-lab15-deploy --resource-group az900-lab15-rg --query '[].properties.targetResource.resourceType' -o tsv

The second command prints the account's blob endpoint, ending in .blob.core.windows.net/. The third lists both resource types: you submitted one file, and Resource Manager worked out the steps and their order from dependsOn. A script of az storage commands would be the imperative way to the same result: you would have written the steps yourself. 5. Deploy exactly the same template again, with the same deployment name, and count what exists:

az deployment group create --name az900-lab15-deploy --resource-group az900-lab15-rg --template-file az900-lab15.json --parameters storageName=az900lab1548213
az resource list --resource-group az900-lab15-rg --query 'length(@)' -o tsv

The second run succeeds without creating anything new, and the count is 1: the storage account. (The container is a child of the account, so it is not listed separately.) That is idempotence: the resources stay in the state the template describes. 6. See the same declaration two other ways. Decompile the JSON into Bicep, the simpler language that compiles back into ARM JSON, and export a template from what now exists, which is how you would reproduce a hand-built resource group elsewhere:

az bicep decompile --file az900-lab15.json
cat az900-lab15.bicep
az group export --name az900-lab15-rg --query 'resources[].type' -o tsv

In the portal, open az900-lab15-rg and choose Deployments: az900-lab15-deploy is listed as Succeeded, with the parameter value it ran with.

Verify

az deployment group show --name az900-lab15-deploy --resource-group az900-lab15-rg --query properties.provisioningState -o tsv

Expected output: Succeeded.

az resource list --resource-group az900-lab15-rg --query 'length(@)' -o tsv

Expected output: 1.

az storage account show --name az900lab1548213 --resource-group az900-lab15-rg --query allowBlobPublicAccess -o tsv

Expected output: false.

Cost and clean-up

A deployment costs nothing in itself; you pay for what it builds. The storage account holds no data and costs next to nothing while it exists, but charges are possible, so clean up today.

Created by this lab: the resource group az900-lab15-rg and its deployment history entry az900-lab15-deploy; the storage account az900lab1548213 (with your own digits) and its private container notes; the files az900-lab15.json and az900-lab15.bicep in your Cloud Shell session.

  1. Delete the resource group. That deletes the storage account az900lab1548213, its container notes and the deployment history entry az900-lab15-deploy with it. Lesson 1's budget is not touched.
az group delete --name az900-lab15-rg --yes
  1. Remove the two files. An ephemeral Cloud Shell session discards them anyway when it ends:
rm az900-lab15.json az900-lab15.bicep

Check it is gone

az group exists --name az900-lab15-rg

Expected output: false.

Notes

az bicep decompile prints a warning that decompiling is a best-effort conversion. That is expected: the point is to compare the Bicep file with the JSON one, not to keep it.

The exported template lists more types than you declared, such as the account's blob, file, queue and table services. Azure created those defaults with the account; an export describes everything that exists, which is why it is a starting point to adjust rather than a finished template.

This is an independent study companion for the Microsoft Azure Fundamentals (AZ-900) exam and is not produced by or endorsed by Microsoft. Microsoft, Azure and Microsoft Entra are trademarks of the Microsoft group of companies.

This is an independent study companion for Microsoft Azure Fundamentals AZ-900 and is not produced by or endorsed by CompTIA.