Rethinking an old Logic App deployment package- part IV

Previously in Note to self…

I got one step closer to my goal to export all logic apps from a resource group to a Visual Studio project, using linked templates to deploy all logic apps in one when required, but still being able to deploy individual logic apps for a patch template. In previous posts, I’ve managed to use the extend a open source component originally created by Jeff Hollan and maintained by Mattias Lögdberg, adding a couple of extra cmdlets. You can get the last post here.

Visual Studio ARM Deployment Project

For the last step in this process, I needed to create an Azure Resource Manager deployment project. I decided to go through the most pragmatic route – to use an existing project as template. So as a first step, I created an empty Azure Resource Group VS project and tried to find out how artefacts can be associated to it.

Visual Studio project selection

A new project creates the following files:

  • azuredeploy.json – an empty ARM template
  • azuredeploy.parameters.json – an empty ARM paramters file
  • <projectname>.deployproj – the VS project definition
  • Deploy-AzureResourceGroup.ps1 – the powershell script used to deploy the ARM templates
  • Deployment.targets – a build definition file

deployproj is an xml file representing the project components. Analysing the deployproj file, and adding a couple of resources to the project, I found that files are associated to the project using the ItemsGroup element, with the following syntax:

<[buildaction] Include="[relativepath/filename.ext]"/>

ARM templates are included with the Content build action. A new project would look like this:

<ItemGroup>
    <Content Include="azuredeploy.json" />
    <Content Include="azuredeploy.parameters.json" />
    <None Include="Deployment.targets">
      <Visible>False</Visible>
    </None>
    <Content Include="Deploy-AzureResourceGroup.ps1" />
</ItemGroup>

Adding a new resource to the project would change it to:

<ItemGroup>
	<Content Include="azuredeploy.json" />
	<Content Include="azuredeploy.parameters.json" />
	<Content Include="newresource/newresource.json" />
	<Content Include="newresource/newresource.parameters.json" />
	<None Include="Deployment.targets">
		<Visible>False</Visible>
	</None>
	<Content Include="Deploy-AzureResourceGroup.ps1" />
</ItemGroup>

So that gave me the clue I needed to implement the cmdlets that would:

  • Create the required files (<project>.deployproj, deployment.targets, Deploy-AzureResourceGroup.ps1)
  • Include all files I’ve created as part of the previous steps into the ItemsGroup area.

Add-DeploymentVSProject cmdlet

By now I was quite used to the cmdlet classes, so here is how I defined the cmdlet:

  • The cmdle will have a single input parameter called SourceDir, which would point to the folder where the project would be created.
  • A base deployproj, as well as the deployment.targets and Deploy-AzureResourceGroup.ps1 files would be included as embedded resources.
  • BeginProcessing virtual method would be overriden to create all three files – deployproj, targets and ps1 files in the folder.
protected override void BeginProcessing()
{
	base.BeginProcessing();
	string[] fileList = Directory.GetFiles(SourceDir, "*.deployproj");
	if (fileList != null && fileList.Length > 0)
	{
		DeployProject = fileList[0];
	}
	else
	{
		InitializeSourceDir();
	}
}

 private void InitializeSourceDir()
{
	string[] folder = SourceDir.Split('\\');
	DeployProject = String.Format(@"{0}.deployproj", folder.Last());
	string targetFile = "Deployment.targets";
	string powershellFile = "Deploy-AzureResourceGroup.ps1";

	CreateFile(DeployProject, SourceDir, "DeployProjectTemplate.deployproj");
	CreateFile(targetFile, SourceDir, targetFile);
	CreateFile(powershellFile, SourceDir, powershellFile);
}
  • ProcessRecords virtual method would be overriden to include the files to the project. For each subfolder inside SourceDir, a couple of entries would be added to ItemsGroup – one for the template file and another for the parameters file.
protected override void ProcessRecord()
{
	string projectfile = Path.Combine(SourceDir, DeployProject);
	WriteDebug(projectfile);
	XNamespace ns = @"http://schemas.microsoft.com/developer/msbuild/2003";
	XDocument project = XDocument.Load(projectfile);
	WriteDebug(project.Root.ToString());
	XElement projectID = project.Descendants(ns + "ProjectGuid").FirstOrDefault();
	if (projectID != null)
	{ 
		projectID.Value = Guid.NewGuid().ToString();
	}
	XElement itemGroups = project.Descendants(ns + "ItemGroup").Where(xl => !xl.HasAttributes).FirstOrDefault();
	if (itemGroups != null)
	{
		WriteDebug("Inside Loop");
		List<string> dirList = Directory.GetDirectories(SourceDir).ToList();
		WriteDebug(dirList.Count.ToString());
		foreach (string dir in dirList)
		{
			string folder = dir.Replace(SourceDir, "").Replace(@"\","");
			string template = string.Format(@"{0}\{0}.json", folder);
			string parameter =string.Format(@"{0}\{0}.parameters.json", folder);
			XElement templateXML = new XElement(ns + "Content", new XAttribute("Include", template));
			XElement parameterXML = new XElement(ns + "Content", new XAttribute("Include", parameter));

			itemGroups.Add(templateXML);
			itemGroups.Add(parameterXML);
		}

		project.Save(projectfile);
	}
}

The new cmdlet can be invoked by executing this command:

Add-DeploymentVSProject -SourceDir "d:\test"

Finalizing Get-RGLogicAppTemplates

So now that the last piece of the puzzle was created, it was just a matter of adding one last line to Get-RGLogicAppTemplates:

Add-DeploymentVSProject -SourceDir $destination

That last line would create the VS project files and add the files created in previous steps to the project. The final script looked like this:

<#
.Synopsis
   Create template Files for All logic Apps in a Resource Group.
.DESCRIPTION
   This script will create template files from all logic apps in a resource group and parameter files and saves them in folders defined as parameter.
   It assumes that the user has already been authenticated against Azure.
   It depends on the following components:
   - armclient
   - AzureRM
.EXAMPLE
   ./Get-RGLogicAppsTemplate.ps1 "My Subscription" "mytenant.onmicrosoft.com" "MyResourceGroup" "c:\mydestination"
.PARAMETER subscriptionname
    The subscription name where the resource group is deployed.
.PARAMETER tenantname
    The tenant associated to the subscription
.PARAMETER resourcegroup
    The Resource Group containing the Logic Apps to be extracted
.PARAMETER destination
    The local folder where the logic app templates will be created. if this folder doesn't exists it will be created. A new params folder will be created inside the destination folder.
#>
param([string] $subscriptionname, [string] $tenantname, [string] $resourcegroup, [string] $destination)

# Import the Logic Apps Template Module #
$module = resolve-path ".\..\bin\Debug\LogicAppTemplate.dll"
Import-Module $module

# Create required folders #
md -Force $destination | Out-Null

# Select the correct subscription #
Get-AzureRmSubscription -SubscriptionName $subscriptionname | Select-AzureRmSubscription | Out-Null

Write-Host

# Gets a list of logic app
Find-AzureRmResource -ResourceGroupNameContains $resourcegroup -ResourceType Microsoft.Logic/workflows | ForEach-Object{ 

	Write-Host $("Creating {0} Logic App Template" -f $_.Name)

	# Define the destination folder #
	$logicappfolder = [IO.Path]::GetFullPath((Join-Path $destination $_.Name))
	md $logicappfolder -Force | Out-Null
	
	# Define the destination file names #
	$destinationfile = $(Join-path $logicappfolder ($_.Name + ".json"))
	$destinationparmfile = $(Join-path $logicappfolder ($_.Name + ".parameters.json"))
	
	# Create Logic App Template #
	armclient token $_.SubscriptionId |	Get-LogicAppTemplate -LogicApp $_.Name -ResourceGroup $_.ResourceGroupName -SubscriptionId $_.SubscriptionId -TenantName $tenantname -Verbose | Out-File $destinationfile -Force
	
	# Generate the Parameter File #
	Get-ParameterTemplate -TemplateFile $destinationfile | Out-File $destinationparmfile -Force}

Write-Host
# Initialize Azure Deploy nested Templates variable #
$azuredeploytemplate = ""

Write-Host "Creating AzureDeploy ARM Template"

# 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}

#Save nested template to destination #
$azuredeploytemplate | Out-File $(Join-path $destination "azuredeploy.json") -Force

Write-Host

Write-Host "Creating AzureDeploy ARM Parameter Template"

#Generate an empty Azure Deploy Parameter

Get-EmptyParameterTemplate | Out-File $(Join-path $destination "azuredeploy.parameters.json") -Force

Write-Host

Write-Host "Creating Visual Studio Project"

Add-DeploymentVSProject -SourceDir $destination

In the end was quite pleased with the process. The project that triggered this whole process, where I needed to amend 3 out of 16 logic apps, was deployed with success, with much bigger confidence than I had before, as I could extract the DEV/TEST logic apps and just update those specific parameter files, guranteeing that none of the other logic apps would be redeployed.

Next steps

I could think in a couple of improvements for this process:

  • As you probably noticed, I’ve only created the deployproj file, so there is no Visual Studio solution in this process. It was a concious decision, as I was running out of time to actually use the code to do the real work. Since VS wraps an “orphan” project in a solution, that was a safe bet.
  • Ideally, I would define which environment I would like to create in order to create multiple parameter files.
  • I still need to understand how nested and parent parameter files work, but maybe I could roll up all the parameters to the root parameter files. That might make life easier for full deployment.

I will probably work on some of those items at a later stage or, if you are interested, you might want to fork the project and have a go yourself? ;-D

I hope that you had fun reading this series – I definitely had lots of fun working on this and documenting as I went.

See you next time!


Sharing is caring...

Leave a Reply

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