Azure/Ansible – Provision a Virtual Machine instance using Infrastructure as Code

Note: This article has been duplicated from the previous article which uses Terraform and has been modified for Ansible.

In this article we will use Ansible (Infrastructure as Code) to swiftly bring up a Microsoft Azure Virtual Machine instance in East US on a static IP, add a DNS Zone for the site in mention and install docker/docker-compose on it.

We will use ‘myweb’ as an example in this article, using the same base path of ‘dev’ that was previously created and the container-admin Service Principal.

Please use ‘Create your Azure free account today’ prior to commencing with this article.

–>
Go in to the dev directory/link located within your home directory:

$ cd ~/dev

Upgrade the Azure CLI on your host:

$ sudo apt update && sudo apt -y upgrade azure-cli

Update PIP:

$ python3 -m pip install --upgrade --user pip

If there was an update, then forget remembered location references in the shell environment:

$ hash -r pip 

Install/Upgrade Ansible:

$ pip3 install ansible --upgrade --user && chmod 754 ~/.local/bin/ansible ~/.local/bin/ansible-playbook

Install the Ansible Azure modules (this may take a while):

$ pip3 install 'ansible[azure]' --upgrade --user

Modify the profile and the key/variable strings in the previously created Azure credentials file:

$ sed -i 's/\[container-admin/\[default/; s/application_id/client_id/; s/client_secret/secret/; s/directory_id/tenant/' ~/.azure/credentials

Note: The above change will break the previously created terraform-az-sp function. If you are also using Terraform, then please do this (user’s startup):

$ sed -i 's:application_id/arm_:client_id/arm_:; s:client_secret/arm_:secret/arm_:; s:directory_id/arm_:tenant/arm_:' ~/.bashrc

Remove the subscription_id and modify the keys/variables in our previously created az-login-sp function (user’s startup).

If you have not gone through the Azure/Terraform article:

$ sed -i "s:\$HOME/.azure/credentials | xargs):\$HOME/.azure/credentials | sed '/subscription_id/d; s/client_id/application_id/; s/secret/client_secret/; s/tenant/directory_id/' | xargs):" ~/.bashrc

If you have gone through the Azure/Terraform article:

$ sed -i "s:\$HOME/.azure/credentials | sed '/subscription_id/d':\$HOME/.azure/credentials | sed '/subscription_id/d; s/client_id/application_id/; s/secret/client_secret/; s/tenant/directory_id/':" ~/.bashrc

Source it in:

$ . ~/.bashrc

Add the <SUBSCRIPTION ID> (UI Console -> Azure Active Directory -> Search for (top left): Subscriptions -> Click your subscription -> Overview) in to the Azure credentials file (replace <SUBSCRIPTION ID>).

Note: This can be omitted if you have gone through the Azure/Terraform article:

$ echo "subscription_id=<SUBSCRIPTION ID>" >> ~/.azure/credentials 

In the Subscription, add roles to container-admin:
Access control (IAM) -> Role assignments ->

Add (Add role assignment) -> Role: DNS Zone Contributor -> Assign access to: Azure AD user, group, or service principal -> Select: container-admin -> Save

Add (Add role assignment) -> Role: Network Contributor -> Assign access to: Azure AD user, group, or service principal -> Select: container-admin -> Save

Add (Add role assignment) -> Role: Virtual Machine Contributor -> Assign access to: Azure AD user, group, or service principal -> Select: container-admin -> Save

Create a work folder and change in to it:

$ mkdir -p ansible/azure/myweb/scripts ansible/azure/myweb/rbac && cd ansible/azure/myweb

Create a custom Role Based Access for Resource Groups so container-admin can Read, Write and Delete Resource Groups in the subscription (replace <SUBSCRIPTION ID>):

$ cat << 'EOF' > rbac/rg-custom.jsn
> {
>    "Name": "Resource Group Allowance",
>    "IsCustom": true,
>    "Description": "Can read, write and delete Resource Groups.",
>    "Actions": [
>       "Microsoft.Resources/subscriptions/resourceGroups/read",
>       "Microsoft.Resources/subscriptions/resourceGroups/write",
>       "Microsoft.Resources/subscriptions/resourceGroups/delete"
>    ],
>    "NotActions": [],
>    "AssignableScopes": [
>       "/subscriptions/<SUBSCRIPTION ID>"
>    ]
> }
> EOF

Authenticate to Azure using the CLI with the same Administrative credentials you use in the UI (a browser window will popup requesting credentials):

$ az login

Create the Role Definition:

$ az role definition create --role-definition rbac/rg-custom.jsn

Add the role to container-admin:

$ az role assignment create --role "Resource Group Allowance" --assignee $(grep client_id ~/.azure/credentials | cut -f2 -d=) --subscription $(grep subscription_id ~/.azure/credentials | cut -f2 -d=)

Logout of the Azure CLI session:

$ az logout

Generate an SSH Key Pair (no password) and restrict permissions on it:

$ ssh-keygen -q -t rsa -b 2048 -N '' -f ~/.ssh/myweb && chmod 400 ~/.ssh/myweb

Create a hosts file and specify localhost:

$ cat << 'EOF' > hosts
> [local]
> localhost
> EOF

The following is performed with this Playbook:

  • create a resource group where all of our resources will be put in (within East US)
  • create an Azure DNS Zone of myweb.com (no A records will be created)
  • create a virtual network of 10.0.0.0/16
  • add a subnet of 10.0.1.0/24 within the VNET
  • allocate a static Public IP
  • create a Network Security Group and add a Security rule for allowing SSH (port 22) Inbound
  • create a Network Interface with a Dynamic Private IP
  • create a Basic A1 instance based off of Ubuntu 18_04 with a Standard SSD, password authentication turned off, our public key added as authorized and reference an extraneous file for custom_data (initialization script on Virtual Machine boot)
  • tag all resources
$ cat << 'EOF' > vm.yml
> # Create an Azure Virtual Machine instance and add a way to destroy it
> ---
> - hosts: local
>   connection: local
>
>   vars:
>     region: EastUS
>     prefix: myweb
>     subnet_name: internal
>     public_ip_name: external
>
>   tasks:
>   - name: Create a resource group
>     azure_rm_resourcegroup:
>       name: "{{ prefix }}-rg"
>       location: "{{ region }}"
>       state: present
>       tags:
>           Site: "{{ prefix }}.com"
>
>   - name: Create a DNS Zone
>     azure_rm_dnszone:
>       name: "{{ prefix }}.com"
>       resource_group: "{{ prefix }}-rg"
>       state: present
>       tags:
>           Site: "{{ prefix }}.com"
>
>   - name: Create a Virtual Network
>     azure_rm_virtualnetwork:
>       name: "{{ prefix }}-net"
>       address_prefixes: "10.0.0.0/16"
>       location: "{{ region }}"
>       resource_group: "{{ prefix }}-rg"
>       state: present
>       tags:
>           Site: "{{ prefix }}.com"
>
>   - name: Add a Subnet
>     azure_rm_subnet:
>       name: "{{ subnet_name }}"
>       resource_group: "{{ prefix }}-rg"
>       virtual_network: "{{ prefix }}-net"
>       address_prefix: "10.0.1.0/24"
>       state: present
>
>   - name: Allocate a Static Public IP
>     azure_rm_publicipaddress:
>       name: "{{ public_ip_name }}"
>       location: "{{ region }}"
>       resource_group: "{{ prefix }}-rg"
>       allocation_method: Static
>       state: present
>       tags:
>           Site: "{{ prefix }}.com"
>     register: static_public_ip
>
>   - name: Create a Network Security Group and allow inbound port(s)
>     azure_rm_securitygroup:
>       name: "{{ prefix }}-nsg"
>       location: "{{ region }}"
>       resource_group: "{{ prefix }}-rg"
>       rules:
>         - name: SSH
>           priority: 1001
>           direction: Inbound
>           access: Allow
>           protocol: Tcp
>           source_port_range: "*"
>           destination_port_range: 22
>           source_address_prefix: "*"
>           destination_address_prefix: "*"
>       state: present
>       tags:
>           Site: "{{ prefix }}.com"
> 
>   - name: Create a Network Interface with a Dynamic Private IP
>     azure_rm_networkinterface:
>       name: "{{ prefix }}-nic"
>       location: "{{ region }}"
>       resource_group: "{{ prefix }}-rg"
>       security_group: "{{ prefix }}-nsg"
>       virtual_network: "{{ prefix }}-net"
>       subnet_name: "{{ subnet_name }}"
>       ip_configurations:
>         - name: "{{ prefix }}-nic_conf"
>           private_ip_allocation_method: Dynamic
>           public_ip_address_name: "{{ public_ip_name }}"
>           primary: True
>       state: present
>       tags:
>           Site: "{{ prefix }}.com"
>
>   - name: Create an Ubuntu Virtual Machine with key based access and run a script on boot; use a Standard SSD
>     azure_rm_virtualmachine:
>       name: "{{ prefix }}-vm"
>       location: "{{ region }}"
>       resource_group: "{{ prefix }}-rg"
>       network_interfaces: "{{ prefix }}-nic"
>       vm_size: Basic_A1
?       image:
>         publisher: Canonical
>         offer: UbuntuServer
>         sku: '18.04-LTS'
>         version: latest
>       os_type: Linux
>       os_disk_name: "{{ prefix }}-disk"
>       os_disk_caching: ReadWrite
>       managed_disk_type: StandardSSD_LRS
>       short_hostname: "{{ prefix }}"
>       admin_username: ubuntu
>       custom_data: "{{ lookup('file', './scripts/install.sh') }}"
>       ssh_password_enabled: false
>       ssh_public_keys:
>             - path: /home/ubuntu/.ssh/authorized_keys
>               key_data: "{{ lookup('file', '~/.ssh/{{ prefix }}.pub') }}"
>       state: present
>       tags:
>           'Site': "{{ prefix }}.com"
>
>   - debug: msg="Public (static) IP is {{ static_public_ip.state.ip_address }} for {{ azure_vm.name }}"
>     when: static_public_ip.state.ip_address is defined
>
>   - debug: msg="Run this playbook for {{ azure_vm.name }} shortly to list the Public (static) IP."
>     when: static_public_ip.state.ip_address is not defined
>
>   - name: Destroy a Resource Group and all resources that fall under it
>     azure_rm_resourcegroup:
>       name: "{{ prefix }}-rg"
>       force_delete_nonempty: yes
>       state: absent
>     tags: [ 'never', 'destroy' ]
>
>   - name: Destroy the Network Watcher Resource Group and all resources that fall under it
>     azure_rm_resourcegroup:
>       name: "NetworkWatcherRG"
>       force_delete_nonempty: yes
>       state: absent
>     tags: [ 'never', 'destroy_networkwatcher' ]
> EOF

Create the shell script for custom_data:

$ cat << 'EOF' > scripts/install.sh
> #!/bin/bash
>
> MY_HOME="/home/ubuntu"
> export DEBIAN_FRONTEND=noninteractive
>
> # Install prereqs
> apt update
> apt install -y python3-pip apt-transport-https ca-certificates curl software-properties-common
> # Install docker
> curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
> add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
> apt update
> apt install -y docker-ce
> # Install docker-compose
> su ubuntu -c "mkdir -p $MY_HOME/.local/bin"
> su ubuntu -c "pip3 install docker-compose --upgrade --user && chmod 754 $MY_HOME/.local/bin/docker-compose"
> usermod -aG docker ubuntu
> # Add PATH
> printf "\nexport PATH=\$PATH:$MY_HOME/.local/bin\n" >> $MY_HOME/.bashrc
>
> exit 0
> EOF

Run the playbook:

$ ansible-playbook -i hosts vm.yml

Log on to the instance after a short while:

$ ssh -i ~/.ssh/myweb ubuntu@<The value of static_public_ip that was reported. One can also re-run the playbook to print it again.>

Type yes and hit enter to accept.

On the host (a short while is needed for the boot initialization script to complete):

$ docker --version
$ docker-compose --version
$ logout

Tear down the instance:

$ ansible-playbook -i hosts vm.yml --tags "destroy"

Destroy the Network Watcher Resource Group that was automatically created (if not found prior), if you do not have other virtual networks in the region which are using it:

$  ansible-playbook -i hosts vm.yml --tags "destroy_networkwatcher" 

<–

References:

Source:
ansible_myweb