A Multi-Tier Azure Environment with Terraform including Active Directory – PART 1

Infrastructure as Code (IAC) is common in DevOps cultures and gives us the ability to manage configurations and automatically provision infrastructure along with deployments. IAC is an approach of defining infrastructure and network components through descriptive or high-level code; i.e., programmable infrastructure. Various tools such as Vagrant, Ansible, Docker, Chef, Terraform, Puppet, and more, independently or when combined, make life easy by automating infrastructure provisioning and deployment automation.

Terraform is one such tool. In this guide, we will use Terraform to provision a Two-Tier infrastructure on Azure including Active Directory. We will create all the components from scratch including the resource group, VNet, subnets, NIC, security groups, VMs, etc.

Terraform must be installed in your system. Find the instructions to install Terraform in the article: Install and configure Terraform

For Terraform to provision resources in Azure, it must be able to authenticate. In this example we will need to create a Client ID and Client Secret in Azure AD that can be used as credentials in Terraform to allow it to provision resources in Azure.

Using Terraform, we will provision the following things:

  • A resource group with one virtual network in it

 

  • wafsubnet – To contain a WAF of your choosing (Not created as part of this example)

 

  • rpsubnet – To contain a Reverse Proxy of your choosing (Not created as part of this example)

 

  • issubnet – To contain IIS VM(s) (Created as part of this example)

 

  • dbsubnet – To contain MS SQL VM(s) (Created as part of this example)

 

  • dcsubnet – To contain pair of Domain Controllers

 

  • DC1 – Primary Domain Controller holding FSMO roles, Static Public & Private IP Addresses

 

  • DC2 – Secondary Domain Controller joined to domain, Static Public & Private IP Addresses

 

  • IIS VM(s) – Scalable using the count function, IIS & Management Tools installed, Windows Server 2012 R2, Added to Availability Set, Static Public & Private IP Addresses Joined to Domain

 

  • SQL VM(s) – Scalable using the count function, SQL 2014 SP2 & SSMS Installed, Windows Server 2012 R2, Windows Failover Clustering Service Installed, Added to Availability Set , Static Public & Private IP Addresses, Joined to Domain

 

  • Azure internal load balancer (ILB) and adds the SQL VM(s) to the backend pool so can be expanded to use AlwaysOn Capability

This example also includes an environments folder containing a .tfvars file, to allow this Infrastructure to be deployed throughout a pipeline i.e. Dev,QA,UAT etc

At first, we will create variables file. In the VARIABLES.TF file, we will configure Azure Provider as well as declare all the variables that we will use in all our Terraform configurations.

# Provider info
variable subscription_id {}

variable client_id {}
variable client_secret {}
variable tenant_id {}

# Generic info
variable location {}

variable resource_group_name {}
variable environment_name {}

# Network
variable address_space {}

variable dns_servers {
type = “list”
}

variable wafsubnet_name {}
variable wafsubnet_prefix {}
variable rpsubnet_name {}
variable rpsubnet_prefix {}
variable issubnet_name {}
variable issubnet_prefix {}
variable dbsubnet_name {}
variable dbsubnet_prefix {}
variable dcsubnet_name {}
variable dcsubnet_prefix {}

# Active Directory & Domain Controller
variable prefix {}
variable private_ip_address {}
variable admin_username {}
variable admin_password {}

# IIS Servers
variable vmcount {}

# Domain Controller 2
variable “dc2private_ip_address” {}
variable “domainadmin_username” {}

# SQL LB
variable “lbprivate_ip_address” {}
# SQL DB Servers
variable sqlvmcount {}

In the VARIABLES.TF file, we haven’t specified any default values for the variables we created. We will assign values to variables by declaring them in another file: ENVIRONMENTNAME.TFVARS.

 

# Provider info
subscription_id = “XXXXXXXXXXXXXXX”

client_id = “XXXXXXXXXXXXXXX”

client_secret = “XXXXXXXXXXXXXXX”

tenant_id = “XXXXXXXXXXXXXXX”

# Generic info
location = “West Europe”

resource_group_name = “productname”

environment_name = “devci1”

# Network
address_space = “10.100.0.0/16”

dns_servers = [“10.100.1.4”, “10.100.1.5”]

dcsubnet_name = “sndc”

dcsubnet_prefix = “10.100.1.0/24”

wafsubnet_name = “snwf”

wafsubnet_prefix = “10.100.10.0/24”

rpsubnet_name = “snrp”

rpsubnet_prefix = “10.100.20.0/24”

issubnet_name = “snis”

issubnet_prefix = “10.100.30.0/24”

dbsubnet_name = “sndb”

dbsubnet_prefix = “10.100.50.0/24”

# Active Directory & Domain Controller 1

prefix = “devad”

private_ip_address = “10.100.1.4”

dc2private_ip_address = “10.100.1.5”

admin_username = “AdminTest”

admin_password = “Password123”

# IIS Servers

vmcount = “1”

# Domain Controller 2

domainadmin_username = “‘AdminTest@devad.local'”

# SQL LB

lbprivate_ip_address = “10.100.50.20”

# SQL DB Servers

sqlvmcount = “1”

This example makes use of 5 modules:

  • modules/active-directory
    This module creates an Active Directory Forest on a single Virtual Machine

 

  • modules/network
    This module creates the Network with 4 subnets.
    In a Production environment there would be Network Security Rules in effect which limited which ports can be used between these Subnets, however for the purposes of keeping this demonstration simple, these have been omitted.

 

  • modules/dc2-vm
    This module creates a secondary domain controller machine for resiliency that is bound to the Active Directory Domain created in the active-directory module above.

 

  • modules/iis-vm
    This module creates IIS VM’s – Choose how many you want using count

 

  • modules/sql-vm This module creates SQL VM’s – Choose how many you want using count. Also creates the ILB so you could scale out to use AlwaysOn

 

The modules are all called from a MAIN.TF file:

# 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 Resource group Network & subnets
##########################################################
module “network” {
source = “..\\modules\\network”
address_space = “${var.address_space}”
dns_servers = [“${var.dns_servers}”]
environment_name = “${var.environment_name}”
resource_group_name = “${var.resource_group_name}”
location = “${var.location}”
dcsubnet_name = “${var.dcsubnet_name}”
dcsubnet_prefix = “${var.dcsubnet_prefix}”
wafsubnet_name = “${var.wafsubnet_name}”
wafsubnet_prefix = “${var.wafsubnet_prefix}”
rpsubnet_name = “${var.rpsubnet_name}”
rpsubnet_prefix = “${var.rpsubnet_prefix}”
issubnet_name = “${var.issubnet_name}”
issubnet_prefix = “${var.issubnet_prefix}”
dbsubnet_name = “${var.dbsubnet_name}”
dbsubnet_prefix = “${var.dbsubnet_prefix}”
}

When you start from scratch, first and foremost you will need a resource group. So in this module, we will create a resource group, the virtual network, and the subnets.

resource “azurerm_resource_group” “network” {
name = “${var.resource_group_name}-${var.environment_name}”
location = “${var.location}”
}

resource “azurerm_virtual_network” “main” {
name = “${var.resource_group_name}-${var.environment_name}-net”
address_space = [“${var.address_space}”]
location = “${var.location}”
resource_group_name = “${var.resource_group_name}-${var.environment_name}”
dns_servers = [“${var.dns_servers}”]

depends_on = [“azurerm_resource_group.network”]
}

resource “azurerm_subnet” “dc-subnet” {
name = “${var.resource_group_name}-${var.dcsubnet_name}-${var.environment_name}”
resource_group_name = “${var.resource_group_name}-${var.environment_name}”
virtual_network_name = “${azurerm_virtual_network.main.name}”
address_prefix = “${var.dcsubnet_prefix}”
}

resource “azurerm_subnet” “waf-subnet” {
name = “${var.resource_group_name}-${var.wafsubnet_name}-${var.environment_name}”
resource_group_name = “${var.resource_group_name}-${var.environment_name}”
virtual_network_name = “${azurerm_virtual_network.main.name}”
address_prefix = “${var.wafsubnet_prefix}”
}

resource “azurerm_subnet” “rp-subnet” {
name = “${var.resource_group_name}-${var.rpsubnet_name}-${var.environment_name}”
resource_group_name = “${var.resource_group_name}-${var.environment_name}”
virtual_network_name = “${azurerm_virtual_network.main.name}”
address_prefix = “${var.rpsubnet_prefix}”
}

resource “azurerm_subnet” “is-subnet” {
name = “${var.resource_group_name}-${var.issubnet_name}-${var.environment_name}”
resource_group_name = “${var.resource_group_name}-${var.environment_name}”
virtual_network_name = “${azurerm_virtual_network.main.name}”
address_prefix = “${var.issubnet_prefix}”
}

resource “azurerm_subnet” “db-subnet” {
name = “${var.resource_group_name}-${var.dbsubnet_name}-${var.environment_name}”
resource_group_name = “${var.resource_group_name}-${var.environment_name}”
virtual_network_name = “${azurerm_virtual_network.main.name}”
address_prefix = “${var.dbsubnet_prefix}”
}

This is the end of PART 1, by now you should have Terraform configured and building a resource group containing a Network with 5 subnets in Azure.

Join me again soon for PART 2 where we will adding a VM which will be our primary Domain Controller.

P.S. If you cant wait and just want to jump to the complete example, you can find it on GitHub where it has also been contributed to the Hashicorp Offical Repo