grupoarrfug.com

Effortless Remote Debugging for Python Apps in Kubernetes

Written on

Introduction to Remote Debugging

Imagine you have several Python applications running on Kubernetes that need to communicate with each other. Occasionally, a bug appears when accessing a specific API endpoint, but it can't be replicated in your local environment. Wouldn’t it be great if you could connect to the application processes running remotely, set breakpoints, and debug in real time? Well, you can!

In this guide, we’ll set up a system for remote debugging Python applications in Kubernetes, enabling you to step through your code and troubleshoot issues without modifying your codebase or deployment configurations.

Defining Our Objectives

Before we dive into debugging, let’s clarify our goals:

  1. Avoid any changes to the application code.
  2. Maintain the integrity of application security.
  3. Ensure we are debugging the actual remote code, with no traffic rerouting to local environments.
  4. Set breakpoints and step through code effectively.
  5. Debug multiple containers or Pods simultaneously, as microservices often interact.
  6. Create a straightforward and efficient setup.

Note: This tutorial takes inspiration from a KubeCon presentation titled “Breakpoints in Your Pod: Interactively Debugging Kubernetes Applications,” originally focused on Go applications, yet applicable to our Python context.

Setting Up the Environment

To begin debugging, we need to create and deploy a couple of applications. We will utilize a Minikube cluster for this tutorial:

minikube start --kubernetes-version=v1.26.3

We will deploy two Python applications to demonstrate debugging across multiple containers. You can find the sample application code in the provided repository, structured as follows:

project-root/

├── app1/

│ ├── __init__.py

│ ├── Dockerfile

│ ├── main.py

│ └── requirements.txt

└── app2/

├── __init__.py

├── Dockerfile

├── main.py

└── requirements.txt

Our focus will primarily be on the main.py files. For the first application, the code is as follows:

# app1/main.py

from fastapi import FastAPI

import os

import requests

app = FastAPI()

API = os.environ.get("API", "")

@app.get("/")

def sample_endpoint():

r = requests.get(f"{API}/api/test")

return {"data": r.json()}

This is a simple FastAPI application with a single endpoint that makes a request to the second application. Here is the code for the second application:

# app2/main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/api/test")

def test_api():

return {"key": "some data"}

This application merely returns a JSON response from the /api/test endpoint, called by the first application. This setup allows us to trigger breakpoints in both applications with just one request to the first app.

Building the Applications

To build these applications, we need to create Dockerfiles. Here’s the Dockerfile for the first application:

FROM python:3.11.4-slim-buster

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

RUN pip install debugpy

COPY ./main.py ./__init__.py /code/app/

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "5000"]

This basic setup for FastAPI images includes the installation of debugpy, which is necessary for debugging. If you want to integrate this debugging approach into your existing applications, this is the only change needed.

Next, we will build and deploy the applications with the following commands:

docker build -f app1/Dockerfile -t docker.io/martinheinz/python-debugging-app1:v1.0 app1

docker build -f app2/Dockerfile -t docker.io/martinheinz/python-debugging-app2:v1.0 app2

minikube image load docker.io/martinheinz/python-debugging-app1:v1.0

minikube image load docker.io/martinheinz/python-debugging-app2:v1.0

For deployment, we’ll apply the configurations defined in deployment.yaml (available in the repository), which sets up the necessary Deployment and Service objects for each application.

We can verify that the applications are functioning correctly by executing:

kubectl port-forward svc/app1 5000

curl localhost:5000/

# {"data":{"key":"some data"}}

This command forwards the application port to the local environment, allowing us to query it and receive the expected response from the second application.

Deploying the Debugger

Now that the applications are running, we can proceed to deploy the debugger. To maintain our goal of not altering the applications, we will use Kubernetes' ephemeral containers, which are temporary containers designed for troubleshooting.

For our debugging container, we will utilize the following Dockerfile:

# debugger.Dockerfile

FROM python:3.11.4-slim-buster

RUN apt-get update && apt install -y gdb

RUN pip install debugpy

ENV DEBUGPY_LOG_DIR=/logs

This image is built with the same base as the applications to ensure compatibility, including gdb and debugpy. We also specify the DEBUGPY_LOG_DIR environment variable for logging purposes.

To build the debugger image, run:

docker build -f debugger.Dockerfile -t docker.io/martinheinz/python-debugger:v1.0 .

minikube image load docker.io/martinheinz/python-debugger:v1.0

Next, we need to inject the ephemeral container into our application Pods:

APP1_POD=$(kubectl get -l=app=app1 pod --output=jsonpath='{.items[0].metadata.name}')

APP2_POD=$(kubectl get -l=app=app2 pod --output=jsonpath='{.items[0].metadata.name}')

./create-debug-container.sh default "$APP1_POD" app1

./create-debug-container.sh default "$APP2_POD" app2

The script injects a container into the Pods with the necessary specifications, including elevated privileges and the ability to attach to the application process.

After the injection, we execute the debugger with:

kubectl exec "$APP1_POD" --container=debugger -- python -m debugpy --listen 0.0.0.0:5678 --pid 1

This command starts the debugger on port 5678 and attaches it to the primary application process.

To access both the debugger and application endpoints, we initiate port forwarding:

kubectl port-forward "$APP1_POD" 5000 5678

kubectl port-forward "$APP2_POD" 5679:5678

With this setup, the first application’s port is forwarded alongside the debugger’s, while the second application only forwards the debugger port.

Connecting to the Debugger

With the debugger waiting for connections, we can now connect through our IDE. For VS Code, configure the following in your .vscode/launch.json file:

{

"version": "0.2.0",

"configurations": [

{

"name": "Python: Remote Attach App 1",

"type": "python",

"request": "attach",

"connect": {

"host": "127.0.0.1",

"port": 5678

},

"pathMappings": [

{

"localRoot": "${workspaceFolder}/app1",

"remoteRoot": "/code/app/"

}

],

"justMyCode": true

},

{

"name": "Python: Remote Attach App 2",

"type": "python",

"request": "attach",

"connect": {

"host": "127.0.0.1",

"port": 5679

},

"pathMappings": [

{

"localRoot": "${workspaceFolder}/app2",

"remoteRoot": "/code/app/"

}

],

"justMyCode": true

}

]

}

The critical elements here are the connect.port values, which correspond to the forwarded ports, and the pathMappings that link local and remote directories.

Now, initiate the debugging sessions in VS Code by selecting “Run and Debug”:

Debugging configuration in VS Code

You can set breakpoints in the code and activate them by executing:

curl localhost:5000

Breakpoint hit in remote code

Success! You’ve hit a breakpoint in remote code and can debug it locally. If you step through the code, you’ll also be able to hit breakpoints in the second application.

Note: If you encounter issues with requests hanging (such as in Flask apps), this may be due to the debugger blocking the application process. Switching to a different web server may resolve this.

Final Thoughts

While the setup may seem complex at first glance, it mostly involves running the script to inject the ephemeral container and forwarding the debugger ports, which only takes a few moments.

Once established, you’ll gain robust remote debugging capabilities without altering your application code or needing to redeploy. This approach also facilitates debugging multiple containers or applications concurrently.

Although this guide primarily covers debugging in VS Code, it’s also compatible with PyCharm, provided you have the “Professional” edition, as it uses pydevd, the underlying library for debugpy.

Want to Connect?

This article originally appeared on martinheinz.dev.

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

Small Acts of Kindness: Their Profound Impact on Lives

Discover how simple, kind words can uplift others and enhance connections.

A Call for Progressive Change: Embracing New Ideas in Politics

Exploring the need for fresh perspectives in the Democratic Party and the impact of generational shifts in political thought.

Writing on Medium in 2034: What's Next for Storytelling?

Explore the future of writing on Medium and how it adapts to technological advancements while maintaining its core essence.

Understanding the Complexities of the Israel-Palestine Conflict

An exploration of the Israel-Palestine conflict, addressing misconceptions and highlighting the plight of innocent Palestinians.

Canned and Frozen Foods: The Unsung Heroes of Nutrition

Discover why canned and frozen foods are essential for a nutritious diet, especially during challenging times.

Insights on Covid-19 and Flu Vaccines: October Research Review

A concise overview of the latest research findings on Covid-19 and flu vaccines from October, highlighting key articles and insights.

Transform Your Health with a 10-Minute Cardio Core Routine

Elevate your fitness with a quick 10-minute cardio core routine that energizes and strengthens you throughout the day.

Exploring the Truth Behind Manuka Honey: Benefits and Myths

Discover the real benefits and myths surrounding Manuka honey, including its properties and effectiveness for various health conditions.