The most intuitive way to build it is to open the AWS console and start clicking. It works. The problem shows up six months later, when something breaks and nobody remembers which boxes were ticked.
That is why a different approach exists: infrastructure as code.
Instead of configuring things by hand, you write a file that describes the result you want, and a tool builds it for you. The most widely used one is Terraform.
It's the difference between cooking from memory and following a recipe. The dish may turn out exactly the same, but only the recipe lets someone else reproduce it, shows what changed between two versions, and lets you fix one ingredient without starting from scratch.
The chicken-and-egg problem
There's a slightly paradoxical step at the very beginning. To create resources on AWS, Terraform needs permissions. And it needs somewhere to record what it has created — a file called the state file, which is its memory: without it, on the second run it couldn't tell the difference between "I built this" and "this doesn't exist yet".
But the permissions and the place to store that memory are themselves AWS resources. Terraform would have to create them... before it can run!
The solution is to take this very first step with a different tool. CloudFormation is often the one used — AWS's native service, which doesn't need an external state file because Amazon keeps that memory itself. A small initial file creates the storage and the identity with the required permissions, and from that point on everything else is Terraform.
It's worth clearing up a common misconception: this step is about the permissions of whoever does the building, not the cluster's. The roles EKS needs in order to manage nodes and networking are created by Terraform with no trouble at all.
Networking comes first
A cluster doesn't live in a vacuum: it needs a network. On AWS it's called a VPC, a private portion of the network reserved just for you.
It helps to think of it as a building with two kinds of rooms. Public subnets have a window onto the street: that's where traffic coming from the Internet passes through. Private subnets don't: the machines inside can go out — to download updates, for example — but nobody from outside can get in directly.
Our application's servers go in the private rooms. It's a security decision: traffic arriving from the Internet never reaches them — it only talks to an intermediate component, the load balancer, which sits in the public area and routes requests inward.
For the private machines to still reach the Internet on the way out, you need a NAT gateway: a kind of concierge desk that takes the mail out and brings the replies back, without ever letting strangers ring the buzzer.
One detail that trips up almost everyone the first time: the subnets need to be tagged. Kubernetes has to work out on its own which rooms it can create public load balancers in and which ones are for internal ones, and it does that by reading these tags.
It's two lines of configuration, and if they're missing the cluster works but the application stays unreachable. Once they're written into the code, you can't forget them again.
The cluster and its nodes
With the network in place, the cluster is almost the easy part. It's made up of two pieces.
The control plane is the brain: it decides where to run applications, keeps track of what's active, and reacts to failures. With EKS, Amazon manages it entirely: you don't see it, you don't update it — you pay for it to just work!
The nodes are the muscles: virtual machines where the applications actually run. You declare them in groups, specifying the machine type and how many you want — with a minimum and a maximum, so the group grows and shrinks on its own depending on the load.
What about the application?
Once the cluster is up and running, the application is typically installed with Helm, which is to Kubernetes what a package manager is to an operating system: one command, and the app arrives with all its components already configured.
Terraform can handle this too, but it's better to keep the two things separate: one project that builds the infrastructure, another that installs the applications on top of it. The reason is practical. Infrastructure changes rarely, applications change every week: mixing them means you risk touching the network every time you update an image.
The real payoff: a well-organized repository
Terraform lets you split the code into modules: reusable blocks, each with one specific job. A typical structure looks like this:
├── modules/
│├── network/# VPC, subnet, NAT, routing
│└── cluster/# EKS, node group, roles
├── environments/
│├── staging/
│└── production/
That way, when someone new joins, they know where to look within thirty seconds.
Staging and production use the same modules with different parameters, so they really are identical — not "almost identical", which is where half of all production problems begin.
And because everything is text in a Git repository, every change has an author, a date, and a reason.
Rebuilding the entire infrastructure from scratch in another region or environment becomes a matter of minutes instead of days.
💡Clicking saves you ten minutes today. Writing gives them back to you every day.

