I discussed how you could use Ansible with Terraform to simplify configuration management in my previous post. If, instead of Terraform, you prefer using Azure Resource Manager (ARM) templates to define the infrastructure and configuration for your project, you can use Ansible for managing parameters that customize each environment by dynamically generating a Resource Manager parameters file.

A great thing about using Ansible for your ARM configuration needs is that it includes a suite of modules for interacting with Azure Resource Manager. You can install the modules using Ansible Galaxy, a repository of Ansible Roles that you can drop directly in your Playbooks.

Since we don’t need the entire Azure modules suite to deploy ARM templates, I will use Azure CLI in my Playbook to do the same. You can install Azure CLI on your DevOps server to run the Playbook in your CI/CD pipeline.

Source Code

The source code of the Playbook is available in the following GitHub repository.

I will walk you through the steps to develop the Playbook. Throughout this exercise, I will often point you to my previous blog post, which will help you understand the various concepts in detail.

Rolling Out The Folders

Ansible Roles require a certain directory structure (why?). Let’s roll out the required folders with the following command.

$ mkdir -p {roles/plan/{tasks,templates},host_vars}
$ tree
.
├── host_vars
└── roles
    └── plan
        ├── tasks
        └── templates

Let’s start populating the folders now. We’ll use a public ARM quickstart template for this exercise that creates an Azure WebApp within a resource group. If you write ARM templates often, you must be aware of the rich library of quickstart templates that you can use as building blocks for your applications.

Setting Up The Role

Create a file named main.yaml in the tasks folder. In this file, we will define the tasks that would execute in sequence upon running the Playbook.

- name: Create parameters
  template:
    src: templates/azuredeploy.parameters.j2
    dest: "azuredeploy.parameters.json"

- name: Deploy ARM template
  shell: az deployment group create \
    --name AnsibleDeployment \
    --resource-group {{ resourceGroup }} \
    --template-uri "https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/201-web-app-github-deploy/azuredeploy.json" \
    --parameters @azuredeploy.parameters.json
  register: deploy

- name: Output
  debug:
    msg: "{{ deploy.stdout }}"

The Create parameters task dynamically generates a unique ARM parameters file for each environment, such as production and staging, using a Jinja2 template named azuredeploy.parameters.j2. We will create this file in the next step.

The Deploy ARM template task is quite simple. It executes the az deployment command of Azure CLI to deploy the remote ARM template using the dynamically generated parameters file as the argument. Finally, the Output task prints the output generated by the previous task to the console.

Let’s define the Jinja2 template, azuredeploy.parameters.j2, now.

Templating ARM Parameters File

Create a file named azuredeploy.parameters.j2 in the templates folder and add the following code.

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "siteName": {
      "value": "{{ arm['%s' | format(env)].siteName }}"
    },
    "location": {
      "value": "{{ arm.siteLocation }}"
    },
    "sku": {
      "value": "{{ arm['%s' | format(env)].sku }}"
    }
  }
}

The template variable arm['%s' | format(env)] will return the appropriate value based on the env variable’s value. We will set the env variable’s value through the command-line argument of the command that runs the playbook. To read more about such use of the Jinja2 template, refer to my previous blog post.

Create a file named localhost.yaml in the host_vars folder. The file localhost.yaml will be read by Ansible when we execute the playbook against the host localhost. The files in the host_vars folder contain the variables that Ansible should use when targeting a particular host. You can also define variables that are common for all the hosts in the same group here.

arm:
  siteLocation: "australiaeast"
  production:
    siteName: "prod-ae-web"
    sku: "B1"
  staging:
    siteName: "prod-ae-web"
    sku: "F1"

With the nested configuration, the Jinja template will set the following values of the variables.

  1. siteName (= arm[‘production’].siteName) will be set to prod-ae-web
  2. sku (= terraform[‘staging’].sku) will be set to F1

Choose an appropriate name of the site and SKU as per your needs. Finally, let’s create a file named deploy.yaml in the root directory. This file will run the role named plan on the localhost. Remember that localhost is an implicit machine available in the Ansible inventory.

- name: Apply configuration via localhost
  hosts: localhost
  connection: local
  roles:
    - plan

Let’s execute the playbook to realize a success scenario and a failure scenario.

Executing The Playbook

I have created a helper script that you can use to execute the playbook easily. Create a shell script named run.sh in the base folder and populate it with the following code.

location='australiaeast'

# Install AZ CLI
echo 'Installing AZ CLI'
if ! command -v az >/dev/null; then
    curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
fi

# Authenticate using service principal on CI: https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal
echo 'Login to Azure'
az login
az account set --subscription $1

# Create resource group
echo 'Creating resource group'
az group create --name $2 --location $location >/dev/null

# Run playbook
echo 'Executing playbook'
ansible-playbook deploy.yaml -e resourceGroup=$2 -e env=$3

The script uses the command’s positional parameters' values to create a resource group and set the value of the parameter, env, of the Playbook.

Let’s first execute the script to spin up the infrastructure on the staging environment. I will take the easy route and allow Ansible to use the auth tokens received by running the command az login. However, on CI server, you must use a service principal for authentication. The following command will create a resource group named ansible-web-rg in the specified subscription and create a Web App in the resource group.

sh run.sh <subscription id> ansible-web-rg staging

Keep a close eye on the file structure of your playbook while you run the command. When the playbook executes, you will find a new file named azuredeploy.parameters.json created in the project’s base directory. Execute the following command to view the contents of the new file.

$ cat azuredeploy.parameters.json

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "siteName": {
      "value": "prod-ae-web"
    },
    "location": {
      "value": "australiaeast"
    },
    "sku": {
      "value": "F1"
    }
  }
}

Let’s get back to the execution of the helper script. Following is the output I received on executing the previous command.

Execute playbook
Execute playbook

Let’s visit the Azure management portal to verify the result.

Inspect resources on the management portal
Inspect resources on the management portal

Let’s now inspect the behavior of the playbook when we induce a bug. Set an unsupported SKU in the localhost.yaml file and replay the script. In this instance, you will receive the following output that contains the error.

Error on playbook execution
Error on playbook execution

💡 Tip: If your terminal does not present a clean output like mine, add/update the following configurations in the /etc/ansible/ansible.cfg file under the [defaults] collection.

bin_ansible_callbacks = True

stdout_callback = yaml

stderr_callback = yaml

Did you notice that we specified configuration values for provisioning the production environment as well? I have left it as an exercise for you.

Conclusion

We discussed how we could combine Ansible and ARM templates and use a little bit of Jinja templates' magic to generate parameters for ARM templates on the fly. I hope this article helps you add yet another tool to your IaC arsenal.

Did you enjoy reading this article? I can notify you the next time I publish on this blog... ✍