<aside> 🤔
Before going to Step 2 & 3
This project will try to implement the EKS cluster to be fully private (no public cluster endpoints, only private) → the cluster API server can only be accessible outside the VPC through a SSM bastion host inside the same VPC
Instead of placing the bastion host in a public subnet, we can use SSM Session Manager to SSH into that host BUT in a private subnet, reducing public availability. There are two ways to connect the private bastion host to AWS services on the Internet: NAT Gateway and VPC Endpoints
https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html#private-access
</aside>
Create a VPC with both DNS resolution and DNS hostnames enabled:
/* locals.tf */
locals {
project_name = "eks-demo"
}
### CREATE A NEW MODULE: **vpc** ###
/* modules/**vpc**/variables.tf */
# Referencing from root
variable "project_name" {
type = string
}
variable "az1" {
type = string
}
variable "az2" {
type = string
}
variable "az3" {
type = string
}
variable "main_vpc_cidr" {
description = "Main VPC's CIDR Range"
type = string
default = "10.0.0.0/16"
}
/* main.tf */
module "vpc" {
source = "./modules/vpc"
project_name = local.project_name
}
/* modules/**vpc**/main.tf */
resource "aws_vpc" "main_vpc" {
cidr_block = var.main_vpc_cidr
# Required for EKS
enable_dns_support = true
enable_dns_hostnames = true
tags = {
"Name" = "${var.project_name}-vpc"
}
}
Create subnets for the VPC (total of 8):
/* variables.tf */
variable "az1" {
description = "AZ1 for subnets: EKS 1, RDS 1, ALB 1"
type = string
default = "us-east-1a"
}
variable "az2" {
description = "AZ for subnets: EKS 2, RDS 2, ALB 2"
type = string
default = "us-east-1b"
}
variable "az3" {
description = "AZ for subnets: Bastion & NAT Gateway"
type = string
default = "us-east-1c"
}
/* main.tf */
module "vpc" {
...
az1 = var.az1
az2 = var.az2
az3 = var.az3
}
/* modules/**vpc**/variables.tf */
# Referencing from root
variable "az1" {}
variable "az2" {}
variable "az3" {}
variable "subnet_eks1_cidr" {
description = "Subnet EKS 1's CIDR Range (private)"
type = string
default = "10.0.1.0/24"
}
variable "subnet_eks2_cidr" {
description = "Subnet EKS 2's CIDR Range (private)"
type = string
default = "10.0.2.0/24"
}
variable "subnet_rds1_cidr" {
description = "Subnet RDS 1's CIDR Range (private)"
type = string
default = "10.0.3.0/24"
}
variable "subnet_rds2_cidr" {
description = "Subnet RDS 2's CIDR Range (private)"
type = string
default = "10.0.4.0/24"
}
variable "subnet_alb1_cidr" {
description = "Subnet ALB 1's CIDR Range (public)"
type = string
default = "10.0.5.0/24"
}
variable "subnet_alb2_cidr" {
description = "Subnet ALB 2's CIDR Range (public)"
type = string
default = "10.0.6.0/24"
}
variable "subnet_bastion_cidr" {
description = "Subnet SSM Bastion's CIDR Range (private)"
type = string
default = "10.0.7.0/24"
}
variable "subnet_natgw_cidr" {
description = "Subnet NAT Gateway's CIDR Range (public)"
type = string
default = "10.0.8.0/24"
}
/* modules/**vpc**/main.tf */
# AZ 1
resource "aws_subnet" "eks1_subnet" {
vpc_id = aws_vpc.main_vpc.id
availability_zone = var.az1
cidr_block = var.subnet_eks1_cidr
tags = {
"Name" = "${var.project_name}-subnet-eks1"
}
}
resource "aws_subnet" "rds1_subnet" {
vpc_id = aws_vpc.main_vpc.id
availability_zone = var.az1
cidr_block = var.subnet_rds1_cidr
tags = {
"Name" = "${var.project_name}-subnet-rds1"
}
}
resource "aws_subnet" "alb1_subnet" {
vpc_id = aws_vpc.main_vpc.id
availability_zone = var.az1
cidr_block = var.subnet_alb1_cidr
tags = {
"Name" = "${var.project_name}-subnet-alb1"
"kubernetes.io/role/elb" = "1"
}
}
# AZ 2
resource "aws_subnet" "eks2_subnet" {
vpc_id = aws_vpc.main_vpc.id
availability_zone = var.az2
cidr_block = var.subnet_eks2_cidr
tags = {
"Name" = "${var.project_name}-subnet-eks2"
}
}
resource "aws_subnet" "rds2_subnet" {
vpc_id = aws_vpc.main_vpc.id
availability_zone = var.az2
cidr_block = var.subnet_rds2_cidr
tags = {
"Name" = "${var.project_name}-subnet-rds2"
}
}
resource "aws_subnet" "alb2_subnet" {
vpc_id = aws_vpc.main_vpc.id
availability_zone = var.az2
cidr_block = var.subnet_alb2_cidr
tags = {
"Name" = "${var.project_name}-subnet-alb2"
"kubernetes.io/role/elb" = "1"
}
}
# AZ 3
resource "aws_subnet" "bastion_subnet" {
vpc_id = aws_vpc.main_vpc.id
availability_zone = var.az3
cidr_block = var.subnet_bastion_cidr
tags = {
"Name" = "${var.project_name}-subnet-bastion"
}
}
resource "aws_subnet" "natgw_subnet" {
vpc_id = aws_vpc.main_vpc.id
availability_zone = var.az3
cidr_block = var.subnet_natgw_cidr
tags = {
"Name" = "${var.project_name}-subnet-natgw"
}
}
Create an Internet Gateway (automatically attached to the VPC with vpc_id):
resource "aws_internet_gateway" "default_igw" {
vpc_id = aws_vpc.main_vpc.id
tags = {
"Name" = "${var.project_name}-igw"
}
}
Create a public NAT Gateway with an Elastic IP:
resource "aws_eip" "default_natgw_eip" {
domain = "vpc"
depends_on = [aws_internet_gateway.default_igw]
}
resource "aws_nat_gateway" "default_natgw" {
allocation_id = aws_eip.default_natgw_eip.id
subnet_id = aws_subnet.natgw_subnet.id
tags = {
"Name" = "${var.project_name}-natgw"
}
depends_on = [aws_internet_gateway.default_igw]
}
Create the route tables, associate them with the correct subnets
# Private subnets: EKS & RDS
resource "aws_route_table" "private_rtb" {
vpc_id = aws_vpc.main_vpc.id
tags = {
"Name" = "${var.project_name}-private-rtb"
}
}
resource "aws_route_table_association" "private_eks_1" {
route_table_id = aws_route_table.private_rtb.id
subnet_id = aws_subnet.eks1_subnet.id
}
resource "aws_route_table_association" "private_eks_2" {
route_table_id = aws_route_table.private_rtb.id
subnet_id = aws_subnet.eks2_subnet.id
}
resource "aws_route_table_association" "private_rds_1" {
route_table_id = aws_route_table.private_rtb.id
subnet_id = aws_subnet.rds1_subnet.id
}
resource "aws_route_table_association" "private_rds_2" {
route_table_id = aws_route_table.private_rtb.id
subnet_id = aws_subnet.rds2_subnet.id
}
# Public subnets: ALB & NAT Gateway
resource "aws_route_table" "public_rtb" {
vpc_id = aws_vpc.main_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.default_igw.id
}
tags = {
"Name" = "${var.project_name}-public-rtb"
}
}
resource "aws_route_table_association" "public_alb_1" {
route_table_id = aws_route_table.public_rtb.id
subnet_id = aws_subnet.alb1_subnet.id
}
resource "aws_route_table_association" "public_alb_2" {
route_table_id = aws_route_table.public_rtb.id
subnet_id = aws_subnet.alb2_subnet.id
}
resource "aws_route_table_association" "public_natgw" {
route_table_id = aws_route_table.public_rtb.id
subnet_id = aws_subnet.natgw_subnet.id
}
# SSM Bastion subnet (private, connect to NAT Gateway only)
resource "aws_route_table" "bastion" {
vpc_id = aws_vpc.main_vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.default_natgw.id
}
tags = {
"Name" = "${var.project_name}-bastion-rtb"
}
}
resource "aws_route_table_association" "bastion" {
route_table_id = aws_route_table.bastion.id
subnet_id = aws_subnet.bastion_subnet.id
}
For each small step, make sure you run the Terraform commands to plan & apply changes to the AWS infrastructure:
# Needed as we added a new module "vpc"
terraform init
terraform validate && terraform fmt
terraform plan -out tf.plan
terraform apply "tf.plan"
The VPC should look something like this after the initial setup in this step:
