На первый взгляд задача кажется простой: нужно зафиксировать событие и предоставить интерфейс для его просмотра. Но на практике за этим стоит множество интересных инженерных решений: как отбирать события, хранить их, масштабировать систему, не потерять данные в случае аварии. И как сделать сервис полезным и для ИБ-специалистов, и для простых пользователей.

Привет! Я Владимир Атасунц, руководитель направления Security Services в MWS Cloud Platform. В этой статье расскажу о сервисе аудитных логов — базовом инструменте облака для контроля действий с ресурсами и анализа изменений в инфраструктуре.
Разберу:
зачем конкретно нужен такой сервис и какие сценарии использования предусматривает;
как он выглядит с точки зрения продуктовой модели;
какие требования легли в основу системного дизайна;
как жизненный цикл события нашел отражение в архитектуре и из каких компонентов она в итоге состоит.
Пойдём последовательно — от бизнес-сценариев к технической реализации.
Что такое сервис аудитных логов
Начнём с простого примера. Представьте, что вы — администратор облачного пространства. Утром вы создали виртуальную машину. Вечером возвращаетесь — а с ней что-то произошло: кто-то добавил ядра, кто-то её выключил. В команде есть коллеги, доступов много, действий — ещё больше.
Возникают базовые вопросы: что именно произошло и кто это сделал? Ответ на это и дают аудитные логи.
Аудитные логи — это запись о том:
какое действие было выполнено;
кто выполнил действие;
когда это произошло;
и чем всё закончилось (успехом или ошибкой).
Эта информация позволяет восстановить цепочку событий и понять, что происходило в инфраструктуре.
Теперь поговорим про основных потребителей сервиса — по нашему мнению, это специалисты по информационной безопасности: ИБ-инженеры, аналитики, архитекторы, SOC-команды. Для них это рабочий инструмент для выполнения регуляторных требований, мониторинга событий безопасности, разбора инцидентов постфактум.
При этом аудитные логи важны не только для ИБ. Ими пользуются и инженеры, которым нужно быстро понять причину изменений в инфраструктуре — например, кто и каким действием остановил кластер или изменил конфигурацию ресурса.
Для нас как для облака, помимо внешних пользователей есть еще и внутренние потребители — команда, которая это облако развивает. Нам важно понимать, что происходит внутри инфраструктуры: для аналитики, выполнения внутренних требований, расследования инцидентов и оперативной диагностики. По сути, мы используем тот же инструмент для тех же задач, что и наши пользователи: чтобы видеть изменения, отслеживать подозрительные действия и быстрее разбираться в проблемах.
Продуктовое видение: базовая модель сервиса аудитных логов
Сегодня сервис аудитных логов — это must-have для любого облака. Такие решения есть у западных, азиатских и российских провайдеров. Мы не стали изобретать велосипед: посмотрели на существующие подходы, выбрали наиболее зрелую модель и адаптировали её под российские реалии, нашу архитектуру и продуктовые сценарии.
В самом простом виде задача сервиса выглядит так: в облаке есть множество источников событий и их нужно куда-то собирать и где-то хранить. Внутри мы называем это common storage — общее хранилище, куда стекаются события со всего облака. Нюансы начинаются дальше — когда нужно дать пользователям управляемый доступ к этим событиям. Для этого мы ввели две отдельные сущности — коллектор и хранилище.
Коллектор (Collector). Нужен, чтобы пользователь мог управлять тем, какие события собирать. Позволяет выбрать из всего объёма доступных событий только те, которые действительно интересны пользователю. В Google Cloud аналогичный ресурс называется Sink.
Пример сценария — события создания виртуальных машин пользователю не важны, а вот удаление ВМ или Kubernetes-кластера — критично.
Хранилище (Storage). Ресурс, в который попадают события после фильтрации. Именно он отвечает за то, где данные будут храниться или куда будут выгружаться дальше.
Мы сознательно выделили хранилище в отдельную сущность и не стали объединять его с коллектором. Коллектор отвечает за отбор событий, а хранилище за их дальнейшее хранение или отгрузку. Такое разделение даёт гибкость: можно независимо развивать логику маршрутизации событий и способы их хранения. Через конфигурацию коллектора пользователь может сначала выбрать хранение данных в хранилище аудитных логов, а при желании через некоторое время переключить хранение на внешнюю систему.
В Google Cloud аналог называется log bucket, но нам не понравилось пересечение с сущностью S3 bucket сервиса Object Storage — S3-совместимое хранилище облака MWS Cloud Platform, поэтому мы упростили нейминг.
Требования к системному дизайну сервиса аудитных логов
Приступив к проработке системного дизайна, мы опирались на технические требования, бизнес-сценарии, продуктовую модель. В архитектуре облака мы разделяем Control Plane и Data Plane.
Control Plane отвечает за хранение конфигурации объектов, реализацию API и ресурсной модели.
Data Plane отвечает за выполнение действий. Например, Control Plane сохраняет конфигурацию виртуальной машины и передаёт её в Data Plane, который уже создаёт ВМ.
Аудитные события могут генерироваться на обоих уровнях. Ниже рассмотрим требования к дизайну сервиса — от технических до потребности в централизованном хранилище.
Технические требования
Чтобы понимать, что происходит в инфраструктуре, события нужно надёжно генерировать, доставлять и хранить. У нас были следующие требования:
Высокая производительность на запись. Поток событий большой и постоянно растёт. На старте мы заложили целевой показатель в 100k RPS. Для молодого облака это серьёзная цифра, но архитектуру хотелось строить с запасом.
Адекватная производительность на чтение. Пользователь не должен ждать по пять минут, пока загрузятся события. Особенно в сценарии расследования инцидентов , где приходится постоянно фильтровать и уточнять выборку. Мы не фиксировали жёсткий SLA, но ориентировались на единицы секунд в типовом запросе — в идеале около пяти. В сложных случаях с большой глубиной хранения запросы могут занимать десятки секунд, но это нетипичный сценарий.
Масштаб и низкая стоимость хранения. Источников событий много: сервисов много, пользовательских ресурсов ещё больше, пользователей — ещё больше. Все что-то делают. При этом хранить эти данные нужно долго, например для целей compliance. Значит, решение должно обеспечивать низкую стоимость хранения.
Высокая надёжность. С учетом вышеописанных требований сервис должен обеспечить непрерывный поток записи событий, быть готовым к инфраструктурным проблемам и неожиданному росту нагрузки, легко масштабироваться.
Ресурсная модель
Следующий шаг — спроектировать ресурсную модель сервиса и понять, как объекты будут взаимодействовать между собой.
Событие, главный объект. Их может быть очень много. В событии хранится вся необходимая информация: кто, что, когда и над каким ресурсом сделал.
Хранилище. Я уже о нём писал, но здесь важно зафиксировать характеристики. Storage может иметь ограничение по объёму — а может не иметь. Это зависит от потребностей клиента. Также можно ограничить срок хранения. Например: максимум 10 ГБ в течение 10 дней.
Коллектор — определяет, какие события нужно собрать и в какое хранилище их отправить.
С ресурсной моделью самого сервиса разобрались — теперь надо понять, как она ложится на модель облака.
Первый контейнер ресурсов, с которым сталкивается пользователь, — это организация. Важно понимать, что события могут происходить и на этом уровне. Например, добавление пользователя — это тоже событие, которое мы хотим фиксировать.
Ниже находится проект — основной рабочий контейнер для пользователей. Именно внутри проекта разворачиваются виртуальные машины, Kubernetes-кластеры, а также создаются наши хранилища и коллекторы.
Есть и сценарий выгрузки во внешнее хранилище. Например, в Object Storage нашего облака. Это достаточно простой вариант, и мы его поддерживаем.

Потребность в централизованном хранилище
Теперь рассмотрим более сложный вариант. Представим крупного клиента с десятком проектов. Создавать в каждом проекте отдельное хранилище и разбираться в них неудобно. Гораздо логичнее иметь единое централизованное хранилище, куда будут стекаться события со всех проектов и, при необходимости, с уровня организации.
Например, можно создать отдельный проект безопасности — условный Project Sec — и разместить в нём общее хранилище. Туда будут отправляться события из других проектов. Доступ к этому проекту можно выдать только команде безопасности, которая занимается мониторингом и аналитикой. С точки зрения ресурсной модели это всё то же хранилище , просто в него попадает больше событий.
При этом возникает вопрос кросс-проектного доступа. Не любой коллектор может читать события в соседнем проекте или писать в чужое хранилище. Поэтому у коллектора появляется дополнительная характеристика — сервисный аккаунт, от имени которого выполняются операции чтения и записи. Это позволяет корректно разграничить доступ. Логика аналогична Object Storage: писать может только тот, у кого есть права.

Отображение жизненного цикла события в архитектуре сервиса аудитных логов
Прорабатывая архитектуру, мы разделили жизненный цикл события на четыре этапа:
Генерация.
Сбор и доставка.
Хранение.
Доступ.
Генерация событий
С генерации всё начинается. Кто-то должен сообщить сервису, что произошло событие. Причём делать это нужно в едином формате и с полным набором данных — это не два-три поля, а довольно большой объём информации.
Поскольку инфраструктура не монолитная и состоит из множества сервисов, каждый из них должен уметь генерировать и передавать события.
На этапе системного дизайна мы решили реализовать собственную библиотеку — по сути SDK, которую подключают сервисы облака. Можно было, например, пойти по классическому пути с REST API и отправкой событий напрямую. Но на решение повлияли особенности нашей инфраструктуры.
Мы работаем в Kubernetes, сервисы развёрнуты в нескольких кластерах. Возможны аварии, сетевые проблемы, перезапуски нод. Нам нужно минимизировать влияние таких ситуаций на качество сервиса.
Синхронная отправка событий — плохой вариант. Если в момент создания пользователем виртуальной машины возникнет проблема с сетью на уровне инфраструктуры облака и сервис начнёт с периодичностью повторять запись аудита, пользователю придется ждать. Фоновая отправка без буферизации — тоже риск. При перезапуске ноды событие может потеряться.
В итоге мы выбрали подход с локальной записью событий на диск. Сервис через библиотеку записывает событие в файл, а отдельный компонент считывает эти файлы и отправляет данные дальше. Так мы отделяем пользовательскую операцию от сетевой доставки. Библиотека инкапсулирует всю эту логику — сервисам не нужно реализовывать её самостоятельно.
Сбор и доставка событий
После генерации событий возникает следующая задача — собрать данные из разных кластеров и доставить их в единое хранилище.
У нас не один кластер и не один сервис. Сервисы распределены, кластеры изолированы друг от друга, а события генерируются повсюду. Важно аккуратно собрать весь этот поток в одной точке и при этом не перегрузить систему.
Мы рассматривали разные варианты организации очереди. В том числе думали о собственной реализации, смотрели и на классические решения, и на нетипичные, но в итоге остановились на Kafka. Она позволила нам:
аккумулировать поток событий со всех кластеров;
выстроить контролируемую модель потребления;
обеспечить высокую производительность записи.
Хранение событий
Теперь нужно было решить, где и как хранить данные. Мы помнили про два ключевых требования: низкая стоимость хранения и высокая производительность на запись. Сразу стало понятно, что хранить всё на локальных дисках — дорого. Поэтому уже на этапе системного дизайна мы приняли решение строить хранилище поверх Object Storage нашего облака. Дальше начался выбор инструмента.
Первым кандидатом был ClickHouse — привычный и понятный инструмент с поддержкой S3-совместимых хранилищ и зрелой экосистемой. Он позволяет строить таблицы с использованием Object Storage, однако в классическом self-managed сценарии ClickHouse копирует данные на диск, что не соответствовало нашей цели отказа от дисков.
Мы также рассматривали zero-copy replication, где между репликами синхронизируются в основном метаданные, а не сами данные, но на момент нашего системного дизайна этот режим в документации был помечен как «not production ready». Рисковать мы не стали.
Дальше мы посмотрели в сторону Apache Iceberg. Важно понимать, что Iceberg — это не полноценная база данных, а слой управления табличными данными поверх Object Storage. Он определяет, как хранить файлы, как вести метаданные и как обеспечивать консистентность.
Архитектуру Iceberg можно условно разделить на четыре компонента.
Первый — собственно Object Storage, где лежат файлы данных. Мы выбрали формат Parquet — колоночный, удобный и хорошо поддерживаемый.
Второй — каталог метаданных. Он хранит информацию о таблицах, версиях и связях между файлами. Мы рассматривали несколько вариантов и в итоге выбрали Nessie — по нашему мнению, на момент проработки архитектуры это был наиболее зрелый и удобный для production-сценариев каталог.
Третий — сервисы обслуживания таблиц. Это процессы, которые удаляют устаревшие файлы, объединяют мелкие файлы в более крупные и обновляют метаданные. Это важно как для экономии места, так и для производительности. Для этих задач мы используем Spark — проверенный инструмент, который хорошо подходит для фонового обслуживания таблиц и обработки данных в Iceberg.
Четвёртый — движок для выполнения запросов, который работает с данными и метаданными Iceberg-таблиц. Здесь мы рассматривали три варианта: Trino, StarRocks и ClickHouse. Мы выбрали StarRocks, о причинах расскажу дальше.
Trino — мощный и зрелый query engine с поддержкой Iceberg и инструментами обслуживания таблиц. Но у него есть архитектурная особенность: координатор не масштабируется из коробки. Решения существуют, но они достаточно сложные.
StarRocks — полноценная аналитическая база данных, которая умеет работать поверх Iceberg и хорошо масштабируется. В её архитектуре есть frontend-ноды, которые планируют выполнение запроса, и compute-ноды, которые выполняют его. При нехватке ресурсов архитектура позволяет горизонтально добавлять необходимые ноды.
ClickHouse, несмотря на симпатию к инструменту, на момент проработки архитектуры не предоставлял нужного нам набора операций поверх Iceberg для реализации полного пайплайна.
В результате мы остановились на StarRocks.
Пользовательский доступ к данным
Финальный этап жизненного цикла события — предоставить пользователю удобный доступ к данным. Для этого у нас есть Control Plane, написанный на Kotlin. Он реализует API, поддерживает ресурсную модель и позволяет выполнять запросы с заданными фильтрами.
Пользователь взаимодействует именно с этим уровнем. Внутри Control Plane обращается к query engine и возвращает результат. Возможно, в будущем добавим собственный язык запросов — по аналогии с тем, как это реализовано в Google. Пока же мы ограничились фильтрами и преднастроенными возможностями поиска.
Архитектура сервиса аудитных логов: реализация на практике
Как я уже писал, архитектура сервиса отражает жизненный цикл события. Ниже расскажу, как она выглядит на практике и из каких компонентов состоит.
Генерация аудитных событий
Как я уже упоминал выше, у нас есть библиотека, с помощью которой сервисы записывают аудитные события. Backend, живущий в Kubernetes, подключает эту библиотеку, генерирует события и записывает их на диск. Библиотеку мы реализовали под основные языки нашего облака — в первую очередь под Kotlin и Go.
Помимо базовой функциональности «записать событие», мы предусмотрели middleware, которую можно подключить в Control Plane на уровне обработчиков запросов. Middleware автоматически собирает большую часть данных, необходимых для аудита. Сервису остаётся передать только специфическую часть: какой ресурс, какая операция и чем она завершилась.
События записываются в файл в каталоге на диске, подключённом через HostPath. Дальше возникает задача корректно прочитать эти данные и отправить их дальше.
Для корректной обработки файлов, в которые идет активная запись, мы реализовали отдельный сервис, условно названный logrotate (это наш собственный компонент, просто одноимённый с известной утилитой). Этот компонент вместе с библиотекой выводит файлы из режима записи и помечает их как готовые к дальнейшей обработке. После этого файлы можно забирать на сбор. Logrotate развёрнут как DaemonSet во всех кластерах, поэтому обслуживает все сервисы.

Локальный сбор и архив данных
Следующий компонент — сервис, который читает подготовленные файлы и отправляет их дальше. Этот сервис мы называем коллектором. Он выполняет две задачи:
Отправляет данные дальше в пайплайн.
Сохраняет сжатые данные во временный локальный архив.
Зачем нужен локальный архив? Ответ — надёжность. Теоретически сеть может упасть на час или два. В такой ситуации события не должны теряться. Поэтому локальный архив хранит данные до тех пор, пока они не будут успешно доставлены в целевое хранилище. Очевидная проблема — архив не бесконечный. Поэтому со старта разработки сервиса мы настроили метрики и алертинг и следим за потреблением архива, чтобы не допустить его переполнения.
Коллекторы также запущены как DaemonSet во всех кластерах. В итоге все коллекторы отправляют данные в Kafka для дальнейшей обработки.

Очередь событий
Kafka в пайплайне сбора событий — это центральная точка агрегации. После этого начинается этап обработки, который делится на две части:
внешняя выгрузка;
внутренняя запись в наше хранилище.
Мы сознательно разделили эти потоки. Нам не хотелось сначала писать события во внутреннее хранилище, а затем читать их оттуда для внешней отправки. Это создавало бы лишнюю нагрузку и увеличивало задержку. Проще и эффективнее отправлять данные напрямую из Kafka.
Внешняя выгрузка данных
Внешний экспортер читает данные из Kafka. Дальше происходит следующее:
События группируются по проектам и организациям.
Экспортер обращается к Control Plane по внутреннему API.
Получает информацию о коллекторах, работающих с этими проектами и организациями.
Определяет:
какие события нужно оставить;
куда их выгружать;
от имени какого сервисного аккаунта выполнять операцию.
После этого экспортер выгружает данные в сконфигурированные пользователем внешнее хранилище. Здесь мы подумали про надёжность и предусмотрели плохие сценарии, например:
проблемы с сетью;
некорректно настроенный сервисный аккаунт;
удалённый пользователем бакет.
Поэтому мы сохраняем данные во временном хранилище и реализуем механизм повторной отправки в течение заданного времени.

Внутреннее хранилище данных
Внутренний экспортер (Iceberg Exporter) также читает данные из Kafka.
Он реализует два сценария:
Запись полного набора событий для внутренних нужд в Common Storage.
Запись в пользовательские хранилища.
В качестве Common Storage мы используем то же хранилище, который предоставляем пользователям. Разница лишь в конфигурации: для внутреннего хранения используется другой коллектор и другие параметры хранения.


Ниже расскажу про некоторые особенности нашего внутреннего хранилища данных:
Прослойка для работы с Iceberg. При записи в Iceberg есть важная особенность: в одну таблицу можно выполнить только один commit за раз. Если два процесса попытаются сделать commit параллельно, один из них завершится неудачей, и придётся перечитывать файлы и пересобирать манифесты.
Чтобы избежать этой проблемы, мы добавили прослойку-агрегатор, которую назвали Coordinator. Она накапливает данные в течение заданного интервала (например, двух минут) и выполняет единый commit. Это своего рода координатор записи.
Хранилище внутри Iceberg. Внутри нашего Iceberg-хранилища пользовательское хранилище — это, по сути, отдельная таблица.
Мы рассматривали другие варианты:
использование view;
комбинированную модель (общая таблица + view для каждого хранилища).
Но оба варианта не подошли. И вот почему:
view дают худшую производительность;
комбинированный вариант сложнее в реализации и затрудняет контроль объёма данных.
Таблица оказалась самым простым и предсказуемым решением.
Кроме того, поскольку на этапе экспорта мы уже знаем, к каким коллекторам относятся события и в какие хранилища они должны попасть, запись в таблицы часто может выполняться параллельно — если это разные таблицы. Прослойка агрегации нужна только в случаях, когда несколько коллекторов пишут в одно хранилище.
Ограничения и производительность чтения. Напомню, одно из требований — адекватная производительность на чтение.
Событий может быть много, глубина хранения — большой. Технически обеспечить высокую производительность на больших объёмах сложно. Поэтому здесь вступает продуктовый подход.
У хранилища есть две характеристики:
максимальный объём;
максимальное время хранения.
В инфраструктуре large enterprise десятки ГБ могут заполниться очень быстро — это неудобно. Поэтому объём решили не ограничивать. А вот срок хранения мы ограничили — 30 дней.
По нашим исследованиям, этого достаточно для реализации основных сценариев: расследований, диагностики, мониторинга.
Если требуется более длительное хранение, пользователь может настроить выгрузку в Object Storage и строить аналитику самостоятельно.
Для внутренних хранилищ мы предоставляем интерфейс просмотра. Для внешних выгрузок (Object Storage и другие будущие направления) интерфейса просмотра не предусмотрено — в том числе из-за требований к производительности.
Что в итоге
Сервис аудитных логов — важный компонент облака, базовый инструмент контроля и анализа событий. «Посмотреть логи» — простая и понятная потребность пользователя, но за ней стоит большая и довольно интересная работа по проектированию: переложить жизненный цикл события на архитектуру сервиса и выбрать оптимальные решения для каждого его компонента.
Сервис продолжит развиваться. Сейчас аудитные логи доступны в Preview — сервис предоставляется бесплатно и предназначен для тестирования и знакомства с его возможностями.
В ближайших планах — кросс-проектная выгрузка событий, подключение новых сервисов как источников аудитных логов, интеграция с SIEM через отдельный Collector, а также развитие интерфейса и более гибких фильтров в UI.
Попробуйте сервис и поделитесь с нами обратной связью в сообществе MWS Cloud Platform.
