
О дивный мир, 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/
Добавить комментарий