Привет! За последнее время вышло много классных инструментов автоматизации как для сборки Docker-образов так и для деплоя в Kubernetes. В связи с этим решил поиграться с гитлабом, как следует изучить его возможности и, конечно же, настроить пайплайн.
Вдохновлением для этой работы стал сайт kubernetes.io, который генерируется из កូដប្រភព автоматически, а на каждый присланный пул реквест робот автоматически генерирует preview-версию сайта с вашими изменениеми и предоставляет ссылку для просмотра.
Я постарался выстроить подобный процесс с нуля, но целиком построенный на Gitlab CI и свободных инструментах, которые я привык использовать для деплоя приложений в Kubernetes. Сегодня я, наконец, расскажу вам о них подробнее.
В статье будут рассмотрены такие инструменты как: ហ៊ូហ្គូ, qbec, កានីកូ, git-crypt и GitLab ស៊ីអាយ с созданием динамических окружений.
В качестве примера нашего проекта мы попробуем создать сайт для публикации документации, построенный на Hugo. Hugo — это статический генератор контента.
Для тех, кто не знаком со статическими генераторами расскажу о них немного подробнее. В отличии от обычных движков сайтов с базой данных и каким-нибудь php, которые, при запросе пользователя, генерируют страницы на лету, статические генераторы устроенны немного иначе. Они позволяют взять исходники, как правило это набор файлов в Markdown-разметке и шаблоны тем, затем скомпилировать их в полностью готовый сайт.
То есть на выходе вы получите структуру директорий и набор сгенерированных html-файлов, которые можно будет просто залить на любой дешёвый хостинг и получить рабочий сайт.
Hugo можно установить локально и попробовать его в деле:
Инициализируем новый сайт:
hugo new site docs.example.org
И заодно git-репозиторий:
cd docs.example.org
git init
Пока что наш сайт девственно чист и чтобы на нём что-то появилось сначала нам нужно подключить тему, тема — всего лишь это набор темплейтов и заданных правил по которым генерируется наш сайт.
В качестве темы мы будем использовать រៀន, которая, на мой взгляд, как нельзя лучше подходит для сайта с документацией.
Отдельное внимание хочется уделить тому, что нам не требуется сохранять файлы темы в репозитории нашего проекта, вместо этого мы можем просто подключить её используя git submodule:
Таким образом в нашем репозитории будут находиться только файлы непосредственно относящиеся к нашему проекту, а подключенная тема останется в виде ссылки на конкретный репозиторий и коммит в нём, то есть её всегда можно будет вытянуть из оригинального источника и не бояться несовместимых изменений.
Подправим конфиг config.toml:
baseURL = "http://docs.example.org/"
languageCode = "en-us"
title = "My Docs Site"
theme = "learn"
Уже на данном этапе можно запустить:
hugo server
И по адресу http://localhost:1313/ проверить наш только что созданный сайт, все изменения произведённые в директории автоматически обновляют и открытую страничку в браузере, очень удобно!
Попробуем создать титульную страницу в content/_index.md:
# My docs site
## Welcome to the docs!
You will be very smart :-)
Скриншот только что созданной страницы
Для генерации сайта достаточно запустить:
hugo
Содержимое директории សាធារណៈ/ и будет являться вашим сайтом.
Да, кстати, давайте сразу внесём её в .gitignore:
echo /public > .gitignore
Не забываем закоммитить наши изменения:
git add .
git commit -m "New site created"
2. Подготовка Dockerfile
Настало время определить структуру нашего репозитория. Обычно я использую что-то вроде:
dockerfiles/ — содержат директории с Dockerfiles и всем необходимым для сборки наших docker-образов.
deploy/ — содержит директории для деплоя наших приложений в Kubernetes
Таким образом наш первый Dockerfile мы создадим по пути dockerfiles/website/Dockerfile
FROM alpine:3.11 as builder
ARG HUGO_VERSION=0.62.0
RUN wget -O- https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-64bit.tar.gz | tar -xz -C /usr/local/bin
ADD . /src
RUN hugo -s /src
FROM alpine:3.11
RUN apk add --no-cache darkhttpd
COPY --from=builder /src/public /var/www
ENTRYPOINT [ "/usr/bin/darkhttpd" ]
CMD [ "/var/www" ]
Как вы можете заметить, Dockerfile содержит два FROM, эта возможность называется multi-stage build и позволяет исключить из финального docker-образа всё ненужное.
Таким образом финальный образ у нас будет содержать только darkhttpd (легковесный HTTP-сервер) и សាធារណៈ/ — контент нашего статически сгенирированного сайта.
Не забываем закоммитить наши изменения:
git add dockerfiles/website
git commit -m "Add Dockerfile for website"
3. Знакомство с kaniko
В качестве сборщика docker-образов я решил использовать កានីកូ, так как для его работы не требуется наличие 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.
Скриншот docker-registry
4. Знакомство с qbec
Qbec — это инструмент деплоя, который позволяет декларативно описывать манифесты вашего приложения и деплоить их в Kubernetes. Использование Jsonnet в качестве основного синтаксиса позволяет очень сильно упростить описание различий для нескольких окружений, а также почти полностью избавляет от повторяемости кода.
Это может быть особенно актуально в тех случаях, когда вам нужно задеплоить приложение в несколько кластеров с разными параметрами и вы хотите декларативно описать их в Git.
Qbec также позволяет рендерить Helm-чарты передавая им необходимые параметры и в дальнейшем оперировать ими также как и обычными манифестами, в том числе можно накладывать на них различные мутации, а это, в свою очередь, позволяет избавиться от необходимости использовать ChartMuseum. То есть можно хранить и рендерить чарты прямо из git, где им и самое место.
Как я говорил раньше, все деплойменты мы будем хранить в директории deploy/:
Здесь нас интересует в первую очередь spec.environments, qbec уже создал за нас default окружение и взял адрес сервера, а также namespace из нашего текущего kubeconfig.
Теперь при деплое в លំនាំដើម окружение, qbec всегда будет деплоить только в указанный Kubernetes-кластер и в указанный неймспейс, то есть вам больше не придётся переключаться между контекстами и неймспейсами для того чтобы выполнить деплой.
В случае необоходимости вы всегда можете обновить настройки в этом файле.
Все ваши окружения описываются в qbec.yaml, и в файле params.libsonnet, где сказано откуда нужно брать для них параметры.
Дальше мы видим две директории:
សមាសភាគ / — здесь будут храниться все манифесты для нашего приложения, они могут быть описанны как в jsonnet так и обычными yaml-файлами
environments/ — здесь мы будем описывать все переменные (параметры) для наших окружений.
По умолчанию мы имеем два файла:
environments/base.libsonnet — он будет содержать общие параметры для всех окружений
environments/default.libsonnet — содержит параметры переопределённые для окружения លំនាំដើម
Давайте откроем environments/base.libsonnet и добавим туда параметры для нашего первого компонента:
В данном файле мы описали сразу три Kubernetes-cущности, это: ការដាក់ពង្រាយ, សេវាកម្ម и Ingress. При желании мы могли бы вынести их в разные компоненты, но на данном этапе нам хватит и одного.
វាក្យសម្ព័ន្ធ jsonnet очень похож на обычный json, в принципе обычный json уже является валидным jsonnet, так что первое время вам возможно будет проще воспользоваться онлайн-сервисами вроде yaml2json чтобы сконвертировать привычный вам yaml в json, либо, если ваши компоненты не содержат никаких переменных, то их вполне можно описать в виде обычного yaml.
នៅពេលធ្វើការជាមួយ jsonnet очень советую установить вам плагин для вашего редактора
К примеру для vim есть плагин vim-jsonnet, который включает подсветку синтаксиса и автоматически выполняет jsonnet fmt при каждом сохранении (требует наличия установленно jsonnet).
Всё готово, теперь можем начинать деплой:
Чтобы посмотреть что у нас получилось, выполним:
qbec show default
На выходе вы увидите отрендеренные yaml-манифесты, которые будут применены в кластер default.
Отлично, теперь применим:
qbec apply default
На выходе вы всегда увидите, что будет проделанно в вашем кластере, qbec попросит вас согласиться с изменениями, набрав y вы сможете подтвердить свои намерения.
Готово теперь наше приложение задеплоено!
В случае внесения изменений вы всегда сможете выполнить:
qbec diff default
чтобы посмотреть как эти изменения отразятся на текущем деплое
Не забываем закоммитить наши изменения:
cd ../..
git add deploy/website
git commit -m "Add deploy for website"
5. Пробуем Gitlab-runner с Kubernetes-executor
До недавного времени я использовал только обычный អ្នករត់ gitlab на заранее подготовленной машине (LXC-контейнере) с shell- или docker-executor. Изначально мы имели несколько таких раннеров глобально определённых в нашем гитлабе. Они собирали docker-образы для всех проектов.
Но как показала практика — этот вариант не самый идеальный, как в плане практичности, так и в плане безопасности. Гораздо лучше и идеологически правильней иметь отдельные раннеры задеплоенные для каждого проекта, а то и для каждого окружения.
К счастью это вовсе не проблема, так как теперь мы будем деплоить អ្នករត់ gitlab непосредственно как часть нашего проекта прямо в Kubernetes.
Gitlab предоставляет готовый helm-чарт для деплоя gitlab-runner в Kubernetes. Таким образом всё что вам нужно, это узнать និមិត្តសញ្ញាចុះឈ្មោះ для нашего проекта в Settings —> CI / CD —> Runners и передать его helm:
yga8y-jdCusVDn_t4Wxc — registration token для вашего проекта.
rbac.create=true — предоставляет раннеру необходимое количество привилегий, чтобы иметь возможность создавать поды для выполнения наших задач с помощью kubernetes-executor.
Если всё сделанно правильно, вы должны увидеть зарегистрированный раннер в секции អ្នករត់, в настройках вашего проекта.
Скриншот добавленного раннера
Вот так просто? — да, так просто! Больше никакой мороки с регистрацией раннеров вручную, с этой минуты раннеры будут создаваться и уничтожаться автоматически.
6. Деплой Helm-чартов с QBEC
Так как мы приняли решение считать អ្នករត់ gitlab частью нашего проекта, настало время описать его в нашем Git-репозитории.
Мы могли бы описать его как отдельный компонент គេហទំព័រ, но в дальнейшем мы планируем деплоить разные копии គេហទំព័រ очень часто, в отличии អ្នករត់ gitlab, который будет задеплоен всего-лишь один раз на каждый Kubernetes-кластер. Так что давайте инициализируем отдельное приложение для него:
cd deploy
qbec init gitlab-runner
cd gitlab-runner
На этот раз мы не будем описывать Kubernetes-сущности вручную, а возьмём готовый Helm-чарт. Одним из преимуществ qbec является возможность рендерить Helm-чарты прямо из Git-репозитория.
Но хранить секреты в Git небезопасно, не так-ли? Так что нам нужно должным образом их зашифровать.
Обычно ради одной переменной это не всегда имеет смысл. Вы можете передавать секреты в qbec и через переменные окружения вашей CI-системы.
Но стоит заметить, что бывают и более сложные проекты, которые могут содержать гораздо больше секретов, передавать их все через переменные окружения будет крайне затруднительно.
Кроме того в таком случае мне не удалось бы рассказать вам о таком замечательном инструменте как git-crypt.
git-crypt ещё удобен тем, что позволяет сохранить всю историю секретов, а также сравнивать, мерджить и разрешать кофликты так-же как мы привыкли делать это в случае с Git.
Первым делом после установки git-crypt нам нужно сгенерировать ключи для нашего репозитория:
git crypt init
Если у вас имеется PGP-ключ, то вы можете сразу добавить себя как collaborator’а для этого проекта:
Таким образом вы всегда сможете расшифровать этот репозиторий используя свой приватный ключ.
Если же PGP-ключа у вас нет и не предвидится, то вы можете пойти другим путём и экспортировать ключ проекта:
git crypt export-key /path/to/keyfile
Таким образом любой, кто обладает экспортированным keyfile сможет расшифровать ваш репозиторий.
Настало время настроить наш первый секрет.
Напомню, мы по прежнему находимся в директории deploy/gitlab-runner/, где у нас имеется директория secrets/, давайте же зашифруем все файлы в ней, для этого создадим файл secrets/.gitattributes с таким содержанием:
Как видно из содержания, все файлы по маске * будут прогоняться через git-crypt, за исключением самого .gitattributes
Проверить это мы можем запустив:
git crypt status -e
На выходе получим список всех файлов в репозитории для которых включенно шифрование
Вот и всё, теперь мы можем смело закоммитить наши изменения:
cd ../..
git add .
git commit -m "Add deploy for gitlab-runner"
Для того чтобы заблокировать репозиторий достаточно выполнить:
git crypt lock
и тут же все зашифрованные файлы превратятся в бинарное нечто, прочесть их будет невозможно.
Чтобы расшифровать репозиторий, выполните:
git crypt unlock
8. Создаём toolbox-образ
Toolbox-образ — это такой образ со всеми инструментами который мы будем использовать для деплоя нашего проекта. Он будет использоваться гитлаб-раннером для выполнения типовых задач деплоя.
Здесь всё просто, создаём новый dockerfiles/toolbox/Dockerfile с таким содержанием:
FROM alpine:3.11
RUN apk add --no-cache git git-crypt
RUN QBEC_VER=0.10.3
&& wget -O- https://github.com/splunk/qbec/releases/download/v${QBEC_VER}/qbec-linux-amd64.tar.gz
| tar -C /tmp -xzf -
&& mv /tmp/qbec /tmp/jsonnet-qbec /usr/local/bin/
RUN KUBECTL_VER=1.17.0
&& wget -O /usr/local/bin/kubectl
https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/linux/amd64/kubectl
&& chmod +x /usr/local/bin/kubectl
RUN HELM_VER=3.0.2
&& wget -O- https://get.helm.sh/helm-v${HELM_VER}-linux-amd64.tar.gz
| tar -C /tmp -zxf -
&& mv /tmp/linux-amd64/helm /usr/local/bin/helm
Как вы можете заметить, в этом образе мы устанавливаем все утилиты, которые мы использовали для деплоя нашего приложения. Нам не нужен здесь разве что kubectl, но возможно вы захотите поиграться с ним на этапе настройки пайплайна.
Также чтобы иметь возможность общаться с Kubernetes и выполнять в него деплой, нам нужно настроить роль для подов генерируемых gitlab-runner’ом.
Для этого перейдём в директорию с gitlab-runner’ом:
cd deploy/gitlab-runner
и добавим новый компонент components/rbac.jsonnet:
Думаю можно смело назвать это версией v0.0.1 и повесить тэг:
git tag v0.0.1
Тэги мы будем вешать всякий раз тогда, когда нам потребуется зарелизить новую версию. Тэги в Docker-образах будут привязаны к Git-тэгам. Каждый push с новым тэгом будет инициализировать сборку образов с этим тэгом.
Выполним git push —tags, и посмотрим на наш первый пайплайн:
Скриншот первого пайплайна
Стоит обратить ваше внимание на то, что сборка по тэгам годится для сборки docker-образов, но не подходит для деплоя приложения в Kubernetes. Так как новые тэги могут назначаться и для старых коммитов, в этом случае инициализация пайплайна для них приведёт к деплою старой версии.
Чтобы решить эту проблему обычно сборка docker-образов привязывается к тэгам, а деплой приложения к ветке មេ, в которой захардкожены версии собранных образов. Именно в этом случае вы сможете инициализировать rollback простым revert មេ-ветки.
10. Автоматизация деплоя
Для того чтобы Gitlab-runner мог расшифровать наши секреты, нам понадобится экспортировать ключ репозитория, и добавить его в переменные окружения нашей CI:
Здесь мы задействовали несколько новых опций для qbec:
—root some/app — позволяет определить директорию конкретного приложения
—force:k8s-context __incluster__ — это магическая переменная, которая говорит что деплой будет происходить в тотже кластер в котором запущен gtilab-runner. Сделать это необходимо, так как в противном случае qbec будет пытаться найти подходяший Kubernetes-сервер в вашем kubeconfig
—wait — заставляет qbec дождаться, когда создаваемые им ресурсы перейдут в состояние Ready и только потом завершится с успешным exit-code.
—yes — просто отключает интерактивный шелл តើអ្នកប្រាកដឬអត់? при деплое.
И после ជំរុញ git мы увидим как наши приложения были задеплоены:
Скриншот второго пайплайна
11. Артефакты и сборка при push в master
Обычно вышеописанных шагов вполне хватает для сборки и доставки почти любого микросервиса, но мы не хотим вешать тэг каждый раз когда нам понадобится обновить сайт. Поэтому мы пойдём более динамическим путём и настроим деплой по digest в master-ветке.
Идея проста: теперь образ нашего គេហទំព័រ будет пересобираться каждый раз при push в មេ, а после этого автоматически деплоиться в Kubernetes.
Давайте обновим эти две джобы в нашем .gitlab-ci.yml:
Обратите внимание, мы добавили ветку មេ к សេចក្តីយោង для джобы 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 который теперь будет выглядить так:
Готово, теперь любой коммит в មេ инициализиует сборку docker-образа для គេហទំព័រ, а затем его деплой в Kubernetes.
Не забываем закоммитить наши изменения:
git add .
git commit -m "Configure dynamic build"
Проверим, после ជំរុញ git мы должны увидеть что-то подобное:
Скриншот пайплайна для master
В принципе нам без надобности передеплоивать gitlab-runner при каждом push, если, конечно, ничего не изменилось в его кофигурации, давайте исправим это в .gitlab-ci.yml:
Настало время разнообразить наш пайплайн динамическими окружениями.
Для начала обновим джобу build_website នៅក្នុងរបស់យើង។ .gitlab-ci.yml, убрав из неё блок តែ, что заставит Gitlab тригеррить её при любом коммите в любую ветку:
Они будут запускаться при push в любые бренчи кроме master и будут деплоить preview версию сайта.
Мы видим новую опцию для qbec: —app-tag — она позволяет тэгировать задеплоенные версии приложения и работать только в пределах этого тэга, при создании и уничтожении ресурсов в Kubernetes qbec будет оперировать только ими.
Таким образом мы можем не создавать отдельный энвайромент под каждый review, а просто переиспользовать один и тот же.
Здесь мы так же используем qbec apply review, ជំនួសអោយ qbec apply default — это как раз тот самый момент когда мы постараемся описать различия для наших окружений (review и default):
Добавим ពិនិត្យឡើងវិញ окружение в deploy/website/qbec.yaml
Затем обьявим его в deploy/website/params.libsonnet:
local env = std.extVar('qbec.io/env');
local paramsMap = {
_: import './environments/base.libsonnet',
default: import './environments/default.libsonnet',
review: import './environments/review.libsonnet',
};
if std.objectHas(paramsMap, env) then paramsMap[env] else error 'environment ' + env + ' not defined in ' + std.thisFile
И запишем кастомные параметры для него в deploy/website/environments/review.libsonnet:
// this file has the param overrides for the default environment
local base = import './base.libsonnet';
local slug = std.extVar('qbec.io/tag');
local subdomain = std.extVar('subdomain');
base {
components+: {
website+: {
name: 'example-docs-' + slug,
domain: subdomain + '.docs.example.org',
},
},
}
Давайте также повнимательнее посмотрим на джобу stop_review, она будет тригериться при удалении брэнча и чтобы gitlab не пытался сделать checkout на неё используется GIT_STRATEGY: none, позже мы клонируем មេ-ветку и удаляем review через неё.
Немного заморочно, но более красивого способа я пока не нашёл.
Альтернативным вариантом может быть деплой каждого review в отельный неймспейс, который всегда можно снести целиком.
Всё работает? — отлично, удаляем нашу тестовую ветку: មេឆែកហ្គ្រីត, git push origin :test, проверяем что джобы на удаление environment отработали без ошибок.
Здесь сразу хочется уточнить, что создавать ветки может любой девелопер в проекте, он также может изменить .gitlab-ci.yml файл и получить доступ к секретным переменным.
По этому настоятельно рекомендуется разрешить их использование только для protected-веток, например в មេ, либо создать отдельный сет переменных под каждое окружение.
13. Review Apps
Review Apps это такая возможность гитлаба, которая позволяет для каждого файла в репозитории добавить кнопку для его быстрого просмотра в задеплоенном окружении.
Для того чтобы эти кнопки появились, необходимо создать файл .gitlab/route-map.yml и описать в нём все трансформации путей, в нашем случае это будет очень просто: