Command Palette

Search for a command to run...

Hectal

Mission 6.4 · Stage 6 — Environments

Sharing Outputs with Other Teams — Without Sharing State

Goal: The analytics team's separate Terraform config can find ShopLite's VPC, private subnets, and database security group through SSM parameters, without ever getting read access to ShopLite's state.

35 min Free (standard SSM parameters) 3 steps 2 break-it drills

By the end of this mission

  • Compare terraform_remote_state, data-source lookups, and published parameters
  • Publish selected outputs to SSM Parameter Store
  • Consume them in a separate configuration with correct types
  • Explain why remote state access is a security decision

Part 1

Understand it first

Three ways to share values between configurations

terraform_remote_state: read another configuration's OUTPUTS directly from its state file. Easy, but reading outputs requires read access to the WHOLE state file, including every secret and attribute in it. It also couples the consumer to the producer's backend layout.

Data-source LOOKUPS: find resources by tags or names (data "aws_vpc" { tags = { Name = "shoplite-prod-vpc" } }). No coupling to state, but it depends on naming conventions, and the lookup can match zero or several things.

PUBLISHED VALUES: the producer writes the values it wants to share to a well-known place, such as SSM parameters under /shoplite/prod/network/*, and consumers read them with aws_ssm_parameter data sources. That's an explicit, least-privilege contract: consumers get read access to exactly those parameters and nothing else.

Outputs are an interface

Whatever you publish, other teams will build on it. Publish only stable, necessary values (IDs, ARNs, endpoints), document them, and treat changes as breaking changes, just like module outputs in Stage 5.

Producer publishes; consumer reads only what's publisheddiagram
Rendering diagram…

Part 2

Your project after this mission · 3 files change

shoplite/
  • analytics/
    • backend.tfnew
    • main.tfnew
  • infra/
    • envs/
      • dev.s3.tfbackend
      • dev.tfvars
      • prod.s3.tfbackend
      • prod.tfvars
    • alb.tf
    • backend.tf
    • billing.tf
    • checks.tf
    • database.tf
    • ecr.tf
    • ecs.tf
    • iam.tf
    • locals.tf
    • logs.tf
    • main.tf
    • outputs.tf
    • probe.tf
    • providers.tf
    • published.tfnew
    • refactors.tf
    • security.tf
    • storage.tf
    • tf
    • uploads.tf
    • variables.tf
    • versions.tf

Part 3

Build it, step by step

  1. 1

    Publish ShopLite's shareable values

    A map of name → value, published with for_each. Lists are stored as StringList (comma-separated). The path includes the environment, so each environment publishes its own set.

    infra/published.tfwhole filehcl
    locals {
      published = {
        "network/vpc_id"             = { type = "String", value = module.network.vpc_id }
        "network/private_subnet_ids" = { type = "StringList", value = join(",", module.network.private_subnet_ids) }
        "security/db_sg_id"          = { type = "String", value = aws_security_group.db.id }
        "database/endpoint"          = { type = "String", value = aws_db_instance.main.address }
      }
    }
    
    resource "aws_ssm_parameter" "published" {
      for_each = local.published
    
      name        = "/${var.project}/${var.environment}/${each.key}"
      type        = each.value.type
      value       = each.value.value
      description = "Published by ShopLite Terraform for other teams. Do not edit by hand."
    }
    terminal
    $ ./tf dev apply
    aws ssm get-parameters-by-path --path /shoplite/dev --recursive --query 'Parameters[].[Name,Value]' --output text
    ── expected output ──
    Plan: 4 to add, 0 to change, 0 to destroy.
    ...
    /shoplite/dev/database/endpoint shoplite-dev-db.c1x2y3z4a5b6.ap-south-1.rds.amazonaws.com
    /shoplite/dev/network/private_subnet_ids subnet-0aa11bb22cc33dd44,subnet-0ee55ff66aa77bb88
    /shoplite/dev/network/vpc_id vpc-0c1d2e3f4a5b6c7d8
    /shoplite/dev/security/db_sg_id sg-0c3d4e5f607182930
  2. 2

    The analytics team's configuration

    A separate root with its own state (key analytics/dev/terraform.tfstate). It reads the parameters, splits the StringList back into a list, and creates its own security group in ShopLite's VPC that the DB will accept. That last part needs a rule on ShopLite's side too, which is a deliberate, reviewable change by the owning team.

    analytics/main.tfwhole filehcl
    provider "aws" {
      region = "ap-south-1"
    }
    
    data "aws_ssm_parameter" "vpc_id" {
      name = "/shoplite/dev/network/vpc_id"
    }
    
    data "aws_ssm_parameter" "private_subnet_ids" {
      name = "/shoplite/dev/network/private_subnet_ids"
    }
    
    locals {
      vpc_id             = data.aws_ssm_parameter.vpc_id.value
      private_subnet_ids = split(",", data.aws_ssm_parameter.private_subnet_ids.value)
    }
    
    resource "aws_security_group" "etl" {
      name_prefix = "analytics-etl-"
      vpc_id      = local.vpc_id
      description = "Analytics ETL jobs"
    }
    
    output "etl_security_group_id" {
      value = aws_security_group.etl.id
    }
    
    output "subnets_used" {
      value = local.private_subnet_ids
    }
    terminal
    $ cd ../analytics && terraform init && terraform apply
    ── expected output ──
    data.aws_ssm_parameter.vpc_id: Read complete after 0s [id=/shoplite/dev/network/vpc_id]
    data.aws_ssm_parameter.private_subnet_ids: Read complete after 0s [id=/shoplite/dev/network/private_subnet_ids]
    ...
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
     
    Outputs:
     
    etl_security_group_id = "sg-0d4e5f6071829304a"
    subnets_used = tolist([
    "subnet-0aa11bb22cc33dd44",
    "subnet-0ee55ff66aa77bb88",
    ])
    The analytics role needs only ssm:GetParameter on arn:aws:ssm:*:*:parameter/shoplite/dev/* — not s3:GetObject on ShopLite's state bucket.
  3. 3

    Compare with terraform_remote_state (don't keep it)

    For contrast, the remote-state version is shorter, but granting it means the analytics role must read shoplite/dev/terraform.tfstate, which includes the DB endpoint, every IAM ARN, and any secret that has ever been stored in state. It also only sees root OUTPUTS, so ShopLite would need to add them.

    analytics/main.tfadd to filehcl

    Read-only illustration. Delete after trying.

    data "terraform_remote_state" "shoplite" {
      backend = "s3"
      config = {
        bucket = "shoplite-tfstate-c41e"
        key    = "shoplite/dev/terraform.tfstate"
        region = "ap-south-1"
      }
    }
    
    # data.terraform_remote_state.shoplite.outputs.vpc_id

Checkpoint — you should now have

  • ✓ShopLite publishes four values under /shoplite/<env>/... via for_each.
  • ✓The analytics/ config reads them with its own state and no access to ShopLite's state.
  • ✓You can explain why terraform_remote_state implies full state read access.

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

Treat a StringList as a list

In analytics/main.tf, set private_subnet_ids = data.aws_ssm_parameter.private_subnet_ids.value (no split) and use it in a resource that expects a list, such as subnet_ids in an aws_db_subnet_group. Run terraform plan.

terminal
$ terraform plan
── what you'll see ──
╷
│ Error: Incorrect attribute value type
│
│ on main.tf line 24, in resource "aws_db_subnet_group" "etl":
│ 24: subnet_ids = local.private_subnet_ids
│ ├────────────────
│ │ local.private_subnet_ids is "subnet-0aa11bb22cc33dd44,subnet-0ee55ff66aa77bb88"
│
│ Inappropriate value for attribute "subnet_ids": set of string required.
╵

Break #2

Read an output that isn't exported

Using the terraform_remote_state example, reference data.terraform_remote_state.shoplite.outputs.db_security_group_id, which ShopLite doesn't output.

terminal
$ terraform plan
── what you'll see ──
╷
│ Error: Unsupported attribute
│
│ on main.tf line 40, in resource "aws_security_group_rule" "x":
│ 40: source_security_group_id = data.terraform_remote_state.shoplite.outputs.db_security_group_id
│ ├────────────────
│ │ data.terraform_remote_state.shoplite.outputs is object with 14 attributes
│
│ This object does not have an attribute named "db_security_group_id".
╵

Part 5

Interview questions from this mission

01

How can one Terraform configuration use values from another?

02

What's the security concern with terraform_remote_state?

Before you stop

Clean up

terminal
$ cd ../analytics && terraform destroy
── expected output ──
Destroy complete! Resources: 1 destroyed.
0/4 · 0%