HelloWorld: Micronaut, Docker, GraalVM, Kubernetes and Helm

No, it’s not a buzzword bingo, but a HelloWorld example.

Many examples of Micronaut projects can be found online, but they almost all focus on Micronaut features. The project described here focuses on the deployment of a Micronaut microservice: it shows the steps to wrap a Micronaut microservice into a configurable Helm chart, ready for integration into your CI/CD pipelines.

This example:

  • builds a simple Micronaut microservice
  • builds a Docker / GraalVM image
  • builds a Helm chart
  • shows how configuration items can be handled during all these steps

Technologies

The technologies used are:

The source code can be found in GitHub: lgorissen/hello-micronaut

If you want to follow this blog hands-on, it is recommended to download the GitHub archive and start from there.

In the remainder of this blog it’s assumed that you have installed all of the above technologies.

1. Micronaut service

1.1 Create project

If you start from the GitHub archive, you don’t have to follow this step. If you start from scratch, then first create a Micronaut project:

mn create-app hello-micronaut

Complete the Micronaut microservice by adding the HelloMicronautController.java class in directory hello-micronaut/src/main/java/hello/micronaut:

  import io.micronaut.context.annotation.Property;

  import io.micronaut.http.MediaType;
  import io.micronaut.http.annotation.Controller;
  import io.micronaut.http.annotation.Post;

  @Controller("/hellomicronaut")
  public class HelloMicronautController {

      @Property(name = "hello.greeting")
      String greeting;

      @Property(name = "hello.location")
      String location;

      @Post(consumes = {MediaType.APPLICATION_JSON}, produces = {MediaType.TEXT_PLAIN})
      public String sayHelloMicronaut(String name) {
          return greeting + " " + name + " from " + location;
      }
}

Note: configuration properties greeting and location are NOT declared as private. Declaring them as private will result in failure. For more information, see paragraph 2.3: ‘Run Docker container’.

The two configuration properties also must be defined in the application.yml file (directory hello-micronaut/src/main/resources):

  micronaut:
    server:
      port: 8090
    application:
      name: helloMicronaut
  hello:
    greeting: Hello
    location: development world

We also changed the server port into 8090 … for no particular reason ????.

1.2 Run and test project locally

Run:

./gradlew run 

Test:

curl  -H 'Content-type: application/json' localhost:8090/hellomicronaut -d '{"name": "Luc"}'

2. Build GraalVM Docker image

The next step is to build a Docker image that has a GraalVM Micronaut application in it. Micronaut has made this fairly simple: no messing around with doing your own GraalVM installation etc., just a simple Gradle task.

2.1 Build GraalVM Docker image

Build GraalVM Docker image: Note: This can take long time, in my case 2m41s, but may well go up to 10 minutes…

./gradlew dockerBuildNative

> Task :compileJava

Note: Writing resource-config.json file to destination: META-INF/native-image/hello.micronaut/hello-micronaut/resource-config.json
Note: Creating bean classes for 1 type elements

> Task :dockerfileNative
Dockerfile written to: /home/developer/projects/hello-micronaut/hello-micronaut/build/docker/DockerfileNative

> Task :dockerBuildNative
Building image using context '/home/developer/projects/hello-micronaut/hello-micronaut'.
Using Dockerfile '/home/developer/projects/hello-micronaut/hello-micronaut/build/docker/DockerfileNative'
Using images 'hello-micronaut'.
Step 1/11 : FROM oracle/graalvm-ce:20.3.0-java11 AS graalvm
 ---> 0b7b0c4bfba5
Step 2/11 : RUN gu install native-image
 ---> Using cache
 ---> 3587e8ce6778
Step 3/11 : WORKDIR /home/app
 ---> Using cache
 ---> b261c52e901c
Step 4/11 : COPY build/layers/libs /home/app/libs
 ---> Using cache
 ---> 9bef7a42e3b4
Step 5/11 : COPY build/layers/resources /home/app/resources
 ---> 28b4af8808ae
Step 6/11 : COPY build/layers/application.jar /home/app/application.jar
 ---> 0b8b092a8f8f
Step 7/11 : RUN native-image -H:Class=hello.micronaut.Application -H:Name=application --no-fallback -cp /home/app/libs/*.jar:/home/app/resources:/home/app/application.jar
 ---> Running in b8f29a4420ce
[application:26]    classlist:   5,079.35 ms,  0.96 GB
[application:26]        (cap):     920.06 ms,  0.96 GB
[application:26]        setup:   2,948.00 ms,  0.96 GB
[application:26]     (clinit):   1,212.49 ms,  4.03 GB
[application:26]   (typeflow):  25,541.89 ms,  4.03 GB
[application:26]    (objects):  30,267.57 ms,  4.03 GB
[application:26]   (features):   3,022.89 ms,  4.03 GB
[application:26]     analysis:  62,317.35 ms,  4.03 GB
[application:26]     universe:   2,645.39 ms,  4.03 GB
[application:26]      (parse):   9,783.03 ms,  4.42 GB
[application:26]     (inline):   7,042.68 ms,  5.19 GB
[application:26]    (compile):  44,759.87 ms,  5.26 GB
[application:26]      compile:  65,952.92 ms,  5.26 GB
[application:26]        image:   9,048.58 ms,  5.18 GB
[application:26]        write:   1,131.09 ms,  5.18 GB
[application:26]      [total]: 149,422.61 ms,  5.18 GB
Removing intermediate container b8f29a4420ce
 ---> 7b0187dda5e4
Step 8/11 : FROM frolvlad/alpine-glibc
 ---> f6858800cf89
Step 9/11 : RUN apk update && apk add libstdc++
 ---> Using cache
 ---> 14f4843a53ac
Step 10/11 : COPY --from=graalvm /home/app/application /app/application
 ---> fc8770574b87
Step 11/11 : ENTRYPOINT ["/app/application"]
 ---> Running in 19dd53aad41f
Removing intermediate container 19dd53aad41f
 ---> 81c10a2a3d4c
Successfully built 81c10a2a3d4c
Successfully tagged hello-micronaut:latest
Created image with ID '81c10a2a3d4c'.

BUILD SUCCESSFUL in 2m 41s
6 actionable tasks: 5 executed, 1 up-to-date

2.2 Check for Docker image

Check for the Docker image and note the size of the image:

docker images | grep -i hello-micronaut

2.3 Run Docker container

Note

The current Micronaut Gradle DockerBuild does NOT include the default configuration file application.yml.
So, when running the built container, the application properties have to be added.

Below, the application.yml file is mounted and the environment variable MICRONAUT_CONFIG_FILES points to it:

docker run -d -v "$(pwd)"/src/main/resources/application.yml:/app/application.yml --env MICRONAUT_CONFIG_FILES=/app/application.yml -p8020:8090 --name my-hello-micronaut hello-micronaut

Test it:

curl  -H 'Content-type: application/json' localhost:8020/hellomicronaut -d '{"name": "Luc"}'

Note: As described above, the configuration properties greeting and location must NOT be declared private.

Declaring them as private will give the following run-time error upon processing a request:

{"message":"Internal Server Error: Failed to inject value for field [greeting] of class: hello.micronaut.HelloMicronautController\n\nMessage: Error setting field value: No field 'greeting' found for type: hello.micronaut.HelloMicronautController\n"}

Stop the container:

docker stop my-hello-micronaut

2.4 Upload the Docker image to Docker Hub

Tag the image and upload to Docker Hub:

docker login --username=lgorissen

docker images | grep  hello-micronaut

docker tag hello-micronaut:latest lgorissen/hello-micronaut:1.0.0

docker push lgorissen/hello-micronaut:1.0.0

Schrödinger’s warning: I know that I don’t have to warn you to use your own Docker Hub account. Meaning of course your Docker account and image name will be different.

2.5 Remove local Docker containers and images

Remove all local Docker artefacts:

docker rm my-hello-micronaut

docker rmi hello-micronaut:latest

docker rmi lgorissen/hello-micronaut:1.0.0

3. Helm chart

Finally, a Helm chart has to be made that has configuration options.

3.1 Set-up

The Helm chart is in the the hello-micronaut/k8s/charts directory.

The directory structure for the Helm chart is:

hello-micronaut/k8s/charts/hello-micronaut/Chart.yml
                                           templates/comfigmap.yml
                                                     deployment.yml
                                                     service.yml
                                           values.yml
  • If you started from scratch, just copy the files from the GitHub archive.
  • If you started with the GitHub archive, the files are already present.

The Helm chart deploys the following Kubernetes resources:

  • ConfigMap: with the yml configuration file for the Micronaut application.
  • Deployment:
    • deploys a number of Pods with the Docker image that was uploaded to Docker Hub
    • mounts the ConfigMap, thus putting the configuration file in /conf/application.yml
    • starts the application with the argument -Dmicronaut.config.files=/conf/application.yml
  • Service: for access to the Deployment’s Pods.

3.2 Running the Helm chart

The Helm chart is customized in the values.yml file:

name: hello-micronaut
deployment:
    replicas: 1
    image: lgorissen/hello-micronaut:1.0.0
    imagePullPolicy: Always
    containerPort: 8090
    greeting: Welcome
    location: on Terra10-micronaut
service:
    port: 8020

Run the Helm chart with the below command in the right directory: hello-micronaut/k8s/charts/hello-micronaut

helm install my-hello-micronaut --values values.yml

Verify the Pod is running with kubectl get pod.

3.3 Testing the Helm chart

In order to test the Helm chart, first port-forward the hello-micronaut service:

kubectl port-forward service/hello-micronaut 8020:8020

And then test with:

curl  -H 'Content-type: application/json' localhost:8020/hellomicronaut -d '{"name": "Luc"}'

By now, we have a Helm chart that can deploy a Micronaut service, including its configuration, to a Kubernetes cluster, using a GraalVM image.