TFLAB24-Module Composition: Passing Data Between Modules

Module Composition: Passing Data Between Modules

🔧 Terraform Core ⭐ Intermediate Module Outputs Cross-Module References depends_on with Modules

Scenario

You have two modules: one creates an S3 bucket, the other creates an SSM parameter that stores the bucket name. The SSM module needs the bucket name from the S3 module's output. This is module composition — wiring modules together through their inputs and outputs.

Your Objectives
  • Create a modules/s3 module that outputs the bucket name.
  • Create a modules/config module that accepts a bucket name input and creates an SSM parameter.
  • In the root module, pass the S3 module's output as input to the config module.
  • Observe the implicit dependency — the config module waits for the S3 module.

Additional Context

Modules communicate only through inputs (variables) and outputs. A parent module passes data by setting module arguments to values from other modules' outputs. Terraform automatically builds the dependency graph — if Module B's input comes from Module A's output, Module A is created first.

modules/s3/main.tf

variable "bucket_name" { type = string }
variable "environment" { type = string; default = "dev" }

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
  tags   = { Environment = var.environment, ManagedBy = "terraform" }
}

output "bucket_id"  { value = aws_s3_bucket.this.id }
output "bucket_arn" { value = aws_s3_bucket.this.arn }

modules/config/main.tf

variable "app_name"    { type = string }
variable "bucket_name" { type = string }   # comes from S3 module output
variable "environment" { type = string; default = "dev" }

resource "aws_ssm_parameter" "bucket_name" {
  name  = "/${var.app_name}/${var.environment}/bucket-name"
  type  = "String"
  value = var.bucket_name   # uses the S3 module's output
  tags  = { ManagedBy = "terraform" }
}

output "parameter_name" { value = aws_ssm_parameter.bucket_name.name }
output "parameter_arn"  { value = aws_ssm_parameter.bucket_name.arn }

main.tf (root module)

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" { region = "us-east-1" }
data "aws_caller_identity" "current" {}

# Module A: Creates the S3 bucket
module "storage" {
  source      = "./modules/s3"
  bucket_name = "composed-app-${data.aws_caller_identity.current.account_id}"
  environment = "dev"
}

# Module B: Creates config that REFERENCES Module A's output
# Terraform automatically knows to create Module A first.
module "config" {
  source      = "./modules/config"
  app_name    = "myapp"
  bucket_name = module.storage.bucket_id  # ← Module A's output → Module B's input
  environment = "dev"
}

outputs.tf (root module)

output "bucket_name"     { value = module.storage.bucket_id }
output "bucket_arn"      { value = module.storage.bucket_arn }
output "parameter_name"  { value = module.config.parameter_name }
output "parameter_arn"   { value = module.config.parameter_arn }

Workflow Commands

mkdir -p modules/s3 modules/config
terraform init && terraform apply

# Verify the SSM parameter stores the bucket name
aws ssm get-parameter --name "/myapp/dev/bucket-name" --query 'Parameter.Value'

terraform destroy
✓ Expected Output After Apply
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

bucket_arn      = "arn:aws:s3:::composed-app-123456789012"
bucket_name     = "composed-app-123456789012"
parameter_arn   = "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/dev/bucket-name"
parameter_name  = "/myapp/dev/bucket-name"