С легкостью устанавливайте точки останова, выполняйте код и интерактивно отлаживайте приложения Python, работающие в Kubernetes.

Давайте представим ситуацию — у вас есть несколько приложений Python, работающих в Kubernetes, которые взаимодействуют друг с другом. Есть ошибка, которую нельзя воспроизвести локально, но она возникает каждый раз, когда вы достигаете определенной конечной точки API. Если бы вы только могли подключаться к удаленным запущенным процессам приложений, устанавливать точки останова и отлаживать их в режиме реального времени… как легко было бы устранить ошибку.

Но вы можете! В этом руководстве мы создадим настройку для удаленной отладки приложений Python, работающих в Kubernetes, что позволит вам устанавливать точки останова, выполнять код и интерактивно отлаживать приложения без каких-либо изменений в коде или развертывании.

Цель

Перед отладкой давайте определим, чего мы хотим здесь добиться. Мы знаем, что хотим удаленно отлаживать некоторые приложения Python, но при этом мы также хотим сделать следующее:

  • Избегайте изменения кода приложения
  • Убедитесь, что мы не нарушаем безопасность приложений
  • Нет перенаправления трафика на локальный — мы хотим отлаживать актуальный удаленный код
  • Мы хотим установить точки останова и выполнить код пошагово.
  • Микросервисы не существуют изолированно — мы хотим отлаживать более одного контейнера/пода одновременно.
  • Мы хотим простую, оптимизированную установку.

Примечание. На создание этой статьи меня вдохновила дискуссия KubeCon Talk — Breakpoints in Your Pod: Interactively Debugging Kubernetes Applications, в которой основное внимание уделяется приложениям Go, но здесь применимо то же обоснование из этой презентации.

Настраивать

Для отладки нам сначала нужно создать пару приложений и развернуть их где-нибудь. В этом уроке мы будем использовать кластер minikube:

minikube start --kubernetes-version=v1.26.3

Мы также развернем два приложения Python, чтобы продемонстрировать возможность одновременной отладки нескольких контейнеров. Для вашего удобства в этом репозитории доступен пример кода приложения, где приложения имеют следующий вид:

project-root/
├── app1/
│   ├── __init__.py
│   ├── Dockerfile
│   ├── main.py
│   └── requirements.txt
└── app2/
    ├── __init__.py
    ├── Dockerfile
    ├── main.py
    └── requirements.txt

Нас действительно интересует только код в main.py файлах. Для первого приложения имеем:

# 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()}

Это тривиальное приложение FastAPI с одной конечной точкой (/), которая отправляет запрос второму приложению и возвращает все, что получает. Кстати говоря, вот код второго приложения:

# app2/main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/api/test")
def test_api():
    return {"key": "some data"}

Этот просто возвращает ответ JSON из конечной точки /api/test, который вызывается первым приложением. С этой настройкой мы сможем сделать только один запрос к первому приложению, чтобы активировать точки останова в обоих приложениях одновременно.

Кроме того, для создания этих приложений нам понадобится несколько файлов Dockerfile. Вот первый:

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"]

Это базовая настройка образов FastAPI на основе docs. Единственное изменение — это добавление RUN pip install debugpy, которое нам нужно для работы отладчика. Если вы хотите реализовать эту настройку отладки в своих существующих приложениях, это единственное изменение, которое вы должны внести в свою кодовую базу.

Чтобы затем собрать и развернуть их, добавьте следующий код:

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

# ... or docker push ...

# Deploy to cluster
kubectl apply -f deployment.yaml

Здесь мы используем minikube image load ... для загрузки образов в кластер. Если вы используете настоящий кластер, вам нужно отправить изображения в реестр. Что касается deployment.yaml (доступно в репозитории), то это базовое развертывание приложения с объектами развертывания и службы для каждого из двух приложений.

Наконец, мы можем проверить, работают ли приложения, запустив этот код:

kubectl port-forward svc/app1 5000
curl localhost:5000/
# {"data":{"key":"some data"}}

Мы перенаправляем порт приложения на локальный и запрашиваем его, который возвращает ожидаемый ответ, переданный вторым приложением.

Развернуть отладчик

После этого мы можем перейти к развертыванию отладчика. Как было сказано ранее, мы хотим отлаживать приложения, не внося в них никаких изменений. Поэтому мы будем использовать новую фичу Kubernetes — эфемерные контейнеры, которые описаны в документации как:

Контейнер особого типа, который временно запускается в существующем поде для выполнения инициированных пользователем действий, таких как устранение неполадок.

По сути, это временные сайдкар-контейнеры, которые можно внедрять в существующие поды.

Для нашего эфемерного контейнера отладки мы будем использовать следующее изображение:

# 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

Необходимо создать отладчик с (более или менее) тем же базовым образом, что и приложения, которые будут отлаживаться. Он также должен включать gdb, отладчик проекта GNU, а также debugpy. Кроме того, мы устанавливаем переменную среды DEBUGPY_LOG_DIR, которая указывает отладчику записывать журналы в файлы в этом каталоге на случай, если нам потребуется проверить/устранить неполадки в самом отладчике.

Чтобы построить этот образ:

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

Далее нам нужно внедрить эфемерный контейнер в поды приложения:

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

Сначала мы находим имена подов с помощью селекторов меток, а затем запускаем скрипт, который внедряет в поды следующий контейнер:

# ...
  spec:
    # ... the existing application container here...
    ephemeralContainers:
    - image: docker.io/martinheinz/python-debugger:v1.0
      name: debugger
      command:
      - sleep
      args:
      - infinity
      tty: true
      stdin: true
      securityContext:
        privileged: true
        capabilities:
          add:
          - SYS_PTRACE
        runAsNonRoot: false
        runAsUser: 0
        runAsGroup: 0
      targetContainerName: "app1"  # or app2

Он указывает целевой контейнер (targetContainerName), к которому он подключается, а также securityContext, дающий ему повышенные привилегии и дополнительные возможности Linux, которые ему необходимо подключить к процессу приложения.

После внедрения контейнера скрипт также выполняет следующее:

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

Это запускает отладчик на порту 5678 и подключается к PID 1, который является идентификатором процесса фактического приложения.

Здесь я опускаю полный скрипт для ясности, но вы можете найти его в репозитории здесь.

Наконец, чтобы получить доступ к отладчику, а также к конечной точке приложения, мы запускаем переадресацию портов:

kubectl port-forward "$APP1_POD" 5000 5678
kubectl port-forward "$APP2_POD" 5679:5678

Для первого приложения мы перенаправляем порт приложения (5000), где мы будем запрашивать его конечную точку, и порт 5678, где отладчик прослушивает соединения. Для второго приложения нам нужно только перенаправить порт отладчика, на этот раз сопоставив его с 5678 (в контейнере) на 5679 (локально), потому что порт 5678 уже занят первым приложением.

Отладка

Пока отладчик ожидает подключения, остается только подключиться. Для этого нам нужно запустить/отладить следующую конфигурацию в VS Code:

{
  "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
    }
  ]
}

Эта конфигурация находится в файле .vscode/launch.json. Важными частями являются значения connect.port, которые определяют порты, которые мы перенаправляем. Также обратите внимание на значения localRoot и remoteRoot. Первый указывает локальные каталоги кода, а второй использует каталог, в который код приложения был скопирован во время сборки.

Теперь пришло время начать сеанс(ы) отладки. В VS Code выберите «Выполнить и отладить» и запустите конфигурации отладки:

Теперь мы можем установить точки останова в любом месте кода и активировать их с помощью curl localhost:5000:

И у нас есть хит! Мы успешно достигли точки останова в удаленном коде и можем отлаживать его локально. Если мы теперь пройдемся по коду, мы также увидим, что можем попасть в точки останова во втором приложении.

Примечание. Если вы пытаетесь сделать это со своим собственным приложением и запросы к вашему приложению зависают (например, с приложением Flask), это, скорее всего, связано с тем, что отладчик блокирует процесс приложения. Единственное решение, которое я нашел, — переключиться на другой веб-сервер.

Заключение

На первый взгляд может показаться, что это немного сложно настроить, но на самом деле вам нужно только запустить скрипт, который внедряет эфемерный контейнер и перенаправляет порты отладчика. Это занимает всего пару секунд.

После этого вы получите надлежащие возможности удаленной отладки без необходимости изменять код приложения, повторно развертывать его или воспроизводить локально. Это также позволит вам отлаживать несколько контейнеров/приложений одновременно.

Хотя в этой статье показано, как выполнять отладку в VS Code, это также должно работать в PyCharm; он использует pydevd, базовую библиотеку для debugpy. Однако вам потребуется версия/лицензия Professional (см. Документацию).



Want to Connect?

This article was originally posted at martinheinz.dev.