TF: Testing Locally

6 minute read

Description:

Follow these steps to test locally on your machine assuming you have Terraform installed.

To Resolve:

  1. Ensure Terraform is installed:

    1
    2
    3
    4
    
    Set-ExecutionPolicy RemoteSigned
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
    Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
    choco install terraform -y --limitoutput
    
  2. To test locally if only using native resources that don’t connect to anything like testing language features specifically:

    • Create a folder: c:\scripts\test1 then cd c:\scripts\test1
    • Paste the following to c:\scripts\test1\test.tf
    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
    
    terraform {
       required_providers {
          random = {
             source  = "hashicorp/random"
             version = "~>3.3.2"
          }
       }
       required_version = "~>1.1.0"
    }
    
    provider "random" {
    }
    
    resource "random_string" "naming_convention_unique" {
       count   = 5
       length  = 5
       upper   = false
       lower   = true
       numeric = true
       special = false
    }
    
    output "random" {
       value = ["${random_string.naming_convention_unique.*.result}"]
    }
    
    • Run: terraform init
    • Run: terraform plan -out tf.plan
    • Run: terraform apply -auto-approve -input=false ./tf.plan
    • This will create c:\scripts\test1\terraform.tfstate, c:\scripts\test1\tf.plan, c:\scripts\test1\terraform.lock.hcl, and a folder called c:\scripts\test1\.terraform
    • What’s happening here?
      • Terraform initializes locally and downloads whatever providers during the init.
      • During the plan it will see that no terraform.state exists so it should only create new resources.
      • During the apply it will create the local terraform.state file in the current directory.
  3. To test locally connecting to Azure but using a local state file:

    • Create a folder: c:\scripts\test2 then cd c:\scripts\test2
    • Paste the following to c:\scripts\test2\test.tf
    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
    
    terraform {
       required_providers {
    
          random = {
          source  = "hashicorp/random"
          version = "~>3.3.2"
          }
    
          azurerm = {
          source  = "hashicorp/azurerm"
          version = "~>3.10.0"
          }
    
       }
       required_version = "~>1.1.0"
    }
    
    provider "random" {
    }
    
    provider "azurerm" {
       client_id                  = var.client_id
       client_secret              = var.client_secret
       subscription_id            = var.subscription_id
       tenant_id                  = var.tenant_id
       skip_provider_registration = true
       features {}
    }
    
    variable "tenant_id" {
       description = "(Required) Service Principal AD Tenant ID - Azure AD for terraform authentication."
       type        = string
    }
    
    variable "subscription_id" {
       description = "(Required) Azure Subscription Id used to connect to AzureRM provider."
       type        = string
    }
    
    variable "client_id" {
       description = "(Required) Service Principal App ID - Azure AD for terraform authentication."
       type        = string
    }
    
    variable "client_secret" {
       description = "(Required) Service Principal Client Secret - Azure AD for terraform authentication."
       type        = string
    }
    
    resource "random_string" "naming_convention_unique" {
       length  = 5
       upper   = false
       lower   = true
       numeric = true
       special = false
    }
    
    resource "azurerm_resource_group" "rg" {
       name     = "my-rg-${random_string.naming_convention_unique.result}"
       location = "westus"
       tags = {
          Owner       = "Automation Admin"
          CostCenter  = "100"
          EntAppname  = "Automation Admin Terraform POC"
          Environment = "tst"
          Contact     = "gerry@automationadmin.com"
       }
    }
    
    output "res_out_rg_name" {
       value = azurerm_resource_group.rg.name
    }
    
    output "res_out_rg_id" {
       value = azurerm_resource_group.rg.id
    }
    
    • Create a new file c:\scripts\test2\env.tfvars
    1
    2
    3
    4
    
    subscription_id="some-guid"
    tenant_id="some-guid"
    client_id="some-guid"
    client_secret="some-guid"
    
    • Run: terraform init
    • Run: terraform plan -var-file="env.tfvars" -out="tf.plan"
    • Run: terraform apply -auto-approve -input=false ./tf.plan
    • This will create c:\scripts\test2\terraform.tfstate, c:\scripts\test2\tf.plan, c:\scripts\test2\terraform.lock.hcl, and a folder called c:\scripts\test2\.terraform
    • What’s happening here?
      • Terraform initializes locally and downloads whatever providers during the init.
      • During the plan it will see that no terraform.state exists so it should only create new resources.
      • During the plan it will read in env.tfvars and inject their values into the variable $x{} blocks inside test.tf.
      • You could also pass them manually by writing 'terraform plan -var="tenant_id=myvar1_value" -var="subscription_id=myvar2_value" -var="client_id=myvar3_value" -var="client_secret=myvar4_value" -out="tf.plan"'
      • Not sure if that will work exactly as you might have to escape some " or add some in some places, it’s usually just easier to point to a local file.
      • During the apply it will create the local terraform.state file in the current directory.
  4. To test locally connecting to Azure but using a remote state file:

    • Create a folder: c:\scripts\test3 then cd c:\scripts\test3
    • Paste the following to c:\scripts\test3\test.tf:
    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
    
    terraform {
          
       backend "azurerm" {
          resource_group_name  = "tx-storage-rg"
          storage_account_name = "automationadminstorage"
          container_name       = "tfstatesbx"
          key                  = "learning_rg"
       }
          
       required_providers {
    
          random = {
          source  = "hashicorp/random"
          version = "~>3.3.2"
          }
    
          azurerm = {
          source  = "hashicorp/azurerm"
          version = "~>3.10.0"
          }
    
       }
       required_version = "~>1.1.0"
    }
    
    provider "random" {
    }
    
    provider "azurerm" {
       client_id                  = var.client_id
       client_secret              = var.client_secret
       subscription_id            = var.subscription_id
       tenant_id                  = var.tenant_id
       skip_provider_registration = true
       features {}
    }
    
    variable "tenant_id" {
       description = "(Required) Service Principal AD Tenant ID - Azure AD for terraform authentication."
       type        = string
    }
    
    variable "subscription_id" {
       description = "(Required) Azure Subscription Id used to connect to AzureRM provider."
       type        = string
    }
    
    variable "client_id" {
       description = "(Required) Service Principal App ID - Azure AD for terraform authentication."
       type        = string
    }
    
    variable "client_secret" {
       description = "(Required) Service Principal Client Secret - Azure AD for terraform authentication."
       type        = string
    }
    
    resource "random_string" "naming_convention_unique" {
       length  = 5
       upper   = false
       lower   = true
       numeric = true
       special = false
    }
    
    resource "azurerm_resource_group" "rg" {
       name     = "my-rg-${random_string.naming_convention_unique.result}"
       location = "westus"
       tags = {
          Owner       = "Automation Admin"
          CostCenter  = "100"
          EntAppname  = "Automation Admin Terraform POC"
          Environment = "tst"
          Contact     = "gerry@automationadmin.com"
       }
    }
    
    output "res_out_rg_name" {
       value = azurerm_resource_group.rg.name
    }
    
    output "res_out_rg_id" {
       value = azurerm_resource_group.rg.id
    }
    
    • Create a new file c:\scripts\test3\env.tfvars
    1
    2
    3
    4
    
    subscription_id="some-guid"
    tenant_id="some-guid"
    client_id="some-guid"
    client_secret="some-guid"
    
    • Create a new file c:\scripts\test3\backend.hcl
    1
    
    access_key="my-access-key"
    
    • Run: terraform init -backend-config="backend.hcl"
    • Run: terraform plan -var-file="env.tfvars" -out="tf.plan"
    • Run: terraform apply -auto-approve -input=false ./tf.plan
    • This will create c:\scripts\test3\tf.plan, c:\scripts\test3\terraform.lock.hcl, and a folder called c:\scripts\test3\.terraform but NOT c:\scripts\test3\terraform.tfstate because that will be stored on the backend.
    • What’s happening here?
      • Terraform initializes locally and downloads whatever providers during the init but this time it will reach out to Azure using the access_key variable which should be the access key of the storage account where your state file exists.
      • During the plan it will see that a terraform.state exists so it will read your changes against the current file like a normal plan does.
      • During the plan it will read in env.tfvars and inject their values into the variable $x{} blocks inside test.tf.
      • During the apply it will update the state file you pointed to in the terraform { backend {} } block in test.tf.
  5. One of the things you can do since you are testing locally is pipe the output to just get the changes:

    1
    
    terraform plan -var-file="env.tfvars" -out="tf.plan" | Select-String -pattern "created","destroyed","Plan:"
    
  6. Lastly, to inspect map objects (or other possible uses I haven’t thought of yet), you might be able to do the following locally to test:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    terraform {
    required_version = "~>1.1.0"
    }
    
    locals {
    map1 = {
       item1 = {
          name1 = "item1value1"
          name2 = "item1value2"
       }
       item2 = {
          name1 = "item2value1"
          name2 = "item2value2"
       }
    }
    }
    
    resource "null_resource" "for_each" {
    for_each = local.map1
    provisioner "local-exec" {
       command = "echo ${each.key} ${each.value.name1} ${each.value.name2}"
    }
    }
    
    • Which gives me:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    null_resource.changeme_null_resource_foreach["item1"]: Destroying... [id=2211496336822316780]
    null_resource.changeme_null_resource_foreach["item2"]: Destroying... [id=799620883465416640]
    null_resource.changeme_null_resource_foreach["item1"]: Destruction complete after 0s
    null_resource.for_each["item2"]: Creating...
    null_resource.for_each["item1"]: Creating...
    null_resource.changeme_null_resource_foreach["item2"]: Destruction complete after 0s
    null_resource.for_each["item2"]: Provisioning with 'local-exec'...
    null_resource.for_each["item1"]: Provisioning with 'local-exec'...
    null_resource.for_each["item2"] (local-exec): Executing: ["cmd" "/C" "echo item2 item2value1 item2value2"]
    null_resource.for_each["item1"] (local-exec): Executing: ["cmd" "/C" "echo item1 item1value1 item1value2"]
    null_resource.for_each["item2"] (local-exec): item2 item2value1 item2value2
    null_resource.for_each["item1"] (local-exec): item1 item1value1 item1value2
    null_resource.for_each["item2"]: Creation complete after 1s [id=3764591095398055352]
    null_resource.for_each["item1"]: Creation complete after 1s [id=7664239314724568197]
    

Comments