If you're anything like me, you know just how painful it can be to crawl through the Internet, blog after blog, only to realise that every blog post is either failing to elaborate, failing to give working examples, or failing to adhere to the KISS principle.

I won't be guaranteeing the absolute accuracy of this guide, since I too am learning, however I will do my best to explain everything clearly.

I am not going to waste your time. If you meet any of the following requirements, I am going to walk you through all the aspects (as of 07 December 2020) to deploy a dead simple "Hello, World!" application using Amazon's Elastic Kubernetes Service and Fargate:

  1. Your experience with Kubernetes is non-existent
  2. Your experience with EKS is non-existent
  3. Your experience with Fargate is non-existent
  4. Your experience with any form of Docker orchestration is non-existent
  5. Your experience with Application Load Balancer is non-existent

Seriously, if you are any of the above, you're safe to keep reading. I will do my best to ensure that relevant initialisms and acronyms are at the very least described in a simple way at least once.

Oh, one more thing: this post might at times assume you're using some form of bash-esque terminal. You likely are using something similar enough if you're on Linux or macOS. For Windows, you may wish to get a copy of Ubuntu (WSL).

Pack Your Bag

Before starting, we need some tools in our backpack. Here's what we need:

  1. Amazon Web Services CLI (awscli) for connecting to our AWS account
  2. Elastic Kubernetes Service Controller (eksctl) for preparing our Kubernetes environment within AWS
  3. Kubernetes Controller (kubectl) for managing our workloads on Kubernetes
  4. Helm for easily installing and upgrading some of the AWS Load Balancer stuff

There's not much that needs simplifying for grabbing these tools, so please accept my apologies while I refer you very briefly to their installation instructions: https://docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html

And for Helm, see: https://helm.sh/docs/intro/install/

Quick Kubernetes (k8s) Introduction

I'm going to assume you know what a container is, and that's all as far as this concept goes.

Kubernetes solves the problem of quickly and easily scaling up your services to handle demand. If you suddenly get a 50% influx in traffic, you need to be able to support that load. Spinning up containers manually or deploying new VMs is just far too slow and cumbersome.

In effect, Kubernetes is commanding your Nodes (servers, whether virtual or physical) to spin up, spin down, or modify containers. This is known as orchestration.

In the above diagram, I am trying to illustrate the layers of how we define a service. You will often see a file specifying kind: Deployment. This is, in essence, defining a Pod, which is made up of one or more containers.

You might be asking, what is a Pod? A Pod is a collection of closely-knit containers which carry out one particular function. If you have multiple containers, you'll find these referred to as "sidecar" containers. Generally if you are running something like a REST API, you'll only have one container per Pod. If your API has a backend database, that would be in a separate Pod (and thus as a separate deployment).

An example of when to have multiple containers in a Pod is when you are exposing a service without authentication, and you wish to put a frontend proxy in front of your application with HTTP Basic Authentication, for example.

Your Deployment is in essence going to be the blueprint of your Pods. Each time you execute the deployment, the number of pods created will be exactly equal to the number of replicas in your specification.

To solidify that knowledge, we'll have a look at an example Deployment later.

If you're developing the application you're going to be running, it's useful to know that Kubernetes will run a Service (we'll get into what these are later) called kube-dns. This, in essence, allows you to reference your other applications by name.

Quick EKS Introduction

EKS (Elastic Kubernetes Service) essentially moves the burden of managing Kubernetes itself away from you, and onto Amazon's operations teams.

When you deploy to EKS, you'll be using Kubernetes just like you would locally, except now you'll be talking to Amazon's Kubernetes servers in order to give your instructions.

Quick Fargate Introduction

This is an absolutely fantastic platform that makes your Kubernetes pods now work in a "serverless" state. Of course, that doesn't mean you are running code in literal clouds, but instead it means that Amazon is automatically spinning up your services on their servers, completely transparently to you. The result? Less hassle for you and your team, and you no longer need to manage virtual machines or physical servers. The grass really is greener.

Quick Load Balancer Introduction

Amazon provides a few types of Load Balancer services. In case you don't already know, a Load Balancer is useful because it can effectively distribute load across multiple pods in your Kubernetes cluster.

There are several types of Load Balancers in AWS. The two relevant to Kubernetes in Fargate are:

  1. Network Load Balancer - these are Load Balancers operating at Layer 4 of the OSI model. This just means that they can do both UDP and TCP, on any ports, and not limited to HTTP/HTTPS.
  2. Application Load Balancer - you can think of this like a website load balancer. They operate at Layer 7 of the OSI model and can only process HTTP/HTTPS.

Deploying "Hello, World!"

Alright, so now that you've at least got some idea about all of this Kubernetes, EKS and Fargate stuff, I think it would be great for us to just get right into things.

Here's a quick rundown of the tasks:

  1. Deploy the infrastructure (VPC, Subnets, Security Groups, IAM Roles, IAM Policies, IAM Service Accounts, EKS Clusters, Fargate Profiles, and more if needed)
  2. Deploy the application
  3. Expose the application with an Application Load Balancer (ALB)

Let's give this a go.

Task 1: Deploy the Infrastructure

We will be using eksctl and a yaml definition to deploy our infrastructure into our AWS account. This essentially makes it so that you can reliably reproduce your VPC, subnets, etc., without really having to worry about all the intricate details in your documentation. Instead, you'll share your yaml file with colleagues.

Defining Infrastructure

See the example below of some sample infrastructure:

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: sl-testing
  region: eu-west-2

fargateProfiles:
  - name: sl-testing-kube-system
    selectors:
      - namespace: kube-system
  - name: sl-testing-hello-world-app
    selectors:
      - namespace: hello-world-app

So let's quickly have a look at some of the key aspects of this YAML file:

  1. the kind indicates to eksctl what we're trying to deploy exactly. In our case, this is ClusterConfig because we wish to deploy a cluster into EKS.
  2. The metadata's name will appear in most aspects of this deployment. Give it something meaningful, for example, you may call it webapp-production if you're deploying something called webapp.
  3. The fargateProfile's name will appear only within Fargate. The name of this doesn't matter too much, but my take on this is that each of your services should be given a fargateProfile.
  4. The kube-system namespace is where various internal bits Kubernetes runs will be kept, such as the DNS service.

Deploying Infrastructure

Deploying the new cluster is pretty straight forward. We just run one command:

$ eksctl create cluster -f fargate.yaml 
[ℹ]  eksctl version 0.32.0 
[ℹ]  using region eu-west-2
[ℹ]  setting availability zones to [eu-west-2c eu-west-2b eu-west-2a]
...
[ℹ]  no tasks
[✔]  all EKS cluster resources for "sl-testing" have been created
[ℹ]  kubectl command should work with "/home/username/.kube/config", try 'kubectl get nodes'
[✔]  EKS cluster "sl-testing" in "eu-west-2" region is ready

For me this took a solid 19 minutes to run, so don't mash CTRL+C if it seems to be taking a while. It's deploying quite a lot:

  • Your VPC
  • The multiple routing tables within your VPC
  • Your subnets in 3 different availability zones
  • Security groups
  • Your EKS cluster
  • Your Fargate profiles

Once you've done that, you can have a click around your AWS Console to see what's appeared. Here's a few of the elements I see in my AWS environment:

One VPC within AWS
Five routing tables within AWS, assigned to my VPC.
Four security groups within AWS, assigned to my VPC.
My cluster within AWS.
Two nodes running on Fargate right out of the box.
These two nodes are running coredns (kube-dns).
Two Fargate profiles in my EKS Compute configuration.

But wait! Why are there two nodes for coredns?!

Fargate will deploy one node per pod. Usually with Kubernetes, a node will have many pods, but to enhance security (at least, I think that's the justification) with the multi-tenant environment in AWS, Fargate will launch a lightweight VM (Virtual Machine) per pod, on which your pod will run. The number of nodes you will see should be roughly equivalent to the number of replicas you've deployed to EKS.

A quick comment about how kubectl will work.

Following the deployment of your EKS cluster, your kubectl will now be configured to directly interact with AWS. You can see this in one of the final log messages.

[ℹ]  kubectl command should work with "/home/username/.kube/config", try 'kubectl get nodes'

And, indeed, if you run the command, you will see the same exact nodes and pods as in the AWS Console.

$ kubectl get nodes
NAME                                                   STATUS   ROLES    AGE   VERSION
fargate-ip-192-168-141-93.eu-west-2.compute.internal   Ready    <none>   15m   v1.18.8-eks-7c9bda
fargate-ip-192-168-98-124.eu-west-2.compute.internal   Ready    <none>   15m   v1.18.8-eks-7c9bda

$ kubectl get pods -A 
NAMESPACE     NAME                       READY   STATUS    RESTARTS   AGE
kube-system   coredns-564d976b7b-5nkrf   1/1     Running   0          16m
kube-system   coredns-564d976b7b-vdmws   1/1     Running   0          16m

Task 2: Create the Namespace

Although we have instructed EKS how to deploy our applications within a namespace (to Fargate), we must also register these within Kubernetes itself. Fortunately, that's easy. Create a file called namespace.yaml (or something else if you'd prefer).

apiVersion: v1
kind: Namespace
metadata:
  name: hello-world-app

In this file, we simply tell Kubernetes that we wish to create a Namespace called hello-world-app. Once you've done this, you can register the Namespace with Kubernetes by running just one command:

$ kubectl apply -f namespace.yaml 
namespace/hello-world-app created

Task 3: Deploy the Application Pods

The next stage is to deploy our application. Fortunately this is pretty simple. We can use just about any hello-world web server for this. Just for the purposes of demonstrating this, I will be using crccheck/hello-world. This provides us with a very basic web server running on port 8000/tcp.

Preparing Application Deployment Specification

We will now start working with kubectl to define and apply our application's Deployment specification. Take a look at the example below and see if you can pick out what each piece of the configuration is responsible for.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world-app
  namespace: hello-world-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hello-world-app
  template:
    metadata:
      labels:
        app: hello-world-app
    spec:
      containers:
      - name: hello-world-app
        image: crccheck/hello-world:latest
        ports:
        - containerPort: 8000

Let's step through this together.

  1. We're letting Kubernetes know that we wish to deploy an application called hello-world-app by defining the name in the metadata section.
  2. We're also stating that this application should run in the hello-world-app Namespace by defining the namespace in the metadata section.
  3. We specify the number of pods to create (replicas). This is how you would scale up (or down) your application.
  4. We apply some labels, which become useful later when we look at deploying Services.
  5. Next up is our template. This is the part of the configuration which tells Kubernetes how to deploy a single pod. Each pod will be labelled, just like the deployment, and also spin up one container called hello-world-app. This container will use the crccheck/hello-world:latest image, and expose port 8000 with a random port on the Node (the actual server the pod is running on).

And that's all as far as the Deployment specification itself goes.

Applying the Deployment

Next we can apply this to EKS, which in turn will deploy it to Fargate.

$ kubectl apply -f deployment.yaml 
deployment.apps/hello-world-app created

$ kubectl get deployments -w -n hello-world-app
NAME              READY   UP-TO-DATE   AVAILABLE   AGE
hello-world-app   0/3     3            0           28s
hello-world-app   1/3     3            1           39s
hello-world-app   2/3     3            2           50s
hello-world-app   3/3     3            3           78s

By running kubectl get deployments with the -w flag, we are saying we wish to watch this and get updates as soon as they happen. You will hopefully see that READY counts from 0 to 3, though it may be a little bit slow depending on what Fargate has to do.

By adding the -n hello-world-app argument, we're just letting Kubernetes know that we want to see the status of just the Deployments within the hello-world-app Namespace.

Note: if your Deployment's READY count sits at 0 for more than about 5-10 minutes, you may want to double check that you've correctly set the namespace in Fargate and your Deployment specification. Fargate will only deploy applications when it recognises the namespace.

Task 4: Prepare Application Load Balancer

Our Application Load Balancer (ALB) will be the publicly-accessible entrypoint to our Kubernetes pods. There are a few notable steps to getting this working:

  1. We must associate an OpenID Connect Provider within IAM.
  2. We must create a policy within IAM which will allow Kubernetes to deploy some ALBs into our AWS account.
  3. We must create a Service Account within IAM for our EKS Cluster, with the kube-system namespace, since that's where traffic actually comes into the system.

Once we've done those three steps, we should be almost ready to whip up a new AWS ALB and access our application.

Let's step through those three steps:

eksctl utils associate-iam-oidc-provider \
    --region eu-west-2 \
    --cluster sl-testing \
    --approve

This should associate a new OpenID Connect Provider.

Next, we need to grab a copy of Amazon's IAM policy document for the AWS Load Balancer Controller.

curl -o iam-policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json

Once you have a copy of that, we can create this policy within AWS.

Important: check IAM in case a policy like this already exists, which you may be conflicting with.

aws iam create-policy \
    --policy-name AWSLoadBalancerControllerIAMPolicy \
    --policy-document file://iam-policy.json

Next up, we need to create the service account and attach that policy. You will need your Account ID for this.

eksctl create iamserviceaccount \
  --cluster=sl-testing \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --attach-policy-arn=arn:aws:iam::<AWS_ACCOUNT_ID>:policy/AWSLoadBalancerControllerIAMPolicy \
  --approve
Replace <AWS_ACCOUNT_ID> with your AWS Account ID.

We will use Helm to quickly spin up a new aws-load-balancer-controller in our kube-system namespace. This will handle talking to AWS for us.

$ kubectl apply -k "github.com/aws/eks-charts/stable/aws-load-balancer-controller//crds?ref=master"
This will install Amazon's Custom Resource Definitions (CRDs) which add some additional functionality to Kubernetes.
$ helm repo add eks https://aws.github.io/eks-charts
This will add a Helm repository for EKS tooling. This is so Helm knows where to look for some of Amazon's Helm Charts.
$ helm upgrade -i aws-load-balancer-controller eks/aws-load-balancer-controller \
  --set clusterName=sl-testing \
  --set serviceAccount.create=false \
  --set serviceAccount.name=aws-load-balancer-controller \
  --set region=<region> \
  --set vpcId=<VPC ID> \
  -n kube-system
This will spin up an instance of aws-load-balancer-controller. Replace the <region> and <VPC ID> with yours, of course.

Task 5: Exposing Our Service

The final piece of the puzzle is to expose our service, thus making it accessible over an ALB.

Prepare Your Service Specification

The Service specification will define which ports to make accessible.

apiVersion: v1
kind: Service
metadata:
  name: hello-world-app
  namespace: hello-world-app
spec:
  selector:
    app: hello-world-app
  type: ClusterIP
  ports:
  - name: http
    port: 80
    targetPort: 8000
    protocol: TCP

So walking through this, we see:

  1. The Service is named after our application, though this doesn't necessarily need to be the case.
  2. We've stated that this Service specification will apply to anything with the app: hello-world-app label.
  3. We state the type of service will be ClusterIP, so that Kubernetes knows to give this particular service an IP of its own, rather than an option such as NodePort which will simply open a port on the Node the pod is sitting on.
  4. We then outline the ports, which just says that traffic on port 80/tcp will be sent to the Pod's port 8000/tcp.

Depending on how much you've read elsewhere, you may be tempted to set that type to LoadBalancer instead of ClusterIP. Note that this won't work with Fargate since we need an ALB, and setting it to LoadBalancer will deploy a Classic Load Balancer (CLB). These aren't compatible with Fargate.

Apply the Service Specification

Applying it, as always, is a breeze.

$ kubectl apply -f service.yaml
service/hello-world-app created

$ kubectl get services            
NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
hello-world-app   ClusterIP   10.100.186.216   <none>        80/TCP    39s

Prepare Your Ingress Specification

The Ingress will utilise the aws-load-balancer-controller to spin up a new ALB within AWS. Let's prepare the specification.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: hello-world-app
  namespace: hello-world-app
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
  labels:
    app: hello-world-app
spec:
  rules:
    - http:
        paths:
          - path: /*
            backend:
              serviceName: hello-world-app
              servicePort: 80

Here's what the Ingress specification is saying:

  1. We want to deploy an Ingress Controller (in our case, an ALB) called hello-world-app.
  2. We make it an ALB by defining kubernetes.io/ingress.class: alb.
  3. We make it have a public IP and hostname by defining alb.ingress.kubernetes.io/scheme: internet-facing.
  4. We make it speak to our Pods over IP by telling it to target by IP: alb.ingress.kubernetes.io/target-type: ip.
  5. This will load balance any services labelled with app: hello-world-app.
  6. We then tell the ALB that any path over HTTP should go to the Service called hello-world-app on port 80.

Apply the Ingress Specification

I'm sure you've guessed how we go about doing this by now.

$ kubectl apply -f ingress.yaml 
ingress.extensions/hello-world-app created

$ kubectl get ingress -n hello-world-app
NAME              CLASS    HOSTS   ADDRESS                                                                  PORTS   AGE
hello-world-app   <none>   *       k8s-hellowor-hellowor-808f26be44-483815596.eu-west-2.elb.amazonaws.com   80      19s

Wait, hold on! Is that a public hostname I see?!

Yep, it is! You should now be able to access your service. Finally.

Discuss with me!

I'm still learning this myself, so I do apologise if there are slight inaccuracies. I spent quite a lot of time talking to many people, including AWS Support and the Docker community, to try to give you something easily (sort of) digestible, with a reliably repeatable result.

Use the comments down below to share any questions you have, and I'll do my best to share any further reading, or update this post to answer those questions.

Happy orchestrating!