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:
- Your experience with Kubernetes is non-existent
- Your experience with EKS is non-existent
- Your experience with Fargate is non-existent
- Your experience with any form of Docker orchestration is non-existent
- 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:
- Amazon Web Services CLI (awscli) for connecting to our AWS account
- Elastic Kubernetes Service Controller (eksctl) for preparing our Kubernetes environment within AWS
- Kubernetes Controller (kubectl) for managing our workloads on Kubernetes
- 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:
- 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.
- 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:
- Deploy the infrastructure (VPC, Subnets, Security Groups, IAM Roles, IAM Policies, IAM Service Accounts, EKS Clusters, Fargate Profiles, and more if needed)
- Deploy the application
- 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.
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:
kindindicates to eksctl what we're trying to deploy exactly. In our case, this is
ClusterConfigbecause we wish to deploy a cluster into EKS.
- The metadata's
namewill appear in most aspects of this deployment. Give it something meaningful, for example, you may call it
webapp-productionif you're deploying something called
- The fargateProfile's
namewill 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.
kube-systemnamespace is where various internal bits Kubernetes runs will be kept, such as the DNS service.
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:
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.
- We're letting Kubernetes know that we wish to deploy an application called
hello-world-appby defining the
- We're also stating that this application should run in the
hello-world-appNamespace by defining the
- We specify the number of pods to create (replicas). This is how you would scale up (or down) your application.
- We apply some labels, which become useful later when we look at deploying Services.
- 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:latestimage, and expose port
8000with 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
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
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:
- We must associate an OpenID Connect Provider within IAM.
- We must create a policy within IAM which will allow Kubernetes to deploy some ALBs into our AWS account.
- 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.
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.
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:
- The Service is named after our application, though this doesn't necessarily need to be the case.
- We've stated that this Service specification will apply to anything with the
- 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
NodePortwhich will simply open a port on the Node the pod is sitting on.
- 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:
- We want to deploy an Ingress Controller (in our case, an ALB) called
- We make it an ALB by defining
- We make it have a public IP and hostname by defining
- We make it speak to our Pods over IP by telling it to target by IP:
- This will load balance any services labelled with
- We then tell the ALB that any path over HTTP should go to the Service called
hello-world-appon 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.