Меня зовут Никита, я системный инженер в компании SEMrush. И в этой статье я расскажу вам, что такое Immutable Infrastructure, какие у этого подхода есть преимущества и недостатки и как мы его используем в компании.
Если вы ни разу не слышали такое словосочетание, то усаживайтесь поудобнее, будет интересно.
Что это такое
Immutable Infrastructure — подход к созданию неизменяемой инфраструктуры. Идея его не нова в мире, хоть и не слишком распространена. Мы начали его использовать, когда поняли, что не все можно запустить в Kubernetes, и нам нужны виртуальные машины.
Это подход о виртуалках, к которым надо относиться как к "пакетам" или контейнерам. Образ виртуальной машины, приложение и его окружение — неделимое целое. Деплой новой версии приложения подразумевает создание нового образа виртуальной машины, развертывание из него виртуалки и введение машины в строй на замену "старых" виртуалок. В общем, это практически попытка сделать контейнер из виртуалки.
Ниже мы рассмотрим преимущества и недостатки этого подхода, но прежде я хотел бы вставить небольшую оговорку.
Мы в компании активно используем облако GCP, поэтому не админим “железо”, на котором запускаются виртуалки. У GCP есть мощный API и множество SaaS продуктов, использование которых делает возможным применение Immutable подхода. Если у вас KVM-хосты, и вы выкатываете виртуалки скриптиком, то вряд ли вам подойдет такое решение. Но при использовании любого облака (Openstack, AWS, Azure и т.д.), можно имплементировать данный подход. Я опишу его применение совместно со всей мощью облака, где, например, создание виртуалки происходит POST-запросом, а увеличение диска производится на лету.
Преимущества Immutable
Такой подход к организации инфраструктуры несет в себе практически все те же плюсы, что дает Docker и Kubernetes, только для виртуальных машин, а именно:
- Повторяемое окружение
Обычно на сервер устанавливается операционная система определенной версии, настраивается окружение для приложения и происходит деплой самого приложения. Но если вы захотите через какое-то время развернуть точно такой же сервер с вашим приложением, то “точно такой же” у вас никак не получится: какие-то пакеты устарели, на другие пакеты выпущены security патчи, ядро уже тоже обновилось. В итоге вы получаете два “похожих” сервера. И вот эта разница часто является ключевой и может привести к багам и ошибкам. С Immutable Infrastructure у вас всегда есть образ виртуальной машины, который точно запустится и будет повторять реальное окружение точь-в-точь. В нем уже все предустановлено: все системные пакеты и библиотеки одной версии, а окружение приложения не изменится со временем. Вы можете повторно развернуть или масштабировать сервис спустя годы, лишь бы у вас был образ. - Инфраструктура как код
Immutable infrastructure всегда описана как код, потому что на каждый коммит (релиз) нам нужно создавать новый образ виртуальной машины, а также проводить его через пайплайны CI/CD. Это не получится делать руками. - Неизменяемое окружение
Как и в случае с Docker контейнером, виртуальная машина не может быть изменена. Не придет какой-нибудь configuration management инструмент или unattended upgrades, которые установят новые версии пакетов и все сломают. Там просто нет этих механизмов. Они не нужны и даже специально отключаются, потому что все, что нужно для функционирования приложения, уже установлено в системе. Окружение неизменяемо во времени и всегда одно и то же, пока не произошел релиз новой версии приложения. - Отсутствие configuration drift
Может быть такое, что у вас есть 100 серверов, на которые нужно вылить новый апдейт, и на несколько из них выкладка зафейлилась по какой-то причине. Тогда ваша конфигурация разъезжается, и вы не имеете одинакового окружения на всех машинах в вашей системе. Или у вас работает unattended upgrade, который периодически ставит обновления безопасности, и где-то ему удалось поставить пакет, а где-то нет. Immutable же гарантирует неизменяемость виртуалок во времени, а также то, что на них одна одинаковая версия всех приложений, зависимостей и операционной системы. - Независимость от внешних источников
В ходе настройки окружения приложению необходимы различные зависимости, которые тянутся из внешних источников. Если внешний источник недоступен, то создание новых инстансов может быть зафейлено. С Immutable такого не произойдет. Все зависимости уже установлены в образе, что гарантирует работоспособность приложения после старта. С таким подходом мы выносим failure point на этап билда образа, а не на этап деплоя, когда что-то пошло не так при обновлении на production VM. После того, как образ собран, мы точно знаем, что виртуалка рабочая, и осталось задеплоить ее как обычно. - Отсутствие устаревших систем
С Immutable нет проблемы "старых" серверов с высоким аптаймом, которые страшно не только обновить, но и даже перезагрузить. Любое изменение — это новый билд образа и новая виртуалка. - Простое масштабирование
Всегда есть некий срез во времени вашего приложения, поднять новый инстанс которого можно по щелчку пальцев. - Все плюсы облака
В облаке дешево и легко создавать новые ресурсы, запрашивать столько CPU и RAM, сколько необходимо. Кроме того, можно интегрировать виртуалки с другими сервисами GCP: Load balancers, GCS для бэкапов, Autoscaler, Cloud SQL и т.д. - Трансформация разработки
Такой подход к инфраструктуре и осознание того, что инфраструктурная часть неотделима от самого кода и приложения, что минимальная единица — это виртуальная машина, заставляет изменить и подход к разработке приложения. Нужно проектировать свое приложение исходя из того, что оно будет immutable. Приложение должно "осознавать" свою эфемерность и быть готовым быть замененным новой версией самого себя. Деплой таких приложений тоже отличается, поэтому нужно предусмотреть нюансы на этапе проектирования. - Базовые образы
Можно создать образы виртуальных машин различных ОС с предустановленным ПО, которые будут являться базовыми. Далее их можно расширять, устанавливая туда конкретные приложения и окружения для них. Все по аналогии с Docker образами. - Простые откаты
Чтобы откатиться на предыдущую версию, достаточно просто поднять виртуалку с предыдущим образом. Как будто бы запускаешь контейнер. - Идентичные окружения
Вы собираете новый образ виртуальной машины и запускаете его в Dev окружении на тестирование. Если тестирование прошло успешно, вы запускаете ТОТ ЖЕ САМЫЙ ОБРАЗ (только с другим конфигом для provisioner) в Prod окружении.
Но есть нюансы
Никогда не бывает так, что все так радужно, и нет никаких подводных камней. В случае с Immutable потенциальные проблемы тоже есть. Все зависит от конкретного приложения, как оно хранит данные, может ли оно быть масштабировано, есть ли у него кластеризация, как часто оно деплоится, как обрабатываются запросы и т.д. Так что следует рассматривать этот список как возможные проблемы при использовании Immutable.
- Более сложный деплой
Immutable подразумевает, что каждый раз при любом изменении вы создаете новый образ, на основе которого запускаете виртуалку. После этого вам может понадобится сконфигурировать приложение (чтобы оно получило свои переменные, конфиги, вступило в кластер и т.д.). Это все необходимо автоматизировать и это может быть достаточно сложно. Пайплайн обновления и его логика могут получаться развесистыми. - Даунтайм при обновлениях
Если есть приложение, которое крутится на виртуалке и использует какие-то данные, хранящиеся локально, то выкладка новой версии приложения подразумевает ряд процессов: выключение текущей виртуалки, отсоединение от нее диска, поднятие нового инстанса, подключение диска к нему. Понятно, что это ведет к даунтайму (по правде сказать, в таком случае перерыв в работе сервиса, но просто более короткий, был бы даже если бы и не использовался Immutable подход). Узкие места: единственный инстанс и локальный storage. Решить это можно через системы динамической конфигурации, несколько инстансов (горячий, холодный резерв или одновременная работа), удаленный storage (но тут уже будет возможный downgrade производительности дисковой подсистемы). Все это зависит от конкретного кейса и архитектуры приложения. Если это Stateless воркеры, то для них данная концепция подходит как никакая другая. А у нас, например, на инфраструктурных сервисах есть SLA, в рамках которого мы иногда можем сделать даунтайм для обновления. - Отсутствие постоянных апдейтов
Как это ни странно, но у Immutable есть обратная сторона своего же преимущества — это отсутствие постоянных апдейтов, security патчей и т.д., которые в обычной системе могут быть настроены на автоматическую установку. В образе не обновится ничего, пока вы не соберете и не задеплоите обновленный вами образ.
Выше я описал данный подход как идеальный случай. Мы стараемся придерживаться Immutable идеологии по максимуму, но на сколько полно получится реализовать идею, зависит от приложения, его сложности, архитектуры, особенностей деплоя и т.д. Иногда не получится автоматизировать прям всё-всё (или это будет дорого и трудозатратно), поэтому всегда нужно придерживаться здравого смысла. Не надо делать Immutable ради Immutable. В первую очередь нужно понимать, какие преимущества вы получите и достаточно ли их. И готовы ли вы к недостаткам.
Push и Pull модели
При проектировании нужно учитывать, какой модели будет соответствовать ваше приложение. Существует два основных пути:
Push
Такая модель не подразумевает, что приложение может автоматически масштабироваться, а соответственно, количество виртуалок строго постоянно в течение времени. Это значит, что новая машина запускается по коммиту — собирается новый образ, создается новая виртуалка, CI/CD накатывает в нее необходимую конфигурацию, приложение запускается. То есть конфигурация происходит снаружи — пушится в виртуалку.
Pull
При таком подходе может работать автоскейлинг, а виртуалки будут в инстанс группе (ну или использовать схожий механизм). Это значит, что создание новой виртуалки будет происходить автоматически, а не по воли оператора или процесса CI. Новосозданная машина должна иметь в себе все необходимое для само конфигурации. То есть конфигурация происходит изнутри — пуллится самой виртуалкой (в себя).
В зависимости от модели нужно по-разному подходить к проектированию CI/CD пайплайна и процесса деплоя.
В главных ролях
При работе с неизменяемой инфраструктурой нам помогают эти инструменты:
- Packer — программа от Hashicorp, которая позволяет создавать образы виртуальных машин, на базе разных облачных провайдеров. Packer берет указанный базовый образ, создает из него виртуалку в нужном облачном провайдере, накатывает в нее конфигурацию с помощью любого из множества провижинеров, а в конце создает из ее диска установочный образ, который можно использовать в выбранном облаке.
- Terraform — еще одна утилита от Hashicorp, которая, наверное, уже не нуждается в представлении. Позволяет описать необходимые инфраструктурные ресурсы нужного облачного провайдера и, используя этот манифест, приводит конфигурацию облака к описанному в конфиге.
- Ansible — инструмент, с которым также знакомы почти все. Он нам нужен для того, чтобы делать provision образа и создаваемой виртуалки — настраивать окружение.
- Gitlab CI — мы используем Gitlab, поэтому и всю автоматизацию пишем на нем.
- GCP — собственно, облако, которое делает запуск виртуалки легким и простым, а также позволяет создавать множество других ресурсов.
Immutable Images
Образ VM является основным артефактом деплоя. У нас есть собственные базовые образы, которые собираются на основе образов Google, специально заточенных под работу в их облаке, образы приложений с каким-то предустановленным, но не сконфигуренным ПО (nginx, mysql, mongodb и т.д.), и продуктовые образы. Последние содержат в себе все необходимое для работы конкретного продукта или его компонента (это может быть образ с настроенной БД, вэб-сервером, бэкэндом). Как можно понять, каждый образ в иерархии базируется на каком-то родителе и расширяет его. Для конфигурации образов (подготовки окружения) мы используем Ansible — отличный инструмент, позволяющий доставлять one-shot конфигурацию.
Если вы разрабатываете концепцию образов виртуальных машин, важно как можно раньше придумать и внедрить конвенцию именования образов. Имя образа должно четко и однозначно определять ОС, на базе которого он собран, и его основную функцию (ПО, его версия), для чего он был собран. Тогда в дальнейшем будет намного проще и удобнее с ними работать.
Image Family
У образа в GCP есть имя, метки, статус, а также принадлежность к какой-то Image Family.
Image Family — удобный механизм, объединяющий все версии одного образа под одной "семьей". Это упрощает использование базовых образов — если вам не нужно точно знать, какую вы используете версию образа, его название, которое включает дату и т.д., вы просто указываете image family, на базе которого вы хотите собраться, и все. Это аналог тега latest в докере.
Однако, если при сборке образа использование Image Family вполне оправдано, то при деплое вам обязательно нужно использовать Image Name и указывать конкретный образ, который вы хотите задеплоить (только что собранный). Как и latest в докере, использование family может привести к дрифту версий или неожиданным изменениям на проде.
Кратко принцип работы с Image Family выглядит так:
У вас есть image-v1 — это самая последняя версия образа вашего приложения. my-image-family указывает на данный образ
gcloud compute images create image-v1 --source-disk disk-1 --source-disk-zone us-central1-f --family my-image-family
gcloud compute images describe-from-family my-image-family
family: my-image-family
id: '50116072863057736'
kind: compute#image
name: image-v1
Вы создаете новый образ image-v2, и теперь my-image-family указывает на него.
gcloud compute images create image-v2 --source-disk disk-2 --source-disk-zone us-central1-f --family my-image-family
gcloud compute images describe-from-family my-image-family
family: my-image-family
id: '50116072863057756'
kind: compute#image
name: image-v2
По какой-то причине нужно откатиться на предыдущую версию, и теперь my-image-family снова указывает на image-v1:
gcloud compute images deprecate image-v2 --state DEPRECATED --replacement image-v1
gcloud compute images describe-from-family my-image-family
family: my-image-family
id: '50116072863057736'
kind: compute#image
name: image-v1
Обновление и ротация базовых образов
У образов есть свой жизненный цикл. Ведь если мы обновили образ, мы это сделали не просто так? Скорее всего были установлены обновления безопасности, новые версии приложений, имеющие новые фичи и т.д. Пользователям надо как-то сказать, что их образ является устаревшим и есть образ новее.
У образов виртуальных машин в GCP есть текущее состояние:
READY — образ готов к использованию
DEPRECATED — образ признан deprecated. Такие образы не отображаются в веб-консоли по-умолчанию, но доступны для gcloud и terraform. В случае их использования выдается предупреждение.
OBSOLETE — образ в этом статусе уже недоступен для использования.
DELETED — образ помечен удаленным, но еще существует физически.
REALLY_DELETED — такого статуса в реальности нет, но спустя некоторое время, как образ был помечен DELETED, он будет удален по-настоящему.
Используя эти статусы, мы создали процесс ротации образов. На картинке ниже представлен жизненный цикл образа и его основные этапы.
Для базовых образов мы делаем автообновление. Раз в сутки производится автоматическая проверка наличия обновления пакетов, входящих в состав образа. Если обновления найдены, производится сборка новой версии образа и указатель image-family сдвигается на нее. После этого происходит ротация всех образов, относящихся к данной image-family.
Для автоматизации жизненного цикла мы написали скрипт image-rotator, который в ближайшее время мы выложим в публичный доступ (в статью будет добавлена ссылка). Его принцип работы следующий:
- Скрипту передаются нужные переменные, главной из которых является
--image-family
. Так он понимает, с чем ему нужно работать - Скрипт находит все образы, принадлежащие данной Image Family, и дальше работает со всеми этими образами КРОМЕ последнего актуального образа, на который указывает Image Family в данный момент
- Далее среди этих образов он делает следующее:
- Все READY образы делает DEPRECATED и проставляет им дату, когда они должны стать OBSOLETE
- Все DEPRECATED образы, у которых дата obsolete меньше текущей даты, переводит в состояние OBSOLETE и выставляет дату, когда образ должен стать DELETED
- Все OBSOLETE образы обрабатывает по аналогии с DEPRECATED
- То же самое с DELETED. Если у DELETED образа подошла дата удаления, то скрипт удаляет образ навсегда
Схема пайплайна
Итак, теперь, когда мы разобрались с основными моментами, можно рассмотреть общий процесс деплоя.
На рисунке ниже, схематично, представлен данный процесс.
Мы подразумеваем, что сначала деплоим приложение в Dev-среду из Master-ветки (название ветки здесь не принципиально), где оно тестируется. После чего, деплоим тот же самый образ на Prod-окружение из другой ветки — stable, посредством синхронизации двух веток через Merge Request.
Этот пайплайн схематичен и может быть усложнен для достижения конкретных целей или абсолютной автоматизации. Например, в наших пайплайнах, дополнительно к тому, что показано на схеме автоматизировано:
- Интеграция с Hashicorp Vault для получения секретов
- Использование снапшотов дисков с данными Prod-инстанса для создания Dev окружения
- Использование последнего бэкапа CloudSQL (примечание: база данных как сервис от Google (MySQL, PostgreSQL)) Prod-инстанса для создания базы данных в Dev-окружении
- Удаление Dev-окружения в конце тестирования (это же облако, нам не хочется тратить лишние деньги)
Дальше Terraform создает виртуалку с приложением в Dev-окружении, создает диск со свежими данными из образа, сделанного в п.1. Так мы получаем свежие независимые данные, на которых протестируем новую виртуалку. Бонусом “случайно” проверяем бэкапы Prod’а :)
Заключение
На этом с теорией покончено. И к сожалению (или к счастью для тех, кто не любит много буков), на самом интересном я заканчиваю свое изложение, так как статья уже получилась достаточно объемной, и реальный пример сервиса, разворачиваемого с помощью данной концепции, она уже не вместит. Напишите в комментариях, если статья получилась интересной/полезной, и хочется посмотреть, как это сделано на практике: с кодом, реальным CI/CD и пояснениями. Я буду благодарен вам за обратную связь.
Immutable Infrastructure — подход, который имеет свои плюсы и минусы. Подход, который подойдет не для каждого сервиса или приложения. И я повторюсь, использовать его надо, понимая и принимая все его плюсы и минусы. И конечно, пусть всегда торжествует здравый смысл. Вы всегда можете взять какую-то его часть и использовать только ее, если это дает вам преимущества.
Сейчас мы в команде работаем над усовершенствованием концепции и нашей реализации, так как через некоторое время использования ее в том виде, в котором она описана здесь, мы поняли, что нам есть над чем работать, и у нас есть идеи как все сделать лучше. И когда все получится, мы напишем еще одну статью, в которой поделимся нашими наработками.
Ikors
Мне кажется, пригодилось бы объяснение того, в каких случаях этот подход применим. Лично у меня без этого рецепты, как сделать контейнер из виртуалки вызывают только вопрос "Зачем?". Контейнеры же и так есть :)
nikitashalnov Автор
Да, согласен.
У нас приоритет — запуск приложения в GKE. Но не всегда GKE подойдет, особенно в случаях если:
Вам не подойдет GKE, если ваше приложение соответствует любому из этих пунктов:
— Приложение не может быть докеризовано
— Приложению необходимо очень много ресурсов CPU и RAM
— Приложение не может быть портировано или запущено в GKE по какой-то причине
— Это база данных (тут мы предпочитаем использовать бд как сервис, но не всегда это прокатывает)
Кроме того, мы разворачиваем различные инфраструктурные сервисы в Immutable манере:
— Artifactory (мы не можем запустить его в кубах, потому что от него зависят кубы — с него пулятся docker образы)
— Gitlab (мы не можем запустить его в кубах, потому что официальный helm чарт не поддерживал работу Gitlab Pages)
— Sentry (у них поставка приложения происходит через docker-compose, очень много компонентов, clickhouse, redis, БД и тд, и нам показалось проще запустить это на виртуалке)
— Aptly (если не ошибаюсь, с докером тоже не вышло)
Но Immutable отлично подойдет для Stateless приложений. Хотя для нас интереснее Stateful, потому что все сервисы выше как раз имеют State и БД. А Stateless запускать в Immutable сам бог велел — только сделай образ и конфигуратор)
gecube
Можете. Просто разворачивается +1 кластер со служебными компонентами (вроде артифактори, Gitlab etc)
По каждому из сервисов есть что сказать.
А нужен ли аптли, если есть артифактори ?