Pull to refresh

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

Level of difficultyMedium
Reading time8 min
Views9.6K
Не волнуйтесь за них, мы позаботились об их бэкапе
Не волнуйтесь за них, мы позаботились об их бэкапе

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

Tags:
Hubs:
Total votes 9: ↑8 and ↓1+11
Comments11

Articles