Deploy a Micronaut Microservices Application to the Amazon Elastic Kubernetes Service

This guide shows how to deploy a Micronaut® application, consisting of three microservices, to the Amazon Elastic Kubernetes Service (EKS) using the Micronaut Kubernetes project.

The Micronaut Kubernetes project provides integration between Micronaut and Kubernetes. It adds support for the following features:

  • Service Discovery
  • Configuration client for config maps and secrets
  • Kubernetes blocking and non-blocking clients built on top of the official Kubernetes Java SDK

EKS is a managed Kubernetes service for deploying containerized applications to the cloud. The guide demonstrates how to use Kubernetes Service Discovery and Distributed Configuration to connect three microservices, and discover how Micronaut integration with Kubernetes simplifies deployment to EKS. The application consists of three microservices:

  • users - contains customer data that can place orders on items, also a new customer can be created. It requires HTTP basic authentication to access it.
  • orders - contains all orders that customers have created as well as available items that customers can order. This microservice also enables the creation of new orders. It requires HTTP basic authentication to access it.
  • api - acts as a gateway to the orders and users microservices. It combines results from both microservices and checks data when a customer creates a new order.

Prerequisites #

A note regarding your development environment

Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit extension.

Note: If you use IntelliJ IDEA, enable annotation processing.

Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.

1. Create or Download a Microservices Application #

You can create a microservices application from scratch by following this guide, or you can download the completed example:

The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.

Note: By default, a Micronaut application detects its runtime environment. A detected environment (in this case, k8s) overrides the default specified environment (in this case, ec2). This means that you should locate your configuration in the application-k8s.properties and bootstrap-k8s.properties files. Alternatively, you can specify the ec2 environment passing it as a command-line option (-Dmicronaut.environments=ec2) or via an environment variable (MICRONAUT_ENVIRONMENTS=ec2).

2. Prepare to Deploy Microservices #

2.1. Export Environment Variables #

Define some environment variables to make deploying process easier:

  • AWS_ACCOUNT_ID to store your AWS account id.
  • AWS_REGION to store AWS region code.
  • EKS_CLUSTER_NAME to store the name of your cluster.

For example:

export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query \"Account\" --output text)"
export AWS_REGION="$(aws configure get region)"
export EKS_CLUSTER_NAME="gdk-k8s"

2.2. Authenticate to Amazon Elastic Container Registry #

Run the next command to log in to Amazon Elastic Container Registry (ECR):

aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

2.3. Create and Publish a Container Image of the Native Users Microservice #

To create a container image of the native users microservice named “users”, run the following command from the users directory:

./gradlew dockerBuildNative

Note: If you encounter problems creating a container image, run the following command from the users/build/docker/native-main/ directory:

docker build . -t users-aws -f DockerfileNative
./mvnw package -Dpackaging=docker-native -Pgraalvm

Note: If you encounter problems creating a container image, run the following command from the users/target/ directory:

docker build . -t users-aws -f Dockerfile

Note: Ensure that you construct container images for the correct CPU architecture. For instance, if you are using AArch64, modify the DOCKER_DEFAULT_PLATFORM environment variable to the value linux/amd64. Alternatively, you have the option to use AArch64 instances within your Kubernetes cluster.

2.4. Update the Users Microservice #

Edit the file named users/k8s-aws.yml as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: gdk-k8s
  name: users
spec:
  selector:
    matchLabels:
      app: users
  template:
    metadata:
      labels:
        app: users
    spec:
      serviceAccountName: gdk-service
      containers:
        - name: users
          image: '<aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/users:latest' # <1>
          imagePullPolicy: Always # <2>
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
            failureThreshold: 10
          env:
            - name: MICRONAUT_ENVIRONMENTS
              value: ec2
---
apiVersion: v1
kind: Service
metadata:
  namespace: gdk-k8s
  name: users
spec:
  selector:
    app: users
  type: NodePort
  ports:
    - protocol: TCP
      port: 8080

1 The URI of the repository that you created in ECR. Change the to your region and change the to your AWS account id. You can achieve this by running sed -i'' -e "s/<aws-region>/$AWS_REGION/" users/k8s-aws.yml and sed -i'' -e "s/<aws-account-id>/$AWS_ACCOUNT_ID/" users/k8s.yml, respectively.

2 Change imagePullPolicy to Always.

2.5. Create a Container Repository for the Users Microservice #

Your next step is to create a container repository.

  1. Create a Container Repository in your account:

     export USERS_REPOSITORY=$(aws ecr create-repository --repository-name users --image-scanning-configuration scanOnPush=true --region $AWS_REGION --output json | jq -r .repository.repositoryUri)
    
  2. Tag an existing users microservice container image:

     docker tag users-aws:latest ${USERS_REPOSITORY}:latest
    
  3. Push the tagged users microservice container image to the remote repository:

     docker push ${USERS_REPOSITORY}:latest
    

2.6. Create and Publish a Container Image of the Native Orders Microservice #

To create a container image of the native orders microservice named “orders”, run the following command from the orders/ directory:

./gradlew dockerBuildNative

Note: If you encounter problems creating a container image, run the following command from the orders/build/docker/native-main/ directory:

docker build . -t orders-aws -f DockerfileNative
./mvnw package -Dpackaging=docker-native -Pgraalvm

Note: If you encounter problems creating a container image, run the following command from the orders/target/ directory:

docker build . -t orders-aws -f Dockerfile

Note: Ensure that you construct container images for the correct CPU architecture. For instance, if you are using AArch64, modify the DOCKER_DEFAULT_PLATFORM environment variable to the value linux/amd64. Alternatively, you have the option to use AArch64 instances within your Kubernetes cluster.

2.7. Update the Orders Microservice #

Edit the file named orders/k8s-aws.yml as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: gdk-k8s
  name: orders
spec:
  selector:
    matchLabels:
      app: orders
  template:
    metadata:
      labels:
        app: orders
    spec:
      serviceAccountName: gdk-service
      containers:
        - name: orders
          image: '<aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/orders:latest' # <1>
          imagePullPolicy: Always # <2>
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
            failureThreshold: 10
          env:
            - name: MICRONAUT_ENVIRONMENTS
              value: ec2
---
apiVersion: v1
kind: Service
metadata:
  namespace: gdk-k8s
  name: orders
spec:
  selector:
    app: orders
  type: NodePort
  ports:
    - protocol: TCP
      port: 8080

1 The URI of the repository that you created in ECR. Change the to your region and change the to your AWS account id. You can achieve this by running sed -i'' -e "s/<aws-region>/$AWS_REGION/" orders/k8s-aws.yml and sed -i'' -e "s/<aws-account-id>/$AWS_ACCOUNT_ID/" orders/k8s-aws.yml, respectively.

2 Change imagePullPolicy to Always.

2.8. Create a Container Repository for the Orders Microservice #

  1. Create a container repository in your account:

     export ORDERS_REPOSITORY=$(aws ecr create-repository --repository-name orders --image-scanning-configuration scanOnPush=true --region $AWS_REGION --output json | jq -r .repository.repositoryUri)
    
  2. Tag an existing orders microservice container image:

     docker tag orders-aws:latest ${ORDERS_REPOSITORY}:latest
    
  3. Push the tagged orders microservice container image to the remote repository:

     docker push ${ORDERS_REPOSITORY}:latest
    

2.9. Create and Publish a Container Image of the Native API (Gateway) Microservice #

To build a container image of the native api microservice named “api”, run the following command from the api/ directory:

./gradlew dockerBuildNative

Note: If you encounter problems creating a container image, run the following command from the api/build/docker/native-main/ directory:

docker build . -t api-aws -f DockerfileNative
./mvnw package -Dpackaging=docker-native -Pgraalvm

Note: If you encounter problems creating a container image, run the following command from the api/target/ directory:

docker build . -t api-aws -f Dockerfile

Note: Ensure that you construct container images for the correct CPU architecture. For instance, if you are using AArch64, modify the DOCKER_DEFAULT_PLATFORM environment variable to the value linux/amd64. Alternatively, you have the option to use AArch64 instances within your Kubernetes cluster.

2.10. Update the API Microservice #

Edit the file named api/k8s-aws.yml as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: gdk-k8s
  name: api
spec:
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      serviceAccountName: gdk-service
      containers:
        - name: api
          image: '<aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/api:latest' # <1>
          imagePullPolicy: Always # <2>
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
            failureThreshold: 10
          env:
            - name: MICRONAUT_ENVIRONMENTS
              value: ec2
---
apiVersion: v1
kind: Service
metadata:
  namespace: gdk-k8s
  name: api
spec:
  selector:
    app: api
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 8080

1 The URI of the repository that you created in ECR. Change the to your region and change the to your aws account id. You can achieve this by running sed -i'' -e "s/<aws-region>/$AWS_REGION/" api/k8s-aws.yml and sed -i'' -e "s/<aws-account-id>/$AWS_ACCOUNT_ID/" api/k8s-aws.yml, respectively.

2 Change imagePullPolicy to Always.

2.11. Create a Container Repository for the API Microservice #

  1. Create a Container Repository in your account:

     export API_REPOSITORY=$(aws ecr create-repository --repository-name api --image-scanning-configuration scanOnPush=true --region $AWS_REGION --output json | jq -r .repository.repositoryUri)
    
  2. Tag an existing api microservice container image:

     docker tag api-aws:latest ${API_REPOSITORY}:latest
    
  3. Push the tagged api microservice container image to the remote repository:

     docker push ${API_REPOSITORY}:latest
    

3. Deploy Microservices to EKS #

  1. Create a directory for a kubectl configuration:

     mkdir -p $HOME/.kube
    
  2. Generate a kubectl configuration for authentication to EKS:

     aws eks update-kubeconfig --region $AWS_REGION --name $EKS_CLUSTER_NAME
    
  3. Set KUBECONFIG to the created config file, as shown below. (This variable is consumed by kubectl.)

     export KUBECONFIG=$HOME/.kube/config
    
  4. Deploy the auth.yml file that you created in the Deploy a Micronaut Microservices Application to a Local Kubernetes Cluster guide:

     kubectl apply -f auth.yml
    
  5. Deploy the users microservice:

     kubectl apply -f users/k8s-aws.yml
    
  6. Deploy the orders microservice:

     kubectl apply -f orders/k8s-aws.yml
    
  7. Run the next command to deploy the api microservice:

     kubectl apply -f api/k8s-aws.yml
    

4. Test Integration Between the Microservices Deployed to EKS #

  1. Run the following command to check the status of the pods and make sure that all of them have the status “Running”:
     kubectl get pods -n=gdk-k8s
    
     NAME                      READY   STATUS    RESTARTS   AGE
     api-6fb4cd949f-kxxx8      1/1     Running   0          2d1h
     orders-595887ddd6-6lzp4   1/1     Running   0          2d1h
     users-df6f78cd7-lgnzx     1/1     Running   0          2d1h
  2. Run this command to check the status of the microservices:

     kubectl get services -n=gdk-k8s
    
     NAME         TYPE           CLUSTER-IP       EXTERNAL-IP             PORT(S)             AGE
     api          LoadBalancer   10.100.208.154   <redacted>              8080:31171/TCP      20m
     orders       NodePort       10.100.157.155   <none>                  8080:30742/TCP      20m
     users        NodePort       10.100.126.97    <none>                  8080:31580/TCP      20m

    If EXTERNAL-IP is in a <pending> state, wait a couple of seconds and then run the command again. AWS will provide you with a hostname instead of an IP address. To access your application you will have to wait until DNS can resolve the hostname.

  3. Retrieve the URL of the api microservice and set it as the value of the $API_URL environment variable:

     export API_URL=http://$(kubectl get svc api -n=gdk-k8s -o json | jq -r '.status.loadBalancer.ingress[0].hostname'):8080
    
  4. Run a curl command to create a new user via the api microservice:

     curl -X "POST" "$API_URL/api/users" \
          -H 'Content-Type: application/json; charset=utf-8' \
          -d '{ "first_name": "Nemanja", "last_name": "Mikic", "username": "nmikic" }'
    

    Your output should look like:

     {
       "id":1,
       "username":"nmikic",
       "first_name":"Nemanja",
       "last_name":"Mikic"
     }
    
  5. Run a curl command to create a new order via the api microservice:

     curl -X "POST" "$API_URL/api/orders" \
          -H 'Content-Type: application/json; charset=utf-8' \
          -d '{ "user_id": 1, "item_ids": [1,2] }'
    

    Your output should include details of the order, as follows:

     {
       "id": 1,
       "user": {
         "first_name": "Nemanja",
         "last_name": "Mikic",
         "id": 1,
         "username": "nmikic"
       },
       "items": [
         {
           "id": 1,
           "name": "Banana",
           "price": 1.5
         },
         {
           "id": 2,
           "name": "Kiwi",
           "price": 2.5
         }
       ],
       "total": 4.0
     }
    
  6. Run a curl command to list the orders:

     curl "$API_URL/api/orders" \
          -H 'Content-Type: application/json; charset=utf-8'
    

    You should see output that is similar to the following:

     [
       {
         "id": 1,
         "user": {
           "first_name": "Nemanja",
           "last_name": "Mikic",
           "id": 1,
           "username": "nmikic"
         },
         "items": [
           {
             "id": 1,
             "name": "Banana",
             "price": 1.5
           },
           {
             "id": 2,
             "name": "Kiwi",
             "price": 2.5
           }
         ],
         "total": 4.0
       }
     ]
    
  7. Try to place an order for a user who does not exist (with id 100). Run a curl command:

     curl -X "POST" "$API_URL/api/orders" \
          -H 'Content-Type: application/json; charset=utf-8' \
          -d '{ "user_id": 100, "item_ids": [1,2] }'
    

    You should see the following error message:

     {
       "message": "Bad Request",
       "_links": {
         "self": [
           {
             "href": "/api/orders",
             "templated": false
           }
         ]
       },
       "_embedded": {
         "errors": [
           {
             "message": "User with id 100 doesn't exist"
           }
         ]
       }
     }
    

5. Clean Up Resources #

To delete all Kubernetes resources that were created in this guide, run this command:

kubectl delete namespaces gdk-k8s

Delete the gdk-k8s/users artifacts container repository:

aws ecr delete-repository --repository-name users --force

Delete the gdk-k8s/orders artifacts container repository:

aws ecr delete-repository --repository-name orders --force

Delete the gdk-k8s/api artifacts container repository:

aws ecr delete-repository --repository-name api --force

Note: If you were following the Getting started with Amazon EKS – AWS Management Console and AWS CLI and you want to clean up everything, don’t forget to follow the Step 5: Delete resources chapter.

Summary #

This guide demonstrated how to use Kubernetes Service Discovery and Distributed Configuration, provided with the Micronaut Kubernetes integration, to connect three microservices, and to deploy these microservices to a Kubernetes cluster in the Amazon Elastic Kubernetes Service.