Managing Azure Resource Manager (ARM) Template deployments with CSV files

Posted by Steven Follis on November 21, 2016

ARM Templates are great. They allow us to provision complex solutions in a repeatable and clearly understood manner. The parameters model enables flexible templates that can adapt to a variety of inputs, however the management of these parameter files can be cumbersome.

During the planning phase of a project, Excel is often used to chart out the required virtual machines for a given solution. Let’s take the following values, and create a solution in Azure using Azure Resource Manager deployments.

screenshot

Create a console application

I generated a small console application written in NodeJS that will wire up several steps. I’m using ECMA6 Promises Syntax to more easily control workflow, and avoid a world of callback mess.

ECMA6 Promises need Node v4 or greater. Use node -v to ensure you are up to date, else visit http://nodejs.org and grab an updated version

Scaffold out the console application with defining dependencies and setting up the Promise flow:

var fs = require('fs');
var path = require('path');
var msRestAzure = require('ms-rest-azure');
var resourceManagement = require("azure-arm-resource");
var Converter = require("csvtojson").Converter;
var converter = new Converter({});
var config = require('./config/config');
var _ = require('lodash');

console.log(`Starting deployment`);

// Login to Azure 
login()
    .then(function (client) {

        // Execute in parallel all deployent preparation tasks
        Promise
            .resolve(client)
            .then(createRG)
            .then(deploySharedTemplate)
            .then(getParameters)
            .then(function (parameters) {
                return ([client, parameters]);
            })
            .then(deployTemplate)
            .then(function (values) {

                console.log(`Deployed ARM Template to Azure`);

            });

    });

Find all source code for this console application on GitHub

Authenticate with a Service Principal

An Azure Service Principal is an application within Azure Active Directory that can be given specific permissions within an Azure Subscription. In our scenario, we can give a Service Principal a Contributor role within our Azure Subscription, allowing the SP to create Resource Groups and Resources on our behalf.

// =========================================================
// Step 1: Login to Azure with a Service Principal
// https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal-cli/
// =========================================================
function login() {

    return new Promise((resolve, reject) => {

        msRestAzure.loginWithServicePrincipalSecret(config.auth.APPLICATION_ID, config.auth.APPLICATION_SECRET, config.auth.DOMAIN, (error, credentials) => {

            if (error) reject(error);

            console.log(`Authenticated with Azure via Service Principal`);

            // Create an ARM client
            var client = new resourceManagement.ResourceManagementClient(credentials, config.auth.AZURE_SUBSCRIPTION_ID);

            // Resolve client
            resolve(client);

        });

    });

}

Service Principals can be created via the Azure Portal, PowerShell, or the Azure XPlat CLI

Create a Resource Group

Our configuration file sets the name and region of a Resource Group that will hold all of our resources. We also add a tag to the RG for further management capabilities in the future.

// =========================================================
// Step 2: Create an Azure Resource Group 
// =========================================================
function createRG(client) {

    return new Promise((resolve, reject) => {

        // Use the RM client to create a resource group
        client
            .resourceGroups
            .createOrUpdate(config.group.name, config.group.properties, function (error, response) {

                if (error) reject(error);

                console.log(`Created Resource Group named ${response.name}`);

                // Resolve Promise
                resolve([client, response.name]);

            });

    });

}

Deploy Shared Resources.

The CSV file lists 5 virtual machines, but there are separate resources that will be used across all of our virtual machines. Right now this consists of a Virtual Network that each VM will use. These shared resources are defined in a dedicated ARM Template, and will generate an output value to be used in future steps.

// =========================================================
// Step 3: Deploy Shared Resources ARM Template
// =========================================================
function deploySharedTemplate(values) {

    var client = values[0];
    var rgName = values[1];
    var deploymentName = `SharedResources-${_.random(100, 999)}`;

    // Read template from disk
    var template = JSON.parse(fs.readFileSync(config.template.sharedResourcesPath, 'utf8'));

    var deploymentParameters = {
        "properties": {
            "parameters": {},
            "template": template,
            "mode": "Incremental"
        }
    };

    return new Promise((resolve, reject) => {

        // Deploy template
        client
            .deployments
            .createOrUpdate(rgName, deploymentName, deploymentParameters, function (error, result) {

                if (error) reject(error);

                console.log(`Deployed Shared Resources ARM Template`);

                // Remove the Type attribute for the RG outputs
                var scrubbedOutputs = _.mapValues(result.properties.outputs, function (output) {

                    return { value: output.value };

                });

                resolve(scrubbedOutputs);

            });

    });

}

Setup Parameters

Each row of the CSV will become a set of parameters used for a deployment. Since Azure works primarily out of JSON objects, we need to serialize the local CSV file into an array of JSON objects. Then we will extend those parameters by adding the output value from the Shared Resources deployment so that each VM knows which virtual network to use.

// =========================================================
// Step 4: Parse CSV File containing ARM Template parameters and merge with Shared RG's outputs
// =========================================================
function getParameters(sharedOutputs) {

    return new Promise((resolve, reject) => {

        converter.fromFile(config.parameters.filePath, function (error, rows) {

            if (error) reject(error);

            // CSV values are returned as s tring (ex. {name: jane} )
            // ARM parameters have a format of parameter name, then a value object
            // ex. {name: jane} needs to be processed to {name: {value:jane}}
            // Loop through the original rows and process a new array

            // Create new array to hold processed parameters
            var processedParameters = [];

            // Loop through the original values
            rows.forEach((row, i) => {

                // Adjust value from a string to an object
                var processed = _.mapValues(row, function (original) {

                    return { value: original };

                });

                // Add parameters from Shared Resources deployment
                _.assignIn(processed, sharedOutputs);

                // Push new object into the array
                processedParameters.push(processed);

            });

            console.log(`Parsed parameters CSV file`);
            resolve(processedParameters);

        });

    });

}

Generate VM Deployments

We have an array of JSON objects corresponding to a desired VM. We will loop through each of these objects, combining them with a Virtual Machine ARM Template to create a series of deployments to the Resource Group.

// =========================================================
// Step 5: Deploy specific ARM Template
// =========================================================
function deployTemplate(values) {

    return new Promise((resolve, reject) => {

        // Set variables from the values array
        var client = values[0];
        var parameters = values[1];
        var rgName = config.group.name;
        var counter = 0;

        // Read template from disk
        var template = JSON.parse(fs.readFileSync(config.template.filePath, 'utf8'));

        // Loop through the sets of parameters 
        // creating a new deployment for each set in the parameters array
        parameters.forEach(function (parameter, i) {

            // Define a unique deployment name
            var deploymentName = `${config.group.name}-${_.random(100, 999)}`;

            // Define object for template deployment
            var deploymentParameters = {
                "properties": {
                    "parameters": parameter,
                    "template": template,
                    "mode": "Incremental"
                }
            };

            // Deploy template
            client
                .deployments
                .createOrUpdate(rgName, deploymentName, deploymentParameters, function (error, result) {

                    if (error) reject(error);

                    console.log(`Deployed ARM Template`);

                });

            // Iterate the counter  
            counter++;
            if (counter === parameters.length) resolve();

        });

    });

}

Run the console application

Navigate in the terminal to your working folder, and execute node app.js to start up the CSVProvisioner.

Using cmder but works across any shell

After a few minutes you should have a shiny new Resource Group full of resources that were defined in your CSV file (along with a shared VNet).

Wind Down

“Hello World” samples for Azure Resource Manager often use a simple template and a PowerShell command, but you can integrate ARM into your process in a variety of ways. For those that rely on spreadsheets to plan and organize infrastructure deployments, this sample demonstrates how deploy a series of Virtual Machines defined in a CSV file into an Azure Resource Group.

Find all source code for this console application on GitHub