How Terraform modules and variables interact is spot on.
1. The “Contract” (The Module):
Inside the ../../modules/app-common-infra folder, there are variable “xxx” {} blocks. These act like a function signature in programming. They define:
- What information is required (e.g., vpc_cidr, resource_prefix).
- The data type (string, list, map).
- Any default values.
2. The “Implementation” (The .tf files inside the modules):
Files like iam.tf, lambda.tf, and lb.tf inside that module folder don’t use hardcoded values. Instead, they use var.xxx.
Example: If iam.tf needs to name a role, it might use name = "${var.resource_prefix}-role". This makes the module reusable because it doesn’t care what the prefix is until you tell it.
3. The “Invocation” (Your main.tf)
In your production/main.tf, you “call” that module using the module “app” block. This is where you provide the actual data (arguments):
module "app" {
source = "../../modules/app-common-infra"
environment = local.client.tags.environment # Passing "production"
resource_prefix = local.client.naming.resource_prefix # Passing "powerx-orig"
# ... and so on
}
4. The “Execution” (terraform apply)
When you run terraform apply:
- Terraform reads your main.tf.
- It sees the module “app” and goes to the source path to find the code.
- It “plugs in” the values from your locals (like local.client.network.vpc_cidr) into the var.xxx placeholders inside the module.
- Finally, it calculates exactly what AWS resources need to be created based on those combined values.
One extra detail in your setup:
You are also doing this for multiple instances using the module “apps” block with for_each = local.apps. This is a more advanced version of the same concept: it creates a separate set of resources for app3, app4, etc., by passing different values (like private_ip or database_name) to the app-instance module for each entry in your map.
Deep Dive: for_each and Module Dependencies: Explaining the deep-dive of for_each loop and variable flow between modules in the Terraform configuration.
Let’s break down the “engine” of your main.tf: the for_each loop and how it handles the flow of variables.
1. The Map-Based “Source of Truth”
In your locals, the apps map is the control center.
apps = {
app3 = { ... } # each.key is "app3", each.value is the object { ... }
app4 = { ... } # each.key is "app4", each.value is the object { ... }
}
When you use for_each = local.apps, Terraform doesn’t just run the module once. It runs it once for every key in that map. If you have 10 keys, you get 10 independent App instances.
2. The Three Layers of Variables
In the module “apps” block, you are combining data from three different places to configure each instance:
| Source | Example in your code | Purpose |
|---|---|---|
| The Loop Key | app_name = each.key | Sets the unique name (app3, app4). |
| The Loop Value | private_ip = each.value.private_ip | Sets values that must be unique for every instance (IPs, priorities). |
| Shared Infra | shared_vpc_id = module.app.app_vpc.id | Connects every instance to the same network created by the first module. |
| Common Defaults | instance_type = local.app_common.instance_type | Ensures all instances use the same hardware/settings unless you choose to override them. |
3. Cross-Module Dependency (The “shared_” vars)
This is the most critical part of your architecture. Notice the variables starting with shared_:
- shared_vpc_id = module.app.app_vpc.id
- shared_lb_arn = module.app.lb_arn
Terraform handles the timing automatically. Because module.apps references module.app, Terraform knows it must finish building the VPC and Load Balancer before it even starts trying to build the individual Apps.
4. The lookup Trick (Smart Overrides)
You have a very clever line for Mackerel monitoring: 1 enable_mackerel = lookup(each.value, “enable_mackerel”, local.app_common.enable_mackerel)
This is a “fallback” logic:
- Check if enable_mackerel is defined specifically for app3 in the map.
- If not found, use the global value from local.app_common.
- This allows you to turn monitoring “ON” for just one specific App while keeping it “OFF” for the rest, without changing the module code.
Direct Connect Terraform Flow: A Quick Overview
The Foundation
Before the Direct Connect components can be created, the VPC infrastructure must exist. This includes the VPC itself, subnets, route tables, and any necessary security groups. Terraform can be used to define and provision this foundational infrastructure. Both aws_vpn_gateway.vgw and aws_route.direct_connect rely on outputs from module.app (like the VPC ID and Route Table ID).
Virtual Private Gateway
The aws_vpn_gateway.vgw is the “anchor” for Direct Connect within your VPC.
- Input: It takes module.app.app_vpc.id to know which VPC to attach to.
- Dependency: The depends_on = [module.app] ensures the VPC is fully ready before Terraform attempts to attach a gateway to it.
The “Missing Link”: DX Gateway Association (dx.tf)
While you didn’t focus on this as much in your summary, this resource is the logical bridge in your Terraform code:
resource "aws_dx_gateway_association" "vgw_association" {
dx_gateway_id = aws_dx_gateway.dx_gateway.id
associated_gateway_id = aws_vpn_gateway.vgw.id
# ...
}
This connects your VGW to the global Direct Connect Gateway.
The Route Configuration (main.tf)
The aws_route.direct_connect resource completes the flow by telling the VPC how to reach the on-premises network.
- Gateway Reference: It uses gateway_id = aws_vpn_gateway.vgw.id.
- Strategic Dependency: It uses depends_on = [aws_dx_gateway_association.vgw_association].
Why this dependency is important:
Even though the aws_route technically only needs the vgw.id to be created, we want to ensure the physical path (the association between the VGW and the DX Gateway) is established before we start routing production traffic to it. This prevents “black-holing” traffic during the deployment phase.
Summary of the Flow
- VPC Created (module.app)
- VGW Created & Attached (aws_vpn_gateway.vgw)
- VGW Linked to DX Gateway (aws_dx_gateway_association.vgw_association)
- Route Added to Private Route Table (aws_route.direct_connect)