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

In this article we will use Terraform (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

Grab Terraform:

$ wget https://releases.hashicorp.com/terraform/0.12.20/terraform_0.12.20_linux_amd64.zip

Install Unzip if you do not have it installed:

$ sudo apt -y install unzip

Unzip it to ~/.local/bin and set permissions accordingly on it:

$ unzip terraform_0.12.20_linux_amd64.zip -d ~/.local/bin && chmod 754 ~/.local/bin/terraform

Add this function in to your user’s startup to parse the previously created credentials file and pass pertinent login information as a servicePrincipal to Terraform, in a sub-shell:

$ cat << 'EOF' >> ~/.bashrc
>
> function terraform-az-sp() {
>         (export $(grep -v '^\[' $HOME/.azure/credentials | sed 's/application_id/arm_client_id/; s/client_secret/arm_client_secret/; s/directory_id/arm_tenant_id/; s/subscription_id/arm_subscription_id/; s/^[^=]*/\U&\E/' | xargs) && terraform $*)
> }
> EOF

Remove the subscription_id in our previously created az-login-sp function (user’s startup):

$ sed -i "s:\$HOME/.azure/credentials | xargs):\$HOME/.azure/credentials | sed '/subscription_id/d' | xargs):" ~/.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>):

$ 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 terraform/azure/myweb/scripts terraform/azure/myweb/rbac && cd terraform/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 application_id ~/.azure/credentials | cut -f2 -d=) --subscription $(grep subscription_id ~/.azure/credentials | cut -f2 -d=)

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

Ensure the terraform version is greater then or equal to 0.12:

$ cat << 'EOF' > versions.tf
> terraform {
>   required_version = ">= 0.12"
> }
> EOF

Set the version for the AzureRM Provider to greater then or equal to 1.44:

$ cat << 'EOF' > provider.tf
> provider "azurerm" {
>   version = ">= 1.44"
> }
> EOF

Set the default region and prefix variable:

$ cat << 'EOF' > vars.tf
> variable "region" {
>   default = "EastUS"
> }
>
> variable "prefix" {
>   default = "myweb"
> }
> EOF

The following is performed with this script/code:

  • 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.tf
> # Create a Resource Group
> resource "azurerm_resource_group" "myweb" {
>   name     = "${var.prefix}-rg"
>   location = var.region
>
>   tags = {
>         Site = "${var.prefix}.com"
>     }
> }
>
> # Create a DNS Zone
> resource "azurerm_dns_zone" "myweb" {
>   name                = "${var.prefix}.com"
>   resource_group_name = azurerm_resource_group.myweb.name
>
>   tags = {
>         Site = "${var.prefix}.com"
>     }
> }
>
> # Create a Virtual Network
> resource "azurerm_virtual_network" "myweb" {
>   name                = "${var.prefix}-net"
>   address_space       = ["10.0.0.0/16"]
>   location            = azurerm_resource_group.myweb.location
>   resource_group_name = azurerm_resource_group.myweb.name
>
>   tags = {
>         Site = "${var.prefix}.com"
>     }
> }
>
> # Add a Subnet
> resource "azurerm_subnet" "internal" {
>   name                 = "internal"
>   resource_group_name  = azurerm_resource_group.myweb.name
>   virtual_network_name = azurerm_virtual_network.myweb.name
>   address_prefix       = "10.0.1.0/24"
> }
>
> # Allocate a Static Public IP
> resource "azurerm_public_ip" "external" {
>   name                = "external"
>   location            = var.region
>   resource_group_name = azurerm_resource_group.myweb.name
>   allocation_method   = "Static"
>
>   tags = {
>         Site = "${var.prefix}.com"
>     }
> }
>
> # Create a Network Security Group and allow inbound port(s)
> resource "azurerm_network_security_group" "myweb" {
>   name                = "${var.prefix}-nsg"
>   location            = var.region
>   resource_group_name = azurerm_resource_group.myweb.name
>
>   security_rule {
>         name                       = "SSH"
>         priority                   = 1001
>         direction                  = "Inbound"
>         access                     = "Allow"
>         protocol                   = "Tcp"
>         source_port_range          = "*"
>         destination_port_range     = "22"
>         source_address_prefix      = "*"
>         destination_address_prefix = "*"
>     }
>
>   tags = {
>         Site = "${var.prefix}.com"
>     }
> }
>
> # Create a Network Interface with a Dynamic Private IP
> resource "azurerm_network_interface" "myweb" {
>   name                      = "${var.prefix}-nic"
>   location                  = azurerm_resource_group.myweb.location
>   resource_group_name       = azurerm_resource_group.myweb.name
>   network_security_group_id = azurerm_network_security_group.myweb.id
>
>   ip_configuration {
>        name                          = "${var.prefix}-nic_conf"
>        subnet_id                     = azurerm_subnet.internal.id
>        private_ip_address_allocation = "Dynamic"
>        public_ip_address_id          = azurerm_public_ip.external.id
>     }
>
>   tags = {
>         Site = "${var.prefix}.com"
>     }
> }
>
> # Create an Ubuntu Virtual Machine with key based access and run a script on boot; use a Standard SSD
> resource "azurerm_virtual_machine" "myweb" {
>   name                  = "${var.prefix}-vm"
>   location              = azurerm_resource_group.myweb.location
>   resource_group_name   = azurerm_resource_group.myweb.name
>   network_interface_ids = [azurerm_network_interface.myweb.id]
>   vm_size               = "Basic_A1"
>
>   storage_image_reference {
>       publisher = "Canonical"
>       offer     = "UbuntuServer"
>       sku       = "18.04-LTS"
>       version   = "latest"
>     }
>
>   storage_os_disk {
>       name              = "${var.prefix}-disk"
>       caching           = "ReadWrite"
>       create_option     = "FromImage"
>       managed_disk_type = "StandardSSD_LRS"
>     }
>
>   os_profile {
>       computer_name  = var.prefix
>       admin_username = "ubuntu"
>       custom_data    = data.template_file.init_script.rendered
>     }
>
>   os_profile_linux_config {
>         disable_password_authentication = true
>         ssh_keys {
>             path     = "/home/ubuntu/.ssh/authorized_keys"
>             key_data = file("~/.ssh/${var.prefix}.pub")
>           }
>     }
>
>   tags = {
>         Site = "${var.prefix}.com"
>     }
> }
> EOF

Output our allocated static Public IP after creation:

$ cat << 'EOF' > output.tf
> output "static_public_ip" {
>   value = azurerm_public_ip.external.ip_address
> }
> EOF

Create a template file to reference a boot initialization script:

$ cat << 'EOF' > install.tf
> data "template_file" "init_script" {
>   template = "${file("scripts/install.sh")}"
> }
> 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

Initialize the directory:

$ terraform init

Run a dry-run to see what will occur:

$ terraform-az-sp plan

Provision:

$ terraform-az-sp apply -auto-approve

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 use 'terraform-az-sp output static_public_ip' 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 what was created by first performing a dry-run to see what will occur:

$ terraform-az-sp plan -destroy 

Tear down the instance:

$ terraform-az-sp destroy -auto-approve

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:

$ az group delete -n NetworkWatcherRG --yes

Logout of the Azure CLI session:

$ az logout

<–

References:

Source:
terraform_azure_myweb