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 = ``` ## 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 --query id -o tsv # Storage account az storage account show --name --resource-group --query id -o tsv # Key Vault az keyvault show --name --query id -o tsv # Virtual network az network vnet show --name --resource-group --query id -o tsv # Any resource (if you know the type) az resource show --name --resource-group --resource-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` (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 "" fi # Wait for demo resource groups to finish deleting if [ -n "$DEMO_RGS" ]; then print_info "Waiting for demo resource groups to delete (this may take several minutes)..." echo "$DEMO_RGS" | while read -r rg; do if [ -n "$rg" ]; then while az group show --name "$rg" &> /dev/null; do echo -n "." sleep 10 done print_info "$rg deleted successfully" fi done echo "" fi # Delete terraform state resource group (must be last) STATE_RG=$(az group list --query "[?name=='rg-terraform-state'].name" -o tsv) if [ -n "$STATE_RG" ]; then print_info "Deleting Terraform state storage..." # Get storage account name before deletion STORAGE_ACCOUNT=$(az storage account list --resource-group "$STATE_RG" --query "[0].name" -o tsv 2>/dev/null || echo "none") az group delete --name "$STATE_RG" --yes 2>/dev/null || print_warning "Failed to delete $STATE_RG" print_info "$STATE_RG deleted (storage account: $STORAGE_ACCOUNT)" echo "" fi # Verify cleanup print_info "Verifying cleanup..." REMAINING=$(az group list --query "[?starts_with(name, 'rg-demo') || name=='rg-terraform-state'].name" -o tsv) if [ -z "$REMAINING" ]; then print_info "✓ Cleanup complete! All course resources have been removed." else print_warning "Some resources may still be deleting:" echo "$REMAINING" print_info "Run this script again in a few minutes to verify." fi } # Function to display usage usage() { echo "Usage: $0 [--check|--delete]" echo "" echo "Options:" echo " --check List all course-related resources without deleting" echo " --delete Delete all course-related resources (requires confirmation)" echo "" echo "Examples:" echo " $0 --check # Preview what will be deleted" echo " $0 --delete # Perform cleanup" } # Main execution main() { if [ "$#" -eq 0 ]; then usage exit 1 fi check_az_cli check_az_login SUBSCRIPTION=$(az account show --query name -o tsv) print_info "Using Azure subscription: $SUBSCRIPTION" echo "" case "$1" in --check) list_course_resources ;; --delete) list_course_resources echo "" delete_course_resources ;; *) print_error "Invalid option: $1" echo "" usage exit 1 ;; esac } main "$@" ================================================ FILE: README.md ================================================ # Terraform on Azure A step-by-step course for deploying and managing Azure infrastructure with Terraform. Learn from basics to advanced patterns through hands-on examples. ## Start here Begin with [Lesson 01: Introduction](01-introduction/) ## Course outline ### Getting Started - [01. Introduction](01-introduction/) - Get familiar with Terraform and Infrastructure as Code - [02. Install Terraform](02-installation/) - Set up Terraform on your machine - [03. Set up VS Code](03-vscode-setup/) - Configure your development environment - [04. Core Terraform commands](04-core-commands/) - Learn init, plan, apply, destroy - [05. Resources and data sources](05-resources-and-data/) - Create and query Azure resources - [06. Azure provider setup](06-azure-provider/) - Configure authentication and providers ### Variables and State - [07. Variables](07-variables/) - Make configurations reusable with variables - [08. State: local](08-state-local/) - Understand Terraform state management - [09. State: remote](09-state-remote/) - Store state in Azure Storage for teams ### Advanced Patterns - [10. Advanced: depends_on](10-advanced-dependencies/) - Control resource dependencies - [11. Advanced: for_each](11-advanced-for-each/) - Create multiple similar resources - [12. Advanced: count](12-advanced-count/) - Create multiple identical resources - [13. Advanced: conditionals](13-advanced-conditionals/) - Make configuration decisions - [14. Advanced: dynamic blocks](14-advanced-dynamic-blocks/) - Generate nested blocks ### Production Best Practices - [15. Secret management](15-secret-management/) - Secure secrets with Azure Key Vault - [16. Modules](16-modules/) - Build reusable infrastructure components - [17. AzAPI provider](17-azapi/) - Use day-one Azure resources - [18. Testing](18-testing/) - Validate configurations with terraform test ### Workflow and Operations - [19. Import](19-import/) - Bring existing Azure resources under Terraform - [20. State commands](20-state-commands/) - Inspect and manage state - [21. Pre- and post-conditions](21-pre-post-conditions/) - Add validation to resources - [22. Terraform functions](22-functions/) - Transform and manipulate data - [23. Terraform MCP server](23-mcp-server/) - Use AI assistance with Terraform ### Course Completion - [24. Cleanup](24-cleanup/) - Remove all Azure resources and avoid charges ## What you will learn - Terraform fundamentals: core concepts, workflow, and Azure provider - State management: local and remote state with Azure Storage backend - Advanced patterns: resource dependencies, loops, conditionals, dynamic blocks - Production practices: secret management, testing, modules, validation - Operations: import existing resources, state manipulation, troubleshooting - Modern tooling: AzAPI provider, Terraform test framework, AI assistance ## Prerequisites - Azure subscription ([free account](https://azure.microsoft.com/free/)) - Basic command-line knowledge - Understanding of cloud infrastructure concepts ## How the course is structured - 24 progressive lessons from basics to advanced topics and cleanup - Each lesson has a `README.md` with explanations and steps - Hands-on examples in `examples/` directories with working Terraform code - Examples use azurerm 4.0+ with latest provider features - Lesson 24 provides scripts to clean up all resources and avoid charges ## Get started ```bash # Clone the repository git clone https://github.com/thomast1906/terraform-on-azure.git cd terraform-on-azure # Start with lesson 01 cd 01-introduction ``` ## Get help - Issues: [Report bugs or request features](https://github.com/thomast1906/terraform-on-azure/issues) - Discussions: Ask questions and share your experience ## Contributing Contributions welcome! Open a pull request to: - Fix errors or typos - Improve explanations - Add new examples - Suggest new lessons ## License This course is provided for educational purposes. ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ] }