variables.tf
file includes variables that typically have a name, description, type and default value.variable "cost_centre" {
description = "Cost Centre Number"
type = string
default = "0001"
}
azurerm_resource_group
resource block using the follow syntax:name = "${var.business_unit}-${var.environment}-${var.resource_group_name}"
location = var.resource_group_location
terraform plan
or terraform apply
. The variable description appears in the prompt.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
$env:TF_VAR_variable_name=value
:$env:TF_VAR_resoure_group_location='uksouth'
dir env:
.terraform.tfvars
file may be used to autoload the variable values replacing any default values.terraform.tfvars
may look like: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.
filename.auto.tfvars
autoloads the variable values from a file replacing any default values. You do not need to use the cli argument -var-file
. Common use case is to separate variable input based on resource type.[]
brackets are used to define a list type variable.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"]
You can reference a list variable type in a resource block simply using var.virtual_network_address_space
.
If you wish to reference a single value you can use:
address_space = [var.virtual_network_address_space[0]]
{}
brackets are used to define a map type variable.variable "default_tags" {
description = "Default Tags for Azure Resources"
type = map(string)
default = {
"deploymentType" = "Terraform",
"costCentre" = "0001"
}
}
default_tags = {
"deploymentType" = "Terraform",
"costCentre" = "1234"
}
var.default_tags
.Note: If a key value starts with a number you must use :
instead of =
when setting a value.
Lookup(map, key, default)
variable "public_ip_sku" {
description = "Azure Public IP Address SKU"
type = map(string)
default = {
"ukwest" = "Basic",
"uksouth" = "Standard"
}
}
azurerm_public_ip
resource block set as:sku = lookup(var.public_ip_sku, var.resoure_group_location, "Basic")
sku
is set based on the resoure_group_location
matching the key value ukwest
or uksouth
, otherwise a default value of Basic is set.condition
and an error_message
.condition
makes use of functions such as contains
, substr
, length
and lower
.error_message
must end string with .
Or ?
.Length ("hi")
2
Length(["a","b"])
2
Length ({"key" = "value"})
1
Substr(string, offset, length)
:Substr("hello world", 1, 4)
ello
Contains(list, value)
:Contains (["a","b"], "a")
true
Lower("ABC")
abc
Regex(pattern, string)
:regex("india$", "westindia")
india
Can(regex("india$", "westindia"))
true
variable "resoure_group_location" {
description = "Resource Group Location"
type = string
default = "uksouth"
validation {
condition = var.resoure_group_location == "uksouth" || var.resoure_group_location =="ukwest"
error_message = "We only allow Resources to be created in uksouth or ukwest locations."
}
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."
}
variable "resoure_group_location" {
description = "Resource Group Location"
type = string
default = "eastus"
validation {
condition = can(regex("india$", var.resoure_group_location))
error_message = "We only allow Resources to be created in westindia or southindia locations."
}
sensitive = true
.variable "localadmin_password" {
description = "Local Administrator Password"
type = string
sensitive = true
}
secrets.tfvars
.secrets.tfvars
to your code repository.terraform plan -var-file="secrets.tfvars"
Note: Environment variable values will appear in command line history.
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" ]
}
azurerm_mysql_server
resource block as follows: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
}
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" ]]
azurerm_mysql_server
resource block as follows: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]
}
}
terraform_remote_state
data source to access outputs that are stored in another project’s remote state.output "resource_group_id" {
description = "Resource Group ID"
# Attribute Reference
value = azurerm_resource_group.rg.id
}
terraform output
- this shows the output from the local state file.terraform output virtual_network_name
.var.list[*].id
is a concise way to express a common expression.azurerm_virtual_network
resource block that is using count
: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
meta-argument will create a map of objects so you cannot use a splat expression as above.for
in the output value.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"
}
local.name
.locals {
# Default tags to be assigned to all resources
businessService = "Reporting"
owner = "IT"
default_tags = {
Service = local.businessService
Owner = local.owner
}
azurerm_resource_group
resource block: tags = local.default_tags
condition ? true_val : false_val
.
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)
}
azurerm_virtual_network
resource block to manipulate count:count = var.environment == "dev" ? 1 : 5
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.
Note: They are not related to terraform cloud workspaces.
You may reference the workspace name using ${terraform.workspace}
. It can be used in your naming and tagging e.g. rg_name = "${var.business_unit}-${terraform.workspace}-${var.resoure_group_name}"
.
terraform workspace list
- shows all workspaces and the current workspace has a *
next to it.terraform workspace show
- shows current workspace.terraform workspace new ws_name
- create a new workspace..\terraform.tfstate.d\ws_name
.terraform workspace select ws_name
- this is how you switch workspaces.terraform workspace delete ws_name
- you cannot delete a workspace that has existing resources.
env:ws_name
e.g. terraform.tfstateenv:dev
.Provisioners are at creation time type by default, usually used for bootstrapping. You can use the when
attribute to change this.
Provisioners should be perceived as a last resort.
ssh
or winrm
using a nested connection
block.connection
blocks cannot refer to a parent resource by local name and use a special self object e.g user = self.admin_username
.
If you wish to use a provisioner
block that is not directly associated with a resource use a null_resource
.
terraform apply
and therefore the provisioner re-run.on_failure
attribute.Upload failed: scp: /var/www/html/file-copy.html: Permission denied
.terraform.tfstate
showing tainted resource:"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
}
connection
block is used to connect to the Windows or Linux VM using winrm and ssh respectively.
azurerm_linux_virtual_machine
resource block example: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"
}
provisioner "remote-exec" {
inline = [
"sudo cp /tmp/index.html /var/www/html"
]
}
Command
is required argument and working_dir
and interpreter
are key optional arguments. provisioner "local-exec" {
when = destroy
command = "echo Destroy-time provisioner Instance Destroyed at `date` >> destroy-time.txt"
working_dir = "terraformhostfolder/"
}
null
resource does nothing.This provisioner
example is used to upload the latest application code to a VM without causing the VM to be re-provisioned.
null = {
source = "hashicorp/null"
version = ">= 3.1.0"
}
time
provider.time = {
source = "hashicorp/time"
version = ">= 0.7.2"
}
time_sleep
resource to wait 90 seconds after VM creation before triggering null_resource
:resource "time_sleep" "wait_90_seconds" {
depends_on = [azurerm_linux_virtual_machine.mylinuxvm]
create_duration = "90s"
}
connection
and provisioner
blocks are in null_resource
you must reference the VM resource rather than use self
: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"
]
}
}
terraform apply
is completed the resource block is re-applied, in this scenario therefore will copy the files:triggers = {
always-update = timestamp()
}
security_rule {}
.for_each
loop to simplify your code by prefixing the block security_rule
with dynamic
:
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.tf
- you can reference a local name a 2nd time and this configuration will override the initial configuration of the object.fileName_override.tf
works as above.Note: If you need to use override.tf files make sure you modify .gitignore
as it will ignore this type of file my default.
A Provider helps provide an interface between terraform and external programs.
Here is an example where the input is captured by the script using the query
block.
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.