In this post we are going to cover Terraform variables, outputs and expressions. These are 7 real Terraform interview questions for data engineers, asked the way they come up on a real project, and the last one is a scenario: the same code must deploy dev, test and prod with different sizes and names, and you design the inputs.
Variables are the inputs of a configuration, locals hold the logic we reuse, and outputs are what other configurations may read. It sounds simple, but this is where real projects go wrong: a typo in a tfvars file, a password in plain text in the state, a value that wins when you didn't expect it. Interviewers ask about it to see if you have been bitten by these.
Part 3 of the Real Time Terraform Interview Questions and Answers series on TechBrothersIT.
- The short answer
- Let's set up the project
- Question 1: A teammate hard-codes the region and names in every resource. How do variables and tfvars files help?
- Question 2: Bad values reach production because nobody checks the inputs. How does variable validation work?
- Question 3: A password shows up in the plan output. How do sensitive values, ephemeral values and write-only arguments work?
- Question 4: Two teams need the storage account name from your configuration. How do outputs and remote state data sources work?
- Question 5: The same naming logic is copied in twenty places. When do you use locals?
- Question 6: Someone asks how Terraform decides which value wins when a variable is set in several places. What is the precedence?
- Scenario 7: The same code must deploy dev, test and prod with different sizes and names. How do you design the inputs?
- The full script
The short answer
Variables are the inputs, typed and validated, with values per environment in .tfvars files. Locals hold shared logic like names and tags, and outputs are what other configurations may read. sensitive only masks the screen; ephemeral values and write-only arguments keep secrets out of state.
Let's set up the project
We use a small Terraform 1.17 project. A terraform_data resource stands in for the storage account, so the plan is real and nothing is created in Azure. The azure/ and analytics-azure/ folders are real azurerm 5.11 code, checked with terraform validate. Before the first step we run terraform init in the root folder and in azure/, analytics/ and analytics-azure/.
terraform {
required_version = ">= 1.17"
}
# Stand-in for azurerm_storage_account: a real plan, nothing in Azure
resource "terraform_data" "lake" {
input = {
name = "stsalesdata${var.environment}"
location = var.location
replication = var.replication
}
}variable "environment" {
type = string
description = "Environment name: dev, test or prod."
}
variable "location" {
type = string
description = "Azure region for every resource."
default = "eastus2"
}
variable "replication" {
type = string
description = "Storage replication: LRS, ZRS or GRS."
default = "LRS"
}environment = "dev"
location = "westeurope"terraform {
required_version = ">= 1.17"
required_providers {
azurerm = { source = "hashicorp/azurerm", version = "~> 5.11" }
}
}
provider "azurerm" {
features {}
}
data "azurerm_key_vault" "platform" {
name = "kv-sales-dev"
resource_group_name = "rg-sales-data-dev"
}# Read during plan and apply; never stored in the plan or state
ephemeral "azurerm_key_vault_secret" "sql_admin" {
name = "sql-admin-password"
key_vault_id = data.azurerm_key_vault.platform.id
}
locals {
sql_password = ephemeral.azurerm_key_vault_secret.sql_admin.value
}
resource "azurerm_mssql_server" "sales" {
name = "sql-sales-data-dev"
resource_group_name = "rg-sales-data-dev"
location = "eastus2"
version = "12.0"
minimum_tls_version = "1.2"
administrator_login = "sqladmin"
administrator_login_password_wo = local.sql_password
administrator_login_password_wo_version = 1
}# The analytics team reads our root outputs
data "terraform_remote_state" "platform" {
backend = "local"
config = {
path = "../terraform.tfstate"
}
}
locals {
platform = data.terraform_remote_state.platform.outputs
}
output "lake_storage_account" {
value = local.platform.storage_account_name
}# Same data source on Azure: the backend settings of the platform
# (tenant and client IDs come from ARM_TENANT_ID and ARM_CLIENT_ID)
data "terraform_remote_state" "platform" {
backend = "azurerm"
config = {
use_oidc = true
use_azuread_auth = true
storage_account_name = "sttfstatesales"
container_name = "tfstate"
key = "platform/prod.terraform.tfstate"
}
}
locals {
platform = data.terraform_remote_state.platform.outputs
}
output "lake_storage_account" {
value = local.platform.storage_account_name
}Question 1: A teammate hard-codes the region and names in every resource. How do variables and tfvars files help?
Variables turn hard-coded values into inputs. We declare each one with a type, a description, and a default when a safe one exists, and read it as var.name. The code stays the same; only the values change per environment.
Let's look at variables.tf. environment has no default, so it is required. location and replication have defaults. The stand-in for the storage account builds its name from var.environment. The values live in terraform.tfvars, which Terraform loads automatically. Let's run terraform plan.
$ terraform plan
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# terraform_data.lake will be created
+ resource "terraform_data" "lake" {
+ id = (known after apply)
+ input = {
+ location = "westeurope"
+ name = "stsalesdatadev"
+ replication = "LRS"
}
+ output = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.The plan shows the name stsalesdatadev, in westeurope, with the default replication, LRS.
Now the pitfall. In the tfvars file, now for prod, someone types replicaton, without the i. We run terraform plan again.
environment = "prod"
location = "westeurope"
replicaton = "GRS"$ terraform plan
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# terraform_data.lake will be created
+ resource "terraform_data" "lake" {
+ id = (known after apply)
+ input = {
+ location = "westeurope"
+ name = "stsalesdataprod"
+ replication = "LRS"
}
+ output = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Warning: Value for undeclared variable
The root module does not declare a variable named "replicaton" but a value
was found in file "terraform.tfvars". If you meant to use this value, add a
"variable" block to the configuration.
To silence these warnings, use TF_VAR_... environment variables to provide
certain "global" settings to all configurations in your organization. To
reduce the verbosity of these warnings, use the -compact-warnings option.
─────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.That's only a warning: Value for undeclared variable. The plan still runs, and prod quietly gets the default, LRS.
Read the warnings in every plan. Give every variable a type and a description, and values that must differ per environment get no default, so a missing value fails loudly.
Question 2: Bad values reach production because nobody checks the inputs. How does variable validation work?
We add a validation block to the variable: a condition that must be true, and an error_message. If the condition is false, the plan fails before anything changes.
Let's add two rules. environment must be dev, test or prod. And since Terraform 1.9, a rule can refer to other variables: production storage must use GRS. Our tfvars file says environment = "qa". We run terraform validate, then terraform plan.
variable "environment" {
type = string
description = "Environment name: dev, test or prod."
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "environment must be one of dev, test, prod."
}
}
variable "location" {
type = string
description = "Azure region for every resource."
default = "eastus2"
}
variable "replication" {
type = string
description = "Storage replication: LRS, ZRS or GRS."
default = "LRS"
validation {
condition = (
var.environment != "prod" || var.replication == "GRS"
)
error_message = "Production storage must use GRS replication."
}
}environment = "qa"
location = "westeurope"$ terraform validate
Success! The configuration is valid.
$ terraform plan
Planning failed. Terraform encountered an error while generating this plan.
Error: Invalid value for variable
on terraform.tfvars line 1:
1: environment = "qa"
├────────────────
│ var.environment is "qa"
environment must be one of dev, test, prod.
This was checked by the validation rule at variables.tf:4,3-13.Validate says Success! The configuration is valid., because validate checks syntax, types and references, not variable values. Then the plan fails: Error: Invalid value for variable, with the file, the line, the value, and our message.
Now prod with LRS.
environment = "prod"
location = "westeurope"
replication = "LRS"$ terraform plan
Planning failed. Terraform encountered an error while generating this plan.
Error: Invalid value for variable
on terraform.tfvars line 3:
3: replication = "LRS"
├────────────────
│ var.environment is "prod"
│ var.replication is "LRS"
Production storage must use GRS replication.
This was checked by the validation rule at variables.tf:20,3-13.It fails the second rule, and the error shows both values: var.environment is "prod" and var.replication is "LRS". Let's set replication = "GRS" and apply, so the lake exists for the next question.
environment = "prod"
location = "westeurope"
replication = "GRS"$ terraform apply -auto-approve
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# terraform_data.lake will be created
+ resource "terraform_data" "lake" {
+ id = (known after apply)
+ input = {
+ location = "westeurope"
+ name = "stsalesdataprod"
+ replication = "GRS"
}
+ output = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
terraform_data.lake: Creating...
terraform_data.lake: Creation complete after <time> [id=<uuid>]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.Apply complete! Resources: 1 added.
The pipeline must plan, not only validate. I also check names against Azure rules, like 3 to 24 lowercase letters and digits for a storage account, and I use preconditions and check blocks for rules about resources.
Question 3: A password shows up in the plan output. How do sensitive values, ephemeral values and write-only arguments work?
Three tools, three levels. Sensitive masks a value on the screen. Ephemeral keeps it out of the plan and the state. And write-only arguments are where ephemeral values can go.
| Setting | What it does | Where the value still is |
|---|---|---|
| sensitive = true | masks it in plan and apply output | plain text in state and plan files |
| ephemeral = true | never written to the plan or the state | only in memory, during the run |
| write-only argument | accepts ephemeral values (Terraform 1.11+) | sent to the provider, never stored |
Let's start with sensitive. In secrets.tf a terraform_data stands in for azurerm_mssql_server, and the variable is sensitive = true. The pipeline passes the password in an environment variable, TF_VAR_sql_admin_password. We run apply.
variable "sql_admin_password" {
type = string
description = "SQL admin password, set by the pipeline."
sensitive = true
}
# Stand-in for azurerm_mssql_server
resource "terraform_data" "sql_server" {
input = {
name = "sql-sales-data-${var.environment}"
admin_login = "sqladmin"
admin_password = var.sql_admin_password
}
}$ TF_VAR_sql_admin_password='Lab-only-Pa55' terraform apply -auto-approve
terraform_data.lake: Refreshing state... [id=<uuid>]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# terraform_data.sql_server will be created
+ resource "terraform_data" "sql_server" {
+ id = (known after apply)
+ input = {
+ admin_login = "sqladmin"
+ admin_password = (sensitive value)
+ name = "sql-sales-data-prod"
}
+ output = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
terraform_data.sql_server: Creating...
terraform_data.sql_server: Creation complete after <time> [id=<uuid>]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.The plan prints admin_password = (sensitive value) instead of the password. But sensitive is only a mask. Let's search the state file.
$ grep 'Lab-only' terraform.tfstate
"admin_password": "Lab-only-Pa55",
"admin_password": "Lab-only-Pa55",The password Lab-only-Pa55 is in plain text, twice: in the input and in the output. Plan files hold it too, so lock down access to both.
Now we mark the same variable ephemeral = true and run terraform validate.
variable "sql_admin_password" {
type = string
description = "SQL admin password, set by the pipeline."
ephemeral = true
}
# Stand-in for azurerm_mssql_server
resource "terraform_data" "sql_server" {
input = {
name = "sql-sales-data-${var.environment}"
admin_login = "sqladmin"
admin_password = var.sql_admin_password
}
}$ terraform validate
Error: Invalid use of ephemeral value
with terraform_data.sql_server,
on secrets.tf line 9, in resource "terraform_data" "sql_server":
9: input = {
10: name = "sql-sales-data-${var.environment}"
11: admin_login = "sqladmin"
12: admin_password = var.sql_admin_password
13: }
Ephemeral values are not valid for "input", because it is not a write-only
attribute and must be persisted to state.Validate refuses: Ephemeral values are not valid for "input", because it is not a write-only attribute and must be persisted to state. Ephemeral values only go where nothing is stored: write-only arguments, locals, provider blocks and other ephemeral blocks.
On Azure, that's the real pattern. In azure/sql.tf an ephemeral azurerm_key_vault_secret feeds administrator_login_password_wo on the SQL server. We run terraform -chdir=azure validate.
$ terraform -chdir=azure validate
Success! The configuration is valid.It says Success! The configuration is valid.
Terraform can't compare a write-only value, so to rotate it, you bump administrator_login_password_wo_version.
Question 4: Two teams need the storage account name from your configuration. How do outputs and remote state data sources work?
Outputs are the public interface of a configuration. Through remote state, other configurations see our outputs, not our resources.
Let's declare two outputs in outputs.tf, each with a description and a value. We remove the secrets example first, apply, and run terraform output.
output "storage_account_name" {
description = "Lake storage account name, used by other teams."
value = terraform_data.lake.output.name
}
output "lake_location" {
description = "Azure region of the lake."
value = terraform_data.lake.output.location
}Delete the file secrets.tf.
$ terraform apply -auto-approve > apply.log
$ terraform output
lake_location = "westeurope"
storage_account_name = "stsalesdataprod"It prints storage_account_name = "stsalesdataprod" and lake_location = "westeurope".
Then the analytics team reads it with the terraform_remote_state data source. In analytics/main.tf it points at our state, here a local file, and gets only the root module outputs. We apply in that folder.
$ cd analytics && terraform apply -auto-approve
data.terraform_remote_state.platform: Reading...
data.terraform_remote_state.platform: Read complete after <time>
Changes to Outputs:
+ lake_storage_account = "stsalesdataprod"
You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
lake_storage_account = "stsalesdataprod"The output lake_storage_account = "stsalesdataprod" came from our state.
On Azure, the data source takes the same settings as our azurerm backend: storage account, container and key, with Entra ID and OIDC. That is analytics-azure/main.tf, and we validate it.
$ terraform -chdir=analytics-azure validate
Success! The configuration is valid.Validate says success.
The reader needs access to the whole state snapshot, secrets included. So the docs recommend publishing shared values to a separate store with its own access control, or tfe_outputs in HCP Terraform. I use remote state only between stacks that one team owns.
Question 5: The same naming logic is copied in twenty places. When do you use locals?
We use a local when a value is computed once and used in many places: names, tags, derived settings. Change the rule once, and every resource follows. And a local is not an input, so nobody can override it from outside.
Let's write locals.tf. The prefix is sales-data- plus the environment. The names map builds the resource group, Key Vault and storage account names from it. The storage account drops the dashes, because Azure allows only lowercase letters and digits there. Tags use merge(). We check both with terraform console, which evaluates them for real.
locals {
prefix = "sales-data-${var.environment}"
names = {
resource_group = "rg-${local.prefix}"
key_vault = "kv-${local.prefix}"
storage_account = "st${replace(local.prefix, "-", "")}"
}
# Standard tags last: merge() keeps the last value for a key
tags = merge(var.extra_tags, {
environment = var.environment
managed_by = "terraform"
})
}terraform {
required_version = ">= 1.17"
}
# Stand-in for azurerm_storage_account: a real plan, nothing in Azure
resource "terraform_data" "lake" {
input = {
name = local.names.storage_account
location = var.location
replication = var.replication
tags = local.tags
}
}variable "environment" {
type = string
description = "Environment name: dev, test or prod."
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "environment must be one of dev, test, prod."
}
}
variable "location" {
type = string
description = "Azure region for every resource."
default = "eastus2"
}
variable "replication" {
type = string
description = "Storage replication: LRS, ZRS or GRS."
default = "LRS"
validation {
condition = (
var.environment != "prod" || var.replication == "GRS"
)
error_message = "Production storage must use GRS replication."
}
}
variable "extra_tags" {
type = map(string)
description = "Tags a team adds; the standard tags always win."
default = {}
}environment = "prod"
location = "westeurope"
replication = "GRS"
extra_tags = { cost_center = "fin-042", managed_by = "manual" }$ echo 'local.names' | terraform console
{
"key_vault" = "kv-sales-data-prod"
"resource_group" = "rg-sales-data-prod"
"storage_account" = "stsalesdataprod"
}
$ echo 'local.tags' | terraform console
{
"cost_center" = "fin-042"
"environment" = "prod"
"managed_by" = "terraform"
}The names are kv-sales-data-prod, rg-sales-data-prod and stsalesdataprod. For tags, the team's extra tags go first and the standard tags last, because merge keeps the last value for a key. The team set managed_by = "manual", and the result is still "terraform".
Locals for everything, so you jump through three files to find a value. Keep locals for logic that is reused. And when ten configurations need the same naming rules, put them in a small naming module.
Question 6: Someone asks how Terraform decides which value wins when a variable is set in several places. What is the precedence?
The last value loaded wins. From lowest to highest:
default- in the variable block (lowest)TF_VAR_name- environment variablesterraform.tfvarsterraform.tfvars.json*.auto.tfvars- in lexical order-var,-var-file- in command-line order (highest)
The surprise for most people: an environment variable does not beat the tfvars file. Let's set TF_VAR_location=centralus while terraform.tfvars says westeurope, and ask the console.
$ echo var.location | TF_VAR_location=centralus terraform console
"westeurope"It prints "westeurope".
Now we add region.auto.tfvars with northeurope, and dr.tfvars with uksouth. Then we ask the console four times: with no flags, with -var, and with -var and -var-file in both orders.
location = "northeurope"location = "uksouth"$ echo var.location | terraform console
"northeurope"
$ echo var.location | terraform console -var location=swedencentral
"swedencentral"
$ echo var.location | terraform console -var location=swedencentral -var-file=dr.tfvars
"uksouth"
$ echo var.location | terraform console -var-file=dr.tfvars -var location=swedencentral
"swedencentral"northeurope wins over terraform.tfvars, and a -var on the command line beats both with swedencentral. On the command line, order matters. -var, then the DR file, gives uksouth. Swap them, and swedencentral wins.
Pass exactly one -var-file per environment, explicitly, and avoid auto files that someone forgets about. If a value surprises you, terraform console shows what Terraform really sees.
Scenario 7: The same code must deploy dev, test and prod with different sizes and names. How do you design the inputs?
One root module, one var file per environment, and as few inputs as possible. Before we start, we destroy the stand-ins and remove the earlier files.
$ terraform destroy -auto-approve > destroy.log
$ rm terraform.tfvars region.auto.tfvars dr.tfvars outputs.tfHere is the design:
- One root module - one .tfvars file per environment, passed with -var-file
- Names from locals - built from the environment, never typed by hand
Names are not inputs: locals build them from the environment, so they follow one rule everywhere. Sizes are typed objects with optional() attributes and defaults, a Terraform 1.3 feature. lake and cluster default to an empty object, so an environment sets only what differs. Dev sets only the environment. Let's plan with -var-file=env/dev.tfvars.
variable "environment" {
type = string
description = "Environment name: dev, test or prod."
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "environment must be one of dev, test, prod."
}
}
variable "lake" {
type = object({
replication = optional(string, "LRS")
retention_days = optional(number, 7)
})
description = "Lake settings; missing fields use the defaults."
default = {}
}
variable "cluster" {
type = object({
node_type = optional(string, "Standard_D4ds_v5")
max_workers = optional(number, 2)
})
description = "Databricks job cluster size."
default = {}
}locals {
prefix = "sales-data-${var.environment}"
names = {
storage_account = "st${replace(local.prefix, "-", "")}"
cluster = "dbc-${local.prefix}"
}
}terraform {
required_version = ">= 1.17"
}
# Stand-ins for the lake and the Databricks job cluster
resource "terraform_data" "lake" {
input = {
name = local.names.storage_account
replication = var.lake.replication
retention_days = var.lake.retention_days
}
}
resource "terraform_data" "cluster" {
input = {
name = local.names.cluster
node_type = var.cluster.node_type
max_workers = var.cluster.max_workers
}
}environment = "dev"environment = "prod"
lake = {
replication = "GRS"
retention_days = 30
}
cluster = {
max_workers = 8
}$ terraform plan -var-file=env/dev.tfvars
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# terraform_data.cluster will be created
+ resource "terraform_data" "cluster" {
+ id = (known after apply)
+ input = {
+ max_workers = 2
+ name = "dbc-sales-data-dev"
+ node_type = "Standard_D4ds_v5"
}
+ output = (known after apply)
}
# terraform_data.lake will be created
+ resource "terraform_data" "lake" {
+ id = (known after apply)
+ input = {
+ name = "stsalesdatadev"
+ replication = "LRS"
+ retention_days = 7
}
+ output = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.The plan shows the defaults: max_workers = 2, LRS, retention_days = 7, and the dev names. Prod sets GRS, 30 days and 8 workers. Let's plan with env/prod.tfvars.
$ terraform plan -var-file=env/prod.tfvars
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# terraform_data.cluster will be created
+ resource "terraform_data" "cluster" {
+ id = (known after apply)
+ input = {
+ max_workers = 8
+ name = "dbc-sales-data-prod"
+ node_type = "Standard_D4ds_v5"
}
+ output = (known after apply)
}
# terraform_data.lake will be created
+ resource "terraform_data" "lake" {
+ id = (known after apply)
+ input = {
+ name = "stsalesdataprod"
+ replication = "GRS"
+ retention_days = 30
}
+ output = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.The node type still comes from the default, Standard_D4ds_v5, and the names follow the same rule: stsalesdataprod.
- Typed objects + optional() defaults - a file sets only what differs
- Validation on inputs - secrets from Key Vault, never in .tfvars
- One state per environment - its own backend key: dev and prod never share a state
Secrets never go into var files: they come from Key Vault at run time. And each environment gets its own state and backend key. We cover that in Part 6 of this series.
Key points to remember
- Typed variables + one .tfvars file per environment
- validation blocks stop bad values at plan time
- sensitive masks output; ephemeral + write-only keep secrets out of state
- outputs are the interface; remote state needs access to the whole state
- Locals for names and tags; the last value set wins
The full script
This is a Python script that writes every file and runs every command above in order: put Terraform 1.17 on your PATH and run it in an empty folder.
Show the full script that writes the files and runs every step
# ========================================================================
# Terraform Interview Q&A #3 - Variables, outputs and expressions
# TechBrothersIT. Names and sample values are invented for teaching.
# Every step below was run with Terraform 1.17.0; the outputs in the video and the blog post are the real outputs.
# Providers used: hashicorp/random, null, local, time (they create nothing in any cloud). Azure and Databricks
# code in this series is checked with terraform validate only: nothing is created in a real subscription.
# To run it yourself (Linux, macOS or WSL; some steps use grep / cat):
# 1. Install Terraform 1.17 from developer.hashicorp.com/terraform/install and put it on your PATH.
# 2. python Terraform_Interview_QA_03_Code.py in an empty folder (terraform init downloads the providers).
# ========================================================================
import os, subprocess
def write(path, text):
if text is None:
if os.path.exists(path): os.remove(path)
return
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(text.strip("\n") + "\n")
def run(cmd, expect_error=False, allow_error=False, env=None):
print("\n$ " + cmd, flush=True)
rc = subprocess.run(cmd, shell=True, env=dict(os.environ, **(env or {}))).returncode
if rc != 0 and not (expect_error or allow_error):
raise SystemExit("stopped: this command was expected to succeed")
# Step 0: the starting files
write('main.tf', """\
terraform {
required_version = ">= 1.17"
}
# Stand-in for azurerm_storage_account: a real plan, nothing in Azure
resource "terraform_data" "lake" {
input = {
name = "stsalesdata${var.environment}"
location = var.location
replication = var.replication
}
}
""")
write('variables.tf', """\
variable "environment" {
type = string
description = "Environment name: dev, test or prod."
}
variable "location" {
type = string
description = "Azure region for every resource."
default = "eastus2"
}
variable "replication" {
type = string
description = "Storage replication: LRS, ZRS or GRS."
default = "LRS"
}
""")
write('terraform.tfvars', """\
environment = "dev"
location = "westeurope"
""")
write('azure/main.tf', """\
terraform {
required_version = ">= 1.17"
required_providers {
azurerm = { source = "hashicorp/azurerm", version = "~> 5.11" }
}
}
provider "azurerm" {
features {}
}
data "azurerm_key_vault" "platform" {
name = "kv-sales-dev"
resource_group_name = "rg-sales-data-dev"
}
""")
write('azure/sql.tf', """\
# Read during plan and apply; never stored in the plan or state
ephemeral "azurerm_key_vault_secret" "sql_admin" {
name = "sql-admin-password"
key_vault_id = data.azurerm_key_vault.platform.id
}
locals {
sql_password = ephemeral.azurerm_key_vault_secret.sql_admin.value
}
resource "azurerm_mssql_server" "sales" {
name = "sql-sales-data-dev"
resource_group_name = "rg-sales-data-dev"
location = "eastus2"
version = "12.0"
minimum_tls_version = "1.2"
administrator_login = "sqladmin"
administrator_login_password_wo = local.sql_password
administrator_login_password_wo_version = 1
}
""")
write('analytics/main.tf', """\
# The analytics team reads our root outputs
data "terraform_remote_state" "platform" {
backend = "local"
config = {
path = "../terraform.tfstate"
}
}
locals {
platform = data.terraform_remote_state.platform.outputs
}
output "lake_storage_account" {
value = local.platform.storage_account_name
}
""")
write('analytics-azure/main.tf', """\
# Same data source on Azure: the backend settings of the platform
# (tenant and client IDs come from ARM_TENANT_ID and ARM_CLIENT_ID)
data "terraform_remote_state" "platform" {
backend = "azurerm"
config = {
use_oidc = true
use_azuread_auth = true
storage_account_name = "sttfstatesales"
container_name = "tfstate"
key = "platform/prod.terraform.tfstate"
}
}
locals {
platform = data.terraform_remote_state.platform.outputs
}
output "lake_storage_account" {
value = local.platform.storage_account_name
}
""")
run('terraform init')
run('terraform -chdir=azure init')
run('terraform -chdir=analytics init')
run('terraform -chdir=analytics-azure init')
run('terraform fmt -check -recursive')
# Step 1: Typed variables, values from terraform.tfvars
run('terraform plan')
# Step 2: A typo in the prod tfvars file is only a warning
write('terraform.tfvars', """\
environment = "prod"
location = "westeurope"
replicaton = "GRS"
""")
run('terraform plan')
# Step 3: validate passes, plan rejects environment "qa"
write('variables.tf', """\
variable "environment" {
type = string
description = "Environment name: dev, test or prod."
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "environment must be one of dev, test, prod."
}
}
variable "location" {
type = string
description = "Azure region for every resource."
default = "eastus2"
}
variable "replication" {
type = string
description = "Storage replication: LRS, ZRS or GRS."
default = "LRS"
validation {
condition = (
var.environment != "prod" || var.replication == "GRS"
)
error_message = "Production storage must use GRS replication."
}
}
""")
write('terraform.tfvars', """\
environment = "qa"
location = "westeurope"
""")
run('terraform validate')
run('terraform plan', expect_error=True) # EXPECTED TO FAIL: the video shows this error
# Step 4: Cross-variable rule: prod needs GRS
write('terraform.tfvars', """\
environment = "prod"
location = "westeurope"
replication = "LRS"
""")
run('terraform plan', expect_error=True) # EXPECTED TO FAIL: the video shows this error
# Step 5: Create the lake (prod, GRS) before the secrets example
write('terraform.tfvars', """\
environment = "prod"
location = "westeurope"
replication = "GRS"
""")
run('terraform apply -auto-approve')
# Step 6: A sensitive variable: masked in the plan
write('secrets.tf', """\
variable "sql_admin_password" {
type = string
description = "SQL admin password, set by the pipeline."
sensitive = true
}
# Stand-in for azurerm_mssql_server
resource "terraform_data" "sql_server" {
input = {
name = "sql-sales-data-${var.environment}"
admin_login = "sqladmin"
admin_password = var.sql_admin_password
}
}
""")
run("TF_VAR_sql_admin_password='Lab-only-Pa55' terraform apply -auto-approve")
# Step 7: ... but stored in plain text in the state
run("grep 'Lab-only' terraform.tfstate")
# Step 8: The same variable marked ephemeral
write('secrets.tf', """\
variable "sql_admin_password" {
type = string
description = "SQL admin password, set by the pipeline."
ephemeral = true
}
# Stand-in for azurerm_mssql_server
resource "terraform_data" "sql_server" {
input = {
name = "sql-sales-data-${var.environment}"
admin_login = "sqladmin"
admin_password = var.sql_admin_password
}
}
""")
run('terraform validate', expect_error=True) # EXPECTED TO FAIL: the video shows this error
# Step 9: Azure: ephemeral Key Vault secret into a write-only argument
run('terraform -chdir=azure validate')
# Step 10: Outputs (the secrets example is removed first)
write('secrets.tf', None)
write('outputs.tf', """\
output "storage_account_name" {
description = "Lake storage account name, used by other teams."
value = terraform_data.lake.output.name
}
output "lake_location" {
description = "Azure region of the lake."
value = terraform_data.lake.output.location
}
""")
run('terraform apply -auto-approve > apply.log')
run('terraform output')
# Step 11: Another configuration reads them with terraform_remote_state
run('cd analytics && terraform apply -auto-approve')
# Step 12: The azurerm backend version
run('terraform -chdir=analytics-azure validate')
# Step 13: Locals for names and tags
write('locals.tf', """\
locals {
prefix = "sales-data-${var.environment}"
names = {
resource_group = "rg-${local.prefix}"
key_vault = "kv-${local.prefix}"
storage_account = "st${replace(local.prefix, "-", "")}"
}
# Standard tags last: merge() keeps the last value for a key
tags = merge(var.extra_tags, {
environment = var.environment
managed_by = "terraform"
})
}
""")
write('main.tf', """\
terraform {
required_version = ">= 1.17"
}
# Stand-in for azurerm_storage_account: a real plan, nothing in Azure
resource "terraform_data" "lake" {
input = {
name = local.names.storage_account
location = var.location
replication = var.replication
tags = local.tags
}
}
""")
write('variables.tf', """\
variable "environment" {
type = string
description = "Environment name: dev, test or prod."
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "environment must be one of dev, test, prod."
}
}
variable "location" {
type = string
description = "Azure region for every resource."
default = "eastus2"
}
variable "replication" {
type = string
description = "Storage replication: LRS, ZRS or GRS."
default = "LRS"
validation {
condition = (
var.environment != "prod" || var.replication == "GRS"
)
error_message = "Production storage must use GRS replication."
}
}
variable "extra_tags" {
type = map(string)
description = "Tags a team adds; the standard tags always win."
default = {}
}
""")
write('terraform.tfvars', """\
environment = "prod"
location = "westeurope"
replication = "GRS"
extra_tags = { cost_center = "fin-042", managed_by = "manual" }
""")
run("echo 'local.names' | terraform console")
run("echo 'local.tags' | terraform console")
# Step 14: terraform.tfvars beats a TF_VAR_ environment variable
run('echo var.location | TF_VAR_location=centralus terraform console')
# Step 15: auto.tfvars, then the command line in order
write('region.auto.tfvars', """\
location = "northeurope"
""")
write('dr.tfvars', """\
location = "uksouth"
""")
run('echo var.location | terraform console')
run('echo var.location | terraform console -var location=swedencentral')
run('echo var.location | terraform console -var location=swedencentral -var-file=dr.tfvars')
run('echo var.location | terraform console -var-file=dr.tfvars -var location=swedencentral')
# Step 16: Reset before the scenario: destroy the stand-ins and remove the earlier files
run('terraform destroy -auto-approve > destroy.log')
run('rm terraform.tfvars region.auto.tfvars dr.tfvars outputs.tf')
# Step 17: One code base: dev
write('variables.tf', """\
variable "environment" {
type = string
description = "Environment name: dev, test or prod."
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "environment must be one of dev, test, prod."
}
}
variable "lake" {
type = object({
replication = optional(string, "LRS")
retention_days = optional(number, 7)
})
description = "Lake settings; missing fields use the defaults."
default = {}
}
variable "cluster" {
type = object({
node_type = optional(string, "Standard_D4ds_v5")
max_workers = optional(number, 2)
})
description = "Databricks job cluster size."
default = {}
}
""")
write('locals.tf', """\
locals {
prefix = "sales-data-${var.environment}"
names = {
storage_account = "st${replace(local.prefix, "-", "")}"
cluster = "dbc-${local.prefix}"
}
}
""")
write('main.tf', """\
terraform {
required_version = ">= 1.17"
}
# Stand-ins for the lake and the Databricks job cluster
resource "terraform_data" "lake" {
input = {
name = local.names.storage_account
replication = var.lake.replication
retention_days = var.lake.retention_days
}
}
resource "terraform_data" "cluster" {
input = {
name = local.names.cluster
node_type = var.cluster.node_type
max_workers = var.cluster.max_workers
}
}
""")
write('env/dev.tfvars', """\
environment = "dev"
""")
write('env/prod.tfvars', """\
environment = "prod"
lake = {
replication = "GRS"
retention_days = 30
}
cluster = {
max_workers = 8
}
""")
run('terraform plan -var-file=env/dev.tfvars')
# Step 18: One code base: prod
run('terraform plan -var-file=env/prod.tfvars')
Part 4: Modules and Reuse
The sample data on this page is invented for teaching. Every terminal output is the real output of the command above it, run with Terraform 1.17.0; IDs, timestamps and durations are masked. Azure and Databricks code is checked with terraform validate. Part of the Real Time Terraform Interview Questions and Answers series on TechBrothersIT. Features, limits and their status (GA or preview) are current as of October 2026; check the official Terraform documentation (developer.hashicorp.com/terraform) for anything you quote in an interview.



No comments:
Post a Comment
Note: Only a member of this blog may post a comment.