Build a Small Reusable Module

AWSBeginner
Practice Now

Introduction

Two teams need the same small report-store design with different names, ownership and report text. Finish a supplied module's interface, call it from two independent roots, and extend a stable report collection without replacing existing members.

You should understand local modules, variables, outputs and resource addresses. The resource skeleton and caller inputs are supplied; you author their contract and deploy them in this fresh environment with prepared tools and no personal AWS account.

Certification Relevance

Certification Exam task Practice
CloudOps Engineer – Associate (SOA-C03) Task 3.1, Skill 3.1.6 Foundational Terraform practice: maintain infrastructure through reviewed configuration and state changes while preserving working resources.

Lab Overview

Concept diagram: the maintenance task preserves the relationship between configuration, state and existing report storage.

Define a Typed Module Interface

In this step, finish a small reusable storage module's input and output contract. A child module packages resources behind an interface; a root module supplies concrete inputs and owns its own state.

cd /home/labex/project/reusable-reports
cat modules/report-store/main.tf
cat dev/main.tf

The supplied resource skeleton uses var.bucket_name, var.owner and var.reports. for_each creates one object instance per map key, using addresses such as aws_s3_object.reports["daily"]. Stable named keys keep an existing object separate from a newly added member. The bucket's versioning resource is an explicit dependency so report writes receive version identities.

Define the interface. map(object({ body = string })) accepts a named collection whose members must each supply report text. It makes the module's expectations explicit instead of relying on undocumented input shapes:

cat > modules/report-store/variables.tf <<'HCL'
variable "bucket_name" {
  type        = string
  description = "Unique bucket name supplied by this environment root."
}
variable "owner" {
  type        = string
  description = "Owner tag supplied by the caller."
}
variable "reports" {
  type        = map(object({ body = string }))
  description = "Stable report keys and their text content."
}
HCL

Outputs expose useful resource facts without requiring a caller to reach inside the module. The for expression below builds a key-to-object-key map:

cat > modules/report-store/outputs.tf <<'HCL'
output "bucket_id" {
  value = aws_s3_bucket.store.id
}
output "object_keys" {
  value = { for key, report in aws_s3_object.reports : key => report.key }
}
HCL

Initialize the dev root and check the contract:

terraform -chdir=dev init
terraform -chdir=dev validate
terraform -chdir=dev plan

Expect a valid configuration and 3 to add: its bucket, versioning configuration and daily report object. Do not apply until you have checked the caller's bucket name, owner and report text. The independent finance reference is unrelated to this module and must remain unchanged.

Call the Same Module from Two Independent Roots

In this step, deploy dev and preview through the same module with different inputs. The roots share module source files, not state. Their separate working directories currently use separate local states; for_each only separates resources inside one state.

Review and apply dev's saved plan:

terraform -chdir=dev plan -out=dev.tfplan

Expect 3 to add, 0 to change, 0 to destroy.

terraform -chdir=dev apply dev.tfplan

Read preview's caller before initializing it:

cat preview/main.tf
terraform -chdir=preview init
terraform -chdir=preview validate
terraform -chdir=preview plan -out=preview.tfplan

Its distinct inputs select module-preview-reports, owner preview and preview daily report. Expect another three-resource creation plan, with no changes to dev.

terraform -chdir=preview apply preview.tfplan

Inspect outputs and the stable instance address in each root:

terraform -chdir=dev output
terraform -chdir=preview output
terraform -chdir=dev state list
terraform -chdir=preview state list

Both roots expose bucket_id and object_keys; each has a report instance keyed "daily", but those addresses belong to different local states and different buckets. Read actual content:

aws s3 \
  cp s3://module-dev-reports/reports/daily.txt -
aws s3 \
  cp s3://module-preview-reports/reports/daily.txt -

Expect dev daily report and preview daily report. AWS View shows two independent stores plus the finance reference. Display dev's current daily VersionId so you can compare it after extending the collection:

aws s3api \
  head-object \
  --bucket module-dev-reports \
  --key reports/daily.txt \
  --query VersionId \
  --output text

Add a Stable Collection Member Without Replacing Existing Reports

In this step, extend only dev's map. Changing a collection's keys changes resource instance identity; retaining the key daily and adding weekly should leave the daily object intact.

Replace the dev caller with the same existing inputs plus one new member:

cat > dev/main.tf <<'HCL'
module "report_store" {
  source      = "../modules/report-store"
  bucket_name = "module-dev-reports"
  owner       = "dev"
  reports = {
    daily  = { body = "dev daily report\n" }
    weekly = { body = "dev weekly report\n" }
  }
}
output "bucket_id" { value = module.report_store.bucket_id }
output "object_keys" { value = module.report_store.object_keys }
HCL

Review the change:

terraform -chdir=dev plan -out=extra.tfplan

Expect 1 to add, 0 to change, 0 to destroy, adding only module.report_store.aws_s3_object.reports["weekly"]. If the existing daily object or bucket is replaced, stop and compare the keys and unchanged inputs.

terraform -chdir=dev apply extra.tfplan

Compare the daily VersionId with the previous step's output:

aws s3api \
  head-object \
  --bucket module-dev-reports \
  --key reports/daily.txt \
  --query VersionId \
  --output text

It must be the same original version. Read the new member and the independent preview report:

aws s3 \
  cp s3://module-dev-reports/reports/weekly.txt -
aws s3 \
  cp s3://module-preview-reports/reports/daily.txt -

Expect dev weekly report and the unchanged preview daily report. The background check compares native first-write versions rather than accepting identical bytes in replacement objects.

terraform -chdir=dev output
terraform -chdir=dev plan
terraform -chdir=preview plan

Dev's output map contains both keys; preview still has only daily. Both roots should converge with No changes. Keep the working stores available. This module defines an interface and a resource collection; choosing separate remote backend keys is an additional environment-isolation decision, covered later.

See HashiCorp's module composition guidance for using module inputs and outputs as boundaries.

Summary

You authored a typed module interface, returned useful outputs to two independent roots, and added a stable for_each member without replacing original reports. A resource collection is distinct from an environment state boundary.