Command Palette

Search for a command to run...

Hectal

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.

45 min Free (a CloudWatch alarm is free-tier) 6 steps 2 break-it drills

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 }.

One identity, two accounts, two regionsdiagram
Rendering diagram…

Part 2

Your project after this mission · 6 files change

shoplite/
  • 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. 1

    Create a deploy role in each account (once)

    In each workload account, a role named shoplite-deployer that 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. 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. 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_tags are 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. 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. 5

    Use the alias: a billing alarm in us-east-1

    AWS publishes EstimatedCharges only in us-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. 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 aws as usual. The module doesn't know or care which region it got. If a module needs BOTH regions, it declares configuration_aliases = [aws.us_east_1] in its required_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-deployer in its own account.
  • ✓allowed_account_ids pins 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 the aws.us_east_1 alias.

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.

terminal
$ ./tf dev plan
── what you'll see ──
╷
│ Error: AWS account ID not allowed: 444455556666
│
│ with provider["registry.terraform.io/hashicorp/aws"],
│ on providers.tf line 10, in provider "aws":
│ 10: provider "aws" {
│
╵

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.

terminal
$ ./tf dev apply
── what you'll see ──
╷
│ Error: creating CloudWatch Metric Alarm (shoplite-dev-estimated-charges):
│ operation error CloudWatch: PutMetricAlarm, https response error
│ StatusCode: 400, api error ValidationError: Invalid region us-east-1
│ specified. Only ap-south-1 is supported.
╵
(or, with an ap-south-1 topic, it succeeds — and sits in INSUFFICIENT_DATA forever)

Part 5

Interview questions from this mission

01

How does Terraform manage resources across multiple AWS accounts?

02

What is a provider alias and when do you need one?

03

What does allowed_account_ids protect against?

0/4 · 0%