Cluster of Puppets: опыт использования Amazon ECS в iFunny
Несмотря на название, эта статья не имеет ничего общего с системой управления конфигурацией Puppet.
Вместе с трендом «распила» больших монолитов на небольшие микросервисы в эксплуатацию веб-приложений пришёл тренд на оркестрацию контейнеров. Сразу после хайпа на Docker поднимается хайп на инструменты запуска сервисов поверх Docker. Чаще всего говорят о Kubernetes, однако его многочисленные альтернативы в настоящем также живут и развиваются.
Вот и в iFunny задумались о пользе и ценности оркестраторов и в итоге выбрали Amazon Elastic Container Service. Вкратце: ECS является платформой управления контейнерами на EC2 instances. О подробностях и опыте в бою читайте ниже.
Зачем нужна оркестрация контейнеров
После многочисленных статей о Docker на «Хабре» и за его пределами предположительно все имеют представление о том, для каких целей он предназначен, а для каких нет. Давайте теперь проясним ситуацию, для чего нужна платформа над Docker:
Автоматизация деплоя сервисов «из коробки»
Закинули контейнеры на машину? Здорово! А как их обновить, избежав каких-либо деградаций работы сервиса при переключении трафика, идущего на статический порт, к которому привязан клиент? А как быстро и безболезненно откатиться в случае проблем с новой версией приложения? Ведь Docker сам по себе не решает этих вопросов.
Да, такую штуку можно написать самому. Поначалу в iFunny так и работало. Для blue-green deployment использовался Ansible playbook, в котором старый добрый Iptables управлялся путем переключения необходимых правил на IP нового контейнера. Чистка коннектов от старого контейнера проводилась через отслеживание соединений таким же старым и добрым conntrack.
Вроде прозрачно и понятно, но, как и любая самодельная вещь, она привела к следующим проблемам:
- некорректная обработка недоступных хостов. Ansible не понимает, что EC2 instance в любой момент может упасть не только из-за неисправностей, но и из-за масштабирования вниз в Autoscaling group. Таким образом, в дежурной ситуации Ansible может вернуть ошибку после выполнения плейбука. Issue по этой проблеме вроде как закрыта, но до сих пор не решена;
- пятисотки от Docker API. Да, иногда Docker при больших нагрузках может отдавать server error, и с Ansible это тоже никак не обработаешь;
- нельзя остановить деплой. Что произойдёт, если убить процесс, который запускает плейбук на момент замены правил в IPtables? Насколько большим будет образовавшийся на хостах хаос? Какая часть машин будет недоступна?
- отсутствие параллелизации задач внутри одного хоста. В Ansible нельзя запустить параллельно итерации, из которых состоит задача. Проще можно объяснить так: если у вас есть задача по запуску 50 контейнеров с общим паттерном, но разными параметрами, то вы будете вынуждены ждать запуска контейнера 1 перед тем, как запустить контейнер 2.
Суммируя все проблемы, можно сделать вывод, что декларативные SCM мало пригодны для таких императивных задач, как деплой. Вы спросите, причём здесь оркестратор? Да при том, что любой оркестратор даст возможность задеплоить сервис одной командой без необходимости описывать процесс. В вашем распоряжении все известные паттерны деплоя и плавная обработка вышеперечисленных отказов.
На мой взгляд, платформы оркестрации пока являются единственной возможностью реализации быстрого, простого и надежного деплоя с Docker. Возможно, такие же любители AWS, как и мы, приведут в пример Elastic Beanstalk. Мы его тоже какое-то время использовали в продуктовой среде, но проблем с ним было достаточно, чтобы они не поместились в эту статью.
Упрощение «configuration management»
В своё время я услышал весьма интересное сравнение платформы оркестрации с запуском процессов на CPU операционной системой. Вам ведь нет никакого дела до того, на каком ядре работает та или иная программа?
Тот же подход применим и к оркестраторам. Вас, по большому счету, не будет заботить, на какой машине и в каком количестве копий запущен сервис, так как конфигурация динамически обновляется на балансировщике. Вам необходим самый минимум конфигурации хостов в продакшен-окружении. В идеале — только установить Docker. В «ещё большем идеале» — убрать Configuration Management вообще, если у вас CoreOS.
Таким образом, ваш парк машин — это не то, за чем нужно следить днём и ночью, а простой пул ресурсов, части которого можно заменить в любую минуту.
Service-centric подход в инфраструктуре
В последние годы в инфраструктуре веб-приложений ощущается переход с host-centric на service-centric подход. Иными словами, это продолжение предыдущего пункта, когда вместо того, чтобы следить за показаниями хостов, вы следите за внешними показателями сервиса. Философия платформы оркестрации вписывается в такую парадигму гораздо более гармонично, чем если вы будете держать сервис в строго закреплённом пуле хостов.
Также можно приплести к этому пункту микросервисы. Кроме автоматизации деплоя, с оркестрацией легко и быстро создавать новые сервисы и связывать их между собой (Service Discovery инструментарий чаще всего поставляется «в коробке» с оркестраторами).
Приближение инфраструктуры к разработчикам
DevOps для команды разработки iFunny — это не пустой звук, и даже не инженер. Здесь стремятся дать разработчикам максимальную свободу действий для ускорения тех самых Flow, Feedback and Experimentation.
В последние год-два активно создаётся API-монолит, постоянно запускаются новые микросервисы. И на практике оркестрация контейнерами очень здорово помогает в быстром старте и стандартизации запуска сервиса как технического процесса. При хорошем подходе разработчик может в любой момент сам создавать новые сервисы, не ожидая пару недель (а то и месяц), пока его задача из общего списка дойдёт до админа.
Найдётся ещё уйма причин, почему хорошо использовать оркестраторы. Можно добавить и про утилизацию ресурсов, но в таком случае даже самый внимательный читатель не дотерпит до конца.
Выбор оркестратора
Здесь можно было бы рассказать про сравнение десятка решений на рынке, бесконечные бенчмарки по запуску контейнеров и развертыванию кластеров, срыв покровов в виде множества багов, блокирующих те или иные возможности продукта.
Но на деле всё гораздо скучнее. В iFunny стараются по максимуму использовать сервисы AWS ввиду того, что команда небольшая, а времени, знаний и опыта для написания собственных велосипедов или «подпиливания» всем известных, как всегда, не хватает. Поэтому было решено идти по проторённой дорожке и взять простой и всем понятный инструмент. И да, ECS как сервис сам по себе бесплатный: вы платите только за EC2 instances, на которых запущены ваши агенты и контейнеры, по стандартному тарифу.
Небольшой спойлер: этот подход сработал, но с ECS появилось немало других вопросов. Исповедь о том, «как жалко, что не Kubernetes», будет в конце статьи.
Терминология
Давайте познакомимся с основными понятиями в ECS.
Cluster → Service → Task
Связка, которую следует запомнить прежде всего. Изначально ECS-платформа представляет собой кластер, который не нуждается в хостинге на инстансе и управляется через API AWS.
В кластере можно запускать задачи из одного или более контейнеров, которые запускаются в общей связке. Иначе говоря, Task — аналог Pod в Kubernetes. Для гибкого управления контейнерами — масштабирования, деплоя и тому подобных вещей — существует концепция сервисов.
Сервис состоит из определённого количества задач. Если мы уже сравниваем ECS с Kubernetes, то сервисы являются аналогом концепции Deployments.
Task Definition
Описание параметров запуска задачи в JSON. Обычная спецификация, которую можно воспринимать как обёртку над командой docker run. Представлены все «ручки» для тегирования и логирования, чего нет, скажем, в связке Docker + Elastic Beanstalk.
ECS-agent
Представляет собой локального агента на instances в виде запущенного контейнера. Занимается контролем состояния instance, утилизацией её ресурсов и передачей команд по запуску контейнеров локальному демону Docker. Исходный код агента доступен на Github.
Application Load Balancer (ALB)
Балансировщик нового поколения от AWS. Отличается от ELB по большей части концепцией: если ELB балансирует трафик на уровне хостов, то ALB балансирует трафик на уровне приложений. В экосистеме ECS балансировщик играет роль пункта назначения пользовательского трафика. Вам не нужно задумываться над тем, как направить трафик на новую версию приложения — вы просто скрываете контейнеры за балансировщиком.
В ALB есть концепция target group, к которой подключаются экземпляры приложения. Именно к target group можно привязать сервис ECS. В такой связке кластер будет забирать информацию о том, на каких портах запущены контейнеры сервиса, и передавать её в target group для распределения трафика из балансировщика. Таким образом, вам не нужно переживать по поводу того, на каком порту открыт контейнер или как не допустить коллизию между несколькими сервисами на одной машине. В ECS это разрешается автоматически.
Task Placement Strategy
Стратегия распределения задач по доступным ресурсам кластера. Стратегия складывается из типа и параметра. Как и в других оркестраторах, существует 3 типа: binpack (т.е. забивать машину до отказа, затем переключаясь на другую), spread (равномерное распределение ресурсов по кластеру) и random (думаю, и так всем понятно). Параметрами могут быть CPU, Memory, Availability Zone и Instance ID.
На практическом опыте в качестве железобетонного варианта была выбрана стратегия распределения задач по Availability Zones (проще говоря, по датацентрам). Таким образом снижается конкуренция за ресурсы машины между контейнерами и стелется солома в случае неожиданного отказа одной из Availability zone в AWS.
Healthy Percentage
Параметр минимальной и максимальной доли от желаемого количества задач, между которыми сервис считается здоровым. Этот параметр пригодится для конфигурации деплоя сервиса.
Само обновление версии приложения может происходить двумя способами:
- если max percentage > 100, то создаются новые задачи в соответствии с параметром, и в таком же количестве после подключения новых задач к трафику убиваются старые; если max percentage = 200, то все происходит за одну итерацию, если 150 — за две и так далее;
- если min percentage < 100, а max percentage = 100, то все происходит наоборот: сначала убиваются старые задачи, чтобы освободить место для создания новых задач; в это время весь трафик принимают оставшиеся задачи.
Первый вариант похож на blue-green deployment и выглядит безупречным, если бы не необходимость держать в 2 раза больше ресурсов в кластере. Второй вариант даёт преимущество в утилизации, но может привести к деградации приложения, если на него приплыло приличное количество трафика. Какой из них выбрать — решать вам.
Autoscaling
Кроме масштабирования на уровне EC2 instances, существует ещё и масштабирование на уровне задач в ECS. Так же, как и в Autoscaling Group, вы можете настроить триггеры для сервиса ECS в рамках Cloudwatch Alarms. Наиболее оптимальный вариант — масштабироваться по проценту используемого CPU от значения, заданного в Task Definition.
Важный момент: параметр CPU, как и в самом Docker, указывается не в количестве ядер, а в величине CPU Units. В дальнейшем время процессора будет распределяться исходя из того, у какой из задач больше юнитов. В терминологии ECS 1 ядро CPU приравнивается к 1024 юнитам.
Elastic Container Repository
Сервис AWS по хостингу Docker образов. Иначе говоря, вы заводите себе Docker Registry бесплатно, без необходимости его хостить. Плюс — в простоте, минус в том, что нельзя иметь больше одного домена, а для каждого сервиса вам приходится отдельно заводить собственный репозиторий.
Интеграция в существующую инфраструктуру
Теперь самое интересное о том, как ECS приживался в iFunny.
Deployment pipeline
Как оркестратор и планировщик ресурсов ECS, может, и хорош, но инструмент деплоя он не даёт. В терминологии для ECS деплой — это обновление сервиса (update service). Для обновления нужно создать новую версию Task Definition, обновить сервис, указав номер ревизии нового definition, подождать, пока он полностью закончит обновление, и откатиться на старую ревизию, если что-нибудь пойдёт не так. И у AWS в момент написания статьи не было готового инструмента, который бы делал всё и сразу. Есть отдельная CLI для ECS, но это, скорее, про аналог Docker Compose, чем про деплой сервисов в изоляции.
К счастью, мир Open Source компенсировал этот недостаток отдельной утилитой ecs-deploy. По факту это shell-скрипт из нескольких сотен строк, однако с прямой задачей справляется очень хорошо. Вы просто указываете сервис, кластер и Docker-образ, который хотите задеплоить, и он выполнит пошагово весь алгоритм. Ещё и откатит в случае фейла обновления, и почистит устаревшие Task Definitions.
Единственным недостатком поначалу у нас была невозможность обновить Task Definition полностью через утилиту. Ну, предположим, вы хотите поменять лимиты по CPU или перенастроить log driver. Но ведь это же shell-скрипт, самая простая вещь для DYI! Эта фича была добавлена в скрипт за пару часов. Она используется и впредь, обновляя сервисы исключительно по Task Definitions, которые хранятся в корне репозиториев приложений.
Правда, на Pull Request уже полгода как не обращают внимание, как и на десяток других. Это к вопросу о минусах Open Source.
Terraform
Через Terraform в iFunny разворачиваются все ресурсы, которые есть в AWS. Не являются исключением и ресурсы, необходимые для работы сервисов: кроме самого сервиса, это Application Load Balancer и связанные с ним Listeners и Target Group, а также ECR-репозиторий, первая версия Task Definition, autoscaling по алармам и необходимые DNS-записи.
Первой идеей было объединить все ресурсы в один модуль Terraform и использовать его каждый раз при создании сервиса. Поначалу выглядело здорово: всего 20 строчек — и у тебя появляется production-ready сервис! Но, как оказалось, поддерживать такую штуку со временем гораздо дороже. Так как сервисы не всегда однородные и постоянно появляются разнообразные требования, то приходилось править модуль практически каждый раз при его использовании.
Чтобы не думать о «синтаксическом сахаре», пришлось вернуть всё на круги своя, описывая в Terraform state пошагово все ресурсы, завернув в маленькие модули те вещи, которые можно завернуть: Load Balancing и Autoscaling.
В какой-то момент state вырос настолько, что один план с его обновлением занимал порядка 5–7 минут, а сам он мог быть заблокирован другим инженером, который что-то поднимает на нём прямо сейчас. Эта проблема решилась разделением одного большого state на несколько маленьких для каждого сервиса.
Monitoring & logging
Здесь всё вышло предельно прозрачно и просто. В дашборды и алерты добавили пару новых метрик по утилизации сервисов и ресурсов кластера, чтобы было наглядно видно, в какой момент сервисы начали масштабироваться и насколько хорошо это в итоге сработало.
Логи мы, как и прежде, писали в локальный агент Fluentd, который доставлял их в Elasticsearch с дальнейшей возможностью прочитать их в Kibana. ECS поддерживает любой log-driver, который есть в Docker, в отличие от того же Beanstalk, и это настраивается в рамках Task Definition.
Также в AWS можно попробовать драйвер awslogs, который выводит логи прямо в management console. Полезная штука, если у вас не так много логов, чтобы отдельно поднимать и поддерживать систему сбора логов.
Scaling & resource distribution
Вот здесь и была большая часть боли. Стратегия масштабирования сервисов выбиралась долгим путём метода проб и ошибок. Из этого опыта стало понятно, что:
- Binpack по CPU, конечно, хорошо утилизирует кластер, но при наплыве нагрузки всё может прилечь на минуту-две, пока Docker не разберётся, как в таких условиях поделить время CPU;
- Ни у одного оркестратора (в том числе и у ECS) в природе нет понятия динамического rebalancing контейнеров. Например, проблему скейлинга в момент пиков можно было бы решить добавлением новых хостов в кластер, чтобы кластер равномерно распределил ресурсы. Но они будут простаивать до тех пор, пока на каком-нибудь сервисе не запустится обновление. Эту тему остро обсуждали в Docker Swarm, но она так и остается нерешённой. Скорее всего, из-за сложности решить её как концептуально, так и технически.
В итоге под нагрузками было принято решение масштабировать сервисы моментально и в большом объеме, а инстансы — по достижении 75% резерваций ресурсов. Возможно, не самый лучший вариант с точки зрения утилизации железа, но, по крайней мере, все сервисы в кластере будут стабильно работать, не мешая друг другу.
Подводные камни
Попробуйте вспомнить случай, когда внедрение чего-то нового для инженеров заканчивалось стопроцентным хеппи-эндом. Не можете? Вот и в iFunny эпизод с ЕCS не стал исключением.
Отсутствие гибкости в healthcheck
В отличие от Kubernetes, где можно гибко настроить проверку доступности и готовности сервиса, в ECS критерий только один: отдаёт ли приложение код 200 (или любой другой, настроенный вами) по одному URL. Критериев того, что сервису плохо, только два: либо контейнер не запустился вообще, либо запустился, но не ответил на healthcheck.
Это создаёт проблемы, например, когда при деплое ломается ключевая часть сервиса, но на check он по-прежнему отвечает. В таком случае придётся передеплоить старую версию самому.
Отсутствие Service Discovery как такового. AWS предлагает свой вариант Service Discovery, но это решение выглядит, мягко говоря, так себе. Наилучшим вариантом в такой ситуации является реализация связки Consul agent + Registrator внутри хостов, чем команда разработки iFunny сейчас и занимается.
Сырая реализация запуска задач по расписанию
Если непонятно, то я про cron. Буквально с июня прошлого года в ECS появилась концепция Scheduled Tasks, позволяющая запускать на кластере задачи по расписанию. Данная фича уже давно доступна для клиентов, однако в эксплуатации до сих пор кажется сырой по многим причинам.
Во-первых, API создаётся не сама задача, а 2 ресурса: Cloudwatch Event с указанием параметров запуска и Cloudwatch Event Target с указанием времени запуска. Со стороны это выглядит непрозрачно. Во-вторых, нет нормального декларативного инструмента деплоя этих задач.
Попробовали решить проблему с помощью Ansible, но в нём пока плохо обстоят дела с шаблонизацией задач.
В конечном итоге для деплоя в iFunny используется самописная утилита на Python с описанием задач в YAML-файле, в планах сделать полноценный инструмент для развертывания cron-задач на ECS.
Отсутствие прямой связи между кластером и хостами
Когда по разным причинам удаляется EC2 instance, он не дерегистрируется в кластере, и все запущенные на нём задачи попросту падают. Так как балансировщик не получил сигнал по выводу таргета из кластера, он будет слать на него запросы вплоть до того момента, пока не поймёт, что контейнер недоступен. Это занимает 10–15 секунд, и за это время вы получаете кучу ошибок от сервера.
На сегодняшний день проблему можно решить с помощью lambda-функции, которая реагирует на удаление instance из Autoscaling Group и отправляет в кластер запрос на удаление задач этой машины (в терминологии — instance draining). Таким образом, instance прибивается только после того, как с него убрали все задачи. Работает хорошо, но Lambda в инфраструктуре всегда выглядит костылём: такое можно было бы включить в функционал платформы.
Отсутствие детализированного мониторинга
API AWS отдаёт от кластера только число зарегистрированных машин и метрики по доле зарезервированных мощностей, от сервиса — только число задач и утилизацию CPU и памяти в проценте от количества, установленного в Task Definition. Здесь наступает боль для адептов церкви «метрик». Отсутствие детализации по использованию ресурсов конкретным контейнером может сыграть злую шутку при отладке проблем с перегрузкой сервиса. Также не помешали бы метрики по утилизации I/O и сети.
Дерегистрация контейнеров в ALB
Важный момент, вычитанный из документации AWS. Параметр deregistration_delay в балансировщике — это не время таймаута ожидания дерегистрации таргета, а полное время ожидания. Иными словами, если параметр равен 30 секундам, а ваш контейнер будет остановлен через 15 секунд, то балансировщик будет по-прежнему присылать запросы на таргет и отдавать клиенту пятисотые ошибки.
Выходом является установить deregistration_delay сервиса выше аналогичного параметра у ALB. Вроде бы очевидно, но явно нигде в документации не прописано, что поначалу доставляет проблемы.
Vendor lock-in внутри AWS
Как и любой облачный сервис от AWS, вы не можете использовать ECS вне AWS. Если вы по каким-то причинам задумались переезжать на Google Cloud или (зачем-то) на Azure, то в таком случае вам нужно будет полностью переделать оркестрацию сервисов.
Простота
Да-да, ECS и его окружение в виде продуктов AWS настолько просты, что с ними сложно реализовывать неординарные задачи в архитектуре вашего приложения. Например, вам нужна на сервисе полная поддержка HTTP/2, но вы этого не можете сделать, так как ALB не поддерживает Server Push.
Или вам нужно, чтобы приложение принимало запросы на 4 уровне (неважно, TCP или UDP), но в ECS вы также не найдёте решение на тему того, как передать трафик на сервис, так как ALB работают только по HTTP/HTTPS, а старый ELB не работает с сервисами ECS и вообще иногда искажает трафик (например, так получалось у нас с gRPC).
Ретроспектива
Подытожив все плюсы оркестрации, упомянутые в начале статьи, можно с уверенностью сказать, что все они являются правдивыми. Теперь у iFunny есть:
- простой и безболезненный деплой;
- меньше самописного кода и конфигурации в Ansible;
- управление юнитами приложения вместо управления хостами;
- запуск сервисов в продакшен с нуля за 20–30 минут напрямую разработчиками.
Но всё ещё нерешённым остается вопрос по утилизации ресурсов.
Последним шагом к полному переносу приложений на ECS являлась миграция основного API. Хоть и прошло всё это быстро, плавно и без даунтайма, остался вопрос о целесообразности использования оркестраторов для больших монолитных приложений. Для одного юнита приложения зачастую приходится выделять отдельный хост, для надёжного деплоя нужно сохранять headroom в виде нескольких незанятых машин, а configuration management в том или ином виде все ещё присутствует. Разумеется, ECS решил многие другие вопросы в позитивную сторону, однако факт остается в том, что с монолитами ты не получишь очень большой выгоды в оркестрации.
В масштабе получилась следующая картина: 4 кластера (один из них — тестовое окружение), 36 сервисов в production и порядка 210–230 запущенных контейнеров в пики, а также 80 задач, которые запускаются по расписанию. Время показало, что с оркестрацией масштабироваться гораздо быстрее и проще. Но если у вас достаточно небольшое количество сервисов и запущенных контейнеров, то вам нужно ещё подумать, нужна ли вам вообще оркестрация.
- Как назло, после всей этой баталии AWS начал запускать собственный сервис для хостинга Kubernetes под названием EKS. Процесс находится на самой ранней стадии и ещё пока нет отзывов о его использовании в продакшене, однако всем и так понятно, что теперь и в AWS можно в две кнопки настроить самую популярную платформу оркестрации и всё ещё иметь доступ к большинству её «ручек». Возвращаясь в тот момент, когда выбирался оркестратор, Kubernetes был бы в приоритете благодаря его гибкости, богатого функционала и быстрого развития проекта.
Также в AWS появился сервис ECS Fargate, который запускает контейнеры без необходимости хостить EC2 instances. В iFunny его уже попробовали его на паре тестовых сервисов и можем сказать, что пока ещё рано делать какие-либо выводы о его возможностях.
P.S. Статья получилась достаточно большой, но даже на этом не заканчиваются все наши случаи с ECS. Задавайте в комментариях любые вопросы по теме или поделитесь собственными успешными кейсами внедрения.