Containers

Announcing Java support for cdk8s

Today, we are announcing Java support for cdk8s, the Cloud Development Kit for Kubernetes. This rounds out support for the top 3 most popular programming languages and probably the single most used here at Amazon. Now, you can leave YAML behind and define your Kubernetes applications in Typescript, Python, and Java.

In this tutorial, we’ll learn how to use Java cdk8s to define a simple deployment of a load balancer service that will front our containerized SpringBoot application. We’ll be using kind for local Kubernetes development, but feel free to skip that section if you already have a cluster up and running. To keep things simple, we’ll define our infrastructure and our core business logic in the same package. To complete this tutorial, you’ll need to have Java installed on your computer as well as homebrew and kind.

Java cdk8s tutorial

Let’s start by installing the cdk8s CLI:

brew install cdk8s

We’ll use the quick start Java template for cdk8s:

cdk8s init java-app

This will set up everything you need to get started with Java. Go ahead and open up src/main/java/com/mycompany/app/Main.java.

You’ll see a Main class with an empty constructor telling you to // define resources here and we’ll do just that. We’ll be using cdk8s+, a construct library built on top of cdk8s.

For readers that are unfamiliar with constructs, constructs are the basic building blocks in CDK applications. In the AWS CDK, these refer to AWS cloud resources (i.e. S3 Bucket, DDB table, etc). In cdk8s, these refer to Kubernetes objects (i.e. Container, Deployment, etc).

Before we go too far into building our app, let’s walk through a quick example to give you a sense for how cdk8s works. We can start with creating a pod, the smallest unit in our Kubernetes application.

Inside the Main class constructor, let’s start with the Container that will be run on our pod:


import org.cdk8s.plus.Container;
import org.cdk8s.plus.ContainerProps;
import org.cdk8s.plus.Pod;
import org.cdk8s.plus.PodSpec;
import org.cdk8s.plus.PodProps;

.
.
.

// define resources here
final List<Container> containers = new ArrayList<>();
final Container container = new Container(new ContainerProps.Builder()
    .image("paulbouwer/hello-kubernetes:1.7")
    .port(8080)
    .name("hello-cdk8s")
    .build());

containers.add(container);

final PodSpec podSpec = new PodSpec.Builder()
    .containers(containers)
    .build();
final Pod pod = new Pod(this, "Pod", new PodProps.Builder()
    .spec(podSpec)
    .build());

Using cdk8s CLI, we should be able to synthesize this Java code into a valid Kubernetes manifest:

// Compile the java code
> mvn compile

// Synthesize the manifest
> cdk8s synth

In dist/, we’ll find our manifest that looks like this:

apiVersion: v1
kind: Pod
metadata:
  name: helloworld-pod-3799acf9
spec:
  containers:
    - env: []
      image: paulbouwer/hello-kubernetes:1.7
      name: hello-cdk8s
      ports:
        - containerPort: 8080
      volumeMounts: []
  volumes: []  

Now that we’ve seen how cdk8s works at a basic level, we can see some of the benefits:

  • You get the type safety of Java (you couldn’t compile or synthesize your manifest with any typos or errors).
  • You get hints from the IDE on what you need to put where, preventing you from making valid YAML but invalid manifests.

The higher level abstractions and familiar programming language can help you reduce the amount of repetitive and error-prone YAML that you may be used to writing.

Now let’s continue on and define our deployment. We can remove the line where we defined our Pod because we’ll let cdk8s-plus handle that by passing in the PodSpec to our Deployment.


import org.cdk8s.plus.Container;
import org.cdk8s.plus.ContainerProps;
import org.cdk8s.plus.PodSpec;
import org.cdk8s.plus.DeploymentSpec;
import org.cdk8s.plus.DeploymentProps;
import org.cdk8s.plus.Deployment;
import org.cdk8s.plus.ImagePullPolicy;

.
.
.

// define resources here
final List<Container> containers = new ArrayList<>();
final Container container = new Container(new ContainerProps.Builder()
    .image("helloworld")
    .port(8080)
    .imagePullPolicy(ImagePullPolicy.NEVER)
    .name("hello-cdk8s")
    .build());

containers.add(container);

final PodSpec podSpec = new PodSpec.Builder()
    .containers(containers)
    .build();

// final Pod pod = new Pod(this, "Pod", new PodProps.Builder().spec(podSpec).build());

final DeploymentSpec deploymentSpec = new DeploymentSpec.Builder()
    .replicas(3)
    .podSpecTemplate(podSpec)
    .build();
                                                           
final Deployment deployment = new Deployment(this, "Deployment", new DeploymentProps.Builder()
    .spec(deploymentSpec)
    .build());

Finally, we’ll expose our deployment through a LoadBalancer service on port 8080:


import org.cdk8s.plus.ExposeOptions;
import org.cdk8s.plus.ServiceType;

.
.
.
                                                     
deployment.expose(new ExposeOptions.Builder()
    .port(8080)
    .serviceType(ServiceType.LOAD_BALANCER)
    .build());   

Simply enough, we define some options for exposing our deployment behind a LoadBalancer Service on port 8080, and then we expose the Deployment with those options.

We’re now done with our infrastructure. You can see the full code here.

The next thing we’ll do is define our basic SpringBoot application. Since we’re doing it here in the same Maven project, let’s go ahead and open up our pom.xml again. We’ll want to add the SpringBoot parent pom and dependency:


<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.3.3.RELEASE</version>
  <relativePath/>
</parent>

<dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
  </depenency>
</dependencies>

And now, in the same package ( com.mycompany.app ) as our infrastructure, we’ll define our service code. It’ll look like this:


@SpringBootApplication
@RestController
public class Server {

  @RequestMapping("/")
  public String home() {
    return "Hello cdk8s!"
  }
  
  public static void main(String[] args) {
    SpringApplication.run(Server.class, args);
  }
}

This starts a SpringApplication server that returns “Hello cdk8s!” when we hit the “/” endpoint. We’re nearly finished! We just need to Dockerize our application so we can deploy it on our cluster. Let’s define our Dockerfile at the root of our project like so:


FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.mycompany.app.Server"]

Yay! We’re done writing code now. All that’s left is packaging and deploying. Let’s first compile our code:


> mvn compile

Next, let’s use the Dockerfile to create an image for our container:


> docker build -t helloworld .
.
.
.
Successfully tagged helloworld:latest

>

Great! So we’ve made an image and tagged it with “helloworld,” which, if we remember earlier in the tutorial, was what we put in for the image for our Container.

Note: at this point, if you are planning to use a container registry, please upload your container there. Additionally, make sure you edit the image in the Container you defined earlier and recompile with mvn compile. We will be using kind to run a local cluster later in this tutorial.

Let’s create a cluster with kind and load our image into it.


> kind create cluster

> kind load docker-image helloworld

The next thing we’ll want to do is synthesize our cdk8s code into a Kubernetes manifest. To synthesize here means that we’ll take the Java code we wrote and pass it through the cdk8s engine to create the YAML that Kubernetes expects. Let’s run this command:


> cdk8s synth

This will create a manifest under dist/helloworld.k8s.yaml and the contents should look something like this:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-deployment-pod-fa6869dc
spec:
  replicas: 3
  selector:
    matchLabels:
      cdk8s.deployment: helloworldDeployment2EB1B5D3
  template:
    metadata:
      labels:
        cdk8s.deployment: helloworldDeployment2EB1B5D3
    spec:
      containers:
        - env: []
          image: helloworld
          name: hello-k8s
          imagePullPolicy: Never
          ports:
            - containerPort: 8080
          volumeMounts: []
      volumes: []
---
apiVersion: v1
kind: Service
metadata:
  name: helloworld-deployment-service-pod-f13ce1c0
spec:
  externalIp: []
  ports:
    - port: 8080
      targetPort: 8080
  selector:
    cdk8s.deployment: helloworldDeployment2EB1B5D3
  type: LoadBalancer

If you already have your cluster all set up, you should be able to apply this manifest to it:


> kubectl apply -f dist/helloworld.k8s.yaml

Finally, to access our service, we’ll want to port-forward into our cluster. First, grab your Service name:


> kubectl get svc

And copy that and port-forward like so:


> kubectl port-forward service/helloworld-deployment-service-pod-f13ce1c0 8080 8080

And if we did everything right, you can see your service running by opening up localhost in your browser or by doing:


> curl localhost:8080
Hello cdk8s!

And just like that, we’re done! We were able to define both our business logic and our Kubernetes infrastructure in the same Java package. I hope that you learned something new in this tutorial. I’m excited to see all the amazing things developers build with Java cdk8s!

Enjoy! ?

TAGS: , ,
Campion Fellin

Campion Fellin

Campion is a Software Development Engineer for Amazon Explore and a frequent contributor to cdk8s. Campion is passionate about infrastructure and the developer experience. He lives in (and is from!) Seattle and enjoys activities like biking and kayaking.