Showing preview only (212K chars total). Download the full file or copy to clipboard to get everything.
Repository: thomast1906/terraform-on-azure
Branch: main
Commit: fc974e3678aa
Files: 83
Total size: 190.2 KB
Directory structure:
gitextract_3nsspg_7/
├── .vscode/
│ └── mcp.json
├── 01-introduction/
│ └── README.md
├── 02-installation/
│ └── README.md
├── 03-vscode-setup/
│ └── README.md
├── 04-core-commands/
│ └── README.md
├── 05-resources-and-data/
│ └── README.md
├── 06-azure-provider/
│ └── README.md
├── 07-variables/
│ ├── 1-terraform-variables.md
│ ├── 2-terraform-tfvars.md
│ ├── 3-terraform-local-variables.md
│ └── README.md
├── 08-state-local/
│ ├── 1-terraform-state-local-vs-remote.md
│ ├── 2-terraform-local-state-deploy.md
│ ├── README.md
│ └── examples/
│ └── local-state-example/
│ ├── main.tf
│ └── providers.tf
├── 09-state-remote/
│ ├── README.md
│ ├── examples/
│ │ └── remote-state-example/
│ │ ├── main.tf
│ │ └── providers.tf
│ └── scripts/
│ ├── 1-create-terraform-storage.sh
│ └── 2-delete-terraform-storage.sh
├── 10-advanced-dependencies/
│ ├── README.md
│ └── examples/
│ └── terraform/
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── variables.tf
├── 11-advanced-for-each/
│ ├── README.md
│ └── examples/
│ └── terraform/
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── variables.tf
├── 12-advanced-count/
│ ├── README.md
│ └── examples/
│ └── terraform/
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── variables.tf
├── 13-advanced-conditionals/
│ ├── README.md
│ └── examples/
│ └── conditional-expressions-example/
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── variables.tf
├── 14-advanced-dynamic-blocks/
│ ├── README.md
│ └── examples/
│ └── terraform/
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── variables.tf
├── 15-secret-management/
│ ├── README.md
│ └── examples/
│ └── terraform/
│ ├── main.tf
│ ├── providers.tf
│ └── variables.tf
├── 16-modules/
│ ├── README.md
│ └── examples/
│ └── terraform/
│ ├── main.tf
│ ├── modules/
│ │ └── acr/
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variables.tf
│ └── providers.tf
├── 17-azapi/
│ ├── README.md
│ └── examples/
│ └── terraform/
│ ├── main.tf
│ ├── providers.tf
│ └── variables.tf
├── 18-testing/
│ ├── README.md
│ └── examples/
│ ├── main.tf
│ ├── main.tftest.hcl
│ ├── outputs.tf
│ ├── providers.tf
│ └── variables.tf
├── 19-import/
│ └── README.md
├── 20-state-commands/
│ └── README.md
├── 21-pre-post-conditions/
│ ├── README.md
│ └── examples/
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── variables.tf
├── 22-functions/
│ ├── README.md
│ └── examples/
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── variables.tf
├── 23-mcp-server/
│ └── README.md
├── 24-cleanup/
│ ├── README.md
│ └── scripts/
│ └── cleanup-all-resources.sh
├── README.md
└── renovate.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .vscode/mcp.json
================================================
{
"servers": {
"terraform": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e", "TFE_TOKEN=${input:tfe_token}",
"-e", "TFE_ADDRESS=${input:tfe_address}",
"hashicorp/terraform-mcp-server:0.4.0"
]
}
},
"inputs": [
{
"type": "promptString",
"id": "tfe_token",
"description": "HCP Terraform / Terraform Enterprise API Token (leave empty for public registry only)",
"password": true
},
{
"type": "promptString",
"id": "tfe_address",
"description": "HCP Terraform / Terraform Enterprise Address (default: https://app.terraform.io)",
"password": false
}
]
}
================================================
FILE: 01-introduction/README.md
================================================
# Introduction
This course teaches Terraform on Azure step by step. Each lesson builds on the previous one.
## How this course is structured
- Lessons are numbered `01-` through `23-`.
- Each lesson has a `README.md` with the steps.
- Examples live in `examples/` or `terraform/` under the lesson.
Use the course outline to jump to any lesson:
- [Course README](../README.md)
## What you will build
You will deploy Azure resources with Terraform, move from local to remote state, and learn patterns like loops, conditions, testing, imports, and state operations.
## Prerequisites
- An Azure account
- A code editor (VS Code recommended)
- Basic command line knowledge
## Start here
Begin with the first lesson:
- [Install Terraform](../02-installation/)
================================================
FILE: 02-installation/README.md
================================================
# Install Terraform
You need Terraform installed on your machine to follow this tutorial. Here's how to get it.
## Windows
Use the Windows Package Manager for the fastest installation:
```powershell
winget install Hashicorp.Terraform
```
Alternatively, if you use Chocolatey:
```powershell
choco install terraform
```
## macOS
Use Homebrew to install Terraform:
```bash
# Add the HashiCorp tap
brew tap hashicorp/tap
# Install Terraform
brew install hashicorp/tap/terraform
```
## Linux
Use your distribution's package manager. For Ubuntu/Debian:
```bash
# Add HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
# Add HashiCorp repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Update and install
sudo apt update && sudo apt install terraform
```
## Verify installation
Check that Terraform installed correctly:
```bash
terraform version
```
You should see output showing the Terraform version. You're ready to start deploying infrastructure on Azure.
================================================
FILE: 03-vscode-setup/README.md
================================================
# Set up your editor
VS Code provides the best experience for writing Terraform. You get syntax highlighting, autocompletion, and inline validation.
## Install VS Code
Windows:
```powershell
winget install Microsoft.VisualStudioCode
```
macOS:
```bash
brew install --cask visual-studio-code
```
Linux:
```bash
sudo snap install code --classic
```
## Install the Terraform extension
Open VS Code and install the HashiCorp Terraform extension:
1. Press `Cmd+Shift+X` (macOS) or `Ctrl+Shift+X` (Windows/Linux)
2. Search for "HashiCorp Terraform"
3. Click Install
This extension gives you:
- Syntax highlighting for `.tf` files
- IntelliSense for resource types and properties
- Automatic formatting with `terraform fmt`
- Inline validation as you type
- Resource documentation on hover
You can also install it from the command line:
```bash
code --install-extension hashicorp.terraform
```
## Optional: Install Azure CLI
You'll need the Azure CLI to authenticate with Azure. Install it:
Windows:
```powershell
winget install Microsoft.AzureCLI
```
macOS:
```bash
brew install azure-cli
```
Linux:
```bash
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
```
Verify the installation:
```bash
az version
```
================================================
FILE: 04-core-commands/README.md
================================================
# Core Terraform commands
You'll use these commands constantly. They form the standard workflow for deploying infrastructure.
## Initialize your workspace
```bash
terraform init
```
This downloads provider plugins and sets up your backend. Run it once when you start a new project, or when you add new providers.
## Check your syntax
```bash
terraform validate
```
Validates your configuration files. Use this to catch syntax errors before running `plan`.
## Preview changes
```bash
terraform plan
```
Shows what Terraform will create, update, or delete. Always run this before applying changes. The output uses symbols:
- `+` creates a new resource
- `~` modifies an existing resource
- `-` deletes a resource
- `-/+` replaces a resource (deletes then recreates)
## Apply changes
```bash
terraform apply
```
Applies your configuration to Azure. Terraform shows you the plan and asks for confirmation. Type `yes` to proceed.
To skip the confirmation prompt:
```bash
terraform apply -auto-approve
```
Use auto-approve only in CI/CD pipelines or when you're certain about the changes.
## Destroy resources
```bash
terraform destroy
```
Deletes all resources Terraform manages. Use this to clean up when you're done testing.
## Format your code
```bash
terraform fmt
```
Formats all `.tf` files in your directory to match Terraform style conventions. Run this before committing code.
## Check version
```bash
terraform version
```
Shows your Terraform version. Useful when troubleshooting or confirming you're on the latest release.
## Standard workflow
Here's the typical sequence you'll follow:
1. Write your configuration in `.tf` files
2. Run `terraform init` (only needed once)
3. Run `terraform validate` to check for errors
4. Run `terraform plan` to preview changes
5. Run `terraform apply` to deploy
6. Run `terraform destroy` when you're done
Always run `validate` before `plan`, and always run `plan` before `apply`.
================================================
FILE: 05-resources-and-data/README.md
================================================
# Resources and data sources
Resources are the building blocks of your infrastructure. Each resource block defines one piece of your Azure environment.
## Create a resource
Here's a resource that creates an Azure resource group:
```terraform
resource "azurerm_resource_group" "rg" {
name = "rg-terraform-demo"
location = "uksouth"
}
```
The structure is:
- `resource` keyword
- Resource type in quotes: `"azurerm_resource_group"`
- Local name in quotes: `"rg"` (you use this to reference the resource elsewhere)
- Block containing the resource properties
## Reference a resource
You can reference one resource from another:
```terraform
resource "azurerm_resource_group" "rg" {
name = "rg-terraform-demo"
location = "uksouth"
}
resource "azurerm_storage_account" "sa" {
name = "sttfdemo${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
```
The storage account references the resource group using `azurerm_resource_group.rg.name` and `azurerm_resource_group.rg.location`. Terraform automatically understands the dependency.
## Use a data source
Data sources let you reference existing resources that Terraform doesn't manage. Use them when you need information about resources created outside Terraform:
```terraform
data "azurerm_resource_group" "existing" {
name = "rg-existing"
}
resource "azurerm_storage_account" "sa" {
name = "sttfdemo${random_string.suffix.result}"
resource_group_name = data.azurerm_resource_group.existing.name
location = data.azurerm_resource_group.existing.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
```
The data source queries Azure for the existing resource group. You access its properties with `data.azurerm_resource_group.existing.name`.
## Find available properties
Every resource and data source has documented properties. Check the [Azure Provider documentation](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) to see what's available.
For example, the [azurerm_resource_group data source](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resource_group) exposes properties like `name`, `location`, `tags`, and `id`.
================================================
FILE: 06-azure-provider/README.md
================================================
# Configure the Azure provider
The Azure provider lets Terraform interact with Azure resources. You configure it once at the start of your project.
## Authenticate with Azure
First, sign in to Azure using the Azure CLI:
```bash
az login
```
This opens your browser for authentication. Once you're signed in, the CLI displays your subscriptions:
```json
[
{
"id": "00000000-0000-0000-0000-000000000000",
"name": "My Subscription",
"tenantId": "00000000-0000-0000-0000-000000000000",
"isDefault": true
}
]
```
If you have multiple subscriptions, set the one you want to use:
```bash
az account set --subscription "00000000-0000-0000-0000-000000000000"
```
## Create your first Terraform configuration
Create a file called `providers.tf`:
```terraform
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
```
This tells Terraform:
- Require Terraform 1.0 or later
- Use the Azure provider version 4.x
- Enable default Azure provider features
The `features {}` block is required. It controls provider-specific behaviors like how resources are deleted.
## Initialize the provider
Run init to download the provider:
```bash
terraform init
```
You'll see output showing the provider installation:
```
Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "~> 4.0"...
- Installing hashicorp/azurerm v4.x.x...
- Installed hashicorp/azurerm v4.x.x
```
## Deploy your first resource
Create `main.tf`:
```terraform
resource "azurerm_resource_group" "example" {
name = "rg-terraform-demo"
location = "uksouth"
}
```
Deploy it:
```bash
terraform validate
terraform plan
terraform apply
```
Type `yes` when prompted. Terraform creates the resource group in Azure.
## Verify in Azure
Check that the resource group exists:
```bash
az group show --name rg-terraform-demo
```
You can also view it in the [Azure Portal](https://portal.azure.com/#blade/HubsExtension/BrowseResourceGroups).
## Clean up
Delete the resource group:
```bash
terraform destroy
```
Type `yes` when prompted.
================================================
FILE: 07-variables/1-terraform-variables.md
================================================
# Use variables
Variables make your Terraform configurations reusable. Instead of hardcoding values, you define them once and reference them everywhere.
## Define a variable
Create `variables.tf`:
```terraform
variable "resource_group_name" {
description = "Name of the resource group"
type = string
default = "rg-terraform-demo"
}
variable "location" {
description = "Azure region for resources"
type = string
default = "uksouth"
}
variable "environment" {
description = "Environment name"
type = string
}
```
Each variable has:
- A name (e.g., `resource_group_name`)
- An optional type (`string`, `number`, `bool`, `list`, `map`)
- An optional description
- An optional default value
## Use variables in your configuration
Reference variables with `var.variable_name`:
```terraform
resource "azurerm_resource_group" "example" {
name = var.resource_group_name
location = var.location
tags = {
Environment = var.environment
}
}
```
## Set variable values
You have several options for setting variables without defaults:
### Command line
```bash
terraform apply -var="environment=dev"
```
### Environment variables
```bash
export TF_VAR_environment="dev"
terraform apply
```
### Variable files
Create `terraform.tfvars`:
```terraform
environment = "dev"
```
Terraform automatically loads `terraform.tfvars`. For other file names, use the `-var-file` flag:
```bash
terraform apply -var-file="dev.tfvars"
```
## Variable types
Use specific types for validation:
```terraform
variable "vm_count" {
description = "Number of VMs to create"
type = number
default = 3
}
variable "allowed_locations" {
description = "List of allowed Azure regions"
type = list(string)
default = ["uksouth", "ukwest", "northeurope"]
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {
ManagedBy = "Terraform"
Project = "Demo"
}
}
```
## Add validation
Validate input values:
```terraform
variable "location" {
description = "Azure region for resources"
type = string
validation {
condition = contains(["uksouth", "ukwest", "northeurope"], var.location)
error_message = "Location must be uksouth, ukwest, or northeurope."
}
}
```
## Mark variables as sensitive
Prevent sensitive values from appearing in logs:
```terraform
variable "admin_password" {
description = "Admin password for VMs"
type = string
sensitive = true
}
```
Terraform masks this value in output and logs.
================================================
FILE: 07-variables/2-terraform-tfvars.md
================================================
# Use variable files
Variable files let you separate configuration values from your Terraform code. This keeps sensitive values out of version control and makes it easy to manage different environments.
## Create a tfvars file
Create `terraform.tfvars`:
```terraform
resource_group_name = "rg-prod-app"
location = "uksouth"
environment = "production"
tags = {
ManagedBy = "Terraform"
Environment = "Production"
CostCenter = "Engineering"
}
```
Terraform automatically loads files named `terraform.tfvars` or `*.auto.tfvars`.
## Define your variables
In `variables.tf`, declare the variables without default values:
```terraform
variable "resource_group_name" {
description = "Name of the resource group"
type = string
}
variable "location" {
description = "Azure region for resources"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
}
```
## Use the variables
In `main.tf`:
```terraform
resource "azurerm_resource_group" "example" {
name = var.resource_group_name
location = var.location
tags = var.tags
}
```
## Deploy
Terraform picks up `terraform.tfvars` automatically:
```bash
terraform apply
```
## Manage multiple environments
Create separate files for each environment:
`dev.tfvars`:
```terraform
resource_group_name = "rg-dev-app"
location = "uksouth"
environment = "development"
tags = {
ManagedBy = "Terraform"
Environment = "Development"
}
```
`prod.tfvars`:
```terraform
resource_group_name = "rg-prod-app"
location = "uksouth"
environment = "production"
tags = {
ManagedBy = "Terraform"
Environment = "Production"
}
```
Deploy to dev:
```bash
terraform apply -var-file="dev.tfvars"
```
Deploy to prod:
```bash
terraform apply -var-file="prod.tfvars"
```
## Keep secrets out of version control
Never commit sensitive values to git. Create a `.gitignore`:
```
# Terraform state files
*.tfstate
*.tfstate.backup
# Variable files with secrets
*.tfvars
!example.tfvars
# Terraform directory
.terraform/
```
For secrets like passwords or keys, use Azure Key Vault (covered in section 5) or environment variables:
```bash
export TF_VAR_admin_password="YourSecretPassword"
terraform apply
```
================================================
FILE: 07-variables/3-terraform-local-variables.md
================================================
# Use local values
Local values let you compute values once and reuse them throughout your configuration. Use locals for transformations, computations, or values derived from other variables.
## Define locals
Create a `locals` block in your configuration:
```terraform
locals {
resource_suffix = "${var.environment}-${var.location}"
common_tags = {
ManagedBy = "Terraform"
Environment = var.environment
Location = var.location
}
}
```
## Use locals in resources
Reference locals with `local.name`:
```terraform
resource "azurerm_resource_group" "example" {
name = "rg-${local.resource_suffix}"
location = var.location
tags = local.common_tags
}
resource "azurerm_storage_account" "example" {
name = "st${replace(local.resource_suffix, "-", "")}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = local.common_tags
}
```
## Locals vs variables
Variables are inputs from users. Locals are computed values internal to your configuration.
Use variables when:
- Values come from outside (user input, tfvars files, environment variables)
- You want to parameterize your configuration
Use locals when:
- You're computing or transforming values
- You want to avoid repeating the same expression
- You're building complex values from multiple sources
## Common patterns
### Conditional values
```terraform
locals {
# Use smaller SKUs in dev
vm_size = var.environment == "production" ? "Standard_D4s_v5" : "Standard_B2s"
}
```
### String manipulation
```terraform
locals {
# Storage account names can't have hyphens
storage_name = lower(replace(var.resource_group_name, "-", ""))
}
```
### Merging tags
```terraform
locals {
default_tags = {
ManagedBy = "Terraform"
Project = "Demo"
}
# Merge default tags with user-provided tags
all_tags = merge(local.default_tags, var.tags)
}
```
### Building resource names
```terraform
locals {
# Consistent naming across resources
prefix = "${var.project}-${var.environment}"
resource_group_name = "rg-${local.prefix}"
storage_account_name = "st${replace(local.prefix, "-", "")}"
key_vault_name = "kv-${local.prefix}"
}
```
## Complete example
`variables.tf`:
```terraform
variable "project" {
description = "Project name"
type = string
default = "myapp"
}
variable "environment" {
description = "Environment name"
type = string
}
variable "location" {
description = "Azure region"
type = string
default = "uksouth"
}
```
`main.tf`:
```terraform
locals {
prefix = "${var.project}-${var.environment}"
common_tags = {
ManagedBy = "Terraform"
Project = var.project
Environment = var.environment
}
}
resource "azurerm_resource_group" "example" {
name = "rg-${local.prefix}"
location = var.location
tags = local.common_tags
}
```
`dev.tfvars`:
```terraform
environment = "dev"
```
Deploy:
```bash
terraform apply -var-file="dev.tfvars"
```
This creates `rg-myapp-dev` with consistent tags across all resources.
================================================
FILE: 07-variables/README.md
================================================
# Variables
This lesson shows how to make Terraform configurations reusable with variables and locals.
## Lessons
- [Input variables](./1-terraform-variables.md)
- [Variable files](./2-terraform-tfvars.md)
- [Local values](./3-terraform-local-variables.md)
## What you will do
- Define input variables with types and defaults
- Override values with `.tfvars`
- Use locals to simplify expressions
## Next step
Move to [Lesson 08: State (local)](../08-state-local/)
================================================
FILE: 08-state-local/1-terraform-state-local-vs-remote.md
================================================
# Understand Terraform state
Terraform state tracks the resources it manages. Every time you run `terraform apply`, Terraform updates this state file with the current configuration of your Azure resources.
## What is state?
The state file maps your Terraform configuration to real Azure resources. Terraform uses it to:
- Know which resources it manages
- Detect configuration drift
- Plan changes efficiently
- Store resource metadata
## Local state
By default, Terraform stores state locally in `terraform.tfstate`:
```terraform
terraform {
backend "local" {}
}
```
Local state works for:
- Learning and experimentation
- Personal projects
- Single-user scenarios
Local state doesn't work for:
- Team collaboration (no one else can access your state)
- CI/CD pipelines (each run starts fresh)
- Production environments (state can be lost or corrupted)
## Remote state
Remote state stores your state file in a shared location. For Azure, use an Azure Storage Account:
```terraform
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "sttfstate12345"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}
```
Remote state gives you:
- State locking prevents concurrent modifications, so teammates do not run `terraform apply` at the same time.
- Collaboration improves because everyone works from the same state and sees changes immediately.
- Security improves with encryption, access controls, and audit logs in Azure Storage.
- Durability improves with built-in redundancy.
- History is available when blob versioning is enabled.
## When to use each
Use local state:
- You're learning Terraform
- You're the only person working on the code
- The infrastructure is temporary (testing, demos)
Use remote state:
- Multiple people work on the infrastructure
- You run Terraform from CI/CD
- The infrastructure is production or shared
- You need audit trails
## Next steps
In the following sections, you'll:
1. Deploy infrastructure using local state
2. Set up an Azure Storage Account for remote state
3. Migrate from local to remote state
4. Deploy infrastructure using remote state
================================================
FILE: 08-state-local/2-terraform-local-state-deploy.md
================================================
# Deploy with local state
You'll deploy a simple Azure resource using local state to see how Terraform tracks infrastructure.
## Create your configuration
Navigate to the `local-state-example` directory:
```bash
cd 08-state-local/examples/local-state-example
```
Check the contents of `providers.tf`:
```terraform
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
backend "local" {}
}
provider "azurerm" {
features {}
}
```
The `backend "local" {}` block tells Terraform to store state locally.
Check `main.tf`:
```terraform
resource "azurerm_resource_group" "rg" {
name = "rg-terraform-local-state"
location = "uksouth"
}
```
## Initialize Terraform
```bash
terraform init
```
You'll see:
```
Initializing the backend...
Successfully configured the backend "local"!
```
## Deploy the resource
Check your configuration:
```bash
terraform validate
terraform plan
```
The plan shows:
```
Terraform will perform the following actions:
# azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
+ id = (known after apply)
+ location = "uksouth"
+ name = "rg-terraform-local-state"
}
Plan: 1 to add, 0 to change, 0 to destroy.
```
Apply the configuration:
```bash
terraform apply
```
Type `yes` when prompted.
## Inspect the state file
Terraform created `terraform.tfstate` in your directory:
```bash
ls -la terraform.tfstate
```
View its contents:
```bash
cat terraform.tfstate
```
The state file is JSON. It contains:
- Resource IDs
- Resource attributes
- Dependencies between resources
- Provider configuration
You'll see your resource group's Azure ID, location, and other properties.
## Verify in Azure
Check that the resource exists:
```bash
az group show --name rg-terraform-local-state
```
View it in the [Azure Portal](https://portal.azure.com/#blade/HubsExtension/BrowseResourceGroups).
## Make a change
Edit `main.tf` to add tags:
```terraform
resource "azurerm_resource_group" "rg" {
name = "rg-terraform-local-state"
location = "uksouth"
tags = {
Environment = "Demo"
ManagedBy = "Terraform"
}
}
```
Plan the change:
```bash
terraform plan
```
Terraform detects the difference:
```
Terraform will perform the following actions:
# azurerm_resource_group.rg will be updated in-place
~ resource "azurerm_resource_group" "rg" {
+ tags = {
+ "Environment" = "Demo"
+ "ManagedBy" = "Terraform"
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
```
The `~` symbol means Terraform will modify the resource in place.
Apply the change:
```bash
terraform apply
```
## Clean up
Destroy the resource:
```bash
terraform destroy
```
Type `yes` when prompted. The state file now shows zero resources.
## Key takeaways
- Terraform stores state in `terraform.tfstate`
- State maps your configuration to real Azure resources
- Terraform uses state to plan changes
- Local state works for single-user scenarios but not teams
================================================
FILE: 08-state-local/README.md
================================================
# State: local
This lesson introduces Terraform state and runs a local-state deployment.
## Lessons
- [Local vs remote state](./1-terraform-state-local-vs-remote.md)
- [Deploy with local state](./2-terraform-local-state-deploy.md)
## Examples
- [Local state example](./examples/local-state-example/)
## Next step
Move to [Lesson 09: State (remote)](../09-state-remote/)
================================================
FILE: 08-state-local/examples/local-state-example/main.tf
================================================
resource "azurerm_resource_group" "rg" {
name = "rg-demo-local"
location = "uksouth"
}
================================================
FILE: 08-state-local/examples/local-state-example/providers.tf
================================================
terraform {
backend "local" {
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 09-state-remote/README.md
================================================
# Deploy with remote state
Remote state stores your state file in Azure Storage. This enables team collaboration and provides state locking to prevent conflicts.
## Create the storage account
You need an Azure Storage Account to hold your state file. Run the provided script:
```bash
cd 09-state-remote/scripts
chmod +x 1-create-terraform-storage.sh
```
Edit the variables at the top of `1-create-terraform-storage.sh`:
```bash
RESOURCE_GROUP_NAME="rg-terraform-state"
STORAGE_ACCOUNT_NAME="sttfstate${RANDOM}"
```
Run the script:
```bash
./1-create-terraform-storage.sh
```
The script creates:
- An Azure resource group
- An Azure storage account with a unique name
- A blob container named `tfstate`
- Blob versioning enabled for state history
The script outputs the values you'll need for your backend configuration.
> **Important**: Keep this storage account! You will use it throughout the remaining lessons in this course. Every example from lesson 10 onwards references this same storage account for remote state. Do not delete it until you've completed all lessons.
## Configure remote backend
Navigate to the remote state example:
```bash
cd ./examples/remote-state-example
```
Check `providers.tf`. Update it with your storage account details:
```terraform
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "sttfstate12345" # Use your storage account name
container_name = "tfstate"
key = "demo.terraform.tfstate"
}
}
provider "azurerm" {
features {}
}
```
The `backend "azurerm"` block tells Terraform where to store state remotely.
## Initialize with remote backend
```bash
terraform init
```
Terraform configures the Azure backend:
```
Initializing the backend...
Successfully configured the backend "azurerm"!
```
## Deploy the resource
Check the configuration in `main.tf`:
```terraform
resource "azurerm_resource_group" "rg" {
name = "rg-terraform-remote-state"
location = "uksouth"
}
```
Validate and plan:
```bash
terraform validate
terraform plan
```
Apply:
```bash
terraform apply
```
Type `yes` when prompted.
## Verify remote state
After applying, check the storage account. Your state file now exists in Azure:
```bash
az storage blob list \
--account-name sttfstate12345 \
--container-name tfstate \
--output table
```
You'll see `demo.terraform.tfstate` in the container.
View it in the [Azure Portal](https://portal.azure.com) under Storage Account > Containers > tfstate.
## Test state locking
Remote state includes automatic state locking. Try running two `terraform apply` commands simultaneously from different terminals:
Terminal 1:
```bash
terraform apply
```
Terminal 2 (while first is still running):
```bash
terraform apply
```
The second command waits or fails with a lock error. This prevents concurrent modifications that could corrupt state.
## Clean up
To clean up the demo resource group created in this lesson:
```bash
terraform destroy
```
The state file remains in Azure Storage but shows zero resources.
> **Note**: Do NOT delete the storage account yet if you plan to continue with the remaining lessons. See the "Reusing the Storage Account" section below.
## Reusing the storage account
**Keep your storage account for the rest of the course.** All subsequent lessons (10-17) use this same storage account for remote state.
Each lesson uses a different container name within the same storage account:
- Lesson 10 (Dependencies): `dependson`
- Lesson 11 (For Each): `foreach`
- Lesson 12 (Count): `count`
- Lesson 13 (Conditionals): `conditional`
- Lesson 14 (Dynamic Blocks): `dynamicblocks`
- Lesson 15 (Secret Management): `keyvault`
- Lesson 16 (Modules): `modules`
- Lesson 17 (AzAPI): `azapi`
This approach:
- Demonstrates real-world usage where teams share a single state storage account
- Keeps costs minimal by using one storage account
- Organizes state files logically by container
- Shows how multiple projects can coexist in the same backend
Update the `storage_account_name` in each lesson's `providers.tf` file with your actual storage account name created in this lesson.
## Final cleanup
When you've completed all lessons in the course, delete the storage account:
```bash
cd scripts
./2-delete-terraform-storage.sh
```
Update the script variables to match your resource group name before running.
This removes:
- The storage account
- All state containers and files
- The `rg-terraform-state` resource group
## Key takeaways
- Remote state enables team collaboration
- Azure Storage provides state locking automatically
- State files are encrypted and backed up
- Blob versioning gives you state history
- Use remote state for any shared or production infrastructure
================================================
FILE: 09-state-remote/examples/remote-state-example/main.tf
================================================
resource "azurerm_resource_group" "rg" {
name = "rg-demo"
location = "uksouth"
}
================================================
FILE: 09-state-remote/examples/remote-state-example/providers.tf
================================================
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "tfstate"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 09-state-remote/scripts/1-create-terraform-storage.sh
================================================
#!/usr/bin/env bash
#set -x
# Creates the relevant storage account to store terraform state locally
RESOURCE_GROUP_NAME="rg-terraform-state"
STORAGE_ACCOUNT_NAME="sttfstate${RANDOM}"
# Create Resource Group
az group create -l uksouth -n $RESOURCE_GROUP_NAME
# Create Storage Account
az storage account create -n $STORAGE_ACCOUNT_NAME -g $RESOURCE_GROUP_NAME -l uksouth --sku Standard_LRS
# Create Storage Account blob
az storage container create --name tfstate --account-name $STORAGE_ACCOUNT_NAME
================================================
FILE: 09-state-remote/scripts/2-delete-terraform-storage.sh
================================================
#!/usr/bin/env bash
#set -x
# Deletes the relevant storage account to store terraform state
RESOURCE_GROUP_NAME="deploy-first-rg"
# Delete Resource Group
az group delete -n $RESOURCE_GROUP_NAME
================================================
FILE: 10-advanced-dependencies/README.md
================================================
# Control resource dependencies
Terraform automatically determines resource dependencies from references. Use `depends_on` only when Terraform can't detect dependencies automatically.
## When to use depends_on
Terraform infers dependencies from resource references:
```terraform
resource "azurerm_resource_group" "example" {
name = "rg-demo"
location = "uksouth"
}
resource "azurerm_storage_account" "example" {
name = "stdemo${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.example.name # Terraform knows to create RG first
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
```
Terraform creates the resource group first because the storage account references it.
Use `depends_on` when:
- A resource relies on another resource's side effects
- Dependencies aren't expressed through references
- You need to control creation order for resources that don't directly reference each other
## Example: Role assignment timing
Role assignments take time to propagate. Use `depends_on` to ensure permissions exist before using them:
```terraform
resource "azurerm_resource_group" "example" {
name = "rg-demo"
location = "uksouth"
}
resource "azurerm_user_assigned_identity" "example" {
name = "id-demo"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
}
resource "azurerm_role_assignment" "example" {
scope = azurerm_resource_group.example.id
role_definition_name = "Contributor"
principal_id = azurerm_user_assigned_identity.example.principal_id
}
resource "azurerm_container_group" "example" {
name = "aci-demo"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
os_type = "Linux"
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.example.id]
}
container {
name = "hello-world"
image = "mcr.microsoft.com/azuredocs/aci-helloworld:latest"
cpu = "0.5"
memory = "1.5"
}
# Wait for role assignment to propagate
depends_on = [azurerm_role_assignment.example]
}
```
The container group needs the role assignment to complete before it starts.
## Example: Module outputs
Use `depends_on` when a module's side effects matter:
```terraform
module "network" {
source = "./modules/network"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
}
module "compute" {
source = "./modules/compute"
subnet_id = module.network.subnet_id
# Ensure network security rules are fully applied
depends_on = [module.network]
}
```
## Try it yourself
Navigate to the example:
```bash
cd 10-advanced-dependencies/examples/terraform
```
Deploy:
```bash
terraform init
terraform validate
terraform plan
terraform apply
```
Clean up:
```bash
terraform destroy
```
================================================
FILE: 10-advanced-dependencies/examples/terraform/main.tf
================================================
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = "uksouth"
}
resource "azurerm_user_assigned_identity" "example" {
name = "id-demo"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
}
resource "azurerm_role_assignment" "example" {
scope = azurerm_resource_group.rg.id
role_definition_name = "Contributor"
principal_id = azurerm_user_assigned_identity.example.principal_id
}
resource "azurerm_container_group" "example" {
name = "aci-demo"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
os_type = "Linux"
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.example.id]
}
container {
name = "hello-world"
image = "mcr.microsoft.com/azuredocs/aci-helloworld:latest"
cpu = "0.5"
memory = "1.5"
ports {
port = 80
protocol = "TCP"
}
}
# Wait for role assignment to propagate
depends_on = [azurerm_role_assignment.example]
}
================================================
FILE: 10-advanced-dependencies/examples/terraform/outputs.tf
================================================
output "resource_group_id" {
description = "ID of the resource group"
value = azurerm_resource_group.rg.id
}
output "user_assigned_identity_id" {
description = "ID of the user assigned identity"
value = azurerm_user_assigned_identity.example.id
}
output "container_group_id" {
description = "ID of the container group"
value = azurerm_container_group.example.id
}
output "container_group_ip" {
description = "IP address of the container group"
value = azurerm_container_group.example.ip_address
}
================================================
FILE: 10-advanced-dependencies/examples/terraform/providers.tf
================================================
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "dependson"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 10-advanced-dependencies/examples/terraform/variables.tf
================================================
variable "resource_group_name" {
type = string
default = "rg-demo-depends"
}
================================================
FILE: 11-advanced-for-each/README.md
================================================
# Create multiple resources with for_each
The `for_each` argument creates multiple instances of a resource. Use it when you need several similar resources with different configurations.
## for_each basics
`for_each` accepts a map or set:
```terraform
resource "azurerm_resource_group" "example" {
for_each = toset(["dev", "staging", "prod"])
name = "rg-${each.key}"
location = "uksouth"
tags = {
Environment = each.key
}
}
```
Inside the resource:
- `each.key` is the current element
- `each.value` is the value (same as key for sets)
## Using maps for complex configurations
Maps let you specify different properties per resource:
```terraform
variable "environments" {
description = "Environment configurations"
type = map(object({
location = string
sku = string
}))
default = {
dev = {
location = "uksouth"
sku = "Standard_B2s"
}
staging = {
location = "ukwest"
sku = "Standard_D2s_v5"
}
prod = {
location = "northeurope"
sku = "Standard_D4s_v5"
}
}
}
resource "azurerm_resource_group" "example" {
for_each = var.environments
name = "rg-${each.key}"
location = each.value.location
tags = {
Environment = each.key
}
}
```
## Reference specific instances
Access created resources by their key:
```terraform
resource "azurerm_storage_account" "example" {
name = "stdemo${each.key}"
resource_group_name = azurerm_resource_group.example["prod"].name # Reference prod RG
location = azurerm_resource_group.example["prod"].location
account_tier = "Standard"
account_replication_type = "GRS"
}
```
## Create related resources
```terraform
resource "azurerm_resource_group" "example" {
for_each = toset(["dev", "staging", "prod"])
name = "rg-${each.key}"
location = "uksouth"
}
resource "azurerm_storage_account" "example" {
for_each = azurerm_resource_group.example
name = "st${each.key}${random_string.suffix.result}"
resource_group_name = each.value.name
location = each.value.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
```
## Output all resources
```terraform
output "resource_group_ids" {
description = "Map of environment names to resource group IDs"
value = { for k, rg in azurerm_resource_group.example : k => rg.id }
}
```
## for_each vs count
Use `for_each` when:
- Each instance has a meaningful identifier
- You might add or remove instances
- Resources aren't identical
Use `count` when:
- You need a specific number of identical resources
- Order matters
- Resources are truly identical
Prefer `for_each` in most cases. It handles resource changes better.
## Try it yourself
```bash
cd 11-advanced-for-each/examples/terraform
terraform init
terraform validate
terraform plan
terraform apply
terraform destroy
```
================================================
FILE: 11-advanced-for-each/examples/terraform/main.tf
================================================
resource "azurerm_resource_group" "rg" {
for_each = toset(var.resource_group_names)
name = each.key
location = "uksouth"
tags = {
Environment = each.key
}
}
resource "azurerm_storage_account" "example" {
for_each = azurerm_resource_group.rg
name = replace("st${each.key}${random_string.suffix.result}", "-", "")
resource_group_name = each.value.name
location = each.value.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = each.value.tags
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
================================================
FILE: 11-advanced-for-each/examples/terraform/outputs.tf
================================================
output "resource_group_ids" {
description = "Map of environment names to resource group IDs"
value = { for k, rg in azurerm_resource_group.rg : k => rg.id }
}
output "storage_account_names" {
description = "Map of environment names to storage account names"
value = { for k, sa in azurerm_storage_account.example : k => sa.name }
}
================================================
FILE: 11-advanced-for-each/examples/terraform/providers.tf
================================================
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "foreach"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.8"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 11-advanced-for-each/examples/terraform/variables.tf
================================================
variable "resource_group_names" {
type = list(string)
default = ["dev", "staging", "prod"]
}
================================================
FILE: 12-advanced-count/README.md
================================================
# Create multiple resources with count
The `count` argument creates multiple identical resources. Use it when you need a specific number of instances.
## count basics
```terraform
resource "azurerm_resource_group" "example" {
count = 3
name = "rg-demo-${count.index}"
location = "uksouth"
tags = {
Index = count.index
}
}
```
`count.index` is the current iteration (0, 1, 2).
This creates:
- `rg-demo-0`
- `rg-demo-1`
- `rg-demo-2`
## Use with variables
```terraform
variable "instance_count" {
description = "Number of VMs to create"
type = number
default = 3
}
resource "azurerm_linux_virtual_machine" "example" {
count = var.instance_count
name = "vm-${count.index}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = "Standard_B2s"
# ... other configuration
}
```
## Reference specific instances
Access a specific resource by index:
```terraform
# Reference the first VM
output "first_vm_id" {
value = azurerm_linux_virtual_machine.example[0].id
}
# Reference the last VM
output "last_vm_id" {
value = azurerm_linux_virtual_machine.example[var.instance_count - 1].id
}
```
## Use with lists
```terraform
variable "locations" {
description = "Azure regions"
type = list(string)
default = ["uksouth", "ukwest", "northeurope"]
}
resource "azurerm_resource_group" "example" {
count = length(var.locations)
name = "rg-${var.locations[count.index]}"
location = var.locations[count.index]
}
```
## Output all resources
```terraform
output "resource_group_names" {
description = "Names of all resource groups"
value = azurerm_resource_group.example[*].name
}
output "resource_group_ids" {
description = "IDs of all resource groups"
value = azurerm_resource_group.example[*].id
}
```
The `[*]` splat expression returns all values.
## Conditional count
Use `count` for conditional resource creation:
```terraform
variable "create_backup" {
description = "Whether to create backup resources"
type = bool
default = false
}
resource "azurerm_backup_vault" "example" {
count = var.create_backup ? 1 : 0
name = "bv-demo"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
datastore_type = "VaultStore"
redundancy = "LocallyRedundant"
}
```
When `create_backup` is `false`, Terraform creates zero backup vaults.
## Limitations
**Don't remove items from the middle of a count list.** Terraform identifies resources by index. Removing index 1 causes all subsequent resources to shift:
```terraform
# Before: 3 VMs (0, 1, 2)
count = 3
# After removing one: Terraform destroys VM-2 and recreates VM-1
count = 2
```
Terraform thinks:
- Index 0: no change
- Index 1: changed (was VM-1, now VM-2)
- Index 2: deleted
This destroys your VM-2.
Use `for_each` when you might add or remove instances.
## Try it yourself
```bash
cd 12-advanced-count/examples/terraform
terraform init
terraform validate
terraform plan
terraform apply
terraform destroy
```
================================================
FILE: 12-advanced-count/examples/terraform/main.tf
================================================
resource "azurerm_resource_group" "rg" {
count = 3
name = "rg-demo-${count.index}"
location = "uksouth"
tags = {
Index = count.index
}
}
================================================
FILE: 12-advanced-count/examples/terraform/outputs.tf
================================================
# Reference the first resource group
output "first_rg_id" {
description = "ID of the first resource group"
value = azurerm_resource_group.rg[0].id
}
# Reference the last resource group
output "last_rg_id" {
description = "ID of the last resource group"
value = azurerm_resource_group.rg[2].id
}
# All resource group IDs
output "all_rg_ids" {
description = "List of all resource group IDs"
value = azurerm_resource_group.rg[*].id
}
================================================
FILE: 12-advanced-count/examples/terraform/providers.tf
================================================
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "count"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 12-advanced-count/examples/terraform/variables.tf
================================================
variable "instance_count" {
description = "Number of resource groups to create"
type = number
default = 3
}
================================================
FILE: 13-advanced-conditionals/README.md
================================================
# Use conditional expressions
Conditional expressions let you make decisions in your Terraform configuration. They use the format: `condition ? true_value : false_value`
## Basic conditionals
```terraform
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
resource "azurerm_resource_group" "example" {
name = "rg-demo"
location = var.environment == "prod" ? "northeurope" : "uksouth"
tags = {
Environment = var.environment
CostCenter = var.environment == "prod" ? "Production" : "Development"
}
}
```
Production resources deploy to North Europe. Everything else goes to UK South.
## Conditional resource creation
Create resources only when needed:
```terraform
variable "enable_backup" {
description = "Enable backup vault"
type = bool
default = false
}
resource "azurerm_data_protection_backup_vault" "example" {
count = var.enable_backup ? 1 : 0
name = "bv-demo"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
datastore_type = "VaultStore"
redundancy = "LocallyRedundant"
}
```
When `enable_backup` is `false`, count is 0 and Terraform creates nothing.
## Choose SKUs by environment
```terraform
variable "environment" {
type = string
default = "dev"
}
locals {
# Use smaller SKUs in non-prod
vm_size = var.environment == "prod" ? "Standard_D4s_v5" : "Standard_B2s"
storage_replication = var.environment == "prod" ? "GRS" : "LRS"
database_sku = var.environment == "prod" ? "GP_Gen5_4" : "GP_Gen5_2"
}
resource "azurerm_linux_virtual_machine" "example" {
name = "vm-demo"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = local.vm_size
# ... other configuration
}
```
## Nested conditionals
```terraform
locals {
# Chain conditions for complex logic
vm_size = (
var.environment == "prod" ? "Standard_D4s_v5" :
var.environment == "staging" ? "Standard_D2s_v5" :
"Standard_B2s"
)
}
```
## Conditional properties
Some resources require different configurations per environment:
```terraform
resource "azurerm_storage_account" "example" {
name = "stdemo${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = var.environment == "prod" ? "GRS" : "LRS"
# Enable advanced threat protection in prod
enable_https_traffic_only = true
min_tls_version = var.environment == "prod" ? "TLS1_2" : "TLS1_0"
tags = {
Environment = var.environment
}
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
```
## Validate with conditionals
Use conditionals in validation rules:
```terraform
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_count" {
description = "Number of instances"
type = number
validation {
condition = var.instance_count > 0 && var.instance_count <= 10
error_message = "Instance count must be between 1 and 10."
}
}
```
## Try it yourself
```bash
cd 13-advanced-conditionals/examples/conditional-expressions-example
terraform init
terraform validate
terraform plan
terraform apply -var="environment=prod"
terraform destroy
```
================================================
FILE: 13-advanced-conditionals/examples/conditional-expressions-example/main.tf
================================================
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.environment == "prod" ? "northeurope" : "uksouth"
tags = {
Environment = var.environment
CostCenter = var.environment == "prod" ? "Production" : "Development"
}
}
resource "azurerm_storage_account" "sa" {
name = "st${var.environment}${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = var.environment == "prod" ? "GRS" : "LRS"
https_traffic_only_enabled = true
min_tls_version = var.environment == "prod" ? "TLS1_2" : "TLS1_0"
tags = azurerm_resource_group.rg.tags
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
================================================
FILE: 13-advanced-conditionals/examples/conditional-expressions-example/outputs.tf
================================================
output "resource_group_name" {
description = "Name of the resource group"
value = azurerm_resource_group.rg.name
}
output "resource_group_location" {
description = "Location of the resource group"
value = azurerm_resource_group.rg.location
}
output "storage_account_name" {
description = "Name of the storage account"
value = azurerm_storage_account.sa.name
}
output "storage_replication_type" {
description = "Replication type based on environment"
value = azurerm_storage_account.sa.account_replication_type
}
output "min_tls_version" {
description = "Minimum TLS version based on environment"
value = azurerm_storage_account.sa.min_tls_version
}
================================================
FILE: 13-advanced-conditionals/examples/conditional-expressions-example/providers.tf
================================================
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "conditional"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.8"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 13-advanced-conditionals/examples/conditional-expressions-example/variables.tf
================================================
variable "resource_group_name" {
description = "Name of the resource group"
type = string
default = "rg-demo"
}
variable "environment" {
description = "Environment name"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
================================================
FILE: 14-advanced-dynamic-blocks/README.md
================================================
# Use dynamic blocks
Dynamic blocks generate repeated nested blocks within a resource. Use them when you need multiple similar nested blocks with different values.
## Basic dynamic blocks
Some resources accept repeated nested blocks. Network security groups accept multiple security rules:
```terraform
resource "azurerm_network_security_group" "example" {
name = "nsg-demo"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
security_rule {
name = "allow-ssh"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "allow-https"
priority = 101
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
```
This works but gets repetitive. Use a dynamic block instead:
```terraform
variable "security_rules" {
description = "Security rules for NSG"
type = list(object({
name = string
priority = number
direction = string
access = string
protocol = string
source_port_range = string
destination_port_range = string
source_address_prefix = string
destination_address_prefix = string
}))
default = [
{
name = "allow-ssh"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
},
{
name = "allow-https"
priority = 101
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
]
}
resource "azurerm_network_security_group" "example" {
name = "nsg-demo"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
dynamic "security_rule" {
for_each = var.security_rules
content {
name = security_rule.value.name
priority = security_rule.value.priority
direction = security_rule.value.direction
access = security_rule.value.access
protocol = security_rule.value.protocol
source_port_range = security_rule.value.source_port_range
destination_port_range = security_rule.value.destination_port_range
source_address_prefix = security_rule.value.source_address_prefix
destination_address_prefix = security_rule.value.destination_address_prefix
}
}
}
```
The dynamic block:
- Uses `for_each` to iterate over a collection
- Has a label matching the nested block name (`security_rule`)
- Contains a `content` block defining the nested block structure
- References values with `security_rule.value`
## Simpler example with maps
```terraform
variable "allowed_ports" {
description = "Ports to allow"
type = map(number)
default = {
ssh = 22
http = 80
https = 443
}
}
resource "azurerm_network_security_group" "example" {
name = "nsg-demo"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
dynamic "security" {
for_each = var.allowed_ports
content {
name = "allow-${security_rule.key}"
priority = 100 + security_rule.value
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = security_rule.value
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
}
```
## When to use dynamic blocks
Use dynamic blocks when:
- A resource needs multiple similar nested blocks
- The number of blocks varies based on input
- Configuration comes from variables or data sources
Don't use dynamic blocks when:
- You have a fixed, small number of blocks (explicit blocks are clearer)
- The blocks differ substantially from each other
- It makes the code harder to read
## Try it yourself
```bash
cd 14-advanced-dynamic-blocks/examples/terraform
terraform init
terraform validate
terraform plan
terraform apply
terraform destroy
```
================================================
FILE: 14-advanced-dynamic-blocks/examples/terraform/main.tf
================================================
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = "uksouth"
}
resource "azurerm_network_security_group" "nsg" {
name = "nsg-demo"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
dynamic "security_rule" {
for_each = var.security_rules
content {
name = security_rule.value.name
priority = security_rule.value.priority
direction = security_rule.value.direction
access = security_rule.value.access
protocol = security_rule.value.protocol
source_port_range = security_rule.value.source_port_range
destination_port_range = security_rule.value.destination_port_range
source_address_prefix = security_rule.value.source_address_prefix
destination_address_prefix = security_rule.value.destination_address_prefix
}
}
}
================================================
FILE: 14-advanced-dynamic-blocks/examples/terraform/outputs.tf
================================================
output "resource_group_name" {
description = "Name of the resource group"
value = azurerm_resource_group.rg.name
}
output "nsg_id" {
description = "ID of the network security group"
value = azurerm_network_security_group.nsg.id
}
output "security_rules_count" {
description = "Number of security rules created"
value = length(var.security_rules)
}
================================================
FILE: 14-advanced-dynamic-blocks/examples/terraform/providers.tf
================================================
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "dynamicblock"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 14-advanced-dynamic-blocks/examples/terraform/variables.tf
================================================
variable "resource_group_name" {
description = "Name of the resource group"
type = string
default = "rg-demo"
}
variable "security_rules" {
description = "Security rules for NSG"
type = list(object({
name = string
priority = number
direction = string
access = string
protocol = string
source_port_range = string
destination_port_range = string
source_address_prefix = string
destination_address_prefix = string
}))
default = [
{
name = "allow-ssh"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
},
{
name = "allow-https"
priority = 101
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
]
}
================================================
FILE: 15-secret-management/README.md
================================================
# Manage secrets with Azure Key Vault
Store sensitive values in Azure Key Vault instead of putting them directly in your Terraform code. This keeps secrets secure and auditable.
## Why use Key Vault
Don't hardcode secrets:
```terraform
# DON'T DO THIS
resource "azurerm_linux_virtual_machine" "example" {
admin_password = "SuperSecret123!" # Exposed in code and state
}
```
Use Key Vault instead. Secrets stay encrypted, access is logged, and you can rotate values without changing code.
## Create a Key Vault
```terraform
data "azurerm_client_config" "current" {}
resource "azurerm_resource_group" "example" {
name = "rg-keyvault-demo"
location = "uksouth"
}
resource "azurerm_key_vault" "example" {
name = "kv-demo-${random_string.suffix.result}"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
purge_protection_enabled = true
soft_delete_retention_days = 7
# Use RBAC for access control (recommended)
enable_rbac_authorization = true
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
```
## Grant yourself access
Assign the Key Vault Secrets Officer role:
```terraform
resource "azurerm_role_assignment" "example" {
scope = azurerm_key_vault.example.id
role_definition_name = "Key Vault Secrets Officer"
principal_id = data.azurerm_client_config.current.object_id
}
```
## Store a secret
```terraform
resource "azurerm_key_vault_secret" "db_password" {
name = "database-password"
value = random_password.db_password.result
key_vault_id = azurerm_key_vault.example.id
depends_on = [azurerm_role_assignment.example]
}
resource "random_password" "db_password" {
length = 32
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
```
## Read a secret
```terraform
data "azurerm_key_vault_secret" "db_password" {
name = "database-password"
key_vault_id = azurerm_key_vault.example.id
depends_on = [azurerm_key_vault_secret.db_password]
}
resource "azurerm_mysql_flexible_server" "example" {
name = "mysql-demo"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
administrator_login = "adminuser"
administrator_password = data.azurerm_key_vault_secret.db_password.value
sku_name = "B_Standard_B1s"
version = "8.0.21"
}
```
## Use existing secrets
Reference secrets from an existing Key Vault:
```terraform
data "azurerm_key_vault" "existing" {
name = "kv-prod"
resource_group_name = "rg-shared"
}
data "azurerm_key_vault_secret" "api_key" {
name = "api-key"
key_vault_id = data.azurerm_key_vault.existing.id
}
resource "azurerm_app_configuration_key" "example" {
configuration_store_id = azurerm_app_configuration.example.id
key = "ApiKey"
value = data.azurerm_key_vault_secret.api_key.value
}
```
## Mark outputs as sensitive
Prevent secrets from appearing in logs:
```terraform
output "database_password" {
value = data.azurerm_key_vault_secret.db_password.value
sensitive = true
}
```
Terraform hides the value in output:
```
Outputs:
database_password = <sensitive>
```
## Store certificates
```terraform
resource "azurerm_key_vault_certificate" "example" {
name = "app-cert"
key_vault_id = azurerm_key_vault.example.id
certificate_policy {
issuer_parameters {
name = "Self"
}
key_properties {
exportable = true
key_size = 2048
key_type = "RSA"
reuse_key = true
}
secret_properties {
content_type = "application/x-pkcs12"
}
x509_certificate_properties {
key_usage = [
"cRLSign",
"dataEncipherment",
"digitalSignature",
"keyAgreement",
"keyCertSign",
"keyEncipherment",
]
subject = "CN=example.com"
validity_in_months = 12
}
}
depends_on = [azurerm_role_assignment.example]
}
```
## Try it yourself
```bash
cd 15-secret-management/examples/terraform
terraform init
terraform validate
terraform plan
terraform apply
terraform destroy
```
## Best practices
- Enable purge protection in production
- Use RBAC instead of access policies
- Store Terraform state remotely (see section 3)
- Mark secret variables as sensitive
- Rotate secrets regularly
- Audit Key Vault access logs
================================================
FILE: 15-secret-management/examples/terraform/main.tf
================================================
data "azurerm_client_config" "current" {}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = "uksouth"
}
resource "azurerm_key_vault" "kv" {
name = "kv-demo-${random_string.suffix.result}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
purge_protection_enabled = true
soft_delete_retention_days = 7
# Use RBAC for access control (recommended in azurerm 4.0+)
rbac_authorization_enabled = true
}
# Assign Key Vault Secrets Officer role
resource "azurerm_role_assignment" "kv_secrets_officer" {
scope = azurerm_key_vault.kv.id
role_definition_name = "Key Vault Secrets Officer"
principal_id = data.azurerm_client_config.current.object_id
}
resource "azurerm_key_vault_secret" "sa" {
name = "saname"
value = "stdemostorage"
key_vault_id = azurerm_key_vault.kv.id
depends_on = [azurerm_role_assignment.kv_secrets_officer]
}
data "azurerm_key_vault_secret" "sa" {
name = "saname"
key_vault_id = azurerm_key_vault.kv.id
depends_on = [azurerm_key_vault_secret.sa]
}
resource "azurerm_storage_account" "sa" {
name = data.azurerm_key_vault_secret.sa.value
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
depends_on = [
azurerm_resource_group.rg
]
}
================================================
FILE: 15-secret-management/examples/terraform/providers.tf
================================================
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "keyvault"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.8"
}
}
}
provider "azurerm" {
features {}
}
data "azurerm_client_config" "current" {}
================================================
FILE: 15-secret-management/examples/terraform/variables.tf
================================================
variable "resource_group_name" {
type = string
default = "rg-demo-keyvault"
}
================================================
FILE: 16-modules/README.md
================================================
# Build reusable modules
Modules package Terraform configurations into reusable components. Instead of copying resources, you create a module once and use it everywhere.
## Why use modules
You'll deploy similar infrastructure repeatedly:\n- Multiple environments (dev, staging, prod)\n- Multiple projects with common patterns\n- Standard resource configurations\n\nModules eliminate duplication and enforce standards.
## Module structure
A module is a directory with Terraform files:
```
modules/
└── storage-account/
├── main.tf
├── variables.tf
└── outputs.tf
```
## Create a module
Create `modules/storage-account/main.tf`:
```terraform
resource "azurerm_storage_account" "this" {
name = var.name
resource_group_name = var.resource_group_name
location = var.location
account_tier = var.account_tier
account_replication_type = var.replication_type
enable_https_traffic_only = true
min_tls_version = "TLS1_2"
tags = var.tags
}
```
Create `modules/storage-account/variables.tf`:
```terraform
variable "name" {
description = "Storage account name"
type = string
}
variable "resource_group_name" {
description = "Resource group name"
type = string
}
variable "location" {
description = "Azure region"
type = string
}
variable "account_tier" {
description = "Storage account tier"
type = string
default = "Standard"
}
variable "replication_type" {
description = "Replication type"
type = string
default = "LRS"
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}
```
Create `modules/storage-account/outputs.tf`:
```terraform
output "id" {
description = "Storage account ID"
value = azurerm_storage_account.this.id
}
output "name" {
description = "Storage account name"
value = azurerm_storage_account.this.name
}
output "primary_blob_endpoint" {
description = "Primary blob endpoint"
value = azurerm_storage_account.this.primary_blob_endpoint
}
```
## Use the module
In your root `main.tf`:
```terraform
resource "azurerm_resource_group" "example" {
name = "rg-demo"
location = "uksouth"
}
module "storage_dev" {
source = "./modules/storage-account"
name = "stdev${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
replication_type = "LRS"
tags = {
Environment = "Development"
}
}
module "storage_prod" {
source = "./modules/storage-account"
name = "stprod${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
replication_type = "GRS" # Geo-redundant for production
tags = {
Environment = "Production"
}
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
```
## Access module outputs
```terraform
output "dev_storage_id" {
value = module.storage_dev.id
}
output "prod_storage_endpoint" {
value = module.storage_prod.primary_blob_endpoint
}
```
## Use public modules
The Terraform Registry has thousands of public modules:
```terraform
module "network" {
source = "Azure/vnet/azurerm"
version = "4.1.0"
resource_group_name = azurerm_resource_group.example.name
vnet_location = azurerm_resource_group.example.location
vnet_name = "vnet-demo"
address_space = ["10.0.0.0/16"]
subnet_names = ["subnet-web", "subnet-data"]
subnet_prefixes = ["10.0.1.0/24", "10.0.2.0/24"]
}
```
Always specify a version to avoid unexpected changes.
## Build a complete module
Here's a more realistic module for an Azure Container Registry:
`modules/acr/main.tf`:
```terraform
resource "azurerm_container_registry" "this" {
name = var.name
resource_group_name = var.resource_group_name
location = var.location
sku = var.sku
admin_enabled = var.admin_enabled
dynamic "identity" {
for_each = var.identity_enabled ? [1] : []
content {
type = "SystemAssigned"
}
}
tags = var.tags
}
resource "azurerm_role_assignment" "acr_pull" {
count = length(var.pull_role_principal_ids)
scope = azurerm_container_registry.this.id
role_definition_name = "AcrPull"
principal_id = var.pull_role_principal_ids[count.index]
}
```
`modules/acr/variables.tf`:
```terraform
variable "name" {
description = "ACR name"
type = string
}
variable "resource_group_name" {
description = "Resource group name"
type = string
}
variable "location" {
description = "Azure region"
type = string
}
variable "sku" {
description = "ACR SKU"
type = string
default = "Basic"
validation {
condition = contains(["Basic", "Standard", "Premium"], var.sku)
error_message = "SKU must be Basic, Standard, or Premium."
}
}
variable "admin_enabled" {
description = "Enable admin user"
type = bool
default = false
}
variable "identity_enabled" {
description = "Enable managed identity"
type = bool
default = false
}
variable "pull_role_principal_ids" {
description = "Principal IDs to grant AcrPull role"
type = list(string)
default = []
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}
```
`modules/acr/outputs.tf`:
```terraform
output "id" {
description = "ACR ID"
value = azurerm_container_registry.this.id
}
output "name" {
description = "ACR name"
value = azurerm_container_registry.this.name
}
output "login_server" {
description = "ACR login server"
value = azurerm_container_registry.this.login_server
}
output "admin_username" {
description = "ACR admin username"
value = azurerm_container_registry.this.admin_username
sensitive = true
}
output "admin_password" {
description = "ACR admin password"
value = azurerm_container_registry.this.admin_password
sensitive = true
}
```
## Try it yourself
```bash
cd 16-modules/examples/terraform
terraform init
terraform validate
terraform plan
terraform apply
terraform destroy
```
## Module best practices
- Keep modules focused on one purpose
- Use meaningful variable names and descriptions
- Add validation rules to variables
- Document all inputs and outputs
- Version your modules when sharing
- Test modules before using in production
- Don't over-abstract. Simple is better
================================================
FILE: 16-modules/examples/terraform/main.tf
================================================
module "acr" {
source = "./modules/acr"
resource_group_name = "rg-demo-modules"
location = "UK South"
acr_name = "acrdemo"
acr_sku = "Standard"
acr_admin_enabled = true
}
================================================
FILE: 16-modules/examples/terraform/modules/acr/main.tf
================================================
# Resource Group
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
# Azure Container Registry
resource "azurerm_container_registry" "acr" {
name = var.acr_name
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku = var.acr_sku
admin_enabled = var.acr_admin_enabled
}
================================================
FILE: 16-modules/examples/terraform/modules/acr/output.tf
================================================
output "acr_id" {
value = azurerm_container_registry.acr.id
}
================================================
FILE: 16-modules/examples/terraform/modules/acr/variables.tf
================================================
variable "resource_group_name" {
type = string
description = "The name of the resource group in which to create the container registry."
}
variable "location" {
type = string
description = "The Azure location where the container registry should exist."
}
variable "acr_name" {
type = string
description = "The name of the container registry."
}
variable "acr_sku" {
type = string
description = "The SKU of the container registry."
}
variable "acr_admin_enabled" {
type = bool
description = "Should the admin user be enabled?"
}
================================================
FILE: 16-modules/examples/terraform/providers.tf
================================================
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "acr"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 17-azapi/README.md
================================================
# Use the AzAPI provider
The AzAPI provider lets you use any Azure resource type on day one, even before the AzureRM provider adds support.
## Why use AzAPI
Azure releases new services constantly. The AzureRM provider lags behind because each resource needs coding, testing, and documentation. AzAPI works immediately because it calls Azure Resource Manager APIs directly.
Use AzAPI when:
- You need a brand new Azure service
- AzureRM doesn't support a specific property
- You need a preview API version
## Configure the provider
Add the AzAPI provider to `providers.tf`:
```terraform
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
azapi = {
source = "azure/azapi"
version = "~> 2.1"
}
}
}
provider "azurerm" {
features {}
}
provider "azapi" {}
```
## Create a resource
AzAPI uses JSON for resource configuration:
```terraform
resource "azurerm_resource_group" "example" {
name = "rg-azapi-demo"
location = "uksouth"
}
resource "azapi_resource" "acr" {
type = "Microsoft.ContainerRegistry/registries@2023-07-01"
name = "acrdemo${random_string.suffix.result}"
parent_id = azurerm_resource_group.example.id
location = azurerm_resource_group.example.location
body = {
sku = {
name = "Standard"
}
properties = {
adminUserEnabled = false
publicNetworkAccess = "Enabled"
}
}
tags = {
Environment = "Demo"
}
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
```
The `type` field specifies:
- Resource type: `Microsoft.ContainerRegistry/registries`
- API version: `@2023-07-01`
## Find API versions
Check Azure documentation for resource types and API versions:
- [Azure Resource Manager REST API](https://learn.microsoft.com/en-us/rest/api/azure/)
- Use `az provider show` to list API versions:
```bash
az provider show --namespace Microsoft.ContainerRegistry --query "resourceTypes[?resourceType=='registries'].apiVersions"
```
## Mix AzAPI with AzureRM
Use AzureRM for stable resources and AzAPI for new features:
```terraform
resource "azurerm_resource_group" "example" {
name = "rg-mixed-demo"
location = "uksouth"
}
# Use AzureRM for VNet (stable)
resource "azurerm_virtual_network" "example" {
name = "vnet-demo"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
address_space = ["10.0.0.0/16"]
}
# Use AzAPI for preview features
resource "azapi_resource" "bastion" {
type = "Microsoft.Network/bastionHosts@2024-01-01"
name = "bastion-demo"
parent_id = azurerm_resource_group.example.id
location = azurerm_resource_group.example.location
body = {
properties = {
dnsName = "bastion-demo"
ipConfigurations = [
{
name = "ipconfig1"
properties = {
subnet = {
id = "${azurerm_virtual_network.example.id}/subnets/AzureBastionSubnet"
}
publicIPAddress = {
id = azurerm_public_ip.bastion.id
}
}
}
]
# New property only in preview API
enableTunneling = true
}
sku = {
name = "Standard"
}
}
}
resource "azurerm_subnet" "bastion" {
name = "AzureBastionSubnet"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/26"]
}
resource "azurerm_public_ip" "bastion" {
name = "pip-bastion"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
allocation_method = "Static"
sku = "Standard"
}
```
## Update existing resources
Use `azapi_update_resource` to patch resources:
```terraform
resource "azurerm_storage_account" "example" {
name = "stdemo${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
}
# Add properties not in AzureRM yet
resource "azapi_update_resource" "storage_advanced" {
type = "Microsoft.Storage/storageAccounts@2023-01-01"
resource_id = azurerm_storage_account.example.id
body = {
properties = {
dnsEndpointType = "Standard"
publicNetworkAccess = "Enabled"
}
}
}
```
## Read data sources
```terraform
data "azapi_resource" "existing_acr" {
type = "Microsoft.ContainerRegistry/registries@2023-07-01"
name = "existing-acr"
parent_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/resourceGroups/rg-existing"
}
output "acr_login_server" {
value = jsondecode(data.azapi_resource.existing_acr.output).properties.loginServer
}
```
## When to use AzAPI
**Use AzAPI for:**
- Brand new Azure services
- Preview API versions
- Properties missing from AzureRM
- Custom resource types
**Use AzureRM for:**
- Stable, mature resources
- Better validation and error messages
- Type safety
- Provider documentation
Start with AzureRM. Switch to AzAPI only when needed.
## Try it yourself
```bash
cd 17-azapi/examples/terraform
terraform init
terraform validate
terraform plan
terraform apply
terraform destroy
```
## Finding resource schemas
Use Azure's ARM template reference:
1. Visit [Azure Template Reference](https://learn.microsoft.com/en-us/azure/templates/)
2. Find your resource type
3. Copy the JSON schema
4. Convert to Terraform's HCL syntax
Or use the Azure CLI:
```bash
az resource show --ids /subscriptions/.../resourceGroups/rg/providers/Microsoft.ContainerRegistry/registries/myacr
```
The output shows the exact JSON structure AzAPI expects.
================================================
FILE: 17-azapi/examples/terraform/main.tf
================================================
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
resource "azapi_resource" "acr" {
location = azurerm_resource_group.rg.location
type = "Microsoft.ContainerRegistry/registries@2023-07-01"
name = var.acr_name
parent_id = azurerm_resource_group.rg.id
body = jsonencode({
sku = {
name = "Standard"
}
properties = {
adminUserEnabled = true
}
})
}
================================================
FILE: 17-azapi/examples/terraform/providers.tf
================================================
provider "azapi" {
}
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "YOUR_STORAGE_ACCOUNT_NAME" # Replace with the storage account name created in lesson 9
container_name = "azapi"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
azapi = {
source = "azure/azapi"
version = "~> 2.8"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 17-azapi/examples/terraform/variables.tf
================================================
variable "resource_group_name" {
type = string
description = "The name of the resource group in which to create the container registry."
default = "rg-demo-azapi"
}
variable "location" {
type = string
description = "The Azure location where the container registry should exist."
default = "uksouth"
}
variable "acr_name" {
type = string
description = "The name of the container registry."
default = "acrdemoazapi"
}
================================================
FILE: 18-testing/README.md
================================================
# Test your Terraform code
Testing catches errors before they reach Azure. Use multiple testing approaches to validate syntax, configuration, and infrastructure behavior.
## Terraform native testing (Terraform 1.6+)
Terraform includes a built-in test framework. Write tests in `.tftest.hcl` files to validate your configurations.
### Create a test file
Create `main.tftest.hcl` in your configuration directory:
```hcl
# Test that dev environment uses correct settings
run "test_dev_environment" {
command = plan
variables {
environment = "dev"
}
assert {
condition = azurerm_storage_account.example.account_tier == "Standard"
error_message = "Dev should use Standard tier"
}
}
```
### Run tests
```bash
terraform test
```
Output:
```
main.tftest.hcl... in progress
run "test_dev_environment"... pass
main.tftest.hcl... pass in 2.1s
Success! 1 passed, 0 failed.
```
### Test commands
Tests support two commands:
`plan`: Runs `terraform plan` without deploying:
```hcl
run "test_configuration" {
command = plan
# Tests planned changes
}
```
`apply`: Actually deploys infrastructure (integration test):
```hcl
run "test_deployment" {
command = apply
# Tests real Azure resources
}
```
### Write assertions
Check resource properties:
```hcl
run "test_security" {
command = plan
assert {
condition = azurerm_storage_account.example.enable_https_traffic_only == true
error_message = "HTTPS traffic must be enforced"
}
assert {
condition = azurerm_storage_account.example.min_tls_version == "TLS1_2"
error_message = "Minimum TLS version should be 1.2"
}
}
```
### Test with variables
Pass different values to test behavior:
```hcl
run "test_prod_sku" {
command = plan
variables {
environment = "prod"
}
assert {
condition = azurerm_storage_account.example.account_replication_type == "GRS"
error_message = "Prod should use geo-redundant storage"
}
}
```
### Test validation rules
Verify that invalid inputs fail:
```hcl
run "test_invalid_environment" {
command = plan
variables {
environment = "invalid"
}
expect_failures = [
var.environment
]
}
```
### Complete Azure example
Working test file for Azure resources (see `examples/main.tftest.hcl`):
```hcl
run "test_azure_storage_security" {
command = plan
variables {
resource_group_name = "rg-test"
storage_account_name = "sttest"
environment = "dev"
}
# Verify HTTPS is enforced
assert {
condition = azurerm_storage_account.test.enable_https_traffic_only == true
error_message = "Storage account must enforce HTTPS"
}
# Verify TLS version
assert {
condition = azurerm_storage_account.test.min_tls_version == "TLS1_2"
error_message = "Storage account must use TLS 1.2 or higher"
}
# Verify tags are applied
assert {
condition = azurerm_storage_account.test.tags["ManagedBy"] == "Terraform"
error_message = "Resources must be tagged with ManagedBy"
}
}
# Test different environments
run "test_dev_uses_standard_tier" {
command = plan
variables {
environment = "dev"
}
assert {
condition = azurerm_storage_account.test.account_tier == "Standard"
error_message = "Dev environment should use Standard tier"
}
}
run "test_prod_uses_premium_tier" {
command = plan
variables {
environment = "prod"
}
assert {
condition = azurerm_storage_account.test.account_tier == "Premium"
error_message = "Prod environment should use Premium tier"
}
}
```
### Integration tests
Test actual deployments:
```hcl
run "test_real_deployment" {
command = apply
variables {
resource_group_name = "rg-integration-test"
storage_account_name = "stintegration"
environment = "dev"
}
assert {
condition = output.resource_group_id != ""
error_message = "Resource group should be created"
}
assert {
condition = length(output.storage_account_name) >= 3
error_message = "Storage account name should be valid"
}
}
```
Integration tests create real resources. Terraform destroys them automatically after the test.
### Run specific tests
Run one test file:
```bash
terraform test -filter=main.tftest.hcl
```
Run tests matching a pattern:
```bash
terraform test -filter="test_prod*"
```
Verbose output:
```bash
terraform test -verbose
```
### Organize tests
Group related tests in separate files:
```
tests/
├── security.tftest.hcl
├── networking.tftest.hcl
└── environments.tftest.hcl
```
Terraform runs all `.tftest.hcl` files in your directory.
### Try the examples
```bash
cd 18-testing/examples
terraform init
terraform test
```
The example includes tests for:
- Environment-specific SKUs
- Security settings
- Resource tags
- Validation rules
- Real deployments
## Level 1: Syntax validation
### terraform validate
The fastest check. Validates syntax and internal consistency:
```bash
terraform validate
```
Catches:
- Syntax errors
- Invalid resource references
- Missing required arguments
- Type mismatches
Run this before every commit.
### terraform fmt
Formats code to Terraform style standards:
```bash
terraform fmt -check
```
The `-check` flag returns an error if files need formatting. Use this in CI/CD pipelines.
Auto-fix formatting:
```bash
terraform fmt -recursive
```
## Level 2: Static analysis with tflint
tflint catches issues validate misses:
- Deprecated syntax
- Provider-specific errors
- Best practice violations
- Potential runtime errors
### Install tflint
```bash
# macOS
brew install tflint
# Windows
choco install tflint
# Linux
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
```
### Configure tflint
Create `.tflint.hcl`:
```hcl
plugin "azurerm" {
enabled = true
version = "0.27.0"
source = "github.com/terraform-linters/tflint-ruleset-azurerm"
}
rule "terraform_deprecated_syntax" {
enabled = true
}
rule "terraform_unused_declarations" {
enabled = true
}
rule "terraform_typed_variables" {
enabled = true
}
rule "terraform_naming_convention" {
enabled = true
format = "snake_case"
}
```
### Run tflint
Initialize plugins:
```bash
tflint --init
```
Run checks:
```bash
tflint
```
### Azure-specific rules
The AzureRM ruleset checks:
- Invalid VM sizes
- Incorrect SKU names
- Deprecated resource properties
- Azure naming constraints
Example output:
```
3 issue(s) found:
Warning: "Standard_A0" is deprecated VM size (azurerm_linux_virtual_machine)
on main.tf line 45:
45: size = "Standard_A0"
Error: Storage account name must be between 3 and 24 characters
on main.tf line 12:
12: name = "my-storage-account-that-is-too-long"
```
## Level 3: Cost estimation
### terraform plan with costs
Preview costs before deploying:
```bash
terraform plan -out=plan.tfplan
```
Use Azure Cost Management or third-party tools like Infracost to estimate costs from the plan file.
### Infracost
Install:
```bash
# macOS
brew install infracost
# Linux/Windows - see https://www.infracost.io/docs/
```
Check costs:
```bash
# Generate plan
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > plan.json
# Get cost estimate
infracost breakdown --path plan.json
```
Output shows estimated monthly costs for each resource.
## Level 4: Integration testing
### Test in isolated environments
Deploy to a test environment before production:
```terraform
# dev.tfvars
environment = "dev"
instance_count = 1
sku = "Basic"
# prod.tfvars
environment = "prod"
instance_count = 3
sku = "Standard"
```
Test workflow:
```bash
# Deploy to dev
terraform workspace select dev
terraform apply -var-file="dev.tfvars"
# Run application tests
# ...
# Destroy dev
terraform destroy -var-file="dev.tfvars"
# Deploy to prod
terraform workspace select prod
terraform apply -var-file="prod.tfvars"
```
## Level 5: Automated testing with Terratest
Terratest writes Go tests that deploy infrastructure, validate it, and clean up.
### Install Go
```bash
# macOS
brew install go
# Others - see https://go.dev/doc/install
```
### Create a test
Create `test/terraform_azure_example_test.go`:
```go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/azure"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestTerraformAzureExample(t *testing.T) {
t.Parallel()
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../",
Vars: map[string]interface{}{
"resource_group_name": "rg-terratest",
"location": "uksouth",
},
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
exists := azure.ResourceGroupExists(t, resourceGroupName, "")
assert.True(t, exists, "Resource group should exist")
}
```
### Run tests
```bash
cd test
go mod init test
go mod tidy
go test -v -timeout 30m
```
Tests:
1. Initialize Terraform
2. Apply configuration
3. Validate resources exist
4. Run custom checks
5. Destroy infrastructure
## CI/CD pipeline example
GitHub Actions workflow:
```yaml
name: Terraform Tests
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Setup TFLint
uses: terraform-linters/setup-tflint@v4
- name: TFLint
run: |
tflint --init
tflint --format compact
```
## Testing checklist
Before deploying:
- [ ] `terraform fmt` passes
- [ ] `terraform validate` succeeds
- [ ] `tflint` shows no errors
- [ ] `terraform plan` output reviewed
- [ ] Costs estimated (if using paid resources)
- [ ] Tested in dev environment
- [ ] Automated tests pass (if using Terratest)
## Best practices
- Run `terraform validate` and `terraform fmt` on every file save.
- Include checks in pre-commit hooks and CI/CD.
- Use dev environments that mirror prod configuration.
- Make sure `terraform destroy` works cleanly.
- Run `terraform apply` twice; the second run should show no changes.
## Next steps
Continue to [lesson 19: Import](../19-import/) to bring existing Azure resources under Terraform management.
================================================
FILE: 18-testing/examples/main.tf
================================================
resource "azurerm_resource_group" "test" {
name = var.resource_group_name
location = var.location
tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
}
resource "azurerm_storage_account" "test" {
name = "${var.storage_account_name}${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
account_tier = var.environment == "prod" ? "Premium" : "Standard"
account_replication_type = var.environment == "prod" ? "GRS" : "LRS"
https_traffic_only_enabled = true
min_tls_version = "TLS1_2"
tags = azurerm_resource_group.test.tags
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
================================================
FILE: 18-testing/examples/main.tftest.hcl
================================================
# Test that dev environment uses correct SKUs
run "test_dev_environment" {
command = plan
variables {
resource_group_name = "rg-test-dev"
location = "uksouth"
storage_account_name = "stdev"
environment = "dev"
}
assert {
condition = azurerm_storage_account.test.account_tier == "Standard"
error_message = "Dev environment should use Standard tier"
}
assert {
condition = azurerm_storage_account.test.account_replication_type == "LRS"
error_message = "Dev environment should use LRS replication"
}
assert {
condition = azurerm_resource_group.test.location == "uksouth"
error_message = "Resource group should be in uksouth"
}
}
# Test that prod environment uses premium features
run "test_prod_environment" {
command = plan
variables {
resource_group_name = "rg-test-prod"
location = "uksouth"
storage_account_name = "stprod"
environment = "prod"
}
assert {
condition = azurerm_storage_account.test.account_tier == "Premium"
error_message = "Prod environment should use Premium tier"
}
assert {
condition = azurerm_storage_account.test.account_replication_type == "GRS"
error_message = "Prod environment should use GRS replication"
}
}
# Test that security settings are enabled
run "test_security_settings" {
command = plan
variables {
resource_group_name = "rg-test-security"
location = "uksouth"
storage_account_name = "stsec"
environment = "dev"
}
assert {
condition = azurerm_storage_account.test.https_traffic_only_enabled == true
error_message = "HTTPS traffic should be enforced"
}
assert {
condition = azurerm_storage_account.test.min_tls_version == "TLS1_2"
error_message = "Minimum TLS version should be 1.2"
}
}
# Test that tags are applied correctly
run "test_resource_tags" {
command = plan
variables {
resource_group_name = "rg-test-tags"
location = "uksouth"
storage_account_name = "sttags"
environment = "staging"
}
assert {
condition = azurerm_resource_group.test.tags["Environment"] == "staging"
error_message = "Environment tag should match variable"
}
assert {
condition = azurerm_resource_group.test.tags["ManagedBy"] == "Terraform"
error_message = "ManagedBy tag should be set to Terraform"
}
assert {
condition = azurerm_storage_account.test.tags["Environment"] == "staging"
error_message = "Storage account should inherit resource group tags"
}
}
# Test validation rules
run "test_invalid_environment" {
command = plan
variables {
resource_group_name = "rg-test-invalid"
location = "uksouth"
storage_account_name = "stinvalid"
environment = "test" # Invalid value
}
expect_failures = [
var.environment
]
}
# Integration test with actual deployment
run "test_deployment" {
command = apply
variables {
resource_group_name = "rg-terratest-${run.suffix}"
location = "uksouth"
storage_account_name = "sttest"
environment = "dev"
}
assert {
condition = output.resource_group_name == "rg-terratest-${run.suffix}"
error_message = "Resource group name should match expected value"
}
assert {
condition = length(output.storage_account_name) > 3
error_message = "Storage account name should be at least 3 characters"
}
assert {
condition = output.storage_account_tier == "Standard"
error_message = "Storage tier should be Standard for dev"
}
}
================================================
FILE: 18-testing/examples/outputs.tf
================================================
output "resource_group_name" {
description = "Name of the resource group"
value = azurerm_resource_group.test.name
}
output "resource_group_id" {
description = "ID of the resource group"
value = azurerm_resource_group.test.id
}
output "storage_account_name" {
description = "Name of the storage account"
value = azurerm_storage_account.test.name
}
output "storage_account_tier" {
description = "Storage account tier"
value = azurerm_storage_account.test.account_tier
}
output "storage_replication_type" {
description = "Storage replication type"
value = azurerm_storage_account.test.account_replication_type
}
================================================
FILE: 18-testing/examples/providers.tf
================================================
terraform {
required_version = ">= 1.6"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.8"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 18-testing/examples/variables.tf
================================================
variable "resource_group_name" {
description = "Name of the resource group"
type = string
default = "rg-test"
}
variable "location" {
description = "Azure region"
type = string
default = "uksouth"
}
variable "storage_account_name" {
description = "Storage account name"
type = string
default = "sttest"
}
variable "environment" {
description = "Environment name"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
================================================
FILE: 19-import/README.md
================================================
# Import existing Azure resources
You can bring existing Azure resources under Terraform management without recreating them. This lets you adopt Terraform gradually.
## Why import resources
Common scenarios:
- Migrating manually created resources to Terraform
- Adopting Terraform in an existing Azure environment
- Managing resources created by other tools
- Recovering from state file loss
## Import basics
Importing requires two steps:
1. Write the resource configuration in Terraform
2. Run `terraform import` to link the resource to your state
## Import a resource group
### 1. Find the resource ID
```bash
az group show --name rg-existing --query id --output tsv
```
Output:
```
/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-existing
```
### 2. Write the configuration
Create `main.tf`:
```terraform
resource "azurerm_resource_group" "imported" {
name = "rg-existing"
location = "uksouth"
}
```
### 3. Import the resource
```bash
terraform import azurerm_resource_group.imported /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-existing
```
Terraform adds the resource to state without modifying Azure.
### 4. Verify the import
```bash
terraform plan
```
If the plan shows no changes, your configuration matches the existing resource. If it shows changes, update your configuration to match:
```bash
terraform show
```
This displays the current state. Copy values to your configuration.
## Import a storage account
### 1. Get the resource ID
```bash
az storage account show --name stexisting --resource-group rg-existing --query id --output tsv
```
### 2. Write the configuration
```terraform
resource "azurerm_storage_account" "imported" {
name = "stexisting"
resource_group_name = "rg-existing"
location = "uksouth"
account_tier = "Standard"
account_replication_type = "LRS"
}
```
### 3. Import
```bash
terraform import azurerm_storage_account.imported /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-existing/providers/Microsoft.Storage/storageAccounts/stexisting
```
### 4. Match configuration
Run `terraform plan`. Terraform shows differences:
```
~ account_replication_type = "GRS" -> "LRS"
+ enable_https_traffic_only = true
+ min_tls_version = "TLS1_2"
```
Update your configuration to match the actual resource:
```terraform
resource "azurerm_storage_account" "imported" {
name = "stexisting"
resource_group_name = "rg-existing"
location = "uksouth"
account_tier = "Standard"
account_replication_type = "GRS" # Match actual value
enable_https_traffic_only = true
min_tls_version = "TLS1_2"
}
```
Run `terraform plan` again. It should show no changes.
## Import blocks (Terraform 1.5+)
Terraform 1.5 introduced import blocks that generate configuration automatically.
### 1. Write an import block
Create `imports.tf`:
```terraform
import {
to = azurerm_resource_group.imported
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-existing"
}
```
### 2. Generate configuration
```bash
terraform plan -generate-config-out=generated.tf
```
Terraform creates `generated.tf` with the resource configuration:
```terraform
resource "azurerm_resource_group" "imported" {
location = "uksouth"
name = "rg-existing"
tags = {}
}
```
### 3. Apply the import
```bash
terraform apply
```
Terraform imports the resource and updates state.
### 4. Clean up
Move the generated configuration to your main files and delete `imports.tf`.
## Import multiple resources
Create multiple import blocks:
```terraform
import {
to = azurerm_resource_group.app
id = "/subscriptions/.../resourceGroups/rg-app"
}
import {
to = azurerm_storage_account.data
id = "/subscriptions/.../resourceGroups/rg-app/providers/Microsoft.Storage/storageAccounts/stdata"
}
import {
to = azurerm_key_vault.secrets
id = "/subscriptions/.../resourceGroups/rg-app/providers/Microsoft.KeyVault/vaults/kv-secrets"
}
```
Generate all configurations:
```bash
terraform plan -generate-config-out=imported.tf
```
## Find resource IDs
### Azure CLI
```bash
# Resource group
az group show --name <name> --query id -o tsv
# Storage account
az storage account show --name <name> --resource-group <rg> --query id -o tsv
# Key Vault
az keyvault show --name <name> --query id -o tsv
# Virtual network
az network vnet show --name <name> --resource-group <rg> --query id -o tsv
# Any resource (if you know the type)
az resource show --name <name> --resource-group <rg> --resource-type <type> --query id -o tsv
```
### Azure Portal
1. Navigate to the resource
2. Click "JSON View" in the Overview
3. Copy the "Resource ID" field
### Resource ID format
Azure resource IDs follow this pattern:
```
/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/{provider}/{type}/{name}
```
Example:
```
/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-prod/providers/Microsoft.Network/virtualNetworks/vnet-prod
```
## Import child resources
Some resources have child resources:
```terraform
# Import VNet
terraform import azurerm_virtual_network.example /subscriptions/.../resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet
# Import subnet (child of VNet)
terraform import azurerm_subnet.example /subscriptions/.../resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet1
```
Import parent resources before children.
## Common issues
### Configuration doesn't match
After import, `terraform plan` shows changes. You need to update your configuration to match the actual resource.
Use `terraform show` to see the imported values, then copy them to your configuration file.
### Resource already in state
Error: "Resource already managed by Terraform"
The resource is already in your state file. Check with:
```bash
terraform state list
```
Remove it if needed:
```bash
terraform state rm azurerm_resource_group.example
```
### Wrong resource ID
Error: "Cannot import: invalid ID"
Verify the resource ID format. Each resource type expects a specific format. Check the provider documentation.
## Import workflow script
Automate importing multiple resources:
```bash
#!/bin/bash
# List of resource IDs to import
resources=(
"azurerm_resource_group.app:/subscriptions/.../resourceGroups/rg-app"
"azurerm_storage_account.data:/subscriptions/.../storageAccounts/stdata"
"azurerm_key_vault.secrets:/subscriptions/.../vaults/kv-secrets"
)
for resource in "${resources[@]}"; do
IFS=: read -r tf_address azure_id <<< "$resource"
echo "Importing $tf_address..."
terraform import "$tf_address" "$azure_id"
done
echo "Import complete. Run 'terraform plan' to verify."
```
## Best practices
**Start small:** Import one resource at a time until you understand the process.
**Verify with plan:** Always run `terraform plan` after import to check for differences.
**Use import blocks:** For Terraform 1.5+, import blocks with `-generate-config-out` save time.
**Document imports:** Keep a list of imported resources and their IDs.
**Test destroy:** After import, test that `terraform destroy` works without errors (in a dev environment).
**Import dependencies together:** If resources reference each other, import them in dependency order.
## Try it yourself
1. Create a resource group manually in Azure Portal
2. Write a matching Terraform configuration
3. Import the resource group
4. Verify with `terraform plan`
5. Test with `terraform destroy` (optional)
Check the [Azure Provider documentation](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) for resource-specific import formats.
================================================
FILE: 20-state-commands/README.md
================================================
# Manage Terraform state
Terraform state commands let you inspect, modify, and troubleshoot your state file. These commands are essential when refactoring or fixing issues.
## List resources in state
Show all resources Terraform manages:
```bash
terraform state list
```
Output:
```
azurerm_resource_group.example
azurerm_storage_account.data
azurerm_key_vault.secrets
```
Filter resources:
```bash
terraform state list azurerm_storage_account.*
```
## Show resource details
View a resource's state:
```bash
terraform state show azurerm_storage_account.data
```
Output shows all attributes Terraform tracks:
```hcl
resource "azurerm_storage_account" "data" {
id = "/subscriptions/.../storageAccounts/stdata"
name = "stdata"
resource_group_name = "rg-prod"
location = "uksouth"
account_tier = "Standard"
account_replication_type = "GRS"
# ... more attributes
}
```
## Move resources with terraform state mv
Use `terraform state mv` to rename resources or move them between modules without recreating them in Azure.
### Rename a resource
You refactored code and renamed a resource:
```terraform
# Old
resource "azurerm_storage_account" "data" {
# ...
}
# New
resource "azurerm_storage_account" "primary_data" {
# ...
}
```
Without `state mv`, Terraform destroys the old resource and creates a new one. Instead:
```bash
terraform state mv azurerm_storage_account.data azurerm_storage_account.primary_data
```
Now `terraform plan` shows no changes.
### Move resources between modules
You extracted resources into a module:
Before:
```terraform
resource "azurerm_storage_account" "data" {
name = "stdata"
# ...
}
```
After:
```terraform
module "storage" {
source = "./modules/storage"
# ...
}
```
Move the resource:
```bash
terraform state mv azurerm_storage_account.data module.storage.azurerm_storage_account.data
```
### Move resources out of modules
The reverse works too:
```bash
terraform state mv module.storage.azurerm_storage_account.data azurerm_storage_account.data
```
### Move resources between workspaces
Move a resource from dev to prod workspace:
```bash
# Export from dev
terraform workspace select dev
terraform state pull > dev_state.json
# Import to prod
terraform workspace select prod
terraform state mv \
-state=dev_state.json \
-state-out=terraform.tfstate \
azurerm_storage_account.data \
azurerm_storage_account.data
```
### Move multiple resources
Move all resources from a module:
```bash
# List resources in module
terraform state list | grep "module.old_module"
# Move each one
terraform state mv module.old_module.azurerm_resource_group.example module.new_module.azurerm_resource_group.example
terraform state mv module.old_module.azurerm_storage_account.data module.new_module.azurerm_storage_account.data
```
## Remove resources from state
Remove a resource from state without destroying it in Azure:
```bash
terraform state rm azurerm_storage_account.data
```
The storage account continues running in Azure, but Terraform no longer manages it.
Use cases:
- Handing off management to another Terraform workspace
- Removing accidentally imported resources
- Splitting infrastructure across multiple state files
## Replace a resource
Force recreation of a specific resource:
```bash
terraform apply -replace=azurerm_linux_virtual_machine.example
```
This destroys and recreates the VM even if the configuration hasn't changed. Useful for:
- Fixing corrupted resources
- Applying changes that require recreation
- Testing disaster recovery
## Pull and push state
### Pull state to a file
```bash
terraform state pull > backup.tfstate
```
Use this to:
- Back up state before major changes
- Inspect state manually
- Debug state issues
### Push state from a file
```bash
terraform state push backup.tfstate
```
**Dangerous:** Only use this for disaster recovery. Pushing incorrect state causes Terraform to lose track of resources.
## Common scenarios
### Scenario 1: Rename resource type
You changed a resource type:
```terraform
# Old
resource "azurerm_sql_database" "example" {
# ...
}
# New
resource "azurerm_mssql_database" "example" {
# ...
}
```
Move it:
```bash
terraform state mv azurerm_sql_database.example azurerm_mssql_database.example
```
### Scenario 2: Split a resource group
You split one resource group into two:
```terraform
# Old
resource "azurerm_resource_group" "all" {
name = "rg-all"
}
# New
resource "azurerm_resource_group" "web" {
name = "rg-web"
}
resource "azurerm_resource_group" "data" {
name = "rg-data"
}
```
You created `rg-web` and `rg-data` manually in Azure. Import them:
```bash
terraform import azurerm_resource_group.web /subscriptions/.../resourceGroups/rg-web
terraform import azurerm_resource_group.data /subscriptions/.../resourceGroups/rg-data
```
Remove the old one:
```bash
terraform state rm azurerm_resource_group.all
```
Then delete `rg-all` in Azure manually or with `az group delete`.
### Scenario 3: Extract to a module
You're converting resources to a module.
Before:
```terraform
resource "azurerm_virtual_network" "example" {
name = "vnet-prod"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
address_space = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "example" {
name = "subnet-web"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
```
After creating the module:
```terraform
module "network" {
source = "./modules/network"
name = "vnet-prod"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
}
```
Move resources:
```bash
terraform state mv azurerm_virtual_network.example module.network.azurerm_virtual_network.this
terraform state mv azurerm_subnet.example module.network.azurerm_subnet.this
```
## Best practices
**Back up before moving:** Always run `terraform state pull > backup.tfstate` before state operations.
**Use version control:** Commit working state before refactoring. You can revert if something breaks.
**Plan after moving:** Always run `terraform plan` after state commands. It should show no changes.
**Test in dev first:** Practice state operations in a dev environment before touching production.
**Avoid manual edits:** Never edit state files directly. Use state commands.
**Document moves:** Keep notes on why you moved resources, especially in production.
## Dangerous operations
These commands can break your infrastructure:
**terraform state push:** Overwrites remote state. Use only for disaster recovery.
**terraform state rm:** Removes resources from Terraform without destroying them. Easy to lose track of resources.
**Editing state files manually:** Causes state corruption. Always use state commands.
## Recovery
If you break state:
1. **Restore from backup:**
```bash
terraform state push backup.tfstate
```
2. **If using remote state with versioning:**
Check your Azure Storage Account blob versions and restore a previous version.
3. **Re-import resources:**
If state is gone but resources exist, re-import them:
```bash
terraform import azurerm_resource_group.example /subscriptions/.../resourceGroups/rg-example
```
## Try it yourself
```bash
cd 20-state-commands/examples
terraform init
terraform apply
# List resources
terraform state list
# Show details
terraform state show azurerm_resource_group.example
# Rename a resource
terraform state mv azurerm_storage_account.data azurerm_storage_account.primary
# Verify
terraform plan # Should show no changes
# Clean up
terraform destroy
```
## Next steps
Master state management to refactor Terraform confidently. Combined with modules (section 6) and imports (section 10), you can reorganize any Terraform codebase without downtime.
================================================
FILE: 21-pre-post-conditions/README.md
================================================
# Use pre-conditions and post-conditions
Catch configuration errors early with lifecycle validation rules. Check inputs before creating resources and validate outputs after.
## What are conditions
Conditions validate assumptions about your infrastructure. They run during plan and apply operations.
Pre-conditions check inputs and dependencies before Terraform creates or modifies a resource.
Post-conditions verify outputs and resource state after Terraform creates or modifies a resource.
Both fail the operation immediately if validation fails.
## Why use conditions
- Catch errors in plan phase instead of during apply.
- Explain what's wrong and how to fix it with clear messages.
- Require specific configurations (tags, security settings, naming patterns).
- Check that Azure regions support features, SKUs are compatible, and resources exist.
## Pre-conditions
Pre-conditions validate inputs before creating resources.
### Basic pre-condition
Check that a variable meets requirements:
```terraform
variable "location" {
type = string
}
resource "azurerm_resource_group" "example" {
name = "rg-example"
location = var.location
lifecycle {
precondition {
condition = contains(["uksouth", "ukwest", "northeurope"], var.location)
error_message = "Location must be uksouth, ukwest, or northeurope. Got: ${var.location}"
}
}
}
```
If you try to deploy with `location = "westus"`:
```
Error: Resource precondition failed
on main.tf line 9, in resource "azurerm_resource_group" "example":
9: condition = contains(["uksouth", "ukwest", "northeurope"], var.location)
Location must be uksouth, ukwest, or northeurope. Got: westus
```
Terraform stops before creating anything.
### Validate Azure region availability
Check that a region supports the service you're deploying:
```terraform
data "azurerm_locations" "available" {
location = var.location
}
resource "azurerm_kubernetes_cluster" "example" {
name = "aks-example"
location = var.location
resource_group_name = azurerm_resource_group.example.name
dns_prefix = "aks-example"
default_node_pool {
name = "default"
node_count = 1
vm_size = "Standard_D2s_v5"
}
identity {
type = "SystemAssigned"
}
lifecycle {
precondition {
condition = length(data.azurerm_locations.available.locations) > 0
error_message = "AKS is not available in ${var.location}"
}
}
}
```
### Validate SKU compatibility
Check that VM size is available in the region:
```terraform
variable "vm_size" {
type = string
default = "Standard_D2s_v5"
}
variable "location" {
type = string
default = "uksouth"
}
locals {
# Map regions to supported VM sizes
supported_vm_sizes = {
uksouth = [
"Standard_B2s",
"Standard_D2s_v5",
"Standard_D4s_v5"
]
ukwest = [
"Standard_B2s",
"Standard_D2s_v5"
]
northeurope = [
"Standard_B2s",
"Standard_D2s_v5",
"Standard_D4s_v5",
"Standard_E4s_v5"
]
}
}
resource "azurerm_linux_virtual_machine" "example" {
name = "vm-example"
resource_group_name = azurerm_resource_group.example.name
location = var.location
size = var.vm_size
# ... other configuration
lifecycle {
precondition {
condition = contains(
local.supported_vm_sizes[var.location],
var.vm_size
)
error_message = <<-EOT
VM size ${var.vm_size} is not available in ${var.location}.
Available sizes: ${join(", ", local.supported_vm_sizes[var.location])}
EOT
}
}
}
```
### Validate environment tags
Require specific tags on all resources:
```terraform
variable "environment" {
type = string
}
variable "cost_center" {
type = string
}
resource "azurerm_storage_account" "example" {
name = "stexample"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
Environment = var.environment
CostCenter = var.cost_center
ManagedBy = "Terraform"
}
lifecycle {
precondition {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be dev, staging, or prod. Got: ${var.environment}"
}
precondition {
condition = can(regex("^CC-[0-9]{4}$", var.cost_center))
error_message = "cost_center must match pattern CC-XXXX (e.g., CC-1234). Got: ${var.cost_center}"
}
}
}
```
### Validate naming conventions
Enforce resource naming patterns:
```terraform
variable "resource_group_name" {
type = string
}
resource "azurerm_resource_group" "example" {
name = var.resource_group_name
location = "uksouth"
lifecycle {
precondition {
condition = can(regex("^rg-[a-z0-9-]+$", var.resource_group_name))
error_message = "Resource group name must start with 'rg-' and contain only lowercase letters, numbers, and hyphens. Got: ${var.resource_group_name}"
}
precondition {
condition = length(var.resource_group_name) <= 90
error_message = "Resource group name must be 90 characters or less. Got: ${length(var.resource_group_name)} characters"
}
}
}
```
### Check dependencies exist
Verify that required resources are present:
```terraform
data "azurerm_key_vault" "existing" {
name = var.key_vault_name
resource_group_name = var.key_vault_rg
}
resource "azurerm_app_service" "example" {
name = "app-example"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
app_service_plan_id = azurerm_app_service_plan.example.id
lifecycle {
precondition {
condition = data.azurerm_key_vault.existing.id != null
error_message = "Key Vault ${var.key_vault_name} must exist in resource group ${var.key_vault_rg}"
}
precondition {
condition = data.azurerm_key_vault.existing.vault_uri != ""
error_message = "Key Vault ${var.key_vault_name} must be properly configured"
}
}
}
```
## Post-conditions
Post-conditions validate resource state after Terraform creates or updates it.
### Verify resource was created
Check that Azure created the resource successfully:
```terraform
resource "azurerm_storage_account" "example" {
name = "stexample${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
lifecycle {
postcondition {
condition = self.id != ""
error_message = "Storage account was not created successfully"
}
postcondition {
condition = self.primary_blob_endpoint != ""
error_message = "Storage account blob endpoint was not configured"
}
}
}
```
### Validate security settings
Confirm security features are enabled:
```terraform
resource "azurerm_storage_account" "secure" {
name = "stsecure${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "GRS"
enable_https_traffic_only = true
min_tls_version = "TLS1_2"
lifecycle {
postcondition {
condition = self.enable_https_traffic_only == true
error_message = "HTTPS traffic enforcement failed to apply"
}
postcondition {
condition = self.min_tls_version == "TLS1_2"
error_message = "TLS 1.2 requirement failed to apply"
}
postcondition {
condition = self.account_replication_type == "GRS"
error_message = "Geo-redundant storage failed to apply"
}
}
}
```
### Verify outputs meet requirements
Check computed values:
```terraform
resource "azurerm_public_ip" "example" {
name = "pip-example"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
allocation_method = "Static"
sku = "Standard"
lifecycle {
postcondition {
condition = self.ip_address != ""
error_message = "Public IP address was not allocated"
}
postcondition {
condition = can(regex("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", self.ip_address))
error_message = "Public IP address has invalid format: ${self.ip_address}"
}
}
}
```
## Multiple conditions
Add multiple validation rules:
```terraform
resource "azurerm_storage_account" "validated" {
name = var.storage_account_name
resource_group_name = azurerm_resource_group.example.name
location = var.location
account_tier = var.environment == "prod" ? "Premium" : "Standard"
account_replication_type = var.environment == "prod" ? "GRS" : "LRS"
enable_https_traffic_only = true
min_tls_version = "TLS1_2"
tags = {
Environment = var.environment
CostCenter = var.cost_center
}
lifecycle {
# Pre-conditions (check inputs)
precondition {
condition = length(var.storage_account_name) >= 3 && length(var.storage_account_name) <= 24
error_message = "Storage account name must be 3-24 characters. Got: ${length(var.storage_account_name)}"
}
precondition {
condition = can(regex("^[a-z0-9]+$", var.storage_account_name))
error_message = "Storage account name must contain only lowercase letters and numbers. Got: ${var.storage_account_name}"
}
precondition {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be dev, staging, or prod. Got: ${var.environment}"
}
precondition {
condition = can(regex("^CC-[0-9]{4}$", var.cost_center))
error_message = "cost_center must match CC-XXXX. Got: ${var.cost_center}"
}
# Post-conditions (verify result)
postcondition {
condition = self.enable_https_traffic_only == true
error_message = "HTTPS enforcement failed"
}
postcondition {
condition = self.min_tls_version == "TLS1_2"
error_message = "TLS 1.2 requirement failed"
}
postcondition {
condition = self.primary_blob_endpoint != ""
error_message = "Blob endpoint not configured"
}
}
}
```
## Conditions in outputs
Validate output values:
```terraform
output "storage_account_id" {
value = azurerm_storage_account.example.id
precondition {
condition = azurerm_storage_account.example.enable_https_traffic_only == true
error_message = "Cannot output storage account ID: HTTPS not enforced"
}
}
output "storage_connection_string" {
value = azurerm_storage_account.example.primary_connection_string
sensitive = true
precondition {
condition = azurerm_storage_account.example.min_tls_version == "TLS1_2"
error_message = "Cannot output connection string: TLS 1.2 not configured"
}
}
```
## Conditions in data sources
Validate data source lookups:
```terraform
data "azurerm_resource_group" "existing" {
name = var.resource_group_name
lifecycle {
postcondition {
condition = self.location == var.expected_location
error_message = "Resource group is in ${self.location}, expected ${var.expected_location}"
}
postcondition {
condition = contains(keys(self.tags), "Environment")
error_message = "Resource group missing required Environment tag"
}
}
}
```
## Complete Azure example
Full working example with validation:
```terraform
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod"
}
}
variable "location" {
type = string
default = "uksouth"
}
variable "cost_center" {
type = string
}
variable "storage_name" {
type = string
}
locals {
# Approved regions for each environment
approved_regions = {
dev = ["uksouth", "ukwest"]
staging = ["uksouth", "northeurope"]
prod = ["northeurope", "westeurope"]
}
# Required tags
required_tags = ["Environment", "CostCenter", "ManagedBy"]
}
resource "azurerm_resource_group" "validated" {
name = "rg-${var.environment}-validated"
location = var.location
tags = {
Environment = var.environment
CostCenter = var.cost_center
ManagedBy = "Terraform"
}
lifecycle {
precondition {
condition = contains(
local.approved_regions[var.environment],
var.location
)
error_message = <<-EOT
${var.location} is not approved for ${var.environment}.
Approved regions: ${join(", ", local.approved_regions[var.environment])}
EOT
}
precondition {
condition = can(regex("^rg-[a-z0-9-]+$", "rg-${var.environment}-validated"))
error_message = "Resource group name must follow naming convention: rg-{env}-{purpose}"
}
postcondition {
condition = self.id != ""
error_message = "Resource group creation failed"
}
}
}
resource "azurerm_storage_account" "validated" {
name = "${var.storage_name}${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.validated.name
location = azurerm_resource_group.validated.location
account_tier = var.environment == "prod" ? "Premium" : "Standard"
account_replication_type = var.environment == "prod" ? "GRS" : "LRS"
enable_https_traffic_only = true
min_tls_version = "TLS1_2"
tags = azurerm_resource_group.validated.tags
lifecycle {
precondition {
condition = length(var.storage_name) >= 3 && length(var.storage_name) <= 18
error_message = "Storage name must be 3-18 characters (suffix will be added)"
}
precondition {
condition = can(regex("^[a-z0-9]+$", var.storage_name))
error_message = "Storage name must contain only lowercase letters and numbers"
}
precondition {
condition = can(regex("^CC-[0-9]{4}$", var.cost_center))
error_message = "Cost center must match CC-XXXX format"
}
postcondition {
condition = self.enable_https_traffic_only == true
error_message = "HTTPS enforcement failed to apply"
}
postcondition {
condition = self.min_tls_version == "TLS1_2"
error_message = "TLS 1.2 failed to apply"
}
postcondition {
condition = length(self.primary_blob_endpoint) > 0
error_message = "Blob endpoint not created"
}
}
}
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}
output "storage_account_id" {
value = azurerm_storage_account.validated.id
precondition {
condition = alltrue([
for tag in local.required_tags :
contains(keys(azurerm_storage_account.validated.tags), tag)
])
error_message = "Storage account missing required tags: ${join(", ", local.required_tags)}"
}
}
```
## Try the examples
```bash
cd 21-pre-post-conditions/examples
terraform init
# Try with valid values
terraform apply \
-var="environment=dev" \
-var="cost_center=CC-1234" \
-var="storage_name=stvalid"
# Try with invalid environment (fails pre-condition)
terraform apply \
-var="environment=test" \
-var="cost_center=CC-1234" \
-var="storage_name=stvalid"
# Try with invalid cost center (fails pre-condition)
terraform apply \
-var="environment=dev" \
-var="cost_center=INVALID" \
-var="storage_name=stvalid"
# Try with invalid storage name (fails pre-condition)
terraform apply \
-var="environment=dev" \
-var="cost_center=CC-1234" \
-var="storage_name=UPPERCASE"
```
## When to use conditions vs validation blocks
Use variable validation blocks to check individual variable values.
```terraform
variable "location" {
type = string
validation {
condition = contains(["uksouth", "ukwest"], var.location)
error_message = "Invalid location"
}
}
```
Use pre-conditions to validate relationships between resources, check dependencies, and enforce complex rules.
```terraform
resource "azurerm_resource_group" "example" {
# ...
lifecycle {
precondition {
condition = contains(
local.approved_regions[var.environment],
var.location
)
error_message = "Region not approved for environment"
}
}
}
```
Use both for broader validation.
## Best practices
- Write clear error messages that explain the fix.
- Use pre-conditions to catch errors in plan phase.
- Focus on critical requirements so validation stays maintainable.
- Use locals for complex logic to keep condition expressions readable.
- Add comments explaining why validations exist.
- Run `terraform plan` with invalid inputs to verify error messages.
- Combine variable validation blocks for simple checks and conditions for complex logic.
================================================
FILE: 21-pre-post-conditions/examples/main.tf
================================================
locals {
# Approved regions for each environment
approved_regions = {
dev = ["uksouth", "ukwest"]
staging = ["uksouth", "northeurope"]
prod = ["northeurope", "westeurope"]
}
# Required tags for compliance
required_tags = ["Environment", "CostCenter", "ManagedBy"]
# Common tags
common_tags = {
Environment = var.environment
CostCenter = var.cost_center
ManagedBy = "Terraform"
}
}
# Resource group with pre-conditions and post-conditions
resource "azurerm_resource_group" "validated" {
name = "rg-${var.environment}-validated"
location = var.location
tags = local.common_tags
lifecycle {
# Pre-condition: Validate region is approved for environment
precondition {
condition = contains(
local.approved_regions[var.environment],
var.location
)
error_message = <<-EOT
Region ${var.location} is not approved for ${var.environment} environment.
Approved regions: ${join(", ", local.approved_regions[var.environment])}
EOT
}
# Pre-condition: Validate naming convention
precondition {
condition = can(regex("^rg-[a-z0-9-]+$", "rg-${var.environment}-validated"))
error_message = "Resource group name must follow naming convention: rg-{env}-{purpose}"
}
# Post-condition: Verify resource was created
postcondition {
condition = self.id != ""
error_message = "Resource group creation failed - ID is empty"
}
# Post-condition: Verify location matches request
postcondition {
condition = self.location == var.location
error_message = "Resource group location ${self.location} does not match requested ${var.location}"
}
}
}
# Storage account with comprehensive validation
resource "azurerm_storage_account" "validated" {
name = "${var.storage_name}${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.validated.name
location = azurerm_resource_group.validated.location
account_tier = var.environment == "prod" ? "Premium" : "Standard"
account_replication_type = var.environment == "prod" ? "GRS" : "LRS"
enable_https_traffic_only = true
min_tls_version = "TLS1_2"
tags = local.common_tags
lifecycle {
# Pre-condition: Validate storage name length
precondition {
condition = length(var.storage_name) >= 3 && length(var.storage_name) <= 18
error_message = "Storage name must be 3-18 characters (6-char suffix will be added). Got: ${length(var.storage_name)} characters"
}
# Pre-condition: Validate storage name format
precondition {
condition = can(regex("^[a-z0-9]+$", var.storage_name))
error_message = "Storage name must contain only lowercase letters and numbers. Got: ${var.storage_name}"
}
# Pre-condition: Validate cost center format
precondition {
condition = can(regex("^CC-[0-9]{4}$", var.cost_center))
error_message = "Cost center must match format CC-XXXX (e.g., CC-1234). Got: ${var.cost_center}"
}
# Pre-condition: Prod must use premium storage
precondition {
condition = var.environment != "prod" || (var.environment == "prod")
error_message = "Production environment detected - additional validation required"
}
# Post-condition: Verify HTTPS enforcement
postcondition {
condition = self.enable_https_traffic_only == true
error_message = "HTTPS traffic enforcement failed to apply"
}
# Post-condition: Verify TLS version
postcondition {
condition = self.min_tls_version == "TLS1_2"
error_message = "TLS 1.2 requirement failed to apply"
}
# Post-condition: Verify blob endpoint exists
postcondition {
condition = length(self.primary_blob_endpoint) > 0
error_message = "Blob endpoint was not created"
}
# Post-condition: Verify replication type for prod
postcondition {
condition = var.environment != "prod" || self.account_replication_type == "GRS"
error_message = "Production storage must use GRS replication. Got: ${self.account_replication_type}"
}
}
}
# Public IP with post-condition validation
resource "azurerm_public_ip" "validated" {
name = "pip-${var.environment}-validated"
location = azurerm_resource_group.validated.location
resource_group_name = azurerm_resource_group.validated.name
allocation_method = "Static"
sku = "Standard"
tags = local.common_tags
lifecycle {
# Post-condition: Verify IP was allocated
postcondition {
condition = self.ip_address != ""
error_message = "Public IP address was not allocated"
}
# Post-condition: Verify IP format
postcondition {
condition = can(regex("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", self.ip_address))
error_message = "Public IP address has invalid format: ${self.ip_address}"
}
}
}
# Random string for unique names
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}
# Data source with validation
data "azurerm_subscription" "current" {}
# Validate subscription state
resource "null_resource" "validate_subscription" {
lifecycle {
precondition {
condition = data.azurerm_subscription.current.state == "Enabled"
error_message = "Azure subscription must be in Enabled state. Current state: ${data.azurerm_subscription.current.state}"
}
}
}
================================================
FILE: 21-pre-post-conditions/examples/outputs.tf
================================================
output "resource_group_name" {
description = "Name of the validated resource group"
value = azurerm_resource_group.validated.name
}
output "storage_account_name" {
description = "Name of the validated storage account"
value = azurerm_storage_account.validated.name
}
output "storage_account_id" {
description = "ID of the validated storage account"
value = azurerm_storage_account.validated.id
# Pre-condition: Ensure all required tags are present
precondition {
condition = alltrue([
for tag in local.required_tags :
contains(keys(azurerm_storage_account.validated.tags), tag)
])
error_message = "Cannot output storage account ID: Missing required tags. Required: ${join(", ", local.required_tags)}"
}
}
output "public_ip_address" {
description = "Allocated public IP address"
value = azurerm_public_ip.validated.ip_address
# Pre-condition: Ensure IP was allocated
precondition {
condition = azurerm_public_ip.validated.ip_address != ""
error_message = "Cannot output IP address: IP was not allocated"
}
}
output "storage_https_only" {
description = "Whether HTTPS is enforced"
value = azurerm_storage_account.validated.enable_https_traffic_only
}
output "storage_tls_version" {
description = "Minimum TLS version"
value = azurerm_storage_account.validated.min_tls_version
}
output "validation_summary" {
description = "Summary of applied validations"
value = {
environment = var.environment
location = var.location
cost_center = var.cost_center
https_enforced = azurerm_storage_account.validated.enable_https_traffic_only
tls_version = azurerm_storage_account.validated.min_tls_version
replication_type = azurerm_storage_account.validated.account_replication_type
}
}
================================================
FILE: 21-pre-post-conditions/examples/providers.tf
================================================
terraform {
required_version = ">= 1.2"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.8"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 21-pre-post-conditions/examples/variables.tf
================================================
variable "environment" {
description = "Environment name"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod"
}
}
variable "location" {
description = "Azure region"
type = string
default = "uksouth"
}
variable "cost_center" {
description = "Cost center code (format: CC-XXXX)"
type = string
default = "CC-1234"
}
variable "storage_name" {
description = "Base storage account name (3-18 characters)"
type = string
default = "stvalidated"
}
================================================
FILE: 22-functions/README.md
================================================
# Use Terraform functions
Terraform includes over 100 built-in functions for manipulating data. Master the most useful ones for Azure infrastructure.
## What are functions
Functions transform values. They take inputs and return outputs.
```terraform
upper("hello") # Returns "HELLO"
length([1, 2, 3]) # Returns 3
```
Functions are pure: same inputs always return same outputs. No side effects.
## String functions
### upper and lower
Convert case:
```terraform
locals {
env = "Production"
env_lower = lower(local.env) # "production"
env_upper = upper(local.env) # "PRODUCTION"
}
resource "azurerm_resource_group" "example" {
name = "rg-${local.env_lower}" # "rg-production"
location = "uksouth"
}
```
### format
Build strings with placeholders:
```terraform
locals {
env = "dev"
app_name = "webapp"
location = "uksouth"
# Format: %s for string, %d for number
resource_name = format("rg-%s-%s-%s", local.env, local.app_name, local.location)
# Result: "rg-dev-webapp-uksouth"
vm_name = format("vm-%s-%02d", local.env, 5)
# Result: "vm-dev-05" (zero-padded)
}
```
### join and split
Combine and separate strings:
```terraform
locals {
regions = ["uksouth", "ukwest", "northeurope"]
# Join with separator
regions_string = join(", ", local.regions)
# Result: "uksouth, ukwest, northeurope"
# Split string into list
connection_string = "Server=myserver;Database=mydb;User=admin"
parts = split(";", local.connection_string)
# Result: ["Server=myserver", "Database=mydb", "User=admin"]
}
```
### regex and regexall
Match patterns:
```terraform
locals {
resource_id = "/subscriptions/12345/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorageacct"
# Extract resource group name
rg_name = regex(".*/resourceGroups/([^/]+)/.*", local.resource_id)[0]
# Result: "my-rg"
# Extract all parts
parts = regex(".*/(resourceGroups)/([^/]+)/.*/([^/]+)$", local.resource_id)
# Result: ["resourceGroups", "my-rg", "mystorageacct"]
}
# Azure-specific: Parse connection string
locals {
connection_string = "DefaultEndpointsProtocol=https;AccountName=mystorageacct;AccountKey=abc123;EndpointSuffix=core.windows.net"
# Extract account name
account_name = regex("AccountName=([^;]+)", local.connection_string)[0]
# Result: "mystorageacct"
}
```
### replace
Replace substrings:
```terraform
locals {
resource_name = "My Resource Name"
# Replace spaces with hyphens
sanitized = replace(local.resource_name, " ", "-")
# Result: "My-Resource-Name"
# Make lowercase and replace spaces
final = lower(replace(local.resource_name, " ", "-"))
# Result: "my-resource-name"
}
```
### trim, trimprefix, trimsuffix
Remove characters:
```terraform
locals {
name_with_spaces = " my-resource "
trimmed = trim(local.name_with_spaces, " ")
# Result: "my-resource"
# Remove prefix
full_name = "rg-production-app"
app_name = trimprefix(local.full_name, "rg-")
# Result: "production-app"
# Remove suffix
resource_with_type = "myvm-vm"
clean_name = trimsuffix(local.resource_with_type, "-vm")
# Result: "myvm"
}
```
## Collection functions
### length
Count elements:
```terraform
locals {
regions = ["uksouth", "ukwest", "northeurope"]
region_count = length(local.regions) # 3
name = "production"
name_length = length(local.name) # 10
}
```
### concat
Combine lists:
```terraform
locals {
dev_regions = ["uksouth", "ukwest"]
prod_regions = ["northeurope", "westeurope"]
all_regions = concat(local.dev_regions, local.prod_regions)
# Result: ["uksouth", "ukwest", "northeurope", "westeurope"]
}
```
### merge
Combine maps:
```terraform
locals {
common_tags = {
ManagedBy = "Terraform"
Department = "Engineering"
}
env_tags = {
Environment = "production"
CostCenter = "CC-1234"
}
all_tags = merge(local.common_tags, local.env_tags)
# Result: All four tags combined
}
resource "azurerm_resource_group" "example" {
name = "rg-example"
location = "uksouth"
tags = merge(local.common_tags, { Environment = "dev" })
}
```
### flatten
Convert nested lists to flat list:
```terraform
locals {
environments = ["dev", "staging", "prod"]
regions = ["uksouth", "ukwest"]
# Create nested list
nested = [
for env in local.environments : [
for region in local.regions :
"${env}-${region}"
]
]
# Result: [["dev-uksouth", "dev-ukwest"], ["staging-uksouth", "staging-ukwest"], ...]
# Flatten to single list
flattened = flatten(local.nested)
# Result: ["dev-uksouth", "dev-ukwest", "staging-uksouth", "staging-ukwest", ...]
}
```
### contains
Check if element exists:
```terraform
locals {
approved_regions = ["uksouth", "ukwest", "northeurope"]
requested_region = "uksouth"
is_approved = contains(local.approved_regions, local.requested_region)
# Result: true
}
resource "azurerm_resource_group" "example" {
name = "rg-example"
location = var.location
lifecycle {
precondition {
condition = contains(["uksouth", "ukwest"], var.location)
error_message = "Region must be uksouth or ukwest"
}
}
}
```
### keys and values
Extract from maps:
```terraform
locals {
environments = {
dev = "uksouth"
staging = "ukwest"
prod = "northeurope"
}
env_names = keys(local.environments)
# Result: ["dev", "staging", "prod"]
locations = values(local.environments)
# Result: ["uksouth", "ukwest", "northeurope"]
}
```
### lookup
Get value from map with default:
```terraform
locals {
vm_sizes = {
dev = "Standard_B2s"
staging = "Standard_D2s_v5"
prod = "Standard_D4s_v5"
}
# Get value with default
size = lookup(local.vm_sizes, "dev", "Standard_B1s")
# Result: "Standard_B2s"
# Non-existent key returns default
size_unknown = lookup(local.vm_sizes, "test", "Standard_B1s")
# Result: "Standard_B1s"
}
resource "azurerm_linux_virtual_machine" "example" {
name = "vm-example"
size = lookup(local.vm_sizes, var.environment, "Standard_B1s")
# ... other configuration
}
```
### slice
Extract subset of list:
```terraform
locals {
all_regions = ["uksouth", "ukwest", "northeurope", "westeurope", "francecentral"]
# slice(list, start_index, end_index)
primary_regions = slice(local.all_regions, 0, 2)
# Result: ["uksouth", "ukwest"]
last_two = slice(local.all_regions, 3, 5)
# Result: ["westeurope", "francecentral"]
}
```
## Type conversion functions
### tostring, tonumber, tobool
Convert types:
```terraform
locals {
# String to number
port_string = "443"
port_number = tonumber(local.port_string) # 443
# Number to string
count_number = 5
count_string = tostring(local.count_number) # "5"
# String to bool
enabled_string = "true"
enabled_bool = tobool(local.enabled_string) # true
}
```
### tolist, toset, tomap
Convert collections:
```terraform
locals {
# Set (unique values) to list
unique_regions = toset(["uksouth", "ukwest", "uksouth"])
# Result: ["uksouth", "ukwest"]
region_list = tolist(local.unique_regions)
# Create map
env_map = tomap({
environment = "production"
tier = "standard"
})
}
```
### can
Test if expression succeeds:
```terraform
locals {
ip_address = "192.168.1.1"
# Check if valid IP format
is_valid_ip = can(regex("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", local.ip_address))
# Result: true
# Check if can convert to number
value = "123"
is_number = can(tonumber(local.value)) # true
value_text = "abc"
is_number2 = can(tonumber(local.value_text)) # false
}
```
### try
Return first expression that succeeds:
```terraform
locals {
# Try to get value, fallback to default
config = {
# region key might not exist
}
region = try(local.config.region, "uksouth")
# Returns "uksouth" if config.region doesn't exist
}
```
## Encoding functions
### base64encode and base64decode
Encode and decode base64:
```terraform
locals {
script = <<-EOT
#!/bin/bash
apt-get update
apt-get install -y nginx
EOT
# Encode for Azure custom data
encoded_script = base64encode(local.script)
}
resource "azurerm_linux_virtual_machine" "example" {
name = "vm-example"
# ...
custom_data = base64encode(local.script)
}
```
### jsonencode and jsondecode
Convert to/from JSON:
```terraform
locals {
# Encode object as JSON
config = {
server = "myserver.database.windows.net"
database = "mydb"
port = 1433
}
config_json = jsonencode(local.config)
# Result: '{"server":"myserver.database.windows.net","database":"mydb","port":1433}'
# Parse JSON string
json_string = '{"environment":"prod","region":"uksouth"}'
parsed = jsondecode(local.json_string)
environment = jsondecode(local.json_string)["environment"] # "prod"
}
```
## Azure-specific examples
### Parse Azure resource IDs
```terraform
locals {
storage_id = azurerm_storage_account.example.id
# "/subscriptions/12345/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mysa"
# Extract parts
subscription_id = split("/", local.storage_id)[2]
resource_group = split("/", local.storage_id)[4]
storage_name = split("/", local.storage_id)[8]
# Or use regex
rg_name = regex(".*/resourceGroups/([^/]+)/.*", local.storage_id)[0]
}
```
### Build connection strings
```terraform
locals {
storage_account_name = azurerm_storage_account.example.name
storage_account_key = azurerm_storage_account.example.primary_access_key
# Build connection string
connection_string = format(
"DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=core.windows.net",
local.storage_account_name,
local.storage_account_key
)
}
```
### Generate unique names
```terraform
locals {
# Base name
base_name = "mystorageacct"
# Add hash of resource group ID for uniqueness
unique_suffix = substr(
sha256(azurerm_resource_group.example.id),
0,
8
)
storage_name = "${local.base_name}${local.unique_suffix}"
}
```
### Clean resource names
```terraform
locals {
# User input with invalid characters
raw_name = "My Storage Account!"
# Clean: lowercase, remove special chars, truncate
clean_name = substr(
lower(
replace(
replace(local.raw_name, " ", ""),
"/[^a-z0-9]/",
""
)
),
0,
24
)
# Result: "mystorageaccount"
}
```
### Create multiple NSG rules
```terraform
locals {
# Define rules
nsg_rules = {
allow_http = {
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
port = "80"
}
allow_https = {
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
port = "443"
}
allow_ssh = {
priority = 120
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
port = "22"
}
}
}
resource "azurerm_network_security_rule" "example" {
for_each = local.nsg_rules
name = each.key
priority = each.value.priority
direction = each.value.direction
access = each.value.access
protocol = each.value.protocol
source_port_range = "*"
destination_port_range = each.value.port
source_address_prefix = "*"
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.example.name
network_security_group_name = azurerm_network_security_group.example.name
}
```
### Combine tags
```terraform
locals {
# Common tags
common_tags = {
ManagedBy = "Terraform"
Environment = var.environment
}
# Resource-specific tags
storage_tags = merge(
local.common_tags,
{
Purpose = "Data Storage"
Backup = "Daily"
CostCenter = var.cost_center
}
)
# VM tags
vm_tags = merge(
local.common_tags,
{
Purpose = "Application Server"
OS = "Linux"
}
)
}
resource "azurerm_storage_account" "example" {
# ...
tags = local.storage_tags
}
resource "azurerm_linux_virtual_machine" "example" {
# ...
tags = local.vm_tags
}
```
## Complete example
Full working example using multiple functions:
```terraform
variable "environment" {
type = string
}
variable "app_name" {
type = string
}
variable "regions" {
type = list(string)
default = ["uksouth", "ukwest"]
}
locals {
# Clean app name
clean_app_name = lower(replace(var.app_name, " ", "-"))
# Build resource names
resource_group_name = format("rg-%s-%s", var.environment, local.clean_app_name)
# Create storage name (max 24 chars, alphanumeric only)
storage_base = replace(local.clean_app_name, "-", "")
storage_name = substr("st${var.environment}${local.storage_base}", 0, 24)
# Common tags
common_tags = {
Environment = title(var.environment)
Application = var.app_name
ManagedBy = "Terraform"
Regions = join(", ", var.regions)
}
# Get primary region
primary_region = element(var.regions, 0)
# Check if prod
is_production = lower(var.environment) == "prod"
# Conditional replication
replication_type = local.is_production ? "GRS" : "LRS"
}
resource "azurerm_resource_group" "example" {
name = local.resource_group_name
location = local.primary_region
tags = local.common_tags
}
resource "azurerm_storage_account" "example" {
name = local.storage_name
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = local.replication_type
tags = merge(
local.common_tags,
{
Purpose = "Application Data"
Tier = local.is_production ? "Premium" : "Standard"
}
)
}
# Output parsed values
output "parsed_resource_id" {
value = {
subscription_id = split("/", azurerm_storage_account.example.id)[2]
resource_group = split("/", azurerm_storage_account.example.id)[4]
storage_name = split("/", azurerm_storage_account.example.id)[8]
}
}
```
## Try the examples
```bash
cd 22-functions/examples
terraform init
terraform apply \
-var="environment=dev" \
-var="app_name=My Test App"
```
## Best practices
- Use locals for complex expressions so you do not repeat function calls.
- Comment complex logic, especially regex patterns.
- Use `can()` to validate inputs before applying transformations.
- Break complex nested functions into multiple locals.
- Test edge cases like empty strings, long strings, and special characters.
## Function reference
Full list at [terraform.io/language/functions](https://www.terraform.io/language/functions).
Common categories:
- String: upper, lower, format, replace, regex, trim, join, split
- Collection: length, concat, merge, flatten, contains, keys, values, lookup, slice
- Type: tostring, tonumber, tobool, tolist, toset, tomap, can, try
- Encoding: base64encode, jsonencode, yamlencode
- Filesystem: file, fileexists, templatefile
- Date: timestamp, formatdate
- Hash: md5, sha256, uuid
- Network: cidrhost, cidrsubnet
Explore them all to simplify your Terraform code.
## Use Terraform MCP server for function assistance
The Terraform MCP server helps you discover and use functions correctly.
### Set up MCP in VS Code
Add to your VS Code MCP settings (see [lesson 23](../23-mcp-server/) for full setup):
```json
{
"mcpServers": {
"terraform": {
"command": "npx",
"args": ["-y", "@terraform-mcp/server"]
}
}
}
```
### Ask about functions
With MCP enabled, ask GitHub Copilot:
**"What Terraform function can I use to remove spaces from a string?"**
Response:
```
Use replace() function:
replace(string, search, replace)
Example:
locals {
name_with_spaces = "My Resource Name"
clean_name = replace(local.name_with_spaces, " ", "-")
# Result: "My-Resource-Name"
}
```
**"How do I extract a resource group name from an Azure resource ID?"**
Response:
```
Use regex() or split():
# With regex
rg_name = regex(".*/resourceGroups/([^/]+)/.*", resource_id)[0]
# With split
rg_name = split("/", resource_id)[4]
```
**"What function combines two maps in Terraform?"**
Response:
```
Use merge():
locals {
common_tags = { ManagedBy = "Terraform" }
env_tags = { Environment = "prod" }
all_tags = merge(local.common_tags, local.env_tags)
}
```
### Get function documentation
Ask for detailed usage:
**"Show me all the parameters for the format() function"**
Response includes:
- Function signature
- Parameter types
- Return type
- Examples
- Common use cases
### Test function expressions
Ask MCP to validate function usage:
**"Will this work: `substr(var.name, 0, 24)`"**
MCP checks:
- Correct function name
- Right number of parameters
- Valid parameter types
- Returns explanation if invalid
### Discover functions for tasks
Ask high-level questions:
**"How can I create a unique storage account name from a resource group name?"**
Response suggests:
```terraform
locals {
# Hash the resource group ID for uniqueness
unique_suffix = substr(sha256(azurerm_resource_group.example.id), 0, 8)
# Clean the name and add suffix
storage_name = "${replace(lower(var.app_name), " ", "")}${local.unique_suffix}"
# Ensure max 24 characters
final_name = substr(local.storage_name, 0, 24)
}
```
### Get Azure-specific examples
**"How do I build an Azure SQL connection string in Terraform?"**
Response:
```terraform
locals {
connection_string = format(
"Server=tcp:%s.database.windows.net,1433;Database=%s;User ID=%s;Password=%s;Encrypt=yes;TrustServerCertificate=no;",
azurerm_mssql_server.example.name,
azurerm_mssql_database.example.name,
var.admin_username,
var.admin_password
)
}
```
### Debug function errors
When you get errors like:
```
Error: Invalid function argument
on main.tf line 5:
5: name = substr(var.storage_name)
```
Ask MCP:
**"Why is my substr() call failing?"**
Response:
```
substr() requires 3 arguments:
substr(string, offset, length)
You're missing offset and length.
Fix:
substr(var.storage_name, 0, 24)
```
### Learn function combinations
**"How do I create resource names for multiple regions?"**
Response shows combining functions:
```terraform
locals {
regions = ["uksouth", "ukwest", "northeurope"]
# Use for expression with format
resource_groups = {
for region in local.regions :
region => format("rg-%s-%s", var.environment, region)
}
# Result: { uksouth = "rg-dev-uksouth", ... }
}
```
### MCP advantages for functions
- Get function documentation without leaving the editor.
- Request Azure-specific usage patterns.
- Validate syntax before running `terraform plan`.
- Discover the right function even if you do not know its name.
- Learn how to chain multiple functions together.
### Try it
```bash
# Open VS Code with MCP enabled
code .
# Ask Copilot Chat:
# "Show me how to use the flatten function with Azure regions"
# "What's the difference between merge and concat?"
# "How do I parse JSON in Terraform?"
```
MCP makes learning and using Terraform functions faster and more accurate.
## Next steps
You've completed all core Terraform on Azure tutorials. You now know how to:
- Deploy infrastructure with Terraform
- Manage state locally and remotely
- Use variables and modules
- Test and validate configurations
- Import existing resources
- Manipulate state safely
- Validate with pre/post-conditions
- Transform data with functions
- Accelerate development with MCP server
### Keep learning
- Build a real project (VMs, networking, storage, databases).
- Explore the Terraform modules registry: [registry.terraform.io/browse/modules](https://registry.terraform.io/browse/modules?provider=azurerm)
- Integrate Terraform with GitHub Actions or Azure DevOps.
- Study Azure Landing Zones for enterprise patterns.
- Join communities:
- [Terraform Discord](https://discord.gg/terraform)
- [HashiCorp Forum](https://discuss.hashicorp.com/c/terraform-core)
- [Azure Terraform GitHub](https://github.com/Azure/terraform-azurerm-examples)
Keep experimenting. The best way to learn is by building.
Return to [main README](../README.md) for the complete learning path.
================================================
FILE: 22-functions/examples/main.tf
================================================
locals {
# String manipulation functions
clean_app_name = lower(replace(var.app_name, " ", "-"))
app_name_upper = upper(var.app_name)
# Format function for building names
resource_group_name = format("rg-%s-%s", var.environment, local.clean_app_name)
# Create storage account name (max 24 chars, alphanumeric only)
storage_base = replace(local.clean_app_name, "-", "")
storage_name = substr("st${var.environment}${local.storage_base}", 0, 24)
# Collection functions - merge tags
common_tags = {
Environment = title(var.environment) # Capitalize first letter
Application = var.app_name
ManagedBy = "Terraform"
Regions = join(", ", var.regions) # Join list into string
}
# Get primary region from list
primary_region = element(var.regions, 0)
secondary_region = length(var.regions) > 1 ? element(var.regions, 1) : null
# Type conversion and conditionals
is_production = lower(var.environment) == "prod"
# Conditional replication type
replication_type = local.is_production ? "GRS" : "LRS"
account_tier = local.is_production ? "Premium" : "Standard"
# Collection functions - flatten nested lists
all_region_combos = flatten([
for env in ["dev", "prod"] : [
for region in var.regions :
"${env}-${region}"
]
])
# Parse cost center using regex
cost_center_number = can(regex("^CC-([0-9]{4})$", var.cost_center)) ? regex("^CC-([0-9]{4})$", var.cost_center)[0] : "0000"
# Build resource tags with multiple merge operations
resource_specific_tags = {
Purpose = "Demo Terraform Functions"
CostCenter = var.cost_center
}
# Combine all tags
all_tags = merge(local.common_tags, local.resource_specific_tags)
# Use contains function for validation
valid_regions = ["uksouth", "ukwest", "northeurope", "westeurope"]
primary_is_valid = contains(local.valid_regions, local.primary_region)
# Use lookup with default
vm_sizes = {
dev = "Standard_B2s"
staging = "Standard_D2s_v5"
prod = "Standard_D4s_v5"
}
vm_size = lookup(local.vm_sizes, var.environment, "Standard_B1s")
# Create NSG rules using map
nsg_rules = {
allow_http = {
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
port = "80"
name = "AllowHTTP"
}
allow_https = {
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
port = "443"
name = "AllowHTTPS"
}
allow_ssh = {
priority = 120
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
port = "22"
name = "AllowSSH"
}
}
}
# Resource group
resource "azurerm_resource_group" "example" {
name = local.resource_group_name
location = local.primary_region
tags = local.all_tags
}
# Storage account demonstrating multiple functions
resource "azurerm_storage_account" "example" {
name = local.storage_name
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = local.replication_type
enable_https_traffic_only = true
min_tls_version = "TLS1_2"
tags = merge(
local.all_tags,
{
ReplicationType = local.replication_type
IsProduction = tostring(local.is_production)
StorageTier = local.account_tier
}
)
}
# Virtual network
resource "azurerm_virtual_network" "example" {
name = format("vnet-%s-%s", var.environment, local.clean_app_name)
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
tags = local.all_tags
}
# Subnet
resource "azurerm_subnet" "example" {
name = "subnet-default"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
# Network Security Group
resource "azurerm_network_security_group" "example" {
name = format("nsg-%s-%s", var.environment, local.clean_app_name)
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
tags = local.all_tags
}
# NSG Rules using for_each with map
resource "azurerm_network_security_rule" "example" {
for_each = local.nsg_rules
name = each.value.name
priority = each.value.priority
direction = each.value.direction
access = each.value.access
protocol = each.value.protocol
source_port_range = "*"
destination_port_range = each.value.port
source_address_prefix = "*"
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.example.name
network_security_group_name = azurerm_network_security_group.example.name
}
# Associate NSG with subnet
resource "azurerm_subnet_network_security_group_association" "example" {
subnet_id = azurerm_subnet.example.id
network_security_group_id = azurerm_network_security_group.example.id
}
# Demonstrate parsing Azure resource IDs
locals {
storage_id = azurerm_storage_account.example.id
# Parse ID: /subscriptions/{sub}/resourceGroups/{rg}/providers/{provider}/{type}/{name}
id_parts = split("/", local.storage_id)
subscription_id = length(local.id_parts) > 2 ? local.id_parts[2] : ""
parsed_rg_name = length(local.id_parts) > 4 ? local.id_parts[4] : ""
parsed_sa_name = length(local.id_parts) > 8 ? local.id_parts[8] : ""
# Use regex to extract resource group
rg_from_regex = can(regex(".*/resourceGroups/([^/]+)/.*", local.storage_id)) ? regex(".*/resourceGroups/([^/]+)/.*", local.storage_id)[0] : ""
}
# Build connection string using format
locals {
connection_string = format(
"DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=core.windows.net",
azurerm_storage_account.example.name,
azurerm_storage_account.example.primary_access_key
)
# Base64 encode a script
init_script = <<-EOT
#!/bin/bash
echo "Environment: ${var.environment}"
echo "App: ${var.app_name}"
apt-get update
EOT
encoded_script = base64encode(local.init_script)
}
# Create JSON configuration
locals {
app_config = {
environment = var.environment
app_name = var.app_name
regions = var.regions
storage_account = azurerm_storage_account.example.name
primary_endpoint = azurerm_storage_account.example.primary_blob_endpoint
}
app_config_json = jsonencode(local.app_config)
}
================================================
FILE: 22-functions/examples/outputs.tf
================================================
output "resource_group_name" {
description = "Generated resource group name"
value = azurerm_resource_group.example.name
}
output "storage_account_name" {
description = "Generated storage account name (cleaned and truncated)"
value = azurerm_storage_account.example.name
}
output "primary_region" {
description = "Primary region from list"
value = local.primary_region
}
output "is_production" {
description = "Whether this is production environment"
value = local.is_production
}
output "replication_type" {
description = "Storage replication type based on environment"
value = local.replication_type
}
output "all_tags" {
description = "Merged tags applied to resources"
value = local.all_tags
}
output "parsed_resource_id" {
description = "Parsed components from storage account resource ID"
value = {
full_id = local.storage_id
subscription_id = local.subscription_id
resource_group = local.parsed_rg_name
storage_name = local.parsed_sa_name
rg_from_regex = local.rg_from_regex
}
}
output "connection_string" {
description = "Generated storage connection string"
value = local.connection_string
sensitive = true
}
output "app_config_json" {
description = "Application configuration as JSON"
value = local.app_config_json
}
output "encoded_script" {
description = "Base64 encoded initialization script"
value = local.encoded_script
}
output "nsg_rules" {
description = "NSG rules created"
value = keys(local.nsg_rules)
}
output "string_functions_demo" {
description = "Demonstration of string manipulation functions"
value = {
original_app_name = var.app_name
clean_app_name = local.clean_app_name
upper_app_name = local.app_name_upper
formatted_name = local.resource_group_name
storage_base = local.storage_base
final_name = local.storage_name
}
}
output "collection_functions_demo" {
description = "Demonstration of collection functions"
value = {
regions = var.regions
regions_joined = join(", ", var.regions)
region_count = length(var.regions)
primary_region = local.primary_region
flattened_combos = local.all_region_combos
vm_size = local.vm_size
cost_center_num = local.cost_center_number
}
}
================================================
FILE: 22-functions/examples/providers.tf
================================================
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
================================================
FILE: 22-functions/examples/variables.tf
================================================
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
variable "app_name" {
description = "Application name"
type = string
default = "My Test App"
}
variable "regions" {
description = "List of Azure regions"
type = list(string)
default = ["uksouth", "ukwest"]
}
variable "cost_center" {
description = "Cost center code"
type = string
default = "CC-1234"
}
================================================
FILE: 23-mcp-server/README.md
================================================
# Terraform MCP Server
Get real-time Terraform provider documentation, module details, and registry information directly in GitHub Copilot. The Terraform Model Context Protocol (MCP) server connects your AI assistant to the Terraform Registry.
## What you get
- Provider docs: latest versions, capabilities, and resource examples
- Module search: find and inspect public/private modules
- Current info: always up-to-date from the live registry
- In-editor: no context switching to browsers
## Setup
This repo includes an MCP configuration at [.vscode/mcp.json](../.vscode/mcp.json). If you have GitHub Copilot with MCP support, it should load automatically.
### Configuration
```json
{
"servers": {
"terraform": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "TFE_TOKEN=${input:tfe_token}",
"-e", "TFE_ADDRESS=${input:tfe_address}",
"hashicorp/terraform-mcp-server:0.4.0"
]
}
}
}
```
**Requirements:**
- Docker installed and running
- GitHub Copilot with MCP support
- Optional: HCP Terraform token for private registry access
### Verify setup
Ask Copilot:
```
"What's the latest version of the azurerm provider?"
```
If you get a version number, MCP is working.
## Usage examples
### Get provider versions
```
"What's the latest Azure provider version?"
```
Returns current version for your `required_providers` block.
### Resource documentation
```
"Show me azurerm_kubernetes_cluster arguments"
"How do I create an Azure Container Registry with geo-replication?"
```
Gets current resource schema with examples.
### Module discovery
```
"Find Azure networking modules"
"Show inputs for Azure/vnet/azurerm module"
```
Searches registry and retrieves module documentation.
### Capabilities check
```
"What resources are available in the azurerm provider?"
"Does the AzureRM provider support Azure Container Apps?"
```
Returns available resource types and data sources.
## Workflow example
**1. Start a new configuration**
```
You: "Create an AKS cluster with the latest best practices"
```
Copilot queries MCP for:
- Latest azurerm provider version
- Current azurerm_kubernetes_cluster schema
- Recommended settings and examples
**2. Add a module**
```
You: "Find a verified module for Azure Key Vault"
```
Copilot searches the registry and gives you options with:
- Module source path
- Input variables
- Usage examples
**3. Check compatibility**
```
You: "Does that module work with azurerm 4.0?"
```
Copilot checks the module's provider requirements.
## Troubleshooting
MCP not responding:
- Check Docker is running: `docker ps`
- Reload the VS Code window
- Check that the GitHub Copilot extension is updated
Token errors:
- `TFE_TOKEN` is only needed for private registries
- Leave it blank for public registry access
- Get a token from https://app.terraform.io/app/settings/tokens
Rate limits:
- Public registry limits apply
- Use an HCP Terraform token for higher limits
## Without MCP
If MCP isn't available, use these directly:
- Providers: https://registry.terraform.io/browse/providers
- Modules: https://registry.terraform.io/browse/modules
- Docs: https://developer.hashicorp.com/terraform
## Benefits
- Current documentation from the registry
- No manual registry searches
- Faster code generation
- Easier module discovery
- Quick compatibility checks
## Next steps
Try asking Copilot to generate a complete Terraform configuration using the latest provider versions and modules. See [lesson 18](../18-testing/) to learn how to validate your code.
================================================
FILE: 24-cleanup/README.md
================================================
# Cleanup Resources
After completing the course, clean up all Azure resources to avoid unnecessary charges. This lesson provides scripts and commands to remove everything created throughout the tutorials.
## What was created during the course
Throughout the course, you created:
**Storage Infrastructure (Lesson 9)**:
- Resource group: `rg-terraform-state`
- Storage account: `sttfstate<random>` (unique name)
- Multiple containers: `tfstate`, `dependson`, `foreach`, `count`, `conditional`, `dynamicblocks`, `keyvault`, `modules`, `azapi`
**Demo Resource Groups** (Lessons 8-17):
- `rg-demo-local` (Lesson 8: Local state)
- `rg-demo` (Lesson 9: Remote state)
- `rg-demo-depends` (Lesson 10: Dependencies)
- `rg-demo-foreach` (Lesson 11: For Each)
- `rg-demo-count` (Lesson 12: Count)
- `rg-demo-conditional` (Lesson 13: Conditionals)
- `rg-demo-dynamic` (Lesson 14: Dynamic blocks)
- `rg-demo-keyvault` (Lesson 15: Key Vault)
- `rg-demo-modules` (Lesson 16: Modules)
- `rg-demo-azapi` (Lesson 17: AzAPI)
**Additional Resources**:
- Storage accounts (within demo resource groups)
- Key Vault instances with secrets and RBAC assignments
- Azure Container Registry instances
- Virtual networks and subnets
- Network security groups with rules
- Various test resources from lessons 18-23
## Automated cleanup script
Use the provided script to check for and delete all course-related resources.
### Step 1: Review what exists
First, see what resources remain in your subscription:
```bash
cd 24-cleanup/scripts
chmod +x cleanup-all-resources.sh
./cleanup-all-resources.sh --check
```
This lists all resource groups matching the course naming patterns without deleting anything.
### Step 2: Run cleanup
After reviewing the list, run the full cleanup:
```bash
./cleanup-all-resources.sh --delete
```
The script:
- Lists all course-related resource groups
- Asks for confirmation before deletion
- Deletes each resource group and its contents
- Removes the Terraform state storage account
- Displays a summary of deleted resources
**Warning**: This permanently deletes all resources. Make sure you don't need any of them before proceeding.
## Manual cleanup
If you prefer manual cleanup or need to selectively remove resources, use these commands.
### Check for demo resource groups
```bash
az group list --query "[?starts_with(name, 'rg-demo')].{Name:name, Location:location}" --output table
```
### Delete individual resource groups
```bash
az group delete --name rg-demo-local --yes --no-wait
az group delete --name rg-demo --yes --no-wait
az group delete --name rg-demo-depends --yes --no-wait
az group delete --name rg-demo-foreach --yes --no-wait
az group delete --name rg-demo-count --yes --no-wait
az group delete --name rg-demo-conditional --yes --no-wait
az group delete --name rg-demo-dynamic --yes --no-wait
az group delete --name rg-demo-keyvault --yes --no-wait
az group delete --name rg-demo-modules --yes --no-wait
az group delete --name rg-demo-azapi --yes --no-wait
```
The `--no-wait` flag allows deletions to run in parallel, speeding up cleanup.
### Check deletion status
```bash
az group list --query "[?starts_with(name, 'rg-demo')].{Name:name, ProvisioningState:properties.provisioningState}" --output table
```
### Delete the Terraform state storage
**Delete this last** after all other resources are removed:
```bash
az group delete --name rg-terraform-state --yes
```
This removes:
- The storage account
- All state containers
- All state files
- The resource group itself
## Using Terraform destroy
Alternatively, navigate to each lesson's example directory and run `terraform destroy`:
```bash
# Lesson 8 - Local State
cd 08-state-local/examples/local-state-example
terraform destroy
# Lesson 9 - Remote State
cd ../../../09-state-remote/examples/remote-state-example
terraform destroy
# Lesson 10 - Dependencies
cd ../../../10-advanced-dependencies/examples/terraform
terraform destroy
# Continue for each lesson...
```
This approach:
- Ensures Terraform properly removes all dependencies
- Updates state files to reflect the deletion
- May take longer than direct Azure CLI deletion
- Useful if you want to preserve state files for reference
## Verify complete cleanup
After cleanup, verify no course resources remain:
```bash
# Check for any remaining demo resource groups
az group list --query "[?starts_with(name, 'rg-demo') || starts_with(name, 'rg-terraform-state')].name" --output table
# Check for storage accounts with course pattern (if you used custom names)
az storage account list --query "[?starts_with(name, 'sttfstate')].{Name:name, ResourceGroup:resourceGroup}" --output table
# List all resource groups (review for any missed resources)
az group list --output table
```
If the commands return no results, cleanup is complete.
## Troubleshooting
### Resource group deletion fails
Some resources have dependencies that prevent deletion:
```bash
# Get detailed error
az group delete --name rg-demo-keyvault --yes --verbose
```
**Key Vault with purge protection**: Key Vaults with purge protection enabled (lesson 15) need special handling:
```bash
# List deleted vaults
az keyvault list-deleted --query "[].{Name:name, Location:properties.location, DeletionDate:properties.deletionDate}" --output table
# Purge the vault (allows resource group deletion)
az keyvault purge --name kv-demo-abc12345 --location uksouth
```
**Storage account soft delete**: Storage accounts may have soft-deleted containers:
```bash
# Disable soft delete before deletion
az storage account blob-service-properties update \
--account-name sttfstate12345 \
--resource-group rg-terraform-state \
--enable-container-delete-retention-policy false
```
### Cannot authenticate
If `az` commands fail with authentication errors:
```bash
# Re-authenticate
az login
# Verify correct subscription
az account show
az account set --subscription "Your Subscription Name"
```
## Cost considerations
Keeping resources running incurs charges:
**Storage Account**: ~$0.02-0.05 per GB per month
**Key Vault**: ~$0.03 per 10,000 operations
**Container Registry**: ~$5/month (Basic tier)
**Virtual Networks**: Usually free, but public IPs cost ~$3-4/month
Delete resources promptly after completing the course to avoid unnecessary costs.
## Best practices for future projects
When working on real projects:
**Use naming conventions**: Consistent names like `rg-project-environment` make bulk operations easier
**Tag resources**: Add tags to identify project, owner, and cost center:
```terraform
resource "azurerm_resource_group" "example" {
name = "rg-production"
location = "uksouth"
tags = {
Environment = "Production"
Project = "MyApp"
ManagedBy = "Terraform"
Owner = "ops-team@company.com"
}
}
```
**Use workspaces or separate state files**: Isolate development, staging, and production
**Enable resource locks**: Prevent accidental deletion of production resources:
```bash
az lock create --name DoNotDelete \
--lock-type CanNotDelete \
--resource-group rg-production
```
**Document cleanup procedures**: Maintain runbooks for removing environments
## Summary
You've completed the Terraform on Azure course and cleaned up all resources. Key takeaways:
- Always clean up demo and test resources to control costs
- Use consistent naming for easier bulk operations
- State storage should be deleted last
- Resource dependencies matter during cleanup
- Terraform destroy vs Azure CLI deletion each have trade-offs
Thank you for completing the course!
================================================
FILE: 24-cleanup/scripts/cleanup-all-resources.sh
================================================
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Function to print colored output
print_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to check if Azure CLI is installed
check_az_cli() {
if ! command -v az &> /dev/null; then
print_error "Azure CLI is not installed. Please install it first."
print_info "Visit: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"
exit 1
fi
}
# Function to check if logged into Azure
check_az_login() {
if ! az account show &> /dev/null; then
print_error "Not logged into Azure. Please run 'az login' first."
exit 1
fi
}
# Function to list all course resource groups
list_course_resources() {
print_info "Scanning for course-related resources..."
echo ""
# Find demo resource groups
DEMO_RGS=$(az group list --query "[?starts_with(name, 'rg-demo')].name" -o tsv)
# Find terraform state resource group
STATE_RG=$(az group list --query "[?name=='rg-terraform-state'].name" -o tsv)
# Count resources
DEMO_COUNT=$(echo "$DEMO_RGS" | grep -c . || echo "0")
STATE_COUNT=$(echo "$STATE_RG" | grep -c . || echo "0")
TOTAL_COUNT=$((DEMO_COUNT + STATE_COUNT))
if [ "$TOTAL_COUNT" -eq 0 ]; then
print_info "No course-related resources found. Cleanup already complete!"
exit 0
fi
echo -e "${GREEN}Found $TOTAL_COUNT resource group(s):${NC}"
echo ""
if [ -n "$DEMO_RGS" ]; then
echo -e "${YELLOW}Demo Resource Groups:${NC}"
echo "$DEMO_RGS" | while read -r rg; do
if [ -n "$rg" ]; then
LOCATION=$(az group show --name "$rg" --query location -o tsv 2>/dev/null || echo "unknown")
RESOURCE_COUNT=$(az resource list --resource-group "$rg" --query "length(@)" -o tsv 2>/dev/null || echo "0")
echo " - $rg (Location: $LOCATION, Resources: $RESOURCE_COUNT)"
fi
done
echo ""
fi
if [ -n "$STATE_RG" ]; then
echo -e "${YELLOW}Terraform State Storage:${NC}"
LOCATION=$(az group show --name "$STATE_RG" --query location -o tsv 2>/dev/null || echo "unknown")
STORAGE_ACCOUNT=$(az storage account list --resource-group "$STATE_RG" --query "[0].name" -o tsv 2>/dev/null || echo "none")
echo " - $STATE_RG (Location: $LOCATION, Storage: $STORAGE_ACCOUNT)"
if [ "$STORAGE_ACCOUNT" != "none" ]; then
CONTAINER_COUNT=$(az storage container list --account-name "$STORAGE_ACCOUNT" --query "length(@)" -o tsv 2>/dev/null || echo "0")
echo " Containers: $CONTAINER_COUNT (tfstate, dependson, foreach, count, conditional, dynamicblocks, keyvault, modules, azapi)"
fi
echo ""
fi
}
# Function to delete all course resources
delete_course_resources() {
print_warning "This will PERMANENTLY DELETE all course resources!"
print_warning "This includes resource groups, storage accounts, and all state files."
echo ""
read -p "Are you sure you want to continue? (type 'yes' to confirm): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
print_info "Cleanup cancelled."
exit 0
fi
echo ""
print_info "Starting cleanup process..."
echo ""
# Delete demo resource groups
DEMO_RGS=$(az group list --query "[?starts_with(name, 'rg-demo')].name" -o tsv)
if [ -n "$DEMO_RGS" ]; then
print_info "Deleting demo resource groups..."
echo "$DEMO_RGS" | while read -r rg; do
if [ -n "$rg" ]; then
print_info "Deleting $rg (running in background)..."
az group delete --name "$rg" --yes --no-wait 2>/dev/null || print_warning "Failed to queue deletion for $rg"
fi
done
echo ""
gitextract_3nsspg_7/ ├── .vscode/ │ └── mcp.json ├── 01-introduction/ │ └── README.md ├── 02-installation/ │ └── README.md ├── 03-vscode-setup/ │ └── README.md ├── 04-core-commands/ │ └── README.md ├── 05-resources-and-data/ │ └── README.md ├── 06-azure-provider/ │ └── README.md ├── 07-variables/ │ ├── 1-terraform-variables.md │ ├── 2-terraform-tfvars.md │ ├── 3-terraform-local-variables.md │ └── README.md ├── 08-state-local/ │ ├── 1-terraform-state-local-vs-remote.md │ ├── 2-terraform-local-state-deploy.md │ ├── README.md │ └── examples/ │ └── local-state-example/ │ ├── main.tf │ └── providers.tf ├── 09-state-remote/ │ ├── README.md │ ├── examples/ │ │ └── remote-state-example/ │ │ ├── main.tf │ │ └── providers.tf │ └── scripts/ │ ├── 1-create-terraform-storage.sh │ └── 2-delete-terraform-storage.sh ├── 10-advanced-dependencies/ │ ├── README.md │ └── examples/ │ └── terraform/ │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── 11-advanced-for-each/ │ ├── README.md │ └── examples/ │ └── terraform/ │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── 12-advanced-count/ │ ├── README.md │ └── examples/ │ └── terraform/ │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── 13-advanced-conditionals/ │ ├── README.md │ └── examples/ │ └── conditional-expressions-example/ │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── 14-advanced-dynamic-blocks/ │ ├── README.md │ └── examples/ │ └── terraform/ │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── 15-secret-management/ │ ├── README.md │ └── examples/ │ └── terraform/ │ ├── main.tf │ ├── providers.tf │ └── variables.tf ├── 16-modules/ │ ├── README.md │ └── examples/ │ └── terraform/ │ ├── main.tf │ ├── modules/ │ │ └── acr/ │ │ ├── main.tf │ │ ├── output.tf │ │ └── variables.tf │ └── providers.tf ├── 17-azapi/ │ ├── README.md │ └── examples/ │ └── terraform/ │ ├── main.tf │ ├── providers.tf │ └── variables.tf ├── 18-testing/ │ ├── README.md │ └── examples/ │ ├── main.tf │ ├── main.tftest.hcl │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── 19-import/ │ └── README.md ├── 20-state-commands/ │ └── README.md ├── 21-pre-post-conditions/ │ ├── README.md │ └── examples/ │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── 22-functions/ │ ├── README.md │ └── examples/ │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── 23-mcp-server/ │ └── README.md ├── 24-cleanup/ │ ├── README.md │ └── scripts/ │ └── cleanup-all-resources.sh ├── README.md └── renovate.json
Condensed preview — 83 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (213K chars).
[
{
"path": ".vscode/mcp.json",
"chars": 705,
"preview": "{\n \"servers\": {\n \"terraform\": {\n \"command\": \"docker\",\n \"args\": [\n \"run\",\n \"-i\",\n \"--r"
},
{
"path": "01-introduction/README.md",
"chars": 766,
"preview": "# Introduction\n\nThis course teaches Terraform on Azure step by step. Each lesson builds on the previous one.\n\n## How thi"
},
{
"path": "02-installation/README.md",
"chars": 1204,
"preview": "# Install Terraform\n\nYou need Terraform installed on your machine to follow this tutorial. Here's how to get it.\n\n## Win"
},
{
"path": "03-vscode-setup/README.md",
"chars": 1235,
"preview": "# Set up your editor\n\nVS Code provides the best experience for writing Terraform. You get syntax highlighting, autocompl"
},
{
"path": "04-core-commands/README.md",
"chars": 1954,
"preview": "# Core Terraform commands\n\nYou'll use these commands constantly. They form the standard workflow for deploying infrastru"
},
{
"path": "05-resources-and-data/README.md",
"chars": 2617,
"preview": "# Resources and data sources\n\nResources are the building blocks of your infrastructure. Each resource block defines one "
},
{
"path": "06-azure-provider/README.md",
"chars": 2191,
"preview": "# Configure the Azure provider\n\nThe Azure provider lets Terraform interact with Azure resources. You configure it once a"
},
{
"path": "07-variables/1-terraform-variables.md",
"chars": 2595,
"preview": "# Use variables\n\nVariables make your Terraform configurations reusable. Instead of hardcoding values, you define them on"
},
{
"path": "07-variables/2-terraform-tfvars.md",
"chars": 2394,
"preview": "# Use variable files\n\nVariable files let you separate configuration values from your Terraform code. This keeps sensitiv"
},
{
"path": "07-variables/3-terraform-local-variables.md",
"chars": 3251,
"preview": "# Use local values\n\nLocal values let you compute values once and reuse them throughout your configuration. Use locals fo"
},
{
"path": "07-variables/README.md",
"chars": 471,
"preview": "# Variables\n\nThis lesson shows how to make Terraform configurations reusable with variables and locals.\n\n## Lessons\n\n- ["
},
{
"path": "08-state-local/1-terraform-state-local-vs-remote.md",
"chars": 2202,
"preview": "# Understand Terraform state\n\nTerraform state tracks the resources it manages. Every time you run `terraform apply`, Ter"
},
{
"path": "08-state-local/2-terraform-local-state-deploy.md",
"chars": 3125,
"preview": "# Deploy with local state\n\nYou'll deploy a simple Azure resource using local state to see how Terraform tracks infrastru"
},
{
"path": "08-state-local/README.md",
"chars": 377,
"preview": "# State: local\n\nThis lesson introduces Terraform state and runs a local-state deployment.\n\n## Lessons\n\n- [Local vs remot"
},
{
"path": "08-state-local/examples/local-state-example/main.tf",
"chars": 94,
"preview": "resource \"azurerm_resource_group\" \"rg\" {\n name = \"rg-demo-local\"\n location = \"uksouth\"\n}"
},
{
"path": "08-state-local/examples/local-state-example/providers.tf",
"chars": 187,
"preview": "terraform {\n backend \"local\" {\n }\n\n required_providers {\n azurerm = {\n source = \"hashicorp/azurerm\"\n ve"
},
{
"path": "09-state-remote/README.md",
"chars": 4945,
"preview": "# Deploy with remote state\n\nRemote state stores your state file in Azure Storage. This enables team collaboration and pr"
},
{
"path": "09-state-remote/examples/remote-state-example/main.tf",
"chars": 88,
"preview": "resource \"azurerm_resource_group\" \"rg\" {\n name = \"rg-demo\"\n location = \"uksouth\"\n}"
},
{
"path": "09-state-remote/examples/remote-state-example/providers.tf",
"chars": 455,
"preview": "terraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_account_name = \"Y"
},
{
"path": "09-state-remote/scripts/1-create-terraform-storage.sh",
"chars": 503,
"preview": "#!/usr/bin/env bash\n#set -x\n\n# Creates the relevant storage account to store terraform state locally\n\nRESOURCE_GROUP_NAM"
},
{
"path": "09-state-remote/scripts/2-delete-terraform-storage.sh",
"chars": 198,
"preview": "#!/usr/bin/env bash\n#set -x\n\n# Deletes the relevant storage account to store terraform state\n\nRESOURCE_GROUP_NAME=\"deplo"
},
{
"path": "10-advanced-dependencies/README.md",
"chars": 3209,
"preview": "# Control resource dependencies\n\nTerraform automatically determines resource dependencies from references. Use `depends_"
},
{
"path": "10-advanced-dependencies/examples/terraform/main.tf",
"chars": 1176,
"preview": "resource \"azurerm_resource_group\" \"rg\" {\n name = var.resource_group_name\n location = \"uksouth\"\n}\n\nresource \"azurer"
},
{
"path": "10-advanced-dependencies/examples/terraform/outputs.tf",
"chars": 540,
"preview": "output \"resource_group_id\" {\n description = \"ID of the resource group\"\n value = azurerm_resource_group.rg.id\n}\n\n"
},
{
"path": "10-advanced-dependencies/examples/terraform/providers.tf",
"chars": 457,
"preview": "terraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_account_name = \"Y"
},
{
"path": "10-advanced-dependencies/examples/terraform/variables.tf",
"chars": 80,
"preview": "variable \"resource_group_name\" {\n type = string\n default = \"rg-demo-depends\"\n}"
},
{
"path": "11-advanced-for-each/README.md",
"chars": 3060,
"preview": "# Create multiple resources with for_each\n\nThe `for_each` argument creates multiple instances of a resource. Use it when"
},
{
"path": "11-advanced-for-each/examples/terraform/main.tf",
"chars": 649,
"preview": "resource \"azurerm_resource_group\" \"rg\" {\n for_each = toset(var.resource_group_names)\n name = each.key\n location ="
},
{
"path": "11-advanced-for-each/examples/terraform/outputs.tf",
"chars": 353,
"preview": "output \"resource_group_ids\" {\n description = \"Map of environment names to resource group IDs\"\n value = { for k, "
},
{
"path": "11-advanced-for-each/examples/terraform/providers.tf",
"chars": 536,
"preview": "terraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_account_name = \"Y"
},
{
"path": "11-advanced-for-each/examples/terraform/variables.tf",
"chars": 99,
"preview": "variable \"resource_group_names\" {\n type = list(string)\n default = [\"dev\", \"staging\", \"prod\"]\n}"
},
{
"path": "12-advanced-count/README.md",
"chars": 3212,
"preview": "# Create multiple resources with count\n\nThe `count` argument creates multiple identical resources. Use it when you need "
},
{
"path": "12-advanced-count/examples/terraform/main.tf",
"chars": 160,
"preview": "resource \"azurerm_resource_group\" \"rg\" {\n count = 3\n name = \"rg-demo-${count.index}\"\n location = \"uksouth\"\n \n"
},
{
"path": "12-advanced-count/examples/terraform/outputs.tf",
"chars": 463,
"preview": "# Reference the first resource group\noutput \"first_rg_id\" {\n description = \"ID of the first resource group\"\n value "
},
{
"path": "12-advanced-count/examples/terraform/providers.tf",
"chars": 453,
"preview": "terraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_account_name = \"Y"
},
{
"path": "12-advanced-count/examples/terraform/variables.tf",
"chars": 124,
"preview": "variable \"instance_count\" {\n description = \"Number of resource groups to create\"\n type = number\n default ="
},
{
"path": "13-advanced-conditionals/README.md",
"chars": 3739,
"preview": "# Use conditional expressions\n\nConditional expressions let you make decisions in your Terraform configuration. They use "
},
{
"path": "13-advanced-conditionals/examples/conditional-expressions-example/main.tf",
"chars": 884,
"preview": "resource \"azurerm_resource_group\" \"rg\" {\n name = var.resource_group_name\n location = var.environment == \"prod\" ? \""
},
{
"path": "13-advanced-conditionals/examples/conditional-expressions-example/outputs.tf",
"chars": 703,
"preview": "output \"resource_group_name\" {\n description = \"Name of the resource group\"\n value = azurerm_resource_group.rg.na"
},
{
"path": "13-advanced-conditionals/examples/conditional-expressions-example/providers.tf",
"chars": 540,
"preview": "terraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_account_name = \"Y"
},
{
"path": "13-advanced-conditionals/examples/conditional-expressions-example/variables.tf",
"chars": 397,
"preview": "variable \"resource_group_name\" {\n description = \"Name of the resource group\"\n type = string\n default = \"rg"
},
{
"path": "14-advanced-dynamic-blocks/README.md",
"chars": 5268,
"preview": "# Use dynamic blocks\n\nDynamic blocks generate repeated nested blocks within a resource. Use them when you need multiple "
},
{
"path": "14-advanced-dynamic-blocks/examples/terraform/main.tf",
"chars": 1016,
"preview": "resource \"azurerm_resource_group\" \"rg\" {\n name = var.resource_group_name\n location = \"uksouth\"\n}\n\nresource \"azurer"
},
{
"path": "14-advanced-dynamic-blocks/examples/terraform/outputs.tf",
"chars": 380,
"preview": "output \"resource_group_name\" {\n description = \"Name of the resource group\"\n value = azurerm_resource_group.rg.na"
},
{
"path": "14-advanced-dynamic-blocks/examples/terraform/providers.tf",
"chars": 460,
"preview": "terraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_account_name = \"Y"
},
{
"path": "14-advanced-dynamic-blocks/examples/terraform/variables.tf",
"chars": 1379,
"preview": "variable \"resource_group_name\" {\n description = \"Name of the resource group\"\n type = string\n default = \"rg"
},
{
"path": "15-secret-management/README.md",
"chars": 4709,
"preview": "# Manage secrets with Azure Key Vault\n\nStore sensitive values in Azure Key Vault instead of putting them directly in you"
},
{
"path": "15-secret-management/examples/terraform/main.tf",
"chars": 1762,
"preview": "data \"azurerm_client_config\" \"current\" {}\n\nresource \"random_string\" \"suffix\" {\n length = 8\n special = false\n upper "
},
{
"path": "15-secret-management/examples/terraform/providers.tf",
"chars": 577,
"preview": "terraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_account_name = \"YO"
},
{
"path": "15-secret-management/examples/terraform/variables.tf",
"chars": 84,
"preview": "variable \"resource_group_name\" {\n type = string\n default = \"rg-demo-keyvault\"\n}"
},
{
"path": "16-modules/README.md",
"chars": 6689,
"preview": "# Build reusable modules\n\nModules package Terraform configurations into reusable components. Instead of copying resource"
},
{
"path": "16-modules/examples/terraform/main.tf",
"chars": 219,
"preview": "module \"acr\" {\n source = \"./modules/acr\"\n\n resource_group_name = \"rg-demo-modules\"\n location = \"UK South\"\n"
},
{
"path": "16-modules/examples/terraform/modules/acr/main.tf",
"chars": 431,
"preview": "# Resource Group\nresource \"azurerm_resource_group\" \"rg\" {\n name = var.resource_group_name\n location = var.location"
},
{
"path": "16-modules/examples/terraform/modules/acr/output.tf",
"chars": 63,
"preview": "output \"acr_id\" {\n value = azurerm_container_registry.acr.id\n}"
},
{
"path": "16-modules/examples/terraform/modules/acr/variables.tf",
"chars": 586,
"preview": "variable \"resource_group_name\" {\n type = string\n description = \"The name of the resource group in which to crea"
},
{
"path": "16-modules/examples/terraform/providers.tf",
"chars": 432,
"preview": "terraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_account_name = \"YOUR_STORA"
},
{
"path": "17-azapi/README.md",
"chars": 5972,
"preview": "# Use the AzAPI provider\n\nThe AzAPI provider lets you use any Azure resource type on day one, even before the AzureRM pr"
},
{
"path": "17-azapi/examples/terraform/main.tf",
"chars": 451,
"preview": "resource \"azurerm_resource_group\" \"rg\" {\n name = var.resource_group_name\n location = var.location\n}\n\nresource \"aza"
},
{
"path": "17-azapi/examples/terraform/providers.tf",
"chars": 531,
"preview": "provider \"azapi\" {\n}\n\nterraform {\n backend \"azurerm\" {\n resource_group_name = \"rg-terraform-state\"\n storage_acco"
},
{
"path": "17-azapi/examples/terraform/variables.tf",
"chars": 469,
"preview": "variable \"resource_group_name\" {\n type = string\n description = \"The name of the resource group in which to crea"
},
{
"path": "18-testing/README.md",
"chars": 10714,
"preview": "# Test your Terraform code\n\nTesting catches errors before they reach Azure. Use multiple testing approaches to validate "
},
{
"path": "18-testing/examples/main.tf",
"chars": 817,
"preview": "resource \"azurerm_resource_group\" \"test\" {\n name = var.resource_group_name\n location = var.location\n \n tags = {\n"
},
{
"path": "18-testing/examples/main.tftest.hcl",
"chars": 3661,
"preview": "# Test that dev environment uses correct SKUs\nrun \"test_dev_environment\" {\n command = plan\n\n variables {\n resource_"
},
{
"path": "18-testing/examples/outputs.tf",
"chars": 664,
"preview": "output \"resource_group_name\" {\n description = \"Name of the resource group\"\n value = azurerm_resource_group.test."
},
{
"path": "18-testing/examples/providers.tf",
"chars": 276,
"preview": "terraform {\n required_version = \">= 1.6\"\n \n required_providers {\n azurerm = {\n source = \"hashicorp/azurerm\"\n"
},
{
"path": "18-testing/examples/variables.tf",
"chars": 627,
"preview": "variable \"resource_group_name\" {\n description = \"Name of the resource group\"\n type = string\n default = \"rg"
},
{
"path": "19-import/README.md",
"chars": 7801,
"preview": "# Import existing Azure resources\n\nYou can bring existing Azure resources under Terraform management without recreating "
},
{
"path": "20-state-commands/README.md",
"chars": 8099,
"preview": "# Manage Terraform state\n\nTerraform state commands let you inspect, modify, and troubleshoot your state file. These comm"
},
{
"path": "21-pre-post-conditions/README.md",
"chars": 17467,
"preview": "# Use pre-conditions and post-conditions\n\nCatch configuration errors early with lifecycle validation rules. Check inputs"
},
{
"path": "21-pre-post-conditions/examples/main.tf",
"chars": 5626,
"preview": "locals {\n # Approved regions for each environment\n approved_regions = {\n dev = [\"uksouth\", \"ukwest\"]\n stagin"
},
{
"path": "21-pre-post-conditions/examples/outputs.tf",
"chars": 1845,
"preview": "output \"resource_group_name\" {\n description = \"Name of the validated resource group\"\n value = azurerm_resource_g"
},
{
"path": "21-pre-post-conditions/examples/providers.tf",
"chars": 276,
"preview": "terraform {\n required_version = \">= 1.2\"\n \n required_providers {\n azurerm = {\n source = \"hashicorp/azurerm\"\n"
},
{
"path": "21-pre-post-conditions/examples/variables.tf",
"chars": 646,
"preview": "variable \"environment\" {\n description = \"Environment name\"\n type = string\n default = \"dev\"\n \n validation"
},
{
"path": "22-functions/README.md",
"chars": 20554,
"preview": "# Use Terraform functions\n\nTerraform includes over 100 built-in functions for manipulating data. Master the most useful "
},
{
"path": "22-functions/examples/main.tf",
"chars": 6891,
"preview": "locals {\n # String manipulation functions\n clean_app_name = lower(replace(var.app_name, \" \", \"-\"))\n app_name_upper = "
},
{
"path": "22-functions/examples/outputs.tf",
"chars": 2383,
"preview": "output \"resource_group_name\" {\n description = \"Generated resource group name\"\n value = azurerm_resource_group.ex"
},
{
"path": "22-functions/examples/providers.tf",
"chars": 195,
"preview": "terraform {\n required_version = \">= 1.0\"\n \n required_providers {\n azurerm = {\n source = \"hashicorp/azurerm\"\n"
},
{
"path": "22-functions/examples/variables.tf",
"chars": 463,
"preview": "variable \"environment\" {\n description = \"Environment name\"\n type = string\n default = \"dev\"\n}\n\nvariable \"ap"
},
{
"path": "23-mcp-server/README.md",
"chars": 3579,
"preview": "# Terraform MCP Server\n\nGet real-time Terraform provider documentation, module details, and registry information directl"
},
{
"path": "24-cleanup/README.md",
"chars": 7573,
"preview": "# Cleanup Resources\n\nAfter completing the course, clean up all Azure resources to avoid unnecessary charges. This lesson"
},
{
"path": "24-cleanup/scripts/cleanup-all-resources.sh",
"chars": 6707,
"preview": "#!/bin/bash\nset -e\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\n# Function"
},
{
"path": "README.md",
"chars": 3950,
"preview": "# Terraform on Azure\n\nA step-by-step course for deploying and managing Azure infrastructure with Terraform. Learn from b"
},
{
"path": "renovate.json",
"chars": 114,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"config:recommended\"\n ]\n}\n"
}
]
About this extraction
This page contains the full source code of the thomast1906/terraform-on-azure GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 83 files (190.2 KB), approximately 49.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.