Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

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

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

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

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

Cодержание

  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. Anviwònman dinamik
  13. Review Apps

1. Знакомство с Hugo

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

Для тех, кто не знаком со статическими генераторами расскажу о них немного подробнее. В отличии от обычных движков сайтов с базой данных и каким-нибудь php, которые, при запросе пользователя, генерируют страницы на лету, статические генераторы устроенны немного иначе. Они позволяют взять исходники, как правило это набор файлов в Markdown-разметке и шаблоны тем, затем скомпилировать их в полностью готовый сайт.

То есть на выходе вы получите структуру директорий и набор сгенерированных html-файлов, которые можно будет просто залить на любой дешёвый хостинг и получить рабочий сайт.

Hugo можно установить локально и попробовать его в деле:

Инициализируем новый сайт:

hugo new site docs.example.org

И заодно git-репозиторий:

cd docs.example.org
git init

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

В качестве темы мы будем использовать Aprann, которая, на мой взгляд, как нельзя лучше подходит для сайта с документацией.

Отдельное внимание хочется уделить тому, что нам не требуется сохранять файлы темы в репозитории нашего проекта, вместо этого мы можем просто подключить её используя 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 :-)

Скриншот только что созданной страницы

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

Для генерации сайта достаточно запустить:

hugo

Содержимое директории piblik/ и будет являться вашим сайтом.
Да, кстати, давайте сразу внесём её в .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 содержит два KI SOTI NAN, эта возможность называется multi-stage build и позволяет исключить из финального docker-образа всё ненужное.
Таким образом финальный образ у нас будет содержать только darkhttpd (легковесный HTTP-сервер) и piblik/ — контент нашего статически сгенирированного сайта.

Не забываем закоммитить наши изменения:

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

Ki kote registry.gitlab.com/kvaps/docs.example.org/website — имя вашего docker-образа, после сборки он будет автоматически запушен в docker-регистри.

Paramèt —cache позволяет кэшировать слои в docker registry, для приведённого примера они будут сохраняються в registry.gitlab.com/kvaps/docs.example.org/website/cache, но вы можете указать и другой путь с помощью параметра —cache-repo.

Скриншот docker-registry

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

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, где сказано откуда нужно брать для них параметры.

Дальше мы видим две директории:

  • konpozan / — здесь будут храниться все манифесты для нашего приложения, они могут быть описанны как в 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ущности, это: deplwaman, Lapòs и Pénétrer. При желании мы могли бы вынести их в разные компоненты, но на данном этапе нам хватит и одного.

sentaks jsonnet очень похож на обычный json, в принципе обычный json уже является валидным jsonnet, так что первое время вам возможно будет проще воспользоваться онлайн-сервисами вроде yaml2json чтобы сконвертировать привычный вам yaml в json, либо, если ваши компоненты не содержат никаких переменных, то их вполне можно описать в виде обычного yaml.

Lè w ap travay ak 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-kourè на заранее подготовленной машине (LXC-контейнере) с shell- или docker-executor. Изначально мы имели несколько таких раннеров глобально определённых в нашем гитлабе. Они собирали docker-образы для всех проектов.

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

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

Gitlab предоставляет готовый helm-чарт для деплоя gitlab-runner в Kubernetes. Таким образом всё что вам нужно, это узнать enskripsyon siy для нашего проекта в 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

Ki kote:

  • https://gitlab.com — адрес вашего Gitlab-сервера.
  • yga8y-jdCusVDn_t4Wxc — registration token для вашего проекта.
  • rbac.create=true — предоставляет раннеру необходимое количество привилегий, чтобы иметь возможность создавать поды для выполнения наших задач с помощью kubernetes-executor.

Если всё сделанно правильно, вы должны увидеть зарегистрированный раннер в секции Runners, в настройках вашего проекта.

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

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

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

6. Деплой Helm-чартов с QBEC

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

Мы могли бы описать его как отдельный компонент sit entènèt, но в дальнейшем мы планируем деплоить разные копии sit entènèt очень часто, в отличии gitlab-kourè, который будет задеплоен всего-лишь один раз на каждый 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 — обязательный параметр, передающий путь к текущему файлу
  • pwolib — показывает команду 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,
      },
    },
  },
}

Peye atansyon runnerRegistrationToken мы забираем из внешнего файла secrets/base.libsonnet, давайте создадим его:

{
  runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}

Проверим, всё ли работает:

qbec show default

если всё в порядке, то можем удалить наш ранее, задеплоенный через Helm, релиз:

helm uninstall gitlab-runner

и задеплоить его же, но уже через qbec:

qbec apply default

7. Знакомство с git-crypt

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 [email protected]

Таким образом вы всегда сможете расшифровать этот репозиторий используя свой приватный ключ.

Если же 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',
    },
  },
}

Peye atansyon $.components.rbac.name ssylaetsya na File для компонента 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, и посмотрим на наш первый пайплайн:

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

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

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

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

10. Автоматизация деплоя

Для того чтобы Gitlab-runner мог расшифровать наши секреты, нам понадобится экспортировать ключ репозитория, и добавить его в переменные окружения нашей CI:

git crypt export-key /tmp/docs-repo.key
base64 -w0 /tmp/docs-repo.key; echo

полученную строку сохраним в Gitlab, для этого перейдём в настройки нашего проекта:
Settings —> CI / CD —> Variables

И создадим новую переменную:

Kalite
Kle
Valè
Pwoteje
Masked
Dimansyon

File
GITCRYPT_KEY
<your string>
true (на время обучения можно и false)
true
All environments

Скриншот добавленной переменной

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

Теперь обновим наш .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__ --wait --yes

deploy_website:
  extends: .deploy_qbec_app
  script:
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes

Здесь мы задействовали несколько новых опций для qbec:

  • —root some/app — позволяет определить директорию конкретного приложения
  • —force:k8s-context __incluster__ — это магическая переменная, которая говорит что деплой будет происходить в тотже кластер в котором запущен gtilab-runner. Сделать это необходимо, так как в противном случае qbec будет пытаться найти подходяший Kubernetes-сервер в вашем kubeconfig
  • —wait — заставляет qbec дождаться, когда создаваемые им ресурсы перейдут в состояние Ready и только потом завершится с успешным exit-code.
  • —yes — просто отключает интерактивный шелл Èske w sèten? при деплое.

Не забываем закоммитить наши изменения:

git add .gitlab-ci.yml
git commit -m "Automate deploy"

И после git pouse мы увидим как наши приложения были задеплоены:

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

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

11. Артефакты и сборка при push в master

Обычно вышеописанных шагов вполне хватает для сборки и доставки почти любого микросервиса, но мы не хотим вешать тэг каждый раз когда нам понадобится обновить сайт. Поэтому мы пойдём более динамическим путём и настроим деплой по digest в master-ветке.

Идея проста: теперь образ нашего sit entènèt будет пересобираться каждый раз при push в mèt, а после этого автоматически деплоиться в 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__ --wait --yes --vm:ext-str digest="$DIGEST"

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

Когда имя docker-тэга для новой версии сайта может быть неизменно, мы по прежнему должны описывать изменения для Kubernetes, в противном случае он просто не передеплоит приложение из нового образа, так как не заметит никаких изменений в манифесте деплоймента.

Opsyon —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',
    },
  },
}

Готово, теперь любой коммит в mèt инициализиует сборку docker-образа для sit entènèt, а затем его деплой в Kubernetes.

Не забываем закоммитить наши изменения:

git add .
git commit -m "Configure dynamic build"

Проверим, после git pouse мы должны увидеть что-то подобное:

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

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

В принципе нам без надобности передеплоивать 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__ --wait --yes
  only:
    changes:
      - deploy/gitlab-runner/**/*

chanjman позволит следить за изменениями в deploy/gitlab-runner/ и будет тригерить нашу джобу только при наличии таковых

Не забываем закоммитить наши изменения:

git add .gitlab-ci.yml
git commit -m "Reduce gitlab-runner deploy"

git pouse, так-то лучше:

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

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

12. Dynamic environments

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

Для начала обновим джобу build_website nan nou .gitlab-ci.yml, убрав из неё блок sèlman, что заставит 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, добавим туда блок anviwònman:

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__ --wait --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__ --wait --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__ --wait --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, kote qbec apply default — это как раз тот самый момент когда мы постараемся описать различия для наших окружений (review и default):

Добавим revize окружение в 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, позже мы клонируем mèt-ветку и удаляем review через неё.
Немного заморочно, но более красивого способа я пока не нашёл.
Альтернативным вариантом может быть деплой каждого review в отельный неймспейс, который всегда можно снести целиком.

Не забываем закоммитить наши изменения:

git add .
git commit -m "Enable automatic review"

git pouse, git checkout -b test, git push origin test, проверяем:

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

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

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

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

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

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

Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

Job is done!

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

Спасибо за внимание, надеюсь вам понравилось Eseye nouvo zouti pou bati ak otomatize deplwaman nan Kubernetes

Sous: www.habr.com

Add nouvo kòmantè