Building Infrastructure from Scratch: Creating Public Subnets in AWS
Tags: publicsubnets, aws, terrform
Published: 2024-03-26 18:01:14

In our previous guide, we explored [how to create a VPC using Terraform](https://dev.to/gritcoding/building-infrastructure-from-scratch-creating-a-vpc-with-terraform-2hmc). Now, let's dive deeper into AWS by creating public subnets using Terraform. Our focus will be on understanding the function of individual AWS resources rather than the syntax of Terraform.

By the end of this tutorial, you will have set up the following components in your AWS account:

- VPC
- Internet Gateway (IGW)
- Route Tables
- Subnets
- IAM Role
- Security Groups (SG)
- EC2 Instances

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b7xyc7s3djc0vx9mjatu.png)
 
## Prerequisites
Make sure you have the following installed and configured:
- The [Terraform CLI (1.2.0+)](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli).
- The [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
- An AWS account and credentials with sufficient permissions


Begin by setting up your AWS credentials. Export the following environment variables with your actual AWS credentials:
```
export AWS_ACCESS_KEY_ID=yourAccessKeyID
export AWS_SECRET_ACCESS_KEY=yourSecretAccessKey
export AWS_DEFAULT_REGION=yourAWSRegion
```

Next, create a `variables.tf` file in your project directory and add the following:
```
variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "vpc_name" {
  type    = string
  default = "my_vpc"
}

variable "environment" {
  description = "The deployment environment (e.g., dev, qa, prod)"
  type        = string
  
  validation {
    condition     = contains(["dev", "qa", "prod"], var.environment)
    error_message = "The environment must be one of: dev, qa, or prod."
  }
}

variable "env_cidr_map" {
  description = "Map of environment names to CIDR block second octet"
  type        = map(string)
  default = {
    "dev"     = "0"
    "qa"      = "10"
    "prod"    = "20"
  }
}

variable "public_subnets" {
  default = {
    "public_subnet_1" = 1
    "public_subnet_2" = 2
    "public_subnet_3" = 3
  }
}
```

Also, create a `main.tf` file in the same directory with this initial content:
```
provider "aws" {
  region = "us-east-1"
}

data "aws_availability_zones" "available" {}
data "aws_region" "current" {}

```

## Create Public Subnets

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l8b24s6yrzsd8pl6jpn3.png)

Now, let's create the infrastructure as depicted in the above chart. Make sure to add the code examples to your `main.tf` file.


#### **VPC Creation** 
Configure the VPC with a specific IPv4 CIDR block based on the environment variable. For example, if you choose `dev` as the environment, the IPv4 CIDR block will be `10.0.0.0/16`.
Refer to our [previous post](https://dev.to/gritcoding/building-infrastructure-from-scratch-creating-a-vpc-with-terraform-2hmc) for more details on VPC creation.

```
resource "aws_vpc" "vpc" {
  cidr_block = "10.${lookup(var.env_cidr_map, var.environment, "0")}.0.0/16"
  assign_generated_ipv6_cidr_block = true

  tags = {
    Name        = var.vpc_name
    Environment = var.environment
    Terraform   = "true"
  }
}

```

#### **Default NACL** 
A Network Access Control List (NACL) is automatically created with the VPC.


#### **Internet Gateway** 
Establish an Internet Gateway to allow internet access to the VPC.

```
resource "aws_internet_gateway" "internet_gateway" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "grit_coding_igw"
  }
}
```

#### **Subnets** 
Create a subnet in each of the three Availability Zones (AZs). Subnets will use the default NACL automatically. For custom NACL applications, you can modify NACL associations.

```
resource "aws_subnet" "public_subnets" {
  for_each                = var.public_subnets
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = cidrsubnet(aws_vpc.vpc.cidr_block, 8, each.value)
  availability_zone       = tolist(data.aws_availability_zones.available.names)[each.value]
  map_public_ip_on_launch = true
  assign_ipv6_address_on_creation = true
  ipv6_cidr_block               = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, each.value)

  tags = {
    Name      = each.key
    Terraform = "true"
  }
}
```

#### **Route Table** 
After creating the route table, add a route to the Internet Gateway. Subnets linked to this route table with the IGW route are defined as public subnets, facilitating access to and from the internet.

```
resource "aws_route_table" "public_route_table" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block     = "0.0.0.0/0"
    gateway_id     = aws_internet_gateway.internet_gateway.id
  }

  route {
    ipv6_cidr_block = "::/0"
    gateway_id      = aws_internet_gateway.internet_gateway.id
  }

  tags = {
    Name      = "grit_coding_public_rtb"
    Terraform = "true"
  }
}


resource "aws_route_table_association" "public" {
  depends_on     = [aws_subnet.public_subnets]
  route_table_id = aws_route_table.public_route_table.id
  for_each       = aws_subnet.public_subnets
  subnet_id      = each.value.id
}
```


With the public subnets in place, let’s add a few more resources to test the public subnet connection. We will need IAM Role, a Security Group, and EC2 instances.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hzmt4zxa5qyirg4wye7j.png)

#### **IAM Role with AmazonSSMManagedInstanceCore**
This role, when attached to an EC2 instance, provides the specified permissions. The `AmazonSSMManagedInstanceCore` policy is necessary for access via the session manager, which is part of the AWS Systems Manager.


```
resource "aws_iam_role" "ec2_ssm_role" {
  name = "ec2-ssm-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = "sts:AssumeRole",
        Effect = "Allow",
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    Terraform = "true"
  }
}
resource "aws_iam_role_policy_attachment" "ssm_managed_instance_core" {
  role       = aws_iam_role.ec2_ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2_ssm_instance_profile" {
  name = "ec2-ssm-instance-profile"
  role = aws_iam_role.ec2_ssm_role.name
}
```

Alternative Option: If you prefer using a key pair for EC2 access, create one in the AWS console, then specify its ID when creating the EC2 instance. Make sure to add an ingress rule for SSH (port 22) to the security group if you choose this method.



#### **Security Group**
We'll set up a security group with only egress rules. Ingress rules are not required since we are using the Systems Manager.

```
resource "aws_security_group" "ec2_sg" {
  name        = "EC2_Instance_SG"
  description = "Security group for EC2 instances with default outbound rules"
  vpc_id      = aws_vpc.vpc.id
}

resource "aws_security_group_rule" "ec2_sg_egress_rule" {
  type              = "egress"
  to_port           = 0
  protocol          = "-1"
  from_port         = 0
  cidr_blocks       = ["0.0.0.0/0"]
  ipv6_cidr_blocks  = ["::/0"]
  security_group_id = aws_security_group.ec2_sg.id
}
```

#### EC2 Instance Creation
Create EC2 instances in each public subnet, attaching the security group and IAM role.

```
resource "aws_instance" "public_instance" {
  for_each                    = aws_subnet.public_subnets
  ami                         = "ami-0c101f26f147fa7fd"
  instance_type               = "t2.micro"
  subnet_id                   = each.value.id
  associate_public_ip_address = true
  vpc_security_group_ids      = [aws_security_group.ec2_sg.id]
  iam_instance_profile        = aws_iam_instance_profile.ec2_ssm_instance_profile.name

  tags = {
    Name      = "Public_Instance_${each.key}"
    Terraform = "true"
  }
}
```


## Deploy and Test
Execute the following commands in your project directory to deploy your Terraform configuration.

```
terraform init
terraform apply -auto-approve
```
Enter `dev` as the environment value. 

 
Connect to the EC2 instance using Session Manager. Run the following commands to retrieve the ID of the connected EC2 instance. You can find more information about the Session Manager in this [post](https://dev.to/gritcoding/simplified-access-using-aws-session-manager-for-ec2-without-a-pem-file-cop).

```
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id
```



## Wrap-up
Today, we successfully set up public subnets and an EC2 instance, demonstrating how Infrastructure as Code (IaC) can make infrastructure deployment efficient, repeatable, and scalable.

For those new to cloud technology, these concepts can initially seem complex. However, I've found that hands-on experience, such as replicating these setups in the AWS console, makes learning more intuitive.

For further guidance, please watch the below demo video, which provides a manual walkthrough of the same setup in the AWS console.
{% youtube D3PSmGzvqTQ&t=6s %}

You can find the source code from today's session in my [GitHub Repository](https://github.com/grit-coding/devto-code-snippets/tree/main/tech-tutorials/infrastructure/terraform/create-public-subnets).

**Note**
If you're just beginning or not yet familiar with AWS terminology and concepts, consider looking into the AWS Cloud Practitioner certification. It provides comprehensive and clear insights into cloud concepts, making it an excellent starting point for beginners.