Part II : Provisioning AWS EKS Cluster with Terraform
What is AWS EKS ?
AWS EKS (Elastic Kubernetes Service) is a managed service that runs Kubernetes for containerized apps on AWS. It handles the heavy lifting — cluster setup, scaling, and control plane management — so focus stays on building and deploying applications.
What is Terraform ?
Terraform is a tool for describing and creating infrastructure with code instead of clicking around in cloud consoles. Write what your infra should look like and let Terraform take care of that. It can even create, update, and destroy infrastructure across different cloud providers.
Enough talk! Let’s jump into the practical part!
Set up the Terraform environment
To get started , you’ll need Terraform installed on your environment, so there’s no better place to follow than the official installation guide.
Verify the installation :
terraform --versionAdditionally to that, install the AWS CLI by following the official installation guide.
$ aws --versionNext, configure your AWS account credentials by providing the Access Key ID and the Secret Access Key in the terminal.
$ aws configureVerify AWS credentials by checking the shared files in the home directory:
$ cat ~/.aws/credentials
$ cat ~/.aws/configGet started with the configuration
To follow the Infrastructure as Code best practices, the Terraform configuration is hosted in a remote GitHub repository so it’s versioned, shareable, and easy to use by tools like Jenkins or a member of the team.
Okay! Let’s get started!
step 1 : Specify your cloud provider
- Create a new directory and move into it.
- Create a new file called providers.tf and specify the AWS provider (Use the exact file name) :
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "6.9.0"
}
}
}Run the command to install the AWS provider on your local :
$ terraform initOnce you done, your project directory will looks like this :
.
└── Project root/
├── .terraform
├── providers.tf
└── .terraform.lock.hclstep 2 : Configure VPC (Virtual Private Cloud)
Get Abdessamad Abidar’s stories in your inbox
Join Medium for free to get updates from this writer.
Create a new file called as example vpc.tf .
provider "aws" {
region = "eu-west-3"
}
variable "vpc_cidr" {}
variable "private_cidrs" {}
variable "public_cidrs" {}
# Virtual Private Cloud
resource "aws_vpc" "eks-vpc" {
cidr_block = var.vpc_cidr
tags = {
"Name" = "ECOMM_VPC"
"Terraform" = "true"
"Environment" = "true"
"kubernetes.io/cluster/ecomm-eks-cluster" = "shared"
}
enable_dns_hostnames = true
}
# Internet Gateway
resource "aws_internet_gateway" "eks-igw" {
vpc_id = aws_vpc.eks-vpc.id
tags = {
"Name" = "ECOMM_IGW"
}
}
# Private subnet 01
resource "aws_subnet" "private-sub-eu-west-3a" {
vpc_id = aws_vpc.eks-vpc.id
cidr_block = var.private_cidrs[0]
availability_zone = "eu-west-3a"
tags = {
"Name" = "private-eu-west-3a"
"kubernetes.io/cluster/ecomm-eks-cluster" = "shared"
"kubernetes.io/role/internal-elb" = 1
}
}
# Private subnet 02
resource "aws_subnet" "private-sub-eu-west-3b" {
vpc_id = aws_vpc.eks-vpc.id
cidr_block = var.private_cidrs[1]
availability_zone = "eu-west-3b"
tags = {
"Name" = "private-eu-west-3b"
"kubernetes.io/cluster/ecomm-eks-cluster" = "shared"
"kubernetes.io/role/internal-elb" = 1
}
}
# Private subnet 03
resource "aws_subnet" "private-sub-eu-west-3c" {
vpc_id = aws_vpc.eks-vpc.id
cidr_block = var.private_cidrs[2]
availability_zone = "eu-west-3c"
tags = {
"Name" = "private-eu-west-3c"
"kubernetes.io/cluster/ecomm-eks-cluster" = "shared"
"kubernetes.io/role/internal-elb" = 1
}
}
# Public subnet 01
resource "aws_subnet" "public-sub-eu-west-3a" {
vpc_id = aws_vpc.eks-vpc.id
cidr_block = var.public_cidrs[0]
availability_zone = "eu-west-3a"
tags = {
"Name" = "public-eu-west-3a"
"kubernetes.io/cluster/ecomm-eks-cluster" = "shared"
"kubernetes.io/role/elb" = 1
}
}
# Public subnet 02
resource "aws_subnet" "public-sub-eu-west-3b" {
vpc_id = aws_vpc.eks-vpc.id
cidr_block = var.public_cidrs[1]
availability_zone = "eu-west-3b"
tags = {
"Name" = "public-eu-west-3b"
"kubernetes.io/cluster/ecomm-eks-cluster" = "shared"
"kubernetes.io/role/elb" = 1
}
}
# Public subnet 03
resource "aws_subnet" "public-sub-eu-west-3c" {
vpc_id = aws_vpc.eks-vpc.id
cidr_block = var.public_cidrs[2]
availability_zone = "eu-west-3c"
tags = {
"Name" = "public-eu-west-3c"
"kubernetes.io/cluster/ecomm-eks-cluster" = "shared"
"kubernetes.io/role/elb" = 1
}
}
# Elastic IP
resource "aws_eip" "eks-eip" {
domain = "vpc"
tags = {
"Name" = "ECOMM_EIP"
}
}
# NAT Gateway
resource "aws_nat_gateway" "eks-nat-gw" {
allocation_id = aws_eip.eks-eip.id
subnet_id = aws_subnet.public-sub-eu-west-3a.id
tags = {
Name = "ECOMM_NAT_GW"
}
# To ensure proper ordering, it is recommended to add an explicit dependency
# on the Internet Gateway for the VPC.
depends_on = [aws_internet_gateway.eks-igw]
}
# Private route table
resource "aws_route_table" "eks-private-rtb" {
vpc_id = aws_vpc.eks-vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.eks-nat-gw.id
}
tags = {
"Name" = "ECOMM_PRIVATE_RT"
}
}
# Public route table
resource "aws_route_table" "eks-public-rtb" {
vpc_id = aws_vpc.eks-vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.eks-igw.id
}
tags = {
"Name" = "ECOMM_PUBLIC_RT"
}
}
# Create an association between private route tables and private subnets
resource "aws_route_table_association" "private-rtb-eu-west-3a-assoc" {
subnet_id = aws_subnet.private-sub-eu-west-3a.id
route_table_id = aws_route_table.eks-private-rtb.id
}
resource "aws_route_table_association" "private-rtb-eu-west-3b-assoc" {
subnet_id = aws_subnet.private-sub-eu-west-3b.id
route_table_id = aws_route_table.eks-private-rtb.id
}
resource "aws_route_table_association" "private-rtb-eu-west-3c-assoc" {
subnet_id = aws_subnet.private-sub-eu-west-3c.id
route_table_id = aws_route_table.eks-private-rtb.id
}
# Create an association between public route tables and public subnets
resource "aws_route_table_association" "public-rtb-eu-west-3a-assoc" {
subnet_id = aws_subnet.public-sub-eu-west-3a.id
route_table_id = aws_route_table.eks-public-rtb.id
}
resource "aws_route_table_association" "public-rtb-eu-west-3b-assoc" {
subnet_id = aws_subnet.public-sub-eu-west-3b.id
route_table_id = aws_route_table.eks-public-rtb.id
}
resource "aws_route_table_association" "public-rtb-eu-west-3c-assoc" {
subnet_id = aws_subnet.public-sub-eu-west-3c.id
route_table_id = aws_route_table.eks-public-rtb.id
}
There are some input variables, and their values are set in a terraform.tfvars file. This file should be excluded from version control because it can contain sensitive data; file looks like this :
vpc_cidr = "10.0.0.0/16"
private_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_cidrs = ["10.0.110.0/24", "10.0.120.0/24", "10.0.130.0/24"]This Terraform snippet configures AWS as the provider and sets the region, relying on AWS CLI-configured credentials rather than hardcoding keys. It declares input variables for VPC and subnet CIDRs, then queries the available Availability Zones in the chosen region to spread subnets across them
Three private subnets and three public subnets are created, with each private/public pair placed in a different Availability Zone for high availability and load distribution.
step 3: Configure EKS (Elastic Kubernetes Service)
Create a new file called as example eks.tf .
# Create EKS Cluster
# Create IAM role for eks cluster
resource "aws_iam_role" "eks-cluster-iam-role" {
name = "eks_cluster_iam_role"
# Terraform's "jsonencode" function converts a
# Terraform expression result to valid JSON syntax.
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"sts:AssumeRole",
"sts:TagSession"
]
Effect = "Allow"
Principal = {
Service = "eks.amazonaws.com"
}
},
]
})
tags = {
"Name" = "ECOMM EKS IAM Role"
}
}
# Attach Policy to EKS cluster IAM role
resource "aws_iam_role_policy_attachment" "eks-policy-AmazonEKSClusterPolic" {
role = aws_iam_role.eks-cluster-iam-role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
}
# Create EKS cluster
resource "aws_eks_cluster" "eks-cluster" {
name = "eks_cluster"
access_config {
bootstrap_cluster_creator_admin_permissions = true
authentication_mode = "API"
}
role_arn = aws_iam_role.eks-cluster-iam-role.arn
version = "1.33"
vpc_config {
endpoint_private_access = true
endpoint_public_access = true
subnet_ids = [
aws_subnet.private-sub-eu-west-3a.id,
aws_subnet.private-sub-eu-west-3b.id,
aws_subnet.private-sub-eu-west-3c.id,
aws_subnet.public-sub-eu-west-3a.id,
aws_subnet.public-sub-eu-west-3b.id,
aws_subnet.public-sub-eu-west-3c.id
]
}
# 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.eks-policy-AmazonEKSClusterPolic,
]
}This Terraform code creates a managed Kubernetes cluster on AWS using EKS. It sets up the necessary permissions through an IAM role and deploys the cluster across multiple availability zones to ensure high availabilty.
step 4: Configure EKS Node Group
Create another file called node-group.tf
# Create IAM role for eks node group
resource "aws_iam_role" "eks-node-group-iam-role" {
name = "eks_node_group_iam_role"
# Terraform's "jsonencode" function converts a
# Terraform expression result to valid JSON syntax.
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"sts:AssumeRole",
"sts:TagSession"
]
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
tags = {
"Name" = "ECOMM EKS Node Group IAM Role"
}
}
# Attach Policies to EKS node group IAM role
resource "aws_iam_role_policy_attachment" "eks-policy-AmazonEKSWorkerNodePolicy" {
role = aws_iam_role.eks-node-group-iam-role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}
resource "aws_iam_role_policy_attachment" "eks-policy-AmazonEKS_CNI_Policy" {
role = aws_iam_role.eks-node-group-iam-role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}
resource "aws_iam_role_policy_attachment" "eks-policy-AmazonEC2ContainerRegistryReadOnly" {
role = aws_iam_role.eks-node-group-iam-role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}
# Create EKS node group
resource "aws_eks_node_group" "eks-node-group" {
cluster_name = aws_eks_cluster.eks-cluster.name
node_group_name = "eks_nodes"
node_role_arn = aws_iam_role.eks-node-group-iam-role.arn
subnet_ids = [
aws_subnet.private-sub-eu-west-3a.id,
aws_subnet.private-sub-eu-west-3b.id,
aws_subnet.private-sub-eu-west-3c.id
]
scaling_config {
desired_size = 2
max_size = 3
min_size = 1
}
update_config {
max_unavailable = 1
}
# capacity_type = "ON_DEMAND"
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.eks-policy-AmazonEKSWorkerNodePolicy,
aws_iam_role_policy_attachment.eks-policy-AmazonEKS_CNI_Policy,
aws_iam_role_policy_attachment.eks-policy-AmazonEC2ContainerRegistryReadOnly,
]
tags = {
"Name" = "ECOMM EKS Node Group"
}
}This Terraform code creates the worker nodes for the EKS cluster by setting up a node group with the necessary IAM permissions. It establishes an IAM role for EC2 instances with three essential policies. The node group is deployed across private subnets in three availability zones with auto-scaling configured for 1–3 t3.medium instances.
step 5: Provision the infrastructure
Since we’ve used modules but they’re not installed locally yet, so we need to run the command :
$ terraform initRun this to see the planned changes before creating anything:
$ terraform planFinally apply the configuration using :
$ terraform applyBefore committing changes, prepare the .gitignore file like this.
# Local .terraform directories
**/.terraform/*
# Terraform lockfile
.terraform.lock.hcl
# .tfstate files
*.tfstate
*.tfstate.*
# Exclude all .tfvars files, which are likely to contain sentitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvarsComplete Jenkins Pipeline
To complete the Jenkins pipeline that we’ve started in the part I of this project, here are additional stages for Terraform.
stage('Checkout IaC') {
steps {
git branch: 'master', credentialsId: 'github-creds', url: 'https://github.com/abdessamadabidar/IaC.git'
}
}
stage('Terraform: Provision Amazon EKS cluster') {
steps {
dir('ecomm-ci-cd/terraform') {
sh 'terraform init'
sh 'terraform apply -auto-approve'
}
}
}The first stage checks out the IaC repository from GitHub. It pulls the master branch so the pipeline always uses the latest infrastructure code.
The second stage moves into the Terraform folder and runs terraform init to download providers/modules, then terraform apply -auto-approve to create or update the EKS cluster with no manual prompt.
Conclusion
That’s it — Terraform is set up, the code is clean, and the repo won’t leak sensitive files. With a solid .gitignore and clear structure, the workflow stays safe, repeatable, and easy to collaborate on. Now it’s just plan, apply, and watch the infrastructure come to life.
Great time to wrap it up with action. Next, the project will be finalized by deploying the application to the existing EKS cluster, using Ansible for setup/automation and Argo CD for GitOps continuous delivery.
A bientôt !
Practical DevOps Pipeline: Part I — Continuous Integration with Jenkins
Practical DevOps Pipeline: Part III — GitOps Deployments on EKS with Argo CD and Ansible









