Почему отправленная MDM-команда ещё не означает выполненную

от автора

Операции «Назначить политику» и «Заблокировать устройство» в MDM-консоли выглядят синхронными, хотя фактически запускают асинхронную цепочку доставки и подтверждения.

На практике между этими событиями лежит распределённая система: бэкенд MDM, очередь, инфраструктура Apple или Google, агент на устройстве и обратный канал. Любой из этих компонентов может быть временно недоступен, а само устройство — находиться без сети.

При разработке Айтера MDM мы пришли к выводу, что одна из ключевых частей системы управления устройствами — не интерфейс создания политики и не сам набор ограничений, а контролируемая доставка требуемого состояния с проверяемым результатом. Постановку команды в очередь нельзя считать её успешным выполнением.

В этой статье разберу, как отличаются модели доставки Android и iOS, какие состояния команды действительно полезны администратору и почему MDM по своей природе работает в модели eventual consistency.

Одна операция — несколько независимых состояний

Фраза «команда отправлена» может означать как минимум четыре вещи:

  1. Запрос администратора принят API.

  2. Команда сохранена и поставлена в очередь.

  3. Внешняя инфраструктура Apple или Google приняла запрос.

  4. Устройство выполнило команду и сообщило результат.

Это не одно и то же.

Ответ 200 OK подтверждает обработку HTTP-запроса сервером. Запись команды в персистентную очередь обеспечивает её сохранность при перезапуске сервиса, но не подтверждает доставку на устройство. Успешный ответ внешнего API также не доказывает, что политика уже применена.

Поэтому в MDM полезно разделять три типа состояния:

  • желаемое состояние — что администратор хочет получить;

  • состояние доставки — где сейчас находится команда;

  • наблюдаемое состояние — что устройство фактически сообщило о себе.

Попытка хранить всё это в одном булевом поле isApplied почти неизбежно приводит к ложным статусам.

Android: политика уходит в Google, а не напрямую на телефон

В основном сценарии Android Enterprise MDM-сервер не открывает соединение с каждым устройством. Он работает через Android Management API.

Упрощённый поток выглядит так:

Администратор    │    ▼MDM API ──► Outbox ──► сервис доставки                              │                              ▼                  Android Management API                              │                              ▼                  Android Device Policy                              │                              ▼                         Устройство

После изменения настроек мы формируем объект Policy, передаём его в Android Management API и привязываем к нему нужные устройства. Дальнейшую синхронизацию выполняет Android Device Policy — системный агент Google на управляемом устройстве.

У такой схемы есть важное следствие: успешный Policies.patch означает, что Google принял новое желаемое состояние. Это ещё не подтверждение применения политики конкретным телефоном.

Разовые операции — например, блокировка, сброс пароля или перевод в режим потерянного устройства — идут другим путём, через device command API. Но и здесь ответ внешнего API сообщает лишь о принятии команды к обработке. Финальный результат появляется позже.

Зачем здесь Outbox

Изменение политики обычно затрагивает несколько сервисов и внешнюю систему. Плохой вариант реализации выглядит так:

1. Сохранить политику в БД.2. Вызвать внешний API.3. Надеяться, что между шагами ничего не упадёт.

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

Мы используем паттерн Outbox: бизнес-транзакция фиксирует и новое состояние, и событие, которое затем заберёт обработчик доставки. Это не делает внешний вызов «ровно один раз» — в распределённой системе такого обещания лучше избегать. Зато мы получаем доставку как минимум один раз и можем строить операции идемпотентно.

Практическое требование к такой схеме:

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

Для этого у команды должен быть стабильный идентификатор, а обработчик должен понимать, видел ли он её раньше.

iOS: APNs инициирует подключение устройства, но не доставляет команду

У Apple используется другая модель. MDM-сервер не передаёт команду через APNs. Push-уведомление инициирует подключение устройства к MDM-серверу, после чего устройство самостоятельно запрашивает следующую команду.

Поток выглядит так:

MDM ──► очередь команд конкретного устройства │ └────► APNs: wake-up push                  │                  ▼              iPhone/iPad                  │                  ├────► запрашивает следующую команду у MDM                  │                  ◄──── plist-команда в HTTP-ответе                  │                  └────► Acknowledged / Error / NotNow

То есть iOS работает по pull-модели:

  1. Сервер кладёт команду в очередь устройства.

  2. Через APNs отправляется тихий wake-up push.

  3. Устройство подключается к MDM-серверу.

  4. Сервер отдаёт следующую plist-команду в ответе.

  5. Устройство выполняет её и при следующем обращении сообщает статус.

В нашей реализации очередь ведётся отдельно для каждого устройства. Команда не удаляется в момент выдачи: сначала устройство должно вернуть финальный статус с соответствующим CommandUUID.

Статусы имеют разную семантику:

  • Acknowledged — команда выполнена;

  • Error — выполнение завершилось ошибкой;

  • NotNow — устройство временно не может выполнить команду, её нужно оставить в очереди.

Именно NotNow хорошо показывает, почему обычной пары «успешно/неуспешно» недостаточно. Команда не потеряна и не отклонена — её выполнение отложено.

APNs при этом остаётся best-effort-механизмом инициации соединения. Push может прийти с задержкой или не привести к немедленному подключению. Поэтому очередь должна оставаться источником истины, а фоновая повторная нотификация — компенсировать отсутствие своевременного запроса со стороны устройства.

Почему нельзя сделать общий транспорт для Android и iOS

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

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

Android Enterprise

Apple MDM

Основная модель

Политика передаётся в Android Management API

Команды ждут в очереди MDM

Роль push

Доставка контролируется инфраструктурой Google

APNs только будит устройство

Получение команды

Через Android Device Policy

Устройство само запрашивает команду

Подтверждение

Состояние и события приходят асинхронно

Acknowledged, Error, NotNow

Порядок

В основном определяется политикой и Google

Важна очередь команд устройства

Поэтому мы унифицируем пользовательский сценарий, но не транспорт. Общими остаются доменные понятия — устройство, политика, команда, результат, инцидент. А адаптеры доставки и модель подтверждения для платформ различаются.

Каким должен быть статус команды

Первый вариант интерфейса часто ограничивается тремя состояниями:

Создана → Отправлена → Выполнена

Для эксплуатации этого мало. Более полезная модель выглядит примерно так:

Queued  ├──► Dispatched  │       ├──► Acknowledged  │       ├──► Error  │       └──► Deferred ──► Dispatched  └──► Cancelled / Expired

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

Для администратора важны следующие данные:

  • время создания и последней попытки доставки;

  • идентификатор команды;

  • текущее состояние;

  • текст и код последней ошибки;

  • время последней активности устройства;

  • источник подтверждения: устройство, Apple/Google или фоновая сверка;

  • число попыток, если операция допускает повтор.

В результате журнал команд становится полноценным инструментом эксплуатации, а не набором технических записей.

Что делать с повторной доставкой

Ретраи нужны, но без ограничений они могут усугубить проблему. Например, внешний сервис отвечает 429 Too Many Requests, а несколько воркеров одновременно начинают повторять запросы. В результате интеграция получает ещё большую нагрузку.

Мы придерживаемся нескольких правил:

  1. Повторяем только временные ошибки: сетевые сбои, таймауты, 429, часть ответов 5xx.

  2. Используем exponential backoff с jitter, чтобы экземпляры сервиса не повторяли запросы синхронно.

  3. Устанавливаем общий бюджет времени, а не только число попыток.

  4. Добавляем circuit breaker для внешнего API.

  5. Не повторяем автоматически команды с необратимым эффектом, пока не доказана их идемпотентность.

  6. После исчерпания попыток оставляем наблюдаемый финальный статус, а не просто строку в серверном логе.

Особенно осторожно нужно обращаться с wipe, удалением профиля и сменой привязки политики. Повтор безопасен только тогда, когда контракт платформы и наша модель состояния позволяют однозначно определить результат.

MDM в защищённом контуре не означает MDM без внешних зависимостей

При on-premises-развёртывании серверы, базы и административная панель могут находиться внутри инфраструктуры заказчика. Но стандартные каналы управления мобильными ОС всё равно зависят от платформенных сервисов.

Для Apple MDM требуется доступ к APNs. Для Android Management API — доступ к инфраструктуре Google. Поэтому «развёртывание в закрытом контуре» и «полностью изолированная сеть» — разные требования.

На этапе проектирования необходимо явно определить:

  • какие исходящие соединения разрешены;

  • через какие прокси они проходят;

  • как обновляются сертификаты и токены;

  • что произойдёт при временной недоступности внешней инфраструктуры;

  • сколько времени команда может ждать устройство;

  • какие метрики и события попадут в мониторинг заказчика.

Если отложить эти вопросы до внедрения, проблемы проявятся уже на пилоте: политика будет создана, команда появится в журнале, но устройство не получит её из-за недоступного сетевого маршрута или заблокированного endpoint.

Интерфейс администратора

Ниже — два экрана текущей версии административной панели: общий обзор состояния парка и работа с политиками.

Обзор состояния парка устройств в Aitera MDM

Обзор состояния парка устройств в Aitera MDM

Обзор парка: устройства, пользователи, профили, соответствие политикам и инциденты.

Управление политиками в Aitera MDM

Управление политиками в Aitera MDM

Раздел политик: состав правил, назначение и контроль применения конфигураций.

Какие метрики действительно помогают

Количество зарегистрированных устройств не характеризует качество работы MDM. Для эксплуатации полезнее измерять:

  • долю устройств, подтвердивших актуальную политику;

  • медианное и верхнепроцентильное время применения политики;

  • размер очередей и возраст самой старой команды;

  • долю команд в Error и NotNow;

  • число устройств без свежего heartbeat или status report;

  • ошибки внешних API по типам;

  • количество повторных доставок;

  • расхождение между желаемым и наблюдаемым состоянием.

Последний показатель особенно важен. Задача MDM — не просто разослать настройки, а постоянно сокращать расхождение между тем, что назначил администратор, и тем, что реально действует на устройствах.

Вывод

В ходе разработки Айтера MDM мы выделили доставку в самостоятельную подсистему со своей моделью состояния, очередями, повторами, подтверждениями и мониторингом.

Главные выводы для нас получились такими:

  • принятие запроса не равно выполнению команды;

  • желаемое и наблюдаемое состояния нужно хранить раздельно;

  • Android и iOS требуют разных транспортных моделей;

  • очередь должна быть источником истины, а push — механизмом ускорения;

  • at-least-once работает только вместе с идемпотентностью;

  • администратору нужен не зелёный индикатор, а проверяемая история доставки.

Именно на этом уровне MDM перестаёт быть панелью с набором кнопок и становится системой управления состоянием корпоративных устройств.

Мы продолжаем развивать эту модель в Айтера MDM. Если будет интерес, в следующем материале могу отдельно разобрать сборку политик: как настройки из нескольких доменов превращаются в Android Policy и Apple configuration profile, и где при этом чаще всего возникают конфликты.

Проект: Айтера MDM

Ссылки

ссылка на оригинал статьи https://habr.com/ru/articles/1066224/