Skip to main content

Set Up a Three-Node Kubernetes Cluster on AWS with kubeadm and Terraform

· 7 min read
Sarat Kota
Cloud Engineer

I have always wanted a Kubernetes cluster of my own. Tools such as Minikube and Kind are excellent for learning, but they run a cluster locally. I wanted to practice with multiple virtual machines, with one control-plane node and two workers, in an environment that feels a little closer to a real cluster.

VirtualBox is one option, but not every computer has enough spare memory and CPU to run several virtual machines comfortably. A cloud provider such as AWS lets us create those machines on demand. The downside is that manually recreating the network, instances, and Kubernetes setup each time takes effort and leaves room for mistakes.

To make that easier, I put together a Terraform configuration that provisions the AWS infrastructure and bootstraps Kubernetes with kubeadm. The goal is to make a repeatable learning environment that is quick to create and easy to destroy—not a production-ready cluster.

The Terraform code is in sarat532/tf-ec2-k8s-cluster.

What we are going to build​

The default configuration creates three EC2 instances:

  • One control-plane node.
  • Two worker nodes.

Terraform also creates the VPC, subnets, internet gateway, routes, security groups, and EC2 key pair. Instance user-data scripts install containerd and Kubernetes packages, initialize the control plane with kubeadm, join the workers, and install Calico for pod networking.

The worker count is configurable, so this setup can also create a larger or smaller learning cluster.

Before you start​

You will need:

  • An AWS account and AWS CLI credentials with permission to manage the resources listed above. Use an appropriately scoped IAM identity; do not put AWS access keys in Terraform files.
  • Terraform CLI ~> 1.17.1.
  • OpenSSH tools (ssh-keygen and ssh).
  • A region where the configured availability zones are available. The configuration currently uses ap-south-1a and ap-south-1b; update the subnet configuration if you use another region.

Configure an AWS CLI profile if you have not already done so, and confirm that it can access your account:

aws configure --profile k8s-admin
aws sts get-caller-identity --profile k8s-admin

Terraform uses the profile and region selected in its variables. The default profile in the configuration is account-admin; the examples below override it with k8s-admin.

A note about cost and security​

EC2 instances and related AWS resources can incur charges for as long as they exist. This configuration uses t3a.medium instances by default, and the actual cost depends on the region, runtime, storage, and other AWS pricing. Check current AWS pricing before applying, and destroy the cluster when you finish.

Review the security groups before creating anything. The current general ingress rules expose ports to 0.0.0.0/0, and the instances receive public IP addresses. In particular, SSH and Kubernetes ports should not be left open to the whole internet. Restrict SSH to your trusted IP range and limit Kubernetes traffic to the networks that need it. Port 8080 is used temporarily to pass the kubeadm join command and is restricted to the worker security group, but the join token is still sensitive.

This is a learning setup. Its network exposure, single control plane, and bootstrap approach are not appropriate defaults for production.

1. Get the Terraform configuration​

Clone the repository and move into its directory:

git clone https://github.com/sarat532/tf-ec2-k8s-cluster.git
cd tf-ec2-k8s-cluster

The important inputs have defaults in _variables.tf:

VariableDefaultWhat it controls
aws_regionap-south-1AWS region
aws_profileaccount-adminAWS CLI profile
project_nametf-ec2-k8s-clusterResource names and tags
vpc_cidr10.190.32.0/26VPC address range
instance_typet3a.mediumInstance type
worker_count2Number of worker nodes
k8s_version1.36Kubernetes package repository version

You can change defaults in _variables.tf, or override them locally with a terraform.tfvars file. That file is ignored by Git and is a good place for your account-specific choices:

aws_profile = "k8s-admin"
aws_region = "ap-south-1"
project_name = "learning-k8s"
vpc_cidr = "10.190.32.0/26"
instance_type = "t3a.medium"
worker_count = 2
k8s_version = "1.36"

Make sure your chosen VPC CIDR does not overlap with networks you need to connect to. If you deploy in a different region, also update the availability zones in vpc.tf.

2. Create an SSH key pair​

From the repository root, create a key pair:

ssh-keygen -t rsa -b 4096 -f ec2-k8s

This creates ec2-k8s (the private key) and ec2-k8s.pub (the public key). Terraform registers the public key with AWS. Keep the private key safe and never commit it; the repository ignores the generated key files.

3. Initialize and review Terraform​

Initialize the working directory to download the required provider plugins:

terraform init

Check that the configuration is valid, then inspect the proposed changes:

terraform validate
terraform plan

Read the plan carefully. Confirm the AWS account, region, instance count, network rules, and expected resources before continuing.

4. Create the cluster​

Apply the configuration:

terraform apply

Terraform asks you to review and confirm the plan. Type yes only after checking it.

Terraform creating the EC2 instances does not mean Kubernetes is ready yet. The user-data scripts continue installing and configuring Kubernetes after the instances start, so allow time for the bootstrap to finish. The exact duration depends on AWS and package downloads; treat the few-minute setup as an estimate, not a guarantee.

The control-plane script initializes Kubernetes with kubeadm and installs Calico using the pod CIDR 172.16.0.0/16. It briefly serves a generated join command to workers over a private cluster path. Each worker validates that command before using it to join the control plane.

5. Connect and verify the nodes​

Use the control-plane public IP shown in Terraform's output to connect:

ssh -i ec2-k8s ec2-user@<control-plane-public-ip>

On the control-plane instance, check the nodes:

kubectl get nodes -o wide

Wait until all three nodes show Ready. The ROLES column should identify the control-plane and workers. For example:

NAME STATUS ROLES AGE VERSION
k8s-master Ready control-plane ... ...
k8s-worker-0 Ready worker ... ...
k8s-worker-1 Ready worker ... ...

The kubectl configuration is set up for both root and ec2-user on the control-plane. If a node does not become ready or join, check that instance's cloud-init output at /var/log/cloud-init-output.log and the EC2 system log. Also confirm that the worker can reach the control-plane join endpoint and that the security groups allow the required cluster traffic.

6. Destroy the cluster when you are finished​

When you no longer need the cluster, return to the Terraform directory and run:

terraform destroy

Review the destroy plan and confirm it before proceeding. This removes the infrastructure managed by this Terraform configuration, including the EC2 instances and networking resources. Keep your Terraform state and SSH private key protected; they should not be shared publicly.

A few things to keep in mind​

  • The configuration installs a specific Kubernetes package repository version, currently 1.36, and pins the Calico manifest separately. Changing k8s_version does not guarantee compatibility with the operating system, container runtime, or Calico version.
  • The default network rules are intentionally something to review, not copy blindly. Tighten them before applying, especially for SSH and the Kubernetes API.
  • Terraform makes the infrastructure repeatable, but it does not make the cluster highly available. This is a small cluster for learning and practice.
  • Destroy the resources when you are done to avoid unnecessary cloud charges.

With the infrastructure automated, creating a multi-node cluster becomes a repeatable workflow: configure, plan, apply, learn, and destroy.

References​