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.
Client hasn't granted Azure access yet. We're using our own Free Trial ($200 credits) to explore, learn, and prepare.
Write all Terraform code, test on free resources, prepare CI/CD pipelines — so when client access arrives, we press one button.
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
10.1.0.0/1610.2.0.0/16What 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
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 |
|---|---|---|
| Provider | A plugin that talks to a specific cloud (Azure, AWS, GCP) | The "API driver" for Azure |
| Resource | A single cloud thing to create (VM, VNet, storage) | A building block |
| State | Terraform's memory of what it has already created | Its "tracking sheet" |
| Plan | A preview of what Terraform WILL do before it does it | A "what-if" dry run |
| Apply | Actually create/modify the infrastructure | Executing the plan |
| Destroy | Delete everything Terraform created | Tear it all down cleanly |
| Module | A reusable package of resources | A template / function |
| Variable | A configurable input value | A parameter |
| Output | A value Terraform exports after creation | A return value |
| Backend | Where Terraform stores its state file | Local disk or remote blob |
| Workspace | An isolated state within the same config | An environment selector |
The Lifecycle — How It Works
.tf files
"I want a VNet with these subnets"
terraform init
Downloads the Azure provider plugin
terraform plan
"I will create 5 resources"
terraform apply
Actually creates them in Azure
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.
Prerequisites — Local Machine Setup
Everything you need on your macOS machine before writing your first Terraform file.
Terraform is a single binary. Install via Homebrew:
What this does: Installs the terraform CLI tool so you can run terraform init, plan, apply, and destroy from VS Code's terminal.
The Azure CLI (az) is how Terraform authenticates to your Azure account:
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.
Press Cmd+Shift+X in VS Code and install these extensions:
| Extension | Publisher | Purpose |
|---|---|---|
| HashiCorp Terraform | HashiCorp | Syntax highlighting, autocomplete, formatting for .tf files |
| Azure Terraform | Microsoft | Azure-specific Terraform snippets and integration |
| Azure Account | Microsoft | Sign in to Azure from VS Code |
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.
You should see the Terraform version and your Azure subscription name. Both appear? You're ready. ✅
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
| Benefit | Details |
|---|---|
| $200 credit | Valid for 30 days. Can be spent on ANY Azure service. |
| 12 months of free services | Specific free-tier SKUs (e.g., B1s VMs, 5 GB Blob Storage). |
| Always-free services | Resource groups, VNets, NSGs, route tables — free forever. |
Cost Mapping to Phase 2 Steps
| Phase 2 Step | Resource | Free Trial Cost | Status |
|---|---|---|---|
| Step 2.1 | Resource Groups (×3) | Free | ✅ Deploy |
| Step 2.2 | Hub VNet | Free | ✅ Deploy |
| Step 2.3 | Spoke VNet + 4 subnets | Free | ✅ Deploy |
| Step 2.3 | VNet Peering | ~$0.01 | ✅ Deploy |
| Step 2.4 | NSGs (×4) + rules | Free | ✅ Deploy |
| Step 2.5 | Key Vault | ~$0.03 | ✅ Deploy |
| Step 2.6 | Blob Storage | 5 GB free | ✅ Deploy |
| Step 2.8 | 1× B1s VM | 750 hrs free | ✅ Deploy |
| Step 2.2 | Azure Bastion | ~$175/mo | ⚠️ Code only |
| Step 2.2 | Azure Firewall | ~$1,060/mo | ⚠️ Code only |
| Step 2.7 | SQL Managed Instance | ~$825/mo | ⚠️ Code only |
| Steps 2.8-10 | D-series VMs (×3) | ~$150-275/mo | ⚠️ Code only |
💡 How to Track Remaining Credits
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 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.
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.
📖 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
The complete reference for .tf file syntax — resources, variables, outputs, modules, expressions, and functions.
Reference for init, plan, apply, destroy, state, import, and every other CLI command.
How destroy works, -target flag for selective destruction, and how it interacts with state. Key for our free trial workflow.
How Terraform tracks real infrastructure via state files. Covers local vs remote backends, locking, and the terraform state subcommands.
How to write reusable modules — exactly the pattern we use for each Azure resource type (networking, compute, storage, etc).
Official HashiCorp tutorial for setting up CI/CD pipelines with GitHub Actions — plan on PR, apply on merge. This is our exact pipeline design.
Azure Provider (AzureRM)
Every Azure resource type we deploy — azurerm_resource_group, azurerm_virtual_network, azurerm_network_security_group, azurerm_windows_virtual_machine, and more.
Microsoft's own tutorials and quickstarts for using Terraform to deploy Azure infrastructure. Covers VMs, VNets, resource groups, and more.
Step-by-step Microsoft tutorial — your first Terraform deployment on Azure. Perfect starting point before Phase A.
Sign up page for the Azure free trial — $200 credit for 30 days + 12 months of free services. This is where we start.
GitHub Actions
Core concepts — workflows, jobs, steps, runners, and triggers. Read this before writing any .yml pipeline files.
How to store ARM_CLIENT_ID, ARM_CLIENT_SECRET, and other Azure credentials securely as repository secrets.
⚠️ 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.
Project Folder Structure
The Terraform code lives inside the repository in a modular structure — write once, deploy to both free trial and real environment.
⚠️ Critical: .gitignore
Terraform state files can contain secrets. Never commit them. Create terraform/.gitignore:
Why This Structure?
- Modules = reusable templates. The VNet module works for both free trial and real environment — only input values change.
- Environments = separate deployments.
free-trial/deploys now;test-environment/deploys the full Pearl estate later. - Write the code ONCE and use it in both places. No duplication.
Terraform Fundamentals — Core Concepts
Before writing Pearl infrastructure code, understand the building blocks through small examples.
Provider Block — "Which Cloud?"
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"
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 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?"
After terraform apply finishes, outputs print to the terminal — resource IDs, IP addresses, etc.
Phase A — Provider & Backend Configuration
Maps to Phase 2 prerequisite — Azure subscription setup. This is the foundation every other file depends on.
Create providers.tf
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
Stores state on your local machine. Fine for solo exploration. For teams + CI/CD, switch to Azure Blob (Section 9).
Pass Sensitive Values Safely
✅ What terraform init Does
- Reads
providers.tf→ sees you needhashicorp/azurerm - Downloads the Azure provider plugin (~150 MB) into
.terraform/ - Creates
.terraform.lock.hcl— locks the provider version (like package-lock.json) - Configures the backend (local state file)
- Prints "Terraform has been successfully initialized!"
Phase B — Resource Groups
Maps to Phase 2 — Step 2.1. Resource groups are free containers that hold related Azure resources.
Module: modules/resource-groups/main.tf
for_each creates one resource per map entry — like a for loop. Pass 3 entries, get 3 resource groups.
Deploy & Verify
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.
Key Concepts
- VNet Peering is bidirectional — you need two resources (Hub→Spoke AND Spoke→Hub)
- Route table sends all internet traffic (
0.0.0.0/0) to Azure Firewall's IP (10.1.1.4). No Firewall yet, but routing is ready. snet-dataNOT route-tabled — SQL MI manages its own routing.- Subnet names matter —
AzureBastionSubnetandAzureFirewallSubnetmust be named exactly that for Azure services.
Code Highlights
Deploy & Verify
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.
NSG Rules Overview
| NSG | Key Inbound | Key Outbound |
|---|---|---|
| nsg-snet-web | Worker/Build → 80,443 • Bastion → 3389 | → SQL 1433 • → Worker 8080 • → Internet 443 |
| nsg-snet-worker | Web → 8080 • Bastion → 3389 | → Web 80,443 • → SQL 1433 |
| nsg-snet-build | Bastion → 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.
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 Design Decisions
purge_protection_enabled = false— allows full deletion duringterraform destroy(settruefor real env)enable_rbac_authorization = true— modern RBAC approach instead of legacy access policiesdepends_on— ensures role assignment exists before creating secrets
Phase G — Azure Blob Storage
Maps to Phase 2 — Step 2.6. Storage account for weekly backup staging. Free tier gives 5 GB.
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
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.
What the Module Creates
- NIC (Network Interface) — connects VM to subnet. No public IP (all access through Bastion).
- Windows VM — using
admin_username+admin_passwordfrom environment variables. - Auto-shutdown schedule — 19:00 UTC daily. Saves credits and matches Phase 2 spec.
- Optional data disk — for VMs that need extra storage (VM1: 256 GB, VM3: 128 GB).
⚠️ 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.
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/moAzure Firewall
Egress control — allowlist-only outbound. The most expensive single resource.
~$1,060/moSQL Managed Instance
General Purpose, 4 vCores. Takes 4-6 hours to provision AND 4-6 hours to destroy.
~$825/moD-series VMs (×3)
D4s_v5 (Web), D2s_v5 (Worker), D2s_v5 (Build) — production-grade SKUs.
~$575/mo total💡 Validate Without Deploying
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."
Selective Destroy
Don't always need to destroy everything. Target specific modules:
End-of-Day Workflow
✅ 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.
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)
This storage holds ONLY Terraform state — separate from Pearl backup storage. Created OUTSIDE Terraform (it can't create its own backend).
Everyone (and GitHub Actions) reads/writes the same state. Azure Blob handles locking — two simultaneous applies won't conflict.
Variables, Environments & Workspaces
Same modules, different values. The only things that change between free trial and real environment are the inputs.
| Setting | free-trial/ | test-environment/ |
|---|---|---|
| VM Size | Standard_B1s | D4s_v5 / D2s_v5 |
| VM Count | 1 | 3 |
| SQL MI | None | GP, 4 vCores |
| Bastion | None | Standard SKU |
| Firewall | None | Standard SKU |
| KV Purge Protection | Off | On |
| Backend | Local | Azure Blob |
| Subscription | Free trial | Client's subscription |
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:
Step 2: Add GitHub Secrets
Go to GitHub repo → Settings → Secrets → Actions:
| Secret Name | Value |
|---|---|
ARM_CLIENT_ID | clientId from JSON |
ARM_CLIENT_SECRET | clientSecret from JSON |
ARM_SUBSCRIPTION_ID | subscriptionId from JSON |
ARM_TENANT_ID | tenantId from JSON |
TF_VAR_vm_admin_password | VM 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.
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.
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.
Full Workflow: Create → Test → Destroy → Recreate
The complete end-to-end process combining everything above.
Local Exploration (Now — Free Trial)
terraform/modules/*terraform planterraform applyaz resource list --resource-group rg-pearl-test-spoke --output table.tf → terraform plan (shows only diffs!)terraform applyterraform destroy (save credits)terraform apply (identical environment returns)CI/CD Workflow (Later — Client Subscription)
git checkout -b feature/add-monitoringterraform plan → plan appears as PR commentterraform apply → infrastructure updatedTransitioning to Client's Azure Subscription
When the client grants access, here's exactly what changes (spoiler: almost nothing).
Update Subscription ID
export TF_VAR_subscription_id="<client-subscription-id>"
Use test-environment/ Config
Full spec: 3 VMs at correct SKUs, SQL MI, Bastion, Firewall. No module changes needed.
Switch to Remote Backend
terraform init with the Azure Blob backend config.
Apply Full Environment
terraform apply — ~50+ resources. Takes 4-6 hours (SQL MI is the bottleneck).
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.
Troubleshooting Common Issues
Quick fixes for the errors you'll encounter most often.
"Error: building account: getting authenticated object ID"
"Error: A resource with the ID already exists"
"Error: Key Vault name is already in use"
"Error: deleting Resource Group: still contains resources"
"Provider hashicorp/azurerm not found"
terraform init wasn't run or .terraform/ was deleted"Error: quota exceeded"
Quick Reference Cheat Sheet
Keep this open in a tab while working.
Essential Commands
| Command | What It Does | When to Use |
|---|---|---|
terraform init | Download providers, configure backend | First time, or after changing providers |
terraform plan | Preview changes (dry run) | Before every apply |
terraform apply | Create/update infrastructure | When plan looks correct |
terraform destroy | Delete all managed infra | End of day, environment reset |
terraform destroy -target=module.X | Delete specific module | Selective cleanup |
terraform validate | Check syntax (no Azure call) | Quick syntax check |
terraform fmt | Auto-format .tf files | Before committing |
terraform state list | Show all resources in state | Debugging |
terraform output | Show output values | Get IPs, names after apply |
terraform import | Import existing resource into state | Adopting manual resources |
File Reference
| File | Purpose |
|---|---|
providers.tf | Which cloud, which version |
backend.tf | Where state is stored |
variables.tf | Input parameter definitions |
terraform.tfvars | Actual input values |
main.tf | Resource definitions / module calls |
outputs.tf | Values to export after apply |
.terraform/ | Downloaded providers (don't commit) |
terraform.tfstate | State file (don't commit — secrets!) |
.terraform.lock.hcl | Provider version lock (do commit) |
Azure CLI Quick Checks
Appendix: Phase 2 Mapping
This table maps every Phase 2 implementation step to its section in this guide.
| Phase 2 Step | Guide Section | Free Trial Status |
|---|---|---|
| Step 2.1 — Resource Groups | Phase B | ✅ Deploy |
| Step 2.2 — Hub VNet | Phase C | ✅ Deploy (VNet only) |
| Step 2.3 — Spoke VNet + Peering | Phase C | ✅ Deploy |
| Step 2.4 — NSGs | Phase E | ✅ Deploy |
| Step 2.5 — Key Vault | Phase F | ✅ Deploy |
| Step 2.6 — Blob Storage | Phase G | ✅ Deploy |
| Step 2.7 — SQL MI | Phase I | ⚠️ Code only |
| Step 2.8 — VM1 Web | Phase H | ⚠️ B1s placeholder |
| Step 2.9 — VM2 Worker | Phase H | ⚠️ Skipped |
| Step 2.10 — VM3 Build | Phase H | ⚠️ B1s placeholder |
| Step 2.2 — Azure Bastion | Phase I | ⚠️ Code only |
| Step 2.2 — Azure Firewall | Phase I | ⚠️ Code only |