О дивный мир, 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 позволяет работать двумя способами:
использование декларативного описания схемы базы данных (как в 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
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.
Что ж, настало время перейти на другой уровень.
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.
Table Create Table
employees CREATE 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.
Table Create Table
employees CREATE 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. Безопасность могут обеспечить как сам инструмент (например, с помощью вычисления контрольных сумм миграций), так и проверка миграций. Нет сомнений, что функционал будет развиваться и дальше.
Atlas и его оператор справляются не только с обычными MySQL и PostgreSQL, но и с MariaDB, TiDB и CockroachDB. (В рамках одной статьи протестировать их все невозможно.)
Atlas неплохо поддерживает 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 как платформы, реализующей основные преимущества сервис-ориентированной архитектуры (SOA) и при этом минимизирующей типичные издержки, связанные с владением сервисом, можно прочитать в нашей статье «Atlas: Путешествие Dropbox от монолита на Python до managed-платформы».
P. S.
Читайте также в нашем блоге:
Больше разработчиков должны знать это о базах данных
Представляем Kubernetes-платформу Deckhouse. Теперь в Open Source и для всех
werf — наш инструмент для CI/CD в Kubernetes (обзор и видео доклада)
Устанавливаем Kubernetes-платформу Deckhouse в закрытом окружении. Пошаговая инструкция
werf vs Docker. Чем лучше собирать образы
Как настроить миграцию etcd между облачными кластерами Kubernetes и избежать простоев
Беспростойная миграция MongoDB в Kubernetes