Всем привет! Меня зовут Павел Агалецкий. Я работаю тимлидом в команде, которая разрабатывает систему доставки Lamoda. В 2018 году я выступал на конференции HighLoad++, а сегодня хочу представить расшифровку своего доклада.
Моя тема посвящена опыту нашей компании по деплою систем и сервисов в разные среды. Начиная от наших доисторических времен, когда мы деплоили все системы в обычные виртуальные сервера, заканчивая постепенным переход от Nomad к деплою в Kubernetes. Расскажу о том, почему мы это сделали и какие у нас были в процессе проблемы.
Начнем с того, что 3 года назад все системы и сервисы компании деплоились на обычные виртуальные сервера. Технически было организовано так, что весь код наших систем лежал и собирался за счет средств автоматической сборки, с помощью jenkins. При помощи Ansible он из нашей системы контроля версий раскатывался уже на виртуальные сервера. При этом каждая система, которая была в нашей компании, деплоилась, как минимум, на 2 сервера: одна из них — на head, вторая — на tail. Эти две системы были абсолютно идентичны между собой по всем своим настройкам, мощности, конфигурации и прочему. Отличие между ними было лишь в том, что head получал пользовательский трафик, а tail никогда трафик пользователей на себя не получал.
Для чего это было сделано?
Когда мы деплоили новые релизы нашего приложения, то хотели обеспечить возможность бесшовной раскатки, то есть без заметных последствий для пользователей. Достигалось это за счет того, что очередной собранный релиз с помощью Ansible выкатывался на tail. Там люди, которые занимались деплоем, могли проверить и убедиться, что все хорошо: все метрики, разделы и приложения работают; запускаются нужные скрипты. Только после того, как они убеждались, что все ок, трафик переключался. Он начинал идти на тот сервер, который до этого был tail. А тот, который до этого был head-ом, оставался без пользовательского трафика, при этом с имеющейся на нем предыдущей версией нашего приложения.
Таким образом, для пользователей это было бесшовно. Потому что переключение одномоментное, так как это просто переключение балансировщика. Очень легко можно откатиться на предыдущую версию, просто переключив балансировщик назад. Также мы могли убедиться в способности приложения на продакшн еще до того, как на него пойдет пользовательский трафик, что было достаточно удобно.
Какие мы видели во всем этом достоинства?
Но в этом всем мы также видели несколько недостатков:
Поэтому мы приняли решение, что будет удобнее перейти от деплоя обычных виртуальных машин к деплою наших приложений в контейнере docker. При наличии docker нужна система, которая сможет запустить приложение в кластере, так как вы не сможете поднять просто так контейнер. Обычно хочется следить за тем, сколько контейнеров поднято, чтобы они поднимались автоматически. По этой причине нам нужно было выбрать систему управления.
Мы долго размышляли над тем, какую из них можно взять. Дело в том, что на тот момент этот стек деплоя на обычные виртуальные сервера был у нас несколько устаревшим, так как там были не самые свежие версии операционных систем. В какой-то момент там даже FreeBSD стояла, которую было не очень удобно поддерживать. Мы понимали, что нужно как можно быстрее мигрировать в docker. Наши девопс посмотрели на свой имеющийся опыт работы с разными решениями и выбрали такую систему как Nomad.
Nomad – это продукт компании “HashiCorp”. Также они известны другими своими решениями:
![image](https://habrastorage.org/r/w780q1/webt/4e/vz/4e/4evz4entvdaauztnu558tb-2z9u.jpeg)
«Consul» — это средство для сервис-дискаверинга.
«Terraform» — система для управления серверами, позволяющая вам настраивать их через конфигурацию, так называемую infrastructure-as-a-code.
«Vagrant» позволяет вам разворачивать виртуальные машины локально или в облаке путем определенных файлов конфигурации.
Nomad на тот момент показался достаточно простым решением, на который можно быстро перейти без изменения всей инфраструктуры. К тому же, он достаточно легко осваивается. Поэтому именно его мы и выбрали в качестве системы фильтрации нашего контейнера.
Что нужно для того, чтобы вообще задеплоить вашу систему в Nomad?
Когда мы говорим про Nomad, то в качестве формата информационного файла он использует язык HСL, что расшифровывается как HashiCorp Configuration Language. Это такое надмножество над Yaml, которое позволяет вам описать ваш сервис в терминах Nomad.
![image](https://habrastorage.org/r/w780q1/webt/vg/q9/vb/vgq9vb_izh4i890ro7giihibz6g.jpeg)
Он позволяет сказать, сколько контейнеров хотите задеплоить, из каких images передать им различные параметры при деплое. Таким образом, вы скармливаете этот файл Nomad, а он запускает в соответствии с ним контейнеры в продакшн.
В нашем случае мы поняли, что просто писать под каждый сервис абсолютно одинаковые, идентичные HСL-файлы будет не очень удобно, потому что сервисов много и иногда хочется их обновить. Бывает такое, что один сервис задеплоен не в одном экземпляре, а в самых разных. Например, одна из систем, которая у нас находится в продакшн, имеет более 100 инстансов в проде. Они запускаются из одних и тех же образов, но отличаются конфигурационными настройками и конфигурационными файлами.
Поэтому мы решили, что нам будет удобно хранить все наши конфигурационные файлы для деплоя в одном общем репозитории. Таким образом, они становились обозреваемы: их было легко поддерживать и можно было увидеть, какие системы у нас есть. В случае необходимости также несложно что-то обновить или поменять. Добавить новую систему тоже не составит труда — достаточно просто завести конфигурационный файл внутри новой директории. Внутри нее лежат файлы: service.hcl, который содержит описание нашего сервиса, и некоторые env-файлы, которые позволяют этот самый сервис, будучи задеплоенным в продакшн, настроить.
![image](https://habrastorage.org/r/w780q1/webt/-g/l7/8h/-gl78hl7nbmdbhbuacuntgxw4ow.jpeg)
Однако некоторые наши системы задеплоены в проде не в одном экземпляре, а в нескольких сразу. Поэтому мы решили, что нам удобно будет хранить не конфиги в чистом виде, а их шаблонизированный вид. И в качестве языка шаблонизации мы выбрали jinja 2. В таком формате у нас хранятся как конфиги самого сервиса, так и env-файлы, нужные для него.
Помимо этого, мы поместили в репозиторий общий для всех проектов скрипт-деплой, который позволяет запустить и задеплоить ваш сервис в продакшн, в нужный environment, в нужный таргет. В случае, когда мы превратили наш HCL-конфиг в шаблон, то тот HСL-файл, который до этого был обычным конфигом Nomad, в этом случае стал выглядеть несколько иначе.
![image](https://habrastorage.org/r/w780q1/webt/9a/ib/zv/9aibzvsme34ra2eg53bjfbqt1tw.jpeg)
То есть мы заменили какие-то переменные места конфига на вставки переменных, которые берутся из env-файлов или из других источников. Помимо этого, мы получили возможность собирать HСL-файлы динамически, то есть мы можем применять не только обычные вставки переменных. Так как jinja поддерживает циклы и условия, туда же можно делать конфигурационные файлы, которые меняются в зависимости от того, куда именно вы деплоите свои приложения.
Например, вы хотите деплоить ваш сервис в препродакшн и в продакшн. Допустим, что в препродакшн вы не хотите запускать крон-cкрипты, а просто хотите видеть сервис на отдельном домене, чтобы убедиться, что он функционирует. Для любого, кто деплоит сервис, процесс выглядит очень просто и прозрачно. Достаточно выполнить файл deploy.sh, указать, какой сервис вы хотите задеплоить и в какой таргет. Например, вы хотите задеплоить некоторую систему в Россию, в Белоруссию или Казахстан. Для этого достаточно просто поменять один из параметров, и у вас соберется правильный конфигурационный файл.
Когда сервис Nomad уже оказывается у вас задеплоенным в кластер, он выглядит следующим образом.
![image](https://habrastorage.org/r/w780q1/webt/nu/au/8d/nuau8dqdcwft0boyw57x37wrkcm.jpeg)
Для начала вам нужен некоторый балансировщик снаружи, который будет принимать весь пользовательский трафик в себя. Он будет работать вместе с Consul и узнавать у него, где, на какой ноде, по какому IP-адресу находится конкретный сервис, который соответствует тому или иному доменному имени. Сервисы в Consul появляются из самого Nomad. Поскольку это продукты одной и той же компании, они неплохо связаны между собой. Можно сказать, что Nomad из коробки умеет регистрировать все запускаемые в нем сервисы внутри Consul.
После того как ваш внешний балансировщик узнает о том, в какой сервис необходимо отправить трафик, он перенаправляет его в соответствующий контейнер или в несколько контейнеров, которые соответствуют вашему приложению. Естественно, что при этом необходимо думать и о безопасности. Даже несмотря на то, что все сервисы запускаются на одних и тех же виртуальных машинах в контейнерах, обычно для этого требуется запретить свободный доступ из любого сервиса к любому другому. Мы достигали этого за счет сегментации. Каждый сервис запускался в своей виртуальной сети, на которой были прописаны правила маршрутизации и правила разрешения/запрета доступа к другим системами и сервисам. Они могли находиться как внутри этого кластера, так и вне него. Например, если вы хотите запретить сервису коннектиться к определённой базе данных, это можно сделать путем сегментации на уровне сети. То есть даже по ошибке вы не можете случайно подсоединяться из тестового окружения к вашей продакшн базе.
Во что нам обошелся процесс перехода в плане человеческих ресурсов?
Примерно 5-6 месяцев занял переход всей компании в Nomad. Мы переходили посервисно, но в достаточно быстром темпе. Каждая команда должна была создать свои собственные контейнеры для сервисов.
У нас принят такой подход, что каждая команда отвечает за docker images своих систем самостоятельно. Девопс же предоставляют общую инфраструктуру, необходимую для деплоя, то есть поддержку самого кластера, поддержку CI-системы и так далее. И на тот момент у нас более 60 систем переехали в Nomad, получилось порядка 2 тысяч контейнеров.
Девопс отвечает за общую инфраструктуру всего, что связано с деплоем, с серверами. А каждая команда разработки в свою очередь отвечает за реализацию контейнеров под свою конкретную систему, поскольку именно команда знает, что ей вообще нужно в том или ином контейнере.
Какие достоинства мы получили, перейдя на деплой с помощью Nomad и docker в том числе?
Но мы также столкнулись и с несколькими недостатками:
Оказалось, что мы не смогли достичь бесшовности деплоев в случае Nomad. При выкатке контейнеров из разных условий могло получиться так, что он оказывался запущенным, и Nomad воспринимал его как готовый к принятию трафика контейнер. Это происходило еще до того, как приложение внутри него успело запуститься. По этой причине система на недолгий срок начинала выдавать 500-е ошибки, потому что трафик начинал идти на контейнер, который еще не готов его принять.
Мы столкнулись с некоторыми багами. Самый существенный баг заключается в том, что Nomad не очень хорошо воспринимает большой кластер, если у вас много систем и контейнеров. Когда вы хотите вывезти в обслуживание один из серверов, который включен в кластер Nomad, есть достаточно большая вероятность того, что кластер почувствует себя не очень хорошо и развалится на части. Часть контейнеров может, например, упасть и не подняться — это вам впоследствии очень дорого обойдется, если все ваши системы в продакшн находятся именно в кластере, управляемом Nomad.
Поэтому мы решили подумать, куда нам идти дальше. На тот момент мы стали намного лучше осознавать, чего хотим добиться. А именно: хотим надежности, немножко больше функций, чем дает Nomad, и более зрелую, более стабильную систему.
В этом плане наш выбор пал на Kubernetes как на самую популярную платформу для запуска кластеров. Особенно при условии того, что размер и количество наших контейнеров был достаточно большой. Для таких целей Kubernetes показался самой подходящей системой из тех, что мы могли посмотреть.
Немножко расскажу о том, какие есть основные понятия Kubernetes и чем они отличаются от Nomad.
![image](https://habrastorage.org/r/w780q1/webt/pv/eh/va/pvehvavusoxszoxum9bsuwyjqbc.jpeg)
В первую очередь самым базовым понятием в Kubernetes является понятие pod. Pod — это группа одного или нескольких контейнеров, которые всегда запускаются вместе. И они работают как будто всегда строго на одной виртуальной машине. Они доступны друг другу по айпишнику 127.0.0.1 на разных портах.
Предположим, что у вас есть РНР-приложение, которое состоит из nginx и php-fpm – классическая схема. Скорее всего, вы захотите, чтобы и nginx и php-fpm контейнеры были всегда вместе. Kubernetes позволяет этого добиться, описав их как один общий pod. Как раз этого мы и не могли получить при помощи Nomad.
Вторым понятием является deployment. Дело в том, что pod сам по себе – это эфемерная вещь, она запускается и исчезает. Хотите ли вы сначала убивать все ваши предыдущие контейнеры, а потом запускать сразу новые версии или же вы хотите выкатывать их постепенно – вот именно за этот процесс отвечает понятие deployment. Он описывает то, как вы деплоите ваши podы, в каком количестве и, как их обновлять.
Третьим понятием является service. Ваш service – это фактически ваша система, которая принимает в себя некий трафик, а потом направляет его на один или несколько podов, соответствующих вашему сервису. То есть он позволяет сказать, что весь входящий трафик на такой-то сервис с таким-то именем необходимо отправлять на конкретно эти podы. И при этом он обеспечивает вам балансировку трафика. То есть вы можете запустить два podа вашего приложения, и весь входящий трафик будет равномерно балансироваться между относящимися к этому сервису podами.
И четвертое основное понятие — Ingress. Это сервис, который запускается в кластере Kubernetes. Он выступает как внешний балансировщик нагрузки, который принимает на себя все запросы. За счет API Kubernetes Ingress может определять, куда эти запросы надо отправить. Причем делает он это очень гибко. Вы можете сказать, что все запросы на этот хост и такой-то URL отправляем на этот сервис. А эти запросы, приходящие на этот хост и на другой URL отправляем на другой сервис.
Самое крутое с точки зрения того, кто разрабатывает приложение — это то, что вы способны этим всем управлять самостоятельно. Задав конфиг Ingress, можете весь трафик, приходящий на такой-то API, отправлять на отдельные контейнеры, прописанные, например, на Go. А вот этот трафик, приходящий на тот же домен, но на другой URL, отправлять на контейнеры, написанные на РНР, где много логики, но они не очень скоростные.
Если сравнить все эти понятия с Nomad, то можно сказать, что первые три понятия – это все вместе Service. А последнее понятие в самом Nomad отсутствует. Мы в качестве него использовали внешний балансировщик: это может быть haproxy, nginx, nginx+ и так далее. В случае с кубом вам не надо вводить это дополнительное понятие отдельно. Однако, если посмотреть на Ingress внутри, то это либо nginx, либо haproxy, либо traefik, но как бы встроенный в Kubernetes.
Все описанные мною понятия – это, по сути дела, ресурсы, которые существуют внутри кластера Kubernetes. Для их описания в кубе используется yaml-формат, более читаемый и привычный, чем НСL-файлы в случае с Nomad. Но структурно они описывают в случае, например, podа то же самое. Они говорят – хочу задеплоить такие-то podы туда-то, с такими-то имаджи, в таком-то количестве.
![image](https://habrastorage.org/r/w780q1/webt/2k/ka/53/2kka53a1vp1lm0rnl4xbhmcpyu4.jpeg)
Помимо этого, мы поняли, что не хотим руками создавать каждый отдельный ресурс: deployment, сервисы, Ingress и прочее. Вместо этого мы хотели при деплое описывать нашу каждую систему, имеющуюся в терминах Kubernetes, чтобы не приходилось вручную пересоздавать в нужном порядке все необходимые зависимости ресурсов. В качестве такой системы, которая позволила нам это сделать, был выбран Helm.
Helm – это пакетный менеджер для Kubernetes. Он очень похож на то, как работают пакетные менеджеры в языках программирования. Они позволяют вам хранить сервис, состоящий из, например, deployment nginx, deployment php-fpm, конфига для Ingress, configmaps (это сущность, которая позволяет вам задать env и другие параметры для вашей системы) в виде так называемых чартов. При этом Helm работает поверх Kubernetes. То есть это не какая-то система, стоящая в стороне, а просто ещё один сервис, запускаемый внутри куба. Вы взаимодействуете с ним по его API через консольную команду. Его удобство и прелесть в том, что даже если helm сломается или вы его удалите из кластера, то ваши сервисы не исчезнут, так как helm служит по сути только для запуска системы. За работоспособность и состояние сервисов дальше отвечает сам Kubernetes.
Также мы поняли, что шаблонизация, которую до этого были вынуждены делать самостоятельно через внедрение jinja в наши конфиги, является одной из основных возможностей helm. Все конфиги, которые вы создаете для ваших систем, хранятся в helm в виде шаблонов, похожих немножко на jinja, но, на самом деле, использующих шаблонизацию языка Go, на котором helm написан, как и Kubernetes.
Helm добавляет нам еще несколько дополнительных понятий.
Chart — это описание вашего сервиса. В других пакетных менеджерах его назвали бы пакет, bundle или что-то подобное. Здесь это называется chart.
Values – это переменные, которые вы хотите использовать для сборки ваших конфигов из шаблонов.
Release. Каждый раз сервис, который деплоится с помощью helm, получает инкрементальную версию релиза. Helm помнит, какой был конфиг сервиса при предыдущем, позапрошлом релизе и так далее. Поэтому, если нужно откатиться, достаточно выполнить команду helm callback, указав ему предыдущую версию релиза. Даже если в момент отката соответствующая конфигурация в вашем репозитории не будет доступна, helm все равно помнит, какой она была, и откатит вашу систему на то состояние, в котором она была в предыдущем релизе.
В случае, когда мы используем helm, обычные конфиги для Kubernetes тоже превращаются в шаблоны, в которых есть возможность использовать переменные, функции, применять условные операторы. Таким образом, вы можете собирать конфиг вашего сервиса в зависимости от среды.
На практике мы решили поступить чуть-чуть иначе, чем мы делали в случае с Nomad. Если в Nomad в одном репозитории хранились и конфиги для деплоя, и n-переменные, которые нужны для того, чтобы задеплоить наш сервис, то здесь мы решили их разделить на два отдельных репозитория. В репозитории «deploy» хранятся только n-переменные, нужные для деплоя, а в репозитории «helm» хранятся конфиги или чарты.
![image](https://habrastorage.org/r/w780q1/webt/2r/lo/yt/2rloytnvlrj6nri8vj7e9-hccg8.jpeg)
Что это нам дало?
Несмотря на то, что в самих конфигурационных файлах мы не храним какие-то действительно чувствительные данные. Например, пароли к базам данных. Они хранятся в виде secrets в Kubernetes, но тем не менее, там все равно есть отдельные вещи, к которым мы не хотим давать доступ всем подряд. Поэтому доступ к репозиторию «deploy» более ограничен, а репозиторий «helm» содержит просто описание сервиса. По этой причине в него можно дать доступ безопасно большему кругу лиц.
Поскольку у нас есть не только продакшн, но и другие среды, то благодаря такому разделению мы можем переиспользовать наши helm-чарты, чтобы деплоить сервисы не только в продакшн, но и, например, в QA-среду. Даже для того, чтобы разворачивать их локально, используя Minikube — это такая штука для локального запуска Kubernetes.
Внутри каждого репозитория мы оставили разделение на отдельные директории для каждого сервиса. То есть внутри каждой директории находятся шаблоны, относящиеся к соответствующему чарту и описывающие те ресурсы, которые необходимо задеплоить для запуска нашей системы. В репозитории «deploy» мы оставили только энвы. В этом случае мы не стали использовать шаблонизацию с помощью jinja, потому что helm сам дает шаблонизацию из коробки – это одна из его основных функций.
Мы оставили скрипт для деплоя – deploy.sh, который упрощает и стандартизирует запуск для деплоя с помощью helm. Таким образом, для любого, кто хочет задеплоить, интерфейс деплоя выглядит точно так же, как он был в случае деплоя через Nomad. Такой же deploy.sh, название вашего сервиса, и то, куда вы хотите его задеплоить. Это приводит к тому, что внутри запускается helm. Он в свою очередь собирает конфиги из шаблонов, подставляет в них необходимые values-файлы, затем деплоит, пуская их в Kubernetes.
Сервис Kubernetes выглядит более сложным, чем Nomad.
![image](https://habrastorage.org/r/w780q1/webt/fe/p6/qi/fep6qibp6hhnbsmqifk2jnmg20a.jpeg)
Здесь исходящий трафик приходит в Ingress. Это как раз фронт-контроллер, который принимает на себя все запросы и впоследствии отправляет их в соответствующие данным запроса сервисы. Определяет он их на основе конфигов, которые являются частью описания вашего приложения в helm и которые разработчики задают самостоятельно. Сервис же отправляет запросы на свои podы, то есть конкретные контейнеры, балансируя входящий трафик между всеми контейнерами, которые относятся к данному сервису. Ну и, конечно, не стоит забывать про то, что от безопасности на уровне сети, мы никуда не должны уходить. Поэтому в кластере Kubernetes работает сегментация, которая основана на тегировании. Все сервисы имеют определенные теги, к которым привязаны права доступов сервисов к тем или иным внешним/внутренним ресурсам внутри или вне кластера.
Выполняя переход, мы увидели, что Kubernetes обладает всеми возможностями Nomad, который до этого мы использовали, а также добавляет много нового. Его можно расширять через плагины, а по факту через кастомные типы ресурсов. То есть у вас есть возможность не просто использовать что-то, что идет в Kubernetes из коробки, а создать свой собственный ресурс и сервис, который будет ваш ресурс читать. Это дает дополнительные возможности расширения вашей системы без необходимости переустановки Kubernetes и без необходимости изменений.
Примером такого использования является Prometheus, который запускается у нас внутри кластера Kubernetes. Для того, чтобы он начал собирать метрики с того или иного сервиса, нам нужно добавить в описание сервиса дополнительный тип ресурса, так называемый сервис-монитор. Prometheus за счет того, что умеет вычитывать, будучи запущенным в Kubernetes, кастомный тип ресурсов, автоматически начинает собирать метрики с новой системы. Это достаточно удобно.
Первый деплой, который мы сделали в Kubernetes был в марте 2018 года. И за это время мы никогда не испытывали с ним никаких проблем. Он достаточно стабильно работает без существенных багов. К тому же, мы можем его дальше расширять. На сегодняшний день нам хватает тех возможностей, которые в нем есть, а темпы развития Kubernetes нам очень нравятся. На данный момент больше 3000 контейнеров находятся в Kubernetes. Кластер занимает несколько Node. При этом он обслуживаемый, стабильный и очень контролируемый.
Моя тема посвящена опыту нашей компании по деплою систем и сервисов в разные среды. Начиная от наших доисторических времен, когда мы деплоили все системы в обычные виртуальные сервера, заканчивая постепенным переход от Nomad к деплою в Kubernetes. Расскажу о том, почему мы это сделали и какие у нас были в процессе проблемы.
Деплой приложений на VM
Начнем с того, что 3 года назад все системы и сервисы компании деплоились на обычные виртуальные сервера. Технически было организовано так, что весь код наших систем лежал и собирался за счет средств автоматической сборки, с помощью jenkins. При помощи Ansible он из нашей системы контроля версий раскатывался уже на виртуальные сервера. При этом каждая система, которая была в нашей компании, деплоилась, как минимум, на 2 сервера: одна из них — на head, вторая — на tail. Эти две системы были абсолютно идентичны между собой по всем своим настройкам, мощности, конфигурации и прочему. Отличие между ними было лишь в том, что head получал пользовательский трафик, а tail никогда трафик пользователей на себя не получал.
Для чего это было сделано?
Когда мы деплоили новые релизы нашего приложения, то хотели обеспечить возможность бесшовной раскатки, то есть без заметных последствий для пользователей. Достигалось это за счет того, что очередной собранный релиз с помощью Ansible выкатывался на tail. Там люди, которые занимались деплоем, могли проверить и убедиться, что все хорошо: все метрики, разделы и приложения работают; запускаются нужные скрипты. Только после того, как они убеждались, что все ок, трафик переключался. Он начинал идти на тот сервер, который до этого был tail. А тот, который до этого был head-ом, оставался без пользовательского трафика, при этом с имеющейся на нем предыдущей версией нашего приложения.
Таким образом, для пользователей это было бесшовно. Потому что переключение одномоментное, так как это просто переключение балансировщика. Очень легко можно откатиться на предыдущую версию, просто переключив балансировщик назад. Также мы могли убедиться в способности приложения на продакшн еще до того, как на него пойдет пользовательский трафик, что было достаточно удобно.
Какие мы видели во всем этом достоинства?
- Прежде всего, это достаточно просто работает. Всем понятно, как функционирует подобная схема деплоя, потому что большинство людей когда-либо деплоили на обычные виртуальные сервера.
- Это достаточно надежно, так как технология деплоя простая, опробованная тысячами компаний. Миллионы серверов деплоятся именно так. Сложно что-то сломать.
- И наконец, мы могли получить атомарные деплои. Деплои, которые для пользователей происходят одномоментно, без заметного этапа переключения между старой версией и новой.
Но в этом всем мы также видели несколько недостатков:
- Помимо продакшн-среды, среды разработки, есть и другие среды. Например, qa и preproduction. На тот момент у нас было много серверов и около 60 сервисов. По этой причине приходилось для каждого сервиса поддерживать актуальную для него версию виртуальной машины. Причем, если вы хотите обновить библиотеки или поставить новые зависимости, вам необходимо это сделать во всех средах. Также нужно было синхронизировать время, когда вы собираетесь деплоить очередную новую версию вашего приложения, со временем, когда devops выполнит необходимые настройки окружения. В таком случае легко попасть в ситуацию, когда окружение у нас будет несколько отличаться сразу во всех средах подряд. Например, в QA-среде будут одни версии библиотек, а в продакшн — другие, что приведет к проблемам.
- Сложность в обновлении зависимостей вашего приложения. Это зависит не от вас, а от другой команды. А именно, от команды devops, которая поддерживает сервера. Вы должны поставить для них соответствующую задачу и дать описание того, что хотите сделать.
- В то время мы также хотели разделить имевшиеся у нас большие крупные монолиты на отдельные маленькие сервисы, так как понимали, что их будет становиться все больше и больше. На тот момент у нас уже было их более 100. Необходимо было для каждого нового сервиса создавать отдельную новую виртуальную машину, которую также надо обслуживать и деплоить. Кроме этого, нужна не одна машина, а, как минимум, две. К этому всему еще добавляется QA-среда. Это вызывает проблемы и делает для вас создание и запуск новых систем более сложным, дорогостоящим и долгим процессом.
Поэтому мы приняли решение, что будет удобнее перейти от деплоя обычных виртуальных машин к деплою наших приложений в контейнере docker. При наличии docker нужна система, которая сможет запустить приложение в кластере, так как вы не сможете поднять просто так контейнер. Обычно хочется следить за тем, сколько контейнеров поднято, чтобы они поднимались автоматически. По этой причине нам нужно было выбрать систему управления.
Мы долго размышляли над тем, какую из них можно взять. Дело в том, что на тот момент этот стек деплоя на обычные виртуальные сервера был у нас несколько устаревшим, так как там были не самые свежие версии операционных систем. В какой-то момент там даже FreeBSD стояла, которую было не очень удобно поддерживать. Мы понимали, что нужно как можно быстрее мигрировать в docker. Наши девопс посмотрели на свой имеющийся опыт работы с разными решениями и выбрали такую систему как Nomad.
Переход на Nomad
Nomad – это продукт компании “HashiCorp”. Также они известны другими своими решениями:
![image](https://habrastorage.org/webt/4e/vz/4e/4evz4entvdaauztnu558tb-2z9u.jpeg)
«Consul» — это средство для сервис-дискаверинга.
«Terraform» — система для управления серверами, позволяющая вам настраивать их через конфигурацию, так называемую infrastructure-as-a-code.
«Vagrant» позволяет вам разворачивать виртуальные машины локально или в облаке путем определенных файлов конфигурации.
Nomad на тот момент показался достаточно простым решением, на который можно быстро перейти без изменения всей инфраструктуры. К тому же, он достаточно легко осваивается. Поэтому именно его мы и выбрали в качестве системы фильтрации нашего контейнера.
Что нужно для того, чтобы вообще задеплоить вашу систему в Nomad?
- Прежде всего нужен docker image вашего приложения. Необходимо собрать его и поместить в хранилище образов docker. В нашем случае это artifactory — такая система, которая позволяет пушить в нее различные артефакты разного типа. Она умеет хранить архивы, образы docker, пакеты composer РНР, NРМ-пакеты и так далее.
- Также необходим конфигурационный файл, который скажет Nomad, что, куда и в каком количестве вы хотите задеплоить.
Когда мы говорим про Nomad, то в качестве формата информационного файла он использует язык HСL, что расшифровывается как HashiCorp Configuration Language. Это такое надмножество над Yaml, которое позволяет вам описать ваш сервис в терминах Nomad.
![image](https://habrastorage.org/webt/vg/q9/vb/vgq9vb_izh4i890ro7giihibz6g.jpeg)
Он позволяет сказать, сколько контейнеров хотите задеплоить, из каких images передать им различные параметры при деплое. Таким образом, вы скармливаете этот файл Nomad, а он запускает в соответствии с ним контейнеры в продакшн.
В нашем случае мы поняли, что просто писать под каждый сервис абсолютно одинаковые, идентичные HСL-файлы будет не очень удобно, потому что сервисов много и иногда хочется их обновить. Бывает такое, что один сервис задеплоен не в одном экземпляре, а в самых разных. Например, одна из систем, которая у нас находится в продакшн, имеет более 100 инстансов в проде. Они запускаются из одних и тех же образов, но отличаются конфигурационными настройками и конфигурационными файлами.
Поэтому мы решили, что нам будет удобно хранить все наши конфигурационные файлы для деплоя в одном общем репозитории. Таким образом, они становились обозреваемы: их было легко поддерживать и можно было увидеть, какие системы у нас есть. В случае необходимости также несложно что-то обновить или поменять. Добавить новую систему тоже не составит труда — достаточно просто завести конфигурационный файл внутри новой директории. Внутри нее лежат файлы: service.hcl, который содержит описание нашего сервиса, и некоторые env-файлы, которые позволяют этот самый сервис, будучи задеплоенным в продакшн, настроить.
![image](https://habrastorage.org/webt/-g/l7/8h/-gl78hl7nbmdbhbuacuntgxw4ow.jpeg)
Однако некоторые наши системы задеплоены в проде не в одном экземпляре, а в нескольких сразу. Поэтому мы решили, что нам удобно будет хранить не конфиги в чистом виде, а их шаблонизированный вид. И в качестве языка шаблонизации мы выбрали jinja 2. В таком формате у нас хранятся как конфиги самого сервиса, так и env-файлы, нужные для него.
Помимо этого, мы поместили в репозиторий общий для всех проектов скрипт-деплой, который позволяет запустить и задеплоить ваш сервис в продакшн, в нужный environment, в нужный таргет. В случае, когда мы превратили наш HCL-конфиг в шаблон, то тот HСL-файл, который до этого был обычным конфигом Nomad, в этом случае стал выглядеть несколько иначе.
![image](https://habrastorage.org/webt/9a/ib/zv/9aibzvsme34ra2eg53bjfbqt1tw.jpeg)
То есть мы заменили какие-то переменные места конфига на вставки переменных, которые берутся из env-файлов или из других источников. Помимо этого, мы получили возможность собирать HСL-файлы динамически, то есть мы можем применять не только обычные вставки переменных. Так как jinja поддерживает циклы и условия, туда же можно делать конфигурационные файлы, которые меняются в зависимости от того, куда именно вы деплоите свои приложения.
Например, вы хотите деплоить ваш сервис в препродакшн и в продакшн. Допустим, что в препродакшн вы не хотите запускать крон-cкрипты, а просто хотите видеть сервис на отдельном домене, чтобы убедиться, что он функционирует. Для любого, кто деплоит сервис, процесс выглядит очень просто и прозрачно. Достаточно выполнить файл deploy.sh, указать, какой сервис вы хотите задеплоить и в какой таргет. Например, вы хотите задеплоить некоторую систему в Россию, в Белоруссию или Казахстан. Для этого достаточно просто поменять один из параметров, и у вас соберется правильный конфигурационный файл.
Когда сервис Nomad уже оказывается у вас задеплоенным в кластер, он выглядит следующим образом.
![image](https://habrastorage.org/webt/nu/au/8d/nuau8dqdcwft0boyw57x37wrkcm.jpeg)
Для начала вам нужен некоторый балансировщик снаружи, который будет принимать весь пользовательский трафик в себя. Он будет работать вместе с Consul и узнавать у него, где, на какой ноде, по какому IP-адресу находится конкретный сервис, который соответствует тому или иному доменному имени. Сервисы в Consul появляются из самого Nomad. Поскольку это продукты одной и той же компании, они неплохо связаны между собой. Можно сказать, что Nomad из коробки умеет регистрировать все запускаемые в нем сервисы внутри Consul.
После того как ваш внешний балансировщик узнает о том, в какой сервис необходимо отправить трафик, он перенаправляет его в соответствующий контейнер или в несколько контейнеров, которые соответствуют вашему приложению. Естественно, что при этом необходимо думать и о безопасности. Даже несмотря на то, что все сервисы запускаются на одних и тех же виртуальных машинах в контейнерах, обычно для этого требуется запретить свободный доступ из любого сервиса к любому другому. Мы достигали этого за счет сегментации. Каждый сервис запускался в своей виртуальной сети, на которой были прописаны правила маршрутизации и правила разрешения/запрета доступа к другим системами и сервисам. Они могли находиться как внутри этого кластера, так и вне него. Например, если вы хотите запретить сервису коннектиться к определённой базе данных, это можно сделать путем сегментации на уровне сети. То есть даже по ошибке вы не можете случайно подсоединяться из тестового окружения к вашей продакшн базе.
Во что нам обошелся процесс перехода в плане человеческих ресурсов?
Примерно 5-6 месяцев занял переход всей компании в Nomad. Мы переходили посервисно, но в достаточно быстром темпе. Каждая команда должна была создать свои собственные контейнеры для сервисов.
У нас принят такой подход, что каждая команда отвечает за docker images своих систем самостоятельно. Девопс же предоставляют общую инфраструктуру, необходимую для деплоя, то есть поддержку самого кластера, поддержку CI-системы и так далее. И на тот момент у нас более 60 систем переехали в Nomad, получилось порядка 2 тысяч контейнеров.
Девопс отвечает за общую инфраструктуру всего, что связано с деплоем, с серверами. А каждая команда разработки в свою очередь отвечает за реализацию контейнеров под свою конкретную систему, поскольку именно команда знает, что ей вообще нужно в том или ином контейнере.
Причины отказа от Nomad
Какие достоинства мы получили, перейдя на деплой с помощью Nomad и docker в том числе?
- Мы обеспечили одинаковые условия для всех сред. В девеломпенте, QA-среде, препродакшне, продакшне используются одни и те же образы контейнеров, с одними и теми же зависимостями. Соответственно у вас практически отсутствует шанс того, что в продакшн окажется не то, что вы до этого протестировали у себя локально или на тестовом окружении.
- Также мы выяснили, что достаточно легко добавить новый сервис. Любые новые системы с точки зрения деплоя запускаются очень просто. Достаточно пойти в репозиторий, хранящий конфиги, добавить туда очередной конфиг для вашей системы, и у вас все готово. Вы можете деплоить вашу систему в продакшн без дополнительных усилий от девопс.
- Все конфигурационные файлы в одном общем репозитории оказались обозреваемые. В тот момент, когда мы деплоили наши системы с помощью виртуальных серверов, мы использовали Ansible, в котором конфиги лежали в одном и том же репозитории. Однако для большинства разработчиков работать было с этим несколько сложнее. Тут объем конфигов и кода, который вам нужно добавить, чтобы задеплоить сервис, стал намного меньше. Плюс для девопс очень легко поправить его или поменять. В случае переходов, например, на новой версии Nomad, они могут взять и массово обновить все операционные файлы, лежащие в одном и том же месте.
Но мы также столкнулись и с несколькими недостатками:
Оказалось, что мы не смогли достичь бесшовности деплоев в случае Nomad. При выкатке контейнеров из разных условий могло получиться так, что он оказывался запущенным, и Nomad воспринимал его как готовый к принятию трафика контейнер. Это происходило еще до того, как приложение внутри него успело запуститься. По этой причине система на недолгий срок начинала выдавать 500-е ошибки, потому что трафик начинал идти на контейнер, который еще не готов его принять.
Мы столкнулись с некоторыми багами. Самый существенный баг заключается в том, что Nomad не очень хорошо воспринимает большой кластер, если у вас много систем и контейнеров. Когда вы хотите вывезти в обслуживание один из серверов, который включен в кластер Nomad, есть достаточно большая вероятность того, что кластер почувствует себя не очень хорошо и развалится на части. Часть контейнеров может, например, упасть и не подняться — это вам впоследствии очень дорого обойдется, если все ваши системы в продакшн находятся именно в кластере, управляемом Nomad.
Поэтому мы решили подумать, куда нам идти дальше. На тот момент мы стали намного лучше осознавать, чего хотим добиться. А именно: хотим надежности, немножко больше функций, чем дает Nomad, и более зрелую, более стабильную систему.
В этом плане наш выбор пал на Kubernetes как на самую популярную платформу для запуска кластеров. Особенно при условии того, что размер и количество наших контейнеров был достаточно большой. Для таких целей Kubernetes показался самой подходящей системой из тех, что мы могли посмотреть.
Переход в Kubernetes
Немножко расскажу о том, какие есть основные понятия Kubernetes и чем они отличаются от Nomad.
![image](https://habrastorage.org/webt/pv/eh/va/pvehvavusoxszoxum9bsuwyjqbc.jpeg)
В первую очередь самым базовым понятием в Kubernetes является понятие pod. Pod — это группа одного или нескольких контейнеров, которые всегда запускаются вместе. И они работают как будто всегда строго на одной виртуальной машине. Они доступны друг другу по айпишнику 127.0.0.1 на разных портах.
Предположим, что у вас есть РНР-приложение, которое состоит из nginx и php-fpm – классическая схема. Скорее всего, вы захотите, чтобы и nginx и php-fpm контейнеры были всегда вместе. Kubernetes позволяет этого добиться, описав их как один общий pod. Как раз этого мы и не могли получить при помощи Nomad.
Вторым понятием является deployment. Дело в том, что pod сам по себе – это эфемерная вещь, она запускается и исчезает. Хотите ли вы сначала убивать все ваши предыдущие контейнеры, а потом запускать сразу новые версии или же вы хотите выкатывать их постепенно – вот именно за этот процесс отвечает понятие deployment. Он описывает то, как вы деплоите ваши podы, в каком количестве и, как их обновлять.
Третьим понятием является service. Ваш service – это фактически ваша система, которая принимает в себя некий трафик, а потом направляет его на один или несколько podов, соответствующих вашему сервису. То есть он позволяет сказать, что весь входящий трафик на такой-то сервис с таким-то именем необходимо отправлять на конкретно эти podы. И при этом он обеспечивает вам балансировку трафика. То есть вы можете запустить два podа вашего приложения, и весь входящий трафик будет равномерно балансироваться между относящимися к этому сервису podами.
И четвертое основное понятие — Ingress. Это сервис, который запускается в кластере Kubernetes. Он выступает как внешний балансировщик нагрузки, который принимает на себя все запросы. За счет API Kubernetes Ingress может определять, куда эти запросы надо отправить. Причем делает он это очень гибко. Вы можете сказать, что все запросы на этот хост и такой-то URL отправляем на этот сервис. А эти запросы, приходящие на этот хост и на другой URL отправляем на другой сервис.
Самое крутое с точки зрения того, кто разрабатывает приложение — это то, что вы способны этим всем управлять самостоятельно. Задав конфиг Ingress, можете весь трафик, приходящий на такой-то API, отправлять на отдельные контейнеры, прописанные, например, на Go. А вот этот трафик, приходящий на тот же домен, но на другой URL, отправлять на контейнеры, написанные на РНР, где много логики, но они не очень скоростные.
Если сравнить все эти понятия с Nomad, то можно сказать, что первые три понятия – это все вместе Service. А последнее понятие в самом Nomad отсутствует. Мы в качестве него использовали внешний балансировщик: это может быть haproxy, nginx, nginx+ и так далее. В случае с кубом вам не надо вводить это дополнительное понятие отдельно. Однако, если посмотреть на Ingress внутри, то это либо nginx, либо haproxy, либо traefik, но как бы встроенный в Kubernetes.
Все описанные мною понятия – это, по сути дела, ресурсы, которые существуют внутри кластера Kubernetes. Для их описания в кубе используется yaml-формат, более читаемый и привычный, чем НСL-файлы в случае с Nomad. Но структурно они описывают в случае, например, podа то же самое. Они говорят – хочу задеплоить такие-то podы туда-то, с такими-то имаджи, в таком-то количестве.
![image](https://habrastorage.org/webt/2k/ka/53/2kka53a1vp1lm0rnl4xbhmcpyu4.jpeg)
Помимо этого, мы поняли, что не хотим руками создавать каждый отдельный ресурс: deployment, сервисы, Ingress и прочее. Вместо этого мы хотели при деплое описывать нашу каждую систему, имеющуюся в терминах Kubernetes, чтобы не приходилось вручную пересоздавать в нужном порядке все необходимые зависимости ресурсов. В качестве такой системы, которая позволила нам это сделать, был выбран Helm.
Основные понятия в Helm
Helm – это пакетный менеджер для Kubernetes. Он очень похож на то, как работают пакетные менеджеры в языках программирования. Они позволяют вам хранить сервис, состоящий из, например, deployment nginx, deployment php-fpm, конфига для Ingress, configmaps (это сущность, которая позволяет вам задать env и другие параметры для вашей системы) в виде так называемых чартов. При этом Helm работает поверх Kubernetes. То есть это не какая-то система, стоящая в стороне, а просто ещё один сервис, запускаемый внутри куба. Вы взаимодействуете с ним по его API через консольную команду. Его удобство и прелесть в том, что даже если helm сломается или вы его удалите из кластера, то ваши сервисы не исчезнут, так как helm служит по сути только для запуска системы. За работоспособность и состояние сервисов дальше отвечает сам Kubernetes.
Также мы поняли, что шаблонизация, которую до этого были вынуждены делать самостоятельно через внедрение jinja в наши конфиги, является одной из основных возможностей helm. Все конфиги, которые вы создаете для ваших систем, хранятся в helm в виде шаблонов, похожих немножко на jinja, но, на самом деле, использующих шаблонизацию языка Go, на котором helm написан, как и Kubernetes.
Helm добавляет нам еще несколько дополнительных понятий.
Chart — это описание вашего сервиса. В других пакетных менеджерах его назвали бы пакет, bundle или что-то подобное. Здесь это называется chart.
Values – это переменные, которые вы хотите использовать для сборки ваших конфигов из шаблонов.
Release. Каждый раз сервис, который деплоится с помощью helm, получает инкрементальную версию релиза. Helm помнит, какой был конфиг сервиса при предыдущем, позапрошлом релизе и так далее. Поэтому, если нужно откатиться, достаточно выполнить команду helm callback, указав ему предыдущую версию релиза. Даже если в момент отката соответствующая конфигурация в вашем репозитории не будет доступна, helm все равно помнит, какой она была, и откатит вашу систему на то состояние, в котором она была в предыдущем релизе.
В случае, когда мы используем helm, обычные конфиги для Kubernetes тоже превращаются в шаблоны, в которых есть возможность использовать переменные, функции, применять условные операторы. Таким образом, вы можете собирать конфиг вашего сервиса в зависимости от среды.
![image](https://habrastorage.org/webt/dc/lr/fh/dclrfhbdr29ms0gouz_pvd8xz44.jpeg)
На практике мы решили поступить чуть-чуть иначе, чем мы делали в случае с Nomad. Если в Nomad в одном репозитории хранились и конфиги для деплоя, и n-переменные, которые нужны для того, чтобы задеплоить наш сервис, то здесь мы решили их разделить на два отдельных репозитория. В репозитории «deploy» хранятся только n-переменные, нужные для деплоя, а в репозитории «helm» хранятся конфиги или чарты.
![image](https://habrastorage.org/webt/2r/lo/yt/2rloytnvlrj6nri8vj7e9-hccg8.jpeg)
Что это нам дало?
Несмотря на то, что в самих конфигурационных файлах мы не храним какие-то действительно чувствительные данные. Например, пароли к базам данных. Они хранятся в виде secrets в Kubernetes, но тем не менее, там все равно есть отдельные вещи, к которым мы не хотим давать доступ всем подряд. Поэтому доступ к репозиторию «deploy» более ограничен, а репозиторий «helm» содержит просто описание сервиса. По этой причине в него можно дать доступ безопасно большему кругу лиц.
Поскольку у нас есть не только продакшн, но и другие среды, то благодаря такому разделению мы можем переиспользовать наши helm-чарты, чтобы деплоить сервисы не только в продакшн, но и, например, в QA-среду. Даже для того, чтобы разворачивать их локально, используя Minikube — это такая штука для локального запуска Kubernetes.
Внутри каждого репозитория мы оставили разделение на отдельные директории для каждого сервиса. То есть внутри каждой директории находятся шаблоны, относящиеся к соответствующему чарту и описывающие те ресурсы, которые необходимо задеплоить для запуска нашей системы. В репозитории «deploy» мы оставили только энвы. В этом случае мы не стали использовать шаблонизацию с помощью jinja, потому что helm сам дает шаблонизацию из коробки – это одна из его основных функций.
Мы оставили скрипт для деплоя – deploy.sh, который упрощает и стандартизирует запуск для деплоя с помощью helm. Таким образом, для любого, кто хочет задеплоить, интерфейс деплоя выглядит точно так же, как он был в случае деплоя через Nomad. Такой же deploy.sh, название вашего сервиса, и то, куда вы хотите его задеплоить. Это приводит к тому, что внутри запускается helm. Он в свою очередь собирает конфиги из шаблонов, подставляет в них необходимые values-файлы, затем деплоит, пуская их в Kubernetes.
Выводы
Сервис Kubernetes выглядит более сложным, чем Nomad.
![image](https://habrastorage.org/webt/fe/p6/qi/fep6qibp6hhnbsmqifk2jnmg20a.jpeg)
Здесь исходящий трафик приходит в Ingress. Это как раз фронт-контроллер, который принимает на себя все запросы и впоследствии отправляет их в соответствующие данным запроса сервисы. Определяет он их на основе конфигов, которые являются частью описания вашего приложения в helm и которые разработчики задают самостоятельно. Сервис же отправляет запросы на свои podы, то есть конкретные контейнеры, балансируя входящий трафик между всеми контейнерами, которые относятся к данному сервису. Ну и, конечно, не стоит забывать про то, что от безопасности на уровне сети, мы никуда не должны уходить. Поэтому в кластере Kubernetes работает сегментация, которая основана на тегировании. Все сервисы имеют определенные теги, к которым привязаны права доступов сервисов к тем или иным внешним/внутренним ресурсам внутри или вне кластера.
Выполняя переход, мы увидели, что Kubernetes обладает всеми возможностями Nomad, который до этого мы использовали, а также добавляет много нового. Его можно расширять через плагины, а по факту через кастомные типы ресурсов. То есть у вас есть возможность не просто использовать что-то, что идет в Kubernetes из коробки, а создать свой собственный ресурс и сервис, который будет ваш ресурс читать. Это дает дополнительные возможности расширения вашей системы без необходимости переустановки Kubernetes и без необходимости изменений.
Примером такого использования является Prometheus, который запускается у нас внутри кластера Kubernetes. Для того, чтобы он начал собирать метрики с того или иного сервиса, нам нужно добавить в описание сервиса дополнительный тип ресурса, так называемый сервис-монитор. Prometheus за счет того, что умеет вычитывать, будучи запущенным в Kubernetes, кастомный тип ресурсов, автоматически начинает собирать метрики с новой системы. Это достаточно удобно.
Первый деплой, который мы сделали в Kubernetes был в марте 2018 года. И за это время мы никогда не испытывали с ним никаких проблем. Он достаточно стабильно работает без существенных багов. К тому же, мы можем его дальше расширять. На сегодняшний день нам хватает тех возможностей, которые в нем есть, а темпы развития Kubernetes нам очень нравятся. На данный момент больше 3000 контейнеров находятся в Kubernetes. Кластер занимает несколько Node. При этом он обслуживаемый, стабильный и очень контролируемый.