Skip to content

private_subnet_attributes_by_az includes isolated subnets – causes compute workloads to deploy into non-routable networks #177

Description

@malyadrireddygft

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_az aggregates 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 = 3

Enable 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 = true

s3_bucket = var.artifacts_bucket_name
s3_key = "apps/user-service/latest/index.js.zip"
source_code_hash = data.aws_s3_object.user_service.etag

vpc_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}"
}
}
}

Image

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_az means 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions