Mission 6.3 · Stage 6 — Environments
Separate AWS Accounts and Provider Aliases
Goal: Prod runs in its own AWS account, reached by assuming a deploy role; each environment's provider refuses to act on any other account; and a second provider in us-east-1 creates a billing alarm where AWS actually publishes billing data.
By the end of this mission
- Configure the AWS provider to assume a role per environment
- Guard environments with
allowed_account_ids - Declare and use a provider alias for another region
- Pass an aliased provider into a module
Part 1
Understand it first
Accounts are the real environment boundary
Separate state files protect Terraform's bookkeeping; separate AWS ACCOUNTS protect the infrastructure itself. With prod in its own account, a dev credential, a dev pipeline, or a dev mistake literally can't touch prod resources. It's the multi-account pattern from the AWS course, Topic 7.3. Terraform reaches each account by assuming a role there (assume_role in the provider), starting from one identity such as your SSO user or the CI OIDC role.
allowed_account_ids: a seatbelt on the provider
allowed_account_ids = ["444455556666"] makes the provider check which account its credentials resolved to before doing anything, and fail if it's not on the list. Combined with per-environment tfvars, the prod configuration can only ever operate on the prod account. Even the dangerous mix-up from Mission 6.2 now fails at the first step.
Provider aliases
A configuration can have several configurations of the same provider, told apart by alias. Some AWS things only exist in one region: billing metrics and CloudFront's ACM certificates live in us-east-1. provider "aws" { alias = "us_east_1" region = "us-east-1" } adds a second configuration; resources opt into it with provider = aws.us_east_1, and modules receive it through providers = { aws = aws.us_east_1 }.
Part 2
Your project after this mission · 6 files change
- infra/
- envs/
- dev.s3.tfbackend
- dev.tfvarsmodified
- prod.s3.tfbackendmodified
- prod.tfvarsmodified
- alb.tf
- backend.tf
- billing.tfnew
- checks.tf
- database.tf
- ecr.tf
- ecs.tf
- iam.tf
- locals.tf
- logs.tf
- main.tf
- outputs.tf
- probe.tf
- providers.tfmodified
- refactors.tf
- security.tf
- storage.tf
- tf
- uploads.tf
- variables.tfmodified
- versions.tf
Part 3
Build it, step by step
- 1
Create a deploy role in each account (once)
In each workload account, a role named
shoplite-deployerthat trusts your identity account (and, in Stage 7, the CI OIDC provider) with permissions to manage ShopLite. Creating it is part of account setup (Control Tower, a bootstrap config per account, or a platform team). This course assumes it exists. Verify you can assume it.terminal$ aws sts assume-role --role-arn arn:aws:iam::444455556666:role/shoplite-deployer \--role-session-name check --query 'AssumedRoleUser.Arn' --output text── expected output ──arn:aws:sts::444455556666:assumed-role/shoplite-deployer/check - 2
Account variables
Two new required inputs per environment. There are no defaults, so you can't forget them.
infra/variables.tfadd to filehcl variable "aws_account_id" { description = "The only AWS account this environment may operate on." type = string validation { condition = can(regex("^[0-9]{12}$", var.aws_account_id)) error_message = "aws_account_id must be a 12-digit account ID." } } variable "deploy_role_arn" { description = "Role Terraform assumes in the target account." type = string } - 3
Provider: assume the role, allow only this account, add a us-east-1 alias
Both provider configurations assume the same role and carry the same guard. The alias differs only in region.
default_tagsare repeated, since each provider configuration is independent.infra/providers.tfwhole filehcl locals { default_tags = { Project = var.project Environment = var.environment ManagedBy = "terraform" Repo = "github.com/you/shoplite" } } provider "aws" { region = var.aws_region allowed_account_ids = [var.aws_account_id] assume_role { role_arn = var.deploy_role_arn session_name = "terraform-${var.environment}" } default_tags { tags = local.default_tags } } provider "aws" { alias = "us_east_1" region = "us-east-1" allowed_account_ids = [var.aws_account_id] assume_role { role_arn = var.deploy_role_arn session_name = "terraform-${var.environment}-use1" } default_tags { tags = local.default_tags } } - 4
Per-environment account settings
Add to each tfvars. Prod's state now lives in a bucket in the prod account (bootstrapped the same way as in Mission 1.2), so prod state is as isolated as prod itself.
infra/envs/*.tfvars + prod.s3.tfbackendadd to filehcl # envs/dev.tfvars aws_account_id = "111122223333" deploy_role_arn = "arn:aws:iam::111122223333:role/shoplite-deployer" # envs/prod.tfvars aws_account_id = "444455556666" deploy_role_arn = "arn:aws:iam::444455556666:role/shoplite-deployer" # envs/prod.s3.tfbackend — prod state in the prod account's own bucket bucket = "shoplite-prod-tfstate-9a2b" key = "shoplite/prod/terraform.tfstate" region = "ap-south-1" encrypt = true use_lockfile = true assume_role = { role_arn = "arn:aws:iam::444455556666:role/shoplite-deployer" } - 5
Use the alias: a billing alarm in us-east-1
AWS publishes
EstimatedChargesonly inus-east-1, so both the SNS topic and the alarm must be created there. Each resource names its provider explicitly. Enable 'Receive Billing Alerts' in the account's billing preferences once.infra/billing.tfwhole filehcl variable "monthly_budget_usd" { type = number default = 50 } variable "billing_alert_email" { type = string } resource "aws_sns_topic" "billing" { provider = aws.us_east_1 name = "${local.name_prefix}-billing" } resource "aws_sns_topic_subscription" "billing_email" { provider = aws.us_east_1 topic_arn = aws_sns_topic.billing.arn protocol = "email" endpoint = var.billing_alert_email } resource "aws_cloudwatch_metric_alarm" "estimated_charges" { provider = aws.us_east_1 alarm_name = "${local.name_prefix}-estimated-charges" namespace = "AWS/Billing" metric_name = "EstimatedCharges" dimensions = { Currency = "USD" } statistic = "Maximum" period = 21600 evaluation_periods = 1 comparison_operator = "GreaterThanThreshold" threshold = var.monthly_budget_usd alarm_actions = [aws_sns_topic.billing.arn] }terminal$ ./tf dev apply -var billing_alert_email=you@example.com── expected output ──# aws_cloudwatch_metric_alarm.estimated_charges will be created+ resource "aws_cloudwatch_metric_alarm" "estimated_charges" {+ alarm_name = "shoplite-dev-estimated-charges"+ region = "us-east-1"...Plan: 3 to add, 0 to change, 0 to destroy....Apply complete! Resources: 3 added, 0 changed, 0 destroyed.Put billing_alert_email in each tfvars file instead of passing it with -var. Confirm the subscription email AWS sends you. - 6
Passing an alias to a module (for reference)
When a MODULE needs the us-east-1 provider (say, a CloudFront + certificate module), pass it explicitly. Inside the module, its resources just use
awsas usual. The module doesn't know or care which region it got. If a module needs BOTH regions, it declaresconfiguration_aliases = [aws.us_east_1]in itsrequired_providers, and the caller maps both.infra/main.tfadd to filehcl Example only — ShopLite doesn't have a cdn module.
module "cdn" { source = "../modules/cdn" providers = { aws = aws.us_east_1 } domain_name = "static.example.com" }
Checkpoint — you should now have
- ✓Each environment's provider assumes
shoplite-deployerin its own account. - ✓
allowed_account_idspins each environment to exactly one account. - ✓Prod state lives in a bucket in the prod account.
- ✓A billing alarm exists in
us-east-1, created through theaws.us_east_1alias.
Part 4
Break it on purpose
Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.
Break #1
Point dev variables at the prod role
Temporarily set deploy_role_arn in envs/dev.tfvars to the PROD role ARN (leave aws_account_id as dev's) and run ./tf dev plan.
Break #2
Forget the provider on a us-east-1 resource
Remove provider = aws.us_east_1 from the alarm (keep it on the topic) and apply.
Part 5
Interview questions from this mission
How does Terraform manage resources across multiple AWS accounts?
What is a provider alias and when do you need one?
What does allowed_account_ids protect against?