Як я видалив базу на продакшені, а бекапів не було. Best practices як робити «Cloud native» бекапи (на прикладі Azure + Terraform)
Дисклеймер
УВАГА! Всі персонажі та описані події є вигаданими. Будь-який збіг з реальними людьми чи подіями є випадковістю. Усі дії виконувались «професіоналами», тож не намагайтесь повторити їх у реальному житті.
Ця стаття складатиметься з двох частин: перша про те, як краще робити «Cloud native» бекапи, а друга — «вигадана» історія, як мій «товариш» видалив базу даних на продакшені, а бекапу не було. Тож приємного перегляду...

Дані — це найбільша цінність, що існуватиме довше, ніж сама система. Дані — найважливіший актив компанії
Перш за все слід розуміти різницю між бекапами та Disaster Recovery планом.
Disaster Recovery план передбачає повне відновлення всієї інфраструктури (hardware, software, networking, data) в разі масштабних колапсів (вихід з ладу датацентра, регіону, проблеми з мережею, хакерські атаки, повені, пожежі тощо). DR дублює всю вашу основну інфраструктуру в інші регіони, передбачає її обслуговування, перевірку на доступність та має чітку послідовність дій для переключення трафіку в разі аварій. Головною метою DR є зменшення даунтайму та якомога скоріше відновлення всієї системи.
Бекапи ж натомість є лише копією даних з подальшою можливістю їх відновлення.
В цій статті мова йтиме саме про бекапи (адже Disaster Recovery є значно ширшим поняттям, яке охоплює не лише бази даних, і для нього однієї статті буде замало).
Насамперед сформулюємо найважливіші критерії, за якими оцінюється ефективність бекапу:
- швидкість виконання;
- зручність його запуску та обслуговування;
- кількість грошей, яку ми витрачаємо на зберігання даних;
- захищеність місця, де ми зберігаємо бекап;
- рівень доступності в разі аварій.
Пропоную почати з останніх двох пунктів. Адже, як зазначено в книзі SRE від Google: «Неважливо, наскільки корисна твоя система, якщо ніхто не може нею користуватись».
Дійсно, який сенс від бекапу, якщо він зберігається в тому ж датацентрі, де ваша база «вмерла» в результаті торнадо? Чи допоможе вам блискавична швидкість резервного копіювання, якщо зловмисник видалить його разом з базою, використовуючи ті ж самі доступи, що і для бази?
Тож перша порада — зберігайте бекапи в іншому датацентрі/регіоні (територіально) та надавайте права до них за принципом «zero trust». Ми можемо дозволити адмінам працювати з ними, але заборонити зміну чи видалення (Write Once Read Many). Можемо використовувати двухфакторку, енкриптити бекапи (що в клаудах є опцією ‘out-of-the-box’), зберігати їх в ізольованій мережі, упевнившись, що доступ до них є лише через певні «канали» (наприклад, через VPN та Private Endpoint або через використання РІМ з підтвердженням запиту на доступ від іншого члена команди)...
Що ж до швидкості резервного копіювання, зручності його обслуговування, а також зменшення коштів на його зберігання, немає нічого кращого, на мою думку, ніж вже готові та заздалегідь оптимізовані рішення від популярних клаудів.
В Azure, до прикладу, для «ізоляції» бекапу найкраще підійде окремий Subscription з налаштованим RBAC (інкапсуляція від іншої інфраструктури на рівні доступів) та додаткова копія бекапу в регіон, що відмінний від регіону продакшн-бази. Слід зазначити, що для Azure Backup Vault доступна також реплікація бекапів поміж Availability-зонами всередині одного регіону, а не лише між Geo-регіонами.
Стоп, а що таке цей Backup Vault?
Azure Backup Vault — це тип сховища в Azure для автоматизації та зберігання бекапів. Наразі він підтримує резервне копіювання таких ресурсів, як Azure Database for PostgreSQL flexible server, Azure Database for MySQL flexible server(preview), Azure Disks, Azure Blobs, та Kubernetes services (preview).
Але за бажанням можна спочатку «перегнати» дані за допомогою Azure Data Factory з інших ресурсів (наприклад, з CosmosDB) в Azure Blobs, а потім так само бекапити їх в Backup Vault (що додасть, звісно, трохи складності у випадку з CosmosDB). Та натомість ми отримуємо зрозумілий, зручний, і нативний для Azure механізм з реалізації резервного копіювання — тобто задовольняємо перші два пункти зі списку вище. Отже, з інкапсуляцією, сусуріті, швидкістю та зручністю розібрались. А як щодо костів?
Враховуючи, що в будь-якому разі нам необхідна буде якась VM (чи інший workload) для виконання pg_dump/pg_restore, а також місце (storage), де ми зберігатимемо дампи..., зменшення часу на обслуговування цих речей за рахунок використання Azure Backup Vault (чи його аналогів в інших клаудах) автоматично зменшує і кости.
Та ми можемо зменшити їх ще більше! Ні для кого не секрет, що чим більше даних зберігаєш — тим більше платиш. Тож нашою ціллю буде зменшити кількість бекапів, при цьому не зменшуючи проміжок часу, який вони покривають. Часто знадобиться безліч резервних копій за певний проміжок часу, щоб відповідати вимогам різних регуляторок по типу GDPR/PSIDSS, та щоб мати змогу «рекурсивно» розбирати інциденти, пов’язані з даними, знайти стан бази, коли ці проблеми почались.
Для таких цілей ми будемо користуватись бекап rotation схемою «Grandfather-Father-Son». До прикладу, якщо вам необхідно мати бекапи за весь попередній рік, не обов’язково зберігати всі 365 бекапів. Достатньо всього 17 (в моєму варіанті). Як це працює:
- Спочатку ми бекапимо нашу базу кожного дня протягом тижня (загалом 7 бекапів).
- Через тиждень ми «перезаписуємо» бекапи, вік яких перевищує 7 днів, але продовжуємо зберігати «weekly» бекапи (резервні копії за понеділок) протягом останнього місяця. Тобто з приходом вівторка бекап за попередній вівторок видаляється і замінюється на новий. При цьому до наших 7 бекапів за останній тиждень додається ще 3 бекапи, зроблених по понеділкам попередніх тижнів місяця.
- Через місяць ми «перезаписуємо» ці «weekly» бекапи новими (якщо такий бекап старіший 30 днів), зберігаючи при цьому лише бекап понеділка першого тижня кожного місяця («monthly» backup) протягом останніх пів року. Це ще плюс 5 бекапів, на додачу до 10 з попередніх степів.
- Після старіння «monthly» бекапів більш ніж на 6 місяців ми видаляємо їх — крім тих, які є «quarterly» бекапами (що були зроблені в перший понеділок першого місяця в кварталі). Таким чином ми зберігаємо ще плюс 2 бекапи.
- Ну і наостанок, через рік ми остаточно видаляємо «quarterly» бекапи — окрім того, що був зроблений в перший понеділок першого місяця першого кварталу за минулий рік.
Таким чином ми маємо ~17 бекапів під кінець року, які покривають стан нашої бази за останні 365 днів, але при цьому не коштують всіх грошей світу (і їх ми кладемо в Azure Backup Vault з Geo-Redundancy фічею).
А ось приклад як такий Backup Vault з «GeoRedundant» виглядатиме в Terraform-коді:
./root_module/vars_values.tfvars (add alias «source_data_subscription» in providers.tf first ).
backup_subscription_id = "h47fq8f0-648f-4fj6-9jg7-85ht0cc75h77"
source_data_subscription_id = "24759666-4yr5-4586-976h-hfy56c3fjg68"
environment = "back"
# Source MSQL servers to backup and their resource groups where they are located
source_mysql_servers = {
"mysql-projectname-eus2-prod" = "data-projectname-prod",
"mysql-projectname-eus2-dev" = "data-projectname-dev",
}
./root_module/variables.tf
variable "backup_subscription_id" {
type = string
}
variable "source_data_subscription_id" {
type = string
}
variable "environment" {
type = string
}
variable "source_mysql_servers" {
description = "A map of MYSQl server names and their resource groups to backup."
type = map(string)
}
./root_module/data.tf
data "azurerm_mysql_flexible_server" "mysql" {
for_each = var.source_mysql_servers
provider = azurerm.source_data_subscription
name = "${each.key}"
resource_group_name = "${each.value}"
}
data "azurerm_resource_group" "mysql" {
for_each = var.source_mysql_servers
provider = azurerm.source_data_subscription
name = "${each.value}"
}
./root_module/main.tf
locals {
source_mysql_servers = {
for name, server in data.azurerm_mysql_flexible_server.mysql :
name => {
id = server.id
server_rg_id = data.azurerm_resource_group.mysql[name].id
}
}
}
module "resource_group" {
source = "../modules/resource_group"
environment = var.environment
}
module "backup_vault" {
source = "../modules/backup_vault"
environment = var.environment
resource_group_name = module.resource_group.resource_group_name
source_mysql_servers = local.source_mysql_servers
}
./modules/backup_vault/variables.tf
variable "environment" {
type = string
}
variable "project" {
type = string
default = "projectname"
}
variable "location" {
type = string
default = "East US 2"
}
variable "short_location" {
type = string
default = "eus2"
}
variable "resource_group_name" {
type = string
}
variable "source_mysql_servers" {
description = "A map of MySQL server names and their resource groups for backups."
type = map(object({
server_rg_id = string
id = string
}))
}
./modules/backup_vault/azure_backup_vault.tf
resource "azurerm_data_protection_backup_vault" "backup" {
name = "bvault-${var.project}-${var.short_location}-${var.environment}"
resource_group_name = var.resource_group_name
location = var.location
datastore_type = "VaultStore"
redundancy = "GeoRedundant"
identity {
type = "SystemAssigned"
}
tags = {
environment = var.environment
}
}
# These roles are needed to grant permissions for Backup Vault to retrieve data from the source MySQL Flexible Server
resource "azurerm_role_assignment" "vault_to_mysql_server_rg" {
for_each = var.source_mysql_servers
scope = each.value.server_rg_id
role_definition_name = "Reader"
principal_id = azurerm_data_protection_backup_vault.backup.identity.0.principal_id
}
resource "azurerm_role_assignment" "vault_to_mysql_server" {
for_each = var.source_mysql_servers
scope = each.value.id
role_definition_name = "MySQL Backup And Export Operator"
principal_id = azurerm_data_protection_backup_vault.backup.identity.0.principal_id
}
resource "azurerm_data_protection_backup_policy_mysql_flexible_server" "mysql" {
for_each = var.source_mysql_servers
name = "bkpol-${each.key}"
vault_id = azurerm_data_protection_backup_vault.backup.id
backup_repeating_time_intervals = ["R/2026-01-05T07:00:00+00:00/P1D"] # Backup every 24 hours (once daily)
#Retention rule for daily backups (store for 7 days)
default_retention_rule {
life_cycle {
duration = "P7D"
data_store_type = "VaultStore"
}
}
# Retention rule for weekend backups (store for 1 month)
retention_rule {
name = "weekend-backup-rule"
priority = 4
criteria {
absolute_criteria = "FirstOfWeek"
}
life_cycle {
data_store_type = "VaultStore"
duration = "P1M"
}
}
# Retention rule for monthly backups (store for 6 months)
retention_rule {
name = "monthly-backup-rule"
priority = 3
criteria {
absolute_criteria = "FirstOfMonth"
}
life_cycle {
data_store_type = "VaultStore"
duration = "P6M"
}
}
# Retention rule for quarterly backups (store for 1 year)
retention_rule {
name = "quarterly-backup-rule"
priority = 2
criteria {
months_of_year = ["January", "April", "July", "October"]
weeks_of_month = ["First"]
days_of_week = ["Monday"]
}
life_cycle {
data_store_type = "VaultStore"
duration = "P1Y"
}
}
# Retention rule for yearly backups (store for 3 years)
retention_rule {
name = "yearly-backup-rule"
priority = 1
criteria {
absolute_criteria = "FirstOfYear"
}
life_cycle {
data_store_type = "VaultStore"
duration = "P3Y"
}
}
depends_on = [
azurerm_role_assignment.vault_to_mysql_server_rg,
azurerm_role_assignment.vault_to_mysql_server,
]
}
resource "azurerm_data_protection_backup_instance_mysql_flexible_server" "mysql" {
for_each = var.source_mysql_servers
name = "${each.key}"
location = var.location
vault_id = azurerm_data_protection_backup_vault.backup.id
server_id = each.value.id
backup_policy_id = azurerm_data_protection_backup_policy_mysql_flexible_server.mysql[each.key].id
}
Звісно, просто робити бекапи — замало! Потрібно постійно тестувати їх, оновлювати runbook, по якому ви будете реанімувати свою базу в разі чого, напрацьовувати навички... Адже наявність резервної копії ще не означає, що ви можете ефективно і швидко її використати за потребою.
Може виникнути питання, навіщо робити ці бекапи, якщо такі ресурси, як Azure MySQL Flexible server вже «out-of-the-box» мають можливість «point-in-time» відновлення до того стану, якими вони були в будь-який момент часу раніше.
Відповідь: це не задовольняє вимогу мати бекапи в окремому захищеному місці з обмеженим доступом на випадок того, що ваші дані будуть скомпроментовані та пошкоджені. Крім того, це не вберігає від видалення всього серверу.
І от саме про такий кейс я і хочу вам розповісти.

Що робити, якщо випадково видалив базу даних на продакшені в Azure, а бекапів немає?
Насправді причин випадкового видалення бази може бути безліч. Починаючи з банального «переплутав, не те клацнув» та закінчуючи новим Terraform-кодом, який «forces replacement» ресурс через зміну значення immutable проперті.
І якщо від останнього кейсу вас може вберегти continuous-delivery реліз-стратегія (а також уважність при перегляді Terraform-плану) чи банальні terraform tests, то у випадку з «переплутав, не те клацнув» вельми маловірогідно, що блокування ресурсу за допомогою «management lock» зупинить максимально рішучу людину з девізом «Fail fast — recovery fast!»
І це ми не беремо до уваги вірогідність того, що така людина виявиться хакером з намірами видалити АБСОЛЮТНО ВСЕ.
Отож, баз немає, реплік немає, бекапів немає, то що ж тепер робити (в контексті Azure)?
Першим логічним рішенням буде спроба відновити нашу базу за допомогою «Restore Blade» в порталі Azure.

Але цей «Restore Blade» доступний лише для баз даних, які мають «Continuous» backup стратегію (тобто реплікуються системою Azure ‘неперервно‘ та можуть бути відновлені до будь-якого ‘point-in-time‘ снепшоту їхнього життя протягом останніх

Тож нам нічого не залишається робити, окрім як завести технічний тікет на відновлення бази до того моменту, коли був зроблений останній «Periodic» backup. На розгляд такого тікету може піти близько години часу, а ще слід врахувати той час, який піде на відновлення самих даних.
І навіть в цьому випадку може виникнути інша проблема (що якраз виникла в мого «товариша») — ви можете не мати відповідного «Support plan», що дає вам право на створення технічних тікетів. Тож ви зіткнетесь з таким обмеженням, довго чекатимете отримання «Owner» ролі задля можливості підвищення цього «Support plan», а потім виявиться, що ваша компанія/продукт «входить» в іншу компанію/групу_компаній, які «об’єднались» задля заключення спільного Service Agreement із Azure для подальшої економії коштів.
Якщо простими словами: ви не можете створити технічний тікет, тому що у вас немає відповідного «Support plan» для цього, але і «Support plan» ви змінити не можете, адже менеджер, відповідальний за спілкування по вашому Service Agreement, вже закінчив свій робочий день та не відповідає на ваші повідомлення... І що тепер?
Не знаю, як би ви вчинили в такій ситуації, але знаю, що робив би я. Тобто не я, а мій «товариш» з «вигаданої» історії.

Тож створювати технічні тікети ми не можемо... Але нам ніхто не забороняє створювати billing-тікети! І хоч на розгляд такого тікету йде до
А там вже діло ваше, як ви поясните, чому вам перед купівлею ресурсів треба відновити базу даних на продакшені. Наприклад: немає бази — немає бізнесу — немає грошей — ніхто не купить ресурсів на мільйон доларів :)
P.S: Головне не кажіть, що ресурси ніхто купувати не збирається....
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
15 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів