From d24a5bb460216dee94e1ac39956a8dbef6798279 Mon Sep 17 00:00:00 2001 From: Tomonori Shimomura Date: Wed, 29 Jul 2026 12:23:55 -0700 Subject: [PATCH 1/3] Updating the Terraform files for HyperPod EKS to support LocalZone deployment --- .../terraform-modules/hyperpod-eks-tf/main.tf | 20 +++++++++---- .../modules/private_subnet/main.tf | 15 ++++++++-- .../modules/private_subnet/variables.tf | 17 +++++++++++ .../hyperpod-eks-tf/variables.tf | 30 +++++++++++++++++++ 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/main.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/main.tf index 7e99fb5e8..a92ce75c1 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/main.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/main.tf @@ -68,6 +68,13 @@ locals { create_hyperpod_training_operator_module = !local.rig_mode && var.create_hyperpod_training_operator_module create_observability_module = !local.rig_mode && var.create_observability_module create_hyperpod_inference_operator_module = !local.rig_mode && var.create_hyperpod_inference_operator_module + + # FSx subnet: by default the first instance group's subnet (primary_subnet_id). + # If fsx_availability_zone_id is set (e.g. the instance group is in a Local Zone + # where FSx is not offered), place FSx in the private subnet matching that AZ ID. + fsx_subnet_id = local.create_fsx_module ? ( + var.fsx_availability_zone_id != "" ? local.az_to_subnet_map[var.fsx_availability_zone_id] : module.hyperpod_cluster[0].primary_subnet_id + ) : null } module "vpc" { @@ -85,11 +92,12 @@ module "private_subnet" { count = var.create_private_subnet_module ? 1 : 0 source = "./modules/private_subnet" - resource_name_prefix = var.resource_name_prefix - vpc_id = local.vpc_id - private_subnet_cidrs = var.private_subnet_cidrs - nat_gateway_id = local.nat_gateway_id - closed_network = var.closed_network + resource_name_prefix = var.resource_name_prefix + vpc_id = local.vpc_id + private_subnet_cidrs = var.private_subnet_cidrs + availability_zone_ids = var.private_subnet_availability_zone_ids + nat_gateway_id = local.nat_gateway_id + closed_network = var.closed_network } module "security_group" { @@ -282,7 +290,7 @@ module "fsx_lustre" { resource_name_prefix = var.resource_name_prefix eks_cluster_name = local.eks_cluster_name - subnet_id = module.hyperpod_cluster[0].primary_subnet_id + subnet_id = local.fsx_subnet_id security_group_id = local.security_group_id create_new_filesystem = var.create_new_fsx_filesystem storage_capacity = var.fsx_storage_capacity diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/main.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/main.tf index e2a1f66d6..dd46b0240 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/main.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/main.tf @@ -6,9 +6,18 @@ data "aws_availability_zones" "available" { } } -# Set subnet count to the lesser of either the number of CIDRs provided or number of AZs available in the region +# Subnet placement: +# - By default, subnets are spread across discovered standard AZs (opt-in-not-required), +# with count = min(#CIDRs, #AZs). +# - When availability_zone_ids is set, subnets are placed in those exact AZ IDs +# (1:1 with private_subnet_cidrs). This allows opt-in zones such as Local Zones, +# which the discovery filter deliberately excludes. locals { - subnet_count = min(length(var.private_subnet_cidrs), length(data.aws_availability_zones.available.names)) + use_explicit_azs = length(var.availability_zone_ids) > 0 + subnet_count = local.use_explicit_azs ? length(var.private_subnet_cidrs) : min( + length(var.private_subnet_cidrs), length(data.aws_availability_zones.available.names) + ) + subnet_zone_ids = local.use_explicit_azs ? var.availability_zone_ids : data.aws_availability_zones.available.zone_ids } resource "aws_vpc_ipv4_cidr_block_association" "additional_cidr" { @@ -21,7 +30,7 @@ resource "aws_subnet" "private" { count = local.subnet_count vpc_id = var.vpc_id cidr_block = var.private_subnet_cidrs[count.index] - availability_zone_id = data.aws_availability_zones.available.zone_ids[count.index] + availability_zone_id = local.subnet_zone_ids[count.index] # Ensure the subnet is created after the CIDR block is associated depends_on = [aws_vpc_ipv4_cidr_block_association.additional_cidr] diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/variables.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/variables.tf index fcd760215..236c536a7 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/variables.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/variables.tf @@ -14,6 +14,23 @@ variable "private_subnet_cidrs" { default = ["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16", "10.4.0.0/16"] } +variable "availability_zone_ids" { + description = <<-EOT + Optional list of Availability Zone IDs to place subnets in, 1:1 with + private_subnet_cidrs. When empty (default), AZs are discovered automatically + (standard opt-in-not-required zones only). Set this to target specific zones, + including opt-in zones such as Local Zones (e.g. usw2-phx2-az1) that the + discovery filter excludes. + EOT + type = list(string) + default = [] + + validation { + condition = length(var.availability_zone_ids) == 0 || length(var.availability_zone_ids) == length(var.private_subnet_cidrs) + error_message = "When set, availability_zone_ids must have the same length as private_subnet_cidrs." + } +} + variable "nat_gateway_id" { description = "The Id of a NAT Gateway to route internet bound traffic (ignored if closed_network is true)" type = string diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/variables.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/variables.tf index b5cd4bde3..e67a1fe81 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/variables.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/variables.tf @@ -60,6 +60,23 @@ variable "private_subnet_cidrs" { default = ["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16", "10.4.0.0/16"] } +variable "private_subnet_availability_zone_ids" { + description = <<-EOT + Optional list of Availability Zone IDs for the private subnets, 1:1 with + private_subnet_cidrs. When empty (default), AZs are discovered automatically + (standard opt-in-not-required zones only). Set this to target specific zones, + including opt-in zones such as Local Zones (e.g. usw2-phx2-az1) that the + discovery filter excludes. + EOT + type = list(string) + default = [] + + validation { + condition = length(var.private_subnet_availability_zone_ids) == 0 || length(var.private_subnet_availability_zone_ids) == length(var.private_subnet_cidrs) + error_message = "When set, private_subnet_availability_zone_ids must have the same length as private_subnet_cidrs." + } +} + variable "existing_nat_gateway_id" { description = "The ID of an existing NAT Gateway" type = string @@ -514,6 +531,19 @@ variable "create_new_fsx_filesystem" { default = false } +variable "fsx_availability_zone_id" { + description = <<-EOT + Optional Availability Zone ID for the FSx for Lustre file system. When empty + (default), FSx is placed in the first HyperPod instance group's subnet. Set + this when that subnet is in an Availability Zone where FSx is not offered + (e.g. a Local Zone): FSx is then placed in the private subnet matching this + AZ ID instead, and mounted cross-zone. Must be one of the AZ IDs backing the + private subnets. + EOT + type = string + default = "" +} + variable "fsx_storage_capacity" { description = "Storage capacity for FSx filesystem in GiB" type = number From 3abeae2a087a87b4537617229ef672696fa734b7 Mon Sep 17 00:00:00 2001 From: aravneelaws Date: Mon, 3 Aug 2026 15:37:36 -0500 Subject: [PATCH 2/3] vpc: optional per-Local-Zone NAT gateway with border-group EIP Add three optional inputs to the vpc module for creating LZ-local NAT gateways. When set, private subnets in matching zones use the LZ NAT instead of the regional NAT via a new nat_gateway_ids_by_zone_id map piped through the private_subnet module. Empty inputs (the default) preserve existing behavior. New root/vpc-module inputs (defaults empty): - local_zone_egress_zone_ids list(string) - local_zone_public_subnet_cidrs list(string) (1:1 with above) - local_zone_network_border_groups list(string) (1:1 with above) New vpc-module resources (per listed zone, count-gated): - aws_subnet.lz_public - aws_eip.lz_nat (with network_border_group set) - aws_nat_gateway.lz_nat - aws_route_table.lz_public + IGW route + association New vpc-module output: nat_gateway_ids_by_zone_id (map of LZ AZ ID -> LZ NAT gateway ID) private_subnet module gains nat_gateway_ids_by_zone_id input (default {}); aws_route.nat_gateway uses lookup(map, subnet_az_id, var.nat_gateway_id) so unmapped AZs keep the regional NAT. Root main.tf pipes the map to module.private_subnet only. Deliberately NOT to module.eks_cluster - EKS control-plane subnets cannot live in a Local Zone and keep using the regional NAT. NetworkBorderGroup is a required input (not derived) because reliable suffix-strip on multi-letter zone names ("us-west-2-lax-1a") is awkward in HCL. The border group is the zone name minus the trailing zone letter (us-west-2-phx-2a -> us-west-2-phx-2). Without an LZ-local NAT, Local Zone egress hairpins through the parent Region, adding latency. Placing a NAT gateway in the Local Zone keeps egress in-zone and improves first-hop latency and internet throughput for Local Zone workers; origin-anchored services dominated by CDN routing benefit less. Does not change DNS latency, FSx cross-zone behavior, or EKS API latency. Fully backward compatible: existing tfvars files that never mention these inputs produce the identical terraform plan as before. --- .../terraform-modules/hyperpod-eks-tf/main.tf | 53 ++++++----- .../modules/private_subnet/main.tf | 11 ++- .../modules/private_subnet/variables.tf | 12 +++ .../hyperpod-eks-tf/modules/vpc/main.tf | 90 +++++++++++++++++++ .../hyperpod-eks-tf/modules/vpc/outputs.tf | 15 ++++ .../hyperpod-eks-tf/modules/vpc/variables.tf | 51 +++++++++++ .../hyperpod-eks-tf/variables.tf | 63 +++++++++++++ 7 files changed, 270 insertions(+), 25 deletions(-) diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/main.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/main.tf index a92ce75c1..bdc86ccae 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/main.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/main.tf @@ -29,18 +29,21 @@ locals { ] is_amp_allowed = contains(local.amp_allowed_regions, var.aws_region) - vpc_id = var.create_vpc_module ? module.vpc[0].vpc_id : var.existing_vpc_id - private_subnet_ids = var.create_private_subnet_module ? module.private_subnet[0].private_subnet_ids : var.existing_private_subnet_ids - security_group_id = var.create_security_group_module ? module.security_group[0].security_group_id : var.existing_security_group_id - eks_cluster_name = var.create_eks_module ? module.eks_cluster[0].eks_cluster_name : var.existing_eks_cluster_name - eks_cluster_arn = var.create_eks_module ? module.eks_cluster[0].eks_cluster_arn : data.aws_eks_cluster.existing_eks_cluster[0].arn - sagemaker_iam_role_name = var.create_sagemaker_iam_role_module ? module.sagemaker_iam_role[0].sagemaker_iam_role_name : var.existing_sagemaker_iam_role_name - create_hyperpod_module = var.create_hyperpod_module && !(var.create_eks_module && !var.create_helm_chart_module) - karpenter_role_arn = var.create_sagemaker_iam_role_module && length(module.sagemaker_iam_role[0].karpenter_role_arn) > 0 ? module.sagemaker_iam_role[0].karpenter_role_arn[0] : null - nat_gateway_id = var.create_vpc_module ? module.vpc[0].nat_gateway_1_id : var.existing_nat_gateway_id - private_route_table_ids = var.create_private_subnet_module ? module.private_subnet[0].private_route_table_ids : var.existing_private_route_table_ids - eks_private_subnet_cidrs = [var.eks_private_subnet_1_cidr, var.eks_private_subnet_2_cidr] - enable_guardduty_cleanup = var.enable_guardduty_cleanup && (var.create_vpc_module || var.create_private_subnet_module || var.create_eks_module) + vpc_id = var.create_vpc_module ? module.vpc[0].vpc_id : var.existing_vpc_id + private_subnet_ids = var.create_private_subnet_module ? module.private_subnet[0].private_subnet_ids : var.existing_private_subnet_ids + security_group_id = var.create_security_group_module ? module.security_group[0].security_group_id : var.existing_security_group_id + eks_cluster_name = var.create_eks_module ? module.eks_cluster[0].eks_cluster_name : var.existing_eks_cluster_name + eks_cluster_arn = var.create_eks_module ? module.eks_cluster[0].eks_cluster_arn : data.aws_eks_cluster.existing_eks_cluster[0].arn + sagemaker_iam_role_name = var.create_sagemaker_iam_role_module ? module.sagemaker_iam_role[0].sagemaker_iam_role_name : var.existing_sagemaker_iam_role_name + create_hyperpod_module = var.create_hyperpod_module && !(var.create_eks_module && !var.create_helm_chart_module) + karpenter_role_arn = var.create_sagemaker_iam_role_module && length(module.sagemaker_iam_role[0].karpenter_role_arn) > 0 ? module.sagemaker_iam_role[0].karpenter_role_arn[0] : null + nat_gateway_id = var.create_vpc_module ? module.vpc[0].nat_gateway_1_id : var.existing_nat_gateway_id + # Per-AZ NAT map: only populated when this module manages the VPC. When + # BYO-VPC, callers currently have to set up LZ-local NATs themselves. + nat_gateway_ids_by_zone_id = var.create_vpc_module ? module.vpc[0].nat_gateway_ids_by_zone_id : {} + private_route_table_ids = var.create_private_subnet_module ? module.private_subnet[0].private_route_table_ids : var.existing_private_route_table_ids + eks_private_subnet_cidrs = [var.eks_private_subnet_1_cidr, var.eks_private_subnet_2_cidr] + enable_guardduty_cleanup = var.enable_guardduty_cleanup && (var.create_vpc_module || var.create_private_subnet_module || var.create_eks_module) # Cilium CNI skip_vpc_cni = var.enable_cilium && var.cilium_mode != "chaining" @@ -81,23 +84,27 @@ module "vpc" { count = var.create_vpc_module ? 1 : 0 source = "./modules/vpc" - resource_name_prefix = var.resource_name_prefix - vpc_cidr = var.vpc_cidr - public_subnet_1_cidr = var.public_subnet_1_cidr - public_subnet_2_cidr = var.public_subnet_2_cidr - closed_network = var.closed_network + resource_name_prefix = var.resource_name_prefix + vpc_cidr = var.vpc_cidr + public_subnet_1_cidr = var.public_subnet_1_cidr + public_subnet_2_cidr = var.public_subnet_2_cidr + closed_network = var.closed_network + local_zone_egress_zone_ids = var.local_zone_egress_zone_ids + local_zone_public_subnet_cidrs = var.local_zone_public_subnet_cidrs + local_zone_network_border_groups = var.local_zone_network_border_groups } module "private_subnet" { count = var.create_private_subnet_module ? 1 : 0 source = "./modules/private_subnet" - resource_name_prefix = var.resource_name_prefix - vpc_id = local.vpc_id - private_subnet_cidrs = var.private_subnet_cidrs - availability_zone_ids = var.private_subnet_availability_zone_ids - nat_gateway_id = local.nat_gateway_id - closed_network = var.closed_network + resource_name_prefix = var.resource_name_prefix + vpc_id = local.vpc_id + private_subnet_cidrs = var.private_subnet_cidrs + availability_zone_ids = var.private_subnet_availability_zone_ids + nat_gateway_id = local.nat_gateway_id + nat_gateway_ids_by_zone_id = local.nat_gateway_ids_by_zone_id + closed_network = var.closed_network } module "security_group" { diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/main.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/main.tf index dd46b0240..a4bc10a61 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/main.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/main.tf @@ -61,10 +61,17 @@ resource "aws_route_table_association" "private" { route_table_id = aws_route_table.private[count.index].id } -# NAT Gateway route - only created if NOT closed network +# NAT Gateway route - only created if NOT closed network. +# Per-AZ NAT selection: if this subnet's AZ ID has an entry in +# nat_gateway_ids_by_zone_id (used for Local Zone-local NATs), use it; +# otherwise fall back to the regional var.nat_gateway_id. resource "aws_route" "nat_gateway" { count = var.closed_network ? 0 : local.subnet_count route_table_id = aws_route_table.private[count.index].id destination_cidr_block = "0.0.0.0/0" - nat_gateway_id = var.nat_gateway_id + nat_gateway_id = lookup( + var.nat_gateway_ids_by_zone_id, + local.subnet_zone_ids[count.index], + var.nat_gateway_id + ) } diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/variables.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/variables.tf index 236c536a7..5e7ba4975 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/variables.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/private_subnet/variables.tf @@ -36,6 +36,18 @@ variable "nat_gateway_id" { type = string } +variable "nat_gateway_ids_by_zone_id" { + description = <<-EOT + Optional per-AZ-ID NAT gateway override. When a private subnet's AZ ID is + a key in this map, its default route uses the mapped NAT gateway instead + of var.nat_gateway_id. Used to route Local Zone subnets to an LZ-local + NAT gateway. Falls back to var.nat_gateway_id for unmapped AZs. + Default: empty map (all subnets use var.nat_gateway_id). + EOT + type = map(string) + default = {} +} + variable "closed_network" { description = "Whether to deploy in closed network mode (no NAT gateway routes)" type = bool diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/main.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/main.tf index a7970ceac..9499cc224 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/main.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/main.tf @@ -123,3 +123,93 @@ resource "aws_route_table_association" "public_2" { route_table_id = aws_route_table.public[0].id subnet_id = aws_subnet.public_2[0].id } + +# ============================================================================ +# Local Zone egress: LZ-local NAT gateway(s) +# ============================================================================ +# When local_zone_egress_zone_ids is set, we create one LZ public subnet, one +# border-group-scoped EIP, and one NAT gateway per LZ. The corresponding +# private-subnet module then routes 0.0.0.0/0 to its LZ NAT instead of the +# regional NAT (which lives in a standard AZ and hairpins packets 24-35 ms +# across the region link). +# +# All resources are gated on `!closed_network AND length(...) > 0` so this +# is a strictly additive, opt-in feature. + +locals { + lz_egress_enabled = !var.closed_network && length(var.local_zone_egress_zone_ids) > 0 + lz_egress_count = local.lz_egress_enabled ? length(var.local_zone_egress_zone_ids) : 0 +} + +resource "aws_subnet" "lz_public" { + count = local.lz_egress_count + vpc_id = aws_vpc.main.id + cidr_block = var.local_zone_public_subnet_cidrs[count.index] + availability_zone_id = var.local_zone_egress_zone_ids[count.index] + map_public_ip_on_launch = true + + tags = merge( + { + Name = "${var.resource_name_prefix}-SMHP-LZ-Public-${count.index + 1}" + }, + var.tags + ) +} + +# NetworkBorderGroup on the EIP is REQUIRED - a region-scoped EIP cannot +# attach to a NAT gateway in an LZ subnet. AWS will reject the association +# with "EIP is not associated with the border group of the subnet". +resource "aws_eip" "lz_nat" { + count = local.lz_egress_count + domain = "vpc" + network_border_group = var.local_zone_network_border_groups[count.index] + + depends_on = [aws_internet_gateway.main] + + tags = merge( + { + Name = "${var.resource_name_prefix}-SMHP-LZ-NAT-EIP-${count.index + 1}" + }, + var.tags + ) +} + +resource "aws_nat_gateway" "lz_nat" { + count = local.lz_egress_count + allocation_id = aws_eip.lz_nat[count.index].id + subnet_id = aws_subnet.lz_public[count.index].id + + depends_on = [aws_internet_gateway.main] + + tags = merge( + { + Name = "${var.resource_name_prefix}-SMHP-LZ-NAT-${count.index + 1}" + }, + var.tags + ) +} + +# LZ public subnets share their own route table so they don't drag the +# regional public subnets around. AWS handles the LZ<->IGW path transparently. +resource "aws_route_table" "lz_public" { + count = local.lz_egress_count + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main[0].id + } + + tags = merge( + { + Name = "${var.resource_name_prefix}-SMHP-LZ-Public-Routes-${count.index + 1}" + }, + var.tags + ) +} + +resource "aws_route_table_association" "lz_public" { + count = local.lz_egress_count + route_table_id = aws_route_table.lz_public[count.index].id + subnet_id = aws_subnet.lz_public[count.index].id +} diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/outputs.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/outputs.tf index 4bd242523..710bf5343 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/outputs.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/outputs.tf @@ -42,3 +42,18 @@ output "availability_zones" { description = "List of availability zones used in the VPC" value = var.closed_network ? [] : [aws_subnet.public_1[0].availability_zone, aws_subnet.public_2[0].availability_zone] } + +output "nat_gateway_ids_by_zone_id" { + description = <<-EOT + Map of LZ AZ ID -> LZ NAT gateway ID. Consumed by the private_subnet + module: when a private subnet is placed in one of these zones, its + default route uses this NAT instead of the regional NAT. + Empty map when no LZ egress NATs are configured. + EOT + value = zipmap(var.local_zone_egress_zone_ids, aws_nat_gateway.lz_nat[*].id) +} + +output "lz_nat_eips_by_zone_id" { + description = "Map of LZ AZ ID -> LZ NAT public IP. Empty when no LZ egress NATs are configured." + value = zipmap(var.local_zone_egress_zone_ids, aws_eip.lz_nat[*].public_ip) +} diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/variables.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/variables.tf index 5fdda6667..885432678 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/variables.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/modules/vpc/variables.tf @@ -27,6 +27,57 @@ variable "closed_network" { default = false } +variable "local_zone_egress_zone_ids" { + description = <<-EOT + Optional list of Local Zone AZ IDs (e.g. usw2-phx2-az1) in which to create + an LZ-local NAT gateway. When empty (default), no LZ-local NATs are created + and any private subnet in those LZs will fall back to the regional NAT + (which hairpins through the parent AZ - measured 24-35 ms per packet). + + When set, one LZ public subnet, one border-group-scoped EIP, and one NAT + gateway are created per zone ID. The lists local_zone_public_subnet_cidrs + and local_zone_network_border_groups must be 1:1 with this list. + + Consumed by the private_subnet module via the vpc module's + nat_gateway_ids_by_zone_id output. + EOT + type = list(string) + default = [] +} + +variable "local_zone_public_subnet_cidrs" { + description = <<-EOT + LZ public subnet CIDRs, 1:1 with local_zone_egress_zone_ids. Each CIDR + must fit within one of the VPC's CIDR blocks (typically the primary CIDR + since secondary CIDRs are usually consumed by the private worker subnet). + EOT + type = list(string) + default = [] + + validation { + condition = length(var.local_zone_public_subnet_cidrs) == length(var.local_zone_egress_zone_ids) + error_message = "local_zone_public_subnet_cidrs must have the same length as local_zone_egress_zone_ids." + } +} + +variable "local_zone_network_border_groups" { + description = <<-EOT + NetworkBorderGroup names for the LZ NAT EIPs, 1:1 with + local_zone_egress_zone_ids. Required: a plain vpc-scoped EIP cannot attach + to a NAT gateway in an LZ subnet. The border-group name is the LZ zone + name minus the trailing zone letter, e.g. us-west-2-phx-2a -> us-west-2-phx-2, + us-west-2-lax-1a -> us-west-2-lax-1. Passed explicitly (rather than derived) + because reliable suffix-strip on multi-letter zone names is awkward in HCL. + EOT + type = list(string) + default = [] + + validation { + condition = length(var.local_zone_network_border_groups) == length(var.local_zone_egress_zone_ids) + error_message = "local_zone_network_border_groups must have the same length as local_zone_egress_zone_ids." + } +} + variable "tags" { description = "Additional tags for all resources" type = map(string) diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/variables.tf b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/variables.tf index e67a1fe81..952ef4201 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/variables.tf +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/hyperpod-eks-tf/variables.tf @@ -77,6 +77,69 @@ variable "private_subnet_availability_zone_ids" { } } +# ============================================================================ +# Local Zone egress inputs (backward-compatible: default empty = disabled) +# ============================================================================ +# By default, private subnets in a Local Zone route 0.0.0.0/0 to the regional +# NAT gateway (created in a standard-AZ public subnet). Every packet then +# hairpins to the parent Region, adding 24-35 ms per RTT. +# +# Setting local_zone_egress_zone_ids (with matching public subnet CIDRs and +# network border groups) creates one LZ-local NAT gateway per listed zone. +# The private_subnet module routes matching subnets to their LZ NAT via the +# nat_gateway_ids_by_zone_id map output by the vpc module. +# +# Only the private_subnet module receives the map. The eks_cluster module +# keeps using the regional NAT because EKS control-plane subnets cannot live +# in a Local Zone. +# +# Measured impact (LAX A/B, c5.large, 2026-08-03): +# - Traceroute hop 1: 23.8 ms -> 0.095 ms (250x) +# - PyPI download total: 797 ms -> 143 ms (5.6x) +# - Cloudflare 25 MB total: 565 ms -> 132 ms (4.3x) +variable "local_zone_egress_zone_ids" { + description = <<-EOT + Optional list of Local Zone AZ IDs (e.g. usw2-phx2-az1) that should get + an LZ-local NAT gateway. Each entry must be matched 1:1 in + local_zone_public_subnet_cidrs and local_zone_network_border_groups. + When empty (default), all private subnets fall back to the regional NAT. + EOT + type = list(string) + default = [] +} + +variable "local_zone_public_subnet_cidrs" { + description = <<-EOT + LZ public subnet CIDRs, 1:1 with local_zone_egress_zone_ids. Typically + carved from the VPC primary CIDR (secondary CIDRs are usually fully + consumed by the private worker subnet). + EOT + type = list(string) + default = [] + + validation { + condition = length(var.local_zone_public_subnet_cidrs) == length(var.local_zone_egress_zone_ids) + error_message = "local_zone_public_subnet_cidrs must have the same length as local_zone_egress_zone_ids." + } +} + +variable "local_zone_network_border_groups" { + description = <<-EOT + NetworkBorderGroup names for the LZ NAT EIPs, 1:1 with + local_zone_egress_zone_ids. Required: a plain vpc-scoped EIP cannot + attach to a NAT in an LZ subnet. The border group is the LZ zone name + minus the trailing zone letter (us-west-2-phx-2a -> us-west-2-phx-2, + us-west-2-lax-1a -> us-west-2-lax-1). + EOT + type = list(string) + default = [] + + validation { + condition = length(var.local_zone_network_border_groups) == length(var.local_zone_egress_zone_ids) + error_message = "local_zone_network_border_groups must have the same length as local_zone_egress_zone_ids." + } +} + variable "existing_nat_gateway_id" { description = "The ID of an existing NAT Gateway" type = string From f40f327517885cd8aac05458315fc3d20cc10395 Mon Sep 17 00:00:00 2001 From: Aravind Neelakantan Date: Tue, 11 Aug 2026 18:51:36 -0500 Subject: [PATCH 3/3] docs(hyperpod-eks): document Local Zone deployment for Terraform modules --- .../terraform-modules/README.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/README.md b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/README.md index c87a47733..40bce3d2d 100644 --- a/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/README.md +++ b/1.architectures/7.sagemaker-hyperpod-eks/terraform-modules/README.md @@ -491,6 +491,105 @@ After deployment, verify connectivity to AWS services: python3 tools/verify-aws-connectivity.py ``` +--- + +### Local Zone Deployment + +You can place HyperPod worker instance groups in an [AWS Local Zone](https://docs.aws.amazon.com/local-zones/latest/ug/what-is-aws-local-zones.html) to run compute closer to a specific metro area. The EKS control plane stays in the parent Region: EKS cannot create control-plane ENIs in a Local Zone, so only the HyperPod worker subnet lives in the Local Zone while the control-plane subnets remain in standard parent Availability Zones. + +Local Zone support is opt-in and additive. All of the variables below default to standard-AZ behavior when unset, so existing deployments are unaffected. + +> **Note:** Not every Local Zone is supported by HyperPod, and there is no API to enumerate the supported zones. Confirm your target zone with AWS before deploying. The instance type must also be both offered in the Local Zone and present in HyperPod's `ClusterInstanceType` enum. + +#### Local Zone Variables + +| Variable | Usage | +|----------|-------| +| `private_subnet_availability_zone_ids` | Pins the HyperPod private (worker) subnets to explicit Availability Zone IDs, 1:1 with `private_subnet_cidrs`. This bypasses the `opt-in-status = "opt-in-not-required"` AZ-discovery filter, which excludes opt-in Local Zones. Default `[]` = discover standard AZs automatically. | +| `local_zone_egress_zone_ids` | List of Local Zone AZ IDs that should get a Local-Zone-local NAT gateway. Default `[]` = worker subnets route through the regional NAT gateway. | +| `local_zone_public_subnet_cidrs` | Local Zone public subnet CIDRs, 1:1 with `local_zone_egress_zone_ids`. Typically carved from the VPC primary CIDR (secondary CIDRs are usually consumed by the worker subnet). | +| `local_zone_network_border_groups` | `NetworkBorderGroup` names for the Local Zone NAT Elastic IPs, 1:1 with `local_zone_egress_zone_ids`. Required: a plain VPC-scoped EIP cannot attach to a NAT gateway in a Local Zone subnet. The border group is the Local Zone name minus the trailing zone letter (e.g. `us-west-2-phx-2a` -> `us-west-2-phx-2`). | + +#### Local Zone egress (NAT placement) + +By default the `vpc` module creates a single regional NAT gateway in a standard-AZ public subnet. A worker subnet in a Local Zone routes `0.0.0.0/0` to that regional NAT, so egress traffic hairpins back to the parent Region and pays an added round trip per packet. + +Setting the three `local_zone_*` variables creates one Local-Zone-local NAT gateway per listed zone (with a border-group-scoped EIP) and routes matching worker subnets to it via the `vpc` module's `nat_gateway_ids_by_zone_id` output. Keeping egress in-zone significantly improves first-hop latency and internet throughput for Local Zone workers. Unmapped AZs continue to use the regional NAT. This has been validated with an end-to-end Local Zone HyperPod deployment. + +#### Example `custom.tfvars` + +```hcl +resource_name_prefix = "hp-eks" +aws_region = "us-west-2" + +# VPC +create_vpc_module = true +vpc_cidr = "10.192.0.0/16" +public_subnet_1_cidr = "10.192.10.0/24" +public_subnet_2_cidr = "10.192.11.0/24" + +# Private (worker) subnet pinned to the Local Zone AZ ID, 1:1 with the CIDR. +# This bypasses the opt-in-not-required discovery filter that excludes Local Zones. +create_private_subnet_module = true +private_subnet_cidrs = ["10.1.0.0/16"] +private_subnet_availability_zone_ids = ["usw2-phx2-az1"] + +# Optional: Local-Zone-local NAT gateway (all three lists non-empty and 1:1). +# Uncomment to keep worker egress in-zone instead of hairpinning to the Region. +# local_zone_egress_zone_ids = ["usw2-phx2-az1"] +# local_zone_public_subnet_cidrs = ["10.192.20.0/24"] +# local_zone_network_border_groups = ["us-west-2-phx-2"] + +# EKS control-plane subnets stay in parent AZs (cannot live in a Local Zone). +create_eks_module = true +create_eks_subnets = true +eks_private_subnet_1_cidr = "10.192.7.0/28" +eks_private_subnet_2_cidr = "10.192.8.0/28" + +# FSx placement: default co-locates FSx with the instance group's subnet (in-Local-Zone). +create_fsx_module = true +create_new_fsx_filesystem = true +fsx_storage_capacity = 1200 +fsx_throughput = 250 +# fsx_availability_zone_id = "" # set to a parent-AZ ID for a cross-zone mount + # if the Local Zone does not offer FSx (or the tier). + +instance_groups = [ + { + name = "instance-group-1" + instance_type = "ml.c6i.2xlarge" + instance_count = 1 + availability_zone_id = "usw2-phx2-az1" # land workers in the Local Zone + ebs_volume_size_in_gb = 100 + threads_per_core = 2 + enable_stress_check = false + enable_connectivity_check = false + lifecycle_script = "on_create.sh" + } +] +``` + +#### FSx for Lustre in a Local Zone + +FSx placement is already configurable through `fsx_availability_zone_id` (see the [FSx for Lustre Module](#fsx-for-lustre-module) section). When empty (default), FSx is created in the first instance group's subnet, which co-locates it with compute in the Local Zone. FSx for Lustre availability and per-tier support vary by Local Zone; if your target zone does not offer FSx (or the tier you need), set `fsx_availability_zone_id` to a parent-AZ ID for a cross-zone mount, or set `create_new_fsx_filesystem = false`. + +#### Prerequisite: opt in to the Local Zone + +The target Local Zone must be opted in before you deploy (a not-yet-opted-in zone makes the private subnet fail to create): + +```bash +# Look up your Local Zone's AZ ID and parent zone +aws ec2 describe-availability-zones --all-availability-zones \ + --query "AvailabilityZones[?ZoneType=='local-zone'].[ZoneName,ZoneId,ParentZoneName]" \ + --output table + +# Opt in (opt-in is asynchronous - verify it reports opted-in before deploying) +aws ec2 modify-availability-zone-group \ + --group-name us-west-2-phx-2a --opt-in-status opted-in +``` + +For a complete, ready-to-run Local Zone example, see the [HyperPod Local Zone quickstart](https://github.com/aravneelaws/hyperpod-local-zone-quickstart/tree/main/terraform/eks). That repository ships only a `local-zone.tfvars` file and applies it against this reference stack (no forked Terraform), so the variable file lives there while the modules live here. + --- ### Enabling Optional Addons Set the following parameters to `true` in your `custom.tfvars` file to enable optional addons for your HyperPod cluster (e.g. `create_task_governance_module = true`):