In this project , we shall be creating infrastructure and kubernetes EKS cluster in AWS using Terraform reusable modules.
Code re-use is essential as the same code can be leveraged and used accross teams, projects, environments and teams. It also promotes consistency and standardisation and can be used to enforce best practices in your organisation. Other advantages are easier collaboration between teams.
We shall be creating
- 
A VPC 2 public subnets and 2 private subnets (I have used 2 private subnets instead of 4 to reduce costs. The lower tier should host the applications MySQL database) The public subnet will host our Jenkin and SonaQube servers with our Java application in the private subnet. |We shall also be using only 2 AZ's which is the minimum requirement for an EKS cluster.
 - 
An Internet Gateway for the Public subnet
 - 
Two(2) NAT Gateways in the public subnets for each AZ.
 - 
We shall deloying an EKS cluster into the private subnets
 - 
Cluster Role
 - 
Nodegroup Role
 
For this demo, we shall be storing our state flies locally but ideally, it should be stored remotely in an S3 bucket with Dynamo DB for state locking.
Our folder structure will be as follows: Folder Structure
config 
    terraform.tfvars
modules
    aws_vpc
    aws_subnets
    aws_IGW
    aws_nat_gateway
    aws_eip
    aws_route_table
    aws_route_table_association
    aws_eks
    aws_eks_nodegroups
main.tf
providers.tf
versions.tf
variables.tf
README.md
.gitignore
Each module folder will contain 3 sub folders
- main.tf
 - variables.tf
 - output.tf
 
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}
AS WE ARE CREATING REUSABLE MODULES, WE SHALL AVOID HARD CODING AND USE VARIABLES FOR ALL VALUES THAT WILL LIKELY CHANGE PER PROJECT OR ENVIRONMENT.
provider "aws" {
  region = var.region
}
This will be created in aws_vpc/main.tf
resource "aws_vpc" "acme_vpc" {
  cidr_block       = var.vpc_cidr_block
  instance_tenancy = var.instance_tenancy
  tags = var.tags
}
variable "vpc_cidr_block" {
  default = "10.0.0.0/16"
}
variable "instance_tenancy" {
  default = "default"
}
variable "tags" {
  
}
output "vpc_id" {
  value = aws_vpc.acme_vpc.id
}
This will be created in aws_subnets/main.tf
resource "aws_subnet" "main" {
  vpc_id     = var.vpc_id
  cidr_block = var.cidr_block
  availability_zone = var.availability_zone
  tags = var.tags
}
variable "subnet_cidr_block" {
  default = "10.0.0.0/24"
}
variable "availability_zone" {
}
variable "tags" {
}
variable "vpc_id" {
}
output "subnet_id" {
  value = aws_subnet.acme_subnet.id
}
This will be created in aws_IGW/main.tf
resource "aws_internet_gateway" "igw" {
  vpc_id = var.vpc_id
  tags = var.tags
}
variable "tags" {
}
variable "vpc_id" {
}
output "IGW_id" {
  value = aws_internet_gateway.igw.id
}
To allow internet access from the private subnets aws_nat_gateway/main.tf
resource "aws_nat_gateway" "ngw" {
  allocation_id = var.allocation_id
  subnet_id     = var.subnet_id
  tags = var.tags
}
variable "allocation_id" {
}
variable "subnet_id" {
}
variable "tags" {
}
output "nat_gateway_id" {
  value = aws_nat_gateway.ngw.id
}
This is needed by the NAT Gateway
aws_eip/main.tf
resource "aws_eip" "natgw_eip" {
  tags = var.tags
}
variable "tags" {
}
output "eip_id" {
  value = aws_eip.natgw_eip.id
}
aws_route_table/main.tf
resource "aws_route_table" "rtb" {
  vpc_id = var.vpc_id
  route {
    cidr_block = var.rtb_cidr_block
    gateway_id = var.gateway_id
  }
tags = var.tags }
variable "vpc_id" {
}
variable "rtb_cidr_block" {
  default = "0.0.0.0/0"
}
variable "gateway_id" {
}
variable "tags" {
}
output "rtb_id" {
  value = aws_route_table.rtb.id
}
Associate the route tables with their subnets aws_route_table_associations/main.tf
resource "aws_route_table_association" "a" {
  subnet_id      = var.subnet_id
  route_table_id = var.route_table_id
}
variable "subnet_id" {
}
variable "route_table_id" {
}
output "rtb_assoc_id" {
  value = aws_route_table_association.a.id
}
aws_eks/main.tf
resource "aws_eks_cluster" "acme_cluster" {
  name     = var.cluster_name
  role_arn = aws_iam_role.acme_eks_cluster.arn
  vpc_config {
    subnet_ids = var.subnet_ids
  }
  # Ensure that IAM Role permissions are created before and deleted after EKS Cluster handling.
  # Otherwise, EKS will not be able to properly delete EKS managed EC2 infrastructure such as Security Groups.
  depends_on = [
    aws_iam_role_policy_attachment.ACME-AmazonEKSClusterPolicy,
    aws_iam_role_policy_attachment.ACME-AmazonEC2ContainerRegistryReadOnly,
  ]
  tags = var.tags
}
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["eks.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}
resource "aws_iam_role" "acme_eks_cluster" {
  name               = "acme-eks-cluster"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
resource "aws_iam_role_policy_attachment" "ACME-AmazonEKSClusterPolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.acme_eks_cluster.name
}
resource "aws_iam_role_policy_attachment" "ACME-AmazonEC2ContainerRegistryReadOnly" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.acme_eks_cluster.name
}
variable "cluster_name" {
  
}
variable "tags" {
  
}
variable "subnet_ids" {
  
}
output "eks_cluster_output_name" {
    value = aws_eks_cluster.acme_cluster.name
  
}
aws_eks_nodegroups/main.tf
resource "aws_eks_node_group" "worker_nodes" {
  cluster_name    = var.cluster_name
  node_group_name = var.node_group_name
  node_role_arn   = aws_iam_role.nodegroup_role.arn
  subnet_ids      = var.subnet_ids
  scaling_config {
    desired_size = 1
    max_size     = 2
    min_size     = 1
  }
  update_config {
    max_unavailable = 1
  }
instance_types = [ "t3.medium" ]
  # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling.
  # Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces.
  depends_on = [
    aws_iam_role_policy_attachment.ACME-AmazonEKSWorkerNodePolicy,
    aws_iam_role_policy_attachment.ACME-AmazonEKS_CNI_Policy,
    aws_iam_role_policy_attachment.ACME-AmazonEC2ContainerRegistryReadOnly,
  ]
  tags = var.tags
}
resource "aws_iam_role" "nodegroup_role" {
  name = var.node_group_name
  assume_role_policy = jsonencode({
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
    Version = "2012-10-17"
  })
}
resource "aws_iam_role_policy_attachment" "ACME-AmazonEKSWorkerNodePolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.nodegroup_role.name
}
resource "aws_iam_role_policy_attachment" "ACME-AmazonEKS_CNI_Policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.nodegroup_role.name
}
resource "aws_iam_role_policy_attachment" "ACME-AmazonEC2ContainerRegistryReadOnly" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.nodegroup_role.name
}
variable "cluster_name" {
  type = string
}
variable "subnet_ids" {
  
}
variable "node_group_name" {
  
}
variable "tags" {
  
}
variable "node_i_am_role" {
  
}
That is the end of our child modules. We shall now create the root module to call the child modules
module "aws_vpc_module" {
  source = "./modules/aws_vpc"
  for_each = var.vpc_config
  vpc_cidr_block = each.value.vpc_cidr_block
  instance_tenancy = each.value.instance_tenancy
  tags = each.value.tags
}
module "aws_subnet_module" {
  source = "./modules/aws_subnets"
  for_each = var.subnet_config
  subnet_cidr_block = each.value.subnet_cidr_block
  availability_zone = each.value.availability_zone
  vpc_id = module.aws_vpc_module[each.value.vpc_name].vpc_id
  tags = each.value.tags
}
module "IGW_module" {
  source   = "./modules/aws_IGW"
  for_each = var.IGW_config
  vpc_id   = module.aws_vpc_module[each.value.vpc_name].vpc_id
  tags     = each.value.tags
}
module "rtb_module" {
  source     = "./modules/aws_route_table"
  for_each   = var.rtb_config
  vpc_id     = module.aws_vpc_module[each.value.vpc_name].vpc_id
  gateway_id = each.value.private == 0 ? module.IGW_module[each.value.gateway_name].IGW_id : module.nat_gateway_module[each.value.gateway_name].nat_gateway_id
  tags       = each.value.tags
}
module "rtb_assoc_module" {
  source         = "./modules/aws_route_table_association"
  for_each       = var.rtb_assoc_config
  subnet_id      = module.aws_subnet_module[each.value.subnet_name].subnet_id
  route_table_id = module.rtb_module[each.value.route_table_name].rtb_id
}
module "nat_gateway_module" {
  source        = "./modules/aws_nat_gateway"
  for_each      = var.natgw_config
  subnet_id     = module.aws_subnet_module[each.value.subnet_name].subnet_id
  allocation_id = module.eip_module[each.value.eip_name].eip_id
  tags          = each.value.tags
}
module "eip_module" {
  source   = "./modules/aws_eip"
  for_each = var.eip_config
  tags     = each.value.tags
}
module "eks_module" {
  source = "./modules/aws_eks"
  for_each = var.eks_cluster_config
  cluster_name = each.value.cluster_name
  subnet_ids = [module.aws_subnet_module[each.value.subnet1].subnet_id, module.aws_subnet_module[each.value.subnet2].subnet_id, module.aws_subnet_module[each.value.subnet3].subnet_id , module.aws_subnet_module[each.value.subnet4].subnet_id]
  tags= each.value.tags
}
module "nodegroups_module" {
  source = "./modules/aws_eks_nodegroups"
  for_each = var.nodegroup_config
  node_group_name = each.value.node_group_name
  cluster_name = module.eks_module[each.value.cluster_name].eks_cluster_output_name
  node_i_am_role = each.value.node_i_am_role
  subnet_ids = [module.aws_subnet_module[each.value.subnet1].subnet_id, module.aws_subnet_module[each.value.subnet2].subnet_id]
  
  tags = each.value.tags
}
Our configuration file is in config/terraform.tfvars
config/terraform.tfvars
region = "us-east-1"
vpc_config = {
  vpc01 = {
    vpc_cidr_block = "10.0.0.0/16"
    instance_tenancy = "default"
    tags = {
      "Name" = "ACME-vpc"
    }
  }
}
subnet_config = {
  "public_us_east_1a" = {
    vpc_name          = "vpc01"
    subnet_cidr_block = "10.0.0.0/18"
    availability_zone = "us-east-1a"
    tags = {
      "Name" = "public_us_east_1a"
    }
  }
  "public_us_east_1b" = {
    vpc_name          = "vpc01"
    subnet_cidr_block = "10.0.64.0/18"
    availability_zone = "us-east-1b"
    tags = {
      "Name" = "public_us_east_1b"
    }
  }
  "private_us_east_1a" = {
    vpc_name          = "vpc01"
    subnet_cidr_block = "10.0.128.0/18"
    availability_zone = "us-east-1b"
    tags = {
      "Name" = "private_us_east_1a"
    }
  }
  "private_us_east_1b" = {
    vpc_name          = "vpc01"
    subnet_cidr_block = "10.0.192.0/18"
    availability_zone = "us-east-1b"
    tags = {
      "Name" = "private_us_east_1b"
    }
  }
}
IGW_config = {
  igw01 = {
    vpc_name = "vpc01"
    tags = {
      "Name" = "ACME-IGW"
    }
  }
}
rtb_config = {
  publicrtb01 = {
    private      = 0
    vpc_name     = "vpc01"
    gateway_name = "igw01"
    tags = {
      "Name" = "public-rtb1"
    }
  }
  privatertb01 = {
    private      = 1
    vpc_name     = "vpc01"
    gateway_name = "natgw01"
    tags = {
      "Name" = "private-rtb1"
    }
  }
  privatertb02 = {
    private      = 1
    vpc_name     = "vpc01"
    gateway_name = "natgw02"
    tags = {
      "Name" = "private-rtb2"
    }
  }
}
rtb_assoc_config = {
  public_rtb_assoc01 = {
    subnet_name      = "public_us_east_1a"
    route_table_name = "publicrtb01"
  }
  public_rtb_assoc02 = {
    subnet_name      = "public_us_east_1b"
    route_table_name = "publicrtb01"
  }
  private_rtb_assoc01 = {
    subnet_name      = "private_us_east_1a"
    route_table_name = "privatertb01"
  }
  private_rtb_assoc02 = {
    subnet_name      = "private_us_east_1b"
    route_table_name = "privatertb02"
  }
}
natgw_config = {
  natgw01 = {
    eip_name    = "eip01"
    subnet_name = "public_us_east_1a"
    tags = {
      "Name" = "natgw01"
    }
    depends_on = ""
  }
  natgw02 = {
    eip_name    = "eip02"
    subnet_name = "public_us_east_1b"
    tags = {
      "Name" = "natgw02"
    }
    depends_on = ""
  }
}
eip_config = {
  eip01 = {
    tags = {
      "Name" = "elasticIP01"
    }
  }
  eip02 = {
    tags = {
      "Name" = "elasticIP02"
    }
  }
}
eks_cluster_config = {
cluster-key = {
cluster_name = "cluster-key"
subnet1 = "public_us_east_1a"
subnet2 = "public_us_east_1b"
subnet3 = "private_us_east_1a"
subnet4 = "private_us_east_1a"
tags = {
    "Name" = "ACME-cluster"
}
}
}
nodegroup_config = {
node1 = {
node_group_name = "node1"
cluster_name = "cluster-key"
node_i_am_role = "eks-node-role1"
subnet1 ="private_us_east_1a"
subnet2 ="private_us_east_1b"
tags = {
    "Name" = "ACME-clusterNode1"
}
}
node2 = {
node_group_name = "node2"
cluster_name = "cluster-key"
node_i_am_role = "eks-node-role1"
subnet1 ="private_us_east_1b"
subnet2 ="private_us_east_1b"
tags = {
    "Name" = "ACME-clusterNode2"
}
}
}
terraform init
terraform validate
terraform plan --var-file="config/terraform.tfvars"
This could take up to 15 minutes or more depending on size , number and type of the resources that are being created.
terraform apply --var-file="config/terraform.tfvars"














