How can we connect to Azure using Ansible?
Ansible is a versatile configuration management tool that has truly taken the world by storm offering strong integrations with cloud providers giving provisioning tools like terraform a definite run for their money. We could very easily connect to our resources in the Azure cloud using Ansible. But this involves creating a service principal for ansible in Azure Active Directory.
I recently came across a task wherein I needed to fetch configuration details about a virtual machine in the Azure cloud but due to security restrictions I could not get a service principal created for ansible. So, I choose a different route. Instead of using the Azure specific modules that Ansible has built in, I relied on embedding Azure CLI commands in the shell module in my ansible playbook, used a variable for the hostname and went about writing the required azure cli commands I needed to fetch the information from the virtual machine.
We could even use this methodology for creating and deleting virtual machines in the Azure cloud. But a major shortcoming of this approach is that the commands being executed would not be idempotent which is one of the cornerstones that configuration management software are built on. As a pre-requisite we need the azure cli installed on the system we’d like to run our ansible playbook from and the az login
command should also be already executed to ensure that we’ve logged in to our Azure account.
In this article, we’ll install the pre-requisite azure cli on a Linux system which already has ansible installed on it and then execute the az login command to authenticate to the Azure portal.
Installing Azure CLI on Linux
Step 1: Import the Microsoft repository key
Execute the following command to import the microsoft repository key which we will use in next step:
[root@ansible-demo ~]# rpm --import https://packages.microsoft.com/keys/microsoft.asc
Step 2: Create local azure-cli repository information
The below command creates the azure-cli.repo
file and populates it with the required Azure CLI repository location.
[root@ansible-demo ~]# echo -e "[azure-cli] > name=Azure CLI > baseurl=https://packages.microsoft.com/yumrepos/azure-cli > enabled=1 > gpgcheck=1 > gpgkey=https://packages.microsoft.com/keys/microsoft.asc" | sudo tee /etc/yum.repos.d/azure-cli.repo [azure-cli] name=Azure CLI baseurl=https://packages.microsoft.com/yumrepos/azure-cli enabled=1 gpgcheck=1 gpgkey=https://packages.microsoft.com/keys/microsoft.asc
Step 3: Install azure-cli package
The Azure cli repository should now appear when we run the yum repolist
command.
[root@ansible-demo ~]# yum repolist Loaded plugins: fastestmirror Loading mirror speeds from cached hostfile * base: download.cf.centos.org * epel: d2lzkl7pfhq30w.cloudfront.net * extras: download.cf.centos.org * nux-dextop: li.nux.ro * updates: download.cf.centos.org azure-cli | 3.0 kB 00:00:00 azure-cli/primary_db | 64 kB 00:00:00 repo id repo name status azure-cli Azure CLI 108 base/7/x86_64 CentOS-7 - Base 10,072 epel/x86_64 Extra Packages for Enterprise Linux 7 - x86_64 13,674 extras/7/x86_64 CentOS-7 - Extras 500 nux-dextop/x86_64 Nux.Ro RPMs for general desktop use 2,724 updates/7/x86_64 CentOS-7 - Updates 2,751 xrdp xrdp 2,724 repolist: 32,553 [root@ansible-demo ~]#
Now that we are certain that the repository is available, let’s install the azure cli binary using yum.
[root@ansible-demo ~]# yum install azure-cli -y
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
* base: download.cf.centos.org
* epel: d2lzkl7pfhq30w.cloudfront.net
* extras: download.cf.centos.org
* nux-dextop: li.nux.ro
* updates: download.cf.centos.org
Resolving Dependencies
--> Running transaction check
---> Package azure-cli.x86_64 0:2.28.0-1.el7 will be installed
--> Finished Dependency Resolution
Dependencies Resolved
====================================================================
Package Arch Version Repository Size
====================================================================
Installing:
azure-cli x86_64 2.28.0-1.el7 azure-cli 45 M
Transaction Summary
====================================================================
Install 1 Package
Total download size: 45 M
Installed size: 595 M
Downloading packages:
azure-cli-2.28.0-1.el7.x86_64.rpm | 45 MB 00:00:00
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
Installing : azure-cli-2.28.0-1.el7.x86_64 1/1
Verifying : azure-cli-2.28.0-1.el7.x86_64 1/1
Installed:
azure-cli.x86_64 0:2.28.0-1.el7
Complete!
Run the Azure CLI with the az command. To sign in, use az login command.
Step 4: Run the login command.
Now that we have successfully installed the Azure CLI package, we’ll authenticate to our Azure account with it so that we may interact with our resources inside the Azure cloud via the Azure CLI.
When we run the az login
command it will display a code to authenticate to the Azure portal. We need to open the URL https://microsoft.com/devicelogin and provide the code.
Once the code is accepted we are prompted for our credentials. After successfully authentication, the portal will display the following message.
On the command line, the az login
command would display some information about our account like the subscription and logged in user as shown below.
[root@ansible-demo ~]# az login
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code HTTE53Q4C to authenticate.
[
{
"cloudName": "AzureCloud",
"homeTenantId": "3617ef9b-98b4-40d9-ba43-e1ed6709cf0d",
"id": "964df7ca-3ba4-48b6-a695-1ed9db5723f8",
"isDefault": true,
"managedByTenants": [],
"name": "cloud-demo ",
"state": "Enabled",
"tenantId": "3617ef9b-98b4-40d9-ba43-e1ed6709cf0d",
"user": {
"name": "demouser@example.com",
"type": "user"
}
}
]
We can now run azure cli commands to interact with our resources in the azure portal.
Ansible playbook setup to query VM information in Azure
Given below is a glimpse of my playbook folder structure.
[root@ansible-demo Ansible_to_Azure]# pwd /root/Ansible_to_Azure [root@ansible-demo Ansible_to_Azure]# ls -ltrR .: total 20 -rw-r--r--. 1 root root 1447 Oct 19 17:01 info.json -rw-r--r--. 1 root root 4374 Oct 19 17:01 get_info.yml drwxr-xr-x. 2 root root 20 Oct 19 17:01 templates -rw-r--r--. 1 root root 10 Oct 19 17:01 inventory ./templates: total 4 -rw-r--r--. 1 root root 1293 Oct 19 17:01 info.j2
Here,
- Inventory file : This is where we will add the hostname of the VM that we wish to query. Note we are not using dynamic inventories at this time.
- Get_info.yml: This is the actual ansible playbook that we are going to execute.
- Templates: This directory contains a jinja2 template file that will be used to store and format the information retrieved by the playbook. The name of the template file is info.j2.
- Info.json: This is the output file generated from the jinja2 template file that we house in the templates directory. The output from the jinja2 template is in json format and hence the name info.json.
The get_info.yml playbook
Azure VMs have a plethora of information that can be retrieved using the Azure CLI. In this playbook, we’ve retrieved the following information:
- CPU
- Memory allocated
- Region/AZ
- Public IP
- Private IP
- Network Interface Card name
- Number of disks
- Disk type
- Size of each disk
- Disk IOPS
- Caching on disk
- Network security group rules
Here is the content of the get_info.yml
playbook.
[root@ansible-demo Ansible_to_Azure]# cat get_info.yml
---
- name: Query information about Azure
hosts: all
gather_facts: no
vars:
rg: 1-7b72c7b6-playground-sandbox-2
region: eastus2
tasks:
- name: Get VM Size
shell: "az vm show -g {{ rg }} -n {{ansible_hostname}} --query 'hardwareProfile.vmSize'"
register: vm_size
delegate_to: localhost
- name: Store vm size as a fact
set_fact:
VM_Size: "{{ vm_size.stdout_lines[0] }}"
- name: Get cpu and memory information of VM
shell: "az vm list-sizes --location {{ region }} | grep -A1 -B1 {{ VM_Size }}| awk '{print $2}'"
register: out
delegate_to: localhost
- name: Get Public IP address of VM
shell: "az vm show -d -g {{ rg }} -n {{ansible_hostname}} --query publicIps -o tsv"
register: publicip
delegate_to: localhost
- name: Get Private IP address of VM
shell: "az vm show -d -g {{ rg }} -n {{ansible_hostname}} --query privateIps -o tsv"
register: privateip
delegate_to: localhost
- name: Get allocated disk count
shell: "az vm show -d -g {{ rg }} -n {{ansible_hostname}} | grep -c Microsoft.Compute/disks"
register: diskcount
delegate_to: localhost
- name: Get disk names and stroage account types
shell: az vm show --resource-group {{ rg }} --name {{ansible_hostname}} --query "storageProfile.dataDisks[][{Name:name,StorageAccountType:managedDisk}]"
register: disk_info
delegate_to: localhost
- name: Set disk info as a fact
set_fact:
disk_list: "{{disk_info.stdout}}"
- name: Get size of each disk
shell: az vm show -g {{ rg }} -n {{ansible_hostname}} --query "storageProfile.dataDisks[][{Name:name,DiskSizeGB:diskSizeGb}]" -o table | awk -F, 'NR > 2 {print $2, $4}' | tr -d ')])'
register: disk_size_list
delegate_to: localhost
- name: Set fact for disk sizes
set_fact: disk_size_lists="{{disk_size_list.stdout_lines}}"
- name: Get VM disk caching information
shell: az vm show -g {{rg}} -n {{ansible_hostname}} --query "storageProfile.dataDisks[][{Name:name,Caching:caching}]" -o table | awk -F, 'NR>2 {print $2, $4}' | tr -d ")])'"
register: disk_caching
delegate_to: localhost
- name: Set Disk caching information as a fact
set_fact: disk_cache="{{disk_caching.stdout_lines}}"
- name: Get VM region and Availability zone
shell: az vm show -g {{rg}} -n {{ansible_hostname}} --query "[location, zones]" -o tsv | tr '\n' ' '
register: az_info
delegate_to: localhost
- name: set fact for region and AZ
set_fact: az_region_info="{{az_info.stdout}}"
- name: Get Default NSG details
shell: az network nsg list --resource-group {{rg}} --query "[].defaultSecurityRules[].{Name:name, Accecc:access, Direction:direction, DestAddr:destinationAddressPrefix, SourceAddr:sourceAddressPrefix,DestPort:destinationPortRange, SourcePort:sourcePortRange }" -o tsv
register: def_nsg_out
delegate_to: localhost
- name: Set fact for default NSG rules
set_fact: def_nsg_out_fact="{{def_nsg_out.stdout}}"
- name: Get User defined NSG rule details
shell: az network nsg list --resource-group {{rg}} --query "[].securityRules[].{Name:name, Accecc:access, Direction:direction, DestAddr:destinationAddressPrefix, SourceAddr:sourceAddressPrefix,DestPort:destinationPortRange, SourcePort:sourcePortRange }" -o tsv
register: ud_nsg_out
delegate_to: localhost
- name: Set fact for User defined NSG rules
set_fact: ud_nsg_out_fact="{{ud_nsg_out.stdout}}"
- name: Get VM NIC name
shell: "az vm nic list -g {{ rg }} --vm-name {{ansible_hostname}} --query [].id | grep subscriptions | awk -F/ '{print $NF}'"
register: nicname
delegate_to: localhost
- name: Set fact for NIC name
set_fact: nic_name="{{nicname.stdout}}"
- name: Set fact for disk count
set_fact: disk_count="{{diskcount.stdout}}"
- name: Set fact for Public IP
set_fact:
public_ip: "{{publicip.stdout_lines[0]}}"
- name: Set fact for Private IP
set_fact:
private_ip: "{{privateip.stdout_lines[0]}}"
- name: Set fact for CPU
set_fact:
num_cpu: "{{ out.stdout_lines[2] | regex_replace(',', '') }}"
- name: Set fact for Memory
set_fact:
mem_mb: "{{ out.stdout_lines[0] | regex_replace(',', '') }}"
- name: Populate template file with info
template:
src: info.j2
dest: /root/ansible_azure/info.json
delegate_to: localhost
We will not be diving deep into the details of each task defined inside the playbook (you can read our ansible tutorial to learn more) but will instead elaborate on the general theme of the playbook.
The tasks using the shell module are running different Azure CLI commands on the virtual machine. We are using the “delegate_to: localhost” flag because these commands are being executed on the localhost. We have used a lot of set_fact tasks
because these allow us to use variables to store the value of registered output variables in the tasks and then use them in the jinja2 template.
In the last task of the playbook, the variables used in the jinja2 template are populated with their actual values and stored in the file /root/ansible_azure/info.json
.
The info.j2 template file
The template file contains a lot of information and uses loop and conditional constructs to filter out the desired information. We’ve also made heavy use of jinj2 filters replace and split in the template file to filter out the data. Given below content of the file.
[root@ansible-demo Ansible_to_Azure]# cat templates/info.j2
{
"Number of CPUs": "{{ num_cpu }}",
"Memory in MB": "{{mem_mb}}",
"region/AZ":"{{az_region_info}}"
"Public IP address": "{{public_ip}}",
"Private IP address": "{{private_ip}}",
"Number of disks": "{{disk_count}}",
"VM NICname is": "{{nic_name}}",
"Disk Storage type:" {
{% for disk in disk_list -%}
"{{ disk[0].Name}}":"{{ disk[0].StorageAccountType.storageAccountType }}"
{% endfor %}
}
"Disk IOPS" {
{% for disk in disk_list -%}
{% if disk[0].StorageAccountType.storageAccountType == 'Premium_LRS' %}
"{{ disk[0].Name}}":"120",
{% elif disk[0].StorageAccountType.storageAccountType == 'StandardSSD_LRS' %}
"{{ disk[0].Name}}":"500",
{% else %}
"{{ disk[0].Name}}":"500"
{% endif %}
{% endfor %}
}
"Disk Size:" {
{% for disk_s in disk_size_lists -%}
"{{disk_s.split(' ')[1]| replace("'",'') }}":"{{disk_s.split(' ')[-1]}}"
{% endfor %}
}
"Disk Cache:" {
{% for disk_c in disk_cache -%}
"{{disk_c.split(' ')[1]}}":"{{disk_c.split(' ')[-1]}}"
{% endfor %}
}
"Default NSG details" {
"Name Access Direction DestAddr SourceAddr DestPort SourcePort",
"{{def_nsg_out_fact| replace("\t",' ')}}",
}
"User DEfined NSG details" {
"Name Access Direction DestAddr SourceAddr DestPort SourcePort",
"{{ud_nsg_out_fact| replace("\t",' ')}}",
}
}
To run the playbook type the following command:
ansible-playbook -i inventory info.yml
The resulting info.json
file has the following content.
[root@ansible-demo Ansible_to_Azure]# cat info.json
{
"Number of CPUs": "2",
"Memory in MB": "8192",
"region/AZ":"eastus2 2"
"Public IP address": "20.190.193.149",
"Private IP address": "10.0.0.4",
"Number of disks": "5",
"VM NICname is": "azuredemo851",
"Disk Storage type:" {
"azuredemo_DataDisk_0":"Premium_LRS"
"azuredemo_DataDisk_1":"StandardSSD_LRS"
"azuredemo_DataDisk_2":"Standard_LRS"
"azuredemo_DataDisk_3":"Premium_LRS"
}
"Disk IOPS" {
"azuredemo_DataDisk_0":"120",
"azuredemo_DataDisk_1":"500",
"azuredemo_DataDisk_2":"500"
"azuredemo_DataDisk_3":"120",
}
"Disk Size:" {
"azuredemo_DataDisk_0":"8"
"azuredemo_DataDisk_1":"32"
"azuredemo_DataDisk_2":"64"
"azuredemo_DataDisk_3":"4"
}
"Disk Cache:" {
"azuredemo_DataDisk_0":"ReadOnly"
"azuredemo_DataDisk_1":"ReadWrite"
"azuredemo_DataDisk_2":"ReadWrite"
"azuredemo_DataDisk_3":"None"
}
"Default NSG details" {
"Name Access Direction DestAddr SourceAddr DestPort SourcePort",
"AllowVnetInBound Allow Inbound VirtualNetwork VirtualNetwork * *
AllowAzureLoadBalancerInBound Allow Inbound * AzureLoadBalancer * *
DenyAllInBound Deny Inbound * * * *
AllowVnetOutBound Allow Outbound VirtualNetwork VirtualNetwork * *
AllowInternetOutBound Allow Outbound Internet * * *
DenyAllOutBound Deny Outbound * * * *",
}
"User DEfined NSG details" {
"Name Access Direction DestAddr SourceAddr DestPort SourcePort",
"SSH Allow Inbound * * 22 *
Port_8080 Allow Inbound * * 8080 *",
}
}
Summary
In this article, we shared a practically tested method of querying information about a virtual machine in Azure using an Ansible playbook. The playbook itself contains multiple Azure CLI commands which we encourage you to check out individually. Also, the way the playbook and the jinja2 template have been written and formatted should help you in writing playbooks in future where there is a similar requirement.
References
We referred to the official Microsoft documentation for installing Azure CLI on our VM and also for adding queries to our Azure CLI commands. Links to both have been shared below.
Install the Azure CLI on Linux
How to query Azure CLI command output using a JMESPath query
Nice one, thanks 🙂