Multi-Environment Azure IaC with Terraform

Deploy dev and prod Azure environments from one Terraform codebase.

Introduction

30 Second Summary

You would never test a new recipe on dinner guests without trying it yourself first. Software teams work the same way, keeping a separate testing environment to try changes before pushing them to real users.

In this project, you will use Terraform to manage two parallel Azure environments from a single codebase. Your infrastructure state gets stored remotely in Azure Blob Storage, keeping your whole team in sync.

What You'll Build

Imagine typing one command and watching a complete set of cloud resources spin up in Azure, then switching workspaces and deploying a second, isolated copy from the exact same code.

By the end of this project, you'll have:

  • Switch between dev and prod with terraform workspace select and deploy completely isolated infrastructure from the same configuration files, each customized with its own .tfvars settings.
  • Run terraform plan and preview every change before anything touches Azure. See exactly which resources get added, changed, or destroyed.
  • Check your remote state blob in the Azure Portal and see that Terraform locked it during operations. No teammate can run a conflicting change while you are deploying.
  • Secret Mission: Add conditional resources that only exist in production, using Terraform's count meta-argument to control what deploys where.

Are there any prerequisites?

You need an Azure account (free tier eligible). Terraform CLI and Azure CLI installation are covered in Step 1.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Tools

Before you can write any Terraform configuration, you need two command-line tools installed and talking to each other.

Terraform CLI translates your .tf files into real cloud resources. Azure CLI handles authentication so Terraform can reach your Azure subscription. In this step, you will install both tools, sign in to Azure, and set up your project directory.

In this step, get ready to:
  • Install Terraform CLI and Azure CLI.
  • Authenticate to Azure and confirm your subscription.
  • Create the project directory and open it in Cursor with the Terraform extension.
Install Terraform CLI and Azure CLI

You will use a terminal for every command in this project. Open one now.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check whether Terraform is already installed by running this command:
terraform version

✔️ I see version 1.15.0 or higher

Terraform is ready. Skip ahead to the Azure CLI check below.

ⓧ I see an older version

You need version 1.15.0 or higher. Follow the instructions for your platform below to upgrade.

macOS

  • Upgrade Terraform by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
  • Close and reopen your terminal, then run terraform version again to confirm you see 1.15.0 or higher.

Windows

  • Download the latest Windows AMD64 binary from the official Terraform install page.
  • Unzip the downloaded file and replace the existing terraform.exe in your PATH with the new one.
  • Close and reopen your terminal, then run terraform version again to confirm the update.

Linux

  • Add the HashiCorp apt repository and install Terraform by running these commands:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
  • Close and reopen your terminal, then run terraform version again to confirm the update.

ⓧ Command not found

Terraform is not installed yet. Follow the instructions for your platform below.

macOS

  • Install Terraform using Homebrew by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
  • Close and reopen your terminal, then run terraform version to confirm the install.

Windows

  • Download the latest Windows AMD64 binary from the official Terraform install page.
  • Unzip the downloaded file to a folder such as C:\terraform.
  • Add that folder to your system PATH. Press the Windows key, search for Environment Variables, edit the Path variable, and add the folder path.
  • Close and reopen your terminal, then run terraform version to confirm the install.

Linux

  • Add the HashiCorp apt repository and install Terraform by running these commands:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
  • Close and reopen your terminal, then run terraform version to confirm the install.
  • Check whether Azure CLI is already installed by running this command:
az version

✔️ I see a version number

Azure CLI is ready. Move on to the authentication substep below.

ⓧ Command not found

Azure CLI is not installed yet. Follow the instructions for your platform below.

macOS

  • Install Azure CLI using Homebrew by running this command:
brew update && brew install azure-cli
  • Close and reopen your terminal, then run az version to confirm the install.

Windows

  • Install Azure CLI using WinGet by running this command in PowerShell:
winget install --exact --id Microsoft.AzureCLI
  • Close and reopen your terminal, then run az version to confirm the install.

Linux

  • Install Azure CLI using Microsoft's install script by running this command:
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
  • Close and reopen your terminal, then run az version to confirm the install.

Still seeing command not found?

Make sure you closed and reopened your terminal after installing. If the problem persists, check that the install location is in your system PATH.

help me troubleshoot why terraform or az is not found after installing

Authenticate to Azure

Terraform needs permission to create resources in your Azure subscription. The Azure CLI handles this by opening a browser sign-in flow.

  • Sign in to Azure by running this command:
az login

What does this command do?

Running az login opens your default browser and asks you to sign in with your Azure credentials. Once you sign in, the CLI stores a token locally so every subsequent az and terraform command can authenticate automatically.

  • A browser window will open. Sign in with the same Microsoft account you used to create your Azure account.
  • After signing in, confirm you are connected to the correct subscription by running this command:
az account show

You should see JSON output showing your subscription name and ID.

Seeing the wrong subscription?

If you have multiple Azure subscriptions, the CLI may default to a different one. You can switch to the correct subscription using the az account set command with your desired subscription ID.

help me switch to a different Azure subscription

Create the project directory and open in Cursor
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project directory and move into it by running these commands:
mkdir terraform-azure-multienv
cd terraform-azure-multienv
  • Confirm the directory was created by running:
ls ~/Desktop

You should see terraform-azure-multienv listed in the output.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • In Cursor, click File in the top menu bar.
  • Click Open Folder.
  • Select the terraform-azure-multienv folder on your Desktop, then click Open.

Now install the Terraform extension for syntax highlighting and autocompletion.

  • Open the Extensions sidebar by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type HashiCorp Terraform in the search bar.
  • Click Install on the extension published by HashiCorp.

Why install this extension?

The HashiCorp Terraform extension adds syntax highlighting, autocompletion, and inline error detection for .tf files. It catches typos and missing arguments before you run terraform plan, saving you debugging time.

Before you move on, do a final check of both tools. What do you think each command will print?

  • Open a terminal inside Cursor by pressing Ctrl+` (both macOS and Windows).
  • Verify Terraform is available by running this command:
terraform version
  • Verify Azure CLI is available by running this command:
az account show

Terraform prints Terraform v1.15.8 (or a higher patch version). Azure CLI prints your subscription details as JSON.

Cursor terminal not finding the commands?

Cursor may not pick up new PATH changes from your system terminal. Close Cursor completely and reopen it. Then open a new terminal inside Cursor and try again.

help me get terraform and az commands working inside the Cursor integrated terminal

Your environment is ready. Next up, you will write your first Terraform configuration and deploy real Azure resources.

Deploy Your First Azure Resources

Your tools are installed and authenticated. Now it is time to see Terraform in action by deploying real infrastructure to Azure.

Before learning modules, remote state, or workspaces, you need to see the basic Terraform workflow end to end. Writing a config, running plan, and applying it gives you the foundation everything else builds on. Your state will be stored locally for now.

In this step, get ready to:
  • Write a reusable Terraform module that defines an Azure Resource Group and Storage Account.
  • Configure the root Terraform project to call the module and initialize the provider.
  • Run terraform plan and terraform apply to deploy real resources to Azure.
Write the infrastructure module

Terraform modules let you package infrastructure as reusable blocks. Instead of writing the same resource definitions for dev and prod later, you define them once inside a module and call that module from different environments.

  • Create the module directory by running this command in your terminal:
mkdir -p modules/infra
  • In Cursor, create a new file at modules/infra/main.tf and paste the following code:
# Create a resource group to hold all infrastructure
resource "azurerm_resource_group" "main" {
  name     = "rg-${var.project_name}-${var.environment}"
  location = var.location

  tags = {
    project     = var.project_name
    environment = var.environment
    managed_by  = "terraform"
  }
}

# Create a storage account inside the resource group
resource "azurerm_storage_account" "main" {
  name                     = "${var.project_name}${var.environment}sa"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

  tags = azurerm_resource_group.main.tags
}

What does this code do?

This file defines two Azure resources inside the module. The azurerm_resource_group creates a logical container for all your project's resources. Its name combines the project name and environment, so dev and prod get separate groups.

The azurerm_storage_account creates a cloud storage account inside that resource group. It references the resource group's name and location directly, so the two resources are always co-located.

  • Save modules/infra/main.tf.
  • Confirm the file was created by running:
ls modules/infra/

You should see main.tf listed in the output.

  • Create a new file at modules/infra/variables.tf and paste the following code:
# Name used in resource naming (e.g. multienv -> rg-multienv-dev)
variable "project_name" {
  description = "Name of the project, used in resource naming"
  type        = string
}

# Environment name with validation to prevent typos
variable "environment" {
  description = "Deployment environment (dev or prod)"
  type        = string

  validation {
    condition     = contains(["dev", "prod"], var.environment)
    error_message = "Environment must be dev or prod."
  }
}

# Azure region where resources will be created
variable "location" {
  description = "Azure region for all resources"
  type        = string
}

What does this code do?

These variables are the module's inputs. Whoever calls this module must provide a project name, an environment, and a location. The validation block on environment restricts it to dev or prod, catching typos before they create misnamed resources.

  • Save modules/infra/variables.tf.
  • Create a new file at modules/infra/outputs.tf and paste the following code:
# Expose the resource group name to the root config
output "resource_group_name" {
  description = "Name of the resource group"
  value       = azurerm_resource_group.main.name
}

# Expose the storage account name to the root config
output "storage_account_name" {
  description = "Name of the storage account"
  value       = azurerm_storage_account.main.name
}

What are module outputs?

Outputs let a module expose specific values to whoever calls it. Without these, the root configuration would have no way to reference the resource group name or storage account name that the module creates.

  • Save modules/infra/outputs.tf.
  • Confirm all three module files exist by running:
ls modules/infra/

You should see main.tf, outputs.tf, and variables.tf listed.

Missing a file?

Make sure you created the files inside modules/infra/, not in the project root. If a file ended up in the wrong location, move it into modules/infra/ using Cursor's file explorer.

Help me fix my Terraform module file structure.

Write the root configuration

The module defines the infrastructure, but it cannot run on its own. You need a root configuration that tells Terraform which provider to use and calls the module with specific values.

  • In Cursor, create a new file called main.tf in the root of your terraform-azure-multienv folder and paste the following code:
# Pin the Terraform version and declare the Azure provider
terraform {
  required_version = ">= 1.15.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "= 5.1.0"
    }
  }
}

# Configure the Azure provider with legacy resource registration
provider "azurerm" {
  resource_provider_registrations = "legacy"
  features {}
}

# Call the infrastructure module with project-level variables
module "infra" {
  source = "./modules/infra"

  project_name = var.project_name
  environment  = var.environment
  location     = var.location
}

What does this code do?

The terraform block pins the CLI to version 1.15.0+ and locks the azurerm provider to exactly 5.1.0. Pinning the provider version prevents surprise breaking changes.

The provider block sets resource_provider_registrations to "legacy" so Azure automatically registers resource providers like Storage and Cosmos DB when you use them. Without this, you would hit confusing registration errors.

The module block calls your modules/infra/ module and passes in the variables it needs.

  • Save main.tf.
  • Create a new file called variables.tf in the root of your project folder and paste the following code:
# Project name used across all resource names
variable "project_name" {
  description = "Name of the project, used in resource naming (lowercase alphanumeric, max 8 chars)"
  type        = string
  default     = "multienv"
}

# Current deployment environment
variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "dev"
}

# Azure region for all resources
variable "location" {
  description = "Azure region for all resources"
  type        = string
  default     = "eastus"
}

Why set defaults here?

The root variables.tf sets defaults so you can run terraform plan without passing values every time. The module's variables.tf has no defaults because the module should never assume values on its own. The root config is the single place where project-wide defaults live.

  • Save variables.tf.
  • Initialize the project by running this command in your terminal:
terraform init

Terraform downloads the azurerm provider, initializes the module, and confirms successful initialization at the end.

Seeing a provider error?

Check that main.tf is in the root of your terraform-azure-multienv folder, not inside modules/infra/. Terraform looks for the provider configuration in the directory where you run the command.

Also make sure your terminal's working directory is terraform-azure-multienv. Run pwd to confirm.

Help me troubleshoot my Terraform initialization.

  • Create a new file called outputs.tf in the root of your project folder and paste the following code:
# Surface the module's resource group name
output "resource_group_name" {
  description = "Name of the resource group"
  value       = module.infra.resource_group_name
}

# Surface the module's storage account name
output "storage_account_name" {
  description = "Name of the storage account"
  value       = module.infra.storage_account_name
}

Why repeat outputs at the root?

Module outputs are only visible inside the module. To surface them when you run terraform output or reference them in other configurations, the root config must re-export them.

  • Save outputs.tf.

✔️ Awesome, I've got everything!

Great. Double check that you have saved all your files before moving on.

ⓧ I'd like to double check the full code

Here is the full main.tf (root):

terraform {
  required_version = ">= 1.15.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "= 5.1.0"
    }
  }
}

provider "azurerm" {
  resource_provider_registrations = "legacy"
  features {}
}

module "infra" {
  source = "./modules/infra"

  project_name = var.project_name
  environment  = var.environment
  location     = var.location
}

Here is the full variables.tf (root):

variable "project_name" {
  description = "Name of the project, used in resource naming (lowercase alphanumeric, max 8 chars)"
  type        = string
  default     = "multienv"
}

variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "dev"
}

variable "location" {
  description = "Azure region for all resources"
  type        = string
  default     = "eastus"
}

Here is the full outputs.tf (root):

output "resource_group_name" {
  description = "Name of the resource group"
  value       = module.infra.resource_group_name
}

output "storage_account_name" {
  description = "Name of the storage account"
  value       = module.infra.storage_account_name
}

Here is the full modules/infra/main.tf:

resource "azurerm_resource_group" "main" {
  name     = "rg-${var.project_name}-${var.environment}"
  location = var.location

  tags = {
    project     = var.project_name
    environment = var.environment
    managed_by  = "terraform"
  }
}

resource "azurerm_storage_account" "main" {
  name                     = "${var.project_name}${var.environment}sa"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

  tags = azurerm_resource_group.main.tags
}

Here is the full modules/infra/variables.tf:

variable "project_name" {
  description = "Name of the project, used in resource naming"
  type        = string
}

variable "environment" {
  description = "Deployment environment (dev or prod)"
  type        = string

  validation {
    condition     = contains(["dev", "prod"], var.environment)
    error_message = "Environment must be dev or prod."
  }
}

variable "location" {
  description = "Azure region for all resources"
  type        = string
}

Here is the full modules/infra/outputs.tf:

output "resource_group_name" {
  description = "Name of the resource group"
  value       = azurerm_resource_group.main.name
}

output "storage_account_name" {
  description = "Name of the storage account"
  value       = azurerm_storage_account.main.name
}
Plan and apply your infrastructure

Your configuration is ready. Terraform's workflow has two phases: plan shows you exactly what will change, and apply makes those changes real.

  • Preview what Terraform will create by running:
terraform plan

Terraform lists every resource it will create, each marked with a + symbol. At the bottom you should see a summary confirming 2 resources to add, with nothing to change or destroy.

What is terraform plan?

The plan command is a dry run. It compares your configuration to the current state (which is empty right now) and shows you what would change. Nothing is created or modified until you run apply.

This is your safety net for catching mistakes before they touch real infrastructure.

Before you run the next command, what do you think will happen when Terraform applies this plan? Will you see any prompts?

  • Deploy the infrastructure by running:
terraform apply
  • Terraform shows the same plan and asks for confirmation. Type yes and press Enter to proceed.

Terraform creates both resources and confirms that 2 resources were added, along with your output values.

Seeing a storage account name error?

Azure storage account names must be globally unique. If the name multienvdevsa is already taken by someone else, open variables.tf (root) and change the project_name default to something unique like your initials plus a number (e.g. jd42env). Then run terraform apply again.

Help me fix my storage account name error.

  • Open the Azure Portal at portal.azure.com.
  • Search for Resource groups in the top search bar and click it.
  • Confirm you see rg-multienv-dev in the list.
  • Click rg-multienv-dev to open it. You should see the storage account multienvdevsa listed inside.

Now take a look at your project directory. What do you think happened locally when Terraform applied?

  • Check your project directory by running:
ls

Notice the terraform.tfstate file that appeared in your project root. This file is Terraform's record of every resource it created and their current attributes.

Why does the state file matter?

Terraform uses terraform.tfstate to know what already exists so it can calculate diffs on the next plan. Right now this file lives on your local disk. If you deleted it, Terraform would forget every resource it just created.

If a teammate cloned this repo, they would start with a blank state and try to recreate everything. You will fix this problem in a later step by migrating state to a remote backend.

Your first two Azure resources are live. Next up, you will extend the module by adding a Cosmos DB database to see how one change to a module ripples across every environment that uses it.

Extend the Module with Cosmos DB

In the last step, you deployed a Resource Group and Storage Account to Azure using Terraform. Your module packages two resources, but you have not yet seen the real payoff of the module pattern.

Adding Cosmos DB to the shared module proves that payoff: one change to the module definition, and every environment that reuses it automatically gets the new resource. You will also run terraform plan against live infrastructure for the first time and see Terraform diff your running environment.

In this step, get ready to:
  • Add an Azure Cosmos DB account to the shared infrastructure module.
  • Wire the new variable and outputs through the root configuration.
  • Run terraform plan to diff your running environment and apply the change.
Add Cosmos DB to the module

Cosmos DB is Azure's globally distributed NoSQL database service. You will add it as a new resource inside your existing modules/infra/ module, right alongside the Resource Group and Storage Account.

  • In Cursor, open modules/infra/main.tf.
  • Add the following Cosmos DB resource block below the azurerm_storage_account resource:
# Provision a Cosmos DB NoSQL database account
resource "azurerm_cosmosdb_account" "main" {
  name                = "${var.project_name}-${var.environment}-cosmos"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  offer_type          = "Standard"
  kind                = "GlobalDocumentDB"
  free_tier_enabled   = var.enable_free_tier

  # Set session-level consistency for balanced reads
  consistency_policy {
    consistency_level = "Session"
  }

  # Required: at least one geo_location for the primary region
  geo_location {
    location          = azurerm_resource_group.main.location
    failover_priority = 0
  }

  tags = azurerm_resource_group.main.tags
}

What does this resource do?

This block tells Terraform to create a Cosmos DB account whose name includes the project name and environment (e.g. multienv-dev-cosmos).

  • offer_type must be "Standard". This is the only valid option Azure accepts.
  • free_tier_enabled controls whether this account uses Azure's always-free Cosmos DB tier (1,000 RU/s and 25 GB). Only one free-tier account is allowed per subscription.
  • consistency_policy and geo_location are both required blocks. Session consistency gives you a good balance of performance and correctness.

The resource references var.enable_free_tier, but that variable does not exist yet. You need to declare it in the module's variables file.

  • Open modules/infra/variables.tf.
  • Add the following variable block below the existing location variable:
# Control whether Cosmos DB uses the free tier (one per subscription)
variable "enable_free_tier" {
  description = "Enable Cosmos DB free tier (only one per subscription)"
  type        = bool
  default     = false
}

Why default to false?

Azure only allows one free-tier Cosmos DB account per subscription. Defaulting to false means you must explicitly opt in. This prevents accidentally claiming the free-tier slot in a throwaway environment.

  • Save both modules/infra/main.tf and modules/infra/variables.tf.

Before you wire anything else, do you think Terraform can already plan the Cosmos DB addition with just the module changes?

  • Check by running this in your terminal:
terraform plan

Because enable_free_tier has a default value in the module, Terraform can plan without the root config passing it explicitly. You should see a summary line:

Plan: 1 to add, 0 to change, 0 to destroy.

What does this diff mean?

Terraform compared its state file (which knows about your Resource Group and Storage Account) with your updated configuration (which now includes Cosmos DB). The diff shows your existing resources are untouched and one new resource will be created.

This is the module pattern in action. You changed the module definition, and Terraform figured out exactly what is new without touching what already exists.

Seeing an error instead of a plan?

  • Check that you added the enable_free_tier variable to modules/infra/variables.tf, not the root variables.tf. The module needs to declare its own variables.
  • Make sure free_tier_enabled in the resource block matches var.enable_free_tier exactly. A typo in either name causes an undeclared variable error.

Why is my terraform plan failing after adding Cosmos DB?

Do not apply yet. You still need to add module outputs and wire the root configuration.

  • Open modules/infra/outputs.tf.
  • Add the following output blocks below the existing storage_account_name output:
output "cosmosdb_endpoint" {
  description = "Endpoint of the Cosmos DB account"
  value       = azurerm_cosmosdb_account.main.endpoint
}

output "cosmosdb_name" {
  description = "Name of the Cosmos DB account"
  value       = azurerm_cosmosdb_account.main.name
}

What are module outputs for?

Module outputs are the public interface of a module. Without these, the root configuration cannot read the Cosmos DB endpoint or name. Think of outputs as the module saying "here is what I created" back to whoever called it.

  • Save modules/infra/outputs.tf.
Update root configuration

The module now knows about Cosmos DB, but the root configuration does not. You need to declare the enable_free_tier variable at the root level, pass it into the module call, and surface the new outputs.

  • Open the root variables.tf (in the project root, not inside modules/infra/).
  • Add the following variable block below the existing location variable:
variable "enable_free_tier" {
  description = "Enable Cosmos DB free tier (only one per subscription)"
  type        = bool
  default     = false
}
  • Save variables.tf.
  • Confirm your configuration has no syntax errors by running:
terraform validate

You should see:

Success! The configuration is valid.

Validation failed?

  • Make sure the variable name enable_free_tier is spelled identically in both modules/infra/variables.tf and the root variables.tf.
  • Check that the Cosmos DB outputs in modules/infra/outputs.tf reference azurerm_cosmosdb_account.main, matching the resource name in modules/infra/main.tf.

Why is terraform validate failing on my module?

Now you need to pass the new variable from root into the module and surface the Cosmos DB outputs.

  • Open the root main.tf.
  • Find the module "infra" block. It currently looks like this:
module "infra" {
  source = "./modules/infra"

  project_name = var.project_name
  environment  = var.environment
  location     = var.location
}
  • Add enable_free_tier = var.enable_free_tier as a new line below location, so the block now looks like this:
module "infra" {
  source = "./modules/infra"

  project_name     = var.project_name
  environment      = var.environment
  location         = var.location
  enable_free_tier = var.enable_free_tier
}
  • Save main.tf.
  • Open the root outputs.tf.
  • Add the following output blocks below the existing storage_account_name output:
output "cosmosdb_endpoint" {
  description = "Endpoint of the Cosmos DB account"
  value       = module.infra.cosmosdb_endpoint
}

output "cosmosdb_name" {
  description = "Name of the Cosmos DB account"
  value       = module.infra.cosmosdb_name
}

Why are there two sets of outputs?

Module outputs (in modules/infra/outputs.tf) expose values from the module to whoever calls it. Root outputs (in outputs.tf) surface those values to your terminal when you run terraform apply or terraform output.

Without the root outputs, Terraform still creates the Cosmos DB account, but you would not see its endpoint or name printed after a successful apply.

  • Save outputs.tf.

✔️ Awesome, I've got everything!

Great. Make sure you have saved all six files before moving on.

ⓧ I'd like to double check the full code

Here is the full content of each modified file. Compare yours against these.

modules/infra/main.tf is too long to show inline. Scroll up to the Cosmos DB code block and confirm your file contains the azurerm_resource_group, azurerm_storage_account, and azurerm_cosmosdb_account resource blocks, in that order.

variable "project_name" {
  description = "Name of the project, used in resource naming"
  type        = string
}

variable "environment" {
  description = "Deployment environment (dev or prod)"
  type        = string

  validation {
    condition     = contains(["dev", "prod"], var.environment)
    error_message = "Environment must be dev or prod."
  }
}

variable "location" {
  description = "Azure region for all resources"
  type        = string
}

variable "enable_free_tier" {
  description = "Enable Cosmos DB free tier (only one per subscription)"
  type        = bool
  default     = false
}
output "resource_group_name" {
  description = "Name of the resource group"
  value       = azurerm_resource_group.main.name
}

output "storage_account_name" {
  description = "Name of the storage account"
  value       = azurerm_storage_account.main.name
}

output "cosmosdb_endpoint" {
  description = "Endpoint of the Cosmos DB account"
  value       = azurerm_cosmosdb_account.main.endpoint
}

output "cosmosdb_name" {
  description = "Name of the Cosmos DB account"
  value       = azurerm_cosmosdb_account.main.name
}
terraform {
  required_version = ">= 1.15.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "= 5.1.0"
    }
  }
}

provider "azurerm" {
  resource_provider_registrations = "legacy"
  features {}
}

module "infra" {
  source = "./modules/infra"

  project_name     = var.project_name
  environment      = var.environment
  location         = var.location
  enable_free_tier = var.enable_free_tier
}
variable "project_name" {
  description = "Name of the project, used in resource naming (lowercase alphanumeric, max 8 chars)"
  type        = string
  default     = "multienv"
}

variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "dev"
}

variable "location" {
  description = "Azure region for all resources"
  type        = string
  default     = "eastus"
}

variable "enable_free_tier" {
  description = "Enable Cosmos DB free tier (only one per subscription)"
  type        = bool
  default     = false
}
output "resource_group_name" {
  description = "Name of the resource group"
  value       = module.infra.resource_group_name
}

output "storage_account_name" {
  description = "Name of the storage account"
  value       = module.infra.storage_account_name
}

output "cosmosdb_endpoint" {
  description = "Endpoint of the Cosmos DB account"
  value       = module.infra.cosmosdb_endpoint
}

output "cosmosdb_name" {
  description = "Name of the Cosmos DB account"
  value       = module.infra.cosmosdb_name
}
Plan the diff and apply

All six files are updated. Time to see the full diff with outputs wired through, then deploy Cosmos DB alongside your existing resources.

Before you run terraform plan, think about what you expect. How many resources will Terraform add? How many will it change or destroy?

  • Preview the changes by running:
terraform plan

Terraform shows the existing Resource Group and Storage Account are unchanged. The summary line reads:

Plan: 1 to add, 0 to change, 0 to destroy.

One resource to add (the Cosmos DB account), zero changes, zero destroys. Exactly what you want.

  • Apply the change by running:
terraform apply
  • When Terraform prompts you, type yes and press Enter.

Cosmos DB takes a few minutes

Azure provisions Cosmos DB accounts in 3-5 minutes. This is normal. Terraform will hold the terminal until provisioning completes, then print the new outputs.

Once the apply finishes, Terraform prints the new cosmosdb_endpoint and cosmosdb_name outputs alongside the ones you already had.

  • Open the Azure Portal in your browser.
  • Search for Resource groups in the top search bar and select it.
  • Click rg-multienv-dev.
  • Confirm you see three resources listed: a Resource Group, a Storage Account (multienvdevsa), and a Cosmos DB account (multienv-dev-cosmos).

Don't see the Cosmos DB account?

  • Check your terminal. If terraform apply is still running, Cosmos DB is still provisioning. Wait for it to finish.
  • If apply finished with an error about the storage account name being taken, change your project_name in the root variables.tf to something unique and run terraform apply again. Storage account names must be globally unique across all of Azure.

My terraform apply failed when adding Cosmos DB. Help me troubleshoot.

Your module now packages three Azure resources into a single reusable block. One change to the module, one apply, and every resource appeared. Next up, you will tackle the biggest risk in your current setup: the fact that your state file is sitting on your local machine.

Migrate to Remote State

In the last step, you extended your module with Cosmos DB and saw terraform plan diff a running environment for the first time.

Right now your terraform.tfstate file is sitting on your local disk. If you deleted it, Terraform would forget every resource it created and try to recreate everything from scratch.

And if a teammate cloned this repo, they would start with a blank state and attempt to provision duplicate resources. In this step, you will move your state into Azure Blob Storage so it is centrally stored and automatically locked during operations.

In this step, get ready to:
  • Bootstrap a dedicated storage account for Terraform state.
  • Add the remote backend configuration to your root config.
  • Migrate your local state to Azure Blob Storage and verify it.
Bootstrap the state storage

You need a storage account to hold Terraform's remote state, but there is a chicken-and-egg problem. Terraform cannot create the storage account that stores its own state because the backend does not exist yet.

Why use az CLI instead of Terraform here?

Terraform needs a backend configured before it can manage resources. The storage account that holds the backend is the one thing Terraform cannot create for itself.

A short shell script using az CLI is the standard way to break this cycle. You run it once and then Terraform takes over.

  • Create the bootstrap directory and the bootstrap.sh file inside it by running these commands in your terminal:
mkdir -p bootstrap
touch bootstrap/bootstrap.sh
  • Confirm the files exist by running:
ls bootstrap/

You should see bootstrap.sh in the output.

  • Open bootstrap/bootstrap.sh in Cursor by clicking on it in the Explorer sidebar.
  • Paste the following code into the file. This section sets up the variables and creates the Azure resources:
#!/bin/bash
set -euo pipefail

# Name the resource group and storage account for state storage
RESOURCE_GROUP="rg-tfstate"
LOCATION="eastus"
STORAGE_ACCOUNT="tfstate$(openssl rand -hex 4)"
CONTAINER="tfstate"

# Create a dedicated resource group for state
echo "Creating resource group for Terraform state..."
az group create --name "$RESOURCE_GROUP" --location "$LOCATION"

# Create the storage account with blob encryption
echo "Creating storage account: $STORAGE_ACCOUNT"
az storage account create \
  --resource-group "$RESOURCE_GROUP" \
  --name "$STORAGE_ACCOUNT" \
  --sku Standard_LRS \
  --encryption-services blob

# Retrieve the access key for the storage account
echo "Retrieving storage account key..."
ACCOUNT_KEY=$(az storage account keys list \
  --resource-group "$RESOURCE_GROUP" \
  --account-name "$STORAGE_ACCOUNT" \
  --query '[0].value' -o tsv)

What does this code do?

  • set -euo pipefail makes the script exit immediately if any command fails, so you catch errors early.
  • STORAGE_ACCOUNT gets a random suffix via openssl rand -hex 4 because Azure storage account names must be globally unique.
  • The script creates a resource group (rg-tfstate), a storage account, and retrieves the access key that Terraform will use to authenticate.
  • Add the following code below the existing code. This section creates the blob container and prints the setup instructions you will need next:
# Create the blob container for state files
echo "Creating blob container: $CONTAINER"
az storage container create \
  --name "$CONTAINER" \
  --account-name "$STORAGE_ACCOUNT" \
  --account-key "$ACCOUNT_KEY"

# Print the values you need for the next steps
echo ""
echo "============================================"
echo "Remote state backend created!"
echo "============================================"
echo ""
echo "1. Update storage_account_name in main.tf:"
echo "   storage_account_name = \"$STORAGE_ACCOUNT\""
echo ""
echo "2. Set this environment variable:"
echo "   export ARM_ACCESS_KEY=\"$ACCOUNT_KEY\""
echo ""
echo "3. Run: terraform init -migrate-state"
echo "============================================"

What does this code do?

  • az storage container create creates a blob container named tfstate inside the storage account. This container holds your Terraform state blobs.
  • The output section prints the storage account name and access key you need to configure Terraform's backend. You will copy these values in the next substep.

✔️ Awesome, I've got everything!

Great. Double check you have saved bootstrap/bootstrap.sh before moving on.

ⓧ I'd like to double check the full code

Your bootstrap/bootstrap.sh file should contain both chunks you pasted above, in order. Scroll up and confirm:

  • The first section starts with #!/bin/bash and ends with the az storage account keys list command.
  • The second section starts with az storage container create and ends with a row of = signs.
  • Save bootstrap/bootstrap.sh.
  • Run the bootstrap script by running this command in your terminal:
bash bootstrap/bootstrap.sh

What should I see?

The script takes about 30 seconds. You will see progress messages as each Azure resource is created.

At the end, it prints a box with three items: the storage_account_name to put in main.tf, the ARM_ACCESS_KEY environment variable to export, and the terraform init -migrate-state command to run. Keep this output visible for the next substep.

Script failed or command not found?

  • If you see a permission error, make sure you are logged in to Azure. Run az account show to check your session.
  • If the storage account name is taken, run the script again. The random suffix generates a new name each time.
  • Make sure you are running the command from the terraform-azure-multienv directory so the relative path bootstrap/bootstrap.sh resolves correctly.

Why is my bootstrap script failing?

  • Open the Azure Portal in your browser at portal.azure.com.
  • Search for Resource groups in the top search bar and select it.
  • Click on rg-tfstate to confirm it exists and contains a storage account.
Add the backend configuration

With the state storage created, you need to tell Terraform to use it. This means adding a backend block to your root configuration and providing the access key as an environment variable.

  • Switch back to main.tf in Cursor.
  • Find the closing brace of the required_providers block. It looks like this:
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "= 5.1.0"
    }
  }
  • Add the following backend "azurerm" block directly below the required_providers closing brace, but still inside the terraform {} block:
  # Store state remotely in Azure Blob Storage
  backend "azurerm" {
    storage_account_name = "[[TFSTATE_ACCOUNT="your-tfstate-account-name"]]"
    container_name       = "tfstate"
    key                  = "multienv.terraform.tfstate"
  }

What does this code do?

  • storage_account_name points to the storage account your bootstrap script just created. Replace the placeholder with the exact name from the script output.
  • container_name is the blob container (tfstate) that holds the state files.
  • key is the filename of the state blob inside the container. Each project gets its own key.
  • Save main.tf.

✔️ Awesome, I've got everything!

Great. Make sure you have replaced the placeholder storage_account_name with the actual name from your bootstrap output.

ⓧ I'd like to double check the full code

Your full main.tf should now look like this:

terraform {
  required_version = ">= 1.15.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "= 5.1.0"
    }
  }

  backend "azurerm" {
    storage_account_name = "[[TFSTATE_ACCOUNT="your-tfstate-account-name"]]"
    container_name       = "tfstate"
    key                  = "multienv.terraform.tfstate"
  }
}

provider "azurerm" {
  resource_provider_registrations = "legacy"
  features {}
}

module "infra" {
  source = "./modules/infra"

  project_name     = var.project_name
  environment      = var.environment
  location         = var.location
  enable_free_tier = var.enable_free_tier
}

Terraform also needs the storage account access key to authenticate with the backend. You will set this as an environment variable so the key stays out of your code files.

  • Set the ARM_ACCESS_KEY environment variable by running this command in your terminal. Replace your-access-key-here with the access key from the bootstrap output:
export ARM_ACCESS_KEY="your-access-key-here"

Why an environment variable?

The ARM_ACCESS_KEY environment variable lets Terraform authenticate to the backend without putting the key in your .tf files. This keeps the secret out of version control.

Using PowerShell on Windows?

The export command is for bash. If you are using PowerShell, use $env:ARM_ACCESS_KEY = "your-access-key-here" instead.

How do I set environment variables for Terraform on Windows?

Migrate state and verify

Your backend is configured and the access key is set. Now you will tell Terraform to move the existing local state into Azure Blob Storage.

Before you run the next command, think about what should happen. Terraform knows about three resources from the local state file. If the migration works, what should terraform plan show afterwards?

  • Run the state migration by running this command:
terraform init -migrate-state

What should I see?

Terraform detects that you changed from a local backend to an azurerm backend. It asks whether you want to copy existing state to the new backend.

Type yes and press Enter to confirm the migration. Terraform copies your local state to the Azure blob and prints a success message.

  • Verify that the migration was successful by running a plan:
terraform plan

You should see the message: No changes. Your infrastructure matches the configuration.

Why does "No changes" prove it worked?

If the state had been lost or corrupted during migration, Terraform would show all three resources as "to add" because it would think they do not exist. The fact that it sees no changes means the remote state matches what is actually deployed in Azure.

  • Confirm that your local state file is now empty by running:
cat terraform.tfstate

The file should be nearly empty or contain only a minimal backend reference. All of your actual resource data now lives in Azure.

  • Return to the Azure Portal in your browser.
  • Navigate to Resource groups and click on rg-tfstate.
  • Click on your storage account (the name starting with tfstate).
  • In the left menu, click Containers under Data storage.
  • Click the tfstate container. You should see a blob named multienv.terraform.tfstate. That is your migrated state.

What about state locking?

Azure Blob Storage automatically locks the state blob during any write operation. If a teammate runs terraform apply at the same time as you, one of you gets a lock error instead of corrupting the state. No extra configuration needed.

Seeing an error during migration?

  • If you see an authentication error, make sure ARM_ACCESS_KEY is set in the same terminal session. Run echo $ARM_ACCESS_KEY to check that it is not empty.
  • If you see a "storage account not found" error, check that the storage_account_name in your main.tf backend block matches the name your bootstrap script printed.
  • If terraform plan shows resources "to add" instead of no changes, the state was not copied. Re-run terraform init -migrate-state and type yes when prompted.

Why is my Terraform state migration failing?

Your state is now stored remotely and locked automatically during every operation. Next up, you will use Terraform workspaces to deploy dev and prod environments side by side from the same codebase.

Deploy Dev and Prod with Workspaces

Your Terraform state is safely stored in Azure Blob Storage with automatic locking. No more worrying about a deleted local file wiping out your infrastructure map.

Right now, all your resources live in a single default workspace. To run dev and prod side by side, you need separate state for each environment so they do not interfere with each other.

In this step, get ready to:
  • Create .tfvars files with per-environment settings.
  • Deploy the dev environment in its own Terraform workspace.
  • Deploy the prod environment alongside dev from the same codebase.
Create .tfvars files and clean up the default workspace

Terraform lets you define variable values in .tfvars files. Instead of changing your Terraform configuration for each environment, you pass a different .tfvars file at deploy time.

  • In Cursor, create a new file called dev.tfvars in the root of your terraform-azure-multienv folder with this content:
project_name     = "multienv"
environment      = "dev"
location         = "eastus"
enable_free_tier = true

What does this file do?

This file sets the variable values for your dev environment. The key setting is enable_free_tier = true, which activates the Cosmos DB free tier (1,000 RU/s and 25 GB storage at no cost).

Azure limits you to one free-tier Cosmos DB account per subscription. Dev claims it so you can iterate without cost.

  • Save dev.tfvars.

✔️ Awesome, I've got everything!

Great. Make sure you have saved dev.tfvars.

ⓧ I'd like to double check the full code

Here is the complete dev.tfvars file:

project_name     = "multienv"
environment      = "dev"
location         = "eastus"
enable_free_tier = true
  • Create another new file called prod.tfvars in the same root folder with this content:
project_name     = "multienv"
environment      = "prod"
location         = "eastus"
enable_free_tier = false

What is different in the prod file?

Two values changed. environment is now "prod", which changes every resource name (e.g. rg-multienv-prod instead of rg-multienv-dev).

enable_free_tier is false because the dev environment already claims the one free-tier Cosmos DB slot. Prod gets a standard 400 RU/s account covered by your Azure free account credits.

  • Save prod.tfvars.
  • Confirm both files exist by running this command in your terminal:
ls *.tfvars

You should see dev.tfvars and prod.tfvars in the output.

✔️ Awesome, I've got everything!

Great. Make sure you have saved prod.tfvars.

ⓧ I'd like to double check the full code

Here is the complete prod.tfvars file:

project_name     = "multienv"
environment      = "prod"
location         = "eastus"
enable_free_tier = false

Not seeing both files?

  • Make sure both files are saved in the root of your terraform-azure-multienv folder, not inside modules/ or any subfolder.
  • Check that the file extension is .tfvars, not .tf or .tfvars.txt.
  • Make sure your terminal is in the terraform-azure-multienv directory. Run pwd to confirm.

Why aren't my .tfvars files showing up?

Your current resources were deployed in the default workspace. Before creating proper workspaces, you need to tear those resources down so you can redeploy them cleanly under the dev workspace.

  • Destroy all resources in the default workspace by running this command:
terraform destroy

What will Terraform ask?

Terraform lists the 3 resources it plans to destroy (resource group, storage account, Cosmos DB) and asks Do you really want to destroy all resources? Type yes and press Enter to confirm.

This may take several minutes because Cosmos DB takes time to deprovision. Wait for the confirmation message before continuing.

You should see Destroy complete! Resources: 3 destroyed. in the output.

Destroy failed or timed out?

  • If you see an authorization error, check that your ARM_ACCESS_KEY environment variable is still set. Run echo $ARM_ACCESS_KEY to verify.
  • If Terraform is taking a long time, Cosmos DB deprovisioning can take up to 5 minutes. Let it finish.

Help me troubleshoot my terraform destroy failure.

Why not just rename the default workspace?

Terraform does not support renaming the default workspace. The cleanest path is to destroy the resources, create named workspaces, and redeploy. Each workspace gets its own state blob in the remote backend, so dev and prod will never interfere with each other.

Deploy the dev environment

Terraform workspaces let you manage multiple environments from the same configuration. Each workspace gets its own state file in the remote backend, so applying in one workspace never touches the other.

What is a Terraform workspace?

A workspace is a named instance of Terraform state within the same backend. Your remote backend container stores one state blob per workspace, keeping each environment completely isolated.

Workspaces do not automatically select .tfvars files. You explicitly pass the right file with -var-file every time you run plan or apply.

  • Create the dev workspace by running this command:
terraform workspace new dev

Terraform creates the workspace and automatically switches to it. You should see a message confirming you are now on the dev workspace.

  • Deploy the dev environment by running this command:
terraform apply -var-file="dev.tfvars"

What will Terraform ask?

Terraform shows a plan to create 3 resources (resource group, storage account, Cosmos DB) and asks Do you want to perform these actions? Type yes and press Enter to confirm.

Cosmos DB takes a few minutes

Cosmos DB provisioning takes 3-5 minutes. This is normal. Wait for Terraform to finish and display the output values before continuing.

  • Open the Azure Portal and navigate to Resource groups.
  • Click on rg-multienv-dev and confirm it contains a storage account (multienvdevsa) and a Cosmos DB account (multienv-dev-cosmos).

Don't see the resource group?

  • Make sure terraform apply finished without errors. Scroll up in your terminal to check for error messages.
  • If you see a storage account name conflict, someone else is using that name. Change project_name in both .tfvars files to something unique (e.g. your initials plus a number) and run terraform apply again.
  • Click the Refresh button in the Azure Portal. New resources can take a moment to appear in the list.

Help me troubleshoot my dev workspace deployment.

Deploy the prod environment

The same codebase is about to create a second, completely independent environment. The only difference is the .tfvars file you pass in.

  • Create the prod workspace by running this command:
terraform workspace new prod

Terraform creates the workspace and switches to it, just like before.

  • Deploy the prod environment by running this command:
terraform apply -var-file="prod.tfvars"
  • Terraform asks Do you want to perform these actions? again. Type yes and press Enter.
  • Wait for the apply to complete. Cosmos DB provisioning takes another 3-5 minutes.

Before you verify, think about what you expect to see. How many resource groups should exist in the Azure Portal now? What names should they have?

  • Verify your workspaces by running this command:
terraform workspace list

You should see three workspaces listed: default, dev, and prod. The * next to prod indicates it is the currently active workspace.

  • Return to the Azure Portal and navigate to Resource groups.
  • Confirm that both rg-multienv-dev and rg-multienv-prod exist side by side, each with their own storage account and Cosmos DB account inside.

Two fully isolated environments, deployed from the same Terraform codebase, each with their own state blob in the remote backend. One command and one .tfvars file is all it takes to switch between them.

Only seeing one resource group?

  • Make sure you created a new workspace before each apply. Run terraform workspace list to confirm both dev and prod exist.
  • If you accidentally ran both applies in the same workspace, the second apply overwrote the first. Switch to the missing workspace with terraform workspace select and run terraform apply with the correct -var-file.
  • Click Refresh in the Azure Portal. Resources can take a moment to appear after a fresh deployment.

Why am I only seeing one environment in Azure?

Secret mission

Add Conditional Resources and Workspace-Aware Outputs

Real production environments need resources that development environments do not. In this mission, you will add a conditional blob container that only exists in prod using Terraform's count meta-argument. You will also wire workspace-aware outputs that change based on which environment is active.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your dev environment uses the Cosmos DB free tier and costs nothing, but the prod Cosmos DB account will incur charges after your Azure free account credits expire.

Cost warning

Your prod Cosmos DB account (multienv-prod-cosmos) runs at 400 RU/s and is covered by the Azure free account $200 credit. After those credits expire, this account will incur charges.

The dev Cosmos DB account uses the always-free tier (1,000 RU/s and 25 GB) and costs nothing. State storage costs are negligible (pennies per month for blob storage). Delete or pause your resources if you are finished with the project.

Resources you used:

  • Terraform state storage: resource group rg-tfstate with a storage account and tfstate blob container.
  • Dev environment: resource group rg-multienv-dev with storage account multienvdevsa and Cosmos DB account multienv-dev-cosmos (free tier).
  • Prod environment: resource group rg-multienv-prod with storage account multienvprodsa, Cosmos DB account multienv-prod-cosmos, and production-data container (if you completed the Secret Mission).
  • Local project directory terraform-azure-multienv on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building on the project.

  • Your dev environment uses the Cosmos DB always-free tier and costs nothing.
  • Your prod Cosmos DB account is covered by Azure free account credits for now. Monitor your credit balance in the Azure Portal under Cost Management + Billing.
  • The state storage account costs pennies per month for blob storage.
  • Terraform CLI and Azure CLI remain installed on your machine.

Pause - I'll come back to this later

Destroy the dev and prod Azure resources to stop any costs. Your state storage backend stays intact so you can recreate the environments later.

  • Switch to the prod workspace and destroy its resources by running:
terraform workspace select prod
terraform destroy
  • Terraform shows a plan of resources to destroy. Type yes to confirm. Cosmos DB takes a few minutes to deprovision.
  • Switch to the dev workspace and destroy its resources by running:
terraform workspace select dev
terraform destroy
  • Type yes to confirm when prompted.

Your rg-tfstate resource group and state storage account remain intact for future projects.

Delete - I don't want to use this again

Remove all project resources from Azure and delete your local files.

  • Switch to the prod workspace and destroy its resources by running:
terraform workspace select prod
terraform destroy
  • Terraform shows a plan of resources to destroy. Type yes to confirm. Cosmos DB takes a few minutes to deprovision.
  • Switch to the dev workspace and destroy its resources by running:
terraform workspace select dev
terraform destroy
  • Type yes to confirm when prompted.
  • Delete the state storage resource group by running:
az group delete --name rg-tfstate --yes --no-wait
  • The --no-wait flag starts the deletion in the background so you do not have to wait.
  • Delete your local project directory. On macOS or Linux, run:
cd ~/Desktop
rm -rf terraform-azure-multienv
  • On Windows, run this instead:
cd ~\Desktop
Remove-Item -Recurse -Force terraform-azure-multienv

The ARM_ACCESS_KEY environment variable only exists in your current terminal session and clears automatically when you close the window.

Nice Work!

Nice Work!

Nice work! You just built a production-grade, multi-environment Terraform project that manages real Azure infrastructure from a single codebase.

You've learned how to:

  • Build a reusable Terraform module that packages a Resource Group, Storage Account, and Cosmos DB account into a single deployable unit.
  • Migrate from local state to Azure Blob Storage remote state with automatic locking, so teammates cannot corrupt each other's work.
  • Deploy dev and prod environments simultaneously using Terraform workspaces and per-environment .tfvars files, previewing every change with terraform plan before anything touches Azure.
  • Secret Mission: Added conditional resources using the count meta-argument so prod gets a data container that dev does not need.

Ready to quiz yourself?