Hello again. You now have a credential-free local Terraform workflow: Azure CLI authenticates you interactively, and Terraform uses that session while the intended subscription is made explicit in your shell.
This lesson turns that connection into a small, reusable Azure lab configuration. You will provision a resource group and a Standard locally redundant storage account, parameterize their environment-specific settings, expose useful outputs, and let Terraform infer the creation order from resource references. The storage account is deliberately modest: in the next lesson it can become the bootstrap location for remote Terraform state.
Treat the Terraform root module as an interface
A Terraform directory is a root module. Splitting it into main.tf, variables.tf, and outputs.tf is an organizational convention: Terraform reads every .tf file in the directory together.
For this lab, the files have distinct responsibilities:
| File | Responsibility |
|---|---|
main.tf | Provider configuration and Azure resources to create |
variables.tf | Inputs that differ by environment or deployment |
outputs.tf | Values the root module exposes after deployment |
lab.auto.tfvars | Your local, ignored values for this personal lab |
.gitignore | Prevents state, plans, and local value files from reaching Git |
A variable is not merely a shorter way to write a string. It is part of the configuration’s contract: it declares what a caller may customize, what type is expected, what defaults are safe, and which values should be rejected before Azure is changed.
variable block reference for the Terraform configuration language
Read HashiCorp’s variable-block reference to establish the difference between a reusable input and a hard-coded implementation detail.
In the Background and Configuration model sections, read the introduction. Then, in the subsections type, default, description, validation, and sensitive, focus on why constraints make failures occur during planning rather than during an Azure deployment. In Examples, review Basic variable declaration and Variable with validation; skip the provider-credential examples, since this course uses Azure CLI authentication locally.
There are four choices worth making deliberately:
typeprevents accidental type confusion and documents expected shape, such asmap(string)for tags.descriptionexplains the input from the consumer’s perspective. This becomes important when the configuration later grows beyond a personal lab.defaultmakes an input optional. Use one only where a default is genuinely safe.validationenforces a rule before Terraform creates a plan. It is ideal for platform naming constraints that Terraform itself cannot infer.
A storage-account name is a useful example. Azure requires it to be globally unique, 3–24 characters long, and composed only of lowercase letters and digits. Terraform can validate the character set and length locally, but it cannot prove global uniqueness without asking Azure during the plan or apply.

Build a small parameterized Azure configuration
Use the same grasp-azure-lab directory from the previous lesson. First, add these extra exclusions to the existing .gitignore:
*.tfplan
lab.auto.tfvars
A saved Terraform plan can contain sensitive data, even where the CLI redacts it. Treat it like state: local and uncommitted.
Now replace the minimal main.tf from the previous lesson with the following complete version. It retains the same AzureRM provider setup, with no secrets or static credentials.
terraform {
required_version = ">= 1.6.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "lab" {
name = var.resource_group_name
location = var.location
tags = merge(
var.tags,
{
managed_by = "terraform"
}
)
}
resource "azurerm_storage_account" "lab" {
name = var.storage_account_name
resource_group_name = azurerm_resource_group.lab.name
location = azurerm_resource_group.lab.location
account_tier = "Standard"
account_replication_type = "LRS"
https_traffic_only_enabled = true
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = false
tags = merge(
var.tags,
{
managed_by = "terraform"
}
)
}
Create variables.tf alongside it:
variable "resource_group_name" {
type = string
description = "Name of the Azure resource group for the lab."
nullable = false
}
variable "location" {
type = string
description = "Azure region for the lab resources."
default = "westeurope"
nullable = false
validation {
condition = contains(["westeurope", "northeurope"], var.location)
error_message = "For this lab, location must be westeurope or northeurope."
}
}
variable "storage_account_name" {
type = string
description = "Globally unique storage account name: 3 to 24 lowercase letters and digits."
nullable = false
validation {
condition = (
length(var.storage_account_name) >= 3 &&
length(var.storage_account_name) <= 24 &&
can(regex("^[a-z0-9]+$", var.storage_account_name))
)
error_message = "storage_account_name must contain 3 to 24 lowercase letters or digits only."
}
}
variable "tags" {
type = map(string)
description = "Tags applied to all lab resources."
default = {
environment = "lab"
course = "grasp-azure"
cleanup = "review"
}
}
Finally, create outputs.tf:
output "resource_group_name" {
description = "Name of the resource group created for this lab."
value = azurerm_resource_group.lab.name
}
output "storage_account_name" {
description = "Name of the storage account created for this lab."
value = azurerm_storage_account.lab.name
}
output "storage_account_id" {
description = "Azure resource ID of the storage account."
value = azurerm_storage_account.lab.id
}
output "storage_account_blob_endpoint" {
description = "Primary Blob service endpoint of the storage account."
value = azurerm_storage_account.lab.primary_blob_endpoint
}
Two design details matter here:
-
Inputs describe the desired deployment.
resource_group_name,storage_account_name, andtagsmight reasonably differ between a personal lab, test environment, and production environment. They are variables. -
References describe relationships inside the deployment.
The storage account does not receive a separately typed resource-group name or location. Instead, it uses the values resolved from the resource group:resource_group_name = azurerm_resource_group.lab.name location = azurerm_resource_group.lab.locationThis is a dependency reference in the form:
resource_type.local_name.attributeTerraform sees these references and constructs a dependency graph. It therefore creates the resource group before the storage account and destroys the storage account before the resource group. No explicit
depends_onis needed, because the data relationship already expresses the true infrastructure relationship.
The merge expression provides one modest example of an internal implementation expression. The caller supplies var.tags, while the root module guarantees that managed_by = "terraform" is applied. That mandatory tag is placed second, so it cannot accidentally be overridden in the variable file.
Supply local lab values safely
Create lab.auto.tfvars in the same directory. It is intentionally ignored by Git. Replace the placeholder text below with a name that will be unique in Azure; do not literally include angle brackets.
resource_group_name = "rg-grasp-lab-yourinitials"
location = "westeurope"
storage_account_name = "grasp<yourinitials><sixrandomdigits>"
tags = {
environment = "lab"
course = "grasp-azure"
cleanup = "review"
owner = "your-initials"
}
For example, a valid storage account name might be:
storage_account_name = "graspab482917"
Do not use a real name, e-mail address, client name, password, connection string, or token in this file. Its Git exclusion reduces accidental exposure, but .tfvars files are not a secret-management mechanism.
Terraform automatically loads files named terraform.tfvars and files ending in .auto.tfvars. Here, lab.auto.tfvars is convenient because it is unmistakably local to this lab and automatically used by terraform plan.
The Microsoft quickstart uses the same overall division: provider configuration, variable definitions, a resource block consuming variables, and outputs exposing resource values.
Quickstart: Create an Azure resource group using Terraform
Read Microsoft Learn’s resource-group quickstart as a compact reference for the conventional Terraform file layout and post-apply output verification.
In Implement the Terraform code, inspect steps 2 through 5, beginning from the setup step. Compare its variable, resource, and output structure with this lesson’s configuration; our lab adds a storage account specifically to demonstrate a resource-to-resource dependency. Then read Verify the results for the use of terraform output, and scan Clean up resources, beginning the cleanup guidance.
Plan first: inspect inputs, outputs, and dependencies
Format and validate the whole root module:
terraform fmt
terraform validate
Then create a saved plan:
terraform plan -out=lab.tfplan
Read the plan rather than immediately confirming it. It should show:
- one
azurerm_resource_group.labto be created; - one
azurerm_storage_account.labto be created; - the resource group name, location, and tags derived from your variables;
- the storage account using the resource group’s name and location;
- some outputs, notably the storage account ID and Blob endpoint, as “known after apply.”
That last point is important. Terraform knows the desired storage-account name before apply because you supplied it. Azure assigns the resource ID and provider-computed endpoint during creation, so Terraform correctly reports those values as unavailable until apply completes.
If the plan errors before showing Azure changes, interpret the failure by layer:
| Failure | Typical cause | Correct response |
|---|---|---|
| Required variable not set | lab.auto.tfvars is missing, misnamed, or an input is absent | Check the filename and variable names. |
| Validation error | Storage account name has uppercase letters, punctuation, or invalid length | Change the local input; do not weaken the validation. |
| Storage-account name unavailable | Another Azure customer already owns the global name | Change only storage_account_name, then plan again. |
| Authorization error | Azure CLI session or subscription context is wrong, or your identity lacks access | Re-run the account check from the previous lesson before changing Terraform. |
Cost of this practical action: USD 0 in Azure usage. terraform fmt, terraform validate, and terraform plan do not create Azure resources. The saved lab.tfplan file exists only on your workstation.
Apply, verify, and use the outputs
When the plan accurately matches your intended names, subscription, region, tags, and two-resource scope, apply the reviewed plan:
terraform apply lab.tfplan
Terraform should create the resource group first and then the storage account. After a successful apply, query the root-module outputs:
terraform output
For values useful in scripts, request one value without Terraform’s quotation marks:
terraform output -raw storage_account_name
terraform output -raw storage_account_blob_endpoint
You can also verify the resources through Azure CLI:
az group show \
--name "$(terraform output -raw resource_group_name)" \
--query "{name:name, location:location, tags:tags}" \
--output jsonc
az storage account show \
--name "$(terraform output -raw storage_account_name)" \
--resource-group "$(terraform output -raw resource_group_name)" \
--query "{name:name, location:primaryLocation, sku:sku.name, httpsOnly:enableHttpsTrafficOnly}" \
--output jsonc
In PowerShell, the $(terraform output -raw ...) subexpressions work as well. If you prefer, run the output commands first and paste the resulting names into the Azure CLI commands.
Outputs make this root module easier to operate and compose:
- A human can read them after
apply. - A shell script can capture them with
terraform output -raw. - A later root module or child module can consume an exposed value through a module output.
- They do not make a value secret. An output should be marked
sensitive = trueif it exposes confidential information, and that still does not remove the value from Terraform state.
The outputs in this lesson are operational metadata rather than credentials. Do not output storage access keys or connection strings. The AzureRM provider may still record sensitive provider-returned values in Terraform state, which is one reason the next lesson moves state to a controlled Azure Storage backend.
Cost of this practical action: the resource group itself costs USD 0. The storage account is configured as Standard LRS, Azure’s lowest-cost replication option for this kind of lab. An empty account with no uploaded blobs, snapshots, backup, diagnostics, or data transfer is normally expected to have effectively zero or negligible usage for a short exercise; Azure Storage pricing is usage-based and varies by region, subscription, and operations. Keep it empty, do not enable extra data services, and check Cost Management if you leave it running. This account is a candidate for the state backend in the next lesson, so retaining it briefly is reasonable.
Keep the dependency graph clean and retain a cleanup choice
A common Terraform mistake is to replace a meaningful reference with duplicated input values:
# Avoid this pattern in this lab.
resource_group_name = var.resource_group_name
location = var.location
It may deploy successfully, but it weakens the resource relationship. If resource-group naming or location logic later changes, the storage account can become inconsistent. The configuration you used instead derives both values from the resource that owns them.
Use this practical rule:
| Situation | Preferred mechanism |
|---|---|
| A deployment choice comes from outside the module | Input variable, referenced as var.<name> |
| A value belongs to an existing Terraform resource | Resource attribute reference |
| A value should be available after apply | Output block |
| A dependency has no data relationship at all | Consider depends_on carefully; do not use it by default |
For now, keep the resource group and storage account if you will continue to the next lesson soon. That lesson will use Azure Storage to establish remote state safely.
If you are pausing the course or want to remove every resource now, destroy only through Terraform:
terraform plan -destroy -out=destroy.tfplan
terraform apply destroy.tfplan
Do not delete one of these resources manually in the Azure portal or CLI while Terraform still manages it. Manual deletion creates drift and is confusing when you later run plan or destroy.
Cost of this cleanup action: USD 0 for the deletion operation itself. Destroying the storage account stops its future storage and transaction usage. Review the destroy plan before applying it; deleting a storage account permanently deletes any blobs it contains.
Key takeaways
You now have a small but realistic Terraform root module for Azure:
- Variables make deployment-specific choices explicit, typed, documented, and validated.
- A local ignored
lab.auto.tfvarsfile supplies personal lab values without placing them in source control. - Dependency references such as
azurerm_resource_group.lab.nameexpress both data flow and creation order. - Outputs expose selected post-deployment values to people and automation.
terraform plan -outfollowed by applying the reviewed plan gives you a safer operational workflow.- The resource group is free; the empty Standard LRS storage account should remain negligible in cost, but should still be retained only while it has a purpose.
Next, you will use Azure Storage deliberately as a Terraform remote-state backend, then practice safe plan, apply, drift inspection, and destroy operations against that shared state.
Can't find a good explanation? Sign up and we'll make it for you
Sign up