Provisions a single Application or Network Load Balancer together with its target groups, listeners, listener rules, and SNI certificates — a complete, internal-by-default, TLS-terminating, access-logged front end from one module call. Built for the AWS provider v6.x.
- ⚖️ One load balancer, fully wired. Creates
aws_lbplus everything that is meaningless without it: target groups (with health checks + stickiness), static target attachments, listeners, path/host listener rules, and additional SNI certificates. - 🔐 TLS by default. Listeners default to
protocol = "HTTPS"pinned to the modernELBSecurityPolicy-TLS13-1-2-2021-06security policy; plain HTTP is an explicit opt-in (typically a redirect-to-HTTPS). - 🛡️ Private by default.
internal = trueis the baseline — PII/privacy-regulation workloads stay off the public internet unless the caller deliberately setsinternal = false. - 🪵 Access logging on by default.
access_logsdefaults to enabled, so the module forces an explicit choice: wire an S3 log bucket or consciously opt out — there is no silent "logging off." - 🚫 Deletion protection on.
enable_deletion_protection = trueblocks accidental teardown of a shared front end; it must be flipped off and applied before a destroy. - 🧱 Deeply-typed, key-addressed children. Target groups, listeners, rules, attachments, and SNI certs are first-class
map(object(...))inputs keyed by stable strings — listeners forward to target groups by key (target_group_key), rules attach to listeners by key (listener_key), so the module resolves keys to ARNs for you with no two-pass wiring. - 🌐 ALB, NLB, and GWLB in one module. Type-specific attributes (idle timeout, HTTP/2, desync mitigation, cross-zone) are gated to the load-balancer type they apply to, so the provider never rejects an argument that doesn't belong.
- 🏷️ Tags everywhere taggable.
var.tagsmerges with providerdefault_tags, flows to the LB, target groups, listeners and rules, and is surfaced astags_all. (Target-group attachments and listener certificates are not taggable — AWS limitation.)
💡 Why it matters: The load balancer is the front door to nearly every application tier. A single secure-by-default module keeps TLS termination, private scheme, access logging, deletion protection, and header hardening consistent — so the blast radius of a misconfigured front end (public scheme, weak TLS, logging off, accidental destroy) is closed by default, not by review.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
tf-mod-aws-lb is a load-balancing module — it consumes networking, security, certificate, and log-bucket inputs from upstream siblings, and is itself consumed by WAF, Global Accelerator, target-registration, and DNS modules downstream.
flowchart LR
vpc["tf-mod-aws-vpc<br/>vpc_id + subnet_ids"]
sg["tf-mod-aws-security-group<br/>security_group_ids (ALB)"]
acm["tf-mod-aws-acm<br/>certificate_arn (regional)"]
s3["tf-mod-aws-s3-bucket<br/>access-log bucket"]
lb["tf-mod-aws-lb"]
waf["tf-mod-aws-wafv2<br/>web ACL association"]
ga["tf-mod-aws-global-accelerator<br/>endpoint"]
asg["tf-mod-aws-autoscaling-group<br/>target attachment"]
ecs["tf-mod-aws-ecs-service<br/>load_balancer block"]
r53["tf-mod-aws-route53-zone<br/>alias records"]
vpc -->|"vpc_id + subnet_ids"| lb
sg -->|"security_group_ids"| lb
acm -->|"certificate_arn"| lb
s3 -->|"access_logs.bucket"| lb
lb -->|"arn"| waf
lb -->|"arn"| ga
lb -->|"target_group_arns"| asg
lb -->|"target_group_arns"| ecs
lb -->|"dns_name + zone_id"| r53
style lb fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
flowchart TD
subgraph mod["tf-mod-aws-lb"]
lb["aws_lb.this<br/>(keystone)<br/>ALB / NLB / GWLB · subnets · access logs"]
tg["aws_lb_target_group.this<br/>for_each target_groups<br/>+ health_check + stickiness"]
att["aws_lb_target_group_attachment.this<br/>for_each target_group_attachments"]
lsn["aws_lb_listener.this<br/>for_each listeners<br/>TLS + default_action"]
rule["aws_lb_listener_rule.this<br/>for_each listener_rules<br/>path/host/header routing"]
cert["aws_lb_listener_certificate.this<br/>for_each listener_certificates<br/>SNI certs"]
end
tg --> att
lb --> lsn
tg --> lsn
lsn --> rule
tg --> rule
lsn --> cert
style lb fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
style tg stroke-dasharray: 5 5
style att stroke-dasharray: 5 5
style lsn stroke-dasharray: 5 5
style rule stroke-dasharray: 5 5
style cert stroke-dasharray: 5 5
| Resource | Count | Created when |
|---|---|---|
aws_lb.this |
1 | always (keystone) |
aws_lb_target_group.this |
0..N | one per target_groups entry |
aws_lb_target_group_attachment.this |
0..N | one per target_group_attachments entry |
aws_lb_listener.this |
0..N | one per listeners entry |
aws_lb_listener_rule.this |
0..N | one per listener_rules entry |
aws_lb_listener_certificate.this |
0..N | one per listener_certificates entry |
Listeners and rules reference target groups by key (target_group_key), rules attach to listeners by key (listener_key), and SNI certs attach to listeners by key — the module resolves each key to the created resource's ARN, so there is no manual ARN threading between children.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
The module declares only a required_providers block (providers.tf) and inherits the single aws provider — there is no provider {} block, no region variable, and no credential variable. Credentials resolve through the standard AWS chain at the root/pipeline level (env vars → SSO/shared credentials → assume_role → instance profile / IRSA → OIDC web identity). The caller chooses the Region by which provider configuration it passes into the aws slot.
⚠️ Unlike CloudFront, an ALB/NLB is a regional service. Its ACM listener certificate(s) must live in the same Region as the load balancer (regional ACM — NOT us-east-1). Wirecertificate_arnfrom atf-mod-aws-acmcall made against the same provider/Region as this module.
Least-privilege actions the Terraform execution identity needs to manage this module.
| Action | Required for | Notes |
|---|---|---|
elasticloadbalancing:CreateLoadBalancer, elasticloadbalancing:DeleteLoadBalancer, elasticloadbalancing:DescribeLoadBalancers, elasticloadbalancing:ModifyLoadBalancerAttributes, elasticloadbalancing:DescribeLoadBalancerAttributes |
LB lifecycle + attributes | Core CRUD on the keystone |
elasticloadbalancing:CreateTargetGroup, elasticloadbalancing:DeleteTargetGroup, elasticloadbalancing:DescribeTargetGroups, elasticloadbalancing:ModifyTargetGroupAttributes, elasticloadbalancing:DescribeTargetGroupAttributes |
Target-group lifecycle | Only when target_groups set |
elasticloadbalancing:RegisterTargets, elasticloadbalancing:DeregisterTargets, elasticloadbalancing:DescribeTargetHealth |
Static target registration | Only when target_group_attachments set |
elasticloadbalancing:CreateListener, elasticloadbalancing:DeleteListener, elasticloadbalancing:ModifyListener, elasticloadbalancing:DescribeListeners |
Listener lifecycle | Only when listeners set |
elasticloadbalancing:CreateRule, elasticloadbalancing:DeleteRule, elasticloadbalancing:ModifyRule, elasticloadbalancing:DescribeRules |
Listener-rule lifecycle | Only when listener_rules set |
elasticloadbalancing:AddListenerCertificates, elasticloadbalancing:RemoveListenerCertificates, elasticloadbalancing:DescribeListenerCertificates |
Additional SNI certificates | Only when listener_certificates set |
elasticloadbalancing:AddTags, elasticloadbalancing:RemoveTags, elasticloadbalancing:DescribeTags |
Tagging | LB, target groups, listeners, rules |
ec2:DescribeVpcs, ec2:DescribeSubnets, ec2:DescribeSecurityGroups, ec2:DescribeInternetGateways, ec2:DescribeAccountAttributes |
Wiring + placement validation | Read-only; ELB validates subnets/SGs at create |
ec2:CreateSecurityGroup, ec2:CreateNetworkInterface, ec2:DeleteNetworkInterface* |
LB network plumbing | ELB performs these on your behalf during create/delete |
acm:DescribeCertificate, acm:ListCertificates |
Resolving listener certificates | On the regional cert ARN(s) |
iam:CreateServiceLinkedRole |
First-time ELB use | Creates AWSServiceRoleForElasticLoadBalancing if absent (idempotent) |
🔒 Service-linked role.
AWSServiceRoleForElasticLoadBalancingis auto-created on first ELB use; the identity needsiam:CreateServiceLinkedRoleonly until it exists. Theec2:*network-plumbing actions marked*are exercised by the ELB service principal during create/delete (ENIs, the managed security group) — scope them to the target VPC. The S3access_logsbucket policy must already grant ELB log delivery; this module does not mutate the bucket policy (own it intf-mod-aws-s3-bucket).
- Service-linked role.
AWSServiceRoleForElasticLoadBalancingis auto-created on first ELB use (the identity may neediam:CreateServiceLinkedRole). - VPC + subnets in ≥2 AZs. Internet-facing LBs need public subnets in ≥2 AZs; internal LBs need private subnets in ≥2 AZs. Wire
vpc_id+subnet_idsfromtf-mod-aws-vpc. ALB subnets can be added after creation; removing an NLB subnet/mapping forces a new resource. - Security groups (ALB). ALBs require security groups; current-generation NLBs optionally support them; GWLBs do not use them. Wire from
tf-mod-aws-security-group. For NLBs, SGs cannot be added if none existed at creation and cannot all be removed once added (either forces a new resource). - ACM certificate (regional). HTTPS/TLS listeners require an ACM certificate in the same Region as the load balancer (regional ACM — NOT us-east-1; that constraint applies only to CloudFront). Wire
certificate_arnfromtf-mod-aws-acm. - Access-log bucket (when logging on, the default). An S3 bucket whose policy grants the regional ELB log-delivery account /
logdelivery.elasticloadbalancing.amazonaws.comwrite access. Wire the bucket name fromtf-mod-aws-s3-bucket(log-archive). The policy must exist before apply or the create fails. - Internet-facing scheme.
internal = falseadditionally requires an Internet Gateway on the VPC and public subnets with a route to it. - Quotas (per Elastic Load Balancing quotas; raisable via Service Quotas):
- 50 ALBs/NLBs per Region.
- 50 listeners per ALB.
- 100 rules per ALB (excluding default rules); 5 condition values + 6 wildcards per rule.
- 100 target groups per ALB; 1,000 targets per ALB; 500 targets per AZ per NLB.
- 25 certificates per ALB (excluding the default certificate — i.e. additional SNI certs).
- Regional service. ALB/NLB is regional — there is no us-east-1 global constraint; the listener cert lives in the LB's Region.
tf-mod-aws-lb/
├── providers.tf # required_providers (aws >= 6.0, < 7.0); region/provider wiring notes; no provider block
├── variables.tf # identity → core (force-new) → LB attributes → access/connection logs → target groups → listeners → rules → SNI certs → tags → timeouts
├── main.tf # aws_lb.this + target_group / attachment / listener / rule / listener_certificate for_each
├── outputs.tf # id + arn (+ lb_arn, arn_suffix) + dns_name + zone_id + child-collection maps + tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM permissions, prerequisites, gotchas
Smallest working call — an internal HTTPS ALB fronting an instance target group, with TLS and the cert wired from upstream:
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "core-app-alb"
load_balancer_type = "application"
# internal = true by default (secure baseline)
vpc_id = module.vpc.vpc_id # from tf-mod-aws-vpc
subnet_ids = module.vpc.private_subnet_ids # ≥2 AZs
security_group_ids = [module.alb_sg.id] # from tf-mod-aws-security-group
# access logging is ON by default — supply a bucket (or opt out explicitly)
access_logs = { enabled = true, bucket = module.log_bucket.bucket, prefix = "core-app-alb" }
target_groups = {
app = { port = 443, protocol = "HTTPS", health_check = { path = "/healthz" } }
}
listeners = {
https = {
port = 443
protocol = "HTTPS" # default; TLS13 policy by default
certificate_arn = module.cert.arn # regional ACM (tf-mod-aws-acm)
default_action = { type = "forward", target_group_key = "app" }
}
}
tags = { Environment = "prod", DataClass = "internal" }
}| Input | Type | Source module |
|---|---|---|
vpc_id |
string (VPC id) |
tf-mod-aws-vpc |
subnet_ids |
list(string) (≥2 AZs) |
tf-mod-aws-vpc |
security_group_ids |
list(string) (ALB required, NLB optional) |
tf-mod-aws-security-group |
listeners[*].certificate_arn / listener_certificates[*].certificate_arn |
string (ACM ARN, regional) |
tf-mod-aws-acm |
access_logs.bucket / connection_logs.bucket |
string (bucket name) |
tf-mod-aws-s3-bucket (log-archive) |
subnet_mapping[*].allocation_id |
string (EIP allocation id) |
tf-mod-aws-elastic-ip (internet-facing NLB) |
target_group_attachments[*].target_id |
string (instance id / IP / Lambda ARN) |
tf-mod-aws-ec2-instance / external |
| Output | Description | Consumed by |
|---|---|---|
id |
Load balancer id (ELBv2 uses the ARN as id) | tagging, monitoring |
arn |
Load balancer ARN — the cross-resource reference type | tf-mod-aws-wafv2 (web ACL association), tf-mod-aws-global-accelerator (endpoint), IAM |
lb_arn |
Alias of the LB ARN, for explicit wiring | tf-mod-aws-wafv2, tf-mod-aws-global-accelerator |
arn_suffix |
ARN suffix for CloudWatch LoadBalancer metric dimension |
CloudWatch alarms |
name |
LB name (generated when name/name_prefix omitted) | tagging, audit |
dns_name |
Public/private DNS name | tf-mod-aws-route53-zone alias records, app config |
zone_id |
Canonical hosted zone id | tf-mod-aws-route53-zone alias records |
target_group_arns |
Map of TG key → ARN | tf-mod-aws-autoscaling-group (aws_autoscaling_attachment), tf-mod-aws-ecs-service load_balancer block |
target_group_arn_suffixes |
Map of TG key → ARN suffix | CloudWatch TargetGroup metrics |
target_group_names |
Map of TG key → name | audit |
listener_arns |
Map of listener key → ARN | external rule / cert attachment |
listener_rule_arns |
Map of rule key → ARN | audit |
tags_all |
All tags incl. provider default_tags (resource tags win) |
governance/audit |
1 · Internal HTTPS ALB → instance target group (default secure posture)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "core-app-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket, prefix = "core-app-alb" }
target_groups = {
app = { port = 443, protocol = "HTTPS", health_check = { path = "/healthz", matcher = "200" } }
}
listeners = {
https = {
port = 443
certificate_arn = module.cert.arn
default_action = { type = "forward", target_group_key = "app" }
}
}
}2 · HTTP → HTTPS redirect + HTTPS forward (the canonical ALB front door)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "web-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = {
web = { port = 80, protocol = "HTTP", health_check = { path = "/" } }
}
listeners = {
# plain HTTP — explicit opt-in, used only to redirect to HTTPS
http = {
port = 80
protocol = "HTTP"
default_action = {
type = "redirect"
redirect = { status_code = "HTTP_301", protocol = "HTTPS", port = "443" }
}
}
https = {
port = 443
certificate_arn = module.cert.arn
default_action = { type = "forward", target_group_key = "web" }
}
}
}3 · Path / host listener rules (multi-service routing)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "api-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = {
web = { port = 80, protocol = "HTTP" }
api = { port = 8080, protocol = "HTTP", health_check = { path = "/api/health" } }
}
listeners = {
https = {
port = 443
certificate_arn = module.cert.arn
default_action = { type = "forward", target_group_key = "web" } # fallback
}
}
listener_rules = {
api = {
listener_key = "https"
priority = 100
action = { type = "forward", target_group_key = "api" }
conditions = [{ path_patterns = ["/api/*"] }]
}
admin = {
listener_key = "https"
priority = 200
action = { type = "fixed-response", fixed_response = { content_type = "text/plain", message_body = "forbidden", status_code = "403" } }
conditions = [{ host_headers = ["admin.example.com"] }, { source_ips = ["10.0.0.0/8"] }]
}
}
}4 · Weighted forward (blue/green) with target-group stickiness
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "bluegreen-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = {
blue = { port = 80, protocol = "HTTP" }
green = { port = 80, protocol = "HTTP" }
}
listeners = {
https = {
port = 443
certificate_arn = module.cert.arn
default_action = {
type = "forward"
forward = {
target_groups = [
{ target_group_key = "blue", weight = 90 },
{ target_group_key = "green", weight = 10 },
]
stickiness = { enabled = true, duration = 300 }
}
}
}
}
}5 · Internet-facing ALB (opt out of internal)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "public-alb"
internal = false # OPT-OUT of the private baseline
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids # public subnets in ≥2 AZs
security_group_ids = [module.public_alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = { web = { port = 80, protocol = "HTTP" } }
listeners = {
https = {
port = 443
certificate_arn = module.cert.arn
default_action = { type = "forward", target_group_key = "web" }
}
}
}6 · Network Load Balancer (TCP) with cross-zone on
module "nlb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "core-nlb"
load_balancer_type = "network"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
# enable_cross_zone_load_balancing = true by default (AWS defaults NLB cross-zone OFF)
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = {
tcp = { port = 443, protocol = "TCP", health_check = { protocol = "TCP" } }
}
listeners = {
tls = {
port = 443
protocol = "TLS"
certificate_arn = module.cert.arn
default_action = { type = "forward", target_group_key = "tcp" }
}
}
}7 · Internet-facing NLB with static Elastic IPs (subnet_mapping)
module "nlb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "edge-nlb"
load_balancer_type = "network"
internal = false
vpc_id = module.vpc.vpc_id
# use subnet_mapping (not subnet_ids) to pin a static EIP per AZ
subnet_mapping = {
az_a = { subnet_id = module.vpc.public_subnet_ids[0], allocation_id = module.eip_a.allocation_id }
az_b = { subnet_id = module.vpc.public_subnet_ids[1], allocation_id = module.eip_b.allocation_id }
}
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = { tcp = { port = 443, protocol = "TCP" } }
listeners = {
tcp = { port = 443, protocol = "TCP", default_action = { type = "forward", target_group_key = "tcp" } }
}
}8 · IP target group + static attachments (off-VPC / on-prem targets)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "ip-targets-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = {
onprem = { target_type = "ip", port = 8443, protocol = "HTTPS", health_check = { path = "/health" } }
}
target_group_attachments = {
node1 = { target_group_key = "onprem", target_id = "10.20.0.11", availability_zone = "all" }
node2 = { target_group_key = "onprem", target_id = "10.20.0.12", availability_zone = "all" }
}
listeners = {
https = {
port = 443
certificate_arn = module.cert.arn
default_action = { type = "forward", target_group_key = "onprem" }
}
}
}9 · Lambda target group + attachment
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "lambda-alb"
vpc_id = module.vpc.vpc_id # not used by lambda TGs, fine to pass
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = {
fn = { target_type = "lambda", lambda_multi_value_headers_enabled = true } # no port/protocol/vpc
}
target_group_attachments = {
fn = { target_group_key = "fn", target_id = module.api_fn.function_arn }
}
listeners = {
https = {
port = 443
certificate_arn = module.cert.arn
default_action = { type = "forward", target_group_key = "fn" }
}
}
}10 · Additional SNI certificates on one HTTPS listener
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "multi-cert-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = { web = { port = 80, protocol = "HTTP" } }
listeners = {
https = {
port = 443
certificate_arn = module.cert_primary.arn # primary cert
default_action = { type = "forward", target_group_key = "web" }
}
}
# extra certs served by SNI on the same listener
listener_certificates = {
brand2 = { listener_key = "https", certificate_arn = module.cert_brand2.arn }
brand3 = { listener_key = "https", certificate_arn = module.cert_brand3.arn }
}
}11 · Custom TLS policy + header hardening tuned (secure-default opt-outs)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "tuned-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
# secure-default opt-outs (each discouraged; document the exception)
enable_deletion_protection = false # allow teardown in non-prod
drop_invalid_header_fields = false # only with a documented exception
desync_mitigation_mode = "monitor" # triage only; "defensive" is baseline
idle_timeout = 120
access_logs = { enabled = false } # documented opt-out of access logging
target_groups = { web = { port = 80, protocol = "HTTP" } }
listeners = {
https = {
port = 443
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" # older policy (discouraged)
certificate_arn = module.cert.arn
default_action = { type = "forward", target_group_key = "web" }
}
}
}12 · Tags (merge with provider default_tags; per-child overrides)
# Caller's provider block owns default_tags; the module never sets it.
provider "aws" {
default_tags { tags = { Owner = "platform", ManagedBy = "terraform" } }
}
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "tagged-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = {
web = { port = 80, protocol = "HTTP", tags = { Tier = "frontend" } } # per-child tag override
}
listeners = {
https = { port = 443, certificate_arn = module.cert.arn, default_action = { type = "forward", target_group_key = "web" } }
}
tags = {
Environment = "prod" # resource tag — wins over default_tags on key conflict
DataClass = "internal"
}
}
# module.alb.tags_all == { Owner, ManagedBy, Environment, DataClass }
# the "web" target group additionally carries Tier = "frontend"13 · Health-check + stickiness tuning
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "sticky-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = {
app = {
port = 80
protocol = "HTTP"
deregistration_delay = 30
load_balancing_algorithm_type = "least_outstanding_requests"
health_check = {
path = "/healthz"
interval = 15
timeout = 5
healthy_threshold = 3
unhealthy_threshold = 3
matcher = "200-299"
}
stickiness = { type = "lb_cookie", enabled = true, cookie_duration = 3600 }
}
}
listeners = {
https = { port = 443, certificate_arn = module.cert.arn, default_action = { type = "forward", target_group_key = "app" } }
}
}14 · Wiring an Auto Scaling group to a target group (downstream)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "asg-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket }
target_groups = { app = { port = 80, protocol = "HTTP" } }
listeners = {
https = { port = 443, certificate_arn = module.cert.arn, default_action = { type = "forward", target_group_key = "app" } }
}
}
# The ASG module registers the fleet — NOT this module (no aws_autoscaling_attachment here)
module "asg" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-autoscaling-group?ref=v1.0.0"
name = "app-asg"
target_group_arns = [module.alb.target_group_arns["app"]] # wire the TG ARN by key
#...
}15 · End-to-end composition — VPC + SG + ACM + log bucket + ALB + WAF + DNS
provider "aws" {} # single regional provider — ALB and its regional ACM cert share it
# Networking foundation
module "vpc" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
name = "core"
#... cidr, subnets in ≥2 AZs...
}
# ALB security group (443 from the trusted CIDR, egress to targets)
module "alb_sg" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-security-group?ref=v1.0.0"
name = "core-alb-sg"
vpc_id = module.vpc.vpc_id
#... ingress 443...
}
# Regional ACM cert (SAME Region as the LB — NOT us-east-1)
module "cert" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-acm?ref=v1.0.0"
domain_name = "app.example.com"
}
# Access-log archive bucket (policy grants ELB log delivery)
module "log_bucket" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-s3-bucket?ref=v1.0.0"
bucket = "casey-elb-logs"
#... ELB log-delivery bucket policy...
}
# This module — the load balancer front end
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
name = "core-app-alb"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.alb_sg.id]
access_logs = { enabled = true, bucket = module.log_bucket.bucket, prefix = "core-app-alb" }
target_groups = {
app = { port = 443, protocol = "HTTPS", health_check = { path = "/healthz" } }
}
listeners = {
http = { port = 80, protocol = "HTTP", default_action = { type = "redirect", redirect = { status_code = "HTTP_301", protocol = "HTTPS", port = "443" } } }
https = { port = 443, certificate_arn = module.cert.arn, default_action = { type = "forward", target_group_key = "app" } }
}
tags = { Environment = "prod", DataClass = "internal" }
}
# WAFv2 web ACL association (REGIONAL scope) — owned by the WAF module, by ARN
module "waf" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-wafv2?ref=v1.0.0"
name = "core-alb-acl"
scope = "REGIONAL"
resource_arns = [module.alb.arn] # associate the ALB by ARN
}
# DNS — alias the host at the load balancer
module "dns" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-route53-zone?ref=v1.0.0"
zone_name = "example.com"
records = {
app = {
name = "app"
type = "A"
alias = { name = module.alb.dns_name, zone_id = module.alb.zone_id, evaluate_target_health = true }
}
}
}
⚠️ The log-bucket policy granting ELB log delivery is owned bytf-mod-aws-s3-bucketand must exist before this module's apply; this module references the bucket by name only and never mutates its policy.
| Name | Type | Default | Description |
|---|---|---|---|
name / name_prefix |
string |
null |
LB name or unique prefix (mutually exclusive). FORCE-NEW. |
load_balancer_type |
string |
"application" |
application | network | gateway. FORCE-NEW. |
internal |
bool |
true |
Private scheme (baseline). FORCE-NEW. |
subnet_ids |
list(string) |
[] |
Subnets in ≥2 AZs. Mutually exclusive with subnet_mapping. |
subnet_mapping |
map(object) |
{} |
Per-AZ subnet + static EIP / private IP (internet-facing NLB). |
security_group_ids |
list(string) |
[] |
SGs — required for ALB, optional NLB, n/a GWLB. |
vpc_id |
string |
null |
Default VPC for target groups (overridable per TG). FORCE-NEW per TG. |
ip_address_type |
string |
null |
ipv4 | dualstack | dualstack-without-public-ipv4. |
enable_deletion_protection |
bool |
true |
Blocks API deletion (baseline). |
idle_timeout |
number |
60 |
ALB idle timeout (1–4000). |
client_keep_alive |
number |
null |
ALB client keep-alive (60–604800). |
enable_http2 |
bool |
true |
HTTP/2 on ALB. |
drop_invalid_header_fields |
bool |
true |
Drop invalid headers (ALB; baseline). |
preserve_host_header |
bool |
false |
Preserve Host header (ALB). |
desync_mitigation_mode |
string |
"defensive" |
monitor | defensive | strictest (ALB). |
enable_cross_zone_load_balancing |
bool |
true |
NLB/GWLB cross-zone (always on for ALB; module omits it there). |
access_logs |
object |
{} → enabled |
S3 access logging — ON by default, requires a bucket or explicit enabled = false. |
connection_logs |
object |
{} (off) |
S3 connection logging (ALB). |
target_groups |
map(object) |
{} |
Target groups + health check + stickiness. target_type/protocol/port/vpc_id FORCE-NEW. |
target_group_attachments |
map(object) |
{} |
Static target registrations (instance/IP/Lambda/ALB). |
listeners |
map(object) |
{} |
Front-end listeners. protocol defaults to HTTPS with a TLS 1.3 policy. |
listener_rules |
map(object) |
{} |
Path/host/header/source-IP/query routing rules (ALB). |
listener_certificates |
map(object) |
{} |
Additional SNI certificates on a listener. |
tags |
map(string) |
{} |
Tags for all taggable resources (merge with default_tags). |
timeouts |
object |
{} |
create / update / delete operation timeouts. |
ℹ️ see
variables.tffor full heredoc schemas.
| Name | Description |
|---|---|
id |
Load balancer id (ELBv2 uses the ARN as the id). |
arn |
Load balancer ARN — the cross-resource reference type. |
lb_arn |
Alias of the LB ARN, for explicit cross-module wiring. |
arn_suffix |
ARN suffix for the CloudWatch LoadBalancer metric dimension. |
name |
LB name (generated when name/name_prefix omitted). |
dns_name |
Public/private DNS name (Route 53 alias target). |
zone_id |
Canonical hosted zone id (Route 53 alias zone). |
target_group_arns |
Map of target-group key → ARN. |
target_group_arn_suffixes |
Map of target-group key → ARN suffix (CloudWatch). |
target_group_names |
Map of target-group key → name. |
listener_arns |
Map of listener key → ARN. |
listener_rule_arns |
Map of listener-rule key → ARN. |
tags_all |
All tags incl. provider default_tags (resource tags win). |
ℹ️ No secret-bearing outputs — this module emits no
sensitivevalues.
- ARN format:
arn:aws:elasticloadbalancing:<region>:<account-id>:loadbalancer/<type>/<name>/<id>(e.g..../app/core-app-alb/50dc6c495c0c9188). For ELBv2 theidandarncarry the same value — both are surfaced for cross-module consistency. Target-group ARN:.../targetgroup/<name>/<id>; listener ARN:.../listener/<type>/<name>/<lb-id>/<listener-id>. arn_suffixfor CloudWatch:arn_suffix(app/<name>/<id>) andtarget_group_arn_suffixes(targetgroup/<name>/<id>) are theLoadBalancer/TargetGroupmetric dimensions — wire them into CloudWatch alarms, not the full ARN.- Force-new / immutable fields: on
aws_lb—name,name_prefix,internal,load_balancer_type,ip_address_type, andsubnets/subnet_mapping(NLB subnet removal) are FORCE-NEW; changing any replaces the LB and its DNS name changes. Onaws_lb_target_group—name,target_type,protocol,port, andvpc_idare FORCE-NEW; the module setscreate_before_destroy = trueon target groups so a replacement is created before the old one is removed (avoids orphaning an in-use listener). tags↔tags_all↔default_tags:var.tagsflows to the LB, target groups, listeners and rules; per-childtagsmerge overvar.tags(child wins).tags_allis the provider-computed merge of resource tags over providerdefault_tags, with resource tags winning on key conflict.default_tagslives in the caller's provider block — never inside this module.aws_lb_target_group_attachmentandaws_lb_listener_certificateare NOT taggable (AWS limitation) — those maps expose notagsfield.- Type-gated attributes: ALB-only attributes (
idle_timeout,client_keep_alive,enable_http2,drop_invalid_header_fields,preserve_host_header,desync_mitigation_mode) are passed only forapplication;enable_cross_zone_load_balancingis passed only fornetwork/gateway(ALB cross-zone is always on and not configurable). This prevents the provider rejecting an argument that doesn't apply to the chosen type. - Eventual consistency: a freshly created LB shows
provisioningfor a short while; access-log delivery and the managed ENIs may lag the create call by moments. Anaccess_logsapply fails if the bucket policy granting ELB log delivery is not yet in place — apply ordering matters (own the bucket policy upstream). - Destroy ordering: Terraform sequences listener rules → listeners → target groups → load balancer via implicit dependencies. The ENIs the LB creates in your subnets detach asynchronously, occasionally delaying deletion of the subnets or the attached security group for a few minutes after the LB is gone.
enable_deletion_protection = truemust be set tofalseand applied before a destroy will succeed. - No us-east-1 constraint: ALB/NLB is a regional service. The listener ACM certificate must be in the same Region as the LB (regional ACM) — the us-east-1 global-resource rule applies only to CloudFront, not here.
Secure-by-default posture and every opt-out, explicitly:
| Posture | Default | Opt-out |
|---|---|---|
| Scheme | internal = true (private) |
internal = false (internet-facing; needs public subnets + IGW) |
| Listener protocol | HTTPS with a TLS 1.3 security policy |
set protocol = "HTTP" explicitly (typically a redirect to HTTPS) |
| TLS policy | ELBSecurityPolicy-TLS13-1-2-2021-06 |
older policy via listeners[*].ssl_policy (discouraged) |
| Access logging | enabled (access_logs.enabled = true; bucket required) |
access_logs = { enabled = false } (documented opt-out) |
| Deletion protection | enable_deletion_protection = true |
set to false (required before destroy) |
| Invalid header dropping (ALB) | drop_invalid_header_fields = true |
set to false (documented exception) |
| Desync mitigation (ALB) | desync_mitigation_mode = "defensive" |
monitor (triage) / strictest (hardest) |
| Cross-zone (NLB/GWLB) | enabled for resilience | enable_cross_zone_load_balancing = false (reduces cross-AZ data-transfer cost) |
| HTTP/2 (ALB) | enable_http2 = true |
set to false |
Other principles:
- One composite, one keystone. The LB owns only the resources meaningless without it (target groups, listeners, rules, SNI certs). WAF association is performed in
tf-mod-aws-wafv2(byarn) and ASG/ECS target registration in their own modules — keeping this module's blast radius to the ELB objects. for_each, nevercount, for every child collection — keyed by stable caller strings so adding/removing one target group, listener, rule, attachment, or cert never re-indexes the others.- Key-based resolution. Listeners forward to target groups by
target_group_key, rules attach to listeners bylistener_key, SNI certs bylistener_key— the module maps the caller's stable string to the created resource's ARN, so there is no manual ARN threading. - Primary outputs
id+arn, plusdns_name,zone_id,arn_suffix, the child-collection maps, andtags_all. - No credentials, no region variable. Credentials and Region come from the caller's provider block; the module inherits the single
awsprovider.
# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check
plan/applyrequire valid AWS credentials (profile / SSO / OIDC) resolved through the standard provider chain and a Region. The listener ACM certificate must be in the same Region as the LB.
⚠️ Always pin the module source with?ref=v1.0.0— never a branch.
terraform init -backend=false && terraform validate— schema + key-reference integrity (listenertarget_group_key, rulelistener_key, certlistener_key).terraform fmt -check— canonical formatting.terraform planagainst a sandbox account to confirm the LB, target groups, listeners, rules, and SNI certs materialize and that the access-log delivery is accepted.- Assert
module.<name>.dns_name,zone_id,arn,arn_suffix, andtags_allin your root-module test harness; asserttarget_group_arns/listener_arnsmap keys resolve correctly.
module.alb.aws_lb_target_group.this["app"]: Creation complete [id=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/core-app-alb-app/abc123]
module.alb.aws_lb.this: Still creating... [1m0s elapsed]
module.alb.aws_lb.this: Creation complete after 2m41s [id=arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/core-app-alb/50dc6c495c0c9188]
module.alb.aws_lb_listener.this["https"]: Creation complete
Outputs:
arn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/core-app-alb/50dc6c495c0c9188"
dns_name = "core-app-alb-1234567890.us-east-1.elb.amazonaws.com"
id = "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/core-app-alb/50dc6c495c0c9188"
target_group_arns = { "app" = "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/core-app-alb-app/abc123" }
tags_all = { "DataClass" = "internal", "Environment" = "prod" }
zone_id = "Z35SXDOTRQ7X7K"
| Symptom | Likely cause | Fix |
|---|---|---|
access_logs.bucket is required when access_logs.enabled is true |
Logging is ON by default and no bucket supplied | Wire access_logs.bucket from tf-mod-aws-s3-bucket, or set access_logs = { enabled = false } |
InvalidConfigurationRequest / access-log delivery failed |
Log-bucket policy doesn't grant ELB log delivery | Add the ELB log-delivery policy in tf-mod-aws-s3-bucket before this apply |
CertificateNotFound / wrong-region cert on a listener |
ACM cert is in a region other than the LB's | Issue the cert in the same Region via tf-mod-aws-acm (regional, not us-east-1) |
At least two subnets in two different AZs |
Fewer than 2 AZs in subnet_ids/subnet_mapping |
Supply subnets in ≥2 AZs from tf-mod-aws-vpc |
Security group required on create |
ALB created without security_group_ids |
ALBs require SGs — wire from tf-mod-aws-security-group |
| Plan wants to replace the LB | Changed a FORCE-NEW field (name, internal, load_balancer_type, ip_address_type, NLB subnet) |
Expected — the DNS name will change; plan a cutover |
OperationNotPermitted on destroy |
enable_deletion_protection = true |
Set it to false, apply, then destroy |
destroy leaves subnet/SG undeletable for minutes |
LB ENIs detach asynchronously | Wait for ENI cleanup; retry the dependent delete |
Credential / NoCredentialProviders errors |
Provider chain not configured | Set AWS_PROFILE / SSO / OIDC at the root; the module carries no credentials |
| Tag drift on every plan | A tag also set by provider default_tags with a different value |
Let resource tags win, or remove the overlap from default_tags |
target_group_key/listener_key not found |
A child references a key not present in target_groups/listeners |
Fix the key string — the module resolves keys to ARNs |
- Elastic Load Balancing User Guide
- Application Load Balancer quotas · Network Load Balancer quotas
- Access logs for your ALB · ELB security policies
- Service-linked role for ELB
- Terraform:
aws_lb·aws_lb_target_group·aws_lb_listener·aws_lb_listener_rule - Sibling modules:
tf-mod-aws-vpc,tf-mod-aws-security-group,tf-mod-aws-acm,tf-mod-aws-s3-bucket,tf-mod-aws-wafv2,tf-mod-aws-autoscaling-group,tf-mod-aws-ecs-service,tf-mod-aws-route53-zone,tf-mod-aws-global-accelerator - Module internals:
SCOPE.md
🧡 "Infrastructure as Code should be standardized, consistent, and secure."