Хабрахабр

Пробуем новые инструменты для сборки и автоматизации деплоя в Kubernetes

За последнее время вышло много классных инструментов автоматизации как для сборки Docker-образов так и для деплоя в Kubernetes. Привет! В связи с этим решил поиграться с гитлабом, как следует изучить его возможности и, конечно же, настроить пайплайн.

Вдохновлением для этой работы стал сайт kubernetes.io, который генерируется из исходных кодов автоматически, а на каждый присланный пул реквест робот автоматически генерирует preview-версию сайта с вашими изменениеми и предоставляет ссылку для просмотра.

Сегодня я, наконец, расскажу вам о них подробнее. Я постарался выстроить подобный процесс с нуля, но целиком построенный на Gitlab CI и свободных инструментах, которые я привык использовать для деплоя приложений в Kubernetes.

В статье будут рассмотрены такие инструменты как:
Hugo, QBEC, Kaniko, Git-crypt и GitLab CI с созданием динамических окружений.

  1. Знакомство с Hugo
  2. Подготовка Dockerfile
  3. Знакомство с Kaniko
  4. Знакомство с QBEC
  5. Пробуем Gitlab-runner с Kubernetes-executor
  6. Деплой Helm-чартов с QBEC
  7. Знакомство с git-crypt
  8. Создаём toolbox-образ
  9. Наш первый пайплайн и сборка образов по тэгам
  10. Автоматизация деплоя
  11. Артефакты и сборка при push в master
  12. Dynamic environments
  13. Review Apps

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"

Обычно я использую что-то вроде: Настало время определить структуру нашего репозитория.

.
├── 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_${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"

В качестве сборщика 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.

Скриншот docker-registry

Использование Jsonnet в качестве основного синтаксиса позволяет очень сильно упростить описание различий для нескольких окружений, а также почти полностью избавляет от повторяемости кода. Qbec — это инструмент деплоя, который позволяет декларативно описывать манифесты вашего приложения и деплоить их в Kubernetes.

Это может быть особенно актуально в тех случаях, когда вам нужно задеплоить приложение в несколько кластеров с разными параметрами и вы хотите декларативно описать их в Git.

То есть можно хранить и рендерить чарты прямо из git, где им и самое место. Qbec также позволяет рендерить Helm-чарты передавая им необходимые параметры и в дальнейшем оперировать ими также как и обычными манифестами, в том числе можно накладывать на них различные мутации, а это, в свою очередь, позволяет избавиться от необходимости использовать ChartMuseum.

Как я говорил раньше, все деплойменты мы будем хранить в директории 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"

Изначально мы имели несколько таких раннеров глобально определённых в нашем гитлабе. До недавного времени я использовал только обычный gitlab-runner на заранее подготовленной машине (LXC-контейнере) с shell- или docker-executor. Они собирали docker-образы для всех проектов.

Гораздо лучше и идеологически правильней иметь отдельные раннеры задеплоенные для каждого проекта, а то и для каждого окружения. Но как показала практика — этот вариант не самый идеальный, как в плане практичности, так и в плане безопасности.

К счастью это вовсе не проблема, так как теперь мы будем деплоить gitlab-runner непосредственно как часть нашего проекта прямо в Kubernetes.

Таким образом всё что вам нужно, это узнать registration token для нашего проекта в Settings --> CI / CD --> Runners и передать его helm: Gitlab предоставляет готовый helm-чарт для деплоя gitlab-runner в Kubernetes.

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, в настройках вашего проекта.

Скриншот добавленного раннера

— да, так просто! Вот так просто? Больше никакой мороки с регистрацией раннеров вручную, с этой минуты раннеры будут создаваться и уничтожаться автоматически.

Так как мы приняли решение считать gitlab-runner частью нашего проекта, настало время описать его в нашем Git-репозитории.

Так что давайте инициализируем отдельное приложение для него: Мы могли бы описать его как отдельный компонент website, но в дальнейшем мы планируем деплоить разные копии website очень часто, в отличии gitlab-runner, который будет задеплоен всего-лишь один раз на каждый Kubernetes-кластер.

cd deploy
qbec init gitlab-runner
cd gitlab-runner

Одним из преимуществ qbec является возможность рендерить Helm-чарты прямо из Git-репозитория. На этот раз мы не будем описывать Kubernetes-сущности вручную, а возьмём готовый Helm-чарт.

Давайте подключим его используя 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

На данный момент структура нашей директории для 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

Он будет использоваться гитлаб-раннером для выполнения задач деплоя. 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"

В корне проекта мы создадим .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"

0. Думаю можно смело назвать это версией v0. 1 и повесить тэг:

git tag v0.0.1

Тэги в Docker-образах будут привязаны к Git-тэгам. Тэги мы будем вешать всякий раз тогда, когда нам потребуется зарелизить новую версию. Каждый push с новым тэгом будет инициализировать сборку образов с этим тэгом.

Выполним git push --tags, и посмотрим на наш первый пайплайн:

Скриншот первого пайплайна

Так как новые тэги могут назначаться и для старых коммитов, в этом случае инициализация пайплайна для них приведёт к деплою старой версии. Стоит обратить ваше внимание на то, что сборка по тэгам годится для сборки docker-образов, но не подходит для деплоя приложения в Kubernetes.

Именно в этом случае вы сможете инициализировать rollback простым revert master-ветки. Чтобы решить эту проблему обычно сборка docker-образов привязывается к тэгам, а деплой приложения к ветке master, в которой захардкожены версии собранных образов.

Для того чтобы 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 мы увидим как наши приложения были задеплоены:

Скриншот второго пайплайна

Поэтому мы пойдём более динамическим путём и настроим деплой по 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"

Стоит заметить, что это также будет работать и с тэгами, что позволит нам сохранять снапшоты сайта с определённой версией в docker-registry. Обратите внимание, мы добавили ветку master к refs для джобы build_website и мы теперь используем $CI_COMMIT_REF_NAME вместо $CI_COMMIT_TAG, то есть мы отвязываемся от тэгов в Git и теперь будем пушить образ с названием ветки коммита инициализировашего пайплайн.

Когда имя 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 мы должны увидеть что-то подобное:

Скриншот пайплайна для master

В принципе нам без надобности передеплоивать 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, так-то лучше:

Скриншот обновлённого пайплайна

Настало время разнообразить наш пайплайн динамическими окружениями.

Для начала обновим джобу 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, проверяем:

Скриншот созданных environments в Gitlab

— отлично, удаляем нашу тестовую ветку: git checkout master, git push origin :test, проверяем что джобы на удаление environment отработали без ошибок. Всё работает?

Здесь сразу хочется уточнить, что создавать ветки может любой девелопер в проекте, он также может изменить .gitlab-ci.yml файл и получить доступ к секретным переменным.
По этому настоятельно рекомендуется разрешить их использование только для protected-веток, например в master, либо создать отдельный сет переменных под каждое окружение.

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, и проверяем:

Скриншот кнопки Review App

Исходники проекта:

Спасибо за внимание, надеюсь вам порнравилось image

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»