Kubernetes 101: Step-by-Step Guide to Creating an EKS Cluster and Deploying a Multi-Container Web Application

Carolina Delwing Rosa
Towards AWS
Published in
12 min readSep 6, 2023

I’m back from a short hiatus when I was thinking about the best way to introduce Kubernetes with nice examples.

So, today, you’ll learn step by step how to deploy a multi-container web application using Kubernetes in an AWS-managed cluster called EKS. In this article, you’ll see how to create an EKS Cluster using the AWS console, but in the upcoming posts, you’ll also learn how to quickly launch the EKS cluster using Infrastructure as Code, Terraform.

Prerequisites of this project:

  • AWS account;
  • IAM user with sufficient rights;
  • Access to a terminal;
  • Have AWS CLI, kubectl, and Git installed on your machine;
  • Basic knowledge of Docker, Kubernetes, AWS EKS, and Git.

Warning: AWS EKS clusters cost $0.10 per hour, so don’t forget to delete everything once you’re done.

For us to start, we need first take a look at some basic concepts:

What is Kubernetes?

Kubernetes (or K8s) is an open-source container orchestration tool. It looks simple, right? Well, it's not. Kubernetes allows you to automate the deployment, scaling, and management of containerized applications, ensuring that there is no downtime in production environments because of its features as for example self-healing. Remember Docker-Compose from my previous post? Well, Docker-Compose is also an orchestration tool, but lightweight and usually used for small-scale applications or test purposes. While in Docker-Compose, you can only have one host, in K8s you can have multiple nodes.

In order to understand and help visualize K8s components, let’s take a look at its architecture:

First of all, a K8s cluster is composed of one or more control planes, responsible for management tasks, and for worker nodes, responsible for executing the applications. But what is a node? It’s an instance, in the case of EKS, a node can be an EC2 instance. You can have multiple pods within a node. Stay tuned to figure out what a pod is.

You can create a local cluster in your machine using tools such as Kind, Minikube, or Docker Desktop. However, in this tutorial, we’ll create a K8s cluster on AWS.

The image below shows in detail the components of your K8s cluster:

The components of a Kubernetes cluster. Source: https://kubernetes.io/docs/concepts/overview/components/

The control plane is really the brain of your cluster, and it’s composed of:

API Server: is responsible for exposing the K8s API, which is how users interact with the cluster.

etcd: is a key-value datastore responsible for storing specifications, state, and configurations of the cluster.

Scheduler: It selects which node will host a specific pod, taking into consideration the available resources of each node and its state.

Controller Manager: ensures that your cluster is running with the last state specified in the etcd. For example, keeping the specified number of replicas of a pod.

Worker Node Components:

Kubelet: It’s an agent placed inside the nodes responsible for managing the pods according to Controller instructions.

Kube-proxy: It’s a network proxy running in your worker nodes responsible for the node network and for routing requisitions to pods.

Container Runtime: manages the execution and lifecycle of containers inside the nodes.

Now that you have learnt the core components of a K8s cluster, we need to take a look at K8S objects, which are entities that you create from YAML or JSON files called manifests:

Pod: It’s well known as the smallest K8s unit that you can create and manage. One pod can have many containers, sharing the same storage and network resources.

Deployment: is responsible for providing declarative updates for Pods and ReplicaSets. You create a deployment file describing the desired state (create ReplicaSets, for example), and then the actual state of your cluster is changed to the desired state. In other words, the deployment acts together with ReplicaSet to ensure that a certain number of replicas of a pod are running in the nodes. You can also use Deployment to specify application parameters such as images, ports, volumes, and variables.

ReplicaSet: It’s a K8s object responsible for maintaining a stable set of replica Pods running inside a node.

Service: It’s how you expose groups of Pods over a network (and, as a consequence, your application running in a Pod) to be accessed. There are three main types of Services: ClusterIP (exposes the Service within the Cluster with an internal IP), NodePort (exposes the Service on each Node’s IP at a static port — the NodePort), or LoadBalancer (exposes the Service externally using an external Load Balancer, but you need to provide it).

Now, if you’re feeling confused, it’s 100% understandable. I believe it takes years to master k8S. When I started to study it, I used to draw the architecture on paper to better visualize it. Hope this helps you too!

Let’s create our cluster! But, wait, what is EKS?

From the AWS website: Amazon Elastic Kubernetes Service (Amazon EKS) is a managed Kubernetes service to run Kubernetes in the AWS cloud and on-premises data centers. Well, in my words, it is a managed Kubernetes Cluster, where the control plane is managed by AWS. And, although you need to pay a few dollars to use it (depending on the time, of course), it’s an easier way to launch a fully functional and high-available Kubernetes cluster.

Let’s begin with the practice part of the tutorial. First, ensure that you’re logged in to your AWS account with the same user that you’ll configure your terminal with the command aws configure. For example, if you create your EKS cluster on the console with your admin user, you need to authenticate in your terminal using the access key of your admin user.

Create a VPC for the Worker Nodes:

The EKS Cluster needs a VPC with some specific networking requirements to work. In the provided link, AWS gives us three options for CloudFormation templates to create VPCs. You’ll have an option with public and private subnets, which is the most recommended, but for learning purposes, we’re going to use the public subnet option. In this address, select the option “Only public subnets” and then copy the link of step 5.

Go to the AWS CloudFormation Console, Click on Create Stack, and paste the link in the field below Amazon S3 URL.

Click on Next, give the Stack a name, and leave the rest of the options as default. Click on submit and wait for the resources to be created. This template will deploy three public subnets, an internet gateway, and a security group.

Create an IAM Role for your EKS Cluster:

Go to the IAM Dashboard, click on Roles, and then on Create Role. Select AWS Service, EKS, and EKS Cluster. Click on next.

Give the Role a name, and click on Create role. From now on, your EKS Cluster is allowed to execute operations such as pods or load balancer creation.

Create the EKS Cluster:

Go to the EKS Console, click on Clusters, and then on Create Cluster.

Give your cluster a name, select the Kubernetes version, and select the EKS Role that you created in the previous step.

Click on Next and select the recently created VPC. The subnets and security group will be selected automatically. Select the Cluster Endpoint Access as Public and Private, and go to the next step.

Set everything else as default, and click on Create. This step might take up to 15 minutes. It’s a good time to grab a coffee :)

Once your cluster is ready, you should see an Active status on the EKS page.

Connect to your EKS Cluster

In your terminal, if you are correctly configured with your AWS credentials and you have kubectl (CLI tool used to run commands against your Kubernetes cluster) installed, then you are ready to connect to your recently created EKS cluster. Run the following command updated with your cluster’s name and AWS region:

aws eks update-kubeconfig --region <region> --name <EKS_cluster_name>

You should see something like this:

Create a Node Group EC2 IAM Role

The next step of this tutorial is to create the nodes of your cluster. In EKS, they are part of an auto-scaling group called Node Group. Before creating the Node Group itself, we need to create an IAM Role responsible for allowing the kubelet daemon of each node to make calls to AWS APIs and manage pods on our behalf.

Again, go to the IAM Dashboard, click on Roles, and then on Create Role. Select AWS Service, EC2, and click on next. Add the following policies:

Last, give the Role a name and click on Create role.

Create a Node Group

Let’s create the Node Group. Go back to the EKS dashboard, select your cluster, and click on Add node group.

Give it a name, select the Node Group IAM Role, and click on Next. Now, select the AMI and size of your nodes. For this project, we’ll use Amazon Linux 2 t2.micro EC2 instances.

You also need to set up the auto-scaling group parameters. The minimum number of EC2 instances set up was 4 and the maximum was 6, because I used the same cluster to deploy applications of my next projects. For this project, if you wish, you can set the desired number of nodes to 2.

Leave everything else as default and create the Node Group.

In your terminal, type the following command to check the nodes of your cluster:

kubectl get nodes

If everything went well, you should see something like this:

Create the YAML manifests

As we mentioned before, we need to create the K8s manifests that we will use to deploy the application, which is composed of a MongoDB database with National Hockey League (NHL) data and a Nodejs API server that exposes endpoints so users can send HTTP requests and get a response. For this project, we will create two deployment files (one for the API and another for the database) as well as two service files (again, one for the API and another for the database). Be careful with the indentation of your files!

On your terminal, run the command below to create the files or clone my GitHub repository:

touch app-deployment.yaml app-service.yaml mongo-deployment.yaml mongo-service.yaml

Database Deployment Manifest

Open the file db-deployment.yaml with a text editor (VS Code, for example), and add the following content:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: mongodb
name: mongodb-deployment
spec:
replicas: 1
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- image: caroldelwing/wcdmongodb:latest
name: mongodb
ports:
- containerPort: 27017

Let’s analyze this file quickly:

apiVersion: specifies the API Version for the resource (the Deployment).

If you are unsure about what API version to use, execute the following command in your terminal:

kubectl api-resources

kind: specifies the type of resource you want to define.

metadata: contains metadata about your Deployment, such as the name of your Deployment and labels, which are key-value pairs used to identify and categorize the Deployment.

spec: contains the specification of the Deployment. First, you define the number of replicas, which is the number of database Pods you want to run. Then, the selector defines which Pods the Deployment will manage, matching them with the app: mongodb label. Last, the template section describes the Pod template that will be used to create new Pods: Pod labels, container name and image, and what port the container should be listening to. The caroldelwing/wcdmongodb:latest image creates a MongoDB container with an NHL collection (check my previous post for more details about this image).

Database Service Manifest

Open the file db-service.yaml with a text editor, and add the following content:

apiVersion: v1
kind: Service
metadata:
name: db
spec:
selector:
app: mongodb # Select pods with label app=mongodb
ports:
- protocol: TCP
port: 27017 # Service Port
targetPort: 27017 # Pods Port

This file will be used to create a ClusterIP Service object called db that serves as a network endpoint to access the database pod. The spec section defines the selector that determines which Pod the Service db should route traffic to. In other words, every Pod that has the app: mongodb label will be part of the Service. Lastly, the ports of the Service are specified: first, the Port that the Service will listen to, and then the target port on the pods to which traffic will be forwarded.

API Server Deployment Manifest

Now, open the file app-deployment.yaml with a text editor, and add the following content:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: app
name: app-deployment
spec:
replicas: 1
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- image: caroldelwing/wcdapp:latest
name: app
ports:
- containerPort: 3000
env:
- name: DB_HOST
value: "db"
- name: DB_PORT
value: "27017"
- name: DB_NAME
value: "WCD_project2"

Similarly, this Deployment file defines the number of replicas of the API Server to be deployed, the Docker image, labels, the port that should be exposed by the container, and some environment variables that will be used by the application code to connect to the database. The caroldelwing/wcdapp:latest image comprises the application code (again, check my previous post if you want to learn more about it).

API Server Service Manifest

Last but not least, open the file app-service.yaml with a text editor, and add the following content:

apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
type: LoadBalancer
selector:
app: app
ports:
- protocol: TCP
port: 80
targetPort: 3000

This YAML file defines the Service that will expose the API Server application using a Load Balancer service type. This service will expose the application externally using an AWS Load Balancer. The Service is set to listen to port 80 (HTTP), and the target port is set to 3000, which means that the traffic will be forwarded to port 3000 in the application Pod.

Deploying the Application

Let’s jump to the funniest part of this tutorial: deploying and testing!

Go to your terminal and execute the following commands:

kubectl apply -f mongo-service.yaml
kubectl apply -f mongo-deployment.yaml
kubectl apply -f app-service.yaml
kubectl apply -f app-deployment.yaml

These commands will create the deployments and services from the YAML files. The output should be similar to:

Let’s check if the pods were created:

Voi-lá! You can see that we have two Pods running: one database Pod and one application Pod.

Let’s check the services:

It looks like everything went well: the pods and services were deployed, and they are running. But what about our application? Let’s make some HTTP requests and test the routes!

To do that, you need first to copy the external IP of the load balancer (image above) and then paste it on your browser.

The available routes are:

  • / - returns all documents in the nhl_stats_2022 collection.
  • /players/top/:number - returns top players. For example, /players/top/10 will return the top 10 players leading in points scored.
  • /players/team/:teamname - returns all players of a team. For example, /players/team/TOR will return all players of Toronto Maple Leafs.
  • /teams - returns a list of the teams.

And there you go:

IT WORKED! :)

Some other routes:

We did it! We just deployed and tested a multi-container web application in an EKS cluster.

I know that was A LOT. I spent the last months studying Kubernetes, and I’m also prepared to spend the next years studying it. The content is endless! I tried to summarize the basics of K8s the best way I could.

Don’t forget to delete everything once you are finished: EKS Cluster, Load Balancer, and VPC.

If you liked this content, stay tuned for the next post, where we’ll learn how to deploy an EKS Cluster using Terraform (way easier) and also monitor our cluster.

Thank you for your time! See you soon!

--

--

Engineer & tech enthusiast. Passionate writer. Documenting my journey into DevOps & Cloud. Reach me here: https://www.linkedin.com/in/carolinadelwingrosa/