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