my blog

$ cat azure-arm-templates/part-05.md

Dynamic Resource Creation in ARM Templates: Making Unique Names Automatically

Hello everyone! Today I want to share something that saved me a lot of time – dynamic resource creation in ARM templates.

Before, I had a problem. Every time I deployed my template, Azure said “name already exists”. I had to manually change names in my template every time. This was annoying!

Then I learned about ARM template functions like uniqueString() and concat(). Now Azure creates unique names automatically. Let me show you!

My Problem with Hardcoded Names

Look at my old Web App template:

{
    "type": "Microsoft.Web/sites",
    "name": "rezawebapp994848",
    "properties": {
        "name": "rezawebapp994848"
    }
}

The problem: rezawebapp994848 is hardcoded!

What happened when I deployed again?

Error: “The website name ‘rezawebapp994848’ already exists”

What happened when my friend tried to use my template?

Error: “The website name ‘rezawebapp994848’ already exists”

I needed unique names every time!

Solution: Use Variables and Functions

Here’s my new template with dynamic names:

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "functions": [],
    "variables": {
        "web-app-name":"[concat('webapp',uniqueString(resourceGroup().id))]"
    },
    "resources": [
        {
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2022-03-01",
            "name": "plan787878",
            "location": "[resourceGroup().location]",
            "sku": {
                "name":"F1",
                "capacity":1
            },
            "properties":{
                "name":"plan787878"
            }
        },
        {
            "type": "Microsoft.Web/sites",
            "apiVersion": "2022-03-01",
            "name": "[variables('web-app-name')]",
            "location": "[resourceGroup().location]",
            "properties":{
                "name":"[variables('web-app-name')]",
                "serverFarmId":"[resourceId('Microsoft.Web/serverfarms','plan787878')]"
            },
            "dependsOn":[
                "[resourceId('Microsoft.Web/serverfarms','plan787878')]"
            ]
        }
    ],
    "outputs": {}
}

Now look at the variables section:

"variables": {
    "web-app-name":"[concat('webapp',uniqueString(resourceGroup().id))]"
}

And how I use it in the resource:

"name": "[variables('web-app-name')]"

What does this do? Let me explain!

Understanding uniqueString() Function

The uniqueString() function creates a unique hash from input.

uniqueString(resourceGroup().id)

This takes the resource group ID and creates a unique string from it.

Example:

  • Resource Group ID: /subscriptions/abc123/resourceGroups/my-rg
  • uniqueString result: ghe7d4kj2p5a

The result is always:

  • 13 characters long
  • Only lowercase letters and numbers
  • Same for same input (deterministic)
  • Different for different input

So if I use resource group my-rg, I always get ghe7d4kj2p5a. If you use resource group your-rg, you get different string like k8j3h5m9n2p4.

This is perfect! Each person gets their own unique name.

Understanding concat() Function

The concat() function joins strings together.

concat('webapp', uniqueString(resourceGroup().id))

This joins:

  • webapp (my prefix)
  • ghe7d4kj2p5a (unique string)

Result: webappghe7d4kj2p5a

Now every deployment gets unique name automatically!

Understanding Variables Section

"variables": {
    "web-app-name":"[concat('webapp',uniqueString(resourceGroup().id))]"
}

Variables are like shortcuts. I define the name once in variables, then use it many times in resources.

To use a variable:

"name": "[variables('web-app-name')]"

Why use variables?

  1. Define once, use many times
  2. Easy to change later
  3. Makes template cleaner
  4. No copy-paste errors

Real Example: Web App with Linked SQL Template

Now I want to make BOTH Web App and SQL Server names unique. Here’s my complete example:

Main template:

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "functions": [],
    "variables": {
        "web-app-name":"[concat('webapp',uniqueString(resourceGroup().id))]"
    },
    "resources": [
        {
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2022-03-01",
            "name": "plan787878",
            "location": "[resourceGroup().location]",
            "sku": {
                "name":"F1",
                "capacity":1
            },
            "properties":{
                "name":"plan787878"
            }
        },
        {
            "type": "Microsoft.Web/sites",
            "apiVersion": "2022-03-01",
            "name": "[variables('web-app-name')]",
            "location": "[resourceGroup().location]",
            "properties":{
                "name":"[variables('web-app-name')]",
                "serverFarmId":"[resourceId('Microsoft.Web/serverfarms','plan787878')]"
            },
            "dependsOn":[
                "[resourceId('Microsoft.Web/serverfarms','plan787878')]"
            ]
        },
        {
            "type":"Microsoft.Resources/deployments",
            "apiVersion": "2021-04-01",
            "name":"LinkedTemplate",
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri": "https://templatestore465656.blob.core.windows.net/templates/sqldatabase.json",
                    "contentVersion":"1.0.0.0"
                },
                "parametersLink": {
                    "uri": "https://templatestore465656.blob.core.windows.net/templates/sqldatabase-parameters.json",
                    "contentVersion":"1.0.0.0"
                }
            }
        }
    ],
    "outputs": {
        "webappname": {
            "type": "string",
            "value":"[variables('web-app-name')]"
        },
        "sqlserverfqdn": {
            "type": "string",
            "value":"[reference('LinkedTemplate').outputs.sqlserverfqdn.value]"
        }
    }
}

SQL template (linked):

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "SQLLogin": {
            "type":"string",
            "metadata":{
                "description":"The administrator user name"
            }
        },
        "SQLPassword": {
            "type":"secureString",
            "metadata":{
                "description":"The administrator password"
            }
        }
    },
    "functions": [],
    "variables": {
        "sql-server-name":"[concat('server',uniqueString(resourceGroup().id))]"
    },
    "resources": [
        {
            "type": "Microsoft.Sql/servers",
            "apiVersion": "2022-02-01-preview",
            "name": "[variables('sql-server-name')]",
            "location": "[resourceGroup().location]",
            "properties":{
                "administratorLogin": "[parameters('SQLLogin')]",
                "administratorLoginPassword": "[parameters('SQLPassword')]"
            }
        },
        {
            "type": "Microsoft.Sql/servers/databases",
            "apiVersion": "2022-02-01-preview",
            "name": "[format('{0}/{1}',variables('sql-server-name'),'appdb')]",
            "location": "[resourceGroup().location]",
            "sku":{
                "name":"Basic",
                "tier":"Basic"
            },
            "properties":{},
            "dependsOn":[
                "[resourceId('Microsoft.Sql/servers',variables('sql-server-name'))]"
            ]
        }
    ],
    "outputs": {
        "sqlserverfqdn": {
            "type": "string",
            "value":"[reference(variables('sql-server-name')).fullyQualifiedDomainName]"
        }
    }
}

Now both Web App and SQL Server get unique names automatically!

Understanding Outputs

Look at the outputs section in my templates:

In main template:

"outputs": {
    "webappname": {
        "type": "string",
        "value":"[variables('web-app-name')]"
    },
    "sqlserverfqdn": {
        "type": "string",
        "value":"[reference('LinkedTemplate').outputs.sqlserverfqdn.value]"
    }
}

In SQL template:

"outputs": {
    "sqlserverfqdn": {
        "type": "string",
        "value":"[reference(variables('sql-server-name')).fullyQualifiedDomainName]"
    }
}

Outputs let you get information AFTER deployment.

After I deploy, Azure CLI shows me:

{
  "outputs": {
    "webappname": {
      "type": "String",
      "value": "webappghe7d4kj2p5a"
    },
    "sqlserverfqdn": {
      "type": "String",
      "value": "serverghe7d4kj2p5a.database.windows.net"
    }
  }
}

Now I know:

  • My Web App name: webappghe7d4kj2p5a
  • My SQL Server: serverghe7d4kj2p5a.database.windows.net

I can use these in my CI/CD pipeline!

Getting Outputs from Linked Templates

This is the tricky part:

"value":"[reference('LinkedTemplate').outputs.sqlserverfqdn.value]"

Let me break it down:

  • reference(‘LinkedTemplate’) – Get the linked template deployment
  • .outputs – Get the outputs section
  • .sqlserverfqdn – Get the specific output
  • .value – Get the actual value

So I can get data from linked template and pass it to main template outputs!

Using Outputs in Azure DevOps Pipeline

Here’s how I use ARM template outputs in my Azure DevOps pipeline. This is from my actual pipeline file:

# ASP.NET Core (.NET Framework)
# Build and test ASP.NET Core projects targeting the full .NET Framework.

trigger:
- main

pool:
  vmImage: 'windows-latest'

resources:
- repo: self

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

stages:
- stage: Build
  displayName: Build stage
  jobs:
  - job: Build
    displayName: Build
    steps:
    - task: NuGetToolInstaller@1

    - task: NuGetCommand@2
      inputs:
        restoreSolution: '$(solution)'

    - task: DotNetCoreCLI@2
      displayName: Build
      inputs:
        command: build
        projects: '**/*.csproj'
        arguments: '--configuration $(buildConfiguration)'

    - task: DotNetCoreCLI@2
      inputs:
        command: publish
        publishWebProjects: True
        arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
        zipAfterPublish: True

    - task: PublishPipelineArtifact@1
      inputs:
        targetPath: '$(Build.ArtifactStagingDirectory)'
        artifactName: 'sqlapp-artifact'

    - task: PublishPipelineArtifact@1
      inputs:
        targetPath: '$(Build.SourcesDirectory)/sqlapp/Templates'
        artifactName: 'template-artifact'

- stage: Deploy
  displayName: Deployment stage
  jobs:
  - job: Deploy
    displayName: Deploy
    steps:

    - download: current
      artifact: sqlapp-artifact

    - download: current
      artifact: template-artifact

    - pwsh: |
        Get-ChildItem -Path $(Pipeline.Workspace)\*.* -Recurse -Force | Out-String -Width 180
      errorActionPreference: continue
      displayName: 'List content'
      continueOnError: true
    - task: AzureResourceManagerTemplateDeployment@3
      displayName: 'ARM Template deployment: Resource Group scope'
      inputs:
        azureResourceManagerConnection: 'Azure subscription 1 (6912d7a0-bc28-459a-9407-33bbba641c07)'
        subscriptionId: '6912d7a0-bc28-459a-9407-33bbba641c07'
        resourceGroupName: 'template-grp'
        location: 'North Europe'
        csmFile: '$(Pipeline.Workspace)/template-artifact/main.json'
        action: 'Create Or Update Resource Group'
        deploymentMode: 'Incremental'

    - task: SqlAzureDacpacDeployment@1
      displayName: 'Azure SQL InlineSqlTask'
      inputs:
        azureSubscription: 'Azure subscription 1 (6912d7a0-bc28-459a-9407-33bbba641c07)'
        ServerName: sqlserver8000505.database.windows.net
        DatabaseName: appdb
        SqlUsername: sqlusr
        SqlPassword: 'Azure@123'
        deployType: InlineSqlTask
        SqlInline: |
          IF (NOT EXISTS (SELECT * 
                       FROM INFORMATION_SCHEMA.TABLES 
                       WHERE TABLE_SCHEMA = 'dbo' 
                       AND  TABLE_NAME = 'Products'))
          BEGIN

          CREATE TABLE Products
          (
           ProductID int,
           ProductName varchar(1000),
           Quantity int
           )


           INSERT INTO Products(ProductID,ProductName,Quantity) VALUES (1,'Mobile',100)

           INSERT INTO Products(ProductID,ProductName,Quantity) VALUES (2,'Laptop',200)

           INSERT INTO Products(ProductID,ProductName,Quantity) VALUES (3,'Tabs',300)

           END
        IpDetectionMethod: IPAddressRange
        StartIpAddress: 0.0.0.0
        EndIpAddress: 0.0.0.0
        DeleteFirewallRule: false

    - task: AzureRmWebAppDeployment@4
      displayName: 'Azure App Service Deploy'
      inputs:
        azureSubscription: 'Azure subscription 1 (6912d7a0-bc28-459a-9407-33bbba641c07)'
        WebAppName: newapp1094848
        Package: '$(Pipeline.Workspace)/sqlapp-artifact/**/*.zip'

    - task: AzureAppServiceSettings@1
      displayName: 'Azure App Service Settings: newapp1094848'
      inputs:
        azureSubscription: 'Azure subscription 1 (6912d7a0-bc28-459a-9407-33bbba641c07)'
        appName: newapp1094848
        resourceGroupName: 'template-grp'
        connectionStrings: |
          [
            {
              "name": "SQLConnection",
              "value": "Server=tcp:sqlserver8000505.database.windows.net,1433;Initial Catalog=appdb;Persist Security Info=False;User ID=sqlusr;Password=Azure@123;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;",
              "type": "SQLAzure",
              "slotSetting": false
            }
          ]

In my pipeline, I deploy the ARM template in the Deploy stage.

The key task is:

- task: AzureResourceManagerTemplateDeployment@3
  displayName: 'ARM Template deployment: Resource Group scope'
  inputs:
    azureResourceManagerConnection: 'Azure subscription 1 (6912d7a0-bc28-459a-9407-33bbba641c07)'
    subscriptionId: '6912d7a0-bc28-459a-9407-33bbba641c07'
    resourceGroupName: 'template-grp'
    location: 'North Europe'
    csmFile: '$(Pipeline.Workspace)/template-artifact/main.json'
    action: 'Create Or Update Resource Group'
    deploymentMode: 'Incremental'

This task:

  1. Takes my main.json template from artifacts
  2. Deploys it to template-grp resource group
  3. Uses Incremental mode (safe!)
  4. Creates Web App with unique name
  5. Calls linked template for SQL Database

After ARM deployment finishes, I run SQL script to create tables, then deploy my application code to the Web App.

The pipeline works end-to-end:

  1. Build stage – Build .NET application
  2. Publish artifacts – Application code + ARM templates
  3. Deploy stage – Deploy ARM templates
  4. Create SQL tables with inline SQL script
  5. Deploy application to Web App
  6. Configure connection string

More Useful Functions

Let me show you other functions I use:

Using toLower():

"variables": {
    "storage-name":"[toLower(concat('storage',uniqueString(resourceGroup().id)))]"
}

This makes everything lowercase. Storage account names must be lowercase!

Using resourceGroup().location:

"location": "[resourceGroup().location]"

This uses the resource group location automatically. I don’t need to type “North Europe” everywhere!

Using multiple concat():

"variables": {
    "vm-name":"[concat('vm-', parameters('environment'), '-', uniqueString(resourceGroup().id))]"
}

If environment parameter is “dev”, result: vm-dev-ghe7d4kj2p5a

If environment parameter is “prod”, result: vm-prod-ghe7d4kj2p5a

My Mistakes and Fixes

I forgot square brackets around function:

"name": "concat('webapp', uniqueString(resourceGroup().id))"

This doesn’t work! ARM treats it as plain text, not a function.

Fix: Add square brackets:

"name": "[concat('webapp', uniqueString(resourceGroup().id))]"

I used wrong variable name:

"variables": {
    "webappname": "..."
}

// Later in resource
"name": "[variables('web-app-name')]"  // Different name!

Error: “The template variable ‘web-app-name’ is not found”

Fix: Use exact same name everywhere.

I forgot single quotes in concat:

concat(webapp, uniqueString(resourceGroup().id))  // Wrong! Missing quotes

Fix: Add quotes around string:

concat('webapp', uniqueString(resourceGroup().id))  // Correct!

I tried to use outputs before deployment finished:

In same template, I tried:

"resources": [
    {
        "name": "webapp",
        "type": "Microsoft.Web/sites",
        ...
    },
    {
        "name": "another-resource",
        "properties": {
            "value": "[reference('webapp').outputs.something]"  // Wrong!
        }
    }
]

This doesn’t work! I can only use outputs AFTER deployment, not during deployment.

Fix: Use dependsOn and reference() correctly, or use outputs only in pipeline steps after deployment.

What I Learned

  1. uniqueString() creates unique names – No more “name already exists” errors
  2. concat() joins strings – Easy to add prefixes
  3. Variables make code cleaner – Define once, use many times
  4. Outputs give information – Use in CI/CD pipelines
  5. Square brackets are required – For all ARM functions
  6. Linked templates can return outputs – Get data from child templates
  7. Azure DevOps can use ARM outputs – Connect templates to pipelines

Why This Matters

Before dynamic names:

  • Manual work every deployment
  • Name conflicts
  • Hard to share templates
  • Cannot automate

After dynamic names:

  • Deploy unlimited times
  • No conflicts
  • Easy to share
  • Full automation

$ ls -t root-access/ | head -3

Hello world! uncategorized 1 min Building a Job Board API, Part 1: The Foundation job-board-api · 1/1 16 min Part 13 – Kubernetes Autoscaling in Action: A FastAPI Microservice That Scales Itself home-lab · 13/13 6 min

Leave a comment

Your email address will not be published. Required fields are marked *