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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


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


Вдохновлением для этой работы стал сайт kubernetes.io, который генерируется из исходных кодов автоматически, а на каждый присланный пул реквест робот автоматически генерирует 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. Dynamic environments
  13. 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.


Скриншот docker-registry




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


Скриншот пайплайна для 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, так-то лучше:


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




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


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


Всё работает? — отлично, удаляем нашу тестовую ветку: 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, и проверяем:


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


Job is done!


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


  • на Gitlab: https://gitlab.com/kvaps/docs.example.org
  • на GitHub: https://github.com/kvaps/docs.example.org

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

Источник: https://habr.com/ru/post/481662/


Интересные статьи

Интересные статьи

Ознакомившись с этим руководством, вы научитесь легко задавать целевые уровни обслуживания (SLO, от англ. Service Level Objectives) для работоспособности сервисов в Kubernetes с помощью&n...
Изображение: Unsplash Вопрос поиска удаленной работы в хороших компаниях из США и Европы актуален всегда – не все хотят переезжать в другую страну, а участвовать в интересных п...
В современном мире Kubernetes-облаков, так или иначе, приходится сталкиваться с ошибками в программном обеспечении, которые допустил не ты и не твой коллега, но решать их придется тебе. Данная ст...
Недавно обнаруженное скопление молодых звезд (обозначено голубой звездой) находится на периферии Млечного Пути. Эти звезды, вероятно, образовались из материала, происходящего из соседних карлик...
За годы эксплуатации Kubernetes в production у нас накопилось немало занимательных историй, как баги в различных системных компонентах приводили к неприятным и/или непонятным последствиям, вл...