Введение
При автоматизации разнородных задач часто возникает паттерн, когда Python-воркер должен запускать различные контейнеры в зависимости от типа задачи. Вместо хардкодинга каждого контейнера мы создаём универсальный диспетчер, управляемый YAML-конфигурацией.
YAML-конфигурация с секцией runners
# config.yaml
runners:
jira-processor:
image: localhost/jira-proc:latest
command: [“python”, “process.py”]
podman_args:
- “–pod”
- “main-stack”
- “–secret”
- “jira-token”
timeout: 300
asterisk-handler:
image: localhost/asterisk-ari:latest
command: [“python”, “handle.py”]
podman_args:
- “–pod”
- “voip-stack”
timeout: 60
report-generator:
image: localhost/reports:latest
command: [“python”, “generate.py”]
podman_args: []
timeout: 600
EphemeralContainer Context Manager
Контекстный менеджер управляет полным жизненным циклом контейнера — от создания до удаления:
import subprocess, json, uuid
class EphemeralContainer:
def init(self, runner_config):
self.config = runner_config
self.name = f"task-{uuid.uuid4().hex[:8]}"
self.process = None
def enter(self):
return self
def run(self, input_data):
cmd = [“podman”, “run”, “–rm”, “-i”,
“–name”, self.name]
cmd.extend(self.config.get(‘podman_args’, []))
cmd.append(self.config[‘image’])
cmd.extend(self.config.get(‘command’, []))
result = subprocess.run(
cmd,
input=json.dumps(input_data).encode(),
capture_output=True,
timeout=self.config.get(‘timeout’, 300)
)
if result.returncode != 0:
raise RuntimeError(
f"Container failed: {result.stderr.decode()}"
)
return json.loads(result.stdout)
def exit(self, *args):
# Гарантируем остановку при ошибке
subprocess.run(
[“podman”, “rm”, “-f”, self.name],
capture_output=True
)
Передача JSON через stdin
Данные задачи сериализуются в JSON и передаются через stdin контейнера. Контейнер читает stdin, обрабатывает данные и записывает результат в stdout. Этот подход обеспечивает полную изоляцию и универсальность — контейнер не зависит от конкретного транспорта.
Захват результатов из stdout
Результат обработки парсится из stdout контейнера. Stderr используется для логов и отладки. При ненулевом коде возврата выбрасывается исключение с содержимым stderr.
Обработка ошибок с блоком finally
Блок finally в контекстном менеджере гарантирует удаление контейнера даже при исключениях. Это предотвращает утечку ресурсов — зависшие контейнеры не будут накапливаться после сбоев.
Интеграция с RQ
Универсальная задача для RQ выглядит элементарно — она принимает имя раннера и данные, загружает конфигурацию и запускает соответствующий контейнер. Это позволяет добавлять новые типы задач без изменения кода воркера.
Заключение
Универсальный контейнерный диспетчер — элегантное решение для систем с разнородными задачами. YAML-конфигурация, context manager и передача данных через stdin/stdout обеспечивают гибкость, надёжность и полную изоляцию выполнения.