Skip to main content

Theoretical Foundation: Modify an existing Azure Resource Manager template


1. Initial Intuition​

In the previous module, we learned to read and interpret ARM Templates and Bicep files. Now the goal is different: given an existing template, how do you modify it safely and effectively to meet new requirements?

Think of the office blueprint analogy again. You have a blueprint that worked perfectly to create offices in ten cities. Now the company wants to add a dedicated server room, change the furniture from wood to metal, and make the meeting room size configurable per city. You don't redo the blueprint from scratch: you modify the existing one in a controlled manner, testing the changes before applying them to all cities.

Modifying an existing template involves three complementary skills: understanding what the template does today, knowing what types of changes have what impact on existing resources, and applying changes so that the deployment is safe and idempotent.


2. Context​

Template modification is the most common activity in the infrastructure as code lifecycle. Templates are rarely created and never touched again. As requirements evolve, the template needs to evolve with them. The difference between a mature team and a beginner team in IaC is precisely in how they manage these changes:

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

The AZ-104 specifically evaluates the ability to take an existing template and modify it to add resources, make values parameterizable, add outputs, or fix configurations.


3. Building the Concepts​

3.1 Types of Modification and Their Impact​

Not every modification has the same impact. Understanding the category of change is the first step in assessing risk:

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Critical point about removal: removing a resource from the template in Incremental mode (default) does not delete the resource in Azure. In Complete mode, the resource is deleted. This means that the behavior of a removal is completely different depending on the deployment mode.

3.2 Properties that Cause Resource Recreation​

Some Azure resource properties are immutable: if you change them, Azure needs to delete and recreate the resource. --what-if will show these changes as - Delete followed by + Create.

Examples of immutable properties that cause recreation:

ResourceImmutable propertyImpact
Storage Accountkind (e.g., Storage to StorageV2)Recreation with data loss
Cosmos DBkind, apiProperties.serverVersionRecreation
AKSagentPoolProfiles[].vmSizeNode pool recreation
VNetaddressSpace (reduction)Blocked if there are conflicting subnets
App Service Plankind (Windows to Linux)Recreation
SQL ServerCannot change nameNew resource

Before modifying structural properties of critical resources, always run --what-if to check if ARM will flag a recreation.

3.3 Common Modifications: Patterns and Techniques​

Convert hardcoded value to parameter​

Before (ARM Template with hardcoded location):

"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "minhaconta",
"location": "brazilsouth",
...
}
]

After (with parameterized location):

"parameters": {
"location": {
"type": "string",
"defaultValue": "[resourceGroup().location]",
"metadata": {
"description": "Region for resource deployment"
}
}
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "minhaconta",
"location": "[parameters('location')]",
...
}
]

The default value [resourceGroup().location] uses the location of the resource group where the template is deployed, which is a good practice to avoid region issues.

Add a new resource to the template​

The most common scenario: a template that creates a storage account needs to be modified to also create a blob container within it.

Before:

"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[parameters('storageAccountName')]",
"location": "[parameters('location')]",
"sku": { "name": "Standard_LRS" },
"kind": "StorageV2",
"properties": {}
}
]

After (with blob service and container added):

"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[parameters('storageAccountName')]",
"location": "[parameters('location')]",
"sku": { "name": "Standard_LRS" },
"kind": "StorageV2",
"properties": {}
},
{
"type": "Microsoft.Storage/storageAccounts/blobServices",
"apiVersion": "2023-01-01",
"name": "[concat(parameters('storageAccountName'), '/default')]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
],
"properties": {
"deleteRetentionPolicy": {
"enabled": true,
"days": 7
}
}
},
{
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
"apiVersion": "2023-01-01",
"name": "[concat(parameters('storageAccountName'), '/default/', parameters('containerName'))]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storageAccountName'), 'default')]"
],
"properties": {
"publicAccess": "None"
}
}
]

The naming pattern for child resources in ARM uses / as separator: storageAccountName/default/containerName.

The same modification in Bicep​

Bicep makes this hierarchical pattern much more readable with parent:

// Add to existing Bicep
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount // symbolic reference to parent resource
name: 'default'
properties: {
deleteRetentionPolicy: {
enabled: true
days: 7
}
}
}

param containerName string = 'meu-container'

resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobService
name: containerName
properties: {
publicAccess: 'None'
}
}

Add an output​

An existing template that creates a storage account may need to return the endpoint for use in a pipeline:

// ARM: add to outputs section
"outputs": {
"blobEndpoint": {
"type": "string",
"value": "[reference(parameters('storageAccountName')).primaryEndpoints.blob]"
},
"storageAccountId": {
"type": "string",
"value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
}
}
// Bicep: add at end of file
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob
output storageAccountId string = storageAccount.id

Add tags to all resources​

Tags are important for organization and cost. Add a tags parameter and apply to all resources:

// ARM: add parameter
"parameters": {
"tags": {
"type": "object",
"defaultValue": {
"ambiente": "producao",
"projeto": "sistema-x",
"gerenciado-por": "iac"
}
}
},
// On each resource:
"tags": "[parameters('tags')]"
// Bicep: parameter with default value
param tags object = {
ambiente: 'producao'
projeto: 'sistema-x'
geradoPor: 'iac'
}

// On each resource, add:
tags: tags

Use the if() function for conditional resources​

A common scenario is wanting to create a resource only in production, not in development:

// ARM: condition on resource
{
"type": "Microsoft.Insights/diagnosticSettings",
"condition": "[equals(parameters('ambiente'), 'prod')]",
"name": "diagnostic-settings",
"apiVersion": "2021-05-01-preview",
"scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]",
"properties": { ... }
}
// Bicep: condition on resource
param ambiente string = 'dev'

resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (ambiente == 'prod') {
name: 'diag-storage'
scope: storageAccount
properties: { ... }
}

Use copy to create multiple instances (ARM) or for (Bicep)​

Create multiple subnets in a VNet:

// ARM: copy element
{
"type": "Microsoft.Network/virtualNetworks/subnets",
"apiVersion": "2023-05-01",
"name": "[concat(parameters('vnetName'), '/', parameters('subnets')[copyIndex()].name)]",
"copy": {
"name": "subnetCopy",
"count": "[length(parameters('subnets'))]"
},
"dependsOn": [
"[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
],
"properties": {
"addressPrefix": "[parameters('subnets')[copyIndex()].prefix]"
}
}
// Bicep: for loop (much more readable)
param subnets array = [
{ name: 'subnet-frontend', prefix: '10.0.1.0/24' }
{ name: 'subnet-backend', prefix: '10.0.2.0/24' }
{ name: 'subnet-database', prefix: '10.0.3.0/24' }
]

resource subnetResources 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = [for subnet in subnets: {
parent: vnet
name: subnet.name
properties: {
addressPrefix: subnet.prefix
}
}]

4. Structural View​

The Safe Modification Process​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

5. Practical Operation​

Get ARM Template from an Existing Resource​

One of the most practical sources of templates to modify is exporting the template from resources that already exist in Azure:

Via portal: Resource Group > Export template (exports all RG resources) or [Specific Resource] > Export template

Via CLI:

# Export template from a complete resource group
az group export \
--name rg-producao \
--output-format json > template-exportado.json

# Export template from a specific resource
az resource show \
--ids /subscriptions/<sub-id>/resourceGroups/rg-producao/providers/Microsoft.Storage/storageAccounts/minhaconta \
--output json > recurso-exportado.json

Attention: templates exported by the portal often contain hardcoded values, omitted passwords (replaced by null), and some runtime properties that should not be in the template. Always review and clean the exported template before using it as a base.

Decompilation: From ARM to Bicep​

When you have an existing ARM Template and want to work in Bicep:

az bicep decompile --file template-exportado.json

This generates an equivalent .bicep file. The decompilation quality is good but not perfect: the generated file may need manual adjustments, especially in templates with copy loops, complex reference(), or runtime properties.

Check Property Changes with --what-if​

After modifying a template that affects an existing resource, --what-if is indispensable:

az deployment group what-if \
--resource-group rg-producao \
--template-file main.bicep \
--parameters @parameters.prod.json \
--result-format FullResourcePayloads

The --result-format FullResourcePayloads parameter shows the complete resource payload after changes, useful for checking exactly which properties will be changed.

The color-coded result:

  • + Create: new resource
  • ~ Modify: existing resource that will be changed
  • - Delete: resource that will be deleted (only in Complete mode)
  • = Nochange: resource without changes
  • x Ignore: resource ignored by ARM (managed externally)

6. Implementation Methods​

6.1 VS Code with Extensions​

When to use: day-to-day editing. VS Code with the right extensions transforms the template editing experience.

Essential extensions:

  • Bicep (Microsoft): autocomplete, real-time validation, resource visualization for Bicep
  • ARM Tools (Microsoft): schema validation, snippets and IntelliSense for ARM JSON
  • Azure Resource Manager (ARM) Template Viewer: graphical template visualization

Advantages: immediate error feedback, autocomplete for resource types, properties and values, integrated linting.

6.2 Azure CLI: Validation and Deployment​

# Validate the modified template BEFORE deploying
az deployment group validate \
--resource-group rg-producao \
--template-file main.bicep \
--parameters @parameters.prod.json

# What-If to see impact BEFORE applying
az deployment group what-if \
--resource-group rg-producao \
--template-file main.bicep \
--parameters @parameters.prod.json

# Deploy with interactive what-if confirmation
az deployment group create \
--resource-group rg-producao \
--template-file main.bicep \
--parameters @parameters.prod.json \
--confirm-with-what-if

The --confirm-with-what-if flag runs what-if first and asks for confirmation before proceeding with deployment. It's the safest flow for interactive production deployments.

6.3 Azure Portal: Template Editor​

The portal has an integrated editor at Create a resource > Deploy a custom template > Build your own template in the editor. Useful for minor edits or quick tests, but doesn't replace the version control flow for production changes.


7. Control and Security​

Version Control for Templates​

Templates should be in Git with the same rigor as application code:

  • Each modification in a separate branch
  • Pull Request with review before merge
  • CI/CD pipeline that validates and deploys automatically after merge
  • Tags for stable template versions

Never modify a production template directly without going through the PR process.

Maintain the Principle of Idempotency​

A modified template should continue to be idempotent: deploying the same template multiple times should always result in the same state, without errors on the second deployment.

Common idempotency problems after modifications:

  • Using newGuid() in a resource name: generates different name each deployment, creating duplicate resources
  • Checking conditions on resources that should be unique: if the condition changes between deployments, the resource may be created or deleted unexpectedly
  • Properties that ARM cannot compare correctly, generating unnecessary "updates"

Testing the Modification in an Isolated Environment​

Before applying a modification in production, always deploy to a dedicated test resource group:

# Create temporary test RG
az group create \
--name rg-teste-modificacao \
--location brazilsouth

# Deploy modified template to test RG
az deployment group create \
--resource-group rg-teste-modificacao \
--template-file main-modificado.bicep \
--parameters @parameters.dev.json

# Verify result
az resource list \
--resource-group rg-teste-modificacao \
--output table

# Clean up after test
az group delete --name rg-teste-modificacao --yes --no-wait

8. Decision Making​

When to modify template vs. create a new one?​

SituationDecisionReason
Add configuration to existing resourceModifyLower impact, incremental evolution
Add new resources to environmentModifyKeep everything in same template for coherence
Radical architectural change (e.g.: swap LB for App Gateway)New template or experimental branchRisk of impact on existing resources
Refactoring to use modulesNew structure with modules + testsStructural change that doesn't affect final resources
Third-party template that cannot be editedTemplate Spec or wrapper moduleEncapsulate the external template

Modify ARM JSON vs. convert to Bicep?​

SituationDecisionReason
Simple template, team uses ARMModify ARM JSONLess effort
Complex template with lots of duplicationConvert to BicepBicep drastically reduces verbosity
Team new to IaCConvert to BicepBetter DX and learning curve
Integration that only accepts ARM JSONKeep ARM, use Bicep as sourceBicep compiles to ARM; use Bicep as source and compile

9. Best Practices​

Use --confirm-with-what-if in all production deploys: the cost of reviewing the what-if is seconds; the cost of an unexpected modification can be hours of recovery.

Maintain one parameter file per environment, never mix values: parameters.dev.json, parameters.staging.json, parameters.prod.json. Never use the same file for different environments, even if only one value changes.

When parameterizing a hardcoded value, preserve the original value as default: if the template had "location": "brazilsouth" hardcoded and you're going to parameterize it, make it "defaultValue": "brazilsouth". This ensures that a deploy without passing the new parameter continues to work with the same behavior as the original template.

Document each modification with comments (in Bicep, use //):

// Added in 2024-03: soft delete configuration for blobs
// Issue #123: compliance requirement for 7-day retention
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount
name: 'default'
properties: {
deleteRetentionPolicy: {
enabled: true
days: 7
}
}
}

Check the apiVersion when adding new properties: a new property may require a more recent apiVersion. If you add a property and the deploy fails with an unrecognized property error, check if the resource's apiVersion supports that property.


10. Common Errors​

Adding parameter without default value and breaking existing CI/CD deploys

A new parameter newFeatureEnabled is added to the template without defaultValue. The CI/CD pipeline doesn't pass this parameter and the deploy fails with a required parameter not provided error. Always add defaultValue to new parameters that are not provided by all existing pipelines.

Changing the name of an existing resource without understanding the impact

The template had "name": "minha-storage" and was modified to "name": "[parameters('storageName')]", with the parameter receiving "nova-storage". On the next deploy, ARM creates nova-storage as a new resource and the minha-storage resource remains (Incremental mode) or is deleted (Complete mode). It's not a "rename": it's creating a new resource. Azure resources cannot be renamed.

Modifying the apiVersion of a resource without checking breaking changes

Updating apiVersion from 2020-01-01 to 2023-05-01 can change the behavior of some properties or make properties mandatory that were previously optional. Always consult the resource provider's API changelog when updating the version.

Removing a property expecting the value to return to default

In many Azure resources, removing a property from the template doesn't reset the value to default: ARM maintains the current value of the resource. To reset a property to default, you need to explicitly define it with the default value in the template.

Directly editing the template exported from the portal without cleaning

Templates exported by the portal often contain: runtime properties (like etags, generated ids), properties that ARM doesn't accept in creation, and hardcoded values that should be parameters. Using the exported template directly without review can cause deploy errors or create accidental dependencies on specific values from the origin environment.


11. Operation and Maintenance​

Track Modification History​

ARM's deploy history records each deploy with name, timestamp, and status. To track modifications over time, use descriptive deploy names:

az deployment group create \
--resource-group rg-producao \
--template-file main.bicep \
--parameters @parameters.prod.json \
--name "add-blob-container-issue-123"

With descriptive names, the deploy history becomes a timeline of modifications:

az deployment group list \
--resource-group rg-producao \
--query "[].{Nome:name, Data:properties.timestamp, Status:properties.provisioningState}" \
--output table

Compare Current State with Template​

To verify if the current state of resources is synchronized with the template (drift detection):

az deployment group what-if \
--resource-group rg-producao \
--template-file main.bicep \
--parameters @parameters.prod.json \
--mode Complete

If the what-if in Complete mode shows changes, it means the current state has diverged from the template (someone modified resources manually or via portal). The indicated changes are what would be necessary to synchronize the real state with the template.


12. Integration and Automation​

Template Validation Pipeline in PR​

A pipeline that runs on each Pull Request to validate modifications:

# .github/workflows/validate-template.yml
name: Validate Template Changes

on:
pull_request:
paths: ['infrastructure/**']

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Lint Bicep
run: az bicep build --file infrastructure/main.bicep

- name: Validate against dev
run: |
az deployment group validate \
--resource-group rg-dev \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/parameters.dev.json

- name: What-If against staging
run: |
az deployment group what-if \
--resource-group rg-staging \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/parameters.staging.json \
--output table

This pipeline ensures that no modification with syntax errors or that breaks the staging deploy reaches the main branch.

Reusable Modules for Standardization​

When modifying multiple templates that share patterns, extract the pattern to a module:

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

When you modify the storage.bicep module, all templates that import it automatically receive the modification on the next deploy, ensuring consistency.


13. Final Summary​

Essential points:

  • Modifying an existing template requires understanding the change impact: adding is low risk, changing properties can cause resource recreation, removing has different behavior in Incremental vs. Complete mode.
  • The --what-if is indispensable before any modified template deploy in production. It shows exactly which resources will be created (+), modified (~), deleted (-) or unchanged (=).
  • When parameterizing a hardcoded value, always preserve the original value as defaultValue to not break existing deploys that don't pass the new parameter.

Critical differences:

  • Renaming a resource in the template doesn't rename the Azure resource: creates a new one with the new name and the old one remains (Incremental mode) or is deleted (Complete mode).
  • Removing a property from the template often doesn't reset the value to default: ARM maintains the current value of the resource. To reset, explicitly define the default value.
  • az deployment group validate checks syntax and schema but doesn't simulate impact. --what-if simulates the real impact on existing resources.
  • Template exported from portal contains runtime properties and hardcoded values that need cleanup before use.

What needs to be remembered:

  • When adding a new child resource (e.g.: blob container within storage account), use parent: in Bicep or the parent/child name pattern in ARM, and ensure explicit dependency via dependsOn or reference().
  • Resources with immutable properties (e.g.: kind in storage accounts) cause recreation when changed. The --what-if signals this as -Delete + +Create.
  • The az bicep decompile command converts ARM JSON to Bicep as a starting point, but the result always needs manual review.
  • Always use --confirm-with-what-if in interactive production deploys to have a last chance to review the impact before confirming.
  • Deploy history has a limit of 800 entries per resource group. Use descriptive deploy names for traceability and configure automatic cleanup in environments with frequent deploys.