We are using the module:
aws-ia/terraform-aws-vpc (version >= 4.4.4)
While working with this module, we observed that the output:
private_subnet_attributes_by_az
includes all non-public subnets, such as:
- Private subnets (NAT-connected)
- Isolated subnets (no outbound access)
This behavior is technically correct from an AWS networking perspective (public vs non-public). However, the output name is misleading. Many users naturally assume that private_subnet_attributes_by_az represents only NAT-enabled private subnets suitable for compute workloads.
In our environment, this misunderstanding caused AWS Lambda functions to be deployed into isolated subnets. Since isolated subnets have no outbound connectivity, Lambda was unable to reach AWS services (e.g., DynamoDB, STS, CloudWatch), resulting in timeouts and API Gateway 504 errors.
Actual Behavior
private_subnet_attributes_by_azaggregates all non-public subnets without distinguishing:- NAT-enabled private subnets
- Isolated / non-routable subnets
- Consumers must manually filter this output to safely deploy compute resources.
My Terraform Scripts:
VPC.tf
module “main_vpc” {
source = “aws-ia/vpc/aws”
version = “>= 4.4.4”name = “{var.organization}-{var.environment}-vpc”
cidr_block = var.cidr_block
az_count = 3Enable NAT gateway in all AZs for prod, single AZ for non-prod
subnets = {
# Public subnet with IGW
public = {
netmask = 24
# Deploy NAT gateway in all AZs for prod, single AZ for non-prod
nat_gateway_configuration = var.is_environment_prod ? “all_azs” : “single_az”
}# Private subnet with NAT gateway for outbound private = { netmask = 24 connect_to_public_natgw = true }Isolated private subnet with no internet connectivity
isolated = { netmask = 24 }}
}
OUTPUT.tf
output “vpc_id” {
value = module.main_vpc.vpc_attributes.id
description = “VPC ID”
}output “vpc_arn” {
value = module.main_vpc.vpc_attributes.arn
description = “VPC ARN”
}output “private_subnet_ids” {
description = “List of private subnet IDs (with NAT gateway).”
value = [for _, value in module.main_vpc.private_subnet_attributes_by_az : value.id]
}
user-service.tf
data “aws_s3_object” “user_service” {
bucket = var.artifacts_bucket_name
key = “apps/user-service/latest/index.js.zip”
}resource “aws_lambda_function” “user_service” {
function_name = “user-service”
role = aws_iam_role.lambda_primary_execution_role.arn
handler = “index.handler”
runtime = “nodejs24.x”
memory_size = 512
timeout = 60
publish = trues3_bucket = var.artifacts_bucket_name
s3_key = “apps/user-service/latest/index.js.zip”
source_code_hash = data.aws_s3_object.user_service.etagvpc_config {
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.lambda_primary_sg.id]
}logging_config {
log_format = “JSON”
application_log_level = “TRACE”
system_log_level = “DEBUG”
}environment {
variables = {
NODE_ENV = “production”
ENVIRONMENT = var.environment
LOG_LEVEL = “INFO”
}
}
}locals {
user_service_endpoints = {
“users_get” = {
method = “GET”
path = “/v1/users”
}
“users_post” = {
method = “POST”
path = “/v1/users”
}
“users_id_get” = {
method = “GET”
path = “/v1/users/{userId}”
}
“users_id_patch” = {
method = “PATCH”
path = “/v1/users/{userId}”
}
“users_id_delete” = {
method = “DELETE”
path = “/v1/users/{userId}”
}
}
}
Expected Behavior
One of the following improvements would help prevent this confusion:
Option A
-
Provide a dedicated output for NAT-enabled private subnets, e.g.:
nat_private_subnet_attributes_by_az
Option B
- Clarify documentation that
private_subnet_attributes_by_azmeans all non-public subnets, not necessarily NAT-connected.
Option C
- Add metadata or flags to distinguish isolated vs NAT-connected subnets more explicitly in outputs.
