Terraform: Testing Locally
Description:
Follow these steps to test locally on your machine assuming you have Terraform installed.
To Resolve:
-
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
-
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
thencd 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 calledc:\scripts\test1\.terraform
- What’s happening here?
- Terraform initializes locally and downloads whatever providers during the
init
. - During the
plan
it will see that noterraform.state
exists so it should only create new resources. - During the
apply
it will create the localterraform.state
file in the current directory.
- Terraform initializes locally and downloads whatever providers during the
- Create a folder:
-
To just test expressions locally, try Terraform Console command
-
To test locally connecting to Azure but using a local state file:
- Create a folder:
c:\scripts\test2
thencd 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 calledc:\scripts\test2\.terraform
- What’s happening here?
- Terraform initializes locally and downloads whatever providers during the
init
. - During the
plan
it will see that noterraform.state
exists so it should only create new resources. - During the
plan
it will read inenv.tfvars
and inject their values into thevariable $x{}
blocks insidetest.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 localterraform.state
file in the current directory.
- Terraform initializes locally and downloads whatever providers during the
- Create a folder:
-
To test locally connecting to Azure but using a remote state file:
- Create a folder:
c:\scripts\test3
thencd 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 calledc:\scripts\test3\.terraform
but NOTc:\scripts\test3\terraform.tfstate
because that will be stored on thebackend
. - What’s happening here?
- Terraform initializes locally and downloads whatever providers during the
init
but this time it will reach out to Azure using theaccess_key
variable which should be the access key of the storage account where your state file exists. - During the
plan
it will see that aterraform.state
exists so it will read your changes against the current file like a normal plan does. - During the
plan
it will read inenv.tfvars
and inject their values into thevariable $x{}
blocks insidetest.tf
. - During the
apply
it will update the state file you pointed to in theterraform { backend {} }
block intest.tf
.
- Terraform initializes locally and downloads whatever providers during the
- Create a folder:
-
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:"
-
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