Lab Goal

This lab creates a single Azure Resource Group named rg-tf-learning-001. Resource groups are management containers in Azure, so this was a safe first target for learning Terraform without creating compute, networking, storage, or database resources.

The workflow covered:

  • Creating a Terraform project folder in VS Code
  • Authenticating to Azure with Azure CLI
  • Writing a minimal Terraform configuration
  • Initializing the Terraform working directory
  • Formatting and validating the configuration
  • Generating and reviewing a Terraform plan
  • Applying the configuration to Azure
  • Confirming Terraform state tracking
  • Updating tags through Terraform
  • Detecting and correcting manual portal drift

Tools Used

The lab used a modern local workflow instead of portal-only administration:

PowerShell 7

Azure CLI
Git
Terraform
Visual Studio Code

I verified the local tooling with:

terraform -version

az version
git --version
$PSVersionTable.PSVersion

Project Folder

I created a dedicated folder for the lab and opened it in VS Code:

mkdir C:\Code\terraform-azure-lab

cd C:\Code\terraform-azure-lab
code .

The initial project structure was:

terraform-azure-lab

main.tf
variables.tf
outputs.tf
.gitignore
README.md

Azure CLI Login

I authenticated to the correct Azure tenant using Azure CLI:

az login --tenant "<tenant-id>" --use-device-code

Then I confirmed the active subscription:

az account show -o table

For the local Terraform session, I set the Azure subscription and tenant environment variables:

$env:ARM_SUBSCRIPTION_ID = "<subscription-id>"

$env:ARM_TENANT_ID = "<tenant-id>"

These values should not be committed to Git or published publicly.

Git Ignore

I added a .gitignore file so local Terraform files and state would not be committed:

.terraform/

*.tfstate
*.tfstate.*
*.tfvars
crash.log
crash.*.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json

For a local learning lab, local state is fine. In a real production workflow, Terraform state should be stored remotely with locking and appropriate access controls.

First Terraform Configuration

The first main.tf created only one Azure Resource Group:

terraform {

required_version = ">= 1.6.0"

required_providers {
azurerm = {
source  = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}

provider "azurerm" {
resource_provider_registrations = "none"

features {}
}

resource "azurerm_resource_group" "lab" {
name     = "rg-tf-learning-001"
location = "eastus"

tags = {
environment = "lab"
owner       = "me"
managed_by  = "terraform"
}
}

The resource_provider_registrations = "none" setting kept the lab focused. During the first plan, the AzureRM provider attempted to register many Azure service namespaces that were not needed for a simple resource group lab.

Initialize Terraform

The first Terraform command was:

terraform init

This initialized the working directory and downloaded the AzureRM provider. A successful initialization returns:

Terraform has been successfully initialized!

Format and Validate

Next, I formatted and validated the Terraform configuration:

terraform fmt

terraform validate

The expected validation result was:

Success! The configuration is valid.

Validation confirms that Terraform understands the configuration. It does not mean Azure has created anything yet.

Terraform Plan

Next, I generated a plan:

terraform plan

The important result was:

Plan: 1 to add, 0 to change, 0 to destroy.

Terraform showed that it would create only one resource:

azurerm_resource_group.lab

This is one of the most important Terraform habits: always read the plan before applying changes.

Terraform Apply

After confirming the plan, I applied the configuration:

terraform apply

Terraform displayed the plan again and prompted for confirmation:

Do you want to perform these actions?

After typing yes, Terraform created the Azure Resource Group.

Verifying in Azure

After the apply completed, I verified the resource group in the Azure Portal. The resource group existed with the expected tags:

environment = lab

owner       = me
managed_by  = terraform

The resource group contained no resources, which was expected. At this stage, Terraform had created only the management container.

Terraform State

After the first successful apply, Terraform created a local state file and began tracking the resource.

terraform state list

Expected output:

azurerm_resource_group.lab

This means Terraform understands that the resource block in main.tf maps to the real Azure Resource Group.

Updating the Resource

Next, I added a new tag to the resource group:

tags = {

environment = "lab"
owner       = "me"
managed_by  = "terraform"
purpose     = "first-terraform-lab"
}

Then I ran:

terraform fmt

terraform plan

Terraform detected that the resource group already existed and only needed an in-place update:

Plan: 0 to add, 1 to change, 0 to destroy.

The plan showed the new tag being added:

+ "purpose" = "first-terraform-lab"

After applying the change, the Azure Portal showed the new tag.

Portal Drift Test

To test Terraform drift detection, I manually changed the purpose tag in the Azure Portal.

Then I ran:

terraform plan

Terraform detected that Azure no longer matched the configuration:

~ "purpose" = "changed it" -> "first-terraform-lab"

The plan showed:

Plan: 0 to add, 1 to change, 0 to destroy.

Terraform was not going to recreate the resource group. It was only going to correct the tag and bring Azure back in line with the code.

Key Takeaways

This lab demonstrated the basic Terraform workflow:

terraform init

terraform fmt
terraform validate
terraform plan
terraform apply
terraform state list

It also demonstrated the core Terraform value:

Code defines the desired state.

Terraform compares desired state to real infrastructure.
Terraform shows the difference before making changes.
Terraform can detect and correct portal drift.

The safest Terraform habit is to slow down and read the plan.

In this lab, the safe plan patterns were:

Plan: 1 to add, 0 to change, 0 to destroy.

Plan: 0 to add, 1 to change, 0 to destroy.
No changes. Your infrastructure matches the configuration.

In future labs, the thing to watch for is unexpected destroy or replace behavior.

Cleanup

When finished, the lab can be destroyed with:

terraform destroy

Terraform will show a destroy plan and ask for confirmation. For this lab, destruction removes only the Terraform-managed resource group.

Final Thoughts

This was a small lab, but it covered the real Terraform loop: write, format, validate, plan, apply, verify, detect drift, and reconcile.

The resource created was intentionally simple. The learning value came from understanding how Terraform thinks, how state works, and how infrastructure changes become reviewable before they are applied.

That is the foundation for moving from manual Azure administration to infrastructure as code.