Deploying to Microsoft Azure with Terraform

Last year, I fell in love with Terraform and I used it with AWS and Digital ocean with really great results.

I recently began to explore Azure and I faced a couple of challenges getting to use Terraform with Azure, so I thought I'd write an article about it.

In this article, I will talk about Terraform and show the steps I followed, and challenges faced while setting up virtual machines in a Virtual network and all the supporting infrastructure (subnets, resource groups, security groups, etc).

Background

HashiCorp's Terraform is my preferred tool for orchestration. Although I still use Ansible for configuration management, I prefer to use Terraform for orchestration.

Configuration management tools install and manage software on a machine that already exists. Terraform is not a configuration management tool, and it allows existing tooling to focus on their strengths: bootstrapping and initializing resources.

With the availability of docker and packer, there's little need for configuration management, so I have found my need for ansible has reduced significantly.

Why I use terraform

I could probably write a blog post about how great terraform is, but I'll just list 2 reasons here.

  • Declarative code : Terraform lets you use a declarative style to write your code, so you just write your desired end state, and leave it to decide the order in which your declared resources need to be created.
  • Client-side architecture : Terraform connects to cloud providers directly through their APIs, so you don't need to setup or run any special softwares on your servers.

Requirements

  • Terraform
  • Azure-CLI

Installing Terraform

To install Terraform, you need to download the appropriate package, unzip the package, and add the executable file to your PATH.

# step 1. Download the package
wget https://releases.hashicorp.com/terraform/0.8.7/terraform_0.8.7_linux_amd64.zip
# step 2. Unzip the package
unzip terraform_0.8.7_linux_amd64.zip
#move the execuatable to directory in PATH 
sudo mv terraform /usr/bin/

At this point, terraform should be added to your path. You can confirm this by running terraform --version which should show your installed version 0.8.7 in my case.

Installing Azure CLI

pip install --user --upgrade azure-cli

If you don't have python or pip installed and would rather not install them, you can read here for alternative installation methods.

Connecting to Azure subscription

Every account on Azure is part of a subscription. To connect to azure services, Terraform needs to be able to provision resources unto your Azure subscription for you.

Azure Active Directory manages access to subscriptions. It is an Identity and Access management solution that provides a set of capabilities to manage users and groups.
I'm still wrapping my head around the idea of Azure Active Directory so you can join me to read about it here

To connect Terraform to azure, you need the following credentials:

  • A subscription ID
  • A client ID
  • A client secret
  • A Tenant ID

HashiCorp and Microsoft have produced scripts to help with obtaining these here however the script continually failed for me, so here's how I went about obtaining these credentials.

Login

# first step, run the login command follow the steps 
$ az login

#you should see an output like below.
[
  {
    "cloudName": "AzureCloud",
    "id": "ads238932-2323-209239-SUSI-02ksjdk",
    "isDefault": true,
    "name": "BizSpark",
    "state": "Enabled",
    "tenantId": "fo90dfoi-23-232ji-a233-c2389jkk2389832",
    "user": {
      "name": "email@outlook.com",
      "type": "user"
    }
  }
]
#save the tenantId and id. The id here is your subscription ID

Now we have the subscription ID and tenant ID, next we find the client secret and client ID.

Set subscription ID

#replace SUBSCRIPTION_ID with the id above or set it as the SUBSCRIPTION_ID environmental variable.

az account set --subscription="${SUBSCRIPTION_ID}"

Create role for subscription

Three things need to be done here:

  • Create Azure active directory application
  • Create Azure service principal
  • Assign a contributor role
#Create a service principal, configure its access to Azure resources and assign Contributor role.

az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/${SUBSCRIPTION_ID}"

# This should return JSON with the appId and password

{
  "appId": "12332skdu-we32-23df-se43-wew23243223",
  "displayName": "azure-cli-2017-02-10-02-18-22",
  "name": "http://azure-cli-2017-02-10-02-18-22",
  "password": "jsiue981289-12-12er-we2344-23ksjksd",
  "tenant": "fo90dfoi-23-232ji-a233-c2389jkk2389832"
} 

The appId returned above is the client ID, and password is the client secret we need to use with terraform.

You can confirm the credentials by running the login command on the cli using the app ID, password and tenant ID obtained above.

az login --service-principal -u [APP ID] -p [PASSWORD] --tenant [TENANT ID]

Terraform script

After obtaining credentials we can move on to create the terraform script. In this part, I'll create an empty resource group and a virtual network with 3 subnets.

I'll create two files; one for variables and one to create resources. I like to keep my variables in a different file, so they can be referenced in my script.

First I'll create a file to save my variables, I'll call this variables.tf

#variables.tf
variable "tenant_id" {
	default = "o90dfoi-23-232ji-a233-c2389jkk2389832"
}

variable "client_id" {
	default="12332skdu-we32-23df-se43-wew23243223"
}

variable "client_secret" {
	default="jsiue981289-12-12er-we2344-23ksjksd"
}

variable "subscription_id" {
	default="ads238932-2323-209239-SUSI-02ksjdk"
}

Next I'll create a file called main.tf with code to create my resources.

#main.tf
# Configure the Microsoft Azure Provider
provider "azurerm" {
  subscription_id = "${var.subscription_id}"
  client_id       = "${var.client_id}"
  client_secret   = "${var.client_secret}"
  tenant_id       = "${var.tenant_id}"
}

# Create a resource group
resource "azurerm_resource_group" "development" {
    name     = "development"
    location = "North Europe"
}

# Create a virtual network in the web_servers resource group
resource "azurerm_virtual_network" "network" {
  name                = "developmentNetwork"
  address_space       = ["192.168.0.0/16"]
  location            = "${azurerm_resource_group.production.location}"
  resource_group_name = "${azurerm_resource_group.production.name}"

  subnet {
    name           = "subnet1"
    address_prefix = "192.168.1.0/24"
  }

  subnet {
    name           = "subnet2"
    address_prefix = "192.168.2.0/24"
  }

  subnet {
    name           = "subnet3"
    address_prefix = "192.168.3.0/24"
  }
}

Running the script

I'll run terraform plan to see what changes will be made. If you're new to terraform. The plan command helps you catch bugs and verify changes before deployment.

$ terraform apply

Refreshing Terraform state in-memory prior to plan...

+ azurerm_resource_group.development
    location: "northeurope"
    name:     "development"
    tags.%:   "<computed>"

+ azurerm_virtual_network.network
    address_space.#:                  "1"
    address_space.0:                  "192.168.0.0/16"
    location:                         "northeurope"
    name:                             "developmentNetwork"
    resource_group_name:              "development"
    subnet.#:                         "3"
    subnet.1008457452.address_prefix: "192.168.3.0/24"
    subnet.1008457452.name:           "subnet3"
    subnet.1008457452.security_group: ""
    subnet.1321000609.address_prefix: "192.168.2.0/24"
    subnet.1321000609.name:           "subnet2"
    subnet.1321000609.security_group: ""
    subnet.3646277238.address_prefix: "192.168.1.0/24"
    subnet.3646277238.name:           "subnet1"
    subnet.3646277238.security_group: ""
    tags.%:                           "<computed>"


Plan: 2 to add, 0 to change, 0 to destroy.

Everything looks okay, I'll run terraform apply to deploy these resources to azure.

$ terraform apply

azurerm_resource_group.development: Creating...
  location: "" => "northeurope"
  name:     "" => "development"
  tags.%:   "" => "<computed>"
azurerm_resource_group.development: Creation complete
azurerm_virtual_network.network: Creating...
  address_space.#:                  "" => "1"
  address_space.0:                  "" => "192.168.0.0/16"
  location:                         "" => "northeurope"
  name:                             "" => "developmentNetwork"
  resource_group_name:              "" => "development"
  subnet.#:                         "" => "3"
  subnet.1472110187.address_prefix: "" => "192.168.1.0/24"
  subnet.1472110187.name:           "" => "subnet1"
  subnet.1472110187.security_group: "" => ""
  subnet.2796830261.address_prefix: "" => "192.168.2.0/24"
  subnet.2796830261.name:           "" => "subnet2"
  subnet.2796830261.security_group: "" => ""
  subnet.4132282879.address_prefix: "" => "192.168.3.0/24"
  subnet.4132282879.name:           "" => "subnet3"
  subnet.4132282879.security_group: "" => ""
  tags.%:                           "" => "<computed>"
azurerm_virtual_network.network: Creation complete

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Conclusion

I decided to make this into a 2-part series. In the next part, I'll create a network with subnets, network interfaces, storage blocks, virtual machines and storage disks.

If you've faced any other challenges or have any other comments, please let me know in the comments.