Containers

How To Expose Multiple Applications on Amazon EKS Using a Single Application Load Balancer

Introduction

Microservices architectures are default for cloud-native applications. Greater granular scalability, isolation of functions, and the possibility of having independent teams working on specific application functionalities are just some of the reasons that such an architectural pattern has been widely adopted. Together with the fact that new applications are being borne in the cloud, you have the perfect match between modular and scalable architectures, with technologies offered by the cloud, that makes it easy to deploy, operate, and manage your microservices.

One pattern, several implementation options

At AWS, there are at least two common paths to be followed for microservices architecture implementation:

1) Move different components of an application or service into a container or a set of containers (also known as containerization) and delegate their management to a container orchestrator, like Amazon Elastic Kubernetes Service (Amazon EKS) and Amazon Elastic Container Service (Amazon ECS).

2) Move different components of an application or service into serverless functions, and delegate their management to AWS Lambda.

The next step is to expose each one of those microservices, regardless of whether they are containers or functions, through an endpoint so a client or an API can send requests and get responses. Usually, you’ll want every microservice to have its own endpoint. For example, every suffix (“/”) at the end of a web address will point to a different microservice:

www.example.com/service1 > microservice1
www.example.com/service2 > microservice2
www.example.com/service3 > microservice3

This type of load balancing or routing is known as path-based routing (routing based in the URL path).

One of the most important benefits of this approach is the low cost and simplicity that it has to expose dozens of applications. Both the Application Load Balancer (ALB) and the Amazon API Gateway support this feature. Therefore, with a single ALB or a single API Gateway, it is possible to expose your microservices running as containers with Amazon EKS or Amazon ECS or as serverless functions with AWS Lambda.

In this blog post, we are going to demonstrate how to expose your applications that are running as containers and being orchestrated by Amazon EKS through an Application Load Balancer.

Architecture and Components

Amazon EKS Cluster is the Kubernetes cluster where the application will run. Amazon EKS is an AWS service that removes the complexity of managing a Kubernetes’ control plane, which is made of API servers and etcd nodes, allowing developers to focus on the data plane, which is the actual servers (data nodes) running the application.

Auto Scaling Group is an AWS feature that allows Amazon EC2 instances to scale in and out, triggered by a monitoring metric. To scale the cluster, you’ll need to use the Cluster Autoscaler, which uses the Auto Scaling group on your behalf. For this demo, this feature will be only used to keep a minimum number of EC2 instances running in high availability. AWS Fargate is another option to run your pods at scale, instead of running those on EC2 instances. By running your containers as Fargate pods, you can also have your data plane fully managed by AWS.

Application Load Balancer (ALB) automatically distributes traffic between multiple targets like EC2 instances, containers, or IP addresses in one or more Availability Zones. An ALB works at layer 7 (Application) of the OSI model (HTTP/HTTPS) and supports, among many other features, path conditions, allowing you to define rules that can route requests based on the URL path (also known as path-based routing). ALB is one of the three load balancing options supported by Elastic Load Balancing.

Ingress is a Kubernetes resource that manages external traffic to the cluster, typically HTTP. An Ingress usually provides load balancing, SSL, and virtual hosting. In order to have the Ingress features in a cluster, you need to install an Ingress Controller.

AWS Load Balancer Controller is a controller that helps manage Elastic Load Balancers for Kubernetes clusters. For this scenario, we are using the Ingress kind to automatically provision an ALB and configure the routing rules needed for this ALB to be defined via Kubernetes manifests.

The final goal is to have different applications answering requests through different paths, but with a single ALB.

Requirements

 To implement this solution, you must have the following prerequisites:

  • An Amazon EKS cluster provisioned with node groups, if you choose to run your workloads on EC2 instances, or a Fargate profile if you choose to run them on AWS Fargate
  • The AWS Load Balancer Controller configured in your cluster, so that an Application Load Balancer is created when you create an Ingress in Kubernetes
  • The container images of your applications must be available in an Amazon ECR repository or some other repository of your choice. In this blog post, we will use container images registered in Amazon ECR, and we will demonstrate the step-by-step image creation process as well as its submission to the repository.

Environment creation

1. Application and Docker image creation process

Let’s first create our example applications and their respective Dockerfiles. In our case, as this is a demo environment, we will use an extremely simple HTML application that displays a yellow background and another one that displays a green background, just to simulate two microservices.

A) Create a directory called green and another one called yellow.

mkdir green/ yellow/

B) Copy the code from each application and save it as index.html in their respective directories.

Green application:

echo '<html style="background-color: green;"></html>' > green/index.html

Yellow application:

echo '<html style="background-color: yellow;"></html>' > yellow/index.html

C) Let’s now create the Dockerfiles in the same directories as the index.html files.

Understand the file: We will use an image base of Nginx with Alpine, we will create a directory for the application, and we will copy the index.html file to this directory. We will also expose our application on port 80 (HTTP).

Green application:

cat <<EOF > green/Dockerfile
FROM public.ecr.aws/nginx/nginx:1.20-alpine
RUN mkdir -p /usr/share/nginx/html/green
COPY ./index.html /usr/share/nginx/html/green/index.html 
EXPOSE 80
EOF

Yellow application:

cat <<EOF > yellow/Dockerfile
FROM nginx:alpine
RUN mkdir -p /usr/share/nginx/html/yellow
COPY ./index.html /usr/share/nginx/html/yellow/index.html 
EXPOSE 80
EOF

D) Let’s now create an Amazon ECR repository for each application and upload the respective Docker images.

Follow this documentation to create the green and yellow repositories for each of the applications.

aws ecr create-repository --repository-name green
aws ecr create-repository --repository-name yellow

E) Having created the repositories, access them and select the View push commands button.

Follow the step-by-step guide that will be displayed for your operating system to create the Docker image and upload it to the repository. In this example, we are using us-east-1 as the Region. Change the Region code if you are not using N. Virginia.

export AWS_REGION=$(aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]')
export AWS_REGISTRY_ID=$(aws ecr describe-registry --query registryId --output text)
export AWS_ECR_REPO=${AWS_REGISTRY_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com

aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ECR_REPO

cd yellow/
docker build . -t yellow
docker tag yellow:latest $AWS_ECR_REPO/yellow:latest
docker push $AWS_ECR_REPO/yellow:latest
cd ..

cd green/
docker build . -t green
docker tag green:latest $AWS_ECR_REPO/green:latest
docker push $AWS_ECR_REPO/green:latest
cd ..

Remember to perform this procedure for both applications.

2. Defining the environment configuration file in Kubernetes

Let’s now create a Kubernetes configuration file defining the objects in our environment.’

A) Create a file named color-app.yaml in your favorite text editor, edit it to your needs if necessary, and execute the command in your terminal. If you’re not using Amazon ECR, change the image data to the URL of your Docker image repository:

cat <<EOF > color-app.yaml 
apiVersion: v1
kind: Namespace
metadata:
  name: color-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: green-app
  namespace: color-app
  labels:
    app: green-app
spec:
  selector:
    matchLabels:
      app: green-app
  replicas: 2
  template:
    metadata:
      labels:
        app: green-app
    spec:
      containers:
      - name: green-container
        image: $AWS_ECR_REPO/green:latest
        ports:
            - containerPort: 80
        resources:
          limits:
            memory: "100Mi"
            cpu: "200m"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: yellow-app
  namespace: color-app
  labels:
    app: yellow-app
spec:
  selector:
    matchLabels:
      app: yellow-app
  replicas: 2
  template:
    metadata:
      labels:
        app: yellow-app
    spec:
      containers:
      - name: yellow-container
        image: $AWS_ECR_REPO/yellow:latest
        ports:
            - containerPort: 80
        resources:
          limits:
            memory: "100Mi"
            cpu: "200m"
EOF

Understand the file: First, we will create a namespace called color-app and two Deployments, one for each application. In these deployments, we will define two replicas, we will add some labels referencing each application, we will indicate the image, and we will define limits for memory and CPU resources.

B) Let’s now add two Services of the NodePort type to our file, one for each application. Services of the NodePort type serve to expose applications in the cluster so that they can be accessed by Ingress, which in our case is done through the Application Load Balancer (ALB) that is automatically created by the AWS Load Balancer Controller, mentioned in the prerequisites. Note that in Services, we are identifying each application through labels and selectors:

cat <<EOF >> color-app.yaml 
---
apiVersion: v1
kind: Service
metadata:
  namespace: color-app
  name: green-service
  labels:
    app: green-app
  annotations:
    alb.ingress.kubernetes.io/healthcheck-path: /green/index.html
spec:
  type: NodePort
  selector:
    app: green-app
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
  namespace: color-app
  name: yellow-service
  labels:
    app: yellow-app
  annotations:
    alb.ingress.kubernetes.io/healthcheck-path: /yellow/index.html
spec:
  type: NodePort
  selector:
    app: yellow-app
  ports:
    - port: 80
      targetPort: 80
EOF

C) Finally, let’s define our Ingress. As mentioned earlier, the Ingress function will be performed by ALB. With this condition, you have the advantage of not having to manage your Ingresses through Pods in your cluster. You will not consume cluster resources for that, and you will also be able to use the powerful features of ALB, such as automatic scalability, advanced security, and functionalities such as path-based routing (URL), which we’re demonstrating in this blog post.

cat <<EOF >> color-app.yaml 
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: color-app-ingress
  namespace: color-app
  labels:
    app: color-app
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
    alb.ingress.kubernetes.io/healthcheck-port: traffic-port
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: '15'
    alb.ingress.kubernetes.io/healthcheck-timeout-seconds: '5'
    alb.ingress.kubernetes.io/success-codes: '200'
    alb.ingress.kubernetes.io/healthy-threshold-count: '2'
    alb.ingress.kubernetes.io/unhealthy-threshold-count: '2'
spec:
  rules:
    - http:
        paths:
          - path: /yellow
            pathType: Prefix
            backend:
              service:
                name: yellow-service
                port:
                  number: 80                        
          - path: /green
            pathType: Prefix
            backend:
              service:
                name: green-service
                port:
                  number: 80
EOF

Understand the file: Note that we are defining Ingress annotations so that the Ingress is provisioned through a public ALB, traffic is routed directly to the Pods, and ALB health check characteristics are configured. Within the “spec” parameter, we are defining specific URL paths for each application and routing traffic to their respective Services.

3. Creating the environment in the Amazon EKS cluster

After creating the yaml file, we will deploy our application on the Amazon EKS cluster, through kubectl.

A) Go to the directory where the color-app.yaml file was saved and run the following command from a workstation that has access to the cluster through kubectl.

kubectl apply -f color-app.yaml

If you have questions about how to configure this access, follow this documentation for RBAC configuration and this one for local context configuration.

The environment will be provisioned in a few minutes.

B) Once ALB is provisioned, you can check the settings automatically made in ALB by going to the AWS Management Console in Amazon EC2 > Load Balancers > select the ALB > click the Listeners tab > click View/edit rules.

C) After the ALB is provisioned, run the command below and copy the DNS entry assigned to it. Paste the URL into your browser using /green or /yellow at the end, as shown in the images below:

kubectl get ingress color-app-ingress -n color-app -o=jsonpath="{'http://'}{.status.loadBalancer.ingress[].hostname}{'\n'}"

/green

/yellow

Next steps

Based on this example, you can now use the yaml file for your own workload. You can rename the Kubernetes labels and components (namespace, Deployments, Services, and Ingress) for your environment and use your own application’s Docker image to replace it in Deployment. Also, this walkthrough can be performed on a Kubernetes cluster that runs purely on Amazon EC2 instead of Amazon EKS, if that is your case.

Conclusion

In this blog post, we saw that there are different ways to approach a microservices architecture within AWS. We then explained some main concepts of the approach using containers. Finally, we demonstrated, in a step-by-step procedure, how to implement it in a simple and cost-effective way using Amazon EKS with a single Application Load Balancer.

If you wish, you could also achieve the same results by using several Ingress objects pointing to the same ALB using the annotation “alb.ingress.kubernetes.io/group.name”. In this case, you would create individual Ingresses and add a common name to this annotation.

By using this approach, different teams can be completely independent from each other because they can deploy and manage their own services and ingresses while relying on the ALB.

JP Santana

JP Santana

João Paulo (JP) Santana is a Principal Solutions Architect at AWS. He spent most of his life in São Paulo, Brazil and now lives in Orlando, FL where he works with our customers in the Southeast region of the US. In his spare time, he enjoys spending time with his wife and three kids, grilling a good Brazilian steak, or practicing Brazilian Jiu Jitsu.

Rubens Devito

Rubens Devito

With 16 years of IT experience and 7 years as a cloud professional, Rubens has been helping companies from all verticals and sizes architect their workloads to AWS. Currently, Rubens is leading the Startup Solutions Architecture team in Brazil, with a goal of making new startups become the next enterprises of the future through AWS.