While working on one of the client requirement I got below assignement which wanted to be demonstrated either with Terraform or ARM. Although I am very good at writing script for both languages, I chose Terraform, as there was another requirement for other projects where I am already working on Terraform. As I was running short on the time, wrote this script in very short time of span but I will revise it later to get more proper input from user when they deploy it.
Updated Version: Azure Terraform fixed Availibility Zones on Virtual Machine Scale Set
My requirement is as below:
Assignment 1.1 OBJECT GOAL This is small project, Deploy a simple web site in a load balanced, highly available and zone-redundant manner using automation and Azure best practices. 1.2 SPECIFICATIONS • Virtual Network with a private subnet and all required dependent infrastructure • Azure Load Balancer with web server instances in the backend pool o Incorporate a simple health probe to make sure the web servers in the backend pool are responding and healthy o The health probe should automatically supersede instances if they are unhealthy, and the • instances should come back into service on their own • Virtual Machine Scale Set that launches Virtual Machines and registers them to the backend pool o Set up a VMSS minimum and desired server count meeting Highly Available and zone o redundant requirement o Establish a VMSS maximum o Establish an Auto scale rule that scales up/down based on a metric of your choice (and be able to exhibit a scaling event) • Network Security Group allowing HTTP traffic to load balancer from anywhere (not directly to the instance(s)) • Network Security Group allowing only HTTP traffic from the load balancer to the instance(s) • Remote management ports such as SSH and RDP must not be open to the world • Some kind of automation or scripting that achieves the following: o Install a web server (your choice – Apache, Nginx, and IIS are common examples) o Deploys a simple “hello world” page for the web server to serve up o May be written in the language of your choice (HTML, PHP, etc) o May be sourced from the location of your choice (Git, cookbook file/ template, etc) o Must include the server’s hostname in the “hello world” presented to the user • All Azure resources must be created using Terraform or Azure Resource Manager (Do not use or download Terraform modules from other users All this should be from scrach) • No resources may be created or managed by hand in the portal, the Azure CLI, or with Powershell
Download the complete terraform script here, or it also available on github.com. Below are the necessary tf files
main.tf
This file uses the Microsoft Azure Provider plugin from hasicorp.
1 2 3 4 |
# Configure the Microsoft Azure Provider provider "azurerm" { features {} } |
variables.tf
In this file I have mentioned what the common configuration information names will be when deploying Azure Resources. Example: Location, Resource Group Name, VMSS name etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
variable "location" { description = "The location where resources will be created" default = "East Us" type = string } variable "tags" { description = "A map of the tags to use for the resources that are deployed" type = map(string) default = { environment = "codelab" } } variable "resource_group_name" { description = "The name of the resource group in which the resources will be created" default = "VMSS" type = string } locals { regions_with_availability_zones = ["eastus"] #["centralus","eastus2","eastus","westus"] zones = contains(local.regions_with_availability_zones, var.location) ? list("1","2","3") : null } /* output "zones" { value = local.zones } */ variable "azurerm_virtual_network" { description = "The name of the virtual network in which the resources will be created" default = "VMSSnet" type = string } variable "azurerm_virtual_machine_scale_set" { description = "The name of the virtual network in which the resources will be created" default = "VMScaleSet" type = string } variable "availability_zone_names" { description = "The name of the virtual network in which the resources will be created" default = ["eastus"] type = list(string) } |
vmss.tf
In this terraform tf configuration file I will deploy vNet (Azure Virtual Network), VMSS (Azure Virtual Machine Scale Set), LB (Azure Machine Load balancer) they are all configured with each other as a part the requirement given by client.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
resource "azurerm_resource_group" "vmss" { name = var.resource_group_name location = var.location tags = var.tags } resource "random_string" "fqdn" { length = 6 special = false upper = false number = false } resource "azurerm_virtual_network" "vmss" { name = var.azurerm_virtual_network address_space = ["10.0.0.0/16"] location = var.location resource_group_name = azurerm_resource_group.vmss.name tags = var.tags } resource "azurerm_subnet" "vmss" { name = "vmss-subnet" resource_group_name = azurerm_resource_group.vmss.name virtual_network_name = azurerm_virtual_network.vmss.name address_prefix = "10.0.2.0/24" } resource "azurerm_public_ip" "vmss" { name = "vmss-public-ip" location = var.location resource_group_name = azurerm_resource_group.vmss.name allocation_method = "Static" domain_name_label = random_string.fqdn.result tags = var.tags } resource "azurerm_lb" "vmss" { name = "vmss-lb" location = var.location resource_group_name = azurerm_resource_group.vmss.name frontend_ip_configuration { name = "PublicIPAddress" public_ip_address_id = azurerm_public_ip.vmss.id } tags = var.tags } resource "azurerm_lb_backend_address_pool" "bpepool" { resource_group_name = azurerm_resource_group.vmss.name loadbalancer_id = azurerm_lb.vmss.id name = "BackEndAddressPool" } resource "azurerm_lb_probe" "vmss" { resource_group_name = azurerm_resource_group.vmss.name loadbalancer_id = azurerm_lb.vmss.id name = "ssh-running-probe" port = var.application_port } resource "azurerm_lb_rule" "lbnatrule" { resource_group_name = azurerm_resource_group.vmss.name loadbalancer_id = azurerm_lb.vmss.id name = "http" protocol = "Tcp" frontend_port = var.application_port backend_port = var.application_port backend_address_pool_id = azurerm_lb_backend_address_pool.bpepool.id frontend_ip_configuration_name = "PublicIPAddress" probe_id = azurerm_lb_probe.vmss.id } resource "azurerm_virtual_machine_scale_set" "vmss" { name = var.azurerm_virtual_machine_scale_set location = var.location resource_group_name = azurerm_resource_group.vmss.name upgrade_policy_mode = "Manual" zones = local.zones sku { name = "Standard_B1S" tier = "Standard" capacity = 2 } storage_profile_image_reference { publisher = "Canonical" offer = "UbuntuServer" sku = "16.04-LTS" version = "latest" } storage_profile_os_disk { name = "" caching = "ReadWrite" create_option = "FromImage" managed_disk_type = "Standard_LRS" } storage_profile_data_disk { lun = 0 caching = "ReadWrite" create_option = "Empty" disk_size_gb = 10 } os_profile { computer_name_prefix = "vmlab" admin_username = var.admin_user admin_password = var.admin_password custom_data = file("web.conf") } os_profile_linux_config { disable_password_authentication = false } network_profile { name = "terraformnetworkprofile" primary = true ip_configuration { name = "IPConfiguration" subnet_id = azurerm_subnet.vmss.id load_balancer_backend_address_pool_ids = [azurerm_lb_backend_address_pool.bpepool.id] primary = true } } tags = var.tags } /* resource "azurerm_virtual_machine_scale_set_extension" "example" { name = "example" virtual_machine_scale_set_id = azurerm_virtual_machine_scale_set.vmss.id publisher = "Microsoft.Azure.Extensions" type = "CustomScript" type_handler_version = "2.0" settings = jsonencode({ "commandToExecute" = "echo Hello World $HOSTNAME " }) } */ resource "azurerm_monitor_autoscale_setting" "vmss" { name = "AutoscaleSetting" resource_group_name = azurerm_resource_group.vmss.name location = azurerm_resource_group.vmss.location target_resource_id = azurerm_virtual_machine_scale_set.vmss.id profile { name = "defaultProfile" capacity { default = 2 minimum = 2 maximum = 10 } rule { metric_trigger { metric_name = "Percentage CPU" metric_resource_id = azurerm_virtual_machine_scale_set.vmss.id time_grain = "PT1M" statistic = "Average" time_window = "PT5M" time_aggregation = "Average" operator = "GreaterThan" threshold = 75 metric_namespace = "microsoft.compute/virtualmachinescalesets" dimensions { name = "AppName" operator = "Equals" values = ["App1"] } } scale_action { direction = "Increase" type = "ChangeCount" value = "1" cooldown = "PT1M" } } rule { metric_trigger { metric_name = "Percentage CPU" metric_resource_id = azurerm_virtual_machine_scale_set.vmss.id time_grain = "PT1M" statistic = "Average" time_window = "PT5M" time_aggregation = "Average" operator = "LessThan" threshold = 25 } scale_action { direction = "Decrease" type = "ChangeCount" value = "1" cooldown = "PT1M" } } } notification { email { send_to_subscription_administrator = true send_to_subscription_co_administrator = true custom_emails = ["admin@contoso.com"] } } } |
web.conf
This is cloud Init configuration once the resources are deployed, This file will deploy the web server nginx, nodejs, npm application on the VMSS instances.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#cloud-config package_upgrade: true packages: - nginx - nodejs - npm write_files: - owner: www-data:www-data path: /etc/nginx/sites-available/default content: | server { listen 80; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } } - owner: azureuser:azureuser path: /home/azureuser/myapp/index.js content: | var express = require('express') var app = express() var os = require('os'); app.get('/', function (req, res) { res.send('Hello World from host ' + os.hostname() + '!') }) app.listen(3000, function () { console.log('Hello world app listening on port 3000!') }) runcmd: - service nginx restart - cd "/home/azureuser/myapp" - npm init - npm install express -y - nodejs index.js |
output.tf
This file has few variables defined, which I am going update with few more information later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
output "vmss_public_ip" { value = azurerm_public_ip.vmss.fqdn } variable "application_port" { description = "The port that you want to expose to the external load balancer" default = 80 } variable "admin_user" { description = "User name to use as the admin account on the VMs that will be part of the VM Scale Set" default = "azureuser" } variable "admin_password" { description = "Default password for admin account" default = "Computer@123" } |
Next to execute the files run terraform init, it downloads required azurerm plugins from HashiCorp. This command is used to initialize a working directory containing Terraform configuration files. This is the first command that should be run after writing a new Terraform configuration or cloning an existing one from version control. It is safe to run this command multiple times.
Run terraform plan to previews the changes that will made to infrastructure. It also Reads the current state of any already-existing remote objects to make sure that the Terraform state is up-to-date.
The terraform apply command executes the actions proposed in a Terraform plan.
Once terraform apply complete you can view the resources on Azure portal. I tested website its working fine.
Useful Articles
PART 1 : MICROSOFT AZURE CREATION AND CONFIGURATION OF VPN TUNNEL SERIES
PART 2 : MICROSOFT AZURE CREATING RESOURCE GROUP
PART 3 : MICROSOFT AZURE CREATING AND ADMINISTERING VIRTUAL NETWORK (VNET)
PART 3.1 : MICROSOFT AZURE POWERSHELL CREATING AND ADMINISTERING VIRTUAL NETWORK (VNET)
PART 4 : MICROSOFT AZURE CREATING AND ADMINISTRATING LOCAL NETWORK GATEWAY VPN
PART 4.1 : MICROSOFT AZURE POWERSHELL CREATING AND ADMINISTRATING LOCAL NETWORK GATEWAY
PART 5: VIRTUAL NETWORK GATEWAY DEPLOYMENT ON MICROSOFT AZURE
PART 5.1: VIRTUAL NETWORK GATEWAY DEPLOYMENT USING MICROSOFT AZURE POWERSHELL
PART 6: INSTALLING ROUTING AND REMOTE ACCESS SERVER ROLE (MICROSOFT RRAS)
PART 6.1: CONFIGURING ROUTING AND REMOTE ACCESS SERVER DEMAND-DIAL (MICROSOFT RRAS AZURE VPN)
PART 6.2: CONFIGURING ROUTING AND REMOTE ACCESS SERVER ROUTER (MICROSOFT RRAS AZURE VPN)
References : https://docs.microsoft.com/en-us/azure/developer/terraform/create-vm-scaleset-network-disks-hcl and https://docs.microsoft.com/en-us/azure/virtual-machines/linux/tutorial-automate-vm-deployment