Исправление ошибок на печатных машинках было утомительным. Нужно было вернуть каретку назад и перепечатывать ошибки, оставляя грязные, видимые исправления. Для больших изменений приходилось перепечатывать целые страницы.
Цифровые инструменты изменили все. Кнопка «Отменить» превратила исправления в простое нажатие клавиши, освобождая создателей от страха совершить непоправимую ошибку. Это подарило свободу пробовать, ошибаться и дорабатывать идеи.
Когда речь идет о доставке программного обеспечения, возможность откатывать изменения — критический фактор безопасности для команд, развертывающих новые функции, обновления или исправления ошибок. В частности, откаты влияют на один из ключевых показателей доставки программного обеспечения: среднее время восстановления (Mean Time to Recovery, MTTR).
MTTR измеряет, как быстро система может восстановиться после сбоев. У команд обычно есть два варианта: определить проблему и исправить ее (откат вперед) или откатиться к предыдущему известному стабильному состоянию.
Когда решение проблемы не является очевидным, либо проблема слишком серьезная, откат — самый быстрый способ восстановления работы сервиса. Именно поэтому наличие надежного механизма отката имеет решающее значение для сокращения MTTR и обеспечения высокой доступности сервисов.
Отмена изменений в локальной среде, например, в текстовом редакторе, — задача достаточно простая. Система отслеживает каждое изменение и может вернуться к предыдущему состоянию.
В распределенной системе, такой как современные облачные приложения, все не так просто. Изменения вносятся в множество компонентов с сложными зависимостями и конфигурациями.
Ключевой способ, позволяющий откатывать изменения, описан в книге Accelerate: The Science of Lean Software and DevOps. Авторы выделяют «полное управление конфигурацией» как одну из ключевых практик, позволяющих достичь высоких показателей производительности при доставке программного обеспечения:
Теоретически это означает, что если мы можем хранить всю информацию в системе контроля версий и иметь автоматический способ применения этих изменений, мы должны быть в состоянии откатиться к предыдущему состоянию, просто применив предыдущий коммит.
Принцип «полного управления конфигурацией» со временем развился в такие идеи, как IaC и GitOps. Эти практики предполагают ключевые для нас вещи:
Проекты ArgoCD и Flux популяризировали подход GitOps для управления кластерами Kubernetes. Обеспечивая структурированный способ определения желаемого состояния системы в Git (например, манифесты Kubernetes) и автоматическую синхронизацию с фактическим состоянием, инструменты GitOps предоставляют стандартизированный способ удовлетворения данного принципа.
На бумаге GitOps наконец-то предоставил нам рабочее решение для откатов. Откатите коммит, который привел к возникновению проблемы, и все проблемы исчезнут. Проблема решена. Конец разговора, верно?
Команды, которые пытались полностью следовать философии GitOps, обычно обнаруживали, что обещания декларативных и постоянно синхронизируемых рабочих процессов не так однозначны, как кажутся. Давайте разберемся почему.
Декларативное управление ресурсами прекрасно работает для stateless ресурсов, таких как контейнеры. Способ, которым Kubernetes обрабатывает развертывания, сервисы и другие ресурсы, идеально подходит для GitOps.
Рассмотрим, как работает типичный процесс развертывания:
Но будет ли это работать для ресурсов с состоянием stateful, таких как базы данных? Предположим, что нам нужно изменить схему базы данных. Можем ли мы применить те же принципы для отката миграции схемы? Вот как это может выглядеть:
Это могло бы сработать... но, вероятно, вскоре после этого вас бы уволили.
Ресурсами без сохранения состояния очень удобно управлять, так как мы всегда можем выбросить все, что не работает, и начать с чистого листа.
Но базы данных — это другое дело. Они включают не только программный компонент (движок базы данных) и конфигурацию (параметры сервера и схема), но и сами данные. А данные, по определению, не могут быть восстановлены из системы контроля версий.
Ресурсы с состоянием, такие как базы данных, требуют другого подхода к управлению изменениями.
Распространенная практика управления изменениями схемы в базах данных — использование скриптов миграции up и down вместе с инструментом миграции (таким как Flyway или Liquibase). Идея проста: когда вы хотите внести изменения в схему, вы пишете скрипт, который описывает, как применить изменение (миграция "вверх"). Дополнительно вы пишете скрипт, который описывает, как отменить изменение (миграция "вниз").
Например, вы хотите добавить столбец с именем short_bio в таблицу users. Ваш скрипт миграции "вверх" может выглядеть так:
ALTER TABLE users
ADD COLUMN short_bio TEXT;
А ваш скрипт миграции "вниз" — так:
ALTER TABLE users DROP COLUMN short_bio;
В теории эта концепция обоснована и удовлетворяет требованиям полного управления конфигурацией. Вся информация, необходимая для применения и отката изменений, хранится в системе контроля версий.
Однако теория, как всегда, сильно отличается от практики.
Несмотря на широкое принятие концепции, миграции вниз редко применяются на практике. Почему?
Наивные предположения
Когда вы пишете скрипт, вы по сути создаете сценарий, который будет выполнен в будущем для отката изменений. По определению, этот скрипт пишется до того, как изменения "вверх" применяются. Это значит, что миграция "вниз" основана на предположении, что изменения будут выполнены корректно.
Но что, если не так?
Допустим, миграция "вверх" должна была добавить два столбца, а файл "вниз" предназначен для удаления этих двух столбцов. Но что, если миграция выполнена частично, и только один столбец был добавлен? Запуск файла миграции "вниз" приведет к сбою.
Да, некоторые базы данных, такие как PostgreSQL, поддерживают транзакционный DDL, что означает, что если миграция не удается, изменения откатываются, и вы получаете состояние, соответствующее определенной версии. Но даже для PostgreSQL некоторые операции не могут быть выполнены в рамках транзакции, и база данных может оказаться в несогласованном состоянии.
Для MySQL, которая не поддерживает транзакционный DDL, ситуация еще хуже. Если миграция прерывается на полпути, вы остаетесь с частично выполненной миграцией и без возможности отката.
Потеря данных
Если вы работаете с локальной базой данных, механизм миграции "вверх/вниз" может ощущаться как нажатие на кнопки "Отменить" и "Повторить" в текстовом редакторе. Но в реальной среде с реальным трафиком это не так.
Если вы успешно внедрили миграцию, добавляющую столбец в таблицу, а затем решили ее откатить, обратная операция (DROP COLUMN) не просто удаляет столбец. Она удаляет все данные в этом столбце. Повторное применение миграции не вернет данные, так как они были потеряны.
По этой причине команды, которые хотят временно развернуть предыдущую версию приложения, обычно не откатывают изменения базы данных, так как это приведет к потере данных для их пользователей. Вместо этого они должны оценить ситуацию и найти другой способ решения проблемы.
Несовместимость с современными практиками развертывания
CD и GitOps выступают за то, чтобы процесс доставки ПО был автоматизированным и повторяемым. Это означает, что процесс развертывания должен быть детерминированным и не требовать ручного вмешательства. Обычно для этого нужен конвейер, который получает коммит и автоматически развертывает артефакты сборки из этого коммита в целевую среду.
Поскольку крайне редко встречаются проекты с нулевым уровнем сбоев при изменениях, все должны быть готовы к откату развертывания.
Теоретически откат развертывания должен быть таким же простым, как развертывание предыдущей версии приложения. Что касается версий нашего кода приложения, это работает идеально. Мы скачиваем и разворачиваем контейнерный образ, соответствующий предыдущей версии.
Эта стратегия не работает для базы данных по двум причинам:
У команды есть два варианта: либо они должны вручную вмешиваться, чтобы откатить изменения в базе данных, либо они должны разработать собственное решение, способное автоматизировать откат.
Возвращаясь к нашей основной теме — исследованию, могут ли откаты базы данных быть совместимы с GitOps, давайте расширим этот последний пункт.
Документация ArgoCD предполагает, что интеграция миграций схемы осуществляется с помощью Kubernetes Job, который выполняет выбранный вами инструмент миграции, и аннотирует эту задачу как PreSync-хук.
Этот образ обычно создается как часть вашего CI/CD-конвейера и будет содержать инструмент и скрипты миграции для соответствующего коммита или релиза.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: migrate
image: your-migration-image:{{ .Values.image.tag }} # Example using Helm values
restartPolicy: Never
Когда ArgoCD обнаруживает новый коммит в Git-репозитории, он создает новую задачу (Job), которая запускает инструмент миграции. Если миграция успешна, задача завершается, и разворачивается новая версия приложения.
Это работает для миграции "вверх". Но что происходит, когда нужно выполнить откат?
Команды часто сталкиваются с двумя проблемами:
Какие последствия?
Паттерн оператора — это нативный для Kubernetes способ расширения API Kubernetes для управления дополнительными ресурсами. Операторы обычно включают два основных компонента: определение пользовательского ресурса (CRD), который определяет новый тип ресурса, и контроллер, который отслеживает изменения этих ресурсов и предпринимает соответствующие действия.
Паттерн оператора идеально подходит для управления состоянием ресурсов, таких как базы данных. Расширяя API Kubernetes новым типом ресурса, представляющим схему базы данных, мы можем управлять изменениями способом, дружественным к GitOps. Специализированный контроллер может отслеживать изменения этих ресурсов и применять необходимые изменения к базе данных, в то время как наивная задача (Job) не может этого сделать.
Atlas — это оператор Kubernetes, который позволяет управлять схемами баз данных непосредственно из кластера Kubernetes. Оператор Atlas расширяет API Kubernetes для поддержки управления схемами баз данных.
Возможности, которые помогают создать решение для управления схемами, подходящее для GitOps:
Atlas поддерживает два типа потоков для управления изменениями схемы базы данных: декларативные и версионные. Эти потоки отражены в двух основных ресурсах, которыми управляет оператор Atlas:
Декларативный: AtlasSchema
С помощью AtlasSchema вы определяете желаемое состояние схемы базы данных декларативным образом, а также строку подключения к целевой базе данных.
Оператор отвечает за генерацию необходимых миграций, чтобы привести схему базы данных к желаемому состоянию, и за их применение к базе данных. Вот пример ресурса AtlasSchema:
apiVersion: db.atlasgo.io/v1alpha1
kind: AtlasSchema
metadata:
name: myapp
spec:
url: mysql://root:pass@mysql:3306/example
schema:
sql: |
create table users (
id int not null auto_increment,
name varchar(255) not null,
email varchar(255) unique not null,
short_bio varchar(255) not null,
primary key (id)
);
Когда ресурс AtlasSchema применяется к кластеру, оператор Atlas вычислит разницу (diff) между базой данных по указанному URL и желаемой схемой, и сгенерирует необходимые миграции, чтобы привести базу данных к нужному состоянию. Каждый раз, когда ресурс AtlasSchema обновляется, оператор пересчитывает разницу и применяет необходимые изменения.
Версионный поток: AtlasMigration
С помощью AtlasMigration вы определяете точную миграцию, которую хотите применить к базе данных. Оператор отвечает за применение всех необходимых миграций, чтобы привести схему базы данных к желаемому состоянию.
Вот пример ресурса AtlasMigration:
apiVersion: db.atlasgo.io/v1alpha1
kind: AtlasMigration
metadata:
name: atlasmigration-sample
spec:
url: mysql://root:pass@mysql:3306/example
dir:
configMapRef:
name: "migration-dir" # Ref to a ConfigMap containing the migration files
Оператор Atlas применяет миграции из каталога, указанного в поле dir, к базе данных по адресу url. Как и классические инструменты миграции, Atlas использует таблицу метаданных в целевой базе данных, чтобы отслеживать, какие миграции применены.
Оператор Atlas спроектирован для обработки откатов в стиле GitOps. Именно здесь раскрывается сила паттерна оператора, так как он может принимать сложные и разумные решения о том, как обрабатывать изменения управляемых ресурсов.
Чтобы откатить изменение схемы в среде, управляемой ArgoCD, достаточно просто вернуть ресурс AtlasSchema или AtlasMigration к предыдущей версии. Затем оператор Atlas проанализирует изменения и сгенерирует необходимые миграции, чтобы вернуть схему базы данных в нужное состояние.
Выше мы неоднократно говорили о крайних случаях, возникающих при откате изменений схемы базы данных, и пришли к выводу, что они требуют ручного рассмотрения и вмешательства. А что, если мы могли бы автоматизировать этот процесс?
Паттерн оператора предназначен для кодификации операционных знаний в программное обеспечение. Рассмотрим, как этот паттерн может быть использован для решения обсуждаемых проблем:
Мы рассмотрели вызовы, связанные с откатом изменений схемы базы данных в среде GitOps. Обсудили ограничения традиционного подхода с миграциями вверх/вниз и то, как паттерн оператора может быть использован для построения более надёжного и автоматизированного решения.