One of Bicep’s greatest advantages over other infrastructure-as-code tools is its ability to take existing Azure resources instantiated using the portal and generate Bicep code for them.

Terraform recently gained this feature with the AzAPI provider and Azure Terrafy, but it has similar limitations

This process isn’t as simple as it could be; you have to either Export Template from the portal UI or use the Azure PowerShell Export-AzResourceGroup cmdlet and then decompile the resulting ARM template into Bicep. And what you end up with is kind of messy; there will be a ton of state properties in the template that you don’t need to set on deployment, the internal labels for the resources will be auto-generated gibberish, and there will likely be child resources in the template you can’t actually instantiate with Bicep because they’re only ever created as part of the instantiation of a parent resource.

If what you’ve got is a legacy Azure infrastructure there’s really no way around this, but you can cut down on some of the work.

Every time you instantiate Azure resources with the portal, an ARM template is generated, used and stored at the resource group level (the smallest scope for an ARM template deployment is the resource group). And you can export that template as it was originally used.

If you go to your resource group and find Settings | Deployments in the sidebar, you’ll find all the changes that have been made to your resources in that resource group. Select and deployment and choose Template in the sidebar to see the ARM template that the portal used to deploy the resource.

For comparison, here’s the template used by the portal to deploy the Azure Static Web App that hosts this blog:

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "name": {
            "type": "String"
        },
        "location": {
            "type": "String"
        },
        "sku": {
            "type": "String"
        },
        "skucode": {
            "type": "String"
        },
        "repositoryUrl": {
            "type": "String"
        },
        "branch": {
            "type": "String"
        },
        "repositoryToken": {
            "type": "SecureString"
        },
        "appLocation": {
            "type": "String"
        },
        "apiLocation": {
            "type": "String"
        },
        "appArtifactLocation": {
            "type": "String"
        }
    },
    "resources": [
        {
            "type": "Microsoft.Web/staticSites",
            "apiVersion": "2021-01-01",
            "name": "[parameters('name')]",
            "location": "[parameters('location')]",
            "tags": {},
            "sku": {
                "Tier": "[parameters('sku')]",
                "Name": "[parameters('skuCode')]"
            },
            "properties": {
                "repositoryUrl": "[parameters('repositoryUrl')]",
                "branch": "[parameters('branch')]",
                "repositoryToken": "[parameters('repositoryToken')]",
                "buildProperties": {
                    "appLocation": "[parameters('appLocation')]",
                    "apiLocation": "[parameters('apiLocation')]",
                    "appArtifactLocation": "[parameters('appArtifactLocation')]"
                }
            }
        }
    ]
}

And here’s the result of exporting the template from the resource group:

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "staticSites_stapp_profblog_chrisdoherty_name": {
            "defaultValue": "stapp-profblog-chrisdoherty",
            "type": "String"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Web/staticSites",
            "apiVersion": "2022-09-01",
            "name": "[parameters('staticSites_stapp_profblog_chrisdoherty_name')]",
            "location": "East US 2",
            "sku": {
                "name": "Free",
                "tier": "Free"
            },
            "properties": {
                "repositoryUrl": "https://github.com/cpdohert/www.chris-doherty.com",
                "branch": "main",
                "stagingEnvironmentPolicy": "Enabled",
                "allowConfigFileUpdates": true,
                "provider": "GitHub",
                "enterpriseGradeCdnStatus": "Disabled"
            }
        },
        {
            "type": "Microsoft.Web/staticSites/customDomains",
            "apiVersion": "2022-09-01",
            "name": "[concat(parameters('staticSites_stapp_profblog_chrisdoherty_name'), '/www.chris-doherty.com')]",
            "location": "East US 2",
            "dependsOn": [
                "[resourceId('Microsoft.Web/staticSites', parameters('staticSites_stapp_profblog_chrisdoherty_name'))]"
            ],
            "properties": {}
        }
    ]
}

You can see that the template used by the portal is much cleaner and already fully parameterized; it will decompile into a Bicep module that’s mcuh easier to convert to a module. On the other hand, the exported template includes the child resource (customDomains) that was added after initial instantiation.

By combining these two methods, you can take a legacy infrastructure and convert it into Infrastructure as Code much more quickly than starting ab initio.

Final Caveat Link to heading

If your Azure resources have been managed solely through the portal for a long time, you’re going to have many portal-generated deployments in the deployment history. The current state of your resources will be the result of all of those iterations. You’ll have to decide whether it’s worth trying to trace all of those deployments from the beginning or whether to just export the whole mess and put in the time to re-engineer the Bicep templates.