Skip to content

Instantly share code, notes, and snippets.

@Basssiiie
Last active June 3, 2024 06:50
Show Gist options
  • Save Basssiiie/a1140a21f853a6196776eae3e01d1205 to your computer and use it in GitHub Desktop.
Save Basssiiie/a1140a21f853a6196776eae3e01d1205 to your computer and use it in GitHub Desktop.
az deployment what-if workaround for skipping reference()
/*
* Workaround for issue:
* https://github.com/Azure/arm-template-whatif/issues/157
*
* This script will evaluate various ARM functions in your Bicep/ARM template and replace them with hardcoded values where possible.
*
* 1. Converts Bicep templates to a single ARM template using `az bicep decompile`.
* 2. Parses ARM template as JSON.
* 3. Uses `eval()` to attempt to evaluate ARM functions in fields that have them.
* 4. References will either:
* - reference local properties within the template;
* - other deployments in the templates;
* - call `az resource show` to retrieve existing resource details from Azure.
* 5. The script can't and doesn't need to solve all ARM functions. If a property evaluation errors or fails, the script
* will keep it as-is so az what-if can try solving it.
* 6. Script will output both Bicep decompile and final template before the what-if to the output folder for personal review.
*
* Requires NodeJS and Azure CLI installed.
*
* Run using:
* `node what-if.mjs --in=<input bicep/arm template> --out=<output arm template> -g=<resource group> --subscription=<azure subscription id>`
*/
import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs'
const args = {}
const params = {}
for (const arg of process.argv)
{
const [key, value] = arg.split("=")
switch (key)
{
case "--in": args.in = value; break;
case "--out": args.out = value; break;
case "--resource-group":
case "-g": args.resourceGroup = value; break;
case "--subscription": args.subscription = value; break;
default: params[key] = value; break;
}
}
let json
if (args.in.endsWith(".bicep"))
{
json = execSync(`az bicep build -f "${args.in}" --stdout`, { encoding: 'utf8' })
}
else if (args.in.endsWith(".json"))
{
json = readFileSync(args.in, 'utf8')
}
else throw Error(`Invalid input file: ${args.in}`)
function findReferences(entry, key, parent, path)
{
if (typeof entry == "string")
{
if (entry[0] == '[')
{
parent[key] = evaluateFunctionsSafe(entry, path)
}
}
else if (typeof entry == "object")
{
const isDeployment = (entry["type"] == "Microsoft.Resources/deployments")
if (isDeployment)
{
const name = evaluateFunctionsSafe(entry["name"])
deploymentTemplates[name] ||= entry.properties.template // todo: only registers the first if deployments have duplicate names
deploymentsStack.push(entry)
}
const isTemplate = (key == "template" && /^https:\/\/schema\.management\.azure\.com\/schemas\/[\d-]+\/deploymentTemplate\.json#$/.test(entry["$schema"]))
if (isTemplate)
{
templatesStack.push(parent)
}
for (const key in entry)
{
findReferences(entry[key], key, entry, `${path}.${key}`)
}
isDeployment && deploymentsStack.pop();
isTemplate && templatesStack.pop()
}
}
function evaluateFunctionsSafe(value, path)
{
try
{
const result = evaluateFunctions(value)
console.log(`Parsed: ${value}\n to ${result}\n at ${path || '?'}`)
return result;
}
catch (error)
{
console.warn(`Failed to parse: ${value}\n at ${path || '?'}\n`, error)
return value;
}
}
function evaluateFunctions(value)
{
if (value[0] != '[')
{
return value;
}
const result = eval(value)[0]
return (result instanceof ResourceId) ? result.toString() : result;
}
function reference(resource, version)
{
if (typeof resource == 'string') // local reference
{
const resources = templatesStack[templatesStack.length - 1].template.resources;
if (resource in resources)
{
return resources[resource].properties.template;
}
}
if (resource.type == "Microsoft.Resources/deployments") // deployment reference
{
return deploymentTemplates[resource.name]
}
// existing reference
const result = execSync(`az resource show --api-version "${version}" --ids "${resource}"`, { encoding: 'utf8' })
return JSON.parse(result).properties
}
function resourceId(type, name)
{
return new ResourceId(type, name)
}
function deployment()
{
return deploymentsStack[deploymentsStack.length - 1];
}
function parameters(name)
{
for (let idx = templatesStack.length - 1; idx >= 0; idx--)
{
const params = templatesStack[idx].parameters?.[name]?.value;
if (params !== undefined)
{
return params;
}
}
if (name in params)
{
return params[name]
}
throw Error(`Parameter not found: ${name}`)
}
function variables(name)
{
for (let idx = templatesStack.length - 1; idx >= 0; idx--)
{
const vars = templatesStack[idx].template.variables?.[name];
if (vars)
{
return vars;
}
}
throw Error(`Variable not found: ${name}`)
}
function format(string, ...args)
{
return string.replace(/{(\d+)}/g, (match, number) => evaluateFunctions(args[number]) ?? match)
}
function split(string, separator)
{
return string.split(separator)
}
class ResourceId
{
constructor(type, name)
{
this.type = type;
this.name = name;
}
toString()
{
return `/subscriptions/${args.subscription}/resourceGroups/${args.resourceGroup}/providers/${this.type}/${this.name}`
}
}
const template = JSON.parse(json);
const deploymentTemplates = {};
const deploymentsStack = [];
const templatesStack = [ { template }];
writeFileSync(args.out.replace('.json', '.tmp.json'), JSON.stringify(template, null, 2), "utf8")
findReferences(template, null, null, "")
writeFileSync(args.out, JSON.stringify(template, null, 2), "utf8")
execSync(`az deployment group what-if -f "${args.out}" -g "${args.resourceGroup}" --subscription "${args.subscription}"`, { stdio: 'inherit' })
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment