Клиент, который взял блокировку, обязан её периодически продлять. Другой клиент, который ждёт блокировки может заметить, что она уже давно не продлевалась (есть некоторый небольшой timeout) и перехватить её. Если первый клиент не смог продлить блокировку из-за сетевого глюка, то он в какой-то момент придёт продлевать блокировку, которую уже перехватили — он это заметит и включит "особый обработчик" данной ситуации, который в случае werf как можно быстрее crash-ит текущий процесс.
Такой подход в lockgate не даёт 100% гарантию корректности для любого проекта, однако предоставляет возможность определить в своём проекте этот обработчик ситуации с перехватом блокировки. В случае werf аварийного завершения процесса в этой ситуации пока хватает, да и на практике кейс возникает очень редко.
Проблему запуска деплоя новой версии, когда не дождались окончания деплоя старой версии мы в werf решаем с помощью блокировки релиза по имени. В один момент времени выкатывается только один релиз с определённым именем, другой выкат будет дожидаться предыдущего (реализовано это с помощью распределённых блокировок через Kubernetes, деплой может быть запущен с любого хоста).
Небольшое дополнение к статье. В werf сейчас реализован принципиально новый подход к сборке и публикации образов в docker registry, отличный от того, что используется при docker build:
Werf делает push каждого собранного слоя (стадии) в registry сразу после успешной сборки, что позволяет сразу переиспользовать его в других процессах сборки на произвольных машинах. Это возможно благодаря новому алгоритму публикации и выборки стадий с оптимистичными блокировками.
На момент публикации финального образа по определённому имени все необходимые слои, из которых состоит этот образ, уже есть в registry. Werf по факту делает push лишь для небольшого слоя с некоторой мета-информацией и ставит на этот слой тег. Таким образом этот push происходит моментально, т.к. registry переиспользует существующие слои.
К тому же werf по умолчанию предлагает content-based-тегирование, что позволяет ещё лучше переиспользовать уже существующие слои и избавится от лишних ребилдов и редеплоев приложения.
Так-то оно так, но ни kaniko, ни buildah не умеют собирать по-настоящему распределённый кеш. Чтобы уже собранные слои были доступны во всех сборках.
В werf теперь возможно такое: werf собирает коммит номер 1 на одном билдере. Пользователь делает новый коммит в git, werf запускается на другом билдере, накладывает патч на уже существующий кеш из предыдущей сборки на другом раннере и публикует новый образ.
Это похоже на то, что предоставляет --cache-from в docker builder, но быстрее и эффективнее и с поддержкой ansible и инкрементальных пересборок по истории git и т.д.
Можно сказать по-другому: мы выстраиваем content-based файловую систему на основе docker-образов, новое состояние в этой фс создается на основе кеша stages путем сборки новых образов, идентификаторы и пересборка тесно связаны с историей правок в git.
Не так. При мерже собранный кеш становится доступен для всех остальных точно также как и изменения которые были сделаны в ветке становятся доступны всем только после merge. Если мерж был не fast-forward, то возможно произойдет пересборка связанная со слиянием изменений мастера и ветки, но опять же уже собранный кеш будет учавствовать в сборке.
Если для ветки сделать rebase, то кеш, собранный для старого коммита потеряется и более не будет использоваться, также как теряется родительская связь между коммитами при rebase — произойдет пересборка. При дальнейшем merge кеш не теряется.
Такая изоляция — это отчасти переложение логики гита на собираемые образы.
Как вариант — запоминать версию кода на этапе сборки образа. Но опять же commit-id — это неидеальный идентификатор кода, потому что его изменение не означает что код в образе изменился. При этом мог быть сделан какой-то нерелевантный коммит и сборщик не стал лишний раз пересобирать образ без надобности — в этом случае образ остался связан со старым коммитом.
Другой вариант — не встраивать информацию о коммите в образы вообще. Зачем добавлять в образ изменчивую инфу, тем более, что commit-id не отражает его содержимое. А добавлять инфу о коммите куда-то в рантайм — Kubernetes — в процессе выката приложения. При выкате werf, например, через values может передать commit-id в шаблоны. Дальше его можно положить в какой-нибудь ConfigMap, приложение же может доставать commit-id в рантайме — читать его из ConfigMap. Тут получается и овцы сыты, и волки целы: лишних пересборок нету, приложение может получить инфу о гите.
Итоговый образ точно также как и кеш стадий будет изолирован за счет создания разных тегов. Вот в этом и заключается влияние истории гита на тег.
Другими словами: вот есть у нас 2 образа одинаковых по контенту, но собранных для разных гит-веток. Верф изолирует эти 2 образа: создаст для каждого свой идентификатор.
При этом в рамках одной ветки образы с общим контентом будут иметь одинаковый идентификатор.
Цель: изоляция кеша стадий и итоговых образов собранных для разных веток в git. Кеш, собранный для какой-либо ветки от master не будет доступен в master до тех пор, пока эта ветка не будет смержена в master.
Это простой шаблонизатор yaml, встроенный в gitlab.
.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition'
image: ruby:2.1
services:
- postgres
- redis
test1:
<<: *job_definition # Merge the contents of the 'job_definition' alias
script:
- test1 project
test2:
<<: *job_definition # Merge the contents of the 'job_definition' alias
script:
- test2 project
Например werf автоматом использует токены, которые выдает gitlab для логина в docker-registry, который также задает gitlab. Werf автоматом использует gitlab-environment если он определен.
Так-то по возможности стараемся лишнего не делать, если это можно переложить на внешнюю CI-систему.
Какой в gitlab есть ci конкретно для деплоя в кубы, где верф будет избыточен, можно подробнее на примере? Везде где используется kubectl apply или helm upgrade можно использовать и werf deploy — тут никаких ограничений не вносится, даже наоборот werf deploy проще использовать из-за наличия интеграции с гитлабом.
Поддерживается любой синтаксис стандартного Dockerfile, т.к. для билда используется docker server (который может использовать buildkit).
После того как образы описаны в werf.yaml, собраны и опубликованы (через werf build-and-publish), их можно использовать для деплоя в кубы. Для этого надо описать chart. В описанном чарте можно ссылаться на имена образов из werf.yaml, в нашем случае например так:
Werf ориентирован на kubernetes. И чтобы деплоить приложения туда. И в дальнейшем чтобы собирать образы используя runner-ы работающие в самом kubernetes.
Планируем плотно развивать тему локальной разработки с использованием werf. Это возможно включает и поддержку docker-compose. Сейчас работаем над стабилизацией версии 1.0. Где-то в конце этого года можно ждать подвижек.
Не оставим, но в любом случае, когда выйдет стабильный helm 3 и будет способ мигрировать старые инсталляции на новый хельм — мы перейдем на кодовую базу helm 3 и будем использовать и развивать kubedog для слежения за ресурсами в werf.
Это только в теории. Есть проблемы с аутентификацией в api например. Ниже ответил развернуто.
В k8s еще есть куча оберток над этим api. Например, shared-informer-ы для слежения за состоянием ресурсов. Эта обертка решает низкоуровневые проблемы и является стандартной для написания kubernetes-операторов. В других языках такого нет, пока сам не напишешь.
Инфраструктура — главный аргумент. На Rust не может быть богаче инфраструктура именно для Docker и Kubernetes.
Например, по опыту с Ruby: отсуствие нормальных клиентов для Kubernetes. У нас был самописный, чтобы достучатся до некоторых фич API типа слежения за ресурсами. Самописный клиент привел к тому, что наш dapp не мог коннектится к Google Kubernetes Engine, из-за использования там кастомной схемы аутентфикации. А времени реализовать эту схему в нашем клиенте не было. Вот такого рода проблемы.
Когда перешли на Golang — мы просто включили стандартный клиент kubernetes и все, коннект к GKE из коробки. Если появится что-то новое — сразу будет из коробки.
Хороший вопрос. Так не сделали, потому что подход выработался не заранее перед началом работы, а уже во время переписывания.
Во время переписывания было удобно распределить работу параллельно по людям и каждому дать задачу типа: вот тебе компонент, ожидается бинарник с таким-то интерфейсом. При этом каких-то договоренностей о том, как устроен бинарь не было, главное чтобы делал то что надо и соответствовал интерфейсу. Так было удобнее делать работу именно параллельно с точки зрения редактирования кода. В случае когда один бинарь и над ним работает несколько человек — надо сразу определиться с "правильной" организацией кода, чтобы несколько человек не редактировало один файл. Нам же был больше нужен быстрый результат, чем экономия места.
Сейчас можно сказать, что ваш вариант правильнее и решает проблему с тем сколько места занимают бинарные зависимости. Но когда прога будет переписана полностью это уже не имеет значения, т.к. это временный код.
Kubectl wait можно использовать в CI/CD, но есть "нюансы".
1) Зафейлить ci/cd pipeline как можно раньше.
Например, есть deployment с ошибкой, он никогда не перейдет в состояние READY. Если запустить kubectl wait --for condition=Available,
то команда завершится с ошибкой только по timeout. Kubedog увидит ошибку и может сразу зафейлить ci/cd pipeline. Для пользователя это дает быстрый фидбек.
2) Показывать "что происходит" во время ожидания.
Kubedog следит не только за указанным ресурсом до достижения какого-то condition, но и за всеми связанными. И получает события и логи этих связанных ресурсов. Вся инфа объединяется в единый поток и выдается наверх. Грубо говоря это аггрегатор "новостей" по указанному ресурсу.
У нас изначально стояла задача: сделать штуку для выката, которая может сказать что "все хорошо", может сказать что "все плохо" и покажет "что происходит сейчас" во время ожидания выката.
В случае с kubectl wait надо придумывать отдельный поток/процесс для показа логов. А чтобы показать логи надо еще узнать имена связанных ресурсов. И как это все в CI/CD по-простому сделать неясно.
Но и логи это не все, еще есть event'ы. С ними отдельная история. В event'ах может содержаться важная пользователю информация для того, чтобы исправить ошибку.
Мы держали в голове такую мысль: следилка должна показать достаточно информации пользователю для дебага проблем. Не надо вызывать kubectl и копатся в консоли. В идеале достаточно посмотреть на вывод CI/CD pipeline и понять где ошибка.
3) Использовать как библиотеку.
Kubectl wait на данный момент это pkg связанный с cli. А хочется подключить этот wait в свое приложение. Будь то helm или dapp. Понятно почему они так делают: не обобщают раньше времени. Но мы сделали библиотеку, потому что было видение сразу.
4) Детали реализации.
Kubedog использует informer-ы из библиотеки того же куба. Это примитив, который позволяет следить за ресурсами сутками, обрабатывает всякие низкоуровневые косяки. Например, натравил сейчас kubectl wait на древний ресурс, который у меня создан месяц назад, вылезла сразу такая ошибка:
error: An error occurred while waiting for the condition to be satisfied: too old resource version: 847449 (1360240)
Вот informer-ы эту ситуацию обрабатывают. Их обычно используют для написания всяких controller-ов.
В общем это надежная такая обертка над низкоуровневым watch-ем. И за счет ее использования в kubedog нет вышеуказанной проблемы. Но это исправимо в kubectl, надо патч им предложить :).
Обобщая. Если сделать kubectl apply + kubedog rollout track, то сразу из коробки получаем и логи, и event-ы, и ожидание до готовности, и прерывание по ошибкам.
Честно, была идея попробовать в kubectl такие функции добавить. Это в идеале. Потому что тогда работа точно зря не пропадет. Но возможно чуть позже.
Указывать произвольную директорию нельзя, выбор только между временной директорией на каждую сборку и build-директорией, которая сохраняется между сборками. Build-директория создается в ~/.dapp/builds/<project-name> и содержит помимо mount'ов всякие кеши git'а, chef-cookbook'ов и т.п.
Sensitive data кодируется симметричным ключом и попадает в repo. При выкате, если указана переменная окружения с ключом, то секретные данные становятся доступны. Эту переменную окружения можно указывать, например, как gitlab-secret-variable. Секретные значения — это либо файлы, либо helm values. Соответственно в yaml-шаблонах либо читается уже расшифрованный на момент запуска файл либо подставляется значение из values.
Подробнее здесь: https://flant.github.io/dapp/secrets_for_deploy.html.
В плане самой фичи mount подойдет. Т.е. mount из host в minikube + mount из minikube в docker позволят пробросить директорию в контейнер из хост-системы. Но главная проблема не в этом, а в том, чтобы этот запущенный контейнер соответствовал тем инструкциям по сборке образа приложения, которые описываются в Dappfile. Например, при изменении Gemfile.lock надо сделать bundle install. Для этого в dapp предусмотрены стадии и для пересборки стадии можно поставить триггер на изменение файла Gemfile.lock. А в случае, если изменяется не Gemfile.lock, то при сборке новой версии происходит простое и быстрое наложение патча. Так вот, если поменялись какие-то файлы, от которых зависят инструкции по сборке каких-то стадий, то образ придется пересобрать и перезапустить контейнер приложения. Но если поменялись другие файлы приложения, то новый образ собирать не надо, если исходники уже примонтированы в запущенный контейнер.
Однако сам minikube mount мы пробовали использовать для хранения файлов docker-registry на хост-системе, а не внутри minikube на виртуалке. Но производительность этой штуки для docker-registry неприемлема.
По задумке dev-режима сборщика предполагается деплоить через helm в minikube. При этом описание helm-конфигурации для development-окружения, используемый namespace — на усмотрение пользователя.
Есть команда dapp kube minikube setup, которая:
Стартует minikube
Поднимает в minikube docker-registry
Поднимает proxy в host-системе для использования docker-registry
Предполагается такой сценарий использования:
Сборка образа в host-системе: dapp dimg build [--dev].
Загрузка образа в minikube docker-registry: dapp dimg push :minikube [--dev].
Но для web-приложений в development-режиме (например) необходимы mount-ы исходников в запущенные образа, чтобы изменения исходников подхватывались уже запущенным web-сервером автоматом. Поддержки такого режима работы из коробки пока нет.