Previously in Note to self…
I’ve been discussing how I’ve leveraged Jeff Hollan’s Logic App Template Generator and some PowerShell goodness to export a ARM templates to all Logic Apps in a specific resource group. You can read about that here.
Linking ARM Templates together
Azure Resource Manager templates have the ability to link templates, creating a “parent-child” or “nested” relationship, allowing a single template to deploy resources that are defined in various individual templates. The advantages of this technique in my opinion are:
- Each resource can be defined in isolation, which makes maintenance of that resource simpler.
- Related components can be grouped in “parent” templates for a simpler deployment experience, but they can still be deployed individually if required.
This was exactly the type of solution I was looking for, after having to deal with the pain of replacing a logic app template in the middle of a monolithic ARM template containing another 15 templates. That would usually take a couple of hours to make sure that the template was replaced correctly and nothing was removed that would make the deploy to break.Jeff’s project didn’t have this functionality so it was time for my first real contribution on the world of open source.
My plan was to create a new cmdlet, following the existing pattern laid out by the existing Get-LogicAppTemplate cmdlet.
Understanding Linked Templates syntax
In order to replicate programatically a linked template, I needed to understand how a linked template is created. That was where Visual Studio came in handy.
Visual Studio ARM project supports nested templates. To create a new nested template, you just need to right click on resources within the JSON outline and select Add Resource:
Then select Nested Deployment from the list of templates:
This nested deployment is defined like this in the ARM template:
{ "name": "NestedTemplate", "type": "Microsoft.Resources/deployments", "apiVersion": "2016-09-01", "dependsOn": [ ], "properties": { "mode": "Incremental", "templateLink": { "uri": "[concat(parameters('_artifactsLocation'), '/', variables('NestedTemplateTemplateFolder'), '/', variables('NestedTemplateTemplateFileName'), parameters('_artifactsLocationSasToken'))]", "contentVersion": "1.0.0.0" }, "parametersLink": { "uri": "[concat(parameters('_artifactsLocation'), '/', variables('NestedTemplateTemplateFolder'), '/', variables('NestedTemplateTemplateParametersFileName'), parameters('_artifactsLocationSasToken'))]", "contentVersion": "1.0.0.0" } } }
So in the end, the nested template have two main properties:
- templateLink – this property points to the location where the ARM template for the nested deployment will be found.
- parametersLink – this property point to the location where the parameters file for the nested deployment will be found.
With that in mind, and a sample empty template, it is time to write the cmdlet.
Creating a new cmdlet
To create a new cmdlet, you need to inherit from the cmdlet class, which exists in the System.Management.Automation namespace.
This class have three virtual operations, that we can override to create the required execution:
- BeginProcessing – executes once before the cmdlet process records.
- ProcessRecord – executed for each record passed for the cmdlet.
- EndProcessing – executes once after the processing is completed.
You can decorate your class to define the verb and name of your cmdlet, and define each parameter, including name, order and if it is mandatory.
For the nested template cmdlet, my assumption were:
- a resource and its parameter file would be stored in a folder.
- the two files and the folder would share the name
That assumption seems to follow the format that Visual Studio creates the file – and that is why the Get-RGLogicAppsTemplate script was implemented.
I would also need that once created the first time, the template could be passed as parameter for the next execution, so it could append a resource.
So in the end, the class definition looks like this:
[Cmdlet(VerbsCommon.Get, "NestedResourceTemplate", ConfirmImpact = ConfirmImpact.None)] public class NestedTemplateGenerator: Cmdlet { [Parameter( Mandatory = true, HelpMessage = "Name of the Resource" )] public string ResourceName; [Parameter( Mandatory = false, HelpMessage = "Existing Deployment Template definition" )] public string Template = ""; private static string NestedTemplateResourceUri = "[concat(parameters('_artifactsLocation'), '/{0}/{0}.json', parameters('_artifactsLocationSasToken'))]"; private static string NestedTemplateParameterUri = "[concat(parameters('_artifactsLocation'), '/{0}/{0}.parameters.json', parameters('_artifactsLocationSasToken'))]"; private DeploymentTemplate nestedtemplate; public NestedTemplateGenerator() { } }
When a new template is being initialized from scratch, the cmdlet would create without the Template parameter, which would make an empty template based on a sample file.
If the Template parameter is populated with an existing resource template, that template will be used for initialization instead, guaranteeing that the next resource would be appended to an existing template. The code for this process can be found in the snippet below. It leverages from the BeginProcessing virtual method:
protected override void BeginProcessing() { base.BeginProcessing(); InitializeTemplate(); } private void InitializeTemplate() { if (string.IsNullOrEmpty(Template)) { var assembly = System.Reflection.Assembly.GetExecutingAssembly(); var resourceName = "LogicAppTemplate.Templates.nestedTemplateShell.json"; using (Stream stream = assembly.GetManifestResourceStream(resourceName)) { using (StreamReader reader = new StreamReader(stream)) { Template = reader.ReadToEnd(); } } } nestedtemplate = JsonConvert.DeserializeObject<DeploymentTemplate>(Template); }
Because of the assumption that resources and parameters shared the same base name, and were stored in a folder that also share that name, the code only requires the name of the resource to include as a nested deployment. The ProcessRecord method looks like this:
protected override void ProcessRecord() { var nestedresource = new Models.NestedResourceTemplate() { name = ResourceName}; nestedresource.properties.templateLink.uri = String.Format(NestedTemplateResourceUri, ResourceName); nestedresource.properties.parametersLink.uri = String.Format(NestedTemplateParameterUri, ResourceName); nestedtemplate.resources.Add(JObject.FromObject(nestedresource)); var result = JObject.FromObject(nestedtemplate); WriteObject(result.ToString()); }
In a nutshell, the code creates a new NestedResourceTemplate object – which is the serialization of a ARM ResourceTemplate element – and include set two properties of this object:
- an uri pointing to the resource template file,
- an uri pointing to the resource parameters file
Using the new cmdlet
This cmdlet can be used in two ways. When creating a new linked template from scratch it can be called as:
Get-NestedResourceTemplate -ResourceName "MyResourceFolder"
Where MyResourceFolder is the name of a folder containing the template and paramater files – just like the folder created by Get-RGLogicAppsTemplate script created in the previous post.
If you already a linked template and want to add a nested resource to the list, you can use it as:
Get-NestedResourceTemplate -ResourceName "MyResourceFolder" -Template $azuredeploytemplate
In this case $azuredeploytemplate contains a json string representing the current template.
Adding an Empty Parameters File
For completeness, I’ve also created a cmdlet that creates an empty parameter file, so the linked template have an associated parameters file, if any manual additions are required. The code is quite straightforward:
[Cmdlet(VerbsCommon.Get, "EmptyParameterTemplate", ConfirmImpact = ConfirmImpact.None)] public class EmptyParameterTemplateGenerator : Cmdlet { private ParameterTemplate paramtemplate; public EmptyParameterTemplateGenerator() { var assembly = System.Reflection.Assembly.GetExecutingAssembly(); var resourceName = "LogicAppTemplate.Templates.paramTemplate.json"; using (Stream stream = assembly.GetManifestResourceStream(resourceName)) using (StreamReader reader = new StreamReader(stream)) { paramtemplate = JsonConvert.DeserializeObject<ParameterTemplate>(reader.ReadToEnd()); } } protected override void ProcessRecord() { var result = JObject.FromObject(paramtemplate); WriteObject(result.ToString()); } }
Extending RGLogicAppsTemplate
With two new cmdlets that creates a nested template, it was just a matter of extending RGLogicAppsTemplate script to use the same resource group information and create the top linked template, by adding the following steps:
- First, we initialize an empty variable to hold the linked template.
# Initialize Azure Deploy nested Templates variable # $azuredeploytemplate = ""
- Then we got a list directories in the $destination folder – which were created in the previous process. For each directory, we append a new entry to the linked template, stored in $azuredeploytemplate.
# Gets a list of resources to add to the nested template # Get-ChildItem $destination -Directory | ForEach-object { # Adds the resource to the nested templates # $azuredeploytemplate = Get-NestedResourceTemplate -ResourceName $_.Name -Template $azuredeploytemplate}
Finally we save the nested template, and create an empty parameters file.
#Save nested template to destination # $azuredeploytemplate | Out-File $(Join-path $destination "azuredeploy.json") -Force #Generate an empty Azure Deploy Parameter # Get-EmptyParameterTemplate | Out-File $(Join-path $destination "azuredeploy.parameters.json") -Force
Next Steps
The final step in this exercise is to try and create a Visual Studio ARM Deployment project. See you next time!
Leave a Reply