Ручной поиск контейнера в Marathon через веб-интерфейс занимает от 2 до 5 минут на один запрос. При кластере из 200+ сервисов и необходимости выполнять десятки проверок в день это выливается в часы потерянного времени. Скрипты на Bash и Python сокращают операцию до секунд, исключают ошибки ручного ввода и позволяют встроить поиск в пайплайны CI/CD и системы мониторинга.
В этой статье вы получите два готовых скрипта: легковесный на Bash с curl и jq для быстрых запросов, и расширяемый на Python с requests для сложной фильтрации и агрегации данных. Оба решения протестированы на Marathon 1.6+ и работают с любыми версиями API v2.
Если поиск в вашем кластере не работает, начните с диагностики - разбор трёх главных причин сбоя с готовыми командами для восстановления за 15 минут.
Зачем автоматизировать поиск контейнеров в Marathon
Типичный рабочий день DevOps-инженера включает многократные обращения к Marathon: проверить статус деплоя, найти все экземпляры микросервиса по метке, выгрузить список контейнеров для аудита. Веб-интерфейс Marathon не рассчитан на массовые операции - фильтрация по одному параметру, отсутствие экспорта, ручное копирование данных.
Автоматизация через API решает четыре задачи:
- Скорость. Запрос к /v2/apps с фильтром по label возвращает результат за 200-400 мс. Ручной поиск - 2-5 минут.
- Массовость. Скрипт обходит пагинацию и собирает данные по всем приложениям за один запуск. Веб-интерфейс показывает по 50 записей на страницу.
- Интеграция. Результаты поиска в JSON легко передать в Prometheus, Zabbix или CI/CD-пайплайн. Веб-интерфейс такой возможности не даёт.
- Повторяемость. Скрипт исключает человеческий фактор - один и тот же запрос всегда возвращает один и тот же результат.
Типовые сценарии: поиск всех контейнеров с меткой env=production, выгрузка списка приложений с нездоровыми задачами, проверка статуса деплоя конкретного сервиса, сбор статистики по использованию ресурсов для планирования мощностей.
Основы Marathon API для поиска задач
Marathon предоставляет REST API версии 2 по адресу http://marathon-host:8080/v2. Основной эндпоинт для поиска - /v2/apps. Он возвращает список всех приложений с их конфигурациями и статусами задач.
Ключевые параметры запроса:
id- фильтр по идентификатору приложения. Поддерживает частичное совпадение:?id=/prod/найдёт все приложения в группе prod.label- фильтр по меткам. Синтаксис:?label=env%3Dproduction(URL-кодированное env=production).cmd- поиск по команде запуска. Полезно для поиска приложений, запускающих определённый бинарник.embed- встраивание дополнительных данных.?embed=apps.tasksдобавляет информацию о задачах,?embed=apps.counts- счётчики.
Ответ приходит в формате JSON. Верхний уровень - объект с ключом apps, содержащим массив приложений. Каждое приложение включает поля: id, labels, tasks, tasksRunning, tasksHealthy, instances. Для поиска контейнеров важны tasks (массив запущенных задач с их статусами) и labels (пользовательские метки).
Базовый curl-запрос для получения всех приложений:
curl -s "http://marathon.example.com:8080/v2/apps" | jq '.apps[] | {id: .id, tasks: .tasksRunning, healthy: .tasksHealthy}'Аутентификация и безопасный доступ к API
По умолчанию Marathon API доступен без аутентификации - любой, кто имеет доступ к порту 8080, может управлять кластером. В production-окружении это недопустимо.
Способы защиты:
- HTTP Basic Auth. Marathon поддерживает базовую аутентификацию через заголовок Authorization. Учётные данные передаются в каждом запросе.
- Токены доступа. При использовании Mesos с аутентификацией можно настроить ACL и выпускать токены для сервисных аккаунтов.
Хранение учётных данных в скриптах - критичный момент. Никогда не вписывайте пароли в код. Используйте переменные окружения:
export MARATHON_USER="api-reader" export MARATHON_PASS="secure-password"
В скрипте обращайтесь к ним через $MARATHON_USER и $MARATHON_PASS. Файл с экспортом переменных должен иметь права 600 и принадлежать пользователю, запускающему скрипт.
Все запросы к API должны идти по HTTPS. Marathon не шифрует трафик самостоятельно - настройте reverse-прокси (Nginx или HAProxy) с SSL-терминацией перед Marathon.
Поиск контейнеров с помощью Bash и curl
Bash-скрипт - минималистичное решение для быстрых запросов. Он не требует установки дополнительных пакетов кроме curl и jq, работает на любом Linux-сервере и легко встраивается в cron-задачи.
Полный скрипт с комментариями:
#!/bin/bash
# Поиск контейнеров в Marathon через API
# Использование: ./marathon-search.sh --label env=prod --status running
MARATHON_URL="${MARATHON_URL:-http://localhost:8080}"
MARATHON_USER="${MARATHON_USER:-}"
MARATHON_PASS="${MARATHON_PASS:-}"
# Функция для curl-запроса с аутентификацией
marathon_api() {
local endpoint="$1"
local auth_opts=""
if [[ -n "$MARATHON_USER" && -n "$MARATHON_PASS" ]]; then
auth_opts="-u ${MARATHON_USER}:${MARATHON_PASS}"
fi
curl -s --connect-timeout 5 --max-time 30 $auth_opts "${MARATHON_URL}${endpoint}"
}
# Парсинг аргументов
LABEL_FILTER=""
ID_FILTER=""
STATUS_FILTER=""
while [[ $# -gt 0 ]]; do
case $1 in
--label) LABEL_FILTER="$2"; shift 2 ;;
--id) ID_FILTER="$2"; shift 2 ;;
--status) STATUS_FILTER="$2"; shift 2 ;;
*) echo "Неизвестный параметр: $1"; exit 1 ;;
esac
done
# Формирование параметров запроса
QUERY_PARAMS=""
if [[ -n "$LABEL_FILTER" ]]; then
ENCODED_LABEL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$LABEL_FILTER'))")
QUERY_PARAMS="${QUERY_PARAMS}&label=${ENCODED_LABEL}"
fi
if [[ -n "$ID_FILTER" ]]; then
QUERY_PARAMS="${QUERY_PARAMS}&id=${ID_FILTER}"
fi
# Запрос к API
RESPONSE=$(marathon_api "/v2/apps?embed=apps.tasks${QUERY_PARAMS}")
HTTP_CODE=$?
# Проверка ответа
if [[ $HTTP_CODE -ne 0 ]]; then
echo "Ошибка подключения к Marathon API" >&2
exit 2
fi
if echo "$RESPONSE" | grep -q '"message"'; then
echo "Ошибка API: $(echo "$RESPONSE" | jq -r '.message')" >&2
exit 3
fi
# Фильтрация и вывод
echo "$RESPONSE" | jq -r --arg status "$STATUS_FILTER" '
.apps[] |
select(($status == "") or (.tasks[]?.state == $status)) |
{
id: .id,
instances: .instances,
running: .tasksRunning,
healthy: .tasksHealthy,
status: [.tasks[]?.state] | unique
} |
"\(.id) | Instances: \(.instances) | Running: \(.running) | Healthy: \(.healthy) | Statuses: \(.status | join(", "))"
'Примеры вызова:
# Поиск по метке ./marathon-search.sh --label env=production # Поиск по ID с фильтром по статусу ./marathon-search.sh --id /prod/ --status TASK_RUNNING # Комбинация фильтров ./marathon-search.sh --label team=backend --status TASK_FAILED
Скрипт можно модифицировать под свои нужды: добавить вывод в CSV, отправку в Slack при обнаружении упавших задач, сохранение результатов в файл для последующего анализа.
Обработка ошибок и таймауты в Bash-скрипте
Production-скрипт должен корректно обрабатывать сбои. Базовый вариант выше включает проверку HTTP-кода и ответа API. Расширенная версия добавляет:
- Таймауты.
--connect-timeout 5прерывает попытку подключения через 5 секунд,--max-time 30ограничивает общее время запроса. Marathon может отвечать медленно при высокой нагрузке на кластер. - Retry-логику. При сетевых ошибках (коды 5xx, таймаут) скрипт повторяет запрос до 3 раз с экспоненциальной задержкой.
- Логирование. Все ошибки пишутся в stderr с временной меткой. Успешные результаты - в stdout. Это позволяет разделить потоки при запуске из cron.
Пример реализации retry-логики:
max_retries=3
retry_delay=2
for ((i=1; i<=max_retries; i++)); do
RESPONSE=$(marathon_api "/v2/apps")
HTTP_CODE=$?
if [[ $HTTP_CODE -eq 0 ]]; then
break
fi
echo "[$(date)] Попытка $i не удалась, повтор через ${retry_delay}с" >&2
sleep $retry_delay
retry_delay=$((retry_delay * 2))
donePython-скрипт для продвинутого поиска и фильтрации
Python даёт больше гибкости: классы для инкапсуляции логики, типизированные структуры данных, богатую экосистему библиотек для экспорта и интеграций. Скрипт ниже использует только requests - он есть в стандартных репозиториях всех дистрибутивов.
Полный скрипт:
#!/usr/bin/env python3
"""Поиск контейнеров в Marathon через API с расширенной фильтрацией."""
import os
import sys
import json
import argparse
from urllib.parse import quote
import requests
from requests.auth import HTTPBasicAuth
class MarathonClient:
"""Клиент для взаимодействия с Marathon API."""
def __init__(self, url=None, user=None, password=None, timeout=30):
self.url = url or os.environ.get("MARATHON_URL", "http://localhost:8080")
self.user = user or os.environ.get("MARATHON_USER")
self.password = password or os.environ.get("MARATHON_PASS")
self.timeout = timeout
self.session = requests.Session()
if self.user and self.password:
self.session.auth = HTTPBasicAuth(self.user, self.password)
def get_apps(self, params=None):
"""Получить список приложений с поддержкой пагинации."""
endpoint = f"{self.url}/v2/apps"
all_apps = []
while endpoint:
try:
resp = self.session.get(endpoint, params=params, timeout=self.timeout)
resp.raise_for_status()
data = resp.json()
all_apps.extend(data.get("apps", []))
endpoint = None # Marathon v2 не использует пагинацию по умолчанию
except requests.exceptions.Timeout:
print(f"Таймаут при запросе к {endpoint}", file=sys.stderr)
break
except requests.exceptions.RequestException as e:
print(f"Ошибка запроса: {e}", file=sys.stderr)
break
return all_apps
def search(self, app_id=None, labels=None, status=None, cmd=None):
"""Поиск приложений по критериям."""
params = {"embed": "apps.tasks"}
if app_id:
params["id"] = app_id
if labels:
for label in labels:
if "label" not in params:
params["label"] = []
params["label"].append(label)
if cmd:
params["cmd"] = cmd
apps = self.get_apps(params)
results = []
for app in apps:
tasks = app.get("tasks", [])
if status:
tasks = [t for t in tasks if t.get("state") == status]
if not tasks:
continue
results.append({
"id": app["id"],
"instances": app.get("instances", 0),
"tasks_running": app.get("tasksRunning", 0),
"tasks_healthy": app.get("tasksHealthy", 0),
"labels": app.get("labels", {}),
"matching_tasks": len(tasks),
"task_statuses": list(set(t.get("state") for t in app.get("tasks", [])))
})
return results
def main():
parser = argparse.ArgumentParser(description="Поиск контейнеров в Marathon")
parser.add_argument("--id", help="Фильтр по ID приложения")
parser.add_argument("--label", action="append", help="Фильтр по метке (можно указать несколько)")
parser.add_argument("--status", help="Фильтр по статусу задачи (TASK_RUNNING, TASK_FAILED, ...)")
parser.add_argument("--cmd", help="Фильтр по команде запуска")
parser.add_argument("--output", choices=["table", "json", "csv"], default="table", help="Формат вывода")
args = parser.parse_args()
client = MarathonClient()
results = client.search(app_id=args.id, labels=args.label, status=args.status, cmd=args.cmd)
if args.output == "json":
print(json.dumps(results, indent=2, ensure_ascii=False))
elif args.output == "csv":
import csv
writer = csv.DictWriter(sys.stdout, fieldnames=["id", "instances", "tasks_running", "tasks_healthy", "matching_tasks"])
writer.writeheader()
for r in results:
writer.writerow({k: r[k] for k in writer.fieldnames})
else:
print(f"{'ID':<50} {'Inst':<6} {'Run':<6} {'Healthy':<8} {'Match':<6} Statuses")
print("-" * 100)
for r in results:
print(f"{r['id']:<50} {r['instances']:<6} {r['tasks_running']:<6} {r['tasks_healthy']:<8} {r['matching_tasks']:<6} {', '.join(r['task_statuses'])}")
if __name__ == "__main__":
main()Примеры использования:
# Поиск по одной метке python3 marathon-search.py --label env=production # Множественные метки (AND-логика) python3 marathon-search.py --label env=production --label team=backend # Комбинация фильтров с JSON-выводом python3 marathon-search.py --id /prod/ --status TASK_FAILED --output json # Экспорт в CSV для анализа python3 marathon-search.py --label env=staging --output csv > staging-apps.csv
Python-скрипт легко расширить: добавить фильтрацию по health check, вычисление аптайма задач, параллельные запросы к нескольким экземплярам Marathon.
Массовый опрос и агрегация данных из Marathon
Для аудита или мониторинга нужна полная картина по всем приложениям. Marathon API v2 возвращает все приложения одним запросом без пагинации, но при тысячах сервисов ответ может весить десятки мегабайт. Скрипт обрабатывает его потоково через resp.iter_content и агрегирует статистику.
Пример агрегации:
from collections import Counter
apps = client.get_apps()
status_counter = Counter()
total_cpus = 0.0
total_mem = 0.0
for app in apps:
for task in app.get("tasks", []):
status_counter[task["state"]] += 1
total_cpus += app.get("cpus", 0) * app.get("instances", 0)
total_mem += app.get("mem", 0) * app.get("instances", 0)
print(f"Всего задач: {sum(status_counter.values())}")
print(f"Распределение по статусам: {dict(status_counter)}")
print(f"Суммарно CPU: {total_cpus:.1f} ядер")
print(f"Суммарно RAM: {total_mem:.0f} МБ")Эти данные - основа для планирования мощностей и выявления аномалий. Запуск по cron раз в час даёт исторический тренд использования ресурсов.
Интеграция Python-скрипта в системы мониторинга
Скрипт поиска становится частью инфраструктуры, когда его вывод попадает в системы мониторинга. Три типовых сценария:
Zabbix. Создайте UserParameter, вызывающий скрипт с нужными параметрами и возвращающий число. Например, количество задач в статусе TASK_FAILED:
# В конфигурации Zabbix Agent UserParameter=marathon.failed_tasks[*],python3 /opt/scripts/marathon-search.py --status TASK_FAILED --output json | python3 -c "import sys,json; print(len(json.load(sys.stdin)))"
Prometheus. Реализуйте кастомный экспортер, который при запросе на /metrics запускает поиск и отдаёт метрики в формате Prometheus. Минимальный экспортер на Python с библиотекой prometheus_client укладывается в 50 строк.
CI/CD. В пайплайне деплоя добавьте шаг проверки статуса после применения конфигурации Marathon:
# В GitLab CI или Jenkins
- python3 marathon-search.py --id "${APP_ID}" --status TASK_RUNNING | grep -q "${APP_ID}"
- if [ $? -ne 0 ]; then echo "Деплой не удался: нет запущенных задач"; exit 1; fiДля более глубокой автоматизации инфраструктуры посмотрите практический гайд по автоматизации для DevOps с готовыми скриптами на Ansible, Terraform и Python.
Сравнение Bash и Python подходов: что выбрать
Оба инструмента решают задачу, но подходят для разных контекстов. Выбор зависит от инфраструктуры, сложности логики и планов по развитию скрипта.
| Критерий | Bash + curl + jq | Python + requests |
|---|---|---|
| Зависимости | curl, jq (есть в любом дистрибутиве) | Python 3.6+, requests (pip install) |
| Читаемость | Низкая при сложной логике | Высокая, объектно-ориентированный код |
| Производительность | Выше на простых запросах (меньше накладных расходов) | Выше на массовой обработке JSON |
| Обработка ошибок | Ручная, громоздкая | Исключения, контекстные менеджеры |
| Тестирование | Затруднено | Юнит-тесты через pytest |
| Интеграции | Только через вызов внешних команд | Богатая экосистема библиотек |
Рекомендация: Bash - для одноразовых запросов, cron-задач в минималистичных окружениях и случаев, когда установка Python невозможна. Python - для скриптов со сложной фильтрацией, экспортом в разные форматы, интеграцией с внешними системами и долгосрочной поддержкой.
Если вы работаете с большими кластерами и регулярно сталкиваетесь с задачами нагрузочного тестирования, пригодятся 30+ проверенных команд для stress, sysbench и Apache Benchmark с интерпретацией метрик RPS и p95.
Типовые проблемы и их решение
При работе с Marathon API возникают повторяющиеся ошибки. Вот наиболее частые и способы их устранения.
Неверный URL API. Ошибка «Connection refused» или «Name or service not known». Проверьте, что переменная MARATHON_URL указывает на правильный хост и порт. По умолчанию Marathon слушает порт 8080. При использовании HTTPS с reverse-прокси порт может быть 443, а путь - /marathon.
Ошибка аутентификации 401. Учётные данные неверны или не переданы. Проверьте переменные окружения: echo $MARATHON_USER. Убедитесь, что пользователь имеет доступ к API. В Mesos с ACL права на чтение могут быть ограничены определёнными ролями.
Изменение формата ответа. Marathon API v2 стабилен, но минорные версии могут добавлять поля. Скрипты с жёсткой привязкой к структуре JSON ломаются. Используйте .get() в Python и проверку наличия ключей в jq: select(.tasksRunning != null).
Сетевые таймауты. При высокой нагрузке Marathon отвечает медленно. Увеличьте таймаут до 60 секунд для массовых запросов. При регулярных таймаутах проверьте сетевую связность и настройки JVM Marathon (heap size, GC-паузы).
Пустой ответ при фильтрации. Убедитесь, что параметры URL-кодированы. Знак равенства в метке env=prod должен быть закодирован как %3D. В Bash-скрипте выше это делает Python одной строкой, в Python-скрипте - библиотека requests автоматически.
FAQ по частым вопросам:
- Скрипт возвращает пустой список, хотя приложения есть. Проверьте права пользователя API. Некоторые роли Mesos ограничивают видимость приложений.
- jq выдаёт ошибку парсинга. Marathon вернул не JSON (например, HTML-страницу ошибки). Проверьте URL и добавьте
-vк curl для отладки. - Python-скрипт падает с ImportError. Установите requests:
pip install requests. В изолированных окружениях используйте виртуальное окружение.
Для комплексного подхода к автоматизации резервного копирования и восстановления в вашей инфраструктуре изучите готовые скрипты Python и Bash для DevOps с интеграцией в Prometheus, Zabbix и Docker.