Бэкапим Кроличьи мозги на случай ядерных войн

от автора

Не волнуйтесь за них, мы позаботились об их бэкапе

Не волнуйтесь за них, мы позаботились об их бэкапе

Когда-нибудь в твоей стране запретят IaC и ты вспомнишь про мои бэкапы…
© Джейсон Стетхем

Не так давно мы в компании столкнулись с маленькой проблемкой — RabbitMQ (далее просто кроль и тп) на дев кластере упал, мы его оживили, а за definitions.json для восстановления юзеров, очередей и тд. пришлось бегать к разработчику, который по чистой случайности эти файлики часто снимал. Это был первый звоночек.

Вторым звоночком стал DR (Disaster Recovery) — сценарий/упражнение по экстренному поднятию нашего продукта в облаке в случае взрыва и уничтожения нашего физического дата центра. Тут надобность в бэкапах нашего кролика стала очевидной и мы занялись решением этой проблемы

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

  • Руками, мануально: Не наш выбор, конечно же (если только редко и на тестовой среде)

  • Из приложения: Наши некоторые приложения имеют права на создание и работу с очередями

  • Ansible : Очень хороший вариант, есть возможность автоматизировать некоторые моменты, восстанавливать конфигурацию. IaC. 

  • StatefulSet в k8s

  • topology operator k8s : Больше Кубера богам Кубера

Рассматривая наш случай, на тестовой среде у нас кролик находится в кластере k8s как StatefulSet. Для тестов достаточно —  поднимается, убивается быстро — все довольны. 

Кролики же на стейджинге и проде установлены на машинах и конфигурируются Ansible, что позволяет отслеживать актуальные настройки в коде, быстро применять их простой командой и имеется возможность отката, ибо хранится ансибл в git репозитории.

Возможности конфигурирования разобрали и, обрисовав конкретно наш случай, который вполне себе production ready, приступим к решению проблемы бэкапов. Начнем с хранилища.

Куда класть добро?

Кибер-кроли усердно думают

Кибер-кроли усердно думают

Наша железка конечно же не вариант — мы уже условились, что это задача к DR, следовательно, железка сгорела, запишите в бух учёт. Куда же класть json? В облако. Почему?

  • Мы можем супер-мега-быстро поднять нужные для этого объекты в облаке с помощью старого доброго Terraform

  • Облако тех же Google-давно проверенная на высоких нагрузках и распределенная система, вероятность сгорания в ней наших конфигов «крайне мала!»

  • До облака можно наладить простой доступ для загрузки и выгрузки бэкапов (в нашем случае Cloud SDK и его официальные докер-образа например)

Разобравшись с местом хранения настроек кролика  можем приступить к приготовлениям всего нужного окружения и инструментов под наш бэкап

В начале было слово, и слово было 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     = "<your_project_id>" }

Для проверки инициализируем стейт и запускаем plan, который выведет нам то, что собирается сделать на основе этого кода:

terraform init terraform plan

Вывод консоли должен создавать три объекта: bucket, SA и IAM Member (Пример вывода есть в прикрепленном в конце репозитории). Если сходится — запускаем адскую машину IaC и деплоим объекты:

terraform apply

Готово — вы очаровательны!

А что дальше?

Naxt?

Naxt?

Хранилище готово, доступ имеем, что дальше? Нужен исполнитель бэкапа, для которого мы всю эту красоту и поднимали. Какие требования к нему мы выдвигаем:

  • Должен быть изолирован от окружения хоста, его зависимостей и тд , для снижения вероятности вмешивания в работу и подделку данных и тд. 

  • Минималистичный и заточенный под ограниченное количество нужных нам действий

  • Должен хранить историю своих итераций (бэкап будет происходить ежедневно) и быть под нашим мониторингом и алертами

  • Решение должно быть универсальным и легко адаптируемым к разным средам и разным кластерам RabbitMQ М

Список не так уж и огромен, но в то же время, довольно действенно сокращает нам количество вариантов. Человек знающий в этот момент начинает потихоньку понимать к чему я веду и тихо ненавидит очередного любителя известного всем инструмента.

Кого же мы выберем на роль этого важного винтика системы, прикрывающего нас от потери настроек кроля?…

Благими намерениями вымощена дорога в Kubernetes © Жак Фреско

CronJob, my k8s dudes

Ква?

Ква?

Почему же мы опять затаскиваем наши решения в K8s :

  • Чтобы всех раздражать 

  • Контейнеры/поды, которыми управляет K8s, изолированы

  • Можно выбрать образ только с нужными инструментами и не нагружать их зависимостями нашу инфру

  • CronJob в кубере оставляет трейсы в виде логов подов, что может помочь в дебаге возможных ошибок и ко всему этому — объекты кубернетес спокойно мониторятся и могут быть дополнены алертами, приходящими нам прямиком в Slack

  • В моем случае наш продукт преимущественно в кластерах, включая CronJob’ы и не хочется усложнять систему чем то еще

После того, как мы определились с нашим бэкапером, нужно понять, что ему потребуется для выполнения всех работ

Нам нужен доступ в бакет , чтобы кидать туда бэкапы , и доступ к RabbitMQ:

  1. В случае доступа к бакету мы уже условились использовать 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
  1. Для доступа в RabbitMQ мы не будем мудрить и создадим пользователей в наших кластерах с нужными для скачивания конфигурации правами. 

    Примечание: в примере использован наш экземпляр бэкапера, поэтому юзера два – для кролика внутренних сервисов и кролика клиентов

              envFrom:               - secretRef:                   name: job-rabbitconfigbackuper-rabbit-secret

С доступом разобрались (не забываем, конечно, что файрвол должен пускать кластер кубера на хосты кролика). Теперь нам нужны инструменты для бэкапа. Всё будет максимально просто – curl для получения definitions.json кроликов, gcloud для авторизации в GCP и gsutil для копирования бэкапа в наш бакет, где он будет лежать на случай DR или пришествия Ктулху.

На глаза сразу попадается образ от Google – cloud-sdk. Но не спешите брать именно его! После запуска и пула я увидел страшную цифру — 3 ГБ! Это явно не похоже на утилиту для чисто бэкапа. Можно подумать и собрать образ самому, но для быстрого решения мы немного покопали и заметили тот же официальный образ, но на базе Alpine. Результат явно лучше — 900 МБ. Остановимся на этом, но при желание образ можно собрать самому.

Алгоритм кроны выглядит так:

  1. Устанавливаем set –e, чтобы ошибки роняли крону и не кидали в бакет старые файлы или некий мусор.

  2. Curl’ом достаем definitions.json с хостов и сохраняем в локальные файлы.

  3. Логинимся в проект с помощью gcloud

  4. Кидаем бэкап файлы в 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: "<your_external_rabbit_host>"               - name: "Rabbit__Host"                 value: "<your_internal_rabbit_host>"               - name: "Rabbit__JSON__Query"                 value: "/api/definitions?download=rabbit_<your_internal_rabbit_host>.json&auth="               - name: "Rabbit__JSON__Query__External"                 value: "/api/definitions?download=rabbit_<your_external_rabbit_host>.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 и вышла ситуация, когда хост кролика кодил одним образом, кубер другим, а я на локальной машине дебажил это третьим.

Следите за руками! 

  1. Наш логин пароль прилетает из секрета кубера, который в закодированном виде выглядит вот так:

  2. Загружаясь в под он декодируется, получаем наши исходные логин пароль пользователя кролика:

А теперь нам надо отправить его в кролик для авторизации в задикодированном виде, под 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 по разворачиванию


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


Комментарии

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

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