diff --git a/modules/aws/lb/README.md b/modules/aws/lb/README.md new file mode 100644 index 00000000..3dfc4f15 --- /dev/null +++ b/modules/aws/lb/README.md @@ -0,0 +1,111 @@ +NLB +```hcl +module "nlb" { + source = "./terraform-modules/modules/aws/nlb" + + name = "my-nlb" + internal = false + load_balancer_type = "network" + subnets = ["subnet-1234", "subnet-5678"] + + enable_cross_zone_load_balancing = true + + tags = { + Environment = "production" + } +} +``` + +For an ALB: + +```hcl +module "alb" { + source = "./terraform-modules/modules/aws/nlb" + + name = "my-alb" + internal = false + load_balancer_type = "application" + security_groups = ["sg-1234"] + subnets = ["subnet-1234", "subnet-5678"] + + access_logs = { + bucket = "my-alb-logs" + prefix = "my-alb" + enabled = true + } + + tags = { + Environment = "production" + } +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_lb.load_balancer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb) | resource | +| [aws_lb_listener.listener](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | +| [aws_lb_listener_rule.listener_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener_rule) | resource | +| [aws_lb_target_group.target_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_target_group) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [access\_logs](#input\_access\_logs) | Access logs configuration for the LB |
map(object({| `null` | no | +| [customer\_owned\_ipv4\_pool](#input\_customer\_owned\_ipv4\_pool) | The ID of the customer owned ipv4 pool to use for this load balancer | `string` | `null` | no | +| [desync\_mitigation\_mode](#input\_desync\_mitigation\_mode) | Determines how the load balancer handles requests that might pose a security risk to your application | `string` | `"defensive"` | no | +| [drop\_invalid\_header\_fields](#input\_drop\_invalid\_header\_fields) | Indicates whether invalid header fields are dropped in application load balancers | `bool` | `false` | no | +| [enable\_cross\_zone\_load\_balancing](#input\_enable\_cross\_zone\_load\_balancing) | If true, cross-zone load balancing of the load balancer will be enabled | `bool` | `false` | no | +| [enable\_deletion\_protection](#input\_enable\_deletion\_protection) | If true, deletion of the load balancer will be disabled | `bool` | `false` | no | +| [enable\_http2](#input\_enable\_http2) | Indicates whether HTTP/2 is enabled in application load balancers | `bool` | `true` | no | +| [enable\_waf\_fail\_open](#input\_enable\_waf\_fail\_open) | Indicates whether to allow a WAF-enabled load balancer to route requests to targets if it is unable to forward the request to AWS WAF | `bool` | `false` | no | +| [idle\_timeout](#input\_idle\_timeout) | The time in seconds that the connection is allowed to be idle | `number` | `60` | no | +| [internal](#input\_internal) | If true, the LB will be internal | `bool` | `false` | no | +| [ip\_address\_type](#input\_ip\_address\_type) | The type of IP addresses used by the subnets for your load balancer | `string` | `"ipv4"` | no | +| [listener\_rules](#input\_listener\_rules) | Map of listener rule configurations |
bucket = string
prefix = string
enabled = bool
}))
map(object({| `{}` | no | +| [listeners](#input\_listeners) | Map of listener configurations |
listener_key = string
priority = optional(number)
action = object({
type = string
target_group_arn = optional(string)
fixed_response = optional(object({
content_type = string
message_body = optional(string)
status_code = optional(string)
}))
redirect = optional(object({
path = optional(string)
host = optional(string)
port = optional(string)
protocol = optional(string)
query = optional(string)
status_code = string
}))
})
conditions = list(object({
host_header = optional(object({
values = list(string)
}))
http_header = optional(map(object({
http_header_name = string
values = list(string)
})))
path_pattern = optional(object({
values = list(string)
}))
query_string = optional(map(object({
key = optional(string)
value = string
})))
source_ip = optional(object({
values = list(string)
}))
}))
}))
map(object({| `{}` | no | +| [load\_balancer\_type](#input\_load\_balancer\_type) | Type of load balancer. Valid values are application or network | `string` | `"network"` | no | +| [name](#input\_name) | Name of the load balancer | `string` | n/a | yes | +| [security\_groups](#input\_security\_groups) | List of security group IDs to assign to the LB | `list(string)` | `[]` | no | +| [subnet\_mappings](#input\_subnet\_mappings) | List of subnet mapping configurations | `list(map(string))` | `[]` | no | +| [subnets](#input\_subnets) | List of subnet IDs to attach to the LB | `list(string)` | `[]` | no | +| [tags](#input\_tags) | A map of tags to assign to the resource | `map(string)` | `{}` | no | +| [target\_group\_name](#input\_target\_group\_name) | Name of the target group | `string` | `null` | no | +| [target\_group\_name\_prefix](#input\_target\_group\_name\_prefix) | Prefix for the target group name | `string` | `null` | no | +| [target\_groups](#input\_target\_groups) | Map of target group configurations |
port = number
protocol = string
ssl_policy = optional(string)
certificate_arn = optional(string)
alpn_policy = optional(string)
authenticate_oidc = optional(object({
authorization_endpoint = string
client_id = string
client_secret = string
issuer = string
token_endpoint = string
user_info_endpoint = string
}))
authenticate_cognito = optional(object({
user_pool_arn = string
user_pool_client_id = string
user_pool_domain = string
}))
mutual_authentication = optional(object({
mode = string # Only valid field, can be "verify" or "strict"
}))
default_action = object({
type = string
target_group_arn = optional(string)
fixed_response = optional(object({
content_type = string
message_body = optional(string)
status_code = optional(string)
}))
redirect = optional(object({
path = optional(string)
host = optional(string)
port = optional(string)
protocol = optional(string)
query = optional(string)
status_code = string
}))
})
}))
map(object({| `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | The ARN of the load balancer | +| [arn\_suffix](#output\_arn\_suffix) | The ARN suffix for use with CloudWatch Metrics | +| [dns\_name](#output\_dns\_name) | The DNS name of the load balancer | +| [id](#output\_id) | The ID of the load balancer | +| [listener\_rules](#output\_listener\_rules) | Map of listener rules created and their attributes | +| [listeners](#output\_listeners) | Map of listeners created and their attributes | +| [name](#output\_name) | The name of the load balancer | +| [target\_groups](#output\_target\_groups) | Map of target groups created and their attributes | +| [vpc\_id](#output\_vpc\_id) | The VPC ID of the load balancer | +| [zone\_id](#output\_zone\_id) | The canonical hosted zone ID of the load balancer | + \ No newline at end of file diff --git a/modules/aws/lb/main.tf b/modules/aws/lb/main.tf new file mode 100644 index 00000000..eebcc7a7 --- /dev/null +++ b/modules/aws/lb/main.tf @@ -0,0 +1,301 @@ +########################### +# Provider Configuration +########################### +terraform { + required_version = ">= 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0.0" + } + } +} + +########################### +# Locals +########################### + +locals { + is_application = var.load_balancer_type == "application" + is_network = var.load_balancer_type == "network" +} + +########################################################### +# Load Balancer +########################################################### + +resource "aws_lb" "load_balancer" { + # Common settings + name = var.name + internal = var.internal + load_balancer_type = var.load_balancer_type + subnets = var.subnets + enable_deletion_protection = var.enable_deletion_protection + customer_owned_ipv4_pool = var.customer_owned_ipv4_pool + ip_address_type = var.ip_address_type + + # Application Load Balancer specific settings + security_groups = local.is_application ? var.security_groups : null + desync_mitigation_mode = local.is_application ? var.desync_mitigation_mode : null + idle_timeout = local.is_application ? var.idle_timeout : null + drop_invalid_header_fields = local.is_application ? var.drop_invalid_header_fields : null + enable_http2 = local.is_application ? var.enable_http2 : null + enable_waf_fail_open = local.is_application ? var.enable_waf_fail_open : null + + # Network Load Balancer specific settings + enable_cross_zone_load_balancing = local.is_network ? var.enable_cross_zone_load_balancing : null + + dynamic "access_logs" { + for_each = var.access_logs != null ? { create = var.access_logs } : {} + content { + bucket = access_logs.value.bucket + prefix = access_logs.value.prefix + enabled = access_logs.value.enabled + } + } + + dynamic "subnet_mapping" { + for_each = var.subnet_mappings + content { + subnet_id = subnet_mapping.value.subnet_id + allocation_id = subnet_mapping.value.allocation_id + private_ipv4_address = subnet_mapping.value.private_ipv4_address + ipv6_address = subnet_mapping.value.ipv6_address + } + } + + tags = merge( + var.tags, + { + Name = var.name + }, + ) +} + +########################################################### +# Target Group +########################################################### + +resource "aws_lb_target_group" "target_group" { + for_each = var.target_groups + + # Common settings + name = each.value.name + port = each.value.port + protocol = each.value.protocol + vpc_id = each.value.vpc_id + target_type = each.value.target_type + deregistration_delay = each.value.deregistration_delay + + # Application Load Balancer specific settings + slow_start = local.is_application ? each.value.slow_start : null + load_balancing_algorithm_type = local.is_application ? each.value.load_balancing_algorithm_type : null + + # Network Load Balancer specific settings + proxy_protocol_v2 = local.is_network ? each.value.target_group_proxy_protocol_v2 : null + preserve_client_ip = local.is_network ? each.value.target_group_preserve_client_ip : null + + dynamic "health_check" { + for_each = each.value.health_check != null ? each.value.health_check : {} + content { + enabled = health_check.value.enabled + healthy_threshold = health_check.value.healthy_threshold + interval = health_check.value.interval + matcher = health_check.value.matcher + path = health_check.value.path + port = health_check.value.port + protocol = health_check.value.protocol + timeout = health_check.value.timeout + unhealthy_threshold = health_check.value.unhealthy_threshold + } + } + + dynamic "stickiness" { + for_each = each.value.stickiness != null ? each.value.stickiness : [] + content { + type = stickiness.value.type + cookie_duration = stickiness.value.cookie_duration + cookie_name = stickiness.value.cookie_name + } + } + + tags = merge( + var.tags, + each.value.tags + ) +} + +########################################################### +# Listeners +########################################################### + +resource "aws_lb_listener" "listener" { + for_each = var.listeners + + # Common settings + load_balancer_arn = aws_lb.load_balancer.arn + port = each.value.port + protocol = each.value.protocol + + # SSL/TLS settings + ssl_policy = each.value.ssl_policy + certificate_arn = each.value.certificate_arn + + # Application Load Balancer specific settings + alpn_policy = local.is_application ? each.value.alpn_policy : null + + dynamic "mutual_authentication" { + for_each = local.is_network && each.value.mutual_authentication != null ? each.value.mutual_authentication : {} + content { + mode = mutual_authentication.value.mode + } + } + + + + dynamic "default_action" { + for_each = [each.value.default_action] + content { + # Common settings + type = default_action.value.type + target_group_arn = aws_lb_target_group.target_group["main"].arn + + # Application Load Balancer specific fixed response action + dynamic "fixed_response" { + for_each = default_action.value.fixed_response != null ? default_action.value.fixed_response : {} + content { + content_type = fixed_response.value.content_type + message_body = fixed_response.value.message_body + status_code = fixed_response.value.status_code + } + } + + # Application Load Balancer specific redirect action + dynamic "redirect" { + for_each = default_action.value.redirect != null ? default_action.value.redirect : {} + content { + path = redirect.value.path + host = redirect.value.host + port = redirect.value.port + protocol = redirect.value.protocol + query = redirect.value.query + status_code = redirect.value.status_code + } + } + + # Application Load Balancer authentication settings + dynamic "authenticate_oidc" { + for_each = each.value.authenticate_oidc != null && local.is_application ? each.value.authenticate_oidc : {} + content { + authorization_endpoint = authenticate_oidc.value.authorization_endpoint + client_id = authenticate_oidc.value.client_id + client_secret = authenticate_oidc.value.client_secret + issuer = authenticate_oidc.value.issuer + token_endpoint = authenticate_oidc.value.token_endpoint + user_info_endpoint = authenticate_oidc.value.user_info_endpoint + } + } + + dynamic "authenticate_cognito" { + for_each = each.value.authenticate_cognito != null && local.is_application ? each.value.authenticate_cognito : {} + content { + user_pool_arn = authenticate_cognito.value.user_pool_arn + user_pool_client_id = authenticate_cognito.value.user_pool_client_id + user_pool_domain = authenticate_cognito.value.user_pool_domain + } + } + } + } +} + +########################################################### +# Listener Rules +########################################################### + +# (Application Load Balancer only) +resource "aws_lb_listener_rule" "listener_rule" { + # Common settings + for_each = var.listener_rules + listener_arn = aws_lb_listener.listener[each.value.listener_key].arn + priority = each.value.priority + + # Application Load Balancer action settings + dynamic "action" { + for_each = [each.value.action] + content { + type = action.value.type + target_group_arn = aws_lb_target_group.target_group["main"].arn + + # ALB fixed response action + dynamic "fixed_response" { + for_each = action.value.fixed_response != null ? action.value.fixed_response : {} + content { + content_type = fixed_response.value.content_type + message_body = fixed_response.value.message_body + status_code = fixed_response.value.status_code + } + } + + # ALB redirect action + dynamic "redirect" { + for_each = action.value.redirect != null ? action.value.redirect : {} + content { + path = redirect.value.path + host = redirect.value.host + port = redirect.value.port + protocol = redirect.value.protocol + query = redirect.value.query + status_code = redirect.value.status_code + } + } + } + } + + # Application Load Balancer condition settings + dynamic "condition" { + for_each = each.value.conditions + content { + # ALB host header condition + dynamic "host_header" { + for_each = condition.value.host_header != null ? condition.value.host_header : {} + content { + values = host_header.value.values + } + } + + # ALB http header condition + dynamic "http_header" { + for_each = condition.value.http_header != null ? condition.value.http_header : {} + content { + http_header_name = http_header.value.http_header_name + values = http_header.value.values + } + } + + # ALB path pattern condition + dynamic "path_pattern" { + for_each = condition.value.path_pattern != null ? condition.value.path_pattern : {} + content { + values = path_pattern.value.values + } + } + + # ALB query string condition + dynamic "query_string" { + for_each = condition.value.query_string != null ? condition.value.query_string : {} + content { + key = query_string.value.key + value = query_string.value.value + } + } + + # Common source IP condition (works for both ALB and NLB) + dynamic "source_ip" { + for_each = condition.value.source_ip != null ? { source_ip = condition.value.source_ip } : {} + content { + values = source_ip.value.values + } + } + } + } +} diff --git a/modules/aws/lb/outputs.tf b/modules/aws/lb/outputs.tf new file mode 100644 index 00000000..cbb0c879 --- /dev/null +++ b/modules/aws/lb/outputs.tf @@ -0,0 +1,49 @@ +output "id" { + description = "The ID of the load balancer" + value = aws_lb.load_balancer.id +} + +output "arn" { + description = "The ARN of the load balancer" + value = aws_lb.load_balancer.arn +} + +output "arn_suffix" { + description = "The ARN suffix for use with CloudWatch Metrics" + value = aws_lb.load_balancer.arn_suffix +} + +output "dns_name" { + description = "The DNS name of the load balancer" + value = aws_lb.load_balancer.dns_name +} + +output "zone_id" { + description = "The canonical hosted zone ID of the load balancer" + value = aws_lb.load_balancer.zone_id +} + +output "name" { + description = "The name of the load balancer" + value = aws_lb.load_balancer.name +} + +output "vpc_id" { + description = "The VPC ID of the load balancer" + value = aws_lb.load_balancer.vpc_id +} + +output "target_groups" { + description = "Map of target groups created and their attributes" + value = aws_lb_target_group.target_group +} + +output "listeners" { + description = "Map of listeners created and their attributes" + value = aws_lb_listener.listener +} + +output "listener_rules" { + description = "Map of listener rules created and their attributes" + value = aws_lb_listener_rule.listener_rule +} diff --git a/modules/aws/lb/variables.tf b/modules/aws/lb/variables.tf new file mode 100644 index 00000000..e880decb --- /dev/null +++ b/modules/aws/lb/variables.tf @@ -0,0 +1,321 @@ +# Load Balancer Variables +variable "name" { + description = "Name of the load balancer" + type = string +} + +variable "internal" { + description = "If true, the LB will be internal" + type = bool + default = false +} + +variable "load_balancer_type" { + description = "Type of load balancer. Valid values are application or network" + type = string + default = "network" + + validation { + condition = contains(["application", "network"], var.load_balancer_type) + error_message = "Valid values for load_balancer_type are (application, network)." + } +} + +variable "security_groups" { + description = "List of security group IDs to assign to the LB" + type = list(string) + default = [] +} + +variable "subnets" { + description = "List of subnet IDs to attach to the LB" + type = list(string) + default = [] +} + +variable "subnet_mappings" { + description = "List of subnet mapping configurations" + type = list(map(string)) + default = [] +} + +variable "enable_deletion_protection" { + description = "If true, deletion of the load balancer will be disabled" + type = bool + default = false +} + +variable "enable_cross_zone_load_balancing" { + description = "If true, cross-zone load balancing of the load balancer will be enabled" + type = bool + default = false +} + +variable "customer_owned_ipv4_pool" { + description = "The ID of the customer owned ipv4 pool to use for this load balancer" + type = string + default = null +} + +variable "ip_address_type" { + description = "The type of IP addresses used by the subnets for your load balancer" + type = string + default = "ipv4" +} + +variable "desync_mitigation_mode" { + description = "Determines how the load balancer handles requests that might pose a security risk to your application" + type = string + default = "defensive" +} + +variable "access_logs" { + description = "Access logs configuration for the LB" + type = map(object({ + bucket = string + prefix = string + enabled = bool + })) + default = null +} + +variable "idle_timeout" { + description = "The time in seconds that the connection is allowed to be idle" + type = number + default = 60 +} + +variable "enable_http2" { + description = "Indicates whether HTTP/2 is enabled in application load balancers" + type = bool + default = true +} + +variable "enable_waf_fail_open" { + description = "Indicates whether to allow a WAF-enabled load balancer to route requests to targets if it is unable to forward the request to AWS WAF" + type = bool + default = false +} + +variable "drop_invalid_header_fields" { + description = "Indicates whether invalid header fields are dropped in application load balancers" + type = bool + default = false +} + +# Target Group Variables +variable "target_group_name" { + description = "Name of the target group" + type = string + default = null +} + +variable "target_group_name_prefix" { + description = "Prefix for the target group name" + type = string + default = null +} + +# variable "target_group_port" { +# description = "Port on which targets receive traffic" +# type = number +# } + +# variable "target_group_protocol" { +# description = "Protocol to use for routing traffic to the targets" +# type = string +# } + +# variable "target_group_vpc_id" { +# description = "Identifier of the VPC in which to create the target group" +# type = string +# } + +# variable "target_group_target_type" { +# description = "Type of target that you must specify when registering targets with this target group" +# type = string +# default = "instance" +# } + +# variable "target_group_deregistration_delay" { +# description = "Amount of time to wait for in-flight requests to complete before deregistering a target" +# type = number +# default = 300 +# } + +# variable "target_group_slow_start" { +# description = "Amount of time for targets to warm up before the load balancer sends them a full share of requests" +# type = number +# default = 0 +# } + +# variable "target_group_proxy_protocol_v2" { +# description = "Whether to enable support for proxy protocol v2" +# type = bool +# default = false +# } + +# variable "target_group_load_balancing_algorithm_type" { +# description = "Determines how the load balancer selects targets when routing requests" +# type = string +# default = null +# } + +# variable "target_group_preserve_client_ip" { +# description = "Whether client IP preservation is enabled" +# type = bool +# default = null +# } + +variable "target_groups" { + description = "Map of target group configurations" + type = map(object({ + name = string + port = number + protocol = string + target_type = string + vpc_id = string + deregistration_delay = optional(number) + slow_start = optional(number) + load_balancing_algorithm_type = optional(string) + target_group_proxy_protocol_v2 = optional(bool) + target_group_preserve_client_ip = optional(bool) + protocol_version = optional(string) + connection_termination = optional(bool) + lambda_multi_value_headers_enabled = optional(bool) + health_check = map(object({ + enabled = optional(bool, true) + healthy_threshold = optional(number, 3) + interval = optional(number, 30) + matcher = optional(string) + path = optional(string) + port = optional(string, "traffic-port") + protocol = optional(string, "HTTP") + timeout = optional(number, 5) + unhealthy_threshold = optional(number, 3) + success_codes = optional(string) + grace_period_seconds = optional(number) + })) + + stickiness = set(object({ + type = string + cookie_duration = optional(number) + cookie_name = optional(string) + })) + + tags = optional(map(string), {}) + })) + default = {} +} + +# Listener Variables +variable "listeners" { + description = "Map of listener configurations" + type = map(object({ + port = number + protocol = string + ssl_policy = optional(string) + certificate_arn = optional(string) + alpn_policy = optional(string) + + authenticate_oidc = optional(object({ + authorization_endpoint = string + client_id = string + client_secret = string + issuer = string + token_endpoint = string + user_info_endpoint = string + })) + + authenticate_cognito = optional(object({ + user_pool_arn = string + user_pool_client_id = string + user_pool_domain = string + })) + + mutual_authentication = optional(object({ + mode = string # Only valid field, can be "verify" or "strict" + })) + + default_action = object({ + type = string + target_group_arn = optional(string) + + fixed_response = optional(object({ + content_type = string + message_body = optional(string) + status_code = optional(string) + })) + + redirect = optional(object({ + path = optional(string) + host = optional(string) + port = optional(string) + protocol = optional(string) + query = optional(string) + status_code = string + })) + }) + })) + default = {} +} + +# Listener Rule Variables +variable "listener_rules" { + description = "Map of listener rule configurations" + type = map(object({ + listener_key = string + priority = optional(number) + + action = object({ + type = string + target_group_arn = optional(string) + + fixed_response = optional(object({ + content_type = string + message_body = optional(string) + status_code = optional(string) + })) + + redirect = optional(object({ + path = optional(string) + host = optional(string) + port = optional(string) + protocol = optional(string) + query = optional(string) + status_code = string + })) + }) + + conditions = list(object({ + host_header = optional(object({ + values = list(string) + })) + + http_header = optional(map(object({ + http_header_name = string + values = list(string) + }))) + + path_pattern = optional(object({ + values = list(string) + })) + + query_string = optional(map(object({ + key = optional(string) + value = string + }))) + + source_ip = optional(object({ + values = list(string) + })) + })) + })) + default = {} +} + +variable "tags" { + description = "A map of tags to assign to the resource" + type = map(string) + default = {} +}
name = string
port = number
protocol = string
target_type = string
vpc_id = string
deregistration_delay = optional(number)
slow_start = optional(number)
load_balancing_algorithm_type = optional(string)
target_group_proxy_protocol_v2 = optional(bool)
target_group_preserve_client_ip = optional(bool)
protocol_version = optional(string)
connection_termination = optional(bool)
lambda_multi_value_headers_enabled = optional(bool)
health_check = map(object({
enabled = optional(bool, true)
healthy_threshold = optional(number, 3)
interval = optional(number, 30)
matcher = optional(string)
path = optional(string)
port = optional(string, "traffic-port")
protocol = optional(string, "HTTP")
timeout = optional(number, 5)
unhealthy_threshold = optional(number, 3)
success_codes = optional(string)
grace_period_seconds = optional(number)
}))
stickiness = set(object({
type = string
cookie_duration = optional(number)
cookie_name = optional(string)
}))
tags = optional(map(string), {})
}))