← Back to Proposal
Terraform Exploration Guide Phase 2 — IaC
Exploration & IaC Preparation

Terraform Exploration Guide

A hands-on, step-by-step walkthrough for learning Terraform locally on VS Code, deploying Azure resources within the free trial, practicing destroy/recreate cycles, and preparing GitHub Actions pipelines — all aligned with the Pearl Phase 2 architecture.

16 Sections
9 Build Phases
$0 Free Resources
3 CI/CD Pipelines
⚠️
Current Status

Client hasn't granted Azure access yet. We're using our own Free Trial ($200 credits) to explore, learn, and prepare.

🎯
Goal

Write all Terraform code, test on free resources, prepare CI/CD pipelines — so when client access arrives, we press one button.

01

Understanding the Big Picture

The Pearl test environment is a complete Azure infrastructure estate mirroring production. Here's the target architecture we're building with Terraform.

Target Architecture

Hub VNet 10.1.0.0/16
🛡️ Azure Bastion 10.1.0.0/26 🔥 Azure Firewall 10.1.1.0/26
⟷ VNet Peering
Spoke VNet 10.2.0.0/16
snet-web 10.2.1.0/24 VM1: IIS + Memcached + Solr
snet-worker 10.2.2.0/24 VM2: Background workers
snet-build 10.2.3.0/24 VM3: GitHub Actions + Restore
snet-data 10.2.4.0/24 SQL MI (17 databases)
🔑 Key Vault 🗄️ Blob Storage 📊 Monitor 🛡️ NSGs

What We're Doing NOW (Free Trial)

✅ Deploy (Free / Pennies)

  • Resource Groups (×3)
  • Hub + Spoke VNets
  • VNet Peering
  • NSGs with full rules
  • Route Tables
  • Key Vault + secrets
  • Blob Storage + containers
  • 1× B1s VM (free tier)

⚠️ Code Only (Expensive)

  • Azure Bastion (~$175/mo)
  • Azure Firewall (~$1,060/mo)
  • SQL MI (~$825/mo)
  • D4s_v5 VM (~$275/mo)
  • D2s_v5 VMs (~$150/mo each)

🔇 Skip Entirely

  • Azure Monitor
  • Backup Vault
  • Defender for Cloud

Why Terraform Instead of Clicking in Azure Portal?

The RFP requires repeatability — wipe and rebuild on demand. Manual portal clicks can't:

❌ Manual / Portal

  • Cannot be repeated reliably
  • Cannot be version-controlled
  • Cannot go through PR reviews
  • Cannot be automated in CI/CD
  • Cannot destroy/recreate with one command

✅ Terraform (IaC)

  • Every resource defined in code
  • Stored in Git, versioned, reviewed
  • Pull request review before any change
  • GitHub Actions runs it automatically
  • terraform destroy → clean slate
02

What Is Terraform and Why We Use It

Terraform is an Infrastructure as Code (IaC) tool by HashiCorp. You write .tf files describing WHAT you want, and Terraform figures out HOW to create it.

Key Concepts in Plain English

Concept What It Means Real-World Analogy
ProviderA plugin that talks to a specific cloud (Azure, AWS, GCP)The "API driver" for Azure
ResourceA single cloud thing to create (VM, VNet, storage)A building block
StateTerraform's memory of what it has already createdIts "tracking sheet"
PlanA preview of what Terraform WILL do before it does itA "what-if" dry run
ApplyActually create/modify the infrastructureExecuting the plan
DestroyDelete everything Terraform createdTear it all down cleanly
ModuleA reusable package of resourcesA template / function
VariableA configurable input valueA parameter
OutputA value Terraform exports after creationA return value
BackendWhere Terraform stores its state fileLocal disk or remote blob
WorkspaceAn isolated state within the same configAn environment selector

The Lifecycle — How It Works

1
Write .tf files

"I want a VNet with these subnets"

2
terraform init

Downloads the Azure provider plugin

3
terraform plan

"I will create 5 resources"

4
terraform apply

Actually creates them in Azure

5
terraform destroy

Deletes everything it created

💡 Critical Insight

Terraform remembers what it created (in the state file). So terraform destroy knows exactly which resources to delete — it won't accidentally touch anything outside its management.

03

Prerequisites — Local Machine Setup

Everything you need on your macOS machine before writing your first Terraform file.

1
Install Terraform

Terraform is a single binary. Install via Homebrew:

# Install Terraform via Homebrew brew tap hashicorp/tap brew install hashicorp/tap/terraform # Verify installation terraform -version # Expected: Terraform v1.x.x (anything 1.5+ is fine)

What this does: Installs the terraform CLI tool so you can run terraform init, plan, apply, and destroy from VS Code's terminal.

2
Install Azure CLI

The Azure CLI (az) is how Terraform authenticates to your Azure account:

# Install Azure CLI via Homebrew brew install azure-cli # Verify installation az version

What this does: Gives you the az tool. Terraform uses the Azure CLI's login session to authenticate — log in once with az login, and Terraform picks up those credentials automatically.

3
Install VS Code Extensions

Press Cmd+Shift+X in VS Code and install these extensions:

ExtensionPublisherPurpose
HashiCorp TerraformHashiCorpSyntax highlighting, autocomplete, formatting for .tf files
Azure TerraformMicrosoftAzure-specific Terraform snippets and integration
Azure AccountMicrosoftSign in to Azure from VS Code
4
Log In to Azure
# Log in to Azure (opens browser) az login # Verify your subscription az account show --output table # If you have multiple subscriptions, set the free trial one: az account set --subscription "<Your Free Trial Subscription ID>" # Confirm az account show --query "{Name:name, SubscriptionId:id, State:state}" --output table

What this does: Opens a browser where you sign in with Azure credentials. After sign-in, Terraform uses these credentials for all operations. az account set ensures Terraform targets the correct subscription.

5
Verify Everything Works
terraform -version && az account show --query name --output tsv

You should see the Terraform version and your Azure subscription name. Both appear? You're ready. ✅

04

Azure Free Trial — Strategy

The Azure Free Trial gives you $200 in credits for 30 days plus 12 months of free-tier services. Here's how to maximise it for Pearl.

What the Free Trial Includes

BenefitDetails
$200 creditValid for 30 days. Can be spent on ANY Azure service.
12 months of free servicesSpecific free-tier SKUs (e.g., B1s VMs, 5 GB Blob Storage).
Always-free servicesResource groups, VNets, NSGs, route tables — free forever.

Cost Mapping to Phase 2 Steps

Phase 2 StepResourceFree Trial CostStatus
Step 2.1Resource Groups (×3)Free✅ Deploy
Step 2.2Hub VNetFree✅ Deploy
Step 2.3Spoke VNet + 4 subnetsFree✅ Deploy
Step 2.3VNet Peering~$0.01✅ Deploy
Step 2.4NSGs (×4) + rulesFree✅ Deploy
Step 2.5Key Vault~$0.03✅ Deploy
Step 2.6Blob Storage5 GB free✅ Deploy
Step 2.81× B1s VM750 hrs free✅ Deploy
Step 2.2Azure Bastion~$175/mo⚠️ Code only
Step 2.2Azure Firewall~$1,060/mo⚠️ Code only
Step 2.7SQL Managed Instance~$825/mo⚠️ Code only
Steps 2.8-10D-series VMs (×3)~$150-275/mo⚠️ Code only

💡 How to Track Remaining Credits

# Check spending from CLI az consumption usage list --query "[].{Service:instanceName, Cost:pretaxCost}" --output table # Or: Azure Portal → Cost Management + Billing → Free Trial status
🎬

Learning Videos — Watch These First

These videos cover Terraform fundamentals, Azure-specific workflows, the destroy/recreate cycle, and CI/CD pipelines. Watch them in order — start with the quick intros, then move to the hands-on courses.

Terraform in 2 Minutes — Quick Intros

Never used Terraform? Start here. These short videos explain what Terraform is and why we use it before you touch any code.

Terraform in 100 Seconds
Fireship Essential Beginner
Terraform Explained in 15 Mins — Tutorial for Beginners
TechWorld with Nana Essential Beginner
What is Infrastructure as Code? IaC Explained
TechWorld with Nana Essential Beginner

Terraform on Azure — Full Hands-On Course

This is the most relevant video for our project. It walks through building a complete Azure dev environment with Terraform — resource groups, virtual networks, subnets, NSGs, VMs — exactly what we're building for Pearl. Watch this end-to-end.

🎯 Why This Video Matters

This course builds the exact same resource types we use in the Pearl architecture — Azure Resource Groups, VNets, Subnets, NSGs, and VMs using the AzureRM provider. By the end you'll understand terraform init, plan, apply, and destroy with real Azure resources.

Complete Terraform Courses

Deep-dive courses that cover Terraform end-to-end — providers, modules, state, workspaces, and the full lifecycle including destroy and recreate workflows.

Complete Terraform Course — From BEGINNER to PRO!
DevOps Directive (Sid Palas) Essential Intermediate
HashiCorp Terraform Associate Certification Course
freeCodeCamp (Andrew Brown) Intermediate
Introduction to HashiCorp Terraform — Official Overview
HashiCorp Beginner

CI/CD Pipelines — GitHub Actions + Terraform

Automating deployments is key. These videos show how GitHub Actions triggers terraform plan on PRs and terraform apply on merge — exactly how our pipelines work.

GitHub Actions Tutorial — Basic Concepts and CI/CD Pipeline
TechWorld with Nana Essential Beginner
DevOps CI/CD Explained in 100 Seconds
Fireship Beginner

📖 Suggested Viewing Order

Day 1: Watch Fireship + Nana quick intros (30 min) → Day 2–3: freeCodeCamp Azure course (follow along) → Day 4: DevOps Directive deep-dive on modules & state → Day 5: GitHub Actions tutorial, then start Phase A of this guide.

📖

Official Documentation & References

Bookmark these. They're the authoritative sources for Terraform, the Azure provider, and GitHub Actions. Refer to them as you work through the build phases.

HashiCorp Terraform

Azure Provider (AzureRM)

GitHub Actions

⚠️ Keep Docs Bookmarked

You'll reference the AzureRM provider docs constantly while writing modules — every resource has specific required and optional arguments. The terraform destroy and state pages are critical for understanding our deploy→test→destroy free trial cycle.

05

Project Folder Structure

The Terraform code lives inside the repository in a modular structure — write once, deploy to both free trial and real environment.

# Create the structure mkdir -p terraform/{modules/{resource-groups,networking,nsg,keyvault,storage,compute},environments/{free-trial,test-environment}}
terraform/
modules/ Reusable building blocks
resource-groups/
main.tf
variables.tf
outputs.tf
networking/
main.tf
variables.tf
outputs.tf
nsg/
main.tf, variables.tf, outputs.tf
keyvault/
main.tf, variables.tf, outputs.tf
storage/
main.tf, variables.tf, outputs.tf
compute/
main.tf, variables.tf, outputs.tf
environments/ Separate deployments
free-trial/ Deploy NOW on free trial
main.tf, variables.tf, terraform.tfvars
providers.tf, backend.tf, outputs.tf
test-environment/ Full Pearl estate (client sub)
main.tf, variables.tf, terraform.tfvars
providers.tf, backend.tf, outputs.tf
.gitignore

⚠️ Critical: .gitignore

Terraform state files can contain secrets. Never commit them. Create terraform/.gitignore:

*.tfstate *.tfstate.* .terraform/ *.auto.tfvars secret.tfvars crash.log

Why This Structure?

06

Terraform Fundamentals — Core Concepts

Before writing Pearl infrastructure code, understand the building blocks through small examples.

Provider Block — "Which Cloud?"

providers.tf
# This tells Terraform: "I want to manage Azure resources" terraform { required_providers { azurerm = { source = "hashicorp/azurerm" # The Azure provider plugin version = "~> 4.0" # Use version 4.x (latest stable) } } } provider "azurerm" { features {} subscription_id = var.subscription_id # Which Azure subscription to target }

required_providers downloads the Azure plugin. subscription_id is the key value that changes when switching from free trial to client's subscription.

Resource Block — "What to Create"

main.tf
resource "azurerm_resource_group" "hub" { name = "rg-pearl-test-hub" location = "uksouth" tags = { Environment = "Test" Project = "Pearl" Owner = "InfraTeam" } }

resource = "create this thing". "azurerm_resource_group" = the type. "hub" = your local reference name. name = what appears in Azure portal.

Variable Block — "Make It Configurable"

variables.tf
variable "location" { description = "Azure region for all resources" type = string default = "uksouth" }

Variables let us change values (like VM size) without editing resource definitions. Free trial: vm_size = "Standard_B1s". Real deployment: vm_size = "Standard_D4s_v5".

Output Block — "What Did We Create?"

outputs.tf
output "hub_resource_group_id" { description = "The ID of the hub resource group" value = azurerm_resource_group.hub.id }

After terraform apply finishes, outputs print to the terminal — resource IDs, IP addresses, etc.

07

Phase A — Provider & Backend Configuration

Maps to Phase 2 prerequisite — Azure subscription setup. This is the foundation every other file depends on.

🔌

Provider & Backend Setup

Configure which cloud, which subscription, and where state is stored.

$0No resources created

Create providers.tf

terraform/environments/free-trial/providers.tf
terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.0" } } required_version = ">= 1.5.0" } provider "azurerm" { features { key_vault { purge_soft_delete_on_destroy = false } resource_group { prevent_deletion_if_contains_resources = false } } subscription_id = var.subscription_id }

prevent_deletion_if_contains_resources = false lets terraform destroy clean up everything without manual steps. For the real environment, leave it true for safety.

Create backend.tf

terraform/environments/free-trial/backend.tf
terraform { backend "local" { path = "terraform.tfstate" } }

Stores state on your local machine. Fine for solo exploration. For teams + CI/CD, switch to Azure Blob (Section 9).

Pass Sensitive Values Safely

# Environment variables (recommended — never committed to Git) export TF_VAR_subscription_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" export TF_VAR_vm_admin_password="YourStr0ng!P@ssw0rd" # First init — downloads the Azure provider plugin cd terraform/environments/free-trial terraform init

✅ What terraform init Does

  1. Reads providers.tf → sees you need hashicorp/azurerm
  2. Downloads the Azure provider plugin (~150 MB) into .terraform/
  3. Creates .terraform.lock.hcl — locks the provider version (like package-lock.json)
  4. Configures the backend (local state file)
  5. Prints "Terraform has been successfully initialized!"
07

Phase B — Resource Groups

Maps to Phase 2 — Step 2.1. Resource groups are free containers that hold related Azure resources.

📦

3 Resource Groups

rg-pearl-test-hub • rg-pearl-test-spoke • rg-pearl-test-data

$0Always free

Module: modules/resource-groups/main.tf

terraform/modules/resource-groups/main.tf
resource "azurerm_resource_group" "this" { for_each = var.resource_groups name = each.key location = each.value.location tags = each.value.tags }

for_each creates one resource per map entry — like a for loop. Pass 3 entries, get 3 resource groups.

Deploy & Verify

# Set subscription ID export TF_VAR_subscription_id=$(az account show --query id --output tsv) # Preview terraform plan # Expected: Plan: 3 to add, 0 to change, 0 to destroy. # Apply terraform apply # Type: yes # Verify in Azure az group list --query "[?starts_with(name, 'rg-pearl-test')].{Name:name, Location:location}" --output table
07

Phase C — Hub-Spoke Networking

Maps to Phase 2 — Steps 2.2 & 2.3. Creates the Hub VNet (control plane), Spoke VNet (workloads), VNet Peering, and Route Tables. All free.

🌐

Hub-Spoke Network Topology

2 VNets • 6 subnets • bidirectional peering • route table

$0VNets, subnets, peering, routes = free

Key Concepts

Code Highlights

VNet Peering (bidirectional)
# Hub → Spoke resource "azurerm_virtual_network_peering" "hub_to_spoke" { name = "hub-to-spoke" resource_group_name = var.hub_resource_group_name virtual_network_name = azurerm_virtual_network.hub.name remote_virtual_network_id = azurerm_virtual_network.spoke.id allow_forwarded_traffic = true # Required for Firewall-routed traffic } # Spoke → Hub resource "azurerm_virtual_network_peering" "spoke_to_hub" { name = "spoke-to-hub" resource_group_name = var.spoke_resource_group_name virtual_network_name = azurerm_virtual_network.spoke.name remote_virtual_network_id = azurerm_virtual_network.hub.id allow_forwarded_traffic = true }

Deploy & Verify

terraform plan # Expected: ~11 resources (2 VNets, 6 subnets, 2 peerings, 1 route table...) terraform apply # Verify az network vnet list --query "[?starts_with(name, 'vnet-pearl')].{Name:name, Address:addressSpace.addressPrefixes[0]}" --output table
07

Phase E — Network Security Groups (NSGs)

Maps to Phase 2 — Step 2.4. NSGs are subnet-level firewall rules. The Phase 2 spec defines exact rules for each subnet.

🛡️

3 NSGs + 19 Security Rules

nsg-snet-web • nsg-snet-worker • nsg-snet-build

$0Always free

NSG Rules Overview

NSGKey InboundKey Outbound
nsg-snet-webWorker/Build → 80,443 • Bastion → 3389→ SQL 1433 • → Worker 8080 • → Internet 443
nsg-snet-workerWeb → 8080 • Bastion → 3389→ Web 80,443 • → SQL 1433
nsg-snet-buildBastion → 3389→ Web 80,443 • → Worker * • → SQL 1433 • → Internet 443

The module uses for_each + flatten to create all NSGs and rules from a single variable map. Full code in the Terraform guide markdown file.

07

Phase F — Azure Key Vault

Maps to Phase 2 — Step 2.5. Key Vault stores secrets (DB connection strings, API keys). Costs fractions of a penny.

🔑

Key Vault + Seeded Secrets

Stripe test key • GoCardless sandbox • Mailgun sandbox • Genesys sandbox

~$0.03Per 10k operations

Key Design Decisions

Key Vault module highlight
resource "azurerm_key_vault" "this" { name = var.name location = var.location resource_group_name = var.resource_group_name tenant_id = data.azurerm_client_config.current.tenant_id sku_name = "standard" soft_delete_retention_days = 7 purge_protection_enabled = false # Allow destroy on free trial enable_rbac_authorization = true # Modern approach } # Seed test secrets resource "azurerm_key_vault_secret" "this" { for_each = var.secrets name = each.key value = each.value key_vault_id = azurerm_key_vault.this.id }
07

Phase G — Azure Blob Storage

Maps to Phase 2 — Step 2.6. Storage account for weekly backup staging. Free tier gives 5 GB.

🗄️

Storage + 3 Containers + Lifecycle

backup • restore • scripts — auto-delete after 28 days

~$0Within 5 GB free tier

Key Settings

  • LRS (Locally Redundant Storage) — cheapest, fine for test backups
  • allow_nested_items_to_be_public = false — nobody on the internet can read blobs
  • Lifecycle rule — auto-deletes files in backup/ after 28 days (prevents unbounded growth)
  • Encryption at rest — on by default in Azure
07

Phase H — Virtual Machines (Free Tier)

Maps to Phase 2 — Steps 2.8-2.10. Deploy ONE B1s VM (free tier) to prove the workflow. The module supports all 3 VMs with different SKUs for the real deployment.

🖥️

1× B1s Exploration VM

Windows Server 2022 • 1 vCPU, 1 GB RAM • Auto-shutdown 19:00 UTC

~$0B1s = 750 hrs free/mo

What the Module Creates

Free trial VM definition
module "compute" { source = "../../modules/compute" vms = { "vm-pearl-explore" = { vm_size = "Standard_B1s" # FREE TIER os_disk_size_gb = 128 source_image = { publisher = "MicrosoftWindowsServer" offer = "WindowsServer" sku = "2022-datacenter-azure-edition-smalldisk" version = "latest" } data_disk_size_gb = 0 # ... subnet, credentials, etc. } } }

⚠️ Cost Note

B1s VM is free, but Premium SSD (P10) charges ~$20/month extra. Change storage_account_type to "StandardSSD_LRS" in the compute module to stay closer to $0 on the free trial.

07

Phase I — Expensive Resources (Code Only)

Write Terraform code and validate with terraform plan, but do NOT apply. These burn through free credits in hours.

🛡️

Azure Bastion

Managed secure RDP/SSH access — no public IPs needed on VMs.

~$175/mo
🔥

Azure Firewall

Egress control — allowlist-only outbound. The most expensive single resource.

~$1,060/mo
🗃️

SQL Managed Instance

General Purpose, 4 vCores. Takes 4-6 hours to provision AND 4-6 hours to destroy.

~$825/mo
🖥️

D-series VMs (×3)

D4s_v5 (Web), D2s_v5 (Worker), D2s_v5 (Build) — production-grade SKUs.

~$575/mo total

💡 Validate Without Deploying

# terraform plan validates syntax + references WITHOUT creating anything # Uncomment expensive resources, run plan, then re-comment terraform plan # If it says "Plan: X to add" without errors — code is valid ✅
08

Terraform Destroy — The Superpower

This is one of the main reasons we use Terraform. The RFP requires: "ability to wipe and rebuild environment on demand."

terraform plan -destroy
Preview what will be deleted (dry run)
terraform destroy
Delete everything Terraform created
terraform apply
Recreate identically from the same code

Selective Destroy

Don't always need to destroy everything. Target specific modules:

# Destroy only VMs (keep networking, storage) terraform destroy -target=module.compute # Destroy only storage terraform destroy -target=module.storage # Destroy networking + NSGs terraform destroy -target=module.networking -target=module.nsg

End-of-Day Workflow

# Morning: Create terraform apply -auto-approve # Do your testing... # Evening: Destroy to save credits terraform destroy -auto-approve # Next morning: Identical environment! terraform apply -auto-approve

✅ Terraform Handles Dependency Order

You can't delete a VNet with active subnets, or a resource group with resources inside it. Terraform knows this and automatically deletes in the correct reverse order: VMs → NICs → subnets → VNets → Resource Groups.

09

Terraform State Management

The terraform.tfstate file is Terraform's memory — it maps your .tf config to real Azure resources.

Local State (Current — Free Trial)

State lives on your machine. Fine for solo exploration but doesn't work for teams or CI/CD.

Remote State in Azure Blob (For Real Environment)

1
Create state storage (one-time, manual)
az group create --name rg-terraform-state --location uksouth az storage account create \ --name stterraformstatepearl \ --resource-group rg-terraform-state \ --location uksouth \ --sku Standard_LRS az storage container create \ --name tfstate \ --account-name stterraformstatepearl

This storage holds ONLY Terraform state — separate from Pearl backup storage. Created OUTSIDE Terraform (it can't create its own backend).

2
Update backend.tf for real environment
terraform/environments/test-environment/backend.tf
terraform { backend "azurerm" { resource_group_name = "rg-terraform-state" storage_account_name = "stterraformstatepearl" container_name = "tfstate" key = "pearl-test.terraform.tfstate" } }

Everyone (and GitHub Actions) reads/writes the same state. Azure Blob handles locking — two simultaneous applies won't conflict.

3
Migrate when ready
terraform init -migrate-state # Terraform asks: "Copy existing state to new backend?" # Type: yes
10

Variables, Environments & Workspaces

Same modules, different values. The only things that change between free trial and real environment are the inputs.

Settingfree-trial/test-environment/
VM SizeStandard_B1sD4s_v5 / D2s_v5
VM Count13
SQL MINoneGP, 4 vCores
BastionNoneStandard SKU
FirewallNoneStandard SKU
KV Purge ProtectionOffOn
BackendLocalAzure Blob
SubscriptionFree trialClient's subscription
11

GitHub Actions — CI/CD for Infrastructure

Automate Terraform so pushing code to Git triggers infrastructure changes. No more manual terraform apply from laptops.

Step 1: Create Azure Service Principal

A "robot account" for GitHub Actions to authenticate to Azure:

az ad sp create-for-rbac \ --name "sp-pearl-terraform-github" \ --role Contributor \ --scopes /subscriptions/<SUBSCRIPTION_ID> \ --sdk-auth # Save the JSON output — you need clientId, clientSecret, subscriptionId, tenantId

Step 2: Add GitHub Secrets

Go to GitHub repo → Settings → Secrets → Actions:

Secret NameValue
ARM_CLIENT_IDclientId from JSON
ARM_CLIENT_SECRETclientSecret from JSON
ARM_SUBSCRIPTION_IDsubscriptionId from JSON
ARM_TENANT_IDtenantId from JSON
TF_VAR_vm_admin_passwordVM admin password

3 Pipelines We Create

🔍

terraform-plan.yml

Trigger: Pull requests that touch terraform/**

What it does: Runs terraform plan and posts the plan as a PR comment. Reviewers see exactly what will change before merging.

On PR
🚀

terraform-apply.yml

Trigger: Push to main or manual dispatch

What it does: Runs terraform apply to deploy. Includes a destroy dropdown for tearing down the environment.

On Merge Manual Destroy

terraform-schedule.yml

Trigger: Cron — destroy at 19:00, recreate at 08:00 (Mon-Fri)

What it does: Automated nightly destroy + morning recreate. Saves up to 60% of compute costs.

Scheduled

✅ The Destroy Dropdown — RFP Requirement Met

In terraform-apply.yml, the workflow_dispatch has a dropdown with "apply" and "destroy" options. Go to GitHub Actions → "Terraform Apply" → Run workflow → select "destroy". This is the destroyable test environment the client asked for.

GitHub Environment Protection

Set up approval gates: GitHub repo → Settings → Environments → test-environment → Required reviewers. Infrastructure changes pause and wait for approval.

12

Full Workflow: Create → Test → Destroy → Recreate

The complete end-to-end process combining everything above.

Local Exploration (Now — Free Trial)

1
Write code in VS Code — terraform/modules/*
2
Preview: terraform plan
3
Deploy: terraform apply
4
Verify: az resource list --resource-group rg-pearl-test-spoke --output table
5
Change & re-plan: Edit .tfterraform plan (shows only diffs!)
6
Apply change: terraform apply
7
Done for the day? terraform destroy (save credits)
8
Next day: terraform apply (identical environment returns)

CI/CD Workflow (Later — Client Subscription)

1
Branch: git checkout -b feature/add-monitoring
2
Push: → GitHub Actions runs terraform plan → plan appears as PR comment
3
Merge to main: → GitHub Actions runs terraform apply → infrastructure updated
4
Fresh env needed? → Actions → "Terraform Apply" → "destroy" → wait → "apply"
5
Automated: Scheduled destroy at 19:00, recreate at 08:00 (Mon-Fri)
13

Transitioning to Client's Azure Subscription

When the client grants access, here's exactly what changes (spoiler: almost nothing).

1

Update Subscription ID

export TF_VAR_subscription_id="<client-subscription-id>"

2

Use test-environment/ Config

Full spec: 3 VMs at correct SKUs, SQL MI, Bastion, Firewall. No module changes needed.

3

Switch to Remote Backend

terraform init with the Azure Blob backend config.

4

Apply Full Environment

terraform apply — ~50+ resources. Takes 4-6 hours (SQL MI is the bottleneck).

5

Update GitHub Secrets

Replace free trial service principal credentials with client's.

✅ What Stays The Same

  • All Terraform modules — zero code changes
  • All GitHub Actions workflows — zero changes
  • All NSG rules, VNet design, Key Vault structure — zero changes

This is the power of IaC. Learn on free trial, point at the real subscription.

14

Troubleshooting Common Issues

Quick fixes for the errors you'll encounter most often.

"Error: building account: getting authenticated object ID"

Azure CLI session expired
az login

"Error: A resource with the ID already exists"

Resource exists in Azure but not in Terraform state
terraform import azurerm_resource_group.this["rg-pearl-test-hub"] /subscriptions/.../resourceGroups/rg-pearl-test-hub

"Error: Key Vault name is already in use"

Soft-deleted vault still occupies the name
az keyvault purge --name kv-pearl-test-explore

"Error: deleting Resource Group: still contains resources"

Azure is slow to delete child resources
# Wait 2-3 minutes and retry terraform destroy

"Provider hashicorp/azurerm not found"

terraform init wasn't run or .terraform/ was deleted
terraform init

"Error: quota exceeded"

Free trial vCPU quota (usually 4 total per region)
# Use smaller VMs (B1s = 1 vCPU) # Or request quota increase: Subscriptions → Usage + Quotas
16

Quick Reference Cheat Sheet

Keep this open in a tab while working.

Essential Commands

CommandWhat It DoesWhen to Use
terraform initDownload providers, configure backendFirst time, or after changing providers
terraform planPreview changes (dry run)Before every apply
terraform applyCreate/update infrastructureWhen plan looks correct
terraform destroyDelete all managed infraEnd of day, environment reset
terraform destroy -target=module.XDelete specific moduleSelective cleanup
terraform validateCheck syntax (no Azure call)Quick syntax check
terraform fmtAuto-format .tf filesBefore committing
terraform state listShow all resources in stateDebugging
terraform outputShow output valuesGet IPs, names after apply
terraform importImport existing resource into stateAdopting manual resources

File Reference

FilePurpose
providers.tfWhich cloud, which version
backend.tfWhere state is stored
variables.tfInput parameter definitions
terraform.tfvarsActual input values
main.tfResource definitions / module calls
outputs.tfValues to export after apply
.terraform/Downloaded providers (don't commit)
terraform.tfstateState file (don't commit — secrets!)
.terraform.lock.hclProvider version lock (do commit)

Azure CLI Quick Checks

# What subscription am I targeting? az account show --query "{Name:name, Id:id}" --output table # List all resource groups az group list --query "[].{Name:name, Location:location}" --output table # List all resources in a group az resource list --resource-group rg-pearl-test-spoke --output table # Delete a resource group manually (emergency) az group delete --name rg-pearl-test-hub --yes --no-wait
📋

Appendix: Phase 2 Mapping

This table maps every Phase 2 implementation step to its section in this guide.

Phase 2 StepGuide SectionFree Trial Status
Step 2.1 — Resource GroupsPhase B✅ Deploy
Step 2.2 — Hub VNetPhase C✅ Deploy (VNet only)
Step 2.3 — Spoke VNet + PeeringPhase C✅ Deploy
Step 2.4 — NSGsPhase E✅ Deploy
Step 2.5 — Key VaultPhase F✅ Deploy
Step 2.6 — Blob StoragePhase G✅ Deploy
Step 2.7 — SQL MIPhase I⚠️ Code only
Step 2.8 — VM1 WebPhase H⚠️ B1s placeholder
Step 2.9 — VM2 WorkerPhase H⚠️ Skipped
Step 2.10 — VM3 BuildPhase H⚠️ B1s placeholder
Step 2.2 — Azure BastionPhase I⚠️ Code only
Step 2.2 — Azure FirewallPhase I⚠️ Code only