Привет! За последнее время вышло много классных инструментов автоматизации как для сборки Docker-образов так и для деплоя в Kubernetes. В связи с этим решил поиграться с гитлабом, как следует изучить его возможности и, конечно же, настроить пайплайн. Вдохновлением для этой работы стал сайт kubernetes.io, который генерируется из исходных кодов автоматически, а на каждый присланный пул реквест робот автоматически генерирует preview-версию сайта с вашими изменениеми и предоставляет ссылку для просмотра. Я постарался выстроить подобный процесс с нуля, но целиком построенный на Gitlab CI и свободных инструментах, которые я привык использовать для деплоя приложений в Kubernetes. Сегодня я, наконец, расскажу вам о них подробнее. В статье будут рассмотрены такие инструменты как:
Hugo, qbec, kaniko, git-crypt и GitLab CI с созданием динамических окружений. В качестве примера нашего проекта мы попробуем создать сайт для публикации документации, построенный на Hugo. Hugo — это статический генератор контента. Для тех, кто не знаком со статическими генераторами расскажу о них немного подробнее. В отличии от обычных движков сайтов с базой данных и каким-нибудь php, которые, при запросе пользователя, генерируют страницы на лету, статические генераторы устроенны немного иначе. Они позволяют взять исходники, как правило это набор файлов в Markdown-разметке и шаблоны тем, затем скомпилировать их в полностью готовый сайт. То есть на выходе вы получите структуру директорий и набор сгенерированных html-файлов, которые можно будет просто залить на любой дешёвый хостинг и получить рабочий сайт. Hugo можно установить локально и попробовать его в деле: Инициализируем новый сайт: И заодно git-репозиторий: Пока что наш сайт девственно чист и чтобы на нём что-то появилось сначала нам нужно подключить тему, тема — всего лишь это набор темплейтов и заданных правил по которым генерируется наш сайт. В качестве темы мы будем использовать Learn, которая, на мой взгляд, как нельзя лучше подходит для сайта с документацией. Отдельное внимание хочется уделить тому, что нам не требуется сохранять файлы темы в репозитории нашего проекта, вместо этого мы можем просто подключить её используя git submodule: Таким образом в нашем репозитории будут находиться только файлы непосредственно относящиеся к нашему проекту, а подключенная тема останется в виде ссылки на конкретный репозиторий и коммит в нём, то есть её всегда можно будет вытянуть из оригинального источника и не бояться несовместимых изменений. Подправим конфиг config.toml: Уже на данном этапе можно запустить: И по адресу http://localhost:1313/ проверить наш только что созданный сайт, все изменения произведённые в директории автоматически обновляют и открытую страничку в браузере, очень удобно! Попробуем создать титульную страницу в content/_index.md:  Для генерации сайта достаточно запустить: Содержимое директории public/ и будет являться вашим сайтом.
Да, кстати, давайте сразу внесём её в .gitignore: Не забываем закоммитить наши изменения: Настало время определить структуру нашего репозитория. Обычно я использую что-то вроде: Таким образом наш первый Dockerfile мы создадим по пути dockerfiles/website/Dockerfile Как вы можете заметить, Dockerfile содержит два FROM, эта возможность называется multi-stage build и позволяет исключить из финального docker-образа всё ненужное.
Таким образом финальный образ у нас будет содержать только darkhttpd (легковесный HTTP-сервер) и public/ — контент нашего статически сгенирированного сайта. Не забываем закоммитить наши изменения: В качестве сборщика docker-образов я решил использовать kaniko, так как для его работы не требуется наличие docker-демона, а саму сборку можно проводить на любой машине и хранить кэш прямо в registry, избавлясь, тем самым, от необходимости иметь полноценное persistent-хранилище. Для сборки образа достаточно запустить контейнер с kaniko executor и передать ему текущий контекст сборки, сделать это можно и локально, через docker: Где registry.gitlab.com/kvaps/docs.example.org/website — имя вашего docker-образа, после сборки он будет автоматически запушен в docker-регистри. Параметр --cache позволяет кэшировать слои в docker registry, для приведённого примера они будут сохраняються в registry.gitlab.com/kvaps/docs.example.org/website/cache, но вы можете указать и другой путь с помощью параметра --cache-repo.  Qbec — это инструмент деплоя, который позволяет декларативно описывать манифесты вашего приложения и деплоить их в Kubernetes. Использование Jsonnet в качестве основного синтаксиса позволяет очень сильно упростить описание различий для нескольких окружений, а также почти полностью избавляет от повторяемости кода. Это может быть особенно актуально в тех случаях, когда вам нужно задеплоить приложение в несколько кластеров с разными параметрами и вы хотите декларативно описать их в Git. Qbec также позволяет рендерить Helm-чарты передавая им необходимые параметры и в дальнейшем оперировать ими также как и обычными манифестами, в том числе можно накладывать на них различные мутации, а это, в свою очередь, позволяет избавиться от необходимости использовать ChartMuseum. То есть можно хранить и рендерить чарты прямо из git, где им и самое место. Как я говорил раньше, все деплойменты мы будем хранить в директории deploy/: Давайте инициализируем наше первое приложение: Сейчас структура нашего приложения выглядит так: посмотрим на файл qbec.yaml: Здесь нас интересует в первую очередь spec.environments, qbec уже создал за нас default окружение и взял адрес сервера, а также namespace из нашего текущего kubeconfig.
Теперь при деплое в default окружение, qbec всегда будет деплоить только в указанный Kubernetes-кластер и в указанный неймспейс, то есть вам больше не придётся переключаться между контекстами и неймспейсами для того чтобы выполнить деплой.
В случае необоходимости вы всегда можете обновить настройки в этом файле. Все ваши окружения описываются в qbec.yaml, и в файле params.libsonnet, где сказано откуда нужно брать для них параметры. Дальше мы видим две директории: По умолчанию мы имеем два файла: Давайте откроем environments/base.libsonnet и добавим туда параметры для нашего первого компонента: Создадим также наш первый компонент components/website.jsonnet: В данном файле мы описали сразу три Kubernetes-cущности, это: Deployment, Service и Ingress. При желании мы могли бы вынести их в разные компоненты, но на данном этапе нам хватит и одного. Синтаксис jsonnet очень похож на обычный json, в принципе обычный json уже является валидным jsonnet, так что первое время вам возможно будет проще воспользоваться онлайн-сервисами вроде yaml2json чтобы сконвертировать привычный вам yaml в json, либо, если ваши компоненты не содержат никаких переменных, то их вполне можно описать в виде обычного yaml. Всё готово, теперь можем начинать деплой: Чтобы посмотреть что у нас получилось, выполним: На выходе вы увидите отрендеренные yaml-манифесты, которые будут применены в кластер default. Отлично, теперь применим: На выходе вы всегда увидите, что будет проделанно в вашем кластере, qbec попросит вас согласиться с изменениями, набрав y вы сможете подтвердить свои намерения. Готово теперь наше приложение задеплоено!  В случае внесения изменений вы всегда сможете выполнить: чтобы посмотреть как эти изменения отразятся на текущем деплое Не забываем закоммитить наши изменения: До недавного времени я использовал только обычный gitlab-runner на заранее подготовленной машине (LXC-контейнере) с shell- или docker-executor. Изначально мы имели несколько таких раннеров глобально определённых в нашем гитлабе. Они собирали docker-образы для всех проектов. Но как показала практика — этот вариант не самый идеальный, как в плане практичности, так и в плане безопасности. Гораздо лучше и идеологически правильней иметь отдельные раннеры задеплоенные для каждого проекта, а то и для каждого окружения. К счастью это вовсе не проблема, так как теперь мы будем деплоить gitlab-runner непосредственно как часть нашего проекта прямо в Kubernetes. Gitlab предоставляет готовый helm-чарт для деплоя gitlab-runner в Kubernetes. Таким образом всё что вам нужно, это узнать registration token для нашего проекта в Settings --> CI / CD --> Runners и передать его helm: Где: Если всё сделанно правильно, вы должны увидеть зарегистрированный раннер в секции Runners, в настройках вашего проекта.  Вот так просто? — да, так просто! Больше никакой мороки с регистрацией раннеров вручную, с этой минуты раннеры будут создаваться и уничтожаться автоматически. Так как мы приняли решение считать gitlab-runner частью нашего проекта, настало время описать его в нашем Git-репозитории. Мы могли бы описать его как отдельный компонент website, но в дальнейшем мы планируем деплоить разные копии website очень часто, в отличии gitlab-runner, который будет задеплоен всего-лишь один раз на каждый Kubernetes-кластер. Так что давайте инициализируем отдельное приложение для него: На этот раз мы не будем описывать Kubernetes-сущности вручную, а возьмём готовый Helm-чарт. Одним из преимуществ qbec является возможность рендерить Helm-чарты прямо из Git-репозитория. Давайте подключим его используя git submodule: Теперь директория vendor/gitlab-runner содержит у нас репозиторий с чартом для gitlab-runner. Давайте опишем компонент components/gitlab-runner.jsonnet: Первым аргументом к expandHelmTemplate мы передаём путь к чарту, затем params.values, которые возьмём из параметров окружения, затем идёт объект с Теперь опишем параметры для нашего компонента в environments/base.libsonnet: Обратите внимание runnerRegistrationToken мы забираем из внешнего файла secrets/base.libsonnet, давайте создадим его: Проверим, всё ли работает: если всё в порядке, то можем удалить наш ранее, задеплоенный через Helm, релиз: и задеплоить его же, но уже через qbec: Git-crypt — это инструмент, который позволяет настроить прозрачное шифрование для вашего репозитория. На данный момент структура нашей директории для gitlab-runner выглядит так: Но хранить секреты в Git небезопасно, не так-ли? Так что нам нужно должным образом их зашифровать. Первым делом после установки git-crypt нам нужно сгенерировать ключи для нашего репозитория: Если у вас имеется PGP-ключ, то вы можете сразу добавить себя как collaborator'а для этого проекта: Таким образом вы всегда сможете расшифровать этот репозиторий используя свой приватный ключ. Если же PGP-ключа у вас нет и не предвидится, то вы можете пойти другим путём и экспортировать ключ проекта: Таким образом любой, кто обладает экспортированным keyfile сможет расшифровать ваш репозиторий. Настало время настроить наш первый секрет.
Напомню, мы по прежнему находимся в директории deploy/gitlab-runner/, где у нас имеется директория secrets/, давайте же зашифруем все файлы в ней, для этого создадим файл secrets/.gitattributes с таким содержанием: Как видно из содержания, все файлы по маске * будут прогоняться через git-crypt, за исключением самого .gitattributes Проверить это мы можем запустив: На выходе получим список всех файлов в репозитории для которых включенно шифрование Вот и всё, теперь мы можем смело закоммитить наши изменения: Для того чтобы заблокировать репозиторий достаточно выполнить: и тут же все зашифрованные файлы превратятся в бинарное нечто, прочесть их будет невозможно.
Чтобы расшифровать репозиторий, выполните: Toolbox-образ — это такой образ со всеми инструментами который мы будем использовать для деплоя нашего проекта. Он будет использоваться гитлаб-раннером для выполнения типовых задач деплоя. Здесь всё просто, создаём новый dockerfiles/toolbox/Dockerfile с таким содержанием: Как вы можете заметить, в этом образе мы устанавливаем все утилиты, которые мы использовали для деплоя нашего приложения. Нам не нужен здесь разве что kubectl, но возможно вы захотите поиграться с ним на этапе настройки пайплайна.  Также чтобы иметь возможность общаться с Kubernetes и выполнять в него деплой, нам нужно настроить роль для подов генерируемых gitlab-runner'ом. Для этого перейдём в директорию с gitlab-runner'ом: и добавим новый компонент components/rbac.jsonnet: Так же опишем новые параметры в environments/base.libsonnet, который теперь выглядит так: Обратите внимание $.components.rbac.name ссылается на name для компонента rbac Давайте проверим что изменилось: и применим наши изменения в Kubernetes: Так же не забываем закоммитить наши изменения в git: В корне проекта мы создадим .gitlab-ci.yml с таким содержанием: Обратите внимание, мы используем GIT_SUBMODULE_STRATEGY: normal для тех джоб, где нужно явно инициализировать сабмодули перед выполнением. Не забываем закоммитить наши изменения: Думаю можно смело назвать это версией v0.0.1 и повесить тэг: Тэги мы будем вешать всякий раз тогда, когда нам потребуется зарелизить новую версию. Тэги в Docker-образах будут привязаны к Git-тэгам. Каждый push с новым тэгом будет инициализировать сборку образов с этим тэгом. Выполним git push --tags, и посмотрим на наш первый пайплайн:  Для того чтобы Gitlab-runner мог расшифровать наши секреты, нам понадобится экспортировать ключ репозитория, и добавить его в переменные окружения нашей CI: полученную строку сохраним в Gitlab, для этого перейдём в настройки нашего проекта:
Settings --> CI / CD --> Variables  И создадим новую переменную:  Теперь обновим наш .gitlab-ci.yml добавив в него: Здесь мы задействовали несколько новых опций для qbec: Не забываем закоммитить наши изменения: И после git push мы увидим как наши приложения были задеплоены:  Обычно вышеописанных шагов вполне хватает для сборки и доставки почти любого микросервиса, но мы не хотим вешать тэг каждый раз когда нам понадобится обновить сайт. Поэтому мы пойдём более динамическим путём и настроим деплой по digest в master-ветке. Идея проста: теперь образ нашего website будет пересобираться каждый раз при push в master, а после этого автоматически деплоиться в Kubernetes. Давайте обновим эти две джобы в нашем .gitlab-ci.yml: Обратите внимание, мы добавили ветку master к refs для джобы build_website и мы теперь используем $CI_COMMIT_REF_NAME вместо $CI_COMMIT_TAG, то есть мы отвязываемся от тэгов в Git и теперь будем пушить образ с названием ветки коммита инициализировашего пайплайн. Стоит заметить, что это также будет работать и с тэгами, что позволит нам сохранять снапшоты сайта с определённой версией в docker-registry. Когда имя docker-тэга для новой версии сайта может быть неизменно, мы по прежнему должны описывать изменения для Kubernetes, в противном случае он просто не передеплоит приложение из нового образа, так как не заметит никаких изменений в манифесте деплоймента. Опция --vm:ext-str digest="$DIGEST" для qbec — позволяет передать внешную переменную в jsonnet. Мы хотим чтобы с каждым релизом нашего приложения оно передеплоивалось в кластере. Использовать имя тэга, которое теперь может быть неизменным, мы здесь больше не можем, так как нам нужно завязываться на конкретную версию образа и триггерить деплой при её изменении. Здесь нам поможет возможность Kaniko сохранять digest образа в файл (опция --digest-file)
Затем этот файл мы передадим и прочитаем в момент деплоя. Обновим параметры для нашего deploy/website/environments/base.libsonnet который теперь будет выглядить так: Готово, теперь любой коммит в master инициализиует сборку docker-образа для website, а затем его деплой в Kubernetes. Не забываем закоммитить наши изменения: Проверим, после git push мы должны увидеть что-то подобное:  В принципе нам без надобности передеплоивать gitlab-runner при каждом push, если, конечно, ничего не изменилось в его кофигурации, давайте исправим это в .gitlab-ci.yml: changes позволит следить за изменениями в deploy/gitlab-runner/ и будет тригерить нашу джобу только при наличии таковых Не забываем закоммитить наши изменения: git push, так-то лучше:  Настало время разнообразить наш пайплайн динамическими окружениями. Для начала обновим джобу build_website в нашем .gitlab-ci.yml, убрав из неё блок only, что заставит Gitlab тригеррить её при любом коммите в любую ветку: Затем обновим джобу deploy_website, добавим туда блок environment: Это позволит Gitlab ассоциировать джобу с prod окружением и выводить правильную ссылку на него. Теперь добавим ещё две джобы: Они будут запускаться при push в любые бренчи кроме master и будут деплоить preview версию сайта. Мы видим новую опцию для qbec: --app-tag — она позволяет тэгировать задеплоенные версии приложения и работать только в пределах этого тэга, при создании и уничтожении ресурсов в Kubernetes qbec будет оперировать только ими.
Таким образом мы можем не создавать отдельный энвайромент под каждый review, а просто переиспользовать один и тот же. Здесь мы так же используем qbec apply review, вместо qbec apply default — это как раз тот самый момент когда мы постараемся описать различия для наших окружений (review и default): Добавим review окружение в deploy/website/qbec.yaml Затем обьявим его в deploy/website/params.libsonnet: И запишем кастомные параметры для него в deploy/website/environments/review.libsonnet: Давайте также повнимательнее посмотрим на джобу stop_review, она будет тригериться при удалении брэнча и чтобы gitlab не пытался сделать checkout на неё используется GIT_STRATEGY: none, позже мы клонируем master-ветку и удаляем review через неё.
Немного заморочно, но более красивого способа я пока не нашёл.
Альтернативным вариантом может быть деплой каждого review в отельный неймспейс, который всегда можно снести целиком. Не забываем закоммитить наши изменения: git push, git checkout -b test, git push origin test, проверяем:  Всё работает? — отлично, удаляем нашу тестовую ветку: git checkout master, git push origin :test, проверяем что джобы на удаление environment отработали без ошибок. Review Apps это такая возможность гитлаба, которая позволяет для каждого файла в репозитории добавить кнопку для его быстрого просмотра в задеплоенном окружении. Для того чтобы эти кнопки появились, необходимо создать файл .gitlab/route-map.yml и описать в нём все трансформации путей, в нашем случае это будет очень просто: Не забываем закоммитить наши изменения: git push, и проверяем:  Исходники проекта: Спасибо за внимание, надеюсь вам понравилось  Суперпользователь