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" {
cidrblock = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
resource "awsinternetgateway" "gw" {
vpcid = awsvpc.main.id
tags = {
Name = "main-gw"
}
}
resource "awssubnet" "public" {
vpcid = awsvpc.main.id
cidrblock = "10.0.1.0/24"
mappubliciponlaunch = true
tags = {
Name = "public-subnet"
}
}
resource "awssubnet" "private" {
vpcid = awsvpc.main.id
cidrblock = "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"
gatewayid = awsinternetgateway.gw.id
}
tags = {
Name = "public-rt"
}
}
resource "awsroutetableassociation" "a" {
subnetid = awssubnet.public.id
routetableid = 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"
subnetid = awssubnet.public.id
vpcsecuritygroupids = [awssecuritygroup.publicsg.id]
associatepublicipaddress = true
keyname = "yourkey_pair"
tags = {
Name = "public-server"
}
}
resource "awsinstance" "privateserver" {
ami = "ami-0195204d5dce06d99"
instancetype = "t2.micro"
subnetid = awssubnet.private.id
vpcsecuritygroupids = [awssecuritygroup.privatesg.id]
keyname = "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
toport = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "example-sg"
}
}
resource "awsvpcsecuritygroupingressrule" "dns" {
count = length(data.awssubnet.privatesubnets)
securitygroupid = awssecuritygroup.sg.id
cidripv4 = data.awssubnet.privatesubnets[count.index].cidrblock
fromport = 53
ipprotocol = "udp"
toport = 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.