Terraform aws_subnet Resource Deep Dive for AWS VPC Networking

Public and private subnet separation is the foundation of secure and scalable AWS networking. The aws_subnet resource in Terraform codifies the AWS subnet model described in the VPC user guide, turning declarative configuration into repeatable VPC layouts with public subnets that can reach the internet and private subnets that are isolated behind routing and security controls.

This article examines the aws_subnet resource through the reference implementation patterns in the Terraform AWS provider, the AWS documentation for subnet configuration, and practical Terraform data source patterns for working with existing subnets.

Introduction

A subnet is a range of IP addresses in your VPC. You can create AWS resources, such as EC2 instances, in specific subnets. Each subnet must reside entirely within one Availability Zone and cannot span zones. By launching AWS resources in separate Availability Zones, you can protect your applications from the failure of a single Availability Zone.

The subnet type is determined by how you configure routing for your subnets. Subnet settings include modifiable attributes that determine whether a network interface created in that subnet is assigned a public IPv4 address and, if applicable, an IPv6 address. This includes the primary network interface for example, eth0 that's created for an instance when you launch an instance in that subnet. Regardless of the subnet attribute, you can still override this setting for a specific instance during launch.

After you create a subnet, you can modify the following settings for the subnet:

Auto-assign IP settings: Enables you to configure the auto-assign IP settings to automatically request a public IPv4 or IPv6 address for a new network interface in this subnet.

Resource-based Name settings: Enables you to specify the hostname type for EC2 instances in this subnet and configure how DNS A and AAAA record queries are handled.

Reference Terraform VPC and Subnet Layout

The sample script comprises of multiple components as specified below:

VPC and Subnets:
A VPC is created with a CIDR block of 10.0.0.0/16.
An Internet Gateway is attached to the VPC.
A public subnet 10.0.1.0/24 and a private subnet

The core resources in the reference pattern are:

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

resource "awsvpc" "main" {
cidr
block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}

resource "awsinternetgateway" "gw" {
vpcid = awsvpc.main.id
tags = {
Name = "main-gw"
}
}

resource "awssubnet" "public" {
vpc
id = awsvpc.main.id
cidr
block = "10.0.1.0/24"
mappubliciponlaunch = true
tags = {
Name = "public-subnet"
}
}

resource "awssubnet" "private" {
vpc
id = awsvpc.main.id
cidr
block = "10.0.2.0/24"
tags = {
Name = "private-subnet"
}
}
```

The public subnet is created with map_public_ip_on_launch = true. This attribute enables auto-assign of public IPv4 addresses for new network interfaces in the subnet. The private subnet is created without this flag, keeping instances private by default.

Routing is added via a route table:

```hcl
resource "awsroutetable" "public" {
vpcid = awsvpc.main.id
route {
cidrblock = "0.0.0.0/0"
gateway
id = awsinternetgateway.gw.id
}
tags = {
Name = "public-rt"
}
}

resource "awsroutetableassociation" "a" {
subnet
id = awssubnet.public.id
route
tableid = awsroute_table.public.id
}
```

Every subnet that you create is automatically associated with the main route table for the VPC. You can change the association, and you can change the contents of the main route table.

Subnet IP Address Range Options

When you create a subnet, you specify its IP addresses, depending on the configuration of the VPC:

  • IPv4 only – The subnet has an IPv4 CIDR block but does not have an IPv6 CIDR block. Resources in an IPv4-only subnet must communicate over IPv4.
  • Dual stack – The subnet has both an IPv4 CIDR block and an IPv6 CIDR block. The VPC must have both an IPv4 CIDR block and an IPv6 CIDR block. Resources in a dual-stack subnet can communicate over IPv4 and IPv6.
  • IPv6 only – The subnet has an IPv6 CIDR block but does not have an IPv4 CIDR block. The VPC must have an IPv6 CIDR block. Resources in an IPv6-only subnet must communicate over IPv6.

Note
Resources in IPv6-only subnets are assigned IPv4 link-local addresses from CIDR block 169.254.0.0/16. These addresses are used to communicate with services that are available only in the VPC.

The reference script uses IPv4 only subnets with CIDR blocks 10.0.1.0/24 for public and 10.0.2.0/24 for private within VPC 10.0.0.0/16.

Subnet Types and Routing Behavior

Subnet types
The subnet type is determined by how you configure routing for your subnets

Public subnets provide direct internet access via an Internet Gateway. The reference public subnet is associated with a route table that routes 0.0.0.0/0 to the Internet Gateway. The map_public_ip_on_launch flag ensures launched instances receive public IPs automatically.

Private subnets require indirect internet access. Use a bastion host or NAT device to provide internet access to resources, such as EC2 instances, in a private subnet.

To protect your AWS resources, we recommend that you use private subnets. Use a bastion host or NAT device to provide internet access to resources, such as EC2 instances, in a private subnet.

Security is layered:

AWS provides features that you can use to increase security for the resources in your VPC. Security groups allow inbound and outbound traffic for associated resources, such as EC2 instances. Network ACLs allow or deny inbound and outbound traffic at the subnet level. In most cases, security groups can meet your needs. However, you can use network ACLs if you want an additional layer of security.

By design, each subnet must be associated with a network ACL. Every subnet that you create is automatically associated with the default network ACL for the VPC. The default network ACL allows all inbound and outbound traffic. You can update the default network ACL, or create custom network ACLs and associate them with your subnets.

You can create a flow log on your VPC or subnet to capture the traffic that flows to and from the network interfaces in your VPC or subnet. You can also create a flow log on an individual network interface.

Security Groups Tied to Subnets

The sample script defines security groups for the public and private tier:

hcl resource "aws_security_group" "public_sg" { vpc_id = aws_vpc.main.id ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "public-sg" } }

Private tier access is restricted to the public subnet CIDR:

hcl resource "aws_security_group" "private_sg" { vpc_id = aws_vpc.main.id ingress { from_port = 3306 to_port = 3306 protocol = "tcp" cidr_blocks = ["10.0.1.0/24"] } ingress { from_port = -1 to_port = -1 protocol = "icmp" cidr_blocks = ["10.0.1.0/24"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "private-sg" } }

Instances are launched into the respective subnets:

```hcl
resource "awsinstance" "publicserver" {
ami = "ami-0195204d5dce06d99"
instancetype = "t2.micro"
subnet
id = awssubnet.public.id
vpc
securitygroupids = [awssecuritygroup.publicsg.id]
associate
publicipaddress = true
keyname = "yourkey_pair"
tags = {
Name = "public-server"
}
}

resource "awsinstance" "privateserver" {
ami = "ami-0195204d5dce06d99"
instancetype = "t2.micro"
subnet
id = awssubnet.private.id
vpc
securitygroupids = [awssecuritygroup.privatesg.id]
key
name = "yourkeypair"
tags = {
Name = "private-db-server"
}
}
```

Working with Existing Subnets via Data Sources

Terraform can import and reference existing subnets. The awssubnets Data Source doesn’t actually contain the CIDR as a return value, it ONLY returns subnet IDs. We need to pass it’s return values in to the confusingly named awssubnet Data Source and then iterate over THAT to get any more useful values such as the CIDR.

The pattern is:

```hcl
data "aws_vpc" "vpc" {
filter {
name = "vpc-id"
values = ["my-vpc"]
}
}

data "awssubnets" "privatesubnets" {
filter {
name = "vpc-id"
values = [data.aws_vpc.vpc.id]
}
tags = {
"tier" = "private"
}
}

data "awssubnet" "privatesubnets" {
count = length(data.awssubnets.privatesubnets.ids)
vpcid = data.awsvpc.vpc.id
id = data.awssubnets.privatesubnets.ids[count.index]
}
```

Working with the data above, we can now pass our CIDRs directly to a Resource.

In the example below we can see this employed in the creation of a new Security Group and Ingress Rule:

```hcl
resource "awssecuritygroup" "sg" {
vpcid = data.awsvpc.vpc.id
egress {
fromport = 0
to
port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "example-sg"
}
}

resource "awsvpcsecuritygroupingressrule" "dns" {
count = length(data.aws
subnet.privatesubnets)
security
groupid = awssecuritygroup.sg.id
cidr
ipv4 = data.awssubnet.privatesubnets[count.index].cidrblock
from
port = 53
ipprotocol = "udp"
to
port = 53
}
```

The aws_subnet Data Source has a few other return values that can be handy, but this is by far the most useful that I’ve come across and the most convenient way of looking up and manipulating subnet data.

Subnet Specification Table

Item Public Subnet Private Subnet
Resource aws_subnet.public aws_subnet.private
VPC ID aws_vpc.main.id aws_vpc.main.id
CIDR Block 10.0.1.0/24 10.0.2.0/24
mappubliciponlaunch true not set
Name Tag public-subnet private-subnet
Route Table public route table with 0.0.0.0/0 to IGW main route table by default
Instance Association publicserver with associatepublicipaddress true privatedbserver without public IP

Subnet Configuration Options Table

Setting Description
Auto-assign IP settings Enables auto-assign public IPv4 or IPv6 address for new network interfaces
Resource-based Name settings Specifies hostname type for EC2 instances and DNS query handling
IPv4 only Subnet has IPv4 CIDR block only
Dual stack Subnet has both IPv4 and IPv6 CIDR block
IPv6 only Subnet has IPv6 CIDR block only, resources get 169.254.0.0/16 link-local IPv4

Conclusion

The aws_subnet resource anchors VPC segmentation in Terraform. The reference implementation shows a VPC with CIDR 10.0.0.0/16, an Internet Gateway, a public subnet 10.0.1.0/24 with map_public_ip_on_launch enabled and a route to 0.0.0.0/0, and a private subnet 10.0.2.0/24 without public IP assignment. Each subnet must reside entirely within one Availability Zone and cannot span zones.

Practical usage extends beyond creation. Subnets support IPv4 only, dual stack, and IPv6 only addressing modes, with IPv6-only resources receiving 169.254.0.0/16 link-local addresses. Subnet types emerge from routing configuration, with public subnets directly reachable via Internet Gateway and private subnets requiring bastion or NAT access for internet egress.

Security is enforced at multiple layers. Security groups control instance-level traffic, network ACLs provide subnet-level allow deny, and default network ACLs allow all traffic unless customized. Flow logs can be created on VPC or subnet to capture traffic.

When working with existing infrastructure, the awssubnets data source returns only subnet IDs, requiring a second awssubnet data source with count to retrieve CIDR blocks for dynamic ingress rules. This pattern enables passing CIDRs directly to resources such as security group ingress rules.

Together, declarative subnet definition, routing association, and data-driven lookups provide a complete workflow for building and evolving public-private VPC architectures in Terraform.

Sources

  1. geeksforgeeks.org/devops/aws-vpc-public-private-subnets-terraform
  2. docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html
  3. tinfoilcipher.co.uk/2025/01/21/terraform-tricks-working-with-aws-subnets/

Related Posts