Skip to content

microsoftexpert/tf-mod-aws-lb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS Elastic Load Balancing (ALB / NLB) Terraform Module

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.

Terraform aws module type resources


🧩 Overview

  • ⚖️ One load balancer, fully wired. Creates aws_lb plus 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 modern ELBSecurityPolicy-TLS13-1-2-2021-06 security policy; plain HTTP is an explicit opt-in (typically a redirect-to-HTTPS).
  • 🛡️ Private by default. internal = true is the baseline — PII/privacy-regulation workloads stay off the public internet unless the caller deliberately sets internal = false.
  • 🪵 Access logging on by default. access_logs defaults 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 = true blocks 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.tags merges with provider default_tags, flows to the LB, target groups, listeners and rules, and is surfaced as tags_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.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


🗺️ Where this fits in the family

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
Loading

🧬 What this module builds

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
Loading
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.


✅ Provider / Versions

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). Wire certificate_arn from a tf-mod-aws-acm call made against the same provider/Region as this module.


🔑 Required IAM Permissions

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. AWSServiceRoleForElasticLoadBalancing is auto-created on first ELB use; the identity needs iam:CreateServiceLinkedRole only until it exists. The ec2:* 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 S3 access_logs bucket policy must already grant ELB log delivery; this module does not mutate the bucket policy (own it in tf-mod-aws-s3-bucket).


📋 AWS Prerequisites

  • Service-linked role. AWSServiceRoleForElasticLoadBalancing is auto-created on first ELB use (the identity may need iam: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_ids from tf-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_arn from tf-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.com write access. Wire the bucket name from tf-mod-aws-s3-bucket (log-archive). The policy must exist before apply or the create fails.
  • Internet-facing scheme. internal = false additionally 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.

📁 Module Structure

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

⚙️ Quick Start

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" }
}

🔌 Cross-Module Contract

Consumes

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

Emits

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

📚 Example Library

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 by tf-mod-aws-s3-bucket and must exist before this module's apply; this module references the bucket by name only and never mutates its policy.


📥 Inputs

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.tf for full heredoc schemas.


🧾 Outputs

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 sensitive values.


🧠 Architecture Notes

  • ARN format: arn:aws:elasticloadbalancing:<region>:<account-id>:loadbalancer/<type>/<name>/<id> (e.g. .../app/core-app-alb/50dc6c495c0c9188). For ELBv2 the id and arn carry 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_suffix for CloudWatch: arn_suffix (app/<name>/<id>) and target_group_arn_suffixes (targetgroup/<name>/<id>) are the LoadBalancer / TargetGroup metric dimensions — wire them into CloudWatch alarms, not the full ARN.
  • Force-new / immutable fields: on aws_lbname, name_prefix, internal, load_balancer_type, ip_address_type, and subnets/subnet_mapping (NLB subnet removal) are FORCE-NEW; changing any replaces the LB and its DNS name changes. On aws_lb_target_groupname, target_type, protocol, port, and vpc_id are FORCE-NEW; the module sets create_before_destroy = true on target groups so a replacement is created before the old one is removed (avoids orphaning an in-use listener).
  • tagstags_alldefault_tags: var.tags flows to the LB, target groups, listeners and rules; per-child tags merge over var.tags (child wins). tags_all is the provider-computed merge of resource tags over provider default_tags, with resource tags winning on key conflict. default_tags lives in the caller's provider block — never inside this module. aws_lb_target_group_attachment and aws_lb_listener_certificate are NOT taggable (AWS limitation) — those maps expose no tags field.
  • 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 for application; enable_cross_zone_load_balancing is passed only for network/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 provisioning for a short while; access-log delivery and the managed ENIs may lag the create call by moments. An access_logs apply 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 = true must be set to false and 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.

🧱 Design Principles

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 (by arn) and ASG/ECS target registration in their own modules — keeping this module's blast radius to the ELB objects.
  • for_each, never count, 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 by listener_key, SNI certs by listener_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, plus dns_name, zone_id, arn_suffix, the child-collection maps, and tags_all.
  • No credentials, no region variable. Credentials and Region come from the caller's provider block; the module inherits the single aws provider.

🚀 Runbook

# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check

plan / apply require 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.


🧪 Testing

  • terraform init -backend=false && terraform validate — schema + key-reference integrity (listener target_group_key, rule listener_key, cert listener_key).
  • terraform fmt -check — canonical formatting.
  • terraform plan against 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, and tags_all in your root-module test harness; assert target_group_arns / listener_arns map keys resolve correctly.

💬 Example Output

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"

🔍 Troubleshooting

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

🔗 Related Docs


🧡 "Infrastructure as Code should be standardized, consistent, and secure."

Packages

 
 
 

Contributors

Languages