Austin Barnes
February 28, 2022

Terraform - Azure Server Deploy

Posted on February 28, 2022  •  12 minutes  • 2360 words

Overview

Deploy and Configure 4 Windows 2022 Datacenter Servers in Azure.

Check out all of the configuration files on GitHub (Azure-Serv-Deploy) at the repository!

Terraform

Main role: Deploy the Virtual Machines, setup Network environment, and provide intial parameters for both Windows and Linux environments running in the cloud


Prerequisites

Terraform process

Terraform File Structure

Create a new directory, and place the following files in it, with your own variables

provider.tf File

provider.tf

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">=2.91.0"
    }
  }
}
provider "azurerm" {
  features {}
}

networking.tf File

networking.tf

# Create a resource group to maintain security settings along with network interfaces for VMs
resource "azurerm_resource_group" "east" {
  name     = "terra-resources"
  location = "East US"
}
# ASSIGN ADDRESS SPACE TO RESOURCE GROUP
resource "azurerm_virtual_network" "east" {
  name                = "east-network"
  address_space       = ["${var.east_address_spaces}"]
  location            = azurerm_resource_group.east.location
  resource_group_name = azurerm_resource_group.east.name
}
# ASSIGN SUBNET TO NETWORK ADDRESS SPACE
resource "azurerm_subnet" "subnet1" {
  name                 = "internal"
  resource_group_name  = azurerm_resource_group.east.name
  virtual_network_name = azurerm_virtual_network.east.name
  address_prefixes     = [var.east_subnets]
}
# Create public IP variable for Linux machine
resource "azurerm_public_ip" "linux_public" {
  name                = "PublicIp1"
  resource_group_name = azurerm_resource_group.east.name
  location            = azurerm_resource_group.east.location
  allocation_method   = "Static"

}
# Create public IP variable for Windows machine
resource "azurerm_public_ip" "win_public" {
  name                = "PublicIp2"
  resource_group_name = azurerm_resource_group.east.name
  location            = azurerm_resource_group.east.location
  allocation_method   = "Static"

}
# ASSIGN NETWORK INTERFACE PER VM WE WILL BE USING
resource "azurerm_network_interface" "linux1" {
  name                = "linux1-nic"
  location            = azurerm_resource_group.east.location
  resource_group_name = azurerm_resource_group.east.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet1.id
    private_ip_address_allocation = "Static"
    private_ip_address            = var.linux1_priavte_ip    
    public_ip_address_id          = azurerm_public_ip.linux_public.id
  }
}
resource "azurerm_network_interface" "winserv1" {
  name                = "winserv1-nic"
  location            = azurerm_resource_group.east.location
  resource_group_name = azurerm_resource_group.east.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet1.id
    private_ip_address_allocation = "Static"
    private_ip_address            = var.winserv1_private_ip
    public_ip_address_id          = azurerm_public_ip.win_public.id
  }
}
resource "azurerm_network_interface" "winserv2" {
  name                = "winserv2-nic"
  location            = azurerm_resource_group.east.location
  resource_group_name = azurerm_resource_group.east.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet1.id
    private_ip_address_allocation = "Static"
    private_ip_address            = var.winserv2_private_ip
  }
}
resource "azurerm_network_interface" "winserv3" {
  name                = "winserv3-nic"
  location            = azurerm_resource_group.east.location
  resource_group_name = azurerm_resource_group.east.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet1.id
    private_ip_address_allocation = "Static"
    private_ip_address            = var.winserv3_private_ip
  }
}
resource "azurerm_network_interface" "winserv4" {
  name                = "winserv4-nic"
  location            = azurerm_resource_group.east.location
  resource_group_name = azurerm_resource_group.east.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet1.id
    private_ip_address_allocation = "Static"
    private_ip_address            = var.winserv4_private_ip
  }
}
# CREATE SECURITY GROUPs TO ALLOW SSH/RDP/ANSIBLE FOR VMs
resource "azurerm_network_security_group" "linux1" {
  name                = "Allow-SSH"
  location            = azurerm_resource_group.east.location
  resource_group_name = azurerm_resource_group.east.name

  security_rule {
    name                       = "SSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}
resource "azurerm_network_security_group" "winserv" {
  name                = "Allow-RDP-SSH-ANS"
  location            = azurerm_resource_group.east.location
  resource_group_name = azurerm_resource_group.east.name
  security_rule {
    name                       = "RDP"
    priority                   = 101
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "3389"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
  security_rule {
    name                       = "SSH"
    priority                   = 102
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
   security_rule {
    name                       = "ANSIBLE"
    priority                   = 103
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "5985"
    source_address_prefix      = "${var.east_subnets}"
    destination_address_prefix = "*"
  }
}
# ASSIGN SECURITY GROUPS TO INTERFACES
# LINUX SSH
resource "azurerm_network_interface_security_group_association" "linux1" {
  network_interface_id      = azurerm_network_interface.linux1.id
  network_security_group_id = azurerm_network_security_group.linux1.id
}
# WINSERV RDP
resource "azurerm_network_interface_security_group_association" "winserv1" {
  network_interface_id      = azurerm_network_interface.winserv1.id
  network_security_group_id = azurerm_network_security_group.winserv.id
}
resource "azurerm_network_interface_security_group_association" "winserv2" {
  network_interface_id      = azurerm_network_interface.winserv2.id
  network_security_group_id = azurerm_network_security_group.winserv.id
}
resource "azurerm_network_interface_security_group_association" "winserv3" {
  network_interface_id      = azurerm_network_interface.winserv3.id
  network_security_group_id = azurerm_network_security_group.winserv.id
}
resource "azurerm_network_interface_security_group_association" "winserv4" {
  network_interface_id      = azurerm_network_interface.winserv4.id
  network_security_group_id = azurerm_network_security_group.winserv.id
}

variables.tf, terraform.tfvars Files

variables.tf

variable "winserv_vm_os_publisher" {}
variable "winserv_vm_os_offer" {}
variable "winserv_vm_os_sku" {}
variable "winserv_vm_size" {}
variable "winadmin_username" {}
variable "winadmin_password" {}
variable "winserv_license" {}
variable "winserv_sa_type" {}
variable "winserv_pdc" {}
variable "winserv_rdc" {} 
variable "winserv_dhcp" {} 
variable "winserv_file" {}    
variable "linux_server" {}
variable "linux_vm_os_publisher" {}
variable "linux_vm_os_offer" {}
variable "linux_vm_os_sku" {}
variable "linux_vm_size" {}
variable "linux_ssh_key" {}
variable "linux_sa_type" {}
variable "linux_ssh_key_pv" {}
variable "winserv_allocation_method" {}
variable "east_address_spaces" {}
variable "east_subnets" {}
variable "winserv_public_ip_sku" {}
variable "winserv1_private_ip" {}
variable "winserv2_private_ip" {}
variable "winserv3_private_ip" {}
variable "winserv4_private_ip" {}
variable "linux1_priavte_ip" {}

locals{
    first_logon_commands        = file("${path.module}/          winfiles/FirstLogonCommands.xml")
    auto_logon                  =           "<AutoLogon><Password><Value>${var.winadmin_password}</          Value></Password><Enabled>true</Enabled><LogonCount>1</          LogonCount><Username>${var.winadmin_username}</          Username></AutoLogon>"
}

terraform.tfvars

# Azure Windows Server related params
winserv_vm_os_publisher     ="MicrosoftWindowsServer"
winserv_vm_os_offer         = "WindowsServer"
winserv_vm_os_sku           = "2022-Datacenter"
winserv_vm_size             = "Standard_DS1_V2"
winserv_license             = "Windows_Server"
winserv_allocation_method   = "Static"
winserv_public_ip_sku       = "Standard"
winserv_sa_type             = "Standard_LRS"

# Azure Linux Server related params
linux_vm_os_publisher = "Canonical"
linux_vm_os_offer     = "UbuntuServer"
linux_vm_os_sku       = "18.04-LTS"
linux_vm_size         = "Standard_B1s"
linux_ssh_key         ="Local-PUBLIC-SSH-KEY-Here"
linux_sa_type         = "Premium_LRS"
linux_ssh_key_pv      = "Local-PRIV-SSH-KEY-Here"

# Which Windows administrator password to setduring vm           customization
winadmin_username = "SuperAdministrator"
winadmin_password = "Password1234"

# Naming Schemes 
winserv_pdc    = "ajb-pdc"
winserv_rdc    = "ajb-rdc"
winserv_dhcp   = "ajb-dhcp"
winserv_file   = "ajb-file"
linux_server   = "ajb-operator"

# Networking Variables
winserv1_private_ip   = "10.0.1.10"
winserv2_private_ip   = "10.0.1.11"
winserv3_private_ip   = "10.0.1.12"
winserv4_private_ip   = "10.0.1.13"
linux1_priavte_ip     = "10.0.1.9"
east_address_spaces  = "10.0.0.0/16"
east_subnets         = "10.0.1.0/24"

01-LinuxClient.tf & 02-WinServers.tf Files

01-LinuxClient.tf

# This pulls a Ubuntu Datacenter from Microsoft's VM platform directly
resource "azurerm_linux_virtual_machine" "operator" {
  name                = var.linux_server
  resource_group_name = azurerm_resource_group.east.name
  location            = azurerm_resource_group.east.location
  size                = var.linux_vm_size
  admin_username      = var.winadmin_username
  network_interface_ids = [
    azurerm_network_interface.linux1.id
  ]

  admin_ssh_key {
    username   = var.winadmin_username
    public_key = file("${var.linux_ssh_key}")
  }

  # Cloud-Init passed here
  custom_data = data.template_cloudinit_config.config.rendered

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = var.linux_sa_type
  }

  source_image_reference {
    publisher = var.linux_vm_os_publisher
    offer     = var.linux_vm_os_offer
    sku       = var.linux_vm_os_sku
    version   = "latest"
  }

  depends_on = [azurerm_resource_group.east, azurerm_network_interface.linux1]
}

# Create cloud-init file to be passed into linux vm
data "template_file" "user_data" {
  template = file("./cloudinit/custom.yml")
}
 
# Render a multi-part cloud-init config making use of the part
# above, and other source files
data "template_cloudinit_config" "config" {
  gzip          = true
  base64_encode = true

  # Main cloud-config configuration file.
  part {
    filename     = "init.cfg"
    content_type = "text/cloud-config"
    content      = "${data.template_file.user_data.rendered}"
  }
}

  # Pass Ansible File into created Linux VM using SCP (SSH Port 22)
resource "null_resource" "copyansible"{ 
  connection {
    type        = "ssh"
    host        = azurerm_public_ip.linux_public.ip_address
    user        = var.winadmin_username
    private_key = file("${var.linux_ssh_key_pv}")
  }
  
  provisioner "file" {
    source = "${path.module}/Ansible"
    destination = "/tmp/" 
  }
  depends_on = [azurerm_linux_virtual_machine.operator]
}

02-WinServers.tf

# This pulls the latest Windows Server Datacenter from Microsoft's VM platform directly
resource "azurerm_windows_virtual_machine" "pdc" {
  name                = var.winserv_pdc
  resource_group_name = azurerm_resource_group.east.name
  location            = azurerm_resource_group.east.location
  size                = var.winserv_vm_size
  admin_username      = var.winadmin_username
  admin_password      = var.winadmin_password
  network_interface_ids = [
    azurerm_network_interface.winserv1.id
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = var.winserv_sa_type
  }

  source_image_reference {
    publisher = var.winserv_vm_os_publisher
    offer     = var.winserv_vm_os_offer
    sku       = var.winserv_vm_os_sku
    version   = "latest"
  }

  additional_unattend_content {
    content = local.auto_logon
    setting = "AutoLogon"
  }

  additional_unattend_content {
    content = local.first_logon_commands
    setting = "FirstLogonCommands"
  }

  depends_on = [azurerm_resource_group.east, azurerm_network_interface.winserv1]
}
resource "azurerm_windows_virtual_machine" "rdc" {
  name                = var.winserv_rdc
  resource_group_name = azurerm_resource_group.east.name
  location            = azurerm_resource_group.east.location
  size                = var.winserv_vm_size
  admin_username      = var.winadmin_username
  admin_password      = var.winadmin_password
  network_interface_ids = [
    azurerm_network_interface.winserv2.id
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = var.winserv_sa_type
  }

  source_image_reference {
    publisher = var.winserv_vm_os_publisher
    offer     = var.winserv_vm_os_offer
    sku       = var.winserv_vm_os_sku
    version   = "latest"
  }

  additional_unattend_content {
    content = local.auto_logon
    setting = "AutoLogon"
  }

  additional_unattend_content {
    content = local.first_logon_commands
    setting = "FirstLogonCommands"
  }

  depends_on = [azurerm_resource_group.east, azurerm_network_interface.winserv2]
}
resource "azurerm_windows_virtual_machine" "dhcp" {
  name                = var.winserv_dhcp
  resource_group_name = azurerm_resource_group.east.name
  location            = azurerm_resource_group.east.location
  size                = var.winserv_vm_size
  admin_username      = var.winadmin_username
  admin_password      = var.winadmin_password
  network_interface_ids = [
    azurerm_network_interface.winserv3.id
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = var.winserv_sa_type
  }

  source_image_reference {
    publisher = var.winserv_vm_os_publisher
    offer     = var.winserv_vm_os_offer
    sku       = var.winserv_vm_os_sku
    version   = "latest"
  }

  additional_unattend_content {
    content = local.auto_logon
    setting = "AutoLogon"
  }

  additional_unattend_content {
    content = local.first_logon_commands
    setting = "FirstLogonCommands"
  }

  depends_on = [azurerm_resource_group.east, azurerm_network_interface.winserv3]
}
resource "azurerm_windows_virtual_machine" "file" {
  name                = var.winserv_file
  resource_group_name = azurerm_resource_group.east.name
  location            = azurerm_resource_group.east.location
  size                = var.winserv_vm_size
  admin_username      = var.winadmin_username
  admin_password      = var.winadmin_password
  network_interface_ids = [
    azurerm_network_interface.winserv4.id
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = var.winserv_sa_type
  }

  source_image_reference {
    publisher = var.winserv_vm_os_publisher
    offer     = var.winserv_vm_os_offer
    sku       = var.winserv_vm_os_sku
    version   = "latest"
  }

  additional_unattend_content {
    content = local.auto_logon
    setting = "AutoLogon"
  }

  additional_unattend_content {
    content = local.first_logon_commands
    setting = "FirstLogonCommands"
  }

  depends_on = [azurerm_resource_group.east, azurerm_network_interface.winserv4]
}

outputs.tf File

outputs.tf

output "Public_IP_Linux" {
    value = azurerm_public_ip.linux_public.ip_address
}
output "Public_IP_Windows" {
    value = azurerm_public_ip.win_public.ip_address
}
output "Private_IP_Linux" {
    value = azurerm_network_interface.linux1.private_ip_address
}

output "Private_IP_WinServ" {
    value = [
        "PDC: ${azurerm_windows_virtual_machine.pdc.private_ip_address}",
        "RDC: ${azurerm_windows_virtual_machine.rdc.private_ip_address}",
        "DHCP: ${azurerm_windows_virtual_machine.dhcp.private_ip_address}",
        "FILE: ${azurerm_windows_virtual_machine.file.private_ip_address}"
        ]
}

Cloud-init

Notice that in the 01-linux file, there is a cloud-init section, where the spun up machine will pull a cloud-init file. We must create that. Make a a folder inside your root terraform directory. Name it cloudinit. Inside that folder, create a file called custom.yml containing the following.

#cloud-config
apt_update: true
packages:
  - python-pip
runcmd:
  - sudo pip install ansible
  - sudo ansible-galaxy install azure.azure_preview_modules
  - sudo pip install -r ~/.ansible/roles/azure.azure_preview_modules/files/requirements-azure.txt
  - pip install "pywinrm>=0.2.2"
  - cd /tmp/Ansible
  - sudo ansible-playbook winlab.yml

Finding variable information for VM Images variables:

   az vm image list \
   --location westus \
   --publisher Canonical \  
   --offer UbuntuServer \    
   --sku 18.04-LTS \
   --all --output table

Ansible

Main role: Configure the deployed Virtual Machines.

Check out the repository on GitHub for the configuration files!

Ansible Variable files

Running Ansible

This is taken care of with terraform cloud-init file along with the file provisioner. The alternative would be below.

Useful Resources

Terraform Resources

Ansible Resources

Follow me

I put a little bit of everything on here, so follow along in my journey if you wish