Привет! За последнее время вышло много классных инструментов автоматизации как для сборки Docker-образов так и для деплоя в Kubernetes. В связи с этим решил поиграться с гитлабом, как следует изучить его возможности и, конечно же, настроить пайплайн.
Вдохновлением для этой работы стал сайт kubernetes.io, который генерируется из исходных кодов автоматически, а на каждый присланный пул реквест робот автоматически генерирует preview-версию сайта с вашими изменениеми и предоставляет ссылку для просмотра.
Я постарался выстроить подобный процесс с нуля, но целиком построенный на Gitlab CI и свободных инструментах, которые я привык использовать для деплоя приложений в Kubernetes. Сегодня я, наконец, расскажу вам о них подробнее.
В статье будут рассмотрены такие инструменты как:
Hugo, QBEC, Kaniko, Git-crypt и GitLab CI с созданием динамических окружений.
Cодержание
- Знакомство с Hugo
- Подготовка Dockerfile
- Знакомство с Kaniko
- Знакомство с QBEC
- Пробуем Gitlab-runner с Kubernetes-executor
- Деплой Helm-чартов с QBEC
- Знакомство с git-crypt
- Создаём toolbox-образ
- Наш первый пайплайн и сборка образов по тэгам
- Автоматизация деплоя
- Артефакты и сборка при push в master
- Dynamic environments
- Review Apps
1. Знакомство с Hugo
В качестве примера нашего проекта мы попробуем создать сайт для публикации документации, построенный на Hugo. Hugo — это статический генератор контента.
Для тех, кто не знаком со статическими генераторами расскажу о них немного подробнее. В отличии от обычных движков сайтов с базой данных и каким-нибудь php, которые, при запросе пользователя, генерируют страницы на лету, статические генераторы устроенны немного иначе. Они позволяют взять исходники, как правило это набор файлов в Markdown-разметке и шаблоны тем, затем скомпилировать их в полностью готовый сайт.
То есть на выходе вы получите структуру директорий и набор сгенерированных html-файлов, которые можно будет просто залить на любой дешёвый
Hugo можно установить локально и попробовать его в деле:
Инициализируем новый сайт:
hugo new site docs.example.org
И заодно git-репозиторий:
cd docs.example.org
git init
Пока что наш сайт девственно чист и чтобы на нём что-то появилось сначала нам нужно подключить тему, тема — всего лишь это набор темплейтов и заданных правил по которым генерируется наш сайт.
В качестве темы мы будем использовать Learn, которая, на мой взгляд, как нельзя лучше подходит для сайта с документацией.
Отдельное внимание хочется уделить тому, что нам не требуется сохранять файлы темы в репозитории нашего проекта, вместо этого мы можем просто подключить её используя git submodule:
git submodule add https://github.com/matcornic/hugo-theme-learn themes/learn
Таким образом в нашем репозитории будут находиться только файлы непосредственно относящиеся к нашему проекту, а подключенная тема останется в виде ссылки на конкретный репозиторий и коммит в нём, то есть её всегда можно будет вытянуть из оригинального источника и не бояться несовместимых изменений.
Подправим конфиг 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
Содержимое директории public/ и будет являться вашим сайтом.
Да, кстати, давайте сразу внесём её в .gitignore:
echo /public > .gitignore
Не забываем закоммитить наши изменения:
git add .
git commit -m "New site created"
2. Подготовка Dockerfile
Настало время определить структуру нашего репозитория. Обычно я использую что-то вроде:
.
├── deploy
│ ├── app1
│ └── app2
└── dockerfiles
├── image1
└── image2
- 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-сервер) и public/ — контент нашего статически сгенирированного сайта.
Не забываем закоммитить наши изменения:
git add dockerfiles/website
git commit -m "Add Dockerfile for website"
3. Знакомство с Kaniko
В качестве сборщика docker-образов я решил использовать Kaniko, так как для его работы не требуется наличие docker-демона, а саму сборку можно проводить на любой машине и хранить кэш прямо в registry, избавлясь, тем самым, от необходимости иметь полноценное persistent-хранилище.
Для сборки образа достаточно запустить контейнер с kaniko executor и передать ему текущий контекст сборки, сделать это можно и локально, через docker:
docker run -ti --rm
-v $PWD:/workspace
-v ~/.docker/config.json:/kaniko/.docker/config.json:ro
gcr.io/kaniko-project/executor:v0.15.0
--cache
--dockerfile=dockerfiles/website/Dockerfile
--destination=registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1
Где registry.gitlab.com/kvaps/docs.example.org/website — имя вашего docker-образа, после сборки он будет автоматически запушен в docker-регистри.
Параметр --cache позволяет кэшировать слои в docker registry, для приведённого примера они будут сохраняються в registry.gitlab.com/kvaps/docs.example.org/website/cache, но вы можете указать и другой путь с помощью параметра --cache-repo.
4. Знакомство с QBEC
Qbec — это инструмент деплоя, который позволяет декларативно описывать манифесты вашего приложения и деплоить их в Kubernetes. Использование Jsonnet в качестве основного синтаксиса позволяет очень сильно упростить описание различий для нескольких окружений, а также почти полностью избавляет от повторяемости кода.
Это может быть особенно актуально в тех случаях, когда вам нужно задеплоить приложение в несколько кластеров с разными параметрами и вы хотите декларативно описать их в Git.
Qbec также позволяет рендерить Helm-чарты передавая им необходимые параметры и в дальнейшем оперировать ими также как и обычными манифестами, в том числе можно накладывать на них различные мутации, а это, в свою очередь, позволяет избавиться от необходимости использовать ChartMuseum. То есть можно хранить и рендерить чарты прямо из git, где им и самое место.
Как я говорил раньше, все деплойменты мы будем хранить в директории deploy/:
mkdir deploy
cd deploy
Давайте инициализируем наше первое приложение:
qbec init website
cd website
Сейчас структура нашего приложения выглядит так:
.
├── components
├── environments
│ ├── base.libsonnet
│ └── default.libsonnet
├── params.libsonnet
└── qbec.yaml
посмотрим на файл qbec.yaml:
apiVersion: qbec.io/v1alpha1
kind: App
metadata:
name: website
spec:
environments:
default:
defaultNamespace: docs
server: https://kubernetes.example.org:8443
vars: {}
Здесь нас интересует в первую очередь spec.environments, qbec уже создал за нас default окружение и взял адрес сервера, а также namespace из нашего текущего kubeconfig.
Теперь при деплое в default окружение, qbec всегда будет деплоить только в указанный Kubernetes-кластер и в указанный неймспейс, то есть вам больше не придётся переключаться между контекстами и неймспейсами для того чтобы выполнить деплой.
В случае необоходимости вы всегда можете обновить настройки в этом файле.
Все ваши окружения описываются в qbec.yaml, и в файле params.libsonnet, где сказано откуда нужно брать для них параметры.
Дальше мы видим две директории:
- components/ — здесь будут храниться все манифесты для нашего приложения, они могут быть описанны как в jsonnet так и обычными yaml-файлами
- environments/ — здесь мы будем описывать все переменные (параметры) для наших окружений.
По умолчанию мы имеем два файла:
- environments/base.libsonnet — он будет содержать общие параметры для всех окружений
- environments/default.libsonnet — содержит параметры для окружения default
Давайте откроем environments/base.libsonnet и добавим туда параметры для нашего первого компонента:
{
components: {
website: {
name: 'example-docs',
image: 'registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1',
replicas: 1,
containerPort: 80,
servicePort: 80,
nodeSelector: {},
tolerations: [],
ingressClass: 'nginx',
domain: 'docs.example.org',
},
},
}
Создадим также наш первый компонент components/website.jsonnet:
local env = {
name: std.extVar('qbec.io/env'),
namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.website;
[
{
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
labels: { app: params.name },
name: params.name,
},
spec: {
replicas: params.replicas,
selector: {
matchLabels: {
app: params.name,
},
},
template: {
metadata: {
labels: { app: params.name },
},
spec: {
containers: [
{
name: 'darkhttpd',
image: params.image,
ports: [
{
containerPort: params.containerPort,
},
],
},
],
nodeSelector: params.nodeSelector,
tolerations: params.tolerations,
imagePullSecrets: [{ name: 'regsecret' }],
},
},
},
},
{
apiVersion: 'v1',
kind: 'Service',
metadata: {
labels: { app: params.name },
name: params.name,
},
spec: {
selector: {
app: params.name,
},
ports: [
{
port: params.servicePort,
targetPort: params.containerPort,
},
],
},
},
{
apiVersion: 'extensions/v1beta1',
kind: 'Ingress',
metadata: {
annotations: {
'kubernetes.io/ingress.class': params.ingressClass,
},
labels: { app: params.name },
name: params.name,
},
spec: {
rules: [
{
host: params.domain,
http: {
paths: [
{
backend: {
serviceName: params.name,
servicePort: params.servicePort,
},
},
],
},
},
],
},
},
]
В данном файле мы описали сразу три Kubernetes-cущности, это: Deployment, Service и Ingress. При желании мы могли бы вынести их в разные компоненты, но на данном этапе нам хватит и одного.
Синтаксис jsonnet очень похож на обычный json, в принципе обычный json уже является валидным jsonnet, так что первое время вам возможно будет проще воспользоваться онлайн-сервисами вроде yaml2json чтобы сконвертировать привычный вам yaml в json, либо, если ваши компоненты не содержат никаких переменных, то их вполне можно описать в виде обычного yaml.
При работе с jsonnet очень советую установить вам плагин для вашего редактора
К примеру для vim есть плагин vim-jsonnet, который включает посветку синтаксиса и автоматически выполняет jesonnet 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-runner на заранее подготовленной машине (LXC-контейнере) с shell- или docker-executor. Изначально мы имели несколько таких раннеров глобально определённых в нашем гитлабе. Они собирали docker-образы для всех проектов.
Но как показала практика — этот вариант не самый идеальный, как в плане практичности, так и в плане безопасности. Гораздо лучше и идеологически правильней иметь отдельные раннеры задеплоенные для каждого проекта, а то и для каждого окружения.
К счастью это вовсе не проблема, так как теперь мы будем деплоить gitlab-runner непосредственно как часть нашего проекта прямо в Kubernetes.
Gitlab предоставляет готовый helm-чарт для деплоя gitlab-runner в Kubernetes. Таким образом всё что вам нужно, это узнать registration token для нашего проекта в Settings --> CI / CD --> Runners и передать его helm:
helm repo add gitlab https://charts.gitlab.io
helm install gitlab-runner
--set gitlabUrl=https://gitlab.com
--set runnerRegistrationToken=yga8y-jdCusVDn_t4Wxc
--set rbac.create=true
gitlab/gitlab-runner
Где:
- https://gitlab.com — адрес вашего Gitlab-сервера.
- yga8y-jdCusVDn_t4Wxc — registration token для вашего проекта.
- rbac.create=true — предоставляет раннеру необходимое количество привилегий, чтобы иметь возможность создавать поды для выполнения наших задач с помощью kubernetes-executor.
Если всё сделанно правильно, вы должны увидеть зарегистрированный раннер в секции Runners, в настройках вашего проекта.
Вот так просто? — да, так просто! Больше никакой мороки с регистрацией раннеров вручную, с этой минуты раннеры будут создаваться и уничтожаться автоматически.
6. Деплой Helm-чартов с QBEC
Так как мы приняли решение считать gitlab-runner частью нашего проекта, настало время описать его в нашем Git-репозитории.
Мы могли бы описать его как отдельный компонент website, но в дальнейшем мы планируем деплоить разные копии website очень часто, в отличии gitlab-runner, который будет задеплоен всего-лишь один раз на каждый Kubernetes-кластер. Так что давайте инициализируем отдельное приложение для него:
cd deploy
qbec init gitlab-runner
cd gitlab-runner
На этот раз мы не будем описывать Kubernetes-сущности вручную, а возьмём готовый Helm-чарт. Одним из преимуществ qbec является возможность рендерить Helm-чарты прямо из Git-репозитория.
Давайте подключим его используя git submodule:
git submodule add https://gitlab.com/gitlab-org/charts/gitlab-runner vendor/gitlab-runner
Теперь директория vendor/gitlab-runner содержит у нас репозиторий с чартом для gitlab-runner.
Подобным образом можно подключать и другие репозитории, например и целиком репозиторий с официальными чартами https://github.com/helm/charts
Давайте опишем компонент components/gitlab-runner.jsonnet:
local env = {
name: std.extVar('qbec.io/env'),
namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.gitlabRunner;
std.native('expandHelmTemplate')(
'../vendor/gitlab-runner',
params.values,
{
nameTemplate: params.name,
namespace: env.namespace,
thisFile: std.thisFile,
verbose: true,
}
)
Первым аргументом к expandHelmTemplate мы передаём путь к чарту, затем params.values, которые возьмём из параметров окружения, затем идёт объект с
- nameTemplate — название релиза
- namespace — неймспейс передаваемый хельму
- thisFile — обязательный параметр, передающий путь к текущему файлу
- verbose — показывает команду helm template со всеми аргументами при рендеринге чарта
Теперь опишем параметры для нашего компонента в environments/base.libsonnet:
local secrets = import '../secrets/base.libsonnet';
{
components: {
gitlabRunner: {
name: 'gitlab-runner',
values: {
gitlabUrl: 'https://gitlab.com/',
rbac: {
create: true,
},
runnerRegistrationToken: secrets.runnerRegistrationToken,
},
},
},
}
Обратите внимание runnerRegistrationToken мы забираем из внешнего файла secrets/base.libsonnet, давайте создадим его:
{
runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}
Проверим, всё ли работает:
qbec show default
если всё в порядке, то можем удалить наш ранее, задеплоенный через Helm, релиз:
helm uninstall gitlab-runner
и задеплоить его же, но уже через qbec:
qbec apply default
7. Знакомство с git-crypt
На данный момент структура нашей директории для gitlab-runner выглядит так:
.
├── components
│ ├── gitlab-runner.jsonnet
├── environments
│ ├── base.libsonnet
│ └── default.libsonnet
├── params.libsonnet
├── qbec.yaml
├── secrets
│ └── base.libsonnet
└── vendor
└── gitlab-runner (submodule)
Но хранить секреты в Git небезопасно, не так-ли? Так что нам нужно должным образом их зашифровать.
Обычно ради одной переменной это не всегда имеет смысл. Вы можете передавать секреты в qbec и через переменные окружения вашей CI-системы.
Но стоит заметить, что бывают и более сложные проекты, которые могут содержать гораздо больше секретов, передавать их все через переменные окружения будет крайне затруднительно.Кроме того в таком случае мне не удалось бы рассказать вам о таком замечательном инструменте как git-crypt.
git-crypt ещё удобен тем, что позволяет сохранить всю историю секретов, а также сравнивать, мерджить и разрешать кофликты так-же как мы привыкли делать это в случае с Git.
Первым делом после установки git-crypt нам нужно сгенерировать ключи для нашего репозитория:
git crypt init
Если у вас имеется pgp-ключ, то вы можете сразу добавить себя как collaborator'а для этого проекта:
git-crypt add-gpg-user kvapss@gmail.com
Таким образом вы всегда сможете расшифровать этот репозиторий используя свой приватный ключ.
Если же pgp-ключа у вас нет и не предвидится, то вы можете пойти другим путём и экспортировать ключ проекта:
git crypt export-key /path/to/keyfile
Таким образом любой, кто обладает экспортированным keyfile сможет расшифровать ваш репозиторий.
Настало время настроить наш первый секрет.
Напомню, мы по прежнему находимся в директории deploy/gitlab-runner/, где у нас имеется директория secrets/, давайте же зашифруем все файлы в ней, для этого создадим файл secrets/.gitattributes с таким содержанием:
* filter=git-crypt diff=git-crypt
.gitattributes !filter !diff
Как видно из содержания, все файлы по маске * будут прогоняться через 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:
local env = {
name: std.extVar('qbec.io/env'),
namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.rbac;
[
{
apiVersion: 'v1',
kind: 'ServiceAccount',
metadata: {
labels: {
app: params.name,
},
name: params.name,
},
},
{
apiVersion: 'rbac.authorization.k8s.io/v1',
kind: 'Role',
metadata: {
labels: {
app: params.name,
},
name: params.name,
},
rules: [
{
apiGroups: [
'*',
],
resources: [
'*',
],
verbs: [
'*',
],
},
],
},
{
apiVersion: 'rbac.authorization.k8s.io/v1',
kind: 'RoleBinding',
metadata: {
labels: {
app: params.name,
},
name: params.name,
},
roleRef: {
apiGroup: 'rbac.authorization.k8s.io',
kind: 'Role',
name: params.name,
},
subjects: [
{
kind: 'ServiceAccount',
name: params.name,
namespace: env.namespace,
},
],
},
]
Так же опишем новые параметры в environments/base.libsonnet, который теперь выглядит так:
local secrets = import '../secrets/base.libsonnet';
{
components: {
gitlabRunner: {
name: 'gitlab-runner',
values: {
gitlabUrl: 'https://gitlab.com/',
rbac: {
create: true,
},
runnerRegistrationToken: secrets.runnerRegistrationToken,
runners: {
serviceAccountName: $.components.rbac.name,
image: 'registry.gitlab.com/kvaps/docs.example.org/toolbox:v0.0.1',
},
},
},
rbac: {
name: 'gitlab-runner-deploy',
},
},
}
Обратите внимание $.components.rbac.name ссылается на name для компонента rbac
Давайте проверим что изменилось:
qbec diff default
и применим наши изменения в Kubernetes:
qbec apply default
Так же не забываем закоммитить наши изменения в git:
cd ../..
git add dockerfiles/toolbox
git commit -m "Add Dockerfile for toolbox"
git add deploy/gitlab-runner
git commit -m "Configure gitlab-runner to use toolbox"
9. Наш первый пайплайн и сборка образов по тэгам
В корне проекта мы создадим .gitlab-ci.yml с таким содержанием:
.build_docker_image:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug-v0.15.0
entrypoint: [""]
before_script:
- echo "{"auths":{"$CI_REGISTRY":{"username":"$CI_REGISTRY_USER","password":"$CI_REGISTRY_PASSWORD"}}}" > /kaniko/.docker/config.json
build_toolbox:
extends: .build_docker_image
script:
- /kaniko/executor --cache --context $CI_PROJECT_DIR/dockerfiles/toolbox --dockerfile $CI_PROJECT_DIR/dockerfiles/toolbox/Dockerfile --destination $CI_REGISTRY_IMAGE/toolbox:$CI_COMMIT_TAG
only:
refs:
- tags
build_website:
extends: .build_docker_image
variables:
GIT_SUBMODULE_STRATEGY: normal
script:
- /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_TAG
only:
refs:
- tags
Обратите внимание, мы используем GIT_SUBMODULE_STRATEGY: normal для тех джоб, где нужно явно инициализировать сабмодули перед выполнением.
Не забываем закоммитить наши изменения:
git add .gitlab-ci.yml
git commit -m "Automate docker build"
Думаю можно смело назвать это версией v0.0.1 и повесить тэг:
git tag v0.0.1
Тэги мы будем вешать всякий раз тогда, когда нам потребуется зарелизить новую версию. Тэги в Docker-образах будут привязаны к Git-тэгам. Каждый push с новым тэгом будет инициализировать сборку образов с этим тэгом.
Выполним git push --tags, и посмотрим на наш первый пайплайн:
Стоит обратить ваше внимание на то, что сборка по тэгам годится для сборки docker-образов, но не подходит для деплоя приложения в Kubernetes. Так как новые тэги могут назначаться и для старых коммитов, в этом случае инициализация пайплайна для них приведёт к деплою старой версии.
Чтобы решить эту проблему обычно сборка docker-образов привязывается к тэгам, а деплой приложения к ветке master, в которой захардкожены версии собранных образов. Именно в этом случае вы сможете инициализировать rollback простым revert master-ветки.
10. Автоматизация деплоя
Для того чтобы Gitlab-runner мог расшифровать наши секреты, нам понадобится экспортировать ключ репозитория, и добавить его в переменные окружения нашей CI:
git crypt export-key /tmp/docs-repo.key
base64 -w0 /tmp/docs-repo.key; echo
полученную строку сохраним в Gitlab, для этого перейдём в настройки нашего проекта:
Settings --> CI / CD --> Variables
И создадим новую переменную:
- Type: File
- Key: GITCRYPT_KEY
- Value: <ваша строка>
- Protected: true (на время обучения можно и false)
- Masked: true
- Scope: All environments
Теперь обновим наш .gitlab-ci.yml добавив в него:
.deploy_qbec_app:
stage: deploy
only:
refs:
- master
deploy_gitlab_runner:
extends: .deploy_qbec_app
variables:
GIT_SUBMODULE_STRATEGY: normal
before_script:
- base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
script:
- qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --yes
deploy_website:
extends: .deploy_qbec_app
script:
- qbec apply default --root deploy/website --force:k8s-context __incluster__ --yes
Здесь мы задействовали несколько новых опций для qbec:
- --root some/app — позволяет определить директорию конкретного приложения
- --force:k8s-context __incluster__ — это магическая переменная, которая говорит что деплой будет происходить в тотже кластер в котором запущен gtilab-runner. Сделать это необходимо, так как в противном случае qbec будет пытаться найти подходяший Kubernetes-сервер в вашем kubeconfig
- --yes — просто отключает интерактивный шелл Are you sure? при деплое.
Не забываем закоммитить наши изменения:
git add .gitlab-ci.yml
git commit -m "Automate deploy"
И после git push мы увидим как наши приложения были задеплоены:
11. Артефакты и сборка при push в master
Обычно вышеописанных шагов вполне хватает для сборки и доставки почти любого микросервиса, но мы не хотим вешать тэг каждый раз когда нам понадобится обновить сайт. Поэтому мы пойдём более динамическим путём и настроим деплой по digest в master-ветке.
Идея проста: теперь образ нашего website будет пересобираться каждый раз при push в master, а после этого автоматически деплоиться в Kubernetes.
Давайте обновим эти две джобы в нашем .gitlab-ci.yml:
build_website:
extends: .build_docker_image
variables:
GIT_SUBMODULE_STRATEGY: normal
script:
- mkdir -p $CI_PROJECT_DIR/artifacts
- /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
artifacts:
paths:
- artifacts/
only:
refs:
- master
- tags
deploy_website:
extends: .deploy_qbec_app
script:
- DIGEST="$(cat artifacts/website.digest)"
- qbec apply default --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST"
Обратите внимание, мы добавили ветку 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 который теперь будет выглядить так:
{
components: {
website: {
name: 'example-docs',
image: 'registry.gitlab.com/kvaps/docs.example.org/website@' + std.extVar('digest'),
replicas: 1,
containerPort: 80,
servicePort: 80,
nodeSelector: {},
tolerations: [],
ingressClass: 'nginx',
domain: 'docs.example.org',
},
},
}
Готово, теперь любой коммит в master инициализиует сборку docker-образа для website, а затем его деплой в Kubernetes.
Не забываем закоммитить наши изменения:
git add .
git commit -m "Configure dynamic build"
Проверим, после git push мы должны увидеть что-то подобное:
В принципе нам без надобности передеплоивать gitlab-runner при каждом push, если, конечно, ничего не изменилось в его кофигурации, давайте исправим это в .gitlab-ci.yml:
deploy_gitlab_runner:
extends: .deploy_qbec_app
variables:
GIT_SUBMODULE_STRATEGY: normal
before_script:
- base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
script:
- qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --yes
only:
changes:
- deploy/gitlab-runner/**/*
changes позволит следить за изменениями в deploy/gitlab-runner/ и будет тригерить нашу джобу только при наличии таковых
Не забываем закоммитить наши изменения:
git add .gitlab-ci.yml
git commit -m "Reduce gitlab-runner deploy"
git push, так-то лучше:
12. Dynamic environments
Настало время разнообразить наш пайплайн динамическими окружениями.
Для начала обновим джобу build_website в нашем .gitlab-ci.yml, убрав из неё блок only, что заставит Gitlab тригеррить её при любом коммите в любую ветку:
build_website:
extends: .build_docker_image
variables:
GIT_SUBMODULE_STRATEGY: normal
script:
- mkdir -p $CI_PROJECT_DIR/artifacts
- /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
artifacts:
paths:
- artifacts/
Затем обновим джобу deploy_website, добавим туда блок environment:
deploy_website:
extends: .deploy_qbec_app
environment:
name: prod
url: https://docs.example.org
script:
- DIGEST="$(cat artifacts/website.digest)"
- qbec apply default --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST"
Это позволит Gitlab ассоциировать джобу с prod окружением и выводить правильную ссылку на него.
Теперь добавим ещё две джобы:
deploy_website:
extends: .deploy_qbec_app
environment:
name: prod
url: https://docs.example.org
script:
- DIGEST="$(cat artifacts/website.digest)"
- qbec apply default --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST"
deploy_review:
extends: .deploy_qbec_app
environment:
name: review/$CI_COMMIT_REF_NAME
url: http://$CI_ENVIRONMENT_SLUG.docs.example.org
on_stop: stop_review
script:
- DIGEST="$(cat artifacts/website.digest)"
- qbec apply review --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
only:
refs:
- branches
except:
refs:
- master
stop_review:
extends: .deploy_qbec_app
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
stage: deploy
before_script:
- git clone "$CI_REPOSITORY_URL" master
- cd master
script:
- qbec delete review --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
variables:
GIT_STRATEGY: none
only:
refs:
- branches
except:
refs:
- master
when: manual
Они будут запускаться при push в любые бренчи кроме master и будут деплоить preview версию сайта.
Мы видим новую опцию для qbec: --app-tag — она позволяет тэгировать задеплоенные версии приложения и работать только в пределах этого тэга, при создании и уничтожении ресурсов в Kubernetes qbec будет оперировать только ими.
Таким образом мы можем не создавать отдельный энвайромент под каждый review, а просто переиспользовать один и тот же.
Здесь мы так же используем qbec apply review, вместо qbec apply default — это как раз тот самый момент когда мы постараемся описать различия для наших окружений (review и default):
Добавим review окружение в deploy/website/qbec.yaml
spec:
environments:
review:
defaultNamespace: docs
server: https://kubernetes.example.org:8443
Затем обьявим его в 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, позже мы клонируем master-ветку и удаляем review через неё.
Немного заморочно, но более красивого способа я пока не нашёл.
Альтернативным вариантом может быть деплой каждого review в отельный неймспейс, который всегда можно снести целиком.
Не забываем закоммитить наши изменения:
git add .
git commit -m "Enable automatic review"
git push, git checkout -b test, git push origin test, проверяем:
Всё работает? — отлично, удаляем нашу тестовую ветку: git checkout master, git push origin :test, проверяем что джобы на удаление environment отработали без ошибок.
Здесь сразу хочется уточнить, что создавать ветки может любой девелопер в проекте, он также может изменить .gitlab-ci.yml файл и получить доступ к секретным переменным.
По этому настоятельно рекомендуется разрешить их использование только для protected-веток, например в master, либо создать отдельный сет переменных под каждое окружение.
13. Review Apps
Review Apps это такая возможность гитлаба, которая позволяет для каждого файла в репозитории добавить кнопку для его быстрого просмотра в задеплоенном окружении.
Для того чтобы эти кнопки появились, необходимо создать файл .gitlab/route-map.yml и описать в нём все трансформации путей, в нашем случае это будет очень просто:
# Indices
- source: /content/(.+?)_index.(md|html)/
public: '1'
# Pages
- source: /content/(.+?).(md|html)/
public: '1/'
Не забываем закоммитить наши изменения:
git add .gitlab/
git commit -m "Enable review apps"
git push, и проверяем:
Job is done!
Исходники проекта:
- на Gitlab: https://gitlab.com/kvaps/docs.example.org
- на GitHub: https://github.com/kvaps/docs.example.org
Спасибо за внимание, надеюсь вам порнравилось
Автор: kvaps