Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

Вітання! Останнім часом вийшло багато класних інструментів автоматизації як для збирання Docker-образів так і для деплою Kubernetes. У зв'язку з цим вирішив погратися з гітлабом, як слід вивчити його можливості та, звичайно ж, налаштувати пайплайн.

Натхненням для цієї роботи став сайт kubernetes.io, який генерується з вихідних кодів автоматично, а на кожен надісланий пул реквест робот автоматично генерує preview-версію сайту з вашими змінами та надає посилання для перегляду.

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

У статті будуть розглянуті такі інструменти як:
Хьюго, qbec, каніко, 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. Огляд додатків

1. Знайомство з Hugo

Як приклад нашого проекту, ми спробуємо створити сайт для публікації документації, побудований на Hugo. Hugo – це статичний генератор контенту.

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

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

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

Ініціалізуємо новий сайт:

hugo new site docs.example.org

І заразом git-репозиторій:

cd docs.example.org
git init

Поки що наш сайт незаймано чистий і щоб на ньому щось з'явилося спочатку нам потрібно підключити тему, тема — це лише набір темплейтів і заданих правил за якими генерується наш сайт.

Як тему ми будемо використовувати Вчитисяяка, на мій погляд, якнайкраще підходить для сайту з документацією.

Окрему увагу хочеться приділити тому, що нам не потрібно зберігати файли теми в репозиторії нашого проекту, натомість ми можемо просто підключити її використовуючи підмодуль git:

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 :-)

Скріншот щойно створеної сторінки

Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

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

hugo

Вміст директорії публічний/ і буде вашим сайтом.
Так, до речі, давайте відразу внесемо її в .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 містить два З, ця можливість називається multi-stage build і дозволяє виключити із фінального docker-образу все непотрібне.
Таким чином фінальний образ у нас міститиме лише darkhttpd (легковажний HTTP-сервер) та публічний/ - Контент нашого статично згенерованого сайту.

Не забуваємо закомітити наші зміни:

git add dockerfiles/website
git commit -m "Add Dockerfile for website"

3. Знайомство з kaniko

Як збирач docker-образів я вирішив використовувати канікоТак як для його роботи не потрібна наявність docker-демона, а саму збірку можна проводити на будь-якій машині і зберігати кеш прямо в registre, позбавляючись, тим самим, необхідності мати повноцінне 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

Пробуємо нові інструменти для збирання та автоматизації деплою в 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.
Тепер при деплої в дефолт оточення, qbec завжди буде деплоїти тільки у вказаний Kubernetes-кластер і в зазначений неймспейс, тобто вам більше не доведеться перемикатися між контекстами та неймспейсами для того, щоб виконати деплой.
У разі потреби ви завжди можете оновити налаштування у цьому файлі.

Всі ваші оточення описуються в qbec.yaml, і у файлі params.libsonnet, де сказано, звідки потрібно брати для них параметри.

Далі ми бачимо дві директорії:

  • компоненти/ — тут зберігатимуться всі маніфести для нашої програми, вони можуть бути описані як у jsonnet так і звичайними yaml-файлами
  • середовища/ - тут ми описуватимемо всі змінні (параметри) для наших оточень.

За замовчуванням ми маємо два файли:

  • environments/base.libsonnet — він міститиме загальні параметри для всіх оточень
  • environments/default.libsonnet — містить параметри перевизначені для оточення дефолт

Давайте відкриємо 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-сутності, це: розгортання, Обслуговування и Вхід. За бажання ми могли б винести їх у різні компоненти, але на цьому етапі нам вистачить і одного.

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

При роботі з jsonnet дуже раджу встановити вам плагін для вашого редактора

Наприклад для vim є плагін vim-jsonnet, який включає підсвічування синтаксису та автоматично виконує jsonnet fmt при кожному збереженні (вимагається наявність встановлено jsonnet).

Все готово, тепер можемо починати деплою:

Щоб подивитися, що в нас вийшло, виконаємо:

qbec show default

На виході ви побачите відрендеровані yaml-маніфести, які будуть застосовані у кластері default.

Відмінно, тепер застосуємо:

qbec apply default

На виході ви завжди побачите, що буде зроблено у вашому кластері, qbec попросить вас погодитися зі змінами, набравши y ви можете підтвердити свої наміри.

Готово тепер наш додаток задеплоєно!

У разі внесення змін ви завжди зможете виконати:

qbec diff default

щоб подивитися як ці зміни позначаться на поточному депло

Не забуваємо закомітити наші зміни:

cd ../..
git add deploy/website
git commit -m "Add deploy for website"

5. Пробуємо Gitlab-runner з Kubernetes-executor

Донедавна я використовував лише звичайний gitlab-runner на заздалегідь підготовленій машині (LXC-контейнері) з shell або docker-executor. Спочатку ми мали кілька таких раннерів, глобально визначених у нашому гітлабі. Вони збирали образи для всіх проектів.

Але, як показала практика, цей варіант не найідеальніший, як у плані практичності, так і в плані безпеки. Набагато краще та ідеологічно правильніше мати окремі раннери задеплоєні для кожного проекту, а то й для кожного оточення.

На щастя, це зовсім не проблема, тому що тепер ми будемо деплоїти. gitlab-runner безпосередньо як частина нашого проекту прямо у Kubernetes.

Gitlab надає готовий helm-чарт для деплою gitlab-runner у Kubernetes. Таким чином все, що вам потрібно, це дізнатися реєстраційний маркер для нашого проекту в 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.

Якщо все зроблено правильно, ви повинні побачити зареєстрований раннер у секції Бігунив налаштуваннях вашого проекту.

Скріншот доданого раннера

Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

Так просто? - та так просто! Більше ніякої мороки з реєстрацією раннерів вручну, з цієї хвилини раннери будуть створюватися та знищуватись автоматично.

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

Оскільки ми вирішили вважати gitlab-runner частиною нашого проекту, настав час описати його у нашому Git-репозиторії.

Ми могли б описати його як окремий компонент сайт, але надалі ми плануємо деплоїти різні копії. сайт дуже часто, на відміну 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 - Назва релізу
  • простору імен - Неймспейс переданий Хельми
  • thisFile — обов'язковий параметр, який передає шлях до поточного файлу
  • докладний - показує команду 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

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

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

Настав час налаштувати наш перший секрет.
Нагадаю, ми, як і раніше, знаходимося в директорії 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

Як ви можете помітити, у цьому образі ми встановлюємо всі утиліти, які ми використовували для деплою нашої програми. Нам не потрібний тут хіба що кубектл, але можливо ви захочете погратися з ним на етапі налаштування пайплайну.

Також щоб мати можливість спілкуватися з 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 посилається на ім'я для компонента 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, і подивимося на наш перший пайплайн:

Скріншот першого пайплайну

Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

Варто звернути вашу увагу на те, що складання по тегах годиться для складання docker-образів, але не підходить для деплою програми в Kubernetes. Оскільки нові теги можуть бути призначені і для старих коммітів, у цьому випадку ініціалізація пайплайну для них призведе до деплою старої версії.

Щоб вирішити цю проблему зазвичай збирання docker-образів прив'язується до тегів, а деплой додатки до гілки майстер, в якій захардшкірено версії зібраних образів. Саме в цьому випадку ви зможете ініціалізувати rollback простим revert майстер-Гілки.

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

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

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

отриманий рядок збережемо в Gitlab, для цього перейдемо в налаштування нашого проекту:
Settings -> CI / CD -> Variables

І створимо нову змінну:

тип
ключ
значення
захищений
замаскований
Сфера

File
GITCRYPT_KEY
<your string>
true (на час навчання можна і false)
true
All environments

Скріншот доданої змінної

Пробуємо нові інструменти для збирання та автоматизації деплою в 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 - просто відключає інтерактивний шелл Ти впевнений? при депло.

Не забуваємо закомітити наші зміни:

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

І після git push ми побачимо як наші програми були задеплоєні:

Скріншот другого пайплайну

Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

11. Артефакти та складання при push у master

Зазвичай вищеописаних кроків цілком вистачає для складання та доставки майже будь-якого мікросервісу, але ми не хочемо вішати тег щоразу, коли нам знадобиться оновити сайт. Тому ми підемо динамічнішим шляхом і налаштуємо деплою по digest у master-гілці.

Ідея проста: тепер образ нашого сайт буде перезбиратися кожного разу у push у майстер, а після цього автоматично деплоїться у 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"

Зверніть увагу, ми додали гілку майстер к рек для джоби 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',
    },
  },
}

Готово, тепер будь-який коміт у майстер ініціалізує складання docker-образу для сайт, а потім його деплою в Kubernetes.

Не забуваємо закомітити наші зміни:

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

Перевіримо, після git push ми маємо побачити щось подібне:

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

Пробуємо нові інструменти для збирання та автоматизації деплою в 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/**/*

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

Не забуваємо закомітити наші зміни:

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

git push, так то краще:

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

Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

12. Dynamic environments

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

Для початку оновимо джобу build_website в нашому .gitlab-ci.ymlприбравши з неї блок тільки, що змусить 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, додамо туди блок навколишнє середовище:

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, замість qbec apply default — це саме той момент, коли ми спробуємо описати відмінності для наших оточень (review і default):

Додамо огляд оточення в 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, пізніше ми клонуємо майстер-Гілку і видаляємо review через неї.
Трохи заморочно, але красивішого способу поки що не знайшов.
Альтернативним варіантом може бути деплой кожного review в готельний неймспейс, який можна знести цілком.

Не забуваємо закомітити наші зміни:

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

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

Скріншот створених environments в Gitlab

Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

Все працює? - Відмінно, видаляємо нашу тестову гілку: майстер вивізного квитка, git push origin :test, перевіряємо, що джоби на видалення environment відпрацювали без помилок.

Тут відразу хочеться уточнити, що створювати гілки може будь-який девелопер у проекті, він також може змінити .gitlab-ci.yml файл та отримати доступ до секретних змінних.
Тому рекомендується дозволити їх використання тільки для protected-гілок, наприклад в майстерабо створити окремий сет змінних під кожне оточення.

13. 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

Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

Job is done!

Вихідники проекту:

Дякую за увагу, сподіваюся вам сподобалося Пробуємо нові інструменти для збирання та автоматизації деплою в Kubernetes

Джерело: habr.com

Додати коментар або відгук