Some Resources Exist Above the Resource Group? Use Deployment Scopes

Published on:

The CloudTrips storage template starts at resource-group scope, so the target resource group must already exist. Creating it separately with az group create means Bicep still cannot provision the complete environment from scratch.

A resource group belongs to a subscription, not to another resource group. Create a subscription-scoped entry point for that higher-level resource, then use a module to deploy the existing workload into the resource group.

Understand the Scope Boundary

targetScope tells Azure Resource Manager where a Bicep deployment starts and which resource types the file can declare directly.

targetScope Typical responsibility Azure CLI command
resourceGroup Storage accounts, key vaults, and application resources az deployment group
subscription Resource groups and subscription-wide governance az deployment sub
managementGroup Governance shared by multiple subscriptions az deployment mg
tenant Tenant-wide resources such as management groups az deployment tenant

One Bicep file has one target scope. A module can target another scope through its scope property.

Create the Subscription Entry Point

Create subscription.bicep beside main.bicep:

targetScope = 'subscription'

@allowed([
  'dev'
  'test'
  'prod'
])
param environment string

param location string = deployment().location
param resourceGroupName string
param storageSku string = 'Standard_LRS'
param owner string
param costCenter string
param containerNames array = [
  'uploads'
]
param deployAuditContainer bool = false
param applicationName string = 'CloudTrips'

var commonTags = {
  Application: applicationName
  CostCenter: costCenter
  Environment: toUpper(environment)
  ManagedBy: 'Bicep'
  Owner: owner
}

resource workloadResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
  name: resourceGroupName
  location: location
  tags: commonTags
}

module workload './main.bicep' = {
  name: '${deployment().name}-workload'
  scope: workloadResourceGroup
  params: {
    applicationName: applicationName
    containerNames: containerNames
    costCenter: costCenter
    deployAuditContainer: deployAuditContainer
    environment: environment
    location: location
    owner: owner
    storageSku: storageSku
  }
}

output resourceGroupId string = workloadResourceGroup.id
output storageAccountName string = workload.outputs.storageAccountName
output storageAccountId string = workload.outputs.storageAccountId
output blobEndpoint string = workload.outputs.blobEndpoint
output containerIds array = workload.outputs.containerIds
output auditContainerId string? = workload.outputs.?auditContainerId

The resource group is declared directly because it belongs to the subscription scope. main.bicep remains resource-group scoped. Setting scope: workloadResourceGroup tells Azure to run that module inside the resource group and also creates an implicit dependency on it.

Add the TEST Scope Parameters

Create environments/test.subscription.bicepparam:

using '../subscription.bicep'

param environment = 'test'
param location = 'westeurope'
param resourceGroupName = 'rg-cloudtrips-bicep-test-weu'
param storageSku = 'Standard_LRS'
param owner = 'DmytroKlymenko@cloudtrips.onmicrosoft.com'
param costCenter = 'CC-TEST-100'
param containerNames = [
  'uploads'
  'exports'
]
param deployAuditContainer = true

This points to the new subscription entry point while preserving the TEST workload values used in the earlier trips.

Validate and Preview Both Scopes

From cloudtrips-bicep, run:

az bicep lint \
  --file subscription.bicep

az bicep build-params \
  --file environments/test.subscription.bicepparam \
  --stdout > /dev/null

az account set \
  --subscription "<CloudTrips TEST subscription name or ID>"

az deployment sub what-if \
  --name deploy-cloudtrips-test-scope \
  --location westeurope \
  --parameters environments/test.subscription.bicepparam

Because the TEST resource group already exists, the preview should update it only if its tags differ. It must not delete or recreate the resource group, storage account, Blob service, or containers. The storage provider defaults identified in the previous trip can still appear as what-if noise.

Subscription-scope what-if preview for the existing CloudTrips TEST environment

Deploy from the Subscription

Run the subscription deployment:

az deployment sub create \
  --name deploy-cloudtrips-test-scope \
  --location westeurope \
  --parameters environments/test.subscription.bicepparam \
  --query "properties.{state:provisioningState,outputs:outputs}" \
  --output yaml

--location is required because Azure stores subscription-deployment metadata in a region. It does not change the scope. Reusing the same deployment name later also requires the same metadata location.

Confirm that the state is Succeeded and that the outputs contain the resource group and storage values.

Verify the Deployment Chain

One command now creates three deployment records:

  1. deploy-cloudtrips-test-scope at subscription scope
  2. deploy-cloudtrips-test-scope-workload in the resource group
  3. deploy-cloudtrips-test-scope-workload-storage for the storage module

The suffixes come from the module name expressions. The records show the scope transitions; they do not deploy three copies of the resources.

Check the parent:

az deployment sub show \
  --name deploy-cloudtrips-test-scope \
  --query "properties.provisioningState" \
  --output tsv

Then list the resource-group deployments:

az deployment group list \
  --resource-group rg-cloudtrips-bicep-test-weu \
  --query "[?starts_with(name, 'deploy-cloudtrips-test-scope')].{name:name,state:properties.provisioningState}" \
  --output table

Successful subscription, workload, and storage-module deployment records for CloudTrips TEST

CloudTrips can now provision the resource-group boundary and its workload from one Bicep entry point. Higher-scope resources stay at subscription scope, while the existing application resources remain correctly isolated inside their resource group.