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
- You serve a global user base and want them to hit the nearest AWS edge instead of a single region’s public endpoint — typical for APIs, gaming backends, IoT ingestion, and VoIP.
- You need static entry-point IPs because a partner, on-prem firewall, or regulator demands an IP allow-list that DNS-based routing (Route 53 / CloudFront) cannot satisfy.
- You want fast, automatic multi-region failover measured in seconds, driven by TCP/HTTP health checks rather than DNS TTL propagation.
- You front non-HTTP workloads (raw TCP/UDP, MQTT, game servers) where CloudFront is not an option but you still want edge acceleration.
- Skip it when a single-region web app behind CloudFront already meets your latency SLO — Global Accelerator adds a fixed hourly charge plus a per-GB data-transfer-premium and is wasted on purely regional traffic.
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 config — live/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 config — live/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
- Pin your edge with the static IPs, not the DNS name, when allow-listing. The whole reason to choose Global Accelerator over CloudFront/Route 53 is the stable IP pair from
static_ip_addresses. Document them as long-lived and never let Terraform recreate the accelerator casually — destroying it releases the IPs permanently unless you’re on BYOIP. - Turn on
client_ip_preservation_enabledfor ALB/NLB endpoints so WAF rules, security groups, and access logs see the genuine source IP rather than a Global Accelerator edge IP. It is off by default in this module and silently breaks IP-based access control if forgotten. - Use
traffic_dial_percentagefor controlled regional shifts and blue/green cutovers — dial a region down to 0 to drain it gracefully for maintenance instead of yanking the endpoint, which is far gentler than a hard health-check failure. - Mind the cost model: a fixed hourly accelerator charge plus a per-GB “data-transfer-premium” on top of normal egress. It’s only worth it for genuinely global or static-IP-bound traffic; set
enabled = falseon non-prod accelerators when idle to stop the hourly meter. - Enable flow logs to S3 from day one (
flow_logs_enabled = true) so you can audit source IPs, ports, and edge locations for security investigations and latency analysis — there’s no after-the-fact way to recover traffic you never logged. - Name and tag consistently (
<app>-<env>plusCostCenter/Environmenttags) because Global Accelerator is a global, account-wide resource with no region in its ARN — clear naming is the only way to tell prod from sandbox at a glance.