DevOps Rich DevOps content for Infrastructure Engineers

Terraform Configuration

Input Variables

variable "cost_centre" {
  description = "Cost Centre Number"
  type = string 
  default = "0001"
}
name = "${var.business_unit}-${var.environment}-${var.resource_group_name}"
location = var.resource_group_location
name = "${azurerm_virtual_network.myvnet.name}-${var.subnet_name}"
terraform plan -var="resource_group_name=hex-rg-01"
terraform plan -var="resource_group_name=hex-rg-01" -out v1.plan
terraform show v1.plan
terraform apply v1.plan

Variable Definition Precedence

Environment Variables

$env:TF_VAR_resoure_group_location='uksouth'

tfvars Files

cost_centre = "1234"
environment = "prod"
terraform plan -var-file="dev.tfvars"

Note: If you run the same code against multiple .tfvars in the same working directory, the state file uses the same local names for a resource so would attempt to replace the resources. This is what terraform workspaces is for to create separate state files.

Complex Variable Types

List

variable "virtual_network_address_space" {
  description = "Virtual Network Address Space"
  type        = list(string)
  default     = ["10.0.0.0/16", "10.1.0.0/16", "10.2.0.0/16"]
}
virtual_network_address_space = ["10.3.0.0/16", "10.4.0.0/16", "10.5.0.0/16"]
address_space = [var.virtual_network_address_space[0]]

Maps

variable "default_tags" {
  description = "Default Tags for Azure Resources"
  type = map(string)
  default = {
    "deploymentType" = "Terraform",
    "costCentre" = "0001"
  }
}
default_tags = {
    "deploymentType" = "Terraform",
    "costCentre" = "1234"
}

Note: If a key value starts with a number you must use : instead of = when setting a value.

Lookup Function

variable "public_ip_sku" {
  description = "Azure Public IP Address SKU"
  type = map(string)
  default = {
    "ukwest" = "Basic",
    "uksouth" = "Standard"
  }
}
sku = lookup(var.public_ip_sku, var.resoure_group_location, "Basic")

Validation Rules in Variables

Examples of Function Use

Length ("hi")
2
Length(["a","b"])
2
Length ({"key" = "value"})
1
Substr("hello world", 1, 4)
ello
Contains (["a","b"], "a")
true
Lower("ABC")
abc
regex("india$", "westindia")
india
Can(regex("india$", "westindia"))
true

Validation Rule Examples

variable "resoure_group_location" {
  description = "Resource Group Location"
  type = string
  default = "uksouth"
  validation {
    condition = contains(["uksouth", "ukwest"], var.resoure_group_location)
    error_message = "We only allow Resources to be created in uksouth or ukwest locations."
  }

Sensitive Input Variables

variable "localadmin_password" {
  description = "Local Administrator Password"
  type = string  
  sensitive = true
}
terraform plan -var-file="secrets.tfvars"

Note: Environment variable values will appear in command line history.

Structural Variable Types

Object

variable "mysql_policy" {
  description = "Azure MySQL DB Threat Detection Policy"
  type = object({
    enabled = bool,
    retention_days = number
    email_account_admins = bool
    email_addresses = list(string)
  })
}

mysql_policy = {
    enabled = true,
    retention_days = 10,
    email_account_admins = true,
    email_addresses = [ "email1@gmail.com", "email2@gmail.com" ]
  }

threat_detection_policy {
    enabled = var.mysql_policy.enabled
    retention_days = var.mysql_policy.retention_days
    email_account_admins = var.mysql_policy.email_account_admins
    email_addresses = var.mysql_policy.email_addresses
  } 

Tuple

variable "mysql_policy" {
  description = "Azure MySQL DB Threat Detection Policy"
  type = tuple([ bool, number, bool, list(string) ])
}
mysql_policy = [true, 10, true, [ "email1@gmail.com", "email2@gmail.com" ]]
threat_detection_policy {
    enabled = var.mysql_policy[0]
    retention_days = var.mysql_policy[1]
    email_account_admins = var.mysql_policy[2]
    email_addresses = var.mysql_policy[3]
  }  
}

Sets

Output Variables

output "resource_group_id" {
  description = "Resource Group ID"
  # Attribute Reference
  value = azurerm_resource_group.rg.id 
}

Count & Splat Expression

resource "azurerm_virtual_network" "vnet" {
  count = 4
  name                = "vnet-${count.index}"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.myrg.location
  resource_group_name = azurerm_resource_group.myrg.name
}
output "virtual_network_name" {
  description = "Virtual Network Name"
  value = azurerm_virtual_network.vnet[*].name 
}

Note: count.index cannot not be used in an output block.

terraform output
virtual_network_name = [
  "it-dev-vnet-0",
  "it-dev-vnet-1",
  "it-dev-vnet-2",
  "it-dev-vnet-3",
]

for_each

variable "environment" {
  description = "Environment Name"
  type = set(string)
  default = ["dev1", "qa1", "staging1", "prod1" ]
}
resource "azurerm_virtual_network" "vnet" {
  for_each = var.environment
  name                = "vnet-${each.key}"
  address_space       = ["10.0.0.0/16"]
  location            = "UKSouth"
  resource_group_name = "rg1"
}
output "virtual_network_name_list_one_input" {
  description = "Virtual Network - Loop through resource block and return a list of vnet names"
  value = [for vnet in azurerm_virtual_network.vnet: vnet.name ]  
}

terraform output virtual_network_name_list_one_input
[
  "vnet-dev1",
  "vnet-prod1",
  "vnet-qa1",
  "vnet-staging1",
]

output "virtual_network_name_list_two_inputs" {
  description = "Virtual Network - Loop through the iterator used by the resource block (var.environment) and return a list of the iterator"  
  value = [for env, vnet in azurerm_virtual_network.vnet: env ]
}

terraform output virtual_network_name_list_two_inputs
[
  "dev1",
  "prod1",
  "qa1",
  "staging1",
]

output "virtual_network_name_map_one_input" {
  description = "Virtual Network - Loop through resource block and return a map of vnet id = vnet name"
  value = {for vnet in azurerm_virtual_network.vnet: vnet.id => vnet.name }
}

terraform output virtual_network_name_map_one_input  
{
  "/subscriptions/a529f686-82de-4a8d-b643-747ed505372a/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet-dev1" = "vnet-dev1" 
  "/subscriptions/a529f686-82de-4a8d-b643-747ed505372a/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet-prod1" = "vnet-prod1"
  "/subscriptions/a529f686-82de-4a8d-b643-747ed505372a/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet-qa1" = "vnet-qa1"   
  "/subscriptions/a529f686-82de-4a8d-b643-747ed505372a/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet-staging1" = "vnet-staging1"
}

output "virtual_network_name_map_two_inputs" {
  description = "Virtual Network - Loop through the iterator used by the resource block (var.environment) and use to loop through the resource block and map iterator = vnet name"
  value = {for env, vnet in azurerm_virtual_network.vnet: env => vnet.name }
}

terraform output virtual_network_name_map_two_inputs 
{
  "dev1" = "vnet-dev1"
  "prod1" = "vnet-prod1"
  "qa1" = "vnet-qa1"
  "staging1" = "vnet-staging1"
}

Locals

locals {
  # Default tags to be assigned to all resources
  businessService = "Reporting"
  owner = "IT"
  default_tags = {
    Service = local.businessService
    Owner = local.owner
  }
  tags = local.default_tags

Conditional Expression

variable "environment" {
  description = "Environment Name"
  type = string
  default = "qa"
}

variable "vnet_address_space_dev" {
  description = "Virtual Network Address Space for Dev Environment"
  type = list(string)
  default = [ "10.0.0.0/16" ]
}

variable "vnet_address_space_all" {
  description = "Virtual Network Address Space for All Environments except dev"
  type = list(string)
  default = [ "10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16"  ]
}
locals {
vnet_address_space = (var.environment == "dev" ? var.vnet_address_space_dev : var.vnet_address_space_all)
}
count = var.environment == "dev" ? 1 : 5

Data Sources

Note: Lifecycle meta-arguments are not supported.

data "azurerm_subscription" "current" {
}
output "current_subscription_display_name" {
  value = data.azurerm_subscription.current.display_name
}

Note: display name is an attribute reference.

CLI Workspaces

Note: They are not related to terraform cloud workspaces.

Provisioners

Failure behaviour

"resources": [
    {
      "mode": "managed",
      "type": "azurerm_linux_virtual_machine",
      "name": "mylinuxvm",
      "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]",
      "instances": [
        {
          "status": "tainted"
provisioner "file" {
    source      = "files/index.html"
    destination = "/var/www/html/index.html"
    on_failure  = continue
   } 

File Provisioner

connection {
    type = "ssh"
    host = self.public_ip_address
    user = self.admin_username
    private_key = file("${path.module}/ssh-keys/key.pem")
  }  
  provisioner "file" {
    source = "files/index.html"
    destination = "/tmp/index.html"
  }

Note: You have to use a self object otherwise you would create an implicit dependency with itself. e.g. the azurerm_linux_virtual_machine resource would have to be created to use the user argument within the connection block.

  provisioner "file" {
    content = "VM Host name: ${self.computer_name}"
    destination = "/vm_folder/file.log"
  }
  provisioner "file" {
    source = "folder/subfolder"
    destination = "/vm_folder"
  }
  provisioner "file" {
    source = "folder/subfolder/"
    destination = "/vm_folder"
  }

Remote-exec

Local-exec

  provisioner "local-exec" {
    when    = destroy
    command = "echo Destroy-time provisioner Instance Destroyed at `date` >> destroy-time.txt"
    working_dir = "terraformhostfolder/"
  }

Null Resource

null = {
      source = "hashicorp/null"
      version = ">= 3.1.0"
    }  
time = {
      source = "hashicorp/time"
      version = ">= 0.7.2"
    }  
resource "time_sleep" "wait_90_seconds" {
  depends_on = [azurerm_linux_virtual_machine.mylinuxvm]
  create_duration = "90s"
}
resource "null_resource" "sync_app1_static" {
  depends_on = [time_sleep.wait_90_seconds]
  triggers = {
    always-update = timestamp() 
  }

  connection {
    type = "ssh"
    host = azurerm_linux_virtual_machine.mylinuxvm.public_ip_address
    user = azurerm_linux_virtual_machine.mylinuxvm.admin_username
    private_key = file("${path.module}/ssh-keys/key.pem")
  }

  provisioner "file" {
    source = "folder/subfolder"
    destination = "/tmp"
  }

  provisioner "remote-exec" {
    inline = [
      "sudo cp -r /tmp/subfolder /var/www/html"
    ]    
  }
}
triggers = {
    always-update = timestamp() 
  }

Dynamic Blocks


locals {
  ports = [22, 80, 8080, 8081, 7080, 7081]
}

resource "azurerm_network_security_group" "nsg" {
  name                = "nsg"
  location            = azurerm_resource_group.myrg.location
  resource_group_name = azurerm_resource_group.myrg.name
  dynamic "security_rule" {
    for_each = local.ports 
    content {
      name                       = "inbound-rule-${security_rule.key}"
      description                = "Inbound Rule ${security_rule.key}"    
      priority                   = sum([100, security_rule.key])
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = security_rule.value
      source_address_prefix      = "*"
      destination_address_prefix = "*"      
    }
  }
}

Note: security_rule.key is the same as security_rule.value because local.ports is a list.

Override Files

Note: If you need to use override.tf files make sure you modify .gitignore as it will ignore this type of file my default.

External Providers & Data Sources

data "external" "ssh_key_generator" {
  program = ["bash", "${path.module}/shell-scripts/ssh_key_generator.sh"]
  
  query = {
    key_name = "terraformdemo"
    key_environment = "dev"
  }
}
output "public_key" {
  description = "public_key"
  value = data.external.ssh_key_generator.result.public_key
}

Note: After destroy, the files remain but the reference to the files and contents are removed from the state.