Remotely Debug your Java Application on Kubernetes with IntelliJ IDEA

A debugger is a powerful tool that allows you to easily inspect the program state in a step-by-step manner. We Java developers are used to debugging programs with the IDE debugger when facing a bug or if we just want to understand how the application works and behaves. Well, that’s if you were able to run the application locally! For a bunch of reasons, sometimes you won’t be able to run the application locally. But then, how can you attach a debugger to your application if you can’t run it locally?

What if you want to inspect the behaviour of an external system that your application depends on and poke around it, but you don’t have access to it on your machine? A remote debugger can do that!

With Java remote debugging, you can attach a debugger to your application whether it’s deployed on a local Kubernetes cluster or a remote one, say on GKE or OpenShift Container Platform, and this is all powered by the Java Debug Wire Protocol.

In this post, we will deploy a Java application on Kubernetes, and we will see how to attach a debugger to a remote JVM.

Whether your cluster is remote or local, the following steps are the same.

Build the image

My Java app is a simple Spring Boot application that provides a REST endpoint:

@RestController
class MyController {

	private final Logger LOGGER = LoggerFactory.getLogger(MyController.class);

	@GetMapping("/debugme")
	public ResponseEntity<String> debugMe() {
		LOGGER.info("entering debugMe rest api");
		return ResponseEntity.ok("Hello World");
	}
}

In my pom.xml, I’ve added the Jib plugin to build the image:

<plugin>
  <groupId>com.google.cloud.tools</groupId>
  <artifactId>jib-maven-plugin</artifactId>
  <version>3.3.1</version>
  <configuration>
    <from>
      <image>openjdk:17</image>
    </from>
    <to>
      <image>docker.io/hamzablm/${project.name}:${project.version}</image>
    </to>
  </configuration>
  <executions>
    <execution>
      <phase>verify</phase>
      <goals>
        <goal>build</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Deploy the image

I’ve created my k8s deployment file to deploy the image:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: debugme
  name: debugme
spec:
  replicas: 1
  selector:
    matchLabels:
      app: debugme
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: debugme
    spec:
      containers:
        - image: docker.io/hamzablm/debugme:0.0.1-SNAPSHOT
          name: debugme
          resources: {}
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
            - name: debug
              containerPort: 5005
              protocol: TCP
          env:
            - name: JAVA_TOOL_OPTIONS
              value: '-Xdebug -agentlib:jdwp=transport=dt_socket,address=0.0.0.0:5005,server=y,suspend=n'

In the container ports section, I’ve exposed two ports in my Pod: One for the spring boot application(8080) and the other for the target VM(5005).

I’ve also added JAVA_TOOL_OPTIONS environment variable, this will enable the JVM to listen on port 5005 for incoming TCP connection requests from the debugger.

Also, note that I’ve set suspend to n since I don’t want to wait for the debugger to attach before executing the main class.

Finally, deploy that image in k8s with kubectl apply -f dep.yaml

Side Note:

If you’re using a CLI utility that manipulates JAVA_TOOL_OPTIONS environment variable like keytool to import certs, make sure to run in on an initContainer, so the CLI utility doesn’t override JAVA_TOOL_OPTIONS environment variable that we’ve set.

Debug the application

Now the image is deployed on k8s, we can check the logs with kubectl logs <pod-name>:

We can see that the pod is started, and the VM is listening on port 5005.

Forward local port 5005 to the pod’s 5005 port:

kubectl port-forward deploy/debugme 5005:debug

Create “Remote JVM Debug” run configuration:

This config comes with these defaults if you’re on JDK 9 or later:

From the config, you can see that we’re hitting on port 5005 that we’ve exposed locally with the port-forward command.

Now let’s port forward to the Spring Boot application so we can access our application locally:

kubectl port-forward deploy/debugme 8080:http

Now let’s send a request to the app and debug it remotely:

Here I was able to attach my IDE debugger to the VM running on k8s. Of course, I have all the power that comes with a debugger, so I can do advanced stuff like evaluate expressions on the fly, use field watchpoint, use an exception breakpoint, set up conditions in breakpoints etc.

Wrap Up

In this post, we saw how we can attach IntelliJ IDEA’s debugger to a remote JVM process running on k8s.
You can get the demo code in this Github repository.

If you happen to find these articles useful, you can buy me a coffee.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s