Capstone: deploy a governed, monitored storage account, then audit the subscription clean

capstone · 75 min · Skills measured 3.4

Task

Run a small, realistic piece of management and governance from start to finish, using every lesson of skill area 3. You estimate the resource before it exists (lesson 12), give it a resource group with a cost centre tag, and make the tag compulsory with a built-in Azure Policy deny rule and the group undeletable with a Delete lock (lesson 13). You work in Cloud Shell and the portal (lesson 14), deploy the storage account from an ARM template (lesson 15), and watch it with a Service Health alert, a metric alert rule, Resource Health, metrics explorer and Advisor (lesson 16). Then you take it all down, and finish the course by auditing the whole subscription: no az900-* resource group, lock or policy assignment from any lab may remain, and lesson 1's budget must still be there.

Steps

  1. Prepare, and take stock before you build. Open Cloud Shell (Bash), choose your subscription, confirm lesson 1's budget is still there, and list any resource groups earlier labs may have left behind:
az account set --subscription 00000000-0000-0000-0000-000000000000
az consumption budget list --query '[].name' -o tsv
az group list --query "[?starts_with(name, 'az900-')].name" -o tsv

The first list prints your lesson 1 budget's name. Write down any az900- group the second one prints: the clean-up removes it. Then, in the Azure pricing calculator, estimate one Standard LRS storage account in East US with a small capacity, so you have a figure to compare with Cost Management later. Do not save or share the estimate. 2. Create the capstone's resource group with its tags, require the cost centre tag on everything inside it, and lock the group against deletion:

az group create --name az900-cap16-rg --location eastus --tags CostCenter=CC-1001 Environment=Lab
az policy definition show --name 871b6d14-10aa-478d-b590-94f262ecfa99 --query displayName -o tsv
az policy assignment create --name az900-cap16-require-costcenter --display-name "AZ-900 capstone require CostCenter" --policy 871b6d14-10aa-478d-b590-94f262ecfa99 --scope "$(az group show --name az900-cap16-rg --query id -o tsv)" --params '{"tagName": {"value": "CostCenter"}}'
az lock create --name az900-cap16-nodelete --lock-type CanNotDelete --resource-group az900-cap16-rg

The second command prints Require a tag on resources, a built-in definition with the deny effect. The tag on the group does not flow down, so from now on every resource you create in this group must carry its own CostCenter tag. 3. Write the ARM template. It declares one private storage account and gives it the cost centre tag from a parameter, so the policy is satisfied by design:

cat > az900-cap16.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 },
    "costCenter": { "type": "string", "defaultValue": "CC-1001" },
    "location": { "type": "string", "defaultValue": "[resourceGroup().location]" }
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2023-05-01",
      "name": "[parameters('storageName')]",
      "location": "[parameters('location')]",
      "tags": { "CostCenter": "[parameters('costCenter')]", "Environment": "Lab" },
      "sku": { "name": "Standard_LRS" },
      "kind": "StorageV2",
      "properties": {
        "allowBlobPublicAccess": false,
        "minimumTlsVersion": "TLS1_2",
        "supportsHttpsTrafficOnly": true
      }
    }
  ],
  "outputs": {
    "storageId": { "type": "string", "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]" }
  }
}
EOF
  1. Preview and deploy it. Replace the digits 48213 with four to six digits of your own, and use that name everywhere below:
az deployment group what-if --resource-group az900-cap16-rg --template-file az900-cap16.json --parameters storageName=az900cap1648213
az deployment group create --name az900-cap16-deploy --resource-group az900-cap16-rg --template-file az900-cap16.json --parameters storageName=az900cap1648213
SA_ID=$(az deployment group show --name az900-cap16-deploy --resource-group az900-cap16-rg --query properties.outputs.storageId.value -o tsv)
echo "$SA_ID"

What-if shows one resource to create; the deployment succeeds; the last line prints the account's resource ID, kept in SA_ID for step 6. 5. Test the governance. At least ten minutes after step 2, so the assignment has taken effect, try the imperative way without a tag, using a second name (your digits, with a t in front of them):

az storage account create --name az900cap16t48213 --resource-group az900-cap16-rg --location eastus --sku Standard_LRS --allow-blob-public-access false

It fails with RequestDisallowedByPolicy: your role allows the request, and the policy denies it. Next, confirm the lock is in place, and only if this prints CanNotDelete, try to delete the group:

az lock show --name az900-cap16-nodelete --resource-group az900-cap16-rg --query level -o tsv
az group delete --name az900-cap16-rg --yes

It fails with ScopeLocked: the Delete lock stops even an Owner. Both protections held, and neither one depended on which tool you used. 6. Add monitoring. Each create carries the CostCenter tag, because the policy denies anything in this group without one:

az monitor action-group create --name az900-cap16-ag --resource-group az900-cap16-rg --short-name az900cap16 --action email learner you@example.com --tags CostCenter=CC-1001
az monitor activity-log alert create --name az900-cap16-health --resource-group az900-cap16-rg --scope "/subscriptions/$(az account show --query id -o tsv)" --condition category=ServiceHealth --action-group az900-cap16-ag --tags CostCenter=CC-1001
az monitor metrics alert create --name az900-cap16-transactions --resource-group az900-cap16-rg --scopes "$SA_ID" --condition "total Transactions > 1000" --window-size 1h --evaluation-frequency 1h --action az900-cap16-ag --tags CostCenter=CC-1001 --description "Unexpectedly busy storage account"

Put your own email address in place of you@example.com. The first alert tells you about Azure's problems; the second about your own resource. Both send through the same action group. If SA_ID is empty because the session timed out, run the SA_ID= line from step 4 again. 7. Look at it all in the portal. Open the storage account: under Help, Resource health reads Available; under Monitoring, Metrics charts Transactions, a numeric time series. Open Advisor and filter to az900-cap16-rg: it may have recommendations for the new account, or none. Open Policy, then Compliance, and select AZ-900 capstone require CostCenter. Open Monitor, then Alerts, then Alert rules: both rules are listed. Open Cost Management, then Cost analysis, and group by the CostCenter tag; data for the account can take up to a day to appear, and the calculator's estimate from step 1 was the forward-looking figure for the same thing.

Verify

The account was deployed from the template with its tag, the untagged second account was refused, the lock holds, and both alerts are enabled.

az storage account show --name az900cap1648213 --resource-group az900-cap16-rg --query tags.CostCenter -o tsv

Expected output: CC-1001.

az storage account list --resource-group az900-cap16-rg --query 'length(@)' -o tsv

Expected output: 1.

az lock show --name az900-cap16-nodelete --resource-group az900-cap16-rg --query level -o tsv

Expected output: CanNotDelete.

az monitor activity-log alert show --name az900-cap16-health --resource-group az900-cap16-rg --query enabled -o tsv

Expected output: true.

az monitor metrics alert show --name az900-cap16-transactions --resource-group az900-cap16-rg --query enabled -o tsv

Expected output: true.

Cost and clean-up

A metric alert rule is charged while it exists, and the storage account costs next to nothing while it holds no data; charges are possible for both, so do the clean-up today. Policy assignments, locks, tags, activity log alerts and deployments are free.

Created by this lab: the resource group az900-cap16-rg with its tags; the policy assignment az900-cap16-require-costcenter on that group; the lock az900-cap16-nodelete on that group; the storage account az900cap1648213 (with your own digits) and the deployment history entry az900-cap16-deploy; the action group az900-cap16-ag, the Service Health alert az900-cap16-health and the metric alert az900-cap16-transactions; the file az900-cap16.json in your Cloud Shell session.

Kept on purpose: lesson 1's budget and its alert, kept on purpose: the audit below checks it is still there.

  1. Remove the lock first: it blocks the resource group delete.
az lock delete --name az900-cap16-nodelete --resource-group az900-cap16-rg
  1. Remove the policy assignment, so nothing outlives the group it was made for:
az policy assignment delete --name az900-cap16-require-costcenter --scope "$(az group show --name az900-cap16-rg --query id -o tsv)"
  1. Delete the resource group. That deletes the storage account az900cap1648213, the deployment history entry az900-cap16-deploy, the action group az900-cap16-ag, the alert az900-cap16-health and the alert az900-cap16-transactions. Then remove the template file az900-cap16.json:
az group delete --name az900-cap16-rg --yes
rm az900-cap16.json
  1. Audit the whole subscription for anything any lab in this course left behind. List what is left:
az group list --query "[?starts_with(name, 'az900-')].name" -o tsv
az lock list --query "[?starts_with(name, 'az900-')].id" -o tsv
az policy assignment list --disable-scope-strict-match --query "[?starts_with(name, 'az900-')].id" -o tsv

For each resource group the first command prints, put its name in place of az900-labNN-rg below: remove any locks in it, then delete it.

az lock list --resource-group az900-labNN-rg --query '[].id' -o tsv
az lock delete --ids LOCK-ID
az group delete --name az900-labNN-rg --yes

Put each ID the lock list printed in place of LOCK-ID. Remove any az900- policy assignment that is still listed with az policy assignment delete --name NAME --scope SCOPE, where NAME is the last part of the ID it printed and SCOPE is everything before /providers/Microsoft.Authorization. Repeat the three lists until all of them print nothing. 5. Keep lesson 1's budget. Leave it and its alert exactly as they are: they keep watching this subscription after the course ends, which is the point of having set them first. If you exported a pricing calculator estimate, delete the spreadsheet from your computer.

Check it is gone

The capstone's group is gone, nothing from any lab remains, and the budget is still there.

az group exists --name az900-cap16-rg

Expected output: false.

az group list --query "[?starts_with(name, 'az900-')].name" -o tsv

Expected output: nothing (an empty result).

az lock list --query "[?starts_with(name, 'az900-')].name" -o tsv

Expected output: nothing (an empty result).

az policy assignment list --disable-scope-strict-match --query "[?starts_with(name, 'az900-')].name" -o tsv

Expected output: nothing (an empty result).

az consumption budget list --query 'length(@)' -o tsv

Expected output: 1 or more, because lesson 1's budget is kept on purpose.

Notes

If the untagged account in step 5 is created instead of refused, the assignment had not taken effect yet. That is harmless: it is in the capstone's group and goes with it at clean-up. The Verify count of storage accounts then reads 2; wait a few minutes and repeat step 5 with another name to see the denial.

Why each piece belongs to which lesson: the estimate and the tag are lesson 12; the policy (what a resource may look like) and the lock (what nobody may delete) are lesson 13; Cloud Shell, the CLI and the portal are lesson 14; the template is lesson 15; Resource Health, metrics, the two alert rules, their action group and Advisor are lesson 16. Microsoft Purview, also lesson 13, governs the data inside resources rather than the resources themselves, so it has no step here.

If an alert create fails with MissingSubscriptionRegistration, register the provider once with az provider register --namespace Microsoft.Insights, wait until az provider show --namespace Microsoft.Insights --query registrationState -o tsv prints Registered, and run the create again.

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.