IaC AWS

Terraform Module: AWS Global Accelerator — anycast static IPs and edge routing in one reusable block

Quick take — A production Terraform module for AWS Global Accelerator: provision an accelerator with anycast static IPs, listeners, and endpoint groups for low-latency global traffic, flow logs, and health-checked failover. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.

Quickstart (copy-paste)

Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):

provider "aws" {
  region = "us-east-1"
}

module "global_accelerator" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-global-accelerator?ref=v1.0.0"

  name      = "..."  # Accelerator name; also used as the `Name` tag. 1–64 cha…
  listeners = {}     # Map of listeners; each defines protocol, client affinit…
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

AWS Global Accelerator is a networking service that sits at the AWS edge. You get two static anycast IPv4 addresses (and optional dual-stack IPv6) advertised from over 100 edge locations worldwide. Client traffic enters AWS at the nearest edge PoP and then rides the AWS backbone — not the public internet — to your application endpoints (ALBs, NLBs, EC2 instances, or Elastic IPs). The payoff is lower and more consistent latency, instant regional failover via active health checks, and a fixed pair of IPs you can hand to firewalls, allow-lists, or hardcoded DNS that never change even if your backends do.

The catch is that a working accelerator is never one resource. You need the accelerator itself, at least one aws_globalaccelerator_listener (which defines the port ranges and client-affinity behaviour), and at least one aws_globalaccelerator_endpoint_group per region pointing at your actual backends with traffic-dial and health-check settings. Wiring those four resource types together by hand — and getting the endpoint_configuration blocks, client_ip_preservation, and weights right — is repetitive and easy to fumble. This module collapses the whole stack into one var-driven block so every accelerator in your estate has consistent flow-log retention, health checks, and tagging.

When to use it

Module structure

terraform-module-aws-global-accelerator/
├── versions.tf
├── main.tf
├── variables.tf
└── outputs.tf

versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

main.tf

locals {
  # Flatten listener -> endpoint_group into a single map so each endpoint group
  # gets a stable, deterministic for_each key (no count-index churn on edits).
  endpoint_groups = merge([
    for lk, lst in var.listeners : {
      for egk, eg in lst.endpoint_groups :
      "${lk}/${egk}" => merge(eg, {
        listener_key = lk
        eg_region    = egk
      })
    }
  ]...)
}

resource "aws_globalaccelerator_accelerator" "this" {
  name            = var.name
  ip_address_type = var.ip_address_type
  enabled         = var.enabled

  # Optionally bring your own /24 or two static IPs from an assigned BYOIP pool.
  ip_addresses = length(var.ip_addresses) > 0 ? var.ip_addresses : null

  dynamic "attributes" {
    for_each = var.flow_logs_enabled ? [1] : []
    content {
      flow_logs_enabled   = true
      flow_logs_s3_bucket = var.flow_logs_s3_bucket
      flow_logs_s3_prefix = var.flow_logs_s3_prefix
    }
  }

  tags = merge(var.tags, { Name = var.name })
}

resource "aws_globalaccelerator_listener" "this" {
  for_each = var.listeners

  accelerator_arn = aws_globalaccelerator_accelerator.this.arn
  protocol        = each.value.protocol
  client_affinity = each.value.client_affinity

  dynamic "port_range" {
    for_each = each.value.port_ranges
    content {
      from_port = port_range.value.from_port
      to_port   = port_range.value.to_port
    }
  }
}

resource "aws_globalaccelerator_endpoint_group" "this" {
  for_each = local.endpoint_groups

  listener_arn                  = aws_globalaccelerator_listener.this[each.value.listener_key].arn
  endpoint_group_region         = each.value.eg_region
  traffic_dial_percentage       = each.value.traffic_dial_percentage
  health_check_protocol         = each.value.health_check_protocol
  health_check_port             = each.value.health_check_port
  health_check_path             = each.value.health_check_protocol == "HTTP" || each.value.health_check_protocol == "HTTPS" ? each.value.health_check_path : null
  health_check_interval_seconds = each.value.health_check_interval_seconds
  threshold_count               = each.value.threshold_count

  dynamic "endpoint_configuration" {
    for_each = each.value.endpoints
    content {
      endpoint_id                    = endpoint_configuration.value.endpoint_id
      weight                         = endpoint_configuration.value.weight
      client_ip_preservation_enabled = endpoint_configuration.value.client_ip_preservation_enabled
    }
  }

  dynamic "port_override" {
    for_each = each.value.port_overrides
    content {
      listener_port = port_override.value.listener_port
      endpoint_port = port_override.value.endpoint_port
    }
  }
}

variables.tf

variable "name" {
  description = "Name of the Global Accelerator (shown in console and used as the Name tag)."
  type        = string

  validation {
    condition     = can(regex("^[a-zA-Z0-9-]{1,64}$", var.name))
    error_message = "name must be 1-64 characters: letters, numbers, and hyphens only."
  }
}

variable "enabled" {
  description = "Whether the accelerator is enabled. Disabling stops it advertising its IPs but retains them."
  type        = bool
  default     = true
}

variable "ip_address_type" {
  description = "Address family for the accelerator's static IPs."
  type        = string
  default     = "IPV4"

  validation {
    condition     = contains(["IPV4", "DUAL_STACK"], var.ip_address_type)
    error_message = "ip_address_type must be either IPV4 or DUAL_STACK."
  }
}

variable "ip_addresses" {
  description = "Optional list of static IPv4 addresses from a BYOIP pool to assign. Leave empty for AWS-assigned IPs."
  type        = list(string)
  default     = []

  validation {
    condition     = length(var.ip_addresses) == 0 || length(var.ip_addresses) == 2
    error_message = "ip_addresses must be empty (AWS-assigned) or exactly 2 addresses (BYOIP)."
  }
}

variable "flow_logs_enabled" {
  description = "Enable Global Accelerator flow logs delivery to S3."
  type        = bool
  default     = false
}

variable "flow_logs_s3_bucket" {
  description = "S3 bucket name for flow logs. Required when flow_logs_enabled is true."
  type        = string
  default     = null
}

variable "flow_logs_s3_prefix" {
  description = "S3 key prefix for flow logs (must end with a trailing slash)."
  type        = string
  default     = "global-accelerator/"
}

variable "listeners" {
  description = <<-EOT
    Map of listeners keyed by a friendly name. Each listener defines its protocol,
    client affinity, port ranges, and one endpoint group per AWS region.
  EOT
  type = map(object({
    protocol        = optional(string, "TCP")
    client_affinity = optional(string, "NONE")
    port_ranges = list(object({
      from_port = number
      to_port   = number
    }))
    endpoint_groups = map(object({
      traffic_dial_percentage       = optional(number, 100)
      health_check_protocol         = optional(string, "TCP")
      health_check_port             = optional(number, null)
      health_check_path             = optional(string, "/")
      health_check_interval_seconds = optional(number, 30)
      threshold_count               = optional(number, 3)
      endpoints = list(object({
        endpoint_id                    = string
        weight                         = optional(number, 128)
        client_ip_preservation_enabled = optional(bool, false)
      }))
      port_overrides = optional(list(object({
        listener_port = number
        endpoint_port = number
      })), [])
    }))
  }))

  validation {
    condition = alltrue([
      for l in values(var.listeners) : contains(["TCP", "UDP"], l.protocol)
    ])
    error_message = "Each listener protocol must be TCP or UDP."
  }

  validation {
    condition = alltrue([
      for l in values(var.listeners) : contains(["NONE", "SOURCE_IP"], l.client_affinity)
    ])
    error_message = "Each listener client_affinity must be NONE or SOURCE_IP."
  }

  validation {
    condition = alltrue(flatten([
      for l in values(var.listeners) : [
        for eg in values(l.endpoint_groups) :
        eg.traffic_dial_percentage >= 0 && eg.traffic_dial_percentage <= 100
      ]
    ]))
    error_message = "traffic_dial_percentage must be between 0 and 100 for every endpoint group."
  }

  validation {
    condition = alltrue(flatten([
      for l in values(var.listeners) : [
        for eg in values(l.endpoint_groups) :
        contains(["TCP", "HTTP", "HTTPS"], eg.health_check_protocol)
      ]
    ]))
    error_message = "health_check_protocol must be TCP, HTTP, or HTTPS for every endpoint group."
  }
}

variable "tags" {
  description = "Tags applied to the accelerator."
  type        = map(string)
  default     = {}
}

outputs.tf

output "id" {
  description = "The ARN of the accelerator (used as its ID)."
  value       = aws_globalaccelerator_accelerator.this.id
}

output "arn" {
  description = "The ARN of the accelerator."
  value       = aws_globalaccelerator_accelerator.this.arn
}

output "name" {
  description = "The name of the accelerator."
  value       = aws_globalaccelerator_accelerator.this.name
}

output "dns_name" {
  description = "The DNS name (e.g. a1234567890abcdef.awsglobalaccelerator.com) that resolves to the static anycast IPs. CNAME your domain here."
  value       = aws_globalaccelerator_accelerator.this.dns_name
}

output "hosted_zone_id" {
  description = "The Global Accelerator Route 53 hosted zone ID, for use in alias records."
  value       = aws_globalaccelerator_accelerator.this.hosted_zone_id
}

output "static_ip_addresses" {
  description = "The two anycast static IPv4 addresses assigned to the accelerator — use these for firewall allow-lists."
  value       = aws_globalaccelerator_accelerator.this.ip_sets[0].ip_addresses
}

output "listener_arns" {
  description = "Map of listener friendly-name to listener ARN."
  value       = { for k, l in aws_globalaccelerator_listener.this : k => l.arn }
}

output "endpoint_group_arns" {
  description = "Map of 'listener/region' key to endpoint group ARN."
  value       = { for k, eg in aws_globalaccelerator_endpoint_group.this : k => eg.arn }
}

How to use it

module "global_accelerator" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-global-accelerator?ref=v1.0.0"

  name              = "kloudvin-api-prod"
  ip_address_type   = "IPV4"
  flow_logs_enabled = true
  flow_logs_s3_bucket = aws_s3_bucket.ga_flow_logs.bucket
  flow_logs_s3_prefix = "ga/api-prod/"

  listeners = {
    https = {
      protocol        = "TCP"
      client_affinity = "SOURCE_IP" # sticky sessions to the same endpoint per source IP
      port_ranges = [
        { from_port = 443, to_port = 443 }
      ]
      endpoint_groups = {
        "us-east-1" = {
          traffic_dial_percentage = 100
          health_check_protocol   = "HTTPS"
          health_check_path       = "/healthz"
          health_check_port       = 443
          threshold_count         = 3
          endpoints = [
            {
              endpoint_id                    = aws_lb.api_use1.arn
              weight                         = 128
              client_ip_preservation_enabled = true
            }
          ]
        }
        "eu-west-1" = {
          traffic_dial_percentage = 100
          health_check_protocol   = "HTTPS"
          health_check_path       = "/healthz"
          health_check_port       = 443
          threshold_count         = 3
          endpoints = [
            {
              endpoint_id                    = aws_lb.api_euw1.arn
              weight                         = 128
              client_ip_preservation_enabled = true
            }
          ]
        }
      }
    }
  }

  tags = {
    Environment = "prod"
    Team        = "platform"
    CostCenter  = "networking"
  }
}

# Downstream reference: CNAME the public domain at the accelerator's DNS name,
# and surface the static IPs for the security team's firewall allow-list.
resource "aws_route53_record" "api" {
  zone_id = data.aws_route53_zone.public.zone_id
  name    = "api.kloudvin.com"
  type    = "A"

  alias {
    name                   = module.global_accelerator.dns_name
    zone_id                = module.global_accelerator.hosted_zone_id
    evaluate_target_health = true
  }
}

output "allowlist_ips" {
  description = "Hand these two stable IPs to partners and on-prem firewalls."
  value       = module.global_accelerator.static_ip_addresses
}

With Terragrunt

Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.

1. Root configlive/terragrunt.hcl (inherited by every module):

remote_state {
  backend = "s3"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    # ...s3 state bucket/container + key per path...
  }
}

2. Module configlive/prod/global_accelerator/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-global-accelerator?ref=v1.0.0"
}

inputs = {
  name = "..."
  listeners = {}
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/global_accelerator && terragrunt apply        # this module
terragrunt run-all apply                      # every module under live/prod

Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.

Inputs

Name Type Default Required Description
name string Yes Accelerator name; also used as the Name tag. 1–64 chars, alphanumeric and hyphens.
enabled bool true No Whether the accelerator advertises its IPs. Disabling retains the IPs but stops routing.
ip_address_type string "IPV4" No Address family: IPV4 or DUAL_STACK.
ip_addresses list(string) [] No BYOIP static IPs. Empty for AWS-assigned, or exactly 2 addresses from your pool.
flow_logs_enabled bool false No Enable flow-log delivery to S3 via the attributes block.
flow_logs_s3_bucket string null No Destination S3 bucket for flow logs. Required when flow_logs_enabled = true.
flow_logs_s3_prefix string "global-accelerator/" No S3 key prefix for flow logs.
listeners map(object) Yes Map of listeners; each defines protocol, client affinity, port ranges, and per-region endpoint groups (health checks, traffic dial, endpoints, port overrides).
tags map(string) {} No Tags applied to the accelerator.

Outputs

Name Description
id The ARN of the accelerator (its Terraform ID).
arn The ARN of the accelerator.
name The accelerator’s name.
dns_name The *.awsglobalaccelerator.com DNS name resolving to the static anycast IPs.
hosted_zone_id Global Accelerator hosted zone ID, for Route 53 alias records.
static_ip_addresses The two anycast static IPv4 addresses — use for firewall allow-lists.
listener_arns Map of listener friendly-name to listener ARN.
endpoint_group_arns Map of listener/region key to endpoint group ARN.

Enterprise scenario

A global payments platform runs its transaction API behind ALBs in us-east-1 and eu-west-1. Their banking partners enforce strict IP allow-lists, so DNS-based failover is a non-starter — the partners need fixed IPs that never change. The team deploys this module with SOURCE_IP client affinity (so a payment session sticks to one region), client_ip_preservation_enabled = true (so the ALB and WAF see the real cardholder IP for fraud scoring), and HTTPS /healthz health checks with threshold_count = 3. If us-east-1 degrades, Global Accelerator drains traffic to eu-west-1 within seconds — and the two static IPs the partners allow-listed never move.

Best practices

TerraformAWSGlobal AcceleratorModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading