Laptop to Production

Environments and regions

dev, stg and a second region by changing variables.

Time
55 minutes
Level
Intermediate

Part 1: The problem

QuickCart, dev becomes three

Maya built QuickCart’s dev network with Terraform, in one folder. It needed a staging copy, so Leo copied the folder to stg, changed the address range, and ran terraform plan.

The plan said 8 to add, 1 to change, 6 to destroy. It wanted to replace a VPC that stg didn’t have yet. The copied folder still had dev’s backend.tf, with dev’s state key. Leo’s “stg” was reading dev’s memory, and planning to turn dev into stg. He read the plan, stopped, and fixed the key.

Six months later, QuickCart opened in Ireland. “The region is just a variable,” they said. The apply failed half-way: the VPC was built in eu-west-1, and the first subnet was refused. Someone, long ago, had typed us-east-1a into the code.

Both problems come from the same place: the difference between environments lived in more places than anyone could see.

This lesson is about keeping environments apart in a way you can see, making them differ only where you choose, and reaching a second region without surprises.

Part 2: The idea

An environment is a complete copy of QuickCart’s infrastructure: dev for trying things, stg for checking a release, prod for customers. They should be built from the same code, so that what you tested in stg is what runs in prod.

So there are only two things that may differ between environments:

What differsWhyWhere it lives
The valuesstg may use three zones where dev uses two, and each environment needs its own address rangeterraform.tfvars
The stateEach environment’s resources must be remembered separately, or one environment’s apply can change anotherbackend.tf: a different key
Key point: Environments differ only in their values and their state key; the code is the same file everywhere.

One folder per environment

Each environment gets a small root module of its own. All of them call the same child module:

Layout
quickcart-envs/
  modules/
    network/            the shared code: one copy only
  envs/
    dev/
      main.tf           the same in every folder
      variables.tf      the same in every folder
      backend.tf        differs: the state key
      terraform.tfvars  differs: the values
    stg/                the same four files

To work on an environment you cd into its folder. The folder name is in your prompt, your shell history and your terminal’s title. You can see where you are.

Here’s dev’s folder, file by file. First, main.tf:

envs/dev/main.tf
terraform {
  required_version = ">= 1.10.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = var.region

  default_tags {
    tags = {
      Project     = "quickcart"
      Environment = var.environment
      ManagedBy   = "terraform"
    }
  }
}

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

  name       = "quickcart-${var.environment}"
  cidr_block = var.vpc_cidr
  zone_count = var.zone_count
}

output "vpc_id" {
  description = "The ID of this environment's VPC"
  value       = module.network.vpc_id
}

output "zones" {
  description = "The availability zones this environment uses"
  value       = module.network.zones
}
SettingWhat it doesIf you leave it outWhen you’d change it
required_version, required_providersTerraform 1.10 or later and AWS provider 6.x, the same in every environmentEach environment might run a different versionWhen you upgrade, in every folder at once
provider "aws" → region = var.regionThe region is a variable, so the same file builds in any regionTerraform uses your login’s default region, which may differ between peopleNever; change the value in terraform.tfvars
default_tags → tagsEvery resource this provider builds gets these tags, including ones inside modules. Environment comes from a variableNo tags unless each resource sets its ownTo add tags your team requires everywhere
module "network" → sourceThe shared module, two folders up. Every environment points at the same oneRequiredNever per environment
namequickcart-dev, quickcart-stg: built from the environment’s nameRequired by the module—
cidr_block, zone_countPassed straight through from this folder’s variablescidr_block is required; zone_count falls back to the module’s defaultNever here; change the values instead
output "vpc_id", output "zones"Print what was built, so you can check itNothing printed after apply—

The variables it needs:

envs/dev/variables.tf
variable "environment" {
  description = "Which environment this folder builds"
  type        = string

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

variable "region" {
  description = "The AWS region to build in"
  type        = string
}

variable "vpc_cidr" {
  description = "The VPC's address range. Different for every environment"
  type        = string
}

variable "zone_count" {
  description = "How many availability zones to use"
  type        = number
}
SettingWhat it doesIf you leave it outWhen you’d change it
environment → validationOnly dev, stg or prod are acceptedA typo creates resources with a strange nameWhen you add an environment
No defaults on any variableEvery environment must set every value in its own terraform.tfvarsA default would quietly apply to any environment that forgot to set it, like prod getting dev’s rangeNever, for values that should be decided per environment
description, typeExplain each value and reject the wrong kindMistakes show up later, with vaguer errors—

Where this environment’s state lives. The only line that differs between folders is key:

envs/dev/backend.tf
terraform {
  backend "s3" {
    bucket       = "quickcart-tfstate-111122223333"
    key          = "dev/network/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}
SettingWhat it doesIf you leave it outWhen you’d change it
bucketThe one state bucket, shared by every environmentRequiredNever
keyThis environment’s own state file. dev/network/… for dev, stg/network/… for stgRequiredOnce, when you create the folder. Never copy it unchanged
regionWhere the bucket is. Stays us-east-1 even when this environment builds somewhere elseTerraform may look for the bucket in the wrong regionOnly if the bucket moves
encrypt, use_lockfileEncrypt the state, and allow one command at a time. Terraform state explains bothNo encryption setting; no lockingNever

And the values, the part that’s allowed to differ:

envs/dev/terraform.tfvars
environment = "dev"
region      = "us-east-1"
vpc_cidr    = "10.10.0.0/16"
zone_count  = 2
SettingWhat it doesIf you leave it outWhen you’d change it
terraform.tfvarsTerraform reads a file with exactly this name automatically. No -var-file flag needed, so nobody can forget itTerraform asks for each value, one by one, when you plan—
environment = "dev"Names everything quickcart-dev and tags it Environment = devAsked for at plan time—
regionWhere dev is builtAsked for at plan timeFor a copy in another region
vpc_cidrdev’s address range. Every environment gets a different one, so they can be connected later without clashingAsked for at plan time—
zone_countTwo zones for dev, to keep it smallAsked for at plan time—

stg’s folder has the same main.tf and variables.tf. Only these two files are its own:

envs/stg/backend.tf
terraform {
  backend "s3" {
    bucket       = "quickcart-tfstate-111122223333"
    key          = "stg/network/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}
envs/stg/terraform.tfvars
environment = "stg"
region      = "us-east-1"
vpc_cidr    = "10.20.0.0/16"
zone_count  = 3
SettingWhy it’s different for stg
key = "stg/network/terraform.tfstate"stg’s own memory. Nothing stg does can touch dev’s state
environment = "stg"Names and tags everything stg
vpc_cidr = "10.20.0.0/16"Its own range, so it never overlaps dev’s
zone_count = 3Closer to production, which uses three zones

Workspaces, and why folders are safer

Terraform has another way to keep environments apart: workspaces. One folder has several states, and you switch between them:

Terminal
terraform workspace new stg
PartWhat it means
terraform workspace newcreate a new workspace and switch to it
stgits name
You should see
Created and switched to workspace "stg"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
Terminal
terraform workspace show
PartWhat it means
terraform workspace showprint which workspace you’re in now
You should see
stg

With the S3 backend, the first workspace, called default, uses your key as it is. Every other workspace’s state goes under env:/, like env:/stg/dev/network/terraform.tfstate. The code can read the current name as terraform.workspace.

WorkspacesOne folder per environment
Which one am I in?Only terraform workspace show says. Easy to forgetThe folder you’re in: it’s in your prompt
Values for each environmentYou must pass the right -var-file every timeterraform.tfvars in the folder, read automatically
Environments in different AWS accountsAwkward: one backend and one login for allEasy: each folder has its own settings
Applying to the wrong environmentEasy: same folder, same files, invisible differenceHard: you’d have to be in the wrong folder
Good forShort-lived identical copies, like one per pull request, created and destroyed by a pipelineLong-lived environments people work in: dev, stg, prod

Folders cost a few copied files. Workspaces save them, and hide the one thing you most need to see.

A second region

QuickCart opening in Europe means building in eu-west-1 (Ireland) as well as us-east-1. There are two ways, for two different needs.

The everyday way: a folder whose region is different. The region is already a variable, so a Europe copy of dev is one more folder: the same main.tf and variables.tf, with its own key and values:

envs/dev-eu/terraform.tfvars
environment = "dev"
region      = "eu-west-1"
vpc_cidr    = "10.30.0.0/16"
zone_count  = 2
SettingWhy
region = "eu-west-1"The only reason this folder exists
environment = "dev"It’s still dev, just in Europe
vpc_cidr = "10.30.0.0/16"Its own range. Regions are often connected later, and overlapping ranges can’t be

Its backend.tf uses key = "dev-eu/network/terraform.tfstate", and keeps region = "us-east-1", because that’s where the bucket is. You’ll build it in the lab.

When one root needs two regions at once: provider aliases. Some things must be in a particular region. The classic case: a certificate for a CloudFront distribution must be in us-east-1, even when everything else is in Europe. So the root has two AWS provider configurations, and gives the second one a name, an alias.

A module that needs both says so with configuration_aliases:

modules/help-site/main.tf
terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      version               = "~> 6.0"
      configuration_aliases = [aws.us_east_1]
    }
  }
}

variable "domain_name" {
  description = "The help site's address, like help.example.com"
  type        = string
}

resource "aws_s3_bucket" "site" {
  bucket_prefix = "quickcart-help-"
}

resource "aws_acm_certificate" "site" {
  provider = aws.us_east_1

  domain_name       = var.domain_name
  validation_method = "DNS"
}

output "bucket_name" {
  description = "The bucket holding the help pages"
  value       = aws_s3_bucket.site.bucket
}

output "certificate_arn" {
  description = "The certificate, for a CloudFront distribution"
  value       = aws_acm_certificate.site.arn
}
SettingWhat it doesIf you leave it outWhen you’d change it
configuration_aliases = [aws.us_east_1]Says: “as well as the normal AWS provider, I need a second one, which I call aws.us_east_1.” The caller decides which real configuration that isTerraform warns about an undefined provider, and the module’s interface doesn’t say it needs two regionsWhen the module needs another region
version = "~> 6.0"The provider versions the module works withAny version is assumed to workWhen the module needs a newer feature
variable "domain_name"The site’s address, from the caller——
aws_s3_bucket → bucket_prefixAWS adds a unique ending to the name. S3 bucket names are unique across all of AWS, in every regionWithout bucket or bucket_prefix, Terraform generates a random nameUse bucket for an exact name
No provider on the bucketIt uses the default AWS provider: wherever the caller builds——
aws_acm_certificate → provider = aws.us_east_1Build this one resource with the second provider: in us-east-1It would be built in the default region, where CloudFront can’t use it—
domain_nameThe name the certificate is forRequired—
validation_method = "DNS"Prove you own the domain with a DNS record. DNS and HTTPS covers thisRequiredEMAIL if you can’t add DNS records
output "bucket_name", output "certificate_arn"Hand the results back to the callerThe caller can’t use them—

The root makes both configurations and maps them onto the module’s names:

envs/help-site/main.tf
terraform {
  required_version = ">= 1.10.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

provider "aws" {
  alias  = "virginia"
  region = "us-east-1"
}

module "help_site" {
  source = "../../modules/help-site"

  providers = {
    aws           = aws
    aws.us_east_1 = aws.virginia
  }

  domain_name = "help.example.com"
}
SettingWhat it doesIf you leave it outWhen you’d change it
The first provider "aws"The default configuration, with no alias: EuropeResources with no provider setting have nowhere to go—
alias = "virginia"A second configuration, referred to as aws.virginiaTwo provider "aws" blocks without an alias are an error—
region = "us-east-1"Where the aliased configuration builds——
providers → aws = awsThe module’s default provider is the root’s default: EuropeThis line alone could be left out; it’s the default—
providers → aws.us_east_1 = aws.virginiaFill the module’s aws.us_east_1 slot with the root’s aws.virginia. The names on each side can differTerraform stops, as below—
domain_nameThe module’s inputRequired—

What doesn’t move to a second region

“The region is just a variable” is only true if nothing else in the code belongs to one region. These are the usual ones:

ThingWhat happens in a new regionWhat to do
Availability zone namesus-east-1a doesn’t exist in eu-west-1Look them up with data "aws_availability_zones", as the network module does
AMI IDs (server images)Each region has its own IDs for the same imageLook them up with data "aws_ami"
ACM certificatesRegional: a certificate in us-east-1 can’t be used by a load balancer in eu-west-1Issue one per region. CloudFront is the exception: always us-east-1
Services and instance typesNot every one is offered in every regionCheck before you promise a date
IAM roles, Route 53, CloudFrontGlobal: one of each serves every regionCreate them once, in one folder, not per region
S3 bucket namesGlobal: the name must be unique across all of AWSPut the region in the name, or use bucket_prefix
The state bucketStays where it isSame bucket, same backend region, a new key
CostEverything is paid for twice, plus data sent between regionsBuild the second region small, and grow it

Part 3: See it

Here are QuickCart’s three folders side by side: dev, stg, and dev in Europe. Pick an environment to see what’s really different about it. Then switch the module to zone names typed in by hand, and try each environment again.

Environment
Zone names in the module
What differs between the three folders
Settingdevstgdev in eu-west-1
Folderwhere you run terraformenvs/devenvs/stgenvs/dev-eu
keybackend.tf: the state filedev/network/terraform.tfstatestg/network/terraform.tfstatedev-eu/network/terraform.tfstate
regionterraform.tfvarsus-east-1us-east-1eu-west-1
vpc_cidrterraform.tfvars10.10.0.0/1610.20.0.0/1610.30.0.0/16
zone_countterraform.tfvars232
# envs/*/main.tf: the same file in every folder
module "network" {
  source = "../../modules/network"

  name       = "quickcart-${var.environment}"
  cidr_block = var.vpc_cidr
  zone_count = var.zone_count
}

# modules/network/main.tf: how the module picks its zones
zones = slice(data.aws_availability_zones.available.names, 0, var.zone_count)
$ cd envs/dev
$ terraform apply

Plan: 7 to add, 0 to change, 0 to destroy.
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Outputs:

zones = tolist([
  "us-east-1a",
  "us-east-1b",
])
devThe module and main.tf are the same in every folder. Only the highlighted column's values are dev's own. Pick another environment to see what changes.

What to notice: the shared code never changes. Everything that makes stg “stg” or Europe “Europe” is in the table: a state key and four values. The typed-in zone names work in dev and stg, because they’re both in us-east-1, which is exactly why that bug survives until the day you open a second region.

Part 4: Do it

Pennies — three small state files in your S3 bucket. VPCs, subnets, internet gateways and route tables are free, and you start no servers.

You’ll build dev and stg from the same code, prove the folders differ only in values, then build dev again in Europe by changing variables. About 30 minutes.

You need the state bucket from Terraform state, Terraform 1.10 or later, and a working AWS login. Each region allows five VPCs by default; this lab adds two to us-east-1 and one to eu-west-1.

  1. Make the folders

    Terminal
    mkdir -p quickcart-envs/modules/network quickcart-envs/envs/dev quickcart-envs/envs/stg quickcart-envs/envs/dev-eu
    PartWhat it means
    mkdir -pmake directories, including any missing parent folders
    quickcart-envs/modules/networkthe shared module
    quickcart-envs/envs/dev, …/stg, …/dev-euone folder per environment
    Terminal
    cd quickcart-envs
    PartWhat it means
    cd quickcart-envsmove into the project folder
  2. Write the shared module

    These are the three files of the network module from Modules, where every line is explained. Create each one in modules/network/:

    modules/network/variables.tf
    variable "name" {
      description = "Name prefix for every resource, like quickcart-dev"
      type        = string
    }
    
    variable "cidr_block" {
      description = "The VPC's address range, like 10.10.0.0/16"
      type        = string
    
      validation {
        condition     = can(cidrhost(var.cidr_block, 0))
        error_message = "cidr_block must be an IPv4 range, like 10.10.0.0/16."
      }
    }
    
    variable "zone_count" {
      description = "How many availability zones to spread the public subnets across"
      type        = number
      default     = 2
    
      validation {
        condition     = var.zone_count >= 2 && var.zone_count <= 3
        error_message = "zone_count must be 2 or 3."
      }
    }
    
    variable "tags" {
      description = "Extra tags to put on every resource in the module"
      type        = map(string)
      default     = {}
    }
    modules/network/main.tf
    data "aws_availability_zones" "available" {
      state = "available"
    }
    
    locals {
      zones = slice(data.aws_availability_zones.available.names, 0, var.zone_count)
    }
    
    resource "aws_vpc" "this" {
      cidr_block           = var.cidr_block
      enable_dns_hostnames = true
    
      tags = merge(var.tags, { Name = var.name })
    }
    
    resource "aws_internet_gateway" "this" {
      vpc_id = aws_vpc.this.id
    
      tags = merge(var.tags, { Name = "${var.name}-igw" })
    }
    
    resource "aws_subnet" "public" {
      for_each = { for index, zone in local.zones : zone => index }
    
      vpc_id                  = aws_vpc.this.id
      availability_zone       = each.key
      cidr_block              = cidrsubnet(var.cidr_block, 8, each.value)
      map_public_ip_on_launch = true
    
      tags = merge(var.tags, { Name = "${var.name}-public-${each.key}" })
    }
    
    resource "aws_route_table" "public" {
      vpc_id = aws_vpc.this.id
    
      route {
        cidr_block = "0.0.0.0/0"
        gateway_id = aws_internet_gateway.this.id
      }
    
      tags = merge(var.tags, { Name = "${var.name}-public" })
    }
    
    resource "aws_route_table_association" "public" {
      for_each = aws_subnet.public
    
      subnet_id      = each.value.id
      route_table_id = aws_route_table.public.id
    }
    modules/network/outputs.tf
    output "vpc_id" {
      description = "The VPC's ID, for security groups and load balancers"
      value       = aws_vpc.this.id
    }
    
    output "public_subnet_ids" {
      description = "One public subnet ID per zone"
      value       = [for subnet in aws_subnet.public : subnet.id]
    }
    
    output "zones" {
      description = "The availability zones the subnets are in"
      value       = local.zones
    }
  3. Write dev's four files

    In envs/dev/, create the four files shown in the idea above, exactly as they are there: main.tf, variables.tf, backend.tf and terraform.tfvars. In backend.tf, put your own account ID in the bucket name in place of 111122223333.

  4. Build dev

    Terminal
    cd envs/dev
    PartWhat it means
    cd envs/devmove into dev’s folder. Everything you run here is about dev
    Terminal
    terraform init
    PartWhat it means
    terraform initconnect to dev’s state in S3, find the module, and download the provider
    You should see
    Initializing the backend...
    
    Successfully configured the backend "s3"! Terraform will automatically
    use this backend unless the backend configuration changes.
    Initializing modules...
    - network in ../../modules/network
    
    Initializing provider plugins...
    - Finding hashicorp/aws versions matching "~> 6.0"...
    - Installing hashicorp/aws v6.66.0...
    - Installed hashicorp/aws v6.66.0 (signed by HashiCorp)
    
    Terraform has been successfully initialized!

    Your provider version may be newer.

    Terminal
    terraform apply
    PartWhat it means
    terraform applyplan dev’s network using the values in terraform.tfvars, and build it once you type yes
    The end of the plan
    Plan: 7 to add, 0 to change, 0 to destroy.

    Type yes.

    You should see
    Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    vpc_id = "vpc-0123456789abcdef0"
    zones = tolist([
      "us-east-1a",
      "us-east-1b",
    ])

    Your VPC ID will be different.

  5. Make stg from the same code

    Copy dev’s two shared files into stg’s folder:

    Terminal
    cp main.tf variables.tf ../stg/
    PartWhat it means
    cpcopy files
    main.tf variables.tfthe two files that must be identical in every environment
    ../stg/into stg’s folder: .. is the folder above this one

    Don’t copy backend.tf or terraform.tfvars. Create stg’s own two files in envs/stg/, exactly as shown in the idea: backend.tf with key = "stg/network/terraform.tfstate", and terraform.tfvars with stg’s values. Again, use your own account ID in the bucket name.

    Then build it:

    Terminal
    cd ../stg
    PartWhat it means
    cd ../stgmove to stg’s folder
    Terminal
    terraform init
    PartWhat it means
    terraform initconnect to stg’s state, which doesn’t exist yet
    You should see
    Terraform has been successfully initialized!
    Terminal
    terraform apply
    PartWhat it means
    terraform applyplan and build stg’s network

    Read the plan before you type yes. It should only add. If it says anything will be destroyed, stop: check the key in backend.tf.

    You should see
    Plan: 9 to add, 0 to change, 0 to destroy.

    Type yes.

    You should see
    Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    vpc_id = "vpc-1234567890abcdef0"
    zones = tolist([
      "us-east-1a",
      "us-east-1b",
      "us-east-1c",
    ])

    Nine resources this time, not seven: the third zone adds a subnet and a route table link.

  6. Prove only the values differ

    Compare the two main.tf files:

    Terminal
    diff ../dev/main.tf main.tf
    PartWhat it means
    diffcompare two files line by line, and print only what differs
    ../dev/main.tfdev’s copy
    main.tfstg’s copy, in the folder you’re in
    You should see

    Nothing at all. diff prints nothing when the files are identical.

    Now compare the values:

    Terminal
    diff ../dev/terraform.tfvars terraform.tfvars
    PartWhat it means
    diffcompare the two files
    ../dev/terraform.tfvars terraform.tfvarsdev’s values, then stg’s
    You should see
    1c1
    < environment = "dev"
    ---
    > environment = "stg"
    3,4c3,4
    < vpc_cidr    = "10.10.0.0/16"
    < zone_count  = 2
    ---
    > vpc_cidr    = "10.20.0.0/16"
    > zone_count  = 3

    Lines starting < are dev’s, lines starting > are stg’s. 1c1 means “line 1 changed”. That’s the whole difference between the two environments, plus the one key line.

  7. See the two state files

    In the AWS console: S3General purpose bucketsyour state bucket

    You should see

    Two folders, dev/ and stg/. Open each, then network/, and there’s a terraform.tfstate in each: two separate memories.

    In the AWS console: VPCYour VPCs

    You should see

    quickcart-dev with range 10.10.0.0/16, and quickcart-stg with 10.20.0.0/16, next to the account’s default VPC.

  8. Build dev in a second region

    Copy the shared files into the Europe folder:

    Terminal
    cp main.tf variables.tf ../dev-eu/
    PartWhat it means
    cp main.tf variables.tfcopy the two identical files
    ../dev-eu/into the Europe folder

    In envs/dev-eu/, create terraform.tfvars exactly as shown in the idea, with region = "eu-west-1", and this backend.tf, with your own account ID:

    envs/dev-eu/backend.tf
    terraform {
      backend "s3" {
        bucket       = "quickcart-tfstate-111122223333"
        key          = "dev-eu/network/terraform.tfstate"
        region       = "us-east-1"
        encrypt      = true
        use_lockfile = true
      }
    }
    SettingWhy
    key = "dev-eu/network/terraform.tfstate"Europe’s dev gets its own state
    region = "us-east-1"The bucket is still in us-east-1. Only the resources go to Europe
    Terminal
    cd ../dev-eu
    PartWhat it means
    cd ../dev-eumove to the Europe folder
    Terminal
    terraform init
    PartWhat it means
    terraform initconnect to the Europe state and set up the folder
    You should see
    Terraform has been successfully initialized!
    Terminal
    terraform apply
    PartWhat it means
    terraform applyplan and build the same network in eu-west-1

    Check the plan only adds, then type yes.

    You should see
    Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    vpc_id = "vpc-0fedcba9876543210"
    zones = tolist([
      "eu-west-1a",
      "eu-west-1b",
    ])

    Nobody typed eu-west-1a. The module asked AWS which zones this region has.

    In the AWS console: VPCYour VPCs

    Switch the region at the top right of the console to Europe (Ireland) eu-west-1.

    You should see

    quickcart-dev, with range 10.30.0.0/16, and its two subnets in eu-west-1a and eu-west-1b.

  9. Clean-up step: Clean up

    Destroy each environment from its own folder. You’re in envs/dev-eu now:

    Terminal
    terraform destroy
    PartWhat it means
    terraform destroyremove everything this folder’s state knows about: Europe’s network only. Type yes
    You should see
    Destroy complete! Resources: 7 destroyed.
    Terminal
    cd ../stg
    PartWhat it means
    cd ../stgmove to stg’s folder
    Terminal
    terraform destroy
    PartWhat it means
    terraform destroyremove stg’s network. Type yes
    You should see
    Destroy complete! Resources: 9 destroyed.
    Terminal
    cd ../dev
    PartWhat it means
    cd ../devmove to dev’s folder
    Terminal
    terraform destroy
    PartWhat it means
    terraform destroyremove dev’s network. Type yes
    You should see
    Destroy complete! Resources: 7 destroyed.

    Each destroy only touched its own environment. That’s the separate state keys at work.

    The three state files stay in the bucket, now holding empty states. They cost a tiny fraction of a cent. To remove them, open the bucket in the S3 console, tick the dev-eu/ and stg/ folders and choose Delete, then open dev/ and delete its network/ folder the same way. Keep the bucket if you’re carrying on with the course.

Part 5: Check yourself

QuickCart's envs/dev and envs/stg folders are set up correctly. Which files differ between them?

Leo copies envs/dev to envs/stg and edits terraform.tfvars, but not backend.tf. His first stg plan says "8 to add, 1 to change, 6 to destroy". Why?

What is the main risk of keeping dev and prod as two workspaces of one folder?

A module works in us-east-1 but fails in eu-west-1 with "Value (us-east-1a) for parameter availabilityZone is invalid". What's the lasting fix?

A module builds an S3 bucket in its default region and a certificate in us-east-1. What must the module and its caller do?

envs/dev-eu builds in eu-west-1. What is the region setting in its backend.tf?

Part 6: Cheat sheet

On one card
ThingWhat to remember
One folder per environmentenvs/dev, envs/stg: you can see where you are
main.tf, variables.tfIdentical in every folder. diff should print nothing
backend.tf → keyDifferent per environment. Never copy it unchanged
backend.tf → regionWhere the state bucket is, not where you build
terraform.tfvarsRead automatically. Holds everything that differs
No defaults on per-environment variablesSo a forgotten value is an error, not dev’s value in prod
terraform init -reconfigureSet a folder up again after changing its backend, without copying state
terraform workspace showWhich workspace you’re in. Fine for short-lived copies, risky for real environments
provider "aws" { alias = "virginia" … }A second configuration, for a second region
configuration_aliases = [aws.us_east_1]In a module: “I need a second AWS provider”
providers = { aws.us_east_1 = aws.virginia }In the module block: which one to use
data "aws_availability_zones", data "aws_ami"Never type in zone names or image IDs

Finished the build and the quiz? Mark it done to fill in its stop on the route map.