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

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

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

Когда-нибудь в твоей стране запретят 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?

Naxt?

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

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

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

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

Благими намерениями вымощена дорога в 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: ""
              - 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 и вышла ситуация, когда хост кролика кодил одним образом, кубер другим, а я на локальной машине дебажил это третьим.

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

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

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

    2a89b5b70a55dccdf62384f88afbab63.png

А теперь нам надо отправить его в кролик для авторизации в задикодированном виде, под base64. Вводим заветную команду и…. Что?!!!

c77756d49d21a26f0bb8b3fdf4c8ca22.png

Это явно не похоже на оригинальный код из секрета и тот же токен авторизации из запроса загрузки. Может мы что-то не понимаем? Давайте раскодируем полученный код:

403e66eec6597d989d9da10346509ef1.png

Это точно не наш пароль. Что же делать?

$(echo -n $RabbitMQ__Creds | base64 -w 0 )

В нашем случае эта строчка всё починила, но отличие кодировок в отличие от параметров и тех же кавычек, которые также влияют на декодирование  и кодирование, может в начале запутать и сломать авторизацию

P.S В обычном ручном энкодинге/декодинге это проблема чинится простыми кавычками, но в случае нашего бэкапера мы оперируем переменными окружения и секретами, так что имеем что имеем.

Кроме этого нужно было иметь в виду то, что оперировать файлами надо в папке вроде /tmp/, иначе под падал с ошибкой из-за отсутствия прав на другие директории (security всё же).

Деплоим нашу cronJob  в кластер и вуаля! — Mission complete, Boss!

fc9b142ecc3bc48ea111050bd681b55c.png

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

Итоги и хэппиэнд

949b6a0394470deb48deef4e83a75c66.png

Кролики в безопасности, все счастливы. Не одним IaC едины, бэкапьте всё что можно, когда-нибудь это вам поможет, а коллеги будут смотреть на вас как на боженьку инфраструктуры.

Надеюсь, если вы прочитали статью до конца, то она оказалась для вас полезна или как минимум интересна. Всем добра и автоматизации!

P.S Мы используем Helm для деплоя в кубер, но шаблоны самописные, так что в статье используется raw yaml, сгенерированный helm template.

P.P. S Моя первая статья на этом ресурсе и первая статья в принципе, буду рад любому адекватному фидбеку

А вот ссылка на гит с исходниками и небольшим readme по разворачиванию

© Habrahabr.ru