Бэкапим Кроличьи мозги на случай ядерных войн
Не волнуйтесь за них, мы позаботились об их бэкапе
Когда-нибудь в твоей стране запретят IaC и ты вспомнишь про мои бэкапы…
© Джейсон Стетхем
Не так давно мы в компании столкнулись с маленькой проблемкой — RabbitMQ (далее просто кроль и тп) на дев кластере упал, мы его оживили, а за definitions.json для восстановления юзеров, очередей и тд. пришлось бегать к разработчику, который по чистой случайности эти файлики часто снимал. Это был первый звоночек.
Вторым звоночком стал DR (Disaster Recovery) — сценарий/упражнение по экстренному поднятию нашего продукта в облаке в случае взрыва и уничтожения нашего физического дата центра. Тут надобность в бэкапах нашего кролика стала очевидной и мы занялись решением этой проблемы
Кратко о том, какие способы управления конфигурацией (создание пользователей, очередей и тд) RabbitMQ имеют место быть:
Руками, мануально: Не наш выбор, конечно же (если только редко и на тестовой среде)
Из приложения: Наши некоторые приложения имеют права на создание и работу с очередями
Ansible: Очень хороший вариант, есть возможность автоматизировать некоторые моменты, восстанавливать конфигурацию. IaC.
StatefulSet в k8s
topology operator k8s: Больше Кубера богам Кубера
Рассматривая наш случай, на тестовой среде у нас кролик находится в кластере k8s как StatefulSet. Для тестов достаточно — поднимается, убивается быстро — все довольны.
Кролики же на стейджинге и проде установлены на машинах и конфигурируются Ansible, что позволяет отслеживать актуальные настройки в коде, быстро применять их простой командой и имеется возможность отката, ибо хранится ансибл в git репозитории.
Возможности конфигурирования разобрали и, обрисовав конкретно наш случай, который вполне себе production ready, приступим к решению проблемы бэкапов. Начнем с хранилища.
Куда класть добро?
Кибер-кроли усердно думают
Наша железка конечно же не вариант — мы уже условились, что это задача к DR, следовательно, железка сгорела, запишите в бух учёт. Куда же класть json? В облако. Почему?
Облако тех же Google-давно проверенная на высоких нагрузках и распределенная система, вероятность сгорания в ней наших конфигов «крайне мала!»
Разобравшись с местом хранения настроек кролика можем приступить к приготовлениям всего нужного окружения и инструментов под наш бэкап
В начале было слово, и слово было Terraform
Нам нужен бакет и доступ к нему (сервис-аккаунт). Terraform позволяет поднять такое в три файла (для красоты, можно впихивать и в один) и деплоить/убивать одной командой в терминале. Выбор инструмента очевиден, давайте без лишних слов всё готовить:
-/* Ресурс нашего бакета, в котором будем хранить бэкапы*/
resource "google_storage_bucket" "configs_backup" {
project = var.project
name = "configs-backup"
location = "EUROPE-WEST4"
storage_class = "Standard"
labels = {}
uniform_bucket_level_access = false
force_destroy = true
versioning {
enabled = true /* Бэкапы будут каждый день,
так что включаем версии для более тонкого восстановления */
}
lifecycle_rule {
condition {
num_newer_versions = 5
}
action {
type = "Delete"
}
}
}
Хранилка готова, но в нее же и доступ нужен нам. Идем дальше и создаем сервис аккаунт с нужными правами
/* Сам сервис аккаунт */
resource "google_service_account" "configs_backup_sa" {
account_id = "configs-backup-sa"
display_name = "Created by terraform configs-backup for control in configs_backup bucket"
project = "${var.project}"
}
/* Выдаем ему права на бакет */
resource "google_storage_bucket_iam_member" "configs_backup_sa" {
bucket = "${google_storage_bucket.configs_backup.name}"
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.configs_backup_sa.email}"
}
И переменная с проектом, чисто для красоты и возможного использования внутри ваших иных Terraform модулей и тп:
variable "project" {
description = "Google Project to create resources in"
type = string
default = ""
}
Для проверки инициализируем стейт и запускаем plan, который выведет нам то, что собирается сделать на основе этого кода:
terraform init
terraform plan
Вывод консоли должен создавать три объекта: bucket, SA и IAM Member (Пример вывода есть в прикрепленном в конце репозитории). Если сходится — запускаем адскую машину IaC и деплоим объекты:
terraform apply
Готово — вы очаровательны!
А что дальше?
Naxt?
Хранилище готово, доступ имеем, что дальше? Нужен исполнитель бэкапа, для которого мы всю эту красоту и поднимали. Какие требования к нему мы выдвигаем:
Должен быть изолирован от окружения хоста, его зависимостей и тд , для снижения вероятности вмешивания в работу и подделку данных и тд.
Список не так уж и огромен, но в то же время, довольно действенно сокращает нам количество вариантов. Человек знающий в этот момент начинает потихоньку понимать к чему я веду и тихо ненавидит очередного любителя известного всем инструмента.
Кого же мы выберем на роль этого важного винтика системы, прикрывающего нас от потери настроек кроля?…
Благими намерениями вымощена дорога в Kubernetes © Жак Фреско
CronJob, my k8s dudes
Ква?
Почему же мы опять затаскиваем наши решения в K8s:
Чтобы всех раздражать
Контейнеры/поды, которыми управляет K8s, изолированы
Можно выбрать образ только с нужными инструментами и не нагружать их зависимостями нашу инфру
CronJob в кубере оставляет трейсы в виде логов подов, что может помочь в дебаге возможных ошибок и ко всему этому — объекты кубернетес спокойно мониторятся и могут быть дополнены алертами, приходящими нам прямиком в Slack
В моем случае наш продукт преимущественно в кластерах, включая CronJob«ы и не хочется усложнять систему чем то еще
После того, как мы определились с нашим бэкапером, нужно понять, что ему потребуется для выполнения всех работ
Нам нужен доступ в бакет, чтобы кидать туда бэкапы , и доступ к RabbitMQ:
В случае доступа к бакету мы уже условились использовать SA, то есть в наш под нужно прокинуть token для логина через gcloud. Это мы сделаем через Volume и VolumeMounts секрета, в который из Vault прилетает этот самый токен
volumeMounts:
- mountPath: /var/secrets/google
name: google-cloud-key
volumes:
- name: google-cloud-key
secret:
secretName: configbackuper-gcp-sa
Для доступа в RabbitMQ мы не будем мудрить и создадим пользователей в наших кластерах с нужными для скачивания конфигурации правами.
Примечание: в примере использован наш экземпляр бэкапера, поэтому юзера два — для кролика внутренних сервисов и кролика клиентов
envFrom:
- secretRef:
name: job-rabbitconfigbackuper-rabbit-secret
С доступом разобрались (не забываем, конечно, что файрвол должен пускать кластер кубера на хосты кролика). Теперь нам нужны инструменты для бэкапа. Всё будет максимально просто — curl для получения definitions.json кроликов, gcloud для авторизации в GCP и gsutil для копирования бэкапа в наш бакет, где он будет лежать на случай DR или пришествия Ктулху.
На глаза сразу попадается образ от Google — cloud-sdk. Но не спешите брать именно его! После запуска и пула я увидел страшную цифру — 3 ГБ! Это явно не похоже на утилиту для чисто бэкапа. Можно подумать и собрать образ самому, но для быстрого решения мы немного покопали и заметили тот же официальный образ, но на базе Alpine. Результат явно лучше — 900 МБ. Остановимся на этом, но при желание образ можно собрать самому.
Алгоритм кроны выглядит так:
Устанавливаем set –e, чтобы ошибки роняли крону и не кидали в бакет старые файлы или некий мусор.
Curl«ом достаем definitions.json с хостов и сохраняем в локальные файлы.
Логинимся в проект с помощью gcloud.
Кидаем бэкап файлы в bucket с помощью gsutil.
На этом моменте можем приступать уже к написанию самого yaml«ика, части которого я показывал выше:
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: job-rabbitconfigbackuper
labels:
helm.sh/chart: job-rabbitconfigbackuper-0.1.0
app.kubernetes.io/name: job-rabbitconfigbackuper
app.kubernetes.io/instance: release-name
app: job-rabbitconfigbackuper
project: habr
type: job
app.kubernetes.io/version: "latest"
app.kubernetes.io/managed-by: Helm
spec:
schedule: "@daily"
concurrencyPolicy: Forbid
failedJobsHistoryLimit: 5
successfulJobsHistoryLimit: 3
startingDeadlineSeconds: 180
jobTemplate:
spec:
backoffLimit: 0
template:
metadata:
labels:
helm.sh/chart: job-rabbitconfigbackuper-0.1.0
app.kubernetes.io/name: job-rabbitconfigbackuper
app.kubernetes.io/instance: release-name
app: job-rabbitconfigbackuper
project: habr
type: job
app.kubernetes.io/version: "latest"
app.kubernetes.io/managed-by: Helm
spec:
serviceAccountName: common-sa
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
restartPolicy: OnFailure
containers:
- name: job-rabbitconfigbackuper
image: "google/cloud-sdk:alpine"
command:
- /bin/sh
args:
- -c
- set -e;
curl http://$Rabbit__Host:$Rabbit__Port$Rabbit__JSON__Query$(echo -n $RabbitMQ__Creds | base64 -w 0) > /tmp/$Environment.json;
curl http://$Rabbit__External__Host:$Rabbit__Port$Rabbit__JSON__Query__External$(echo -n $RabbitMQ__Creds__External | base64 -w 0) > /tmp/$Environment\_external.json;
yes | gcloud auth login --cred-file=$GOOGLE_APPLICATION_CREDENTIALS;
gsutil cp /tmp/$Environment.json gs://configs-backup/rabbitmq/$Environment.json;
gsutil cp /tmp/$Environment\_external.json gs://configs-backup/rabbitmq/$Environment\_external.json;
env:
- name: "Environment"
value: "stage"
- name: "GOOGLE_APPLICATION_CREDENTIALS"
value: "/var/secrets/google/key.json"
- name: "Rabbit__External__Host"
value: ""
- name: "Rabbit__Host"
value: ""
- name: "Rabbit__JSON__Query"
value: "/api/definitions?download=rabbit_.json&auth="
- name: "Rabbit__JSON__Query__External"
value: "/api/definitions?download=rabbit_.json&auth="
- name: "Rabbit__Port"
value: "15672"
envFrom:
- secretRef:
name: job-rabbitconfigbackuper-rabbit-secret
volumeMounts:
- mountPath: /var/secrets/google
name: google-cloud-key
volumes:
- name: google-cloud-key
secret:
secretName: configbackuper-gcp-sa
Энкод в base64 обусловлен тем, что хост кролика через curl принимает авторизацию только так. И этот пунктик был особенно болезненым, потому что секреты в кубере изначально также хранятся в base64 и вышла ситуация, когда хост кролика кодил одним образом, кубер другим, а я на локальной машине дебажил это третьим.
Следите за руками!
Наш логин пароль прилетает из секрета кубера, который в закодированном виде выглядит вот так:
Загружаясь в под он декодируется, получаем наши исходные логин пароль пользователя кролика:
А теперь нам надо отправить его в кролик для авторизации в задикодированном виде, под base64. Вводим заветную команду и…. Что?!!!
Это явно не похоже на оригинальный код из секрета и тот же токен авторизации из запроса загрузки. Может мы что-то не понимаем? Давайте раскодируем полученный код:
Это точно не наш пароль. Что же делать?
$(echo -n $RabbitMQ__Creds | base64 -w 0 )
В нашем случае эта строчка всё починила, но отличие кодировок в отличие от параметров и тех же кавычек, которые также влияют на декодирование и кодирование, может в начале запутать и сломать авторизацию
P.S В обычном ручном энкодинге/декодинге это проблема чинится простыми кавычками, но в случае нашего бэкапера мы оперируем переменными окружения и секретами, так что имеем что имеем.
Кроме этого нужно было иметь в виду то, что оперировать файлами надо в папке вроде /tmp/, иначе под падал с ошибкой из-за отсутствия прав на другие директории (security всё же).
Деплоим нашу cronJob в кластер и вуаля! — Mission complete, Boss!
Наши файлики лежат в бакете, имеют версии, и в случае падения кролей мы просто деплоим заново их и импортим туда эти файлы (что при желании можно также делать автоматом).
Итоги и хэппиэнд
Кролики в безопасности, все счастливы. Не одним IaC едины, бэкапьте всё что можно, когда-нибудь это вам поможет, а коллеги будут смотреть на вас как на боженьку инфраструктуры.
Надеюсь, если вы прочитали статью до конца, то она оказалась для вас полезна или как минимум интересна. Всем добра и автоматизации!
P.S Мы используем Helm для деплоя в кубер, но шаблоны самописные, так что в статье используется raw yaml, сгенерированный helm template.
P.P. S Моя первая статья на этом ресурсе и первая статья в принципе, буду рад любому адекватному фидбеку
А вот ссылка на гит с исходниками и небольшим readme по разворачиванию