Операции «Назначить политику» и «Заблокировать устройство» в MDM-консоли выглядят синхронными, хотя фактически запускают асинхронную цепочку доставки и подтверждения.
На практике между этими событиями лежит распределённая система: бэкенд MDM, очередь, инфраструктура Apple или Google, агент на устройстве и обратный канал. Любой из этих компонентов может быть временно недоступен, а само устройство — находиться без сети.
При разработке Айтера MDM мы пришли к выводу, что одна из ключевых частей системы управления устройствами — не интерфейс создания политики и не сам набор ограничений, а контролируемая доставка требуемого состояния с проверяемым результатом. Постановку команды в очередь нельзя считать её успешным выполнением.
В этой статье разберу, как отличаются модели доставки Android и iOS, какие состояния команды действительно полезны администратору и почему MDM по своей природе работает в модели eventual consistency.
Одна операция — несколько независимых состояний
Фраза «команда отправлена» может означать как минимум четыре вещи:
Запрос администратора принят API.
Команда сохранена и поставлена в очередь.
Внешняя инфраструктура Apple или Google приняла запрос.
Устройство выполнило команду и сообщило результат.
Это не одно и то же.
Ответ 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-модели:
Сервер кладёт команду в очередь устройства.
Через APNs отправляется тихий wake-up push.
Устройство подключается к MDM-серверу.
Сервер отдаёт следующую plist-команду в ответе.
Устройство выполняет её и при следующем обращении сообщает статус.
В нашей реализации очередь ведётся отдельно для каждого устройства. Команда не удаляется в момент выдачи: сначала устройство должно вернуть финальный статус с соответствующим 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 |
Устройство само запрашивает команду |
Подтверждение |
Состояние и события приходят асинхронно |
|
Порядок |
В основном определяется политикой и Google |
Важна очередь команд устройства |
Поэтому мы унифицируем пользовательский сценарий, но не транспорт. Общими остаются доменные понятия — устройство, политика, команда, результат, инцидент. А адаптеры доставки и модель подтверждения для платформ различаются.
Каким должен быть статус команды
Первый вариант интерфейса часто ограничивается тремя состояниями:
Создана → Отправлена → Выполнена
Для эксплуатации этого мало. Более полезная модель выглядит примерно так:
Queued ├──► Dispatched │ ├──► Acknowledged │ ├──► Error │ └──► Deferred ──► Dispatched └──► Cancelled / Expired
При этом статус доступности устройства должен храниться отдельно. Команда может находиться в очереди корректно, а устройство — быть офлайн. Это не ошибка MDM и не повод немедленно помечать операцию как неуспешную.
Для администратора важны следующие данные:
время создания и последней попытки доставки;
идентификатор команды;
текущее состояние;
текст и код последней ошибки;
время последней активности устройства;
источник подтверждения: устройство, Apple/Google или фоновая сверка;
число попыток, если операция допускает повтор.
В результате журнал команд становится полноценным инструментом эксплуатации, а не набором технических записей.
Что делать с повторной доставкой
Ретраи нужны, но без ограничений они могут усугубить проблему. Например, внешний сервис отвечает 429 Too Many Requests, а несколько воркеров одновременно начинают повторять запросы. В результате интеграция получает ещё большую нагрузку.
Мы придерживаемся нескольких правил:
Повторяем только временные ошибки: сетевые сбои, таймауты,
429, часть ответов5xx.Используем exponential backoff с jitter, чтобы экземпляры сервиса не повторяли запросы синхронно.
Устанавливаем общий бюджет времени, а не только число попыток.
Добавляем circuit breaker для внешнего API.
Не повторяем автоматически команды с необратимым эффектом, пока не доказана их идемпотентность.
После исчерпания попыток оставляем наблюдаемый финальный статус, а не просто строку в серверном логе.
Особенно осторожно нужно обращаться с wipe, удалением профиля и сменой привязки политики. Повтор безопасен только тогда, когда контракт платформы и наша модель состояния позволяют однозначно определить результат.
MDM в защищённом контуре не означает MDM без внешних зависимостей
При on-premises-развёртывании серверы, базы и административная панель могут находиться внутри инфраструктуры заказчика. Но стандартные каналы управления мобильными ОС всё равно зависят от платформенных сервисов.
Для Apple MDM требуется доступ к APNs. Для Android Management API — доступ к инфраструктуре Google. Поэтому «развёртывание в закрытом контуре» и «полностью изолированная сеть» — разные требования.
На этапе проектирования необходимо явно определить:
какие исходящие соединения разрешены;
через какие прокси они проходят;
как обновляются сертификаты и токены;
что произойдёт при временной недоступности внешней инфраструктуры;
сколько времени команда может ждать устройство;
какие метрики и события попадут в мониторинг заказчика.
Если отложить эти вопросы до внедрения, проблемы проявятся уже на пилоте: политика будет создана, команда появится в журнале, но устройство не получит её из-за недоступного сетевого маршрута или заблокированного endpoint.
Интерфейс администратора
Ниже — два экрана текущей версии административной панели: общий обзор состояния парка и работа с политиками.

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

Раздел политик: состав правил, назначение и контроль применения конфигураций.
Какие метрики действительно помогают
Количество зарегистрированных устройств не характеризует качество работы MDM. Для эксплуатации полезнее измерять:
долю устройств, подтвердивших актуальную политику;
медианное и верхнепроцентильное время применения политики;
размер очередей и возраст самой старой команды;
долю команд в
ErrorиNotNow;число устройств без свежего heartbeat или status report;
ошибки внешних API по типам;
количество повторных доставок;
расхождение между желаемым и наблюдаемым состоянием.
Последний показатель особенно важен. Задача MDM — не просто разослать настройки, а постоянно сокращать расхождение между тем, что назначил администратор, и тем, что реально действует на устройствах.
Вывод
В ходе разработки Айтера MDM мы выделили доставку в самостоятельную подсистему со своей моделью состояния, очередями, повторами, подтверждениями и мониторингом.
Главные выводы для нас получились такими:
принятие запроса не равно выполнению команды;
желаемое и наблюдаемое состояния нужно хранить раздельно;
Android и iOS требуют разных транспортных моделей;
очередь должна быть источником истины, а push — механизмом ускорения;
at-least-once работает только вместе с идемпотентностью;
администратору нужен не зелёный индикатор, а проверяемая история доставки.
Именно на этом уровне MDM перестаёт быть панелью с набором кнопок и становится системой управления состоянием корпоративных устройств.
Мы продолжаем развивать эту модель в Айтера MDM. Если будет интерес, в следующем материале могу отдельно разобрать сборку политик: как настройки из нескольких доменов превращаются в Android Policy и Apple configuration profile, и где при этом чаще всего возникают конфликты.
Проект: Айтера MDM