Atlas — инструмент управления схемами миграций БД с открытым исходным кодом: наш опыт и ошибки создателей

от автора

О дивный мир, Infrastructure as a Code! С каждым днём появляется всё больше инструментов для работы с гетерогенными инфраструктурами любой сложности. На каждом уровне абстракции — свои утилиты и комбайны, которые если и не решают всех задач, то активно к этому стремятся. Примеров много. Самый популярный — Terraform. Чуть менее востребованы не такие функциональные Pulumi и Crossplane.

Все давно привыкли, что управлять контейнерами и кластерами Kubernetes помогает целый набор решений совершенно разного калибра и функциональности: Rancher, Deckhouse, OpenShift, Kubespray, Helm, werf, K9s, Komodor.

Привет! На связи Василий Мармер, DevOps-тимлид компании «Флант». Сегодня мы поговорим об Atlas — еще одной утилите, которая делает работу DevOps-инженера более комфортной. Atlas увидел мир в ноябре 2021 года, а сейчас у него более 3,3 тысячи звёзд на GitHub. Язык программирования — Go.

Введение

Рассматриваемая утилита служит для управления схемами миграций баз данных и позволяет задействовать современные паттерны DevOps. Разработчики Atlas воспринимают свой инструмент как универсальный способ визуализировать, планировать и выстраивать миграции схем баз данных в соответствии с CI.

Atlas и его оператор справляются не только с обычными MySQL и PostgreSQL, но и с MariaDB, TiDB и CockroachDB, неплохо поддерживает SQLite, что может стать подспорьем для разработчиков, которые предпочитают эту простую и легковесную базу данных. К сожалению, в рамках одной статьи протестировать их все невозможно.

Из важных особенностей Atlas стоит отметить, в частности, возможность настраивать политику сравнения схем: можно выбрать skip destructive (чтобы обезопасить всю базу целиком или конкретные таблицы) или concurrent_index (чтобы удалять или создавать индексы конкурентно, без блокирования таблиц, — это опция, кстати, по умолчанию выключена). Удобно и логично настраиваются типы колонок в HCL.

В Kubernetes можно использовать Atlas не только как оператор, но и в качестве init-контейнера конкретной базы данных. Ещё можно использовать Argo CD. Этот вариант выглядит наиболее интересным из всех, однако требует отдельной и довольно объемной статьи. Можно, конечно, ограничиться рекомендуемыми на официальном сайте инструментами миграции и импорта. Кроме того, есть неплохо описанная интеграция с Terraform — эта статья может стать неплохим подспорьем для тех, кто с помощью Terraform «крутит базы в облаках».

Одним словом, вариантов использования Atlas очень много: на хостах — как инструмент или контейнер, в Kubernetes — от sidecar-, init-контейнера, управления схемами баз данных (независимо от количества кластеров, в том числе созданных с помощью Custom Resources) до использования подхода GitOps с помощью Argo CD и подобных инструментов.

В любом случае теперь DDL гораздо проще сделать частью детально описанных инфраструктур.

Подготовка к экспериментам

Atlas позволяет работать двумя способами:

  • использование декларативного описания схемы базы данных (как в Terraform) — сравниваем текущее состояние базы со схемой, описанной в HCL-, SQL-форматах или используем интеграцию с ORM (в результате получаем diff для дальнейшей работы);

  • версионирование — планируем изменения, проверяем их и, наконец, применяем с помощью Atlas.

Нас, DevOps-инженеров, для воплощения мечты World as a Code интересует прежде всего именно декларативный вариант использования Atlas. Что ж, проведём полевые испытания.

Прежде всего установим на хост сам инструмент:

# curl -sSf https://atlasgo.sh | sh Install 'atlas-linux-amd64-latest' to '/usr/local/bin/atlas'? [y/N] y Downloading https://release.ariga.io/atlas/atlas-linux-amd64-latest   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                  Dload  Upload   Total   Spent    Left  Speed 100 69.0M  100 69.0M    0     0  51.5M      0  0:00:01  0:00:01 --:--:-- 51.5M atlas version v0.12.1-972da10-canary https://github.com/ariga/atlas/releases/latest Installation successful!

Не забываем про completion:

atlas completion bash > /etc/bash_completion.d/atlas

Для экспериментов нам понадобится плагин Compose для Docker. Убедимся, что он установлен:

# docker compose version Docker Compose version v2.19.1

Работа с Atlas в Docker Compose

Начнем изучение Atlas с использования Docker Compose. Не будем затрагивать самые основы, такие как установка и принципы работы, — они достаточно хорошо описаны в официальной документации. А более сложные варианты использования утилиты, такие как совместная работа с K8s, обсудим далее.

Первые опыты начнём над MySQL. Для этого поднимем в Docker небольшой пример:

version: '3.8'  services:   db:     image: mysql:8.0     command: --default-authentication-plugin=mysql_native_password     restart: always     environment:       MYSQL_ROOT_PASSWORD: groot_password     ports:       - "3306:3306" 

Создадим для наглядности простую базу данных:

DROP DATABASE IF EXISTS employees…
DROP DATABASE IF EXISTS employees; CREATE DATABASE IF NOT EXISTS employees; USE employees;  SELECT 'CREATING DATABASE STRUCTURE' as 'INFO';  DROP TABLE IF EXISTS dept_emp,                      dept_manager,                      titles,                      salaries,                       employees,                       departments;  /*!50503 set default_storage_engine = InnoDB */; /*!50503 select CONCAT('storage engine: ', @@default_storage_engine) as INFO */;  CREATE TABLE employees (     emp_no      INT             NOT NULL,     birth_date  DATE            NOT NULL,     first_name  VARCHAR(14)     NOT NULL,     last_name   VARCHAR(16)     NOT NULL,     gender      ENUM ('M','F')  NOT NULL,         hire_date   DATE            NOT NULL,     PRIMARY KEY (emp_no) );  CREATE TABLE departments (     dept_no     CHAR(4)         NOT NULL,     dept_name   VARCHAR(40)     NOT NULL,     PRIMARY KEY (dept_no),     UNIQUE  KEY (dept_name) );  CREATE TABLE dept_manager (    emp_no       INT             NOT NULL,    dept_no      CHAR(4)         NOT NULL,    from_date    DATE            NOT NULL,    to_date      DATE            NOT NULL,    FOREIGN KEY (emp_no)  REFERENCES employees (emp_no)    ON DELETE CASCADE,    FOREIGN KEY (dept_no) REFERENCES departments (dept_no) ON DELETE CASCADE,    PRIMARY KEY (emp_no,dept_no) );   CREATE TABLE dept_emp (     emp_no      INT             NOT NULL,     dept_no     CHAR(4)         NOT NULL,     from_date   DATE            NOT NULL,     to_date     DATE            NOT NULL,     FOREIGN KEY (emp_no)  REFERENCES employees   (emp_no)  ON DELETE CASCADE,     FOREIGN KEY (dept_no) REFERENCES departments (dept_no) ON DELETE CASCADE,     PRIMARY KEY (emp_no,dept_no) );  CREATE TABLE titles (     emp_no      INT             NOT NULL,     title       VARCHAR(50)     NOT NULL,     from_date   DATE            NOT NULL,     to_date     DATE,     FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE,     PRIMARY KEY (emp_no,title, from_date) );   CREATE TABLE salaries (     emp_no      INT             NOT NULL,     salary      INT             NOT NULL,     from_date   DATE            NOT NULL,     to_date     DATE            NOT NULL,     FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE,     PRIMARY KEY (emp_no, from_date) ); 

Зальем эту базу в наш MySQL-контейнер:

# cat db.sql | docker exec -i root-db-1 mysql -pgroot_password mysql: [Warning] Using a password on the command line interface can be insecure. INFO CREATING DATABASE STRUCTURE INFO storage engine: InnoDB 

Появилось шесть табличек с простой структурой:

# docker exec -it root-db-1 mysql -pgroot_password -e 'use employees; show tables' mysql: [Warning] Using a password on the command line interface can be insecure. +---------------------+ | Tables_in_employees | +---------------------+ | departments         | | dept_emp            | | dept_manager        | | employees           | | salaries            | | titles              | +---------------------+ 

Теперь можно испытать Atlas в деле:

# atlas schema inspect -u «mysql://root…
# atlas schema inspect -u "mysql://root:groot_password@localhost:3306/employees" table "departments" {   schema = schema.employees   column "dept_no" {     null = false     type = char(4)   }   column "dept_name" {     null = false     type = varchar(40)   }   primary_key {     columns = [column.dept_no]   }   index "dept_name" {     unique  = true     columns = [column.dept_name]   } } table "dept_emp" {   schema = schema.employees   column "emp_no" {     null = false     type = int   }   column "dept_no" {     null = false     type = char(4)   }   column "from_date" {     null = false     type = date   }   column "to_date" {     null = false     type = date   }   primary_key {     columns = [column.emp_no, column.dept_no]   }   foreign_key "dept_emp_ibfk_1" {     columns     = [column.emp_no]     ref_columns = [table.employees.column.emp_no]     on_update   = NO_ACTION     on_delete   = CASCADE   }   foreign_key "dept_emp_ibfk_2" {     columns     = [column.dept_no]     ref_columns = [table.departments.column.dept_no]     on_update   = NO_ACTION     on_delete   = CASCADE   }   index "dept_no" {     columns = [column.dept_no]   } } table "dept_manager" {   schema = schema.employees   column "emp_no" {     null = false     type = int   }   column "dept_no" {     null = false     type = char(4)   }   column "from_date" {     null = false     type = date   }   column "to_date" {     null = false     type = date   }   primary_key {     columns = [column.emp_no, column.dept_no]   }   foreign_key "dept_manager_ibfk_1" {     columns     = [column.emp_no]     ref_columns = [table.employees.column.emp_no]     on_update   = NO_ACTION     on_delete   = CASCADE   }   foreign_key "dept_manager_ibfk_2" {     columns     = [column.dept_no]     ref_columns = [table.departments.column.dept_no]     on_update   = NO_ACTION     on_delete   = CASCADE   }   index "dept_no" {     columns = [column.dept_no]   } } table "employees" {   schema = schema.employees   column "emp_no" {     null = false     type = int   }   column "birth_date" {     null = false     type = date   }   column "first_name" {     null = false     type = varchar(14)   }   column "last_name" {     null = false     type = varchar(16)   }   column "gender" {     null = false     type = enum("M","F")   }   column "hire_date" {     null = false     type = date   }   primary_key {     columns = [column.emp_no]   } } table "salaries" {   schema = schema.employees   column "emp_no" {     null = false     type = int   }   column "salary" {     null = false     type = int   }   column "from_date" {     null = false     type = date   }   column "to_date" {     null = false     type = date   }   primary_key {     columns = [column.emp_no, column.from_date]   }   foreign_key "salaries_ibfk_1" {     columns     = [column.emp_no]     ref_columns = [table.employees.column.emp_no]     on_update   = NO_ACTION     on_delete   = CASCADE   } } table "titles" {   schema = schema.employees   column "emp_no" {     null = false     type = int   }   column "title" {     null = false     type = varchar(50)   }   column "from_date" {     null = false     type = date   }   column "to_date" {     null = true     type = date   }   primary_key {     columns = [column.emp_no, column.title, column.from_date]   }   foreign_key "titles_ibfk_1" {     columns     = [column.emp_no]     ref_columns = [table.employees.column.emp_no]     on_update   = NO_ACTION     on_delete   = CASCADE   } } schema "employees" {   charset = "utf8mb4"   collate = "utf8mb4_0900_ai_ci" } 

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

Важный момент: нам нужен ключ --dev-url, который указывает на соединение с пустой dev-базой, с помощью которой Atlas симулирует, подсчитывает и проигрывает изменения схем. Укажем docker://mysql/8/schema  — и Atlas сам поднимет пустую вре́менную базу в Docker.

Получаем готовый diff:

# atlas schema diff -f file://employees.hcl --to file://employees_v2.hcl --dev-url docker://mysql/8/schema -- Modify "departments" table ALTER TABLE `departments` MODIFY COLUMN `dept_name` varchar(200) NOT NULL; -- Modify "employees" table ALTER TABLE `employees` MODIFY COLUMN `first_name` varchar(50) NOT NULL, MODIFY COLUMN `last_name` varchar(50) NOT NULL; 

Такой же результат будет, если сравнить схему базы данных и новый файл.

Продолжим эксперименты. Добавим ещё одну (уже вторую) пустую базу данных, просто скопировав предыдущий контейнер и изменив порт, который пробрасывается на localhost:

# docker compose up -d [+] Running 2/2  ✔ Container root-db2-1  Started                                                                                                                                                                           1.1s  ✔ Container root-db-1   Running # cat db.sql | docker exec -i root-db2-1 mysql -pgroot_password mysql: [Warning] Using a password on the command line interface can be insecure. INFO CREATING DATABASE STRUCTURE INFO storage engine: InnoDB # atlas schema diff -f mysql://root:groot_password@localhost:3306/employees --to mysql://root:groot_password@localhost:3307/employees --dev-url docker://mysql/8/schema Schemas are synced, no changes to be made. 

Приведем первую базу данных к состоянию первой миграции с помощью файла схемы:

# atlas schema apply -u mysql://root:groot_password@localhost:3306/employees --to file://employees_v2.hcl --dry-run -- Planned Changes: -- Modify "departments" table ALTER TABLE `employees`.`departments` MODIFY COLUMN `dept_name` varchar(200) NOT NULL; -- Modify "employees" table ALTER TABLE `employees`.`employees` MODIFY COLUMN `first_name` varchar(50) NOT NULL, MODIFY COLUMN `last_name` varchar(50) NOT NULL; # atlas schema apply -u mysql://root:groot_password@localhost:3306/employees --to file://employees_v2.hcl -- Planned Changes: -- Modify "departments" table ALTER TABLE `employees`.`departments` MODIFY COLUMN `dept_name` varchar(200) NOT NULL; -- Modify "employees" table ALTER TABLE `employees`.`employees` MODIFY COLUMN `first_name` varchar(50) NOT NULL, MODIFY COLUMN `last_name` varchar(50) NOT NULL; ✔ Apply

Вторую базу данных приведем к состоянию первой напрямую:

# atlas schema apply -u mysql://root:groot_password@localhost:3307/employees --to mysql://root:groot_password@localhost:3306/employees --dry-run -- Planned Changes: -- Modify "departments" table ALTER TABLE `departments` MODIFY COLUMN `dept_name` varchar(200) NOT NULL; -- Modify "employees" table ALTER TABLE `employees` MODIFY COLUMN `first_name` varchar(50) NOT NULL, MODIFY COLUMN `last_name` varchar(50) NOT NULL; # atlas schema apply -u mysql://root:groot_password@localhost:3307/employees --to mysql://root:groot_password@localhost:3306/employees --auto-approve -- Planned Changes: -- Modify "departments" table ALTER TABLE `departments` MODIFY COLUMN `dept_name` varchar(200) NOT NULL; -- Modify "employees" table ALTER TABLE `employees` MODIFY COLUMN `first_name` varchar(50) NOT NULL, MODIFY COLUMN `last_name` varchar(50) NOT NULL; 

Ключ --auto-approve позволяет выполнять операции без вмешательства пользователя.

Дальше посмотрим, как Atlas работает с PostgreSQL. Для этого понадобится короткий compose:

version: '3.8'  services:   db:     image: postgres     restart: always     environment:       POSTGRES_PASSWORD: cheburek15     ports:       - "5432:5432"   db2:     image: postgres     restart: always     environment:       POSTGRES_PASSWORD: cheburek15     ports:       - "5433:5432” 

Запустим его:

# docker compose up -d [+] Running 3/3  ✔ Network root_default  Created                                                                                                                                                                           0.1s  ✔ Container root-db2-1  Started                                                                                                                                                                           1.2s  ✔ Container root-db-1   Started 

Теперь приготовим тестовый DDL:

CREATE SCHEMA IF NOT EXISTS cd;  CREATE TABLE cd.facilities (     facid integer NOT NULL PRIMARY KEY,     name character varying(100) NOT NULL,     membercost numeric NOT NULL,     guestcost numeric NOT NULL,     initialoutlay numeric NOT NULL,     monthlymaintenance numeric NOT NULL );  CREATE TABLE cd.members (     memid integer NOT NULL PRIMARY KEY,     surname character varying(200) NOT NULL,     firstname character varying(200) NOT NULL,     address character varying(300) NOT NULL,     zipcode integer NOT NULL,     telephone character varying(20) NOT NULL,     recommendedby integer,     joindate timestamp without time zone NOT NULL ); 

Зальём его в первую базу:

# cat psql1.sql | docker exec -i root-db-1 psql postgresql://postgres:cheburek15@root-db-1 CREATE SCHEMA CREATE TABLE CREATE TABLE 

Подготовим слегка изменённый DDL:

CREATE SCHEMA IF NOT EXISTS cd;  CREATE TABLE IF NOT EXISTS cd.facilities (     facid integer NOT NULL PRIMARY KEY,     name character varying(100) NOT NULL,     membercost numeric NOT NULL,     guestcost numeric NOT NULL,     initialoutlay numeric NOT NULL,     monthlymaintenance numeric NOT NULL );  CREATE TABLE IF NOT EXISTS cd.members (     memid integer NOT NULL PRIMARY KEY,     surname character varying(200) NOT NULL,     firstname character varying(200) NOT NULL,     address character varying(300) NOT NULL,     zipcode integer NOT NULL,     telephone character varying(20) NOT NULL,     recommendedby integer,     joindate timestamp without time zone NOT NULL,     FOREIGN KEY (recommendedby) REFERENCES cd.members(memid) ON DELETE SET NULL );  CREATE TABLE IF NOT EXISTS cd.bookings (     bookid integer NOT NULL PRIMARY KEY,     facid integer NOT NULL,     memid integer NOT NULL,     starttime timestamp without time zone NOT NULL,     slots integer NOT NULL,         FOREIGN KEY (facid) REFERENCES cd.facilities(facid),         FOREIGN KEY (memid) REFERENCES cd.members(memid) ); 

Зальем наш DDL во вторую базу:

# cat psql2.sql | docker exec -i root-db2-1 psql postgresql://postgres:cheburek15@root-db2-1 CREATE SCHEMA CREATE TABLE CREATE TABLE CREATE TABLE 

Применим изменения во второй базе данных к первой. Всё происходит так, как и ожидалось:

# atlas schema apply -u postgresql://postgres:cheburek15@localhost:5432/postgres?sslmode=disable --to postgresql://postgres:cheburek15@localhost:5433/postgres?sslmode=disable --dry-run -- Planned Changes: -- Modify "members" table ALTER TABLE "cd"."members" ADD CONSTRAINT "members_recommendedby_fkey" FOREIGN KEY ("recommendedby") REFERENCES "cd"."members" ("memid") ON UPDATE NO ACTION ON DELETE SET NULL; -- Create "bookings" table CREATE TABLE "cd"."bookings" ("bookid" integer NOT NULL, "facid" integer NOT NULL, "memid" integer NOT NULL, "starttime" timestamp NOT NULL, "slots" integer NOT NULL, PRIMARY KEY ("bookid"), CONSTRAINT "bookings_facid_fkey" FOREIGN KEY ("facid") REFERENCES "cd"."facilities" ("facid") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "bookings_memid_fkey" FOREIGN KEY ("memid") REFERENCES "cd"."members" ("memid") ON UPDATE NO ACTION ON DELETE NO ACTION);  # atlas schema apply -u postgresql://postgres:cheburek15@localhost:5432/postgres?sslmode=disable --to postgresql://postgres:cheburek15@localhost:5433/postgres?sslmode=disable --auto-approve -- Planned Changes: -- Modify "members" table ALTER TABLE "cd"."members" ADD CONSTRAINT "members_recommendedby_fkey" FOREIGN KEY ("recommendedby") REFERENCES "cd"."members" ("memid") ON UPDATE NO ACTION ON DELETE SET NULL; -- Create "bookings" table CREATE TABLE "cd"."bookings" ("bookid" integer NOT NULL, "facid" integer NOT NULL, "memid" integer NOT NULL, "starttime" timestamp NOT NULL, "slots" integer NOT NULL, PRIMARY KEY ("bookid"), CONSTRAINT "bookings_facid_fkey" FOREIGN KEY ("facid") REFERENCES "cd"."facilities" ("facid") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "bookings_memid_fkey" FOREIGN KEY ("memid") REFERENCES "cd"."members" ("memid") ON UPDATE NO ACTION ON DELETE NO ACTION); 

Если подумать — плоховато нам будет без индексов. Для разнообразия накинем их вручную на вторую базу:

CREATE INDEX…
CREATE INDEX "bookings.memid_facid"   ON cd.bookings   USING btree   (memid, facid);  CREATE INDEX "bookings.facid_memid"   ON cd.bookings   USING btree   (facid, memid);  CREATE INDEX "bookings.facid_starttime"   ON cd.bookings   USING btree   (facid, starttime);  CREATE INDEX "bookings.memid_starttime"   ON cd.bookings   USING btree   (memid, starttime);  CREATE INDEX "bookings.starttime"   ON cd.bookings   USING btree   (starttime);  CREATE INDEX "members.joindate"   ON cd.members   USING btree   (joindate);  CREATE INDEX "members.recommendedby"   ON cd.members   USING btree   (recommendedby);  # cat psql3.sql | docker exec -i root-db2-1 psql postgresql://postgres:cheburek15@root-db2-1 CREATE INDEX CREATE INDEX CREATE INDEX CREATE INDEX CREATE INDEX CREATE INDEX CREATE INDEX 

Теперь, чтобы накинуть индексы на первую базу, испытаем миграции: сначала создадим с помощью create_tables миграцию с базы данных без индексов:

# atlas migrate diff create_tables --dir "file://migrations" --to "postgresql://postgres:cheburek15@localhost:5432/postgres?sslmode=disable" --dev-url "docker://postgres/15/test"

Посмотрим, что содержит директория:

# ls migrations/ 20230712165510_create_tables.sql  atlas.sum # cat migrations/20230712165510_create_tables.sql -- Add new schema named "cd" CREATE SCHEMA "cd"; -- Create "facilities" table CREATE TABLE "cd"."facilities" ("facid" integer NOT NULL, "name" character varying(100) NOT NULL, "membercost" numeric NOT NULL, "guestcost" numeric NOT NULL, "initialoutlay" numeric NOT NULL, "monthlymaintenance" numeric NOT NULL, PRIMARY KEY ("facid")); -- Create "members" table CREATE TABLE "cd"."members" ("memid" integer NOT NULL, "surname" character varying(200) NOT NULL, "firstname" character varying(200) NOT NULL, "address" character varying(300) NOT NULL, "zipcode" integer NOT NULL, "telephone" character varying(20) NOT NULL, "recommendedby" integer NULL, "joindate" timestamp NOT NULL, PRIMARY KEY ("memid"), CONSTRAINT "members_recommendedby_fkey" FOREIGN KEY ("recommendedby") REFERENCES "cd"."members" ("memid") ON UPDATE NO ACTION ON DELETE SET NULL); -- Create "bookings" table CREATE TABLE "cd"."bookings" ("bookid" integer NOT NULL, "facid" integer NOT NULL, "memid" integer NOT NULL, "starttime" timestamp NOT NULL, "slots" integer NOT NULL, PRIMARY KEY ("bookid"), CONSTRAINT "bookings_facid_fkey" FOREIGN KEY ("facid") REFERENCES "cd"."facilities" ("facid") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "bookings_memid_fkey" FOREIGN KEY ("memid") REFERENCES "cd"."members" ("memid") ON UPDATE NO ACTION ON DELETE NO ACTION); # cat migrations/atlas.sum h1:TBIl3WYjzvTYokioT+K3FOOlq9LSAzryhjxw5gAGz6U= 20230712165510_create_tables.sql h1:BWXssdgcYxJcYSNOaK7bYlhgB5LsumtrNXiBfkC3HiU= 

Теперь с помощью create_indexes создаем вторую миграцию, уже с индексами:

# atlas migrate diff create_indexes --dir "file://migrations" --to "postgresql://postgres:cheburek15@localhost:5433/postgres?sslmode=disable" --dev-url "docker://postgres/15/test"

Внутри видим следующее:

# cat migrations/20230712165923_create_indexes.sql -- Create index "bookings.facid_memid" to table: "bookings" CREATE INDEX "bookings.facid_memid" ON "cd"."bookings" ("facid", "memid"); -- Create index "bookings.facid_starttime" to table: "bookings" CREATE INDEX "bookings.facid_starttime" ON "cd"."bookings" ("facid", "starttime"); -- Create index "bookings.memid_facid" to table: "bookings" CREATE INDEX "bookings.memid_facid" ON "cd"."bookings" ("memid", "facid"); -- Create index "bookings.memid_starttime" to table: "bookings" CREATE INDEX "bookings.memid_starttime" ON "cd"."bookings" ("memid", "starttime"); -- Create index "bookings.starttime" to table: "bookings" CREATE INDEX "bookings.starttime" ON "cd"."bookings" ("starttime"); -- Create index "members.joindate" to table: "members" CREATE INDEX "members.joindate" ON "cd"."members" ("joindate"); -- Create index "members.recommendedby" to table: "members" CREATE INDEX "members.recommendedby" ON "cd"."members" ("recommendedby"); 

Что ж… Выглядит прекрасно!

А вот и вишенка на торте. Если попросить Atlas проверить последние две миграции из директории migrations на тестовой базе в Docker, то Atlas сообщит: «При создании индексов будут заблокированы таблицы в базе данных». Мы же не готовы с этим согласиться, не так ли?

# atlas migrate lint --dir file://migrations --dev-url "docker://postgres/15/test" --latest 2 20230712165923_create_indexes.sql: concurrent index violations detected:   L2: Creating index "bookings.facid_memid" non-concurrently causes write locks on the "bookings" table L4: Creating index "bookings.facid_starttime" non-concurrently causes write locks on the "bookings" table L6: Creating index "bookings.memid_facid" non-concurrently causes write locks on the "bookings" table L8: Creating index "bookings.memid_starttime" non-concurrently causes write locks on the "bookings" table L10: Creating index "bookings.starttime" non-concurrently causes write locks on the "bookings" table L12: Creating index "members.joindate" non-concurrently causes write locks on the "members" table L14: Creating index "members.recommendedby" non-concurrently causes write locks on the "members" table 

Поправим миграцию руками, добавив волшебное слово CONCURRENTLY:

-- Create index "bookings.facid_memid" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.facid_memid" ON "cd"."bookings" ("facid", "memid"); -- Create index "bookings.facid_starttime" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.facid_starttime" ON "cd"."bookings" ("facid", "starttime"); -- Create index "bookings.memid_facid" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.memid_facid" ON "cd"."bookings" ("memid", "facid"); -- Create index "bookings.memid_starttime" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.memid_starttime" ON "cd"."bookings" ("memid", "starttime"); -- Create index "bookings.starttime" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.starttime" ON "cd"."bookings" ("starttime"); -- Create index "members.joindate" to table: "members" CREATE INDEX CONCURRENTLY "members.joindate" ON "cd"."members" ("joindate"); -- Create index "members.recommendedby" to table: "members" CREATE INDEX CONCURRENTLY "members.recommendedby" ON "cd"."members" ("recommendedby"); 

Запускаем линтер снова:

# atlas migrate lint --dir file://migrations --dev-url "docker://postgres/15/test" --latest 2 atlas.sum: checksum mismatch 

Всё как у взрослых. Пытаемся исправить проблему:

# atlas migrate hash [team-alfa] root@demo-gitlab-runner ~ # atlas migrate lint --dir file://migrations --dev-url "docker://postgres/15/test" --latest 2 20230712165923_create_indexes.sql: concurrent index violations detected:  L2: Creating index "bookings.facid_memid" non-concurrently causes write locks on the "bookings" table L4: Creating index "bookings.facid_starttime" non-concurrently causes write locks on the "bookings" table L6: Creating index "bookings.memid_facid" non-concurrently causes write locks on the "bookings" table L8: Creating index "bookings.memid_starttime" non-concurrently causes write locks on the "bookings" table L10: Creating index "bookings.starttime" non-concurrently causes write locks on the "bookings" table L12: Creating index "members.joindate" non-concurrently causes write locks on the "members" table L14: Creating index "members.recommendedby" non-concurrently causes write locks on the "members" table 

Странно… Линтер игнорирует CONCURRENTLY!

Чтобы разобраться, почему так происходит, мы создали issue. Ответ последовал в тот же день — авторы исправили найденный нами недочет.

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

Мы уверены в безупречности наших миграций. Применим их на первой базе, наконец-то указав первую миграцию как базовую (--baseline 20230712165510):

# atlas migrate apply --dir "file://migrations" -u "postgresql://postgres:cheburek15@localhost:5432/postgres?sslmode=disable" --baseline 20230712165510 Migrating to version 20230712165923 from 20230712165510 (1 migrations in total):    -- migrating version 20230712165923     -> CREATE INDEX CONCURRENTLY "bookings.facid_memid" ON "cd"."bookings" ("facid", "memid");     pq: CREATE INDEX CONCURRENTLY cannot run inside a transaction block    -------------------------   -- 13.541209ms   -- 0 migrations ok (1 with errors)   -- 0 sql statements ok (1 with errors) Error: sql/migrate: sql/migrate: execute: executing statement "CREATE INDEX CONCURRENTLY \"bookings.facid_memid\" ON \"cd\".\"bookings\" (\"facid\", \"memid\");" from version "20230712165923": pq: CREATE INDEX CONCURRENTLY cannot run inside a transaction block: sql/migrate: execute: write revision: pq: current transaction is aborted, commands ignored until end of transaction block 

Возникшая ошибка вполне логична. Исправление напрашивается само:

# atlas migrate apply --dir "file://migrations" -u "postgresql://postgres:cheburek15@localhost:5432/postgres?sslmode=disable" --baseline 20230712165510 --tx-mode none Migrating to version 20230712165923 from 20230712165510 (1 migrations in total):    -- migrating version 20230712165923     -> CREATE INDEX CONCURRENTLY "bookings.facid_memid" ON "cd"."bookings" ("facid", "memid");     -> CREATE INDEX CONCURRENTLY "bookings.facid_starttime" ON "cd"."bookings" ("facid", "starttime");     -> CREATE INDEX CONCURRENTLY "bookings.memid_facid" ON "cd"."bookings" ("memid", "facid");     -> CREATE INDEX CONCURRENTLY "bookings.memid_starttime" ON "cd"."bookings" ("memid", "starttime");     -> CREATE INDEX CONCURRENTLY "bookings.starttime" ON "cd"."bookings" ("starttime");     -> CREATE INDEX CONCURRENTLY "members.joindate" ON "cd"."members" ("joindate");     -> CREATE INDEX CONCURRENTLY "members.recommendedby" ON "cd"."members" ("recommendedby");   -- ok (140.526595ms)    -------------------------   -- 148.935351ms   -- 1 migrations   -- 7 sql statements 

С этого момента внутри базы существуют схема и табличка atlas_schema_revisions.atlas_schema_revisions, где будут сохраняться миграции:

20230712165923 | create_indexes |    2 |       7 |     7 | 2023-07-12 17:47:38.182729+00 |        3720354 |       |            | WQxGEQSGBr+QCZQB+/cRZ84Ei8Br2iJ9nbwC9/k2oW8= | ["h1:sro8dOuCifHn590KwRbuoLE1w3  8YVON6O2KtEA0pbMQ=", "h1:RlXV4ShxPexTzjxAJZTrOLohhHskcmEHg34yCE1UqAc=", "h1:3yLYUR5xmF7yOWqA01WQrXZDNSHz7iDMa2ln0ZE23EU=", "h1:ZykewJpaiOCFHTEQrJ5mwj1S4uxGjHpTZcb4lG+qTyc=", "h1:TL2rPzD9teKkY4RxGU//7PgInoonG8  1+UrPkB1ykRO0=", "h1:X1salktLooVmN73SG2G8dUjgrbOysmHah5bs3o2eahk=", "h1:+DMH0dQEBDmoGUVXJS0FS9TqXYK6Jd4UbqaMXBd40lQ="] | Atlas CLI v0.12.1-972da10-canary

Более подробно описывать функционал миграций в Atlas не будем. Отметим лишь, что основную часть потребностей разработчика (если вообще не все) инструмент закрывает — вплоть до импорта всех известных форматов утилит для миграции, а также позволяет создавать кастомные форматы.

Свобода никак не ограничивается. Можно использовать Gorm, Beego как провайдеров схем для Atlas. Можно написать свой провайдер с помощью Atlas SDK.

Что ж, настало время перейти на другой уровень.

Работа с Atlas в Kubernetes

Давайте соберём в K8s стенд из нескольких баз данных и накатим на них миграции с помощью Atlas Operator:

$ helm install atlas-operator oci://ghcr.io/ariga/charts/atlas-operator -n atlas-operator --create-namespace Pulled: ghcr.io/ariga/charts/atlas-operator:0.2.0 Digest: sha256:15c3d10510acf7ba7ba203a3d6c1ecf4a2d54549031eb74aef6452c33488a5e2 NAME: atlas-operator LAST DEPLOYED: Wed Jul 12 21:46:56 2023 NAMESPACE: atlas-operator STATUS: deployed REVISION: 1 TEST SUITE: None $ kubectl -n atlas-operator get po NAME                              READY   STATUS    RESTARTS   AGE atlas-operator-77f748578b-rxhbr   1/1     Running   0          38s

Запустим кучку баз с помощью CloudNativePG и Bitpoke MySQL из конфигурационного файла db.yaml:

apiVersion: v1…
apiVersion: v1 kind: Secret metadata:   namespace: databases   name: shop-secret type: kubernetes.io/basic-auth stringData:   username: shop   password: cheburek15 --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata:   name: psql1 spec:   instances: 1   imageName: ghcr.io/cloudnative-pg/postgresql:15   bootstrap:     initdb:       database: shop       owner: shop       secret:         name: shop-secret   storage:     size: 1Gi --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata:   name: psql2 spec:   instances: 1   imageName: ghcr.io/cloudnative-pg/postgresql:13   bootstrap:     initdb:       database: shop       owner: shop       secret:         name: shop-secret   storage:     size: 1Gi --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata:   name: psql3 spec:   instances: 1   imageName: ghcr.io/cloudnative-pg/postgresql:11   bootstrap:     initdb:       database: shop       owner: shop       secret:         name: shop-secret   storage:     size: 1Gi --- apiVersion: v1 kind: Secret metadata:   name: mysql-secret type: Opaque stringData:   # root password is required to be specified   ROOT_PASSWORD: groot_password   ## application credentials that will be created at cluster bootstrap   DATABASE: employees   # USER:   # PASSWORD: --- apiVersion: mysql.presslabs.org/v1alpha1 kind: MysqlCluster metadata:   name: db1 spec:   replicas: 1   secretName: mysql-secret   mysqlVersion: "8.0"   volumeSpec:     persistentVolumeClaim:       accessModes: ["ReadWriteOnce"]       resources:         requests:           storage: 1Gi --- apiVersion: mysql.presslabs.org/v1alpha1 kind: MysqlCluster metadata:   name: db2 spec:   replicas: 1   secretName: mysql-secret   mysqlVersion: "5.7"   volumeSpec:     persistentVolumeClaim:       accessModes: ["ReadWriteOnce"]       resources:         requests:           storage: 1Gi ---

Создадим ns и применим файл db.yaml:

$ kubectl create ns databases namespace/databases created $ kubectl -n databases apply -f db.yaml secret/shop-secret created cluster.postgresql.cnpg.io/psql1 created cluster.postgresql.cnpg.io/psql2 created cluster.postgresql.cnpg.io/psql3 created secret/mysql-secret created mysqlcluster.mysql.presslabs.org/mysql1 created mysqlcluster.mysql.presslabs.org/mysql2 created

Восемьдесят секунд  — и всё работает!

$ kubectl -n databases get po NAME          READY   STATUS    RESTARTS   AGE db1-mysql-0   4/4     Running   0          81s db2-mysql-0   4/4     Running   0          81s psql1-1       1/1     Running   0          60s psql2-1       1/1     Running   0          61s psql3-1       1/1     Running   0          60s

Приготовим две большие портянки, одна из них — для наших «мусь»:

apiVersion: v1…
--- apiVersion: v1 kind: Secret metadata:   name: mysql1-creds type: Opaque stringData:   url: "mysql://root:groot_password@db1-mysql.databases:3306/employees" --- apiVersion: v1 kind: Secret metadata:   name: mysql2-creds type: Opaque stringData:   url: "mysql://root:groot_password@db2-mysql.databases:3306/employees" --- apiVersion: v1 kind: ConfigMap metadata:   name: migrationdir-mysql data:   20230717152110_create_tables.sql: |     -- Create "employees" table     CREATE TABLE employees (emp_no int NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender enum('M','F') NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no)) CHARSET utf8mb4;     -- Create "departments" table     CREATE TABLE departments (dept_no char(4) NOT NULL, dept_name varchar(40) NOT NULL, PRIMARY KEY (dept_no), UNIQUE INDEX dept_name (dept_name)) CHARSET utf8mb4;     -- Create "dept_manager" table     CREATE TABLE dept_manager (emp_no int NOT NULL, dept_no char(4) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no, dept_no), INDEX dept_no (dept_no), CONSTRAINT dept_manager_ibfk_1 FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT dept_manager_ibfk_2 FOREIGN KEY (dept_no) REFERENCES departments (dept_no) ON UPDATE NO ACTION ON DELETE CASCADE) CHARSET utf8mb4;     -- Create "salaries" table     CREATE TABLE salaries (emp_no int NOT NULL, salary int NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no, from_date), CONSTRAINT salaries_ibfk_1 FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON UPDATE NO ACTION ON DELETE CASCADE) CHARSET utf8mb4;     -- Create "titles" table     CREATE TABLE titles (emp_no int NOT NULL, title varchar(50) NOT NULL, from_date date NOT NULL, to_date date NULL, PRIMARY KEY (emp_no, title, from_date), CONSTRAINT titles_ibfk_1 FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON UPDATE NO ACTION ON DELETE CASCADE) CHARSET utf8mb4;     -- Create "dept_emp" table     CREATE TABLE dept_emp (emp_no int NOT NULL, dept_no char(4) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no, dept_no), INDEX dept_no (dept_no), CONSTRAINT dept_emp_ibfk_1 FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT dept_emp_ibfk_2 FOREIGN KEY (dept_no) REFERENCES departments (dept_no) ON UPDATE NO ACTION ON DELETE CASCADE) CHARSET utf8mb4;   20230717152222_change_lengths.sql: |     -- Modify "departments" table     ALTER TABLE departments MODIFY COLUMN dept_name varchar(100) NOT NULL;     -- Modify "employees" table     ALTER TABLE employees MODIFY COLUMN first_name varchar(50) NOT NULL, MODIFY COLUMN last_name varchar(50) NOT NULL;   atlas.sum: |     h1:FfpuPZJBiN0hYk3K9vIDQfvpbN90jnDVXR53+8YtrBw=     20230717152110_create_tables.sql h1:CLp9rnUH4jHwWkpAiCScuDtUzYrXbSnJtxI8Mh8ntjQ=     20230717152222_change_lengths.sql h1:T0Hi658Y7ZsVhaGSD6mhIyFTgURYFpmEf83Yl9Nd4oA= --- apiVersion: db.atlasgo.io/v1alpha1 kind: AtlasMigration metadata:   name: atlasmigration-mysql1 spec:   urlFrom:     secretKeyRef:       key: url       name: mysql1-creds   dir:     configMapRef:       name: migrationdir-mysql --- apiVersion: db.atlasgo.io/v1alpha1 kind: AtlasMigration metadata:   name: atlasmigration-mysql2 spec:   urlFrom:     secretKeyRef:       key: url       name: mysql2-creds   dir:     configMapRef:       name: migrationdir-mysql 

Внутри у нас есть секреты с URL, которые укажут Atlas’у, куда идти и как (обязательно с указанием namespace, поскольку оператор живёт в другом пространстве имен), общий configmap с нашей схемой и её изменениями в виде миграций, а также custom resource AtlasMigration, который сведёт всё воедино и попросит оператор провести миграции наших баз данных.

Посмотрим, что получится. Применим наш YAML-файл:

$ kubectl -n databases apply -f migrations-mysql.yaml secret/mysql1-creds created secret/mysql2-creds created configmap/migrationdir-mysql created atlasmigration.db.atlasgo.io/atlasmigration-mysql1 created atlasmigration.db.atlasgo.io/atlasmigration-mysql2 created

Вывод команды get нельзя назвать очень информативным:

$ kubectl -n databases get atlasmigrations.db.atlasgo.io NAME                    AGE atlasmigration-mysql1   2m59s atlasmigration-mysql2   2m59s

Посмотрим описание базы данных. Хм… Видим, что Reason: Applied, однако для полной уверенности информации всё-таки не хватает:

$ kubectl -n databases describe…
$ kubectl -n databases describe atlasmigrations.db.atlasgo.io atlasmigration-mysql1 Name:         atlasmigration-mysql1 Namespace:    databases Labels:       <none> Annotations:  <none> API Version:  db.atlasgo.io/v1alpha1 Kind:         AtlasMigration Metadata:   Creation Timestamp:  2023-07-17T16:01:56Z   Generation:          1   Managed Fields:     API Version:  db.atlasgo.io/v1alpha1     Fields Type:  FieldsV1     fieldsV1:       f:metadata:         f:annotations:           .:           f:kubectl.kubernetes.io/last-applied-configuration:       f:spec:         .:         f:dir:           .:           f:configMapRef:         f:urlFrom:           .:           f:secretKeyRef:     Manager:      kubectl-client-side-apply     Operation:    Update     Time:         2023-07-17T16:01:56Z     API Version:  db.atlasgo.io/v1alpha1     Fields Type:  FieldsV1     fieldsV1:       f:status:         .:         f:conditions:         f:lastApplied:         f:lastAppliedVersion:         f:observed_hash:     Manager:         manager     Operation:       Update     Subresource:     status     Time:            2023-07-17T16:15:19Z   Resource Version:  53849536   UID:               43b829b4-c36d-4761-adff-aa304457c21a Spec:   Dir:     Config Map Ref:       Name:  migrationdir-mysql   URL From:     Secret Key Ref:       Key:   url       Name:  mysql1-creds Status:   Conditions:     Last Transition Time:  2023-07-17T16:15:18Z     Message:     Reason:                Applied     Status:                True     Type:                  Ready   Last Applied:            1689610518   Last Applied Version:    20230717152222   observed_hash:           c7911afe9a6f0918b5cdebfc41f85baf6d9bc08cd272aed87b25b3541be71a35 Events:                    <none>

Если же заглянуть в базу данных, то все сомнения развеиваются:

$ kubectl -n databases exec -i db1-mysql-0 -cmysql -- mysql -uroot -pgroot_password employees -e'show create table employees;' mysql: [Warning] Using a password on the command line interface can be insecure. TableCreate Table employeesCREATE TABLE `employees` (\n  `emp_no` int NOT NULL,\n  `birth_date` date NOT NULL,\n  `first_name` varchar(50) NOT NULL,\n  `last_name` varchar(50) NOT NULL,\n  `gender` enum('M','F') NOT NULL,\n  `hire_date` date NOT NULL,\n  PRIMARY KEY (`emp_no`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci $ kubectl -n databases exec -i db2-mysql-0 -cmysql -- mysql -uroot -pgroot_password employees -e'show create table employees;' mysql: [Warning] Using a password on the command line interface can be insecure. TableCreate Table employeesCREATE TABLE `employees` (\n  `emp_no` int(11) NOT NULL,\n  `birth_date` date NOT NULL,\n  `first_name` varchar(50) NOT NULL,\n  `last_name` varchar(50) NOT NULL,\n  `gender` enum('M','F') NOT NULL,\n  `hire_date` date NOT NULL,\n  PRIMARY KEY (`emp_no`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

С PostgreSQL аналогично:

apiVersion: v1…
--- apiVersion: v1 kind: Secret metadata:   name: psql1-creds type: Opaque stringData:   url: "postgres://shop:cheburek15@psql1-rw:5432/shop?sslmode=disable" --- apiVersion: v1 kind: Secret metadata:   name: psql2-creds type: Opaque stringData:   url: "postgres://shop:cheburek15@psql2-rw:5432/shop?sslmode=disable" --- apiVersion: v1 kind: Secret metadata:   name: psql3-creds type: Opaque stringData:   url: "postgres://shop:cheburek15@psql3-rw:5432/shop?sslmode=disable" --- apiVersion: v1 kind: ConfigMap metadata:   name: migrationdir-psql data:   20230712165510_create_tables.sql: |     -- Add new schema named "cd"     CREATE SCHEMA "cd";     -- Create "facilities" table     CREATE TABLE "cd"."facilities" ("facid" integer NOT NULL, "name" character varying(100) NOT NULL, "membercost" numeric NOT NULL, "guestcost" numeric NOT NULL, "initialoutlay" numeric NOT NULL, "monthlymaintenance" numeric NOT NULL, PRIMARY KEY ("facid"));     -- Create "members" table     CREATE TABLE "cd"."members" ("memid" integer NOT NULL, "surname" character varying(200) NOT NULL, "firstname" character varying(200) NOT NULL, "address" character varying(300) NOT NULL, "zipcode" integer NOT NULL, "telephone" character varying(20) NOT NULL, "recommendedby" integer NULL, "joindate" timestamp NOT NULL, PRIMARY KEY ("memid"), CONSTRAINT "members_recommendedby_fkey" FOREIGN KEY ("recommendedby") REFERENCES "cd"."members" ("memid") ON UPDATE NO ACTION ON DELETE SET NULL);     -- Create "bookings" table     CREATE TABLE "cd"."bookings" ("bookid" integer NOT NULL, "facid" integer NOT NULL, "memid" integer NOT NULL, "starttime" timestamp NOT NULL, "slots" integer NOT NULL, PRIMARY KEY ("bookid"), CONSTRAINT "bookings_facid_fkey" FOREIGN KEY ("facid") REFERENCES "cd"."facilities" ("facid") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "bookings_memid_fkey" FOREIGN KEY ("memid") REFERENCES "cd"."members" ("memid") ON UPDATE NO ACTION ON DELETE NO ACTION);     -- Create "employees" table   20230712165923_create_indexes.sql: |     -- Create index "bookings.facid_memid" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.facid_memid" ON "cd"."bookings" ("facid", "memid"); -- Create index "bookings.facid_starttime" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.facid_starttime" ON "cd"."bookings" ("facid", "starttime"); -- Create index "bookings.memid_facid" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.memid_facid" ON "cd"."bookings" ("memid", "facid"); -- Create index "bookings.memid_starttime" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.memid_starttime" ON "cd"."bookings" ("memid", "starttime"); -- Create index "bookings.starttime" to table: "bookings" CREATE INDEX CONCURRENTLY "bookings.starttime" ON "cd"."bookings" ("starttime"); -- Create index "members.joindate" to table: "members" CREATE INDEX CONCURRENTLY "members.joindate" ON "cd"."members" ("joindate"); -- Create index "members.recommendedby" to table: "members" CREATE INDEX CONCURRENTLY "members.recommendedby" ON "cd"."members" ("recommendedby");  ("recommendedby");   atlas.sum: |     h1:xTbw3AxHRH3YSW7dOGteMxvcigM3f/Y7pdkwwScmbGM=     20230712165510_create_tables.sql h1:BWXssdgcYxJcYSNOaK7bYlhgB5LsumtrNXiBfkC3HiU=     20230712165923_create_indexes.sql h1:WQxGEQSGBr+QCZQB+/cRZ84Ei8Br2iJ9nbwC9/k2oW8= --- apiVersion: db.atlasgo.io/v1alpha1 kind: AtlasMigration metadata:   name: atlasmigration-psql1 spec:   urlFrom:     secretKeyRef:       key: url       name: psql1-creds   dir:     configMapRef:       name: "migrationdir-psql" --- apiVersion: db.atlasgo.io/v1alpha1 kind: AtlasMigration metadata:   name: atlasmigration-psql2 spec:   urlFrom:     secretKeyRef:       key: url       name: psql2-creds   dir:     configMapRef:       name: "migrationdir-psql" --- apiVersion: db.atlasgo.io/v1alpha1 kind: AtlasMigration metadata:   name: atlasmigration-psql3 spec:   urlFrom:     secretKeyRef:       key: url       name: psql3-creds   dir:     configMapRef:       name: "migrationdir-psql" 

На этом пути нас ждал один курьёзный Pull Request — в ссылке на issue tracker всего проекта мы обнаружили опечатку, пришлось поправить и её. Это, конечно же, никак не помешало создать новый issue о нашей неудавшейся миграции, когда свойство CONCURRENTLY приводит к ошибке при создании индекса:

Status:   Conditions:     Last Transition Time:  2023-07-17T17:18:37Z     Message:               sql/migrate: sql/migrate: execute: executing statement "CREATE INDEX CONCURRENTLY \"bookings.facid_memid\" ON \"cd\".\"bookings\" (\"facid\", \"memid\");" from version "20230712165923": pq: CREATE INDEX CONCURRENTLY cannot run inside a transaction block: sql/migrate: execute: write revision: pq: current transaction is aborted, commands ignored until end of transaction block     Reason:                Reconciling     Status:                False     Type:                  Ready   Last Applied:            0

С помощью разработчиков мы поправили наши миграции до исправного состояния. Из документации было не совсем очевидно, что именно нужно сделать. Но мы разобрались и стало понятнее:

A file directive should be separated by two newlines from the first statement’s comment.

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

Заключение

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

  • что именно находится в миграциях DDL;

  • какой результат необходим в состояниях баз данных;

  • каковы ожидания от инструмента миграции и оператора, который им управляет. 

Контейнеризованная инфраструктура, которую используют разные группы разработчиков с несколькими (и даже вполне многочисленными) контурами-окружениями, — очень интересный и живой кейс.

Научившись совместно использовать Atlas, можно с максимальной воспроизводимостью и прозрачностью для всех участников процесса — а, главное, безопасно! — тиражировать изменения не только кода, но и DDL: от локального окружения разработчика до production. Безопасность могут обеспечить как сам инструмент (например, с помощью вычисления контрольных сумм миграций), так и проверка миграций. Нет сомнений, что функционал будет развиваться и дальше.

P. S.

Читайте также в нашем блоге:


ссылка на оригинал статьи https://habr.com/ru/articles/753122/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *