Шаг за шагом: Реализация автоматического резервного копирования PostgreSQL в Kubernetes и его синхронная отправка на S3

от автора

Привет! У каждого из нас бывает что после какой-то задачи, ты хочешь чем-то поделиться. Но зачастую мотивации хватает только на поделиться в рамках внутреннего Confluence. Сейчас, я реализовал решение которое объявлено в названии статьи. Сразу хотелось бы сказать, что я не претендую на истину в последней инстанции со своим решением, оно просто отражает путь который пройден мной. Более того, СУБД в кластере здесь тоже не предмет для обсуждения.

Итак, задача следующая, создать простое для конечного пользователя решение которое позволит настроить автоматическое резервное копирование базы данных. Я использовал для решения следующие инструменты:

  • Python, из него состоит скрипт первичной установки, а также скрипт который выполняет резервное копирование и отправку.
  • Docker, для создания образа в котором будет выполняться резервное копирование и отправка в хранилище.
  • Gitlab, как система хранения, а также для ci-cd.
  • Harbor, как система хранения образов и helm чартов предоставляемых конечным пользователям.
  • Helm, как инструмент управления пакетами.
  • Kubernetes, в нем собственно и будет развернуты PostgreSQL и CronJob

Здесь я опишу концепцию, а дальше более детально покажу реализацию.

Вкратце, Postgresql не смотрит наружу из кластера, поэтому мы должны создать образ который запустится через CronJob, сделает резервную копию, отправит ее в s3 хранилище и спрячется до следующего раза. Тот кто будет это внедрять не должен выполнять специфических действий, а только ответить на вопросы установщика.

Теперь к реализации. В интернете есть множество статей на эту тему, некоторые не актуальны, некоторые не подходящие. Часть реализации я подсмотрел вот в этой статье, она не актуальна в плане кода и манифестов, но помогла сформировать технический каркас.

Код&Контейнер. А также страсти по PostgreSQL

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

import os import boto3 from datetime import datetime  def main():     # Выполняет команду pg_dump для создания резервной копии PostgreSQL     os.system('pg_dump -h $PG_HOST -U $PG_USER --section pre-data --section data --section post-data --format custom --blobs $PG_DATABASE > pgsql')     # --encoding UTF8      # Проверяет, создан ли файл резервной копии     if os.path.exists('pgsql'):         source_path = 'pgsql'         # Формирует уникальное имя файла резервной копии на основе текущей даты и времени         destination_filename = source_path + '_' + datetime.strftime(datetime.now(), "%Y.%m.%d.%H:%M") + 'UTC' + '.backup'         # Вызывает функцию для загрузки файла в S3         upload_to_s3(source_path, destination_filename)  def upload_to_s3(source_path, destination_filename):     # Создает клиент S3 с использованием учетных данных из переменных окружения     s3 = boto3.client(         's3',         aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),         aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY'),         endpoint_url=os.getenv('AWS_HOST'),         region_name=os.getenv('AWS_REGION')     )      # Получает имя бакета из переменной окружения     bucket_name = os.getenv('AWS_BUCKET_NAME')      # Файл резервной копии загружает в указанный бакет     with open(source_path, "rb") as data:         s3.upload_fileobj(data, bucket_name, destination_filename)  if __name__ == '__main__':     # Вызывает функцию main, если скрипт выполняется как самостоятельное приложение     main() 

Описание:

  1. main():

    • Выполняет команду pg_dump для создания резервной копии базы данных PostgreSQL.
    • Проверяет, успешно ли создан файл резервной копии (pgsql).
    • Формирует уникальное имя файла резервной копии на основе текущей даты и времени.
    • Вызывает функцию upload_to_s3() для загрузки файла в S3.

  2. upload_to_s3(source_path, destination_filename):

    • Создает клиент S3 с использованием учетных данных из переменных окружения.
    • Получает имя бакета из переменной окружения.
    • Загружает файл резервной копии в указанный бакет с именем (destination_filename).

  3. name == ‘main’:

    • Проверяет, выполняется ли скрипт как самостоятельное приложение.
    • Если да, вызывает функцию main().
      Код достаточно тривиальный, переменные будут переданы от пользователя в дальнейшем. Важным моментом для меня были опции создания резервной копии. Обратившись к знакомым разработчикам 1С (их можно сколько угодно считать не такими как все, но на резервном копировании они съели столько собак, что некоторые из стран столько и не видели), мне подсказали использовать вот такие ключи:

--section pre-data --section data --section post-data --format custom --blobs

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

  1. --section pre-data:

    • Описание: Этот ключ определяет, что нужно включить в резервную копию данные (pre-data). Обычно это включает в себя определения таблиц, внешних ключей, индексов и других структур данных, которые не изменяются в ходе выполнения транзакций.
    • Помощь при резервной копии: Включение этого раздела обеспечивает структуру базы данных, необходимую для восстановления схемы данных.

  2. --section data:

    • Описание: Этот ключ указывает на включение данных (data) в резервную копию. Это фактические записи в таблицах.
    • Помощь при резервной копии: Без данных резервная копия не будет содержать реальные записи, что делает ее неполной.

  3. --section post-data:

    • Описание: Этот ключ определяет, что следует включить в резервную копию данные (post-data). Это может включать в себя индексы, триггеры и другие элементы, которые могут изменяться в ходе выполнения транзакций.
    • Помощь при резервной копии: Включение этого раздела обеспечивает дополнительные элементы, которые могут быть важными для восстановления полной функциональности базы данных.

  4. --format custom:

    • Описание: Этот ключ определяет формат резервной копии. В данном случае, это пользовательский формат.
    • Помощь при резервной копии: Пользовательский формат обеспечивает более гибкую и эффективную резервную копию, чем другие форматы, такие как plain или tar.

  5. --blobs:

    • Описание: Этот ключ указывает, что нужно включить большие объекты (BLOBs) в резервную копию. BLOBs могут включать в себя бинарные данные, такие как изображения или документы.
    • Помощь при резервной копии: Если в базе данных присутствуют большие объекты, и мы хотим их сохранить, этот ключ необходим для включения этих данных в резервную копию.
      Если у кого-то есть какая-то более подробная информация о ключах, или какие-то уточнения, то вы можете поделиться, я исправлю в статье.

Следующим шагом в этой части необходимо написать манифест Dockerfile который в дальнейшем соберем через ci-cd и отправим образ в registry.

FROM python:3.9-alpine  WORKDIR /app COPY backup.py .  RUN apk --update add \     postgresql \     python3 \     py3-pip \     && pip3 install --upgrade pip \     && pip3 install awscli  RUN pip install boto3 CMD ["python", "backup.py"] 

В этом манифесте необходимый минимум для рабоспособности решения. Краткое описание:

  1. FROM python:3.9-alpine:
    • Используется базовый образ Python 3.9 на основе Alpine Linux.
  2. WORKDIR /app:
    • Устанавливается рабочая директория**/app** внутри контейнера, где будут храниться файлы приложения.
  3. COPY backup.py .:
    • Копируется файл backup.py из локальной директории внутрь контейнера в текущую рабочую директорию.
  4. RUN apk --update add ...:
    • Устанавливаются пакеты, такие как PostgreSQL, Python 3, pip и AWS CLI.
  5. RUN pip install boto3:
    • Устанавливается библиотека boto3 для работы с S3.
  6. CMD ["python", "backup.py"]:
    • Определяется команда, которая будет выполнена при запуске контейнера. В данном случае, запускается скрипт backup.py

Промежуточный манифест

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

Команда для создания секрета:

kubectl create secret docker-registry docker-config-secret \       --docker-server="registry.ru" \       --docker-username="Alex_Vlan" \       --docker-password="TOKEN" \       --docker-email="example@registry.ru" \       --namespace="default"

Манифест:

--- apiVersion: batch/v1 kind: CronJob metadata:   name: database-backup spec:   schedule: "*/2 * * * *"   successfulJobsHistoryLimit: 2   concurrencyPolicy: Replace   jobTemplate:     spec:       template:         spec:           restartPolicy: OnFailure           containers:             - name: database-backup               image: registry.ru/image:latest               env:                 - name: PG_HOST                   value: postgresql                 - name: PGPASSWORD                   value: password                 - name: PG_USER                   value: postgres                 - name: PG_DATABASE                   value: example                 - name: AWS_ACCESS_KEY_ID                   value: access_key                 - name: AWS_SECRET_ACCESS_KEY                   value: secret_key                 - name: AWS_BUCKET_NAME                   value: example                 - name: AWS_HOST                   value: https://s3.ru-1.storage.selcloud.ru                 - name: AWS_PORT                   value: '443'                 - name: AWS_REGION                   value: 'ru-1'               imagePullPolicy: Always           imagePullSecrets:             - name: docker-config-secret 

В этот манифест нужно дописать(захардкодить) свои данные и запустить через kubectl apply -f manifest.yaml

Значения будут переданы в контейнер при запуске. Здесь я оставлю краткое описание манифеста:

  • apiVersion: batch/v1 и kind: CronJob определяют тип ресурса Kubernetes как CronJob.
  • metadata содержит метаданные, такие как имя database-backup.
  • spec.schedule определяет расписание в формате cron, в данном случае — каждые 2 минуты.
  • spec.successfulJobsHistoryLimit устанавливает лимит сохраняемых успешно завершенных задач.
  • spec.concurrencyPolicy определяет политику конкурентности (в данном случае, заменять существующую задачу новой).
  • spec.jobTemplate определяет шаблон для создания задачи.
  • spec.template.spec.restartPolicy устанавливает политику перезапуска контейнера.
  • spec.template.spec.containers содержит настройки контейнера, в том числе используемый образ и переменные окружения.
  • spec.template.spec.imagePullSecrets используется для указания секрета, необходимого для загрузки образа из регистра контейнеров.

Как и было сказано ранее, этого недостаточно для решения задачи, продолжаем.

Кратко о CI-CD

Учитывая что сейчас ямлики для ci-cd учат писать на всех-всех курсах для мамкиных программистов за много-много денег, здесь останется только минимум, для демонстрации и работоспособности. По условиям задачи мы отправляем проект в платформу управления кодом(конкретно в этом случае gitlab), там через ci-cd собирается образ отправляется в registry Gitlab и в Harbor. Реализация именно такая, Gitlab registry для себя, Harbor для остальных. Объединил все в один job для демонстрации.

variables:   DOCKER_HOST: tcp://docker:2375   DOCKER_DRIVER: overlay2   DOCKER_TLS_CERTDIR: ""  default:   image: docker:24.0.4   interruptible: true  stages:   - build_deploy  build-deploy-backup-docker:   stage: build_deploy   script:     - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY     - docker build -t $CI_REGISTRY/infra/database-backup:$CI_COMMIT_REF_NAME-latest .     - docker push $CI_REGISTRY/infra/database-backup:$CI_COMMIT_REF_NAME-latest     - docker images     # upload in harbor     - docker login --username $HARBOR_USER --password $HARBOR_TOKEN harbor.regitry.ru     - docker tag $CI_REGISTRY/infra/database-backup:$CI_COMMIT_REF_NAME-latest harbor.regitry.ru/images/database-backup:$CI_COMMIT_REF_NAME-latest     - docker push harbor.regitry.ru/images/database-backup:$CI_COMMIT_REF_NAME-latest   when: manual 

Остаеться добавить переменные HARBOR_USER, HARBOR_TOKEN и можно запускать.

Краткое описание манифеста:

  • variables:
    • Определяет переменные окружения, такие как DOCKER_HOST, DOCKER_DRIVER, и DOCKER_TLS_CERTDIR, которые используются в рамках сборки и деплоя Docker-образа.
  • default:
    • Устанавливает образ по умолчанию для выполнения шагов CI/CD. В данном случае используется образ Docker версии 24.0.4.
  • stages:
    • Определяет этапы выполнения CI/CD. В данном случае, у нас есть только один этап — build_deploy.
  • build-deploy-backup-docker:
    • Определяет задачу для этапа build_deploy. Эта задача выполняет сборку и отправку Docker-образа.
    • script: Содержит команды, которые выполняются внутри job. Это включает в себя логин в реестр GitLab, сборку и пуш Docker-образа, а также загрузку в Harbor.
    • when: manual: Определяет, что задача должна выполняться вручную, не автоматически.

Helm. Просто Helm

На этом этапе необходимо подготовить Helm-чарты используя которые можно произвести деплой решения в кластер.

В этой реализации взято два Helm-чарта, первый для создания секрета, взят здесь. Чарт для CronJob самописный. В реализации сейчас они используются как отдельные чарты, будет видно дальше в скрипте. Не хватает времени собрать в один чтобы было более красиво.

Сейчас структура выглядит вот так:

Разберем для начала чарт database-backup-chart.

values.yaml:

image:   repository: ""   tag: ""  # set schedule schedule: ""  # set env for inside container env:   PG_HOST: ""   PGPASSWORD: ""   PG_USER: ""   PG_DATABASE: ""   AWS_ACCESS_KEY_ID: ""   AWS_SECRET_ACCESS_KEY: ""   AWS_BUCKET_NAME: ""   AWS_HOST: ""   AWS_PORT: ""   AWS_REGION: ""

В этом манифесте будут заданы переменные для запуска CronJob.

  • image:
    • repository: Определяет переменную для репозитория Docker-образа.
    • tag: Определяет переменную для тега Docker-образа.
  • schedule:
    • Определяет переменную для расписания, которое использоваться в контексте планирования задачи.
  • env:

    • Задает переменные окружения, которые будут использоваться внутри контейнера. В данном случае, переменные являются параметрами подключения к базе данных PostgreSQL и конфигурацией S3.

    Chart.yaml

apiVersion: v2 name: database-backup-chart description: A Helm chart for create Kubernetes CronJob version: 0.1.0 

  • apiVersion: v2:
    • Определяет версию Helm (API).
  • name: database-backup-chart:
    • Задает имя Helm-чарта.
  • description:
    • Содержит краткое описание Helm-чарта.
  • version: 0.1.0:
    • Устанавливает версию Helm-чарта.
      templates/cronjob.yaml

apiVersion: batch/v1 kind: CronJob metadata:   name: {{ .Release.Name }} spec:   schedule: {{ .Values.schedule }}   successfulJobsHistoryLimit: 2   concurrencyPolicy: Replace   jobTemplate:     spec:       template:         spec:           restartPolicy: OnFailure           containers:             - name: {{ .Release.Name }}               image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"               env:                 - name: PG_HOST                   value: {{ .Values.env.PG_HOST | quote }}                 - name: PGPASSWORD                   value: {{ .Values.env.PGPASSWORD | quote }}                 - name: PG_USER                   value: {{ .Values.env.PG_USER | quote }}                 - name: PG_DATABASE                   value: {{ .Values.env.PG_DATABASE | quote }}                 - name: AWS_ACCESS_KEY_ID                   value: {{ .Values.env.AWS_ACCESS_KEY_ID | quote }}                 - name: AWS_SECRET_ACCESS_KEY                   value: {{ .Values.env.AWS_SECRET_ACCESS_KEY | quote }}                 - name: AWS_BUCKET_NAME                   value: {{ .Values.env.AWS_BUCKET_NAME | quote }}                 - name: AWS_HOST                   value: {{ .Values.env.AWS_HOST | quote }}                 - name: AWS_PORT                   value: {{ .Values.env.AWS_PORT | quote }}                 - name: AWS_REGION                   value: {{ .Values.env.AWS_REGION | quote }}               imagePullPolicy: Always           imagePullSecrets:             - name: docker-config-secret-backup 

  • metadata.name: {{ .Release.Name }}:
    • Имя CronJob формируется на основе имени Helm-релиза.
  • spec.schedule: {{ .Values.schedule }}:
    • Расписание выполнения задачи в формате cron. Это значение передается из файла values.yaml.
  • spec.successfulJobsHistoryLimit: 2:
    • Ограничение на количество успешных выполненных задач, которые будут сохранены в истории.
  • spec.concurrencyPolicy: Replace:
    • Политика управления параллелизмом задач. В данном случае, новая задача заменяет предыдущую, если она ещё не завершилась.
  • spec.jobTemplate.spec.template.spec.restartPolicy: OnFailure:
    • Политика перезапуска контейнера в случае сбоя задачи.
  • spec.jobTemplate.spec.template.spec.containers:
    • Определение контейнера, который будет выполнен внутри каждой задачи CronJob.
  • spec.imagePullPolicy: Always:
    • Политика загрузки Docker-образа. В данном случае, образ будет всегда загружаться перед выполнением задачи.
  • spec.imagePullSecrets: docker-config-secret-backup:
    • Секрет, используемый для аутентификации при загрузке Docker-образа из реестра.

Здесь будут использованы переменные, которые необходимо задать в values.yaml. Конкретно в этой реализации переменные будут передаваться в helm-чарт из скрипта установки.

Теперь helm-чарт секрета dockerconfigjson.

Здесь не будет подробного описания всего чарта, так как это сторонний чарт и описание можно посмотреть у его создателя в репозитории. Останется здесь только values.yaml чтобы не отходить от конекста реализации.

values.yaml

imageCredentials:   - registry: ""     username: ""     accessToken: ""

Здесь передаются переменные для создания секрета.

Теперь можно дополнить манифест пайплайна в ci-cd вот таким джобом:

build_deploy_tgz_chart:   stage: build_deploy   image:     name: alpine/helm     entrypoint: [""]   script:     - chart_package=$(helm package database-backup-chart | awk -F' ' '{print $NF}')     - echo $chart_package     - helm registry login -u $HARBOR_USER -p $HARBOR_TOKEN harbor.regitry.ru     - helm push $chart_package oci://harbor.regitry.ru/helm-charts     - chart_package=$(helm package dockerconfigjson | awk -F' ' '{print $NF}')     - echo $chart_package     - helm push $chart_package oci://harbor.regitry.ru/helm-charts   when: manual 

Здесь мы запаковываем и отправляем helm-чарты в Harbor.

  1. stage: build_deploy: Этот пайплайн выполняется на этапе build_deploy.
  2. image: alpine/helm: Используется образ Alpine Linux с предустановленным Helm. Он предоставляет необходимые инструменты для упаковки и публикации Helm-чартов.
  3. entrypoint: [""]: Переопределяет точку входа, чтобы не запускать команду по умолчанию. Это позволяет использовать Helm-команды напрямую.
  4. script:
    • chart_package=$(helm package database-backup-chart | awk -F' ' '{print $NF}'): Упаковывает Helm-чарт database-backup-chart и извлекает имя упакованного файла.
    • echo $chart_package: Выводит имя упакованного файла в консоль.
    • helm registry login -u $HARBOR_USER -p $HARBOR_TOKEN harbor.regitry.ru: Логин в Harbor Registry с использованием учетных данных пользователя и токена.
    • helm push $chart_package oci://harbor.regitry.ru/helm-charts: Публикует упакованный Helm-чарт в Harbor Registry.
    • chart_package=$(helm package dockerconfigjson | awk -F' ' '{print $NF}'): Упаковывает Helm-чарт dockerconfigjson и извлекает имя упакованного файла.
    • echo $chart_package: Выводит имя упакованного файла в консоль.
    • helm push $chart_package oci://harbor.regitry.ru/helm-charts: Публикует упакованный Helm-чарт dockerconfigjson в Harbor Registry.
  5. when: manual: Пайплайн будет выполняться только вручную.

Сладковатый запах Питона

Итак, плавно можно перейти к реализации скрипта внедрения. Как и у многих девопсов, выбор встал между использовать Bash или Python. Последние года полтора, я предпочитаю такие небольшие вещи делать на Python. Не хочу заводить каких-либо споров, мне просто удобнее.

Здесь будут описаны куски кода, в той же последовательности, в которой они расположены в скрипте. Мой код не самый чистый и я успокаиваю себя тем, что я девопс, а не руки кривые. Поэтому, опытные разработчики, простите если своим кодом оскорбил кого-то, а также если есть какие-то вещи которые режут глаз, сообщите мне об этом, буду делать рефакторинг, изменю.

Для начала импортируем необходимые библиотеки:

import os import subprocess import logging import yaml import tarfile from datetime import datetime import time import json import sys

  1. import os: Модуль os предоставляет функции для взаимодействия с операционной системой.
  2. import subprocess: Модуль subprocess предоставляет возможность запускать новые процессы, соединять их стандартные потоки ввода/вывода/ошибок, и получать результат выполнения команд.
  3. import logging: Модуль logging предоставляет инфраструктуру для ведения логов.
  4. import yaml: Модуль yaml обеспечивает работу с данными в формате YAML.
  5. import tarfile: Модуль tarfile предоставляет функциональность для работы с файлами в формате tar.
  6. from datetime import datetime: Этот код импортирует только datetime из модуля datetime. datetime используется для работы с датой и временем.
  7. import sys: Модуль sys предоставляет доступ к некоторым переменным и функциям, взаимодействующим с интерпретатором Python.

chart_names = ["database-backup-chart", "dockerconfigjson"] repository = "harbor.repository.ru/images/database-backup" tag = "main-latest"

Переменные для скрипта:

  1. chart_names: Helm-чарты которые в дальнейшем будут скачаны, распакованы и применены.
  2. repository: URL репозитория с чартами.
  3. tag: Просто тег для определения окружения.

Настройка логирования:

log_filename = f"installation_log_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt" logging.basicConfig(filename=log_filename, level=logging.INFO,                     format='%(asctime)s - %(levelname)s - %(message)s')

  • log_filename: Генерируется имя файла для журнала, содержащее текущую дату и время.
  • basicConfig: Настройка базовых параметров системы логирования, включая имя файла, уровень логирования (INFO и выше) и формат сообщений.

Функция выполнения команды:

def run_command(command):     logging.info(f"Выполнение команды: {command}")     try:         result = subprocess.run(command, shell=True, stdout=subprocess.PIPE,                                 stderr=subprocess.PIPE, text=True, check=True)         logging.info(f"Стандартный вывод:\n{result.stdout}")     except subprocess.CalledProcessError as e:         logging.error(f"Ошибка при выполнении команды: {command}")         logging.error(f"Стандартная ошибка:\n{e.stderr}")         exit(1)

  • run_command: Это функция, которая принимает команду в виде строки и выполняет ее в системной оболочке.
  • logging.info: Записывает информационное сообщение о том, какая команда выполняется.
  • subprocess.run: Выполняет команду в системной оболочке с использованием модуля subprocess.
  • logging.error: В случае ошибки записывает сообщение об ошибке и стандартный вывод ошибки.
  • exit(1): Принудительно завершает выполнение программы с кодом ошибки 1.

Функция проверка доступности Kubernetes кластера:

def configure_kubernetes(k8s_config_path):     print(f"\033[94mПуть к файлу конфигурации: {k8s_config_path}\033[0m")      # Проверяем, существует ли файл по указанному пути     if os.path.exists(k8s_config_path):         os.environ["KUBECONFIG"] = k8s_config_path         logging.info(f"Переменная окружения KUBECONFIG успешно установлена.")         print("\033[92mПеременная окружения KUBECONFIG успешно установлена.\033[0m")          # Проверяем доступность кластера Kubernetes по файлу конфигурации         try:             subprocess.run(["kubectl", "version"], check=True)             logging.info(f"Кластер Kubernetes доступен.")             print("\033[92mКластер Kubernetes доступен.\033[0m")         except subprocess.CalledProcessError as e:             logging.error(                 f"Ошибка при проверке доступности кластера Kubernetes. Стандартная ошибка:\n{e.stderr}")             print(                 "\033[91mОшибка при проверке доступности кластера Kubernetes.\033[0m")             print(f"Стандартная ошибка:\n{e.stderr}")             k8s_config_path = input(                 "Введите путь к файлу конфигурации Kubernetes: ")             configure_kubernetes(k8s_config_path)     else:         print("\033[91mФайл по указанному пути не существует.\033[0m")         k8s_config_path = input(             "Введите путь к файлу конфигурации Kubernetes: ")         configure_kubernetes(k8s_config_path)

Эта функция выполняет следующие шаги:

  1. Выводит путь к файлу конфигурации Kubernetes.
  2. Проверяет существование файла по указанному пути.
  3. Если файл существует, устанавливает переменную окружения KUBECONFIG, проверяет доступность кластера Kubernetes с использованием команды kubectl version.
  4. Если доступ к кластеру успешен, сообщает об этом.
  5. Если файл по указанному пути не существует, сообщает об ошибке, запрашивает новый путь к файлу и рекурсивно вызывает саму себя.

Функция загрузки и распаковки Helm Charts.

 def download_and_unpack_helm_charts(username, access_token, registry, chart_names):     run_command(         f"helm registry login -u {username} -p {access_token} {registry}")     logging.info("Авторизация")     original_directory = os.getcwd()     run_command("rm -rf database_backup_helm_charts/")     os.mkdir("database_backup_helm_charts/")     os.chdir("database_backup_helm_charts/")     chart_paths = {}     for chart_name in chart_names:         run_command(             f"helm pull oci://harbor.regitry.ru/helm-charts/{chart_name}")         chart_archive_name = max(             [f for f in os.listdir() if f.endswith(".tgz")])         output_dir = f"./{chart_name}"         with tarfile.open(chart_archive_name, "r:gz") as tar:             tar.extractall(path=output_dir)         os.remove(chart_archive_name)         chart_paths[chart_name] = output_dir     for chart_name, path in chart_paths.items():         print(             f"\033[94mРаспакованный Helm Chart '{chart_name}': {path}\033[0m")     os.chdir(original_directory) 

Данный код загружает и распаковывает Helm Charts из удаленного реестра (registry) при использовании Helm CLI. Функция принимает параметры, такие как имя пользователя (username), токен доступа (access_token), адрес реестра (registry) и имена Helm Charts (chart_names), которые требуется загрузить и распаковать.

Основные шаги, которые выполняет функция:

  1. Авторизация в реестре:
    • Используется Helm CLI для авторизации в Helm registry с предоставленными учетными данными пользователя (username) и токеном доступа (access_token).
  2. Подготовка рабочей директории:
    • Скрипт создает временную директорию с именем «database_backup_helm_charts/» и переходит в нее.
  3. Загрузка и распаковка Helm Charts:
    • Для каждого указанного Helm Chart (chart_name), скрипт использует команду helm pull для загрузки Chart из указанного реестра.
    • Загруженный архив Helm Chart распаковывается в новую директорию с именем Helm Chart (chart_name).
    • Исходные архивы Helm Charts удаляются после распаковки.
  4. Вывод результатов:
    • После успешной загрузки и распаковки каждого Helm Chart, функция выводит информацию о распакованном Helm Chart, предоставляя путь к распакованному содержимому.
  5. Восстановление исходной директории:
    • По завершении всех операций, функция возвращает текущую директорию в исходное состояние.

Функция изменения значений в values файле для создания секрета.

 def insert_values_yaml_secrets(registry, username, access_token):     with open('database_backup_helm_charts/dockerconfigjson/dockerconfigjson/values.yaml', 'r') as file:         yaml_data = yaml.safe_load(file)     credentials = yaml_data['imageCredentials'][0]     credentials['registry'] = f"{registry}"     credentials['username'] = f"{username}"     credentials['accessToken'] = f"{access_token}"     with open('database_backup_helm_charts/dockerconfigjson/dockerconfigjson/values.yaml', 'w') as file:         yaml.dump(yaml_data, file) 

Данная функция предназначена для изменения значений в файле YAML (values.yaml) с целью создания секрета, связанного с данными реестра. Функция обеспечивает внесение актуальных учетных данных пользователя (имя пользователя, токен доступа) и адреса registry.

Шаги, выполняемые функцией:

  1. Открытие YAML файла для чтения:
    • Функция открывает файл values.yaml.
  2. Чтение YAML данных:
    • С использованием библиотеки PyYAML (yaml.safe_load), функция читает данные из файла values.yaml.
  3. Обновление учетных данных:
    • Значения, такие как registry, username и accessToken, обновляются новыми значениями, переданными функции в качестве параметров (registry, username, access_token).
  4. Запись обновленных данных обратно в файл:
    • Функция открывает файл values.yaml для записи и использует yaml.dump для записи обновленных данных обратно в файл.

Функция изменения значений в values файле для создания CronJob.

 def insert_values_yaml_cj(repository, tag, schedule, pg_host, pg_password, pg_user, pg_database, aws_access_key_id, aws_secret_access_key, aws_bucket_name, aws_host, aws_port, aws_region):     with open('database_backup_helm_charts/database-backup-chart/database-backup-chart/values.yaml', 'r') as file:         yaml_data = yaml.safe_load(file)     yaml_data['image']['repository'] = f"{repository}"     yaml_data['image']['tag'] = f"{tag}"     yaml_data['schedule'] = f"{schedule}"     yaml_data['env']['PG_HOST'] = f"{pg_host}"     yaml_data['env']['PGPASSWORD'] = f"{pg_password}"     yaml_data['env']['PG_USER'] = f"{pg_user}"     yaml_data['env']['PG_DATABASE'] = f"{pg_database}"     yaml_data['env']['AWS_ACCESS_KEY_ID'] = f"{aws_access_key_id}"     yaml_data['env']['AWS_SECRET_ACCESS_KEY'] = f"{aws_secret_access_key}"     yaml_data['env']['AWS_BUCKET_NAME'] = f"{aws_bucket_name}"     yaml_data['env']['AWS_HOST'] = f"{aws_host}"     yaml_data['env']['AWS_PORT'] = f"{aws_port}"     yaml_data['env']['AWS_REGION'] = f"{aws_region}"     with open('database_backup_helm_charts/database-backup-chart/database-backup-chart/values.yaml', 'w') as file:         yaml.dump(yaml_data, file, default_flow_style=False)

Данная функция предназначена для изменения значений в файле YAML (values.yaml), используемом в контексте Helm Chart для создания CronJob.

Шаги, выполняемые функцией:

  1. Открытие YAML файла для чтения:
    • Функция открывает файл values.yaml, содержащий конфигурационные параметры Helm Chart для CronJob.
  2. Чтение YAML данных:
    • С использованием библиотеки PyYAML (yaml.safe_load), функция читает данные из файла values.yaml.
  3. Замена значений:
    • Обновляются значения параметров Helm Chart:
      • repository и tag для указания местоположения Docker-образа.
      • schedule для установки расписания CronJob.
      • Параметры среды (env) для настройки подключения к PostgreSQL (PG_HOST, PGPASSWORD, PG_USER, PG_DATABASE).
      • Параметры среды для настройки подключения к S3 хранилищу(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_BUCKET_NAME, AWS_HOST, AWS_PORT, AWS_REGION).
  4. Открытие YAML файла для записи:
    • Функция открывает файл values.yaml для записи обновленных данных.
  5. Запись обновленных данных обратно в файл:
    • Используется yaml.dump для записи обновленных данных обратно в файл.

Функция настройки шедулера.

 def get_cron_schedule():     print("Выберите частоту запуска кронджоба:")     print("1. Каждый час")     print("2. Каждую ночь (в полночь)")     print("3. Каждую неделю (в воскресенье в полночь)")     print("4. Свой вариант (введите свой cron-формат)")     choice = input("Введите номер варианта (1-4): ")     if choice == "1":         return "0 * * * *"     elif choice == "2":         return "0 0 * * *"     elif choice == "3":         return "0 0 * * 0"     elif choice == "4":         custom_schedule = input("Введите свой cron-формат: ")         return custom_schedule     else:         print("Некорректный выбор. Пожалуйста, выберите от 1 до 4.")         return get_cron_schedule()

Данная функция предназначена для получения и настройки расписания (schedule) для запуска CronJob, используемого в контексте управления периодическим выполнением задачи резервного копирования базы данных.

Шаги, выполняемые функцией:

  1. Вывод сообщения с предложением выбора частоты запуска:
    • Выводит на экран пользовательское меню с вариантами частоты запуска CronJob.
  2. Получение выбора пользователя:
    • Запрашивает ввод пользователя для выбора одного из вариантов (1-4).
  3. Обработка выбора пользователя:
    • В зависимости от выбора пользователя, функция возвращает соответствующий cron-формат для расписания.
    • Варианты:
      • «1»: Каждый час — возвращает "0 * * * *"
      • «2»: Каждую ночь (в полночь) — возвращает *`»0 0 «`**
      • «3»: Каждую неделю (в воскресенье в полночь) — возвращает "0 0 * * 0"
      • «4»: Пользовательский вариант — запрашивает у пользователя ввод собственного cron-формата.
  4. Проверка корректности выбора:
    • Если пользователь ввел некорректный номер варианта, выводит сообщение об ошибке и предлагает повторить ввод.
  5. Рекурсивный вызов функции:
    • В случае некорректного выбора, функция вызывает саму себя рекурсивно, чтобы предоставить пользователю новый выбор.

Функция выбора действия(основное меню).

def display_menu():     print("Вас приветствует программа установки резервного копирования для СУБД PostgreSQL.")     print("Выберите действие:")     print("1. Установить систему резервного копирования")     print("2. Удалить систему резервного копирования")     print("3. Посмотреть информацию о компонентах, используемых для резервного копирования")     print("0. Выйти") 

Описание функции:

Данная функция предназначена для отображения текстового меню в консоли, приветствуя пользователя и предоставляя ему варианты действий в контексте установки резервного копирования для СУБД PostgreSQL.

Шаги, выполняемые функцией:

  1. Приветствие:
    • Выводит приветственное сообщение, приветствуя пользователя и сообщая о назначении программы.
  2. Вывод меню:
    • Печатает на экран текстовое меню с доступными действиями для выбора.
    • Варианты действий:
      • «1»: Установить систему резервного копирования
      • «2»: Удалить систему резервного копирования
      • «3»: Посмотреть информацию о компонентах, используемых для резервного копирования
      • «0»: Выйти

Функция установки системы резервного копирования.

def install_backup_system():     print("Установка системы резервного копирования...")     pg_host = input("Введите PostgreSQL host. По умолчанию [backup-infrastructure-postgresql]: ").strip(     ) or "backup-infrastructure-postgresql"     pg_password = input("Введите пароль для PostgreSQL []: ").strip() or ""     pg_user = input(         "Введите имя пользователя для PostgreSQL. По умолчанию [postgres]: ").strip() or "postgres"     pg_database = input(         "Введите имя базы данных для которой реализовано резервное копирование. По умолчанию [backup]: ").strip() or "backup"     aws_access_key_id = input(         "Введите ACCESS_KEY_ID для S3 системы хранения, в которой будут храниться резервные копии []: ").strip() or ""     aws_secret_access_key = input(         "Введите SECRET_ACCESS_KEY для S3 системы хранения, в которой будут храниться резервные копии []: ").strip() or ""     aws_bucket_name = input(         "Введите BUCKET_NAME для S3 системы хранения, в котором будут храниться резервные копии []: ").strip() or ""     aws_host = input("Введите URL для S3 системы хранения, в которой будут храниться резервные копии. По умолчанию [https://s3.ru-1.storage.selcloud.ru]: ").strip(     ) or "https://s3.ru-1.storage.selcloud.ru"     aws_port = input(         "Введите PORT для S3 системы хранения, в которой будут храниться резервные копии. По умолчанию стандартный https порт [443] : ").strip() or "443"     aws_region = input(         "Введите REGION для S3 системы хранения, в которой будут храниться резервные копии. По умолчанию [ru-1]: ").strip() or "ru-1"     namespace_backup = input(         "Введите namespace в котором развернуто решение PostgreSQL. По умолчанию [backup]: ").strip() or "backup"     registry = "https://harbor.regitry.ru"     username = input(         "Введите имя пользователя для авторизации в системе хранения Harbor []: ").strip() or ""     access_token = input(         "Введите access_token пользователя для авторизации в системе хранения Harbor []: ").strip() or ""     k8s_config_path = input("Введите путь к файлу конфигурации Kubernetes: ")     schedule = get_cron_schedule()     print(f"Выбранная частота запуска кронджоба: {schedule}")      # Вызов функции проверки доступности кластера.     configure_kubernetes(k8s_config_path)      # Установка Helm     print("Установка Helm...")     run_command(         "curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3")     run_command("chmod 700 get_helm.sh")     run_command("./get_helm.sh")      # скачивание и распаковка helm charts     download_and_unpack_helm_charts(         username, access_token, registry, chart_names)      # Запуск функции обработки values файла values.yaml для секрета     insert_values_yaml_secrets(registry, username, access_token)     # Запуск функции обработки values файла values.yaml для Cronjob     insert_values_yaml_cj(repository, tag, schedule, pg_host, pg_password, pg_user, pg_database,                           aws_access_key_id, aws_secret_access_key, aws_bucket_name, aws_host, aws_port, aws_region)      # Установка charts     check_existing_release_command = "helm list --all-namespaces --short"     existing_releases = subprocess.check_output(         check_existing_release_command, shell=True, text=True)     if "dockerconfigjson" not in existing_releases:         # Получаем текущую рабочую директорию         original_directory = os.getcwd()         os.chdir("database_backup_helm_charts/dockerconfigjson/")         run_command(             f"helm upgrade --install dockerconfigjson ./dockerconfigjson --namespace {namespace_backup}")         print("\033[92mСекрет успешно cоздан\033[0m")         # Возвращаемся в исходную директорию         os.chdir(original_directory)     else:         print("\033[93mСекрет уже существует.\033[0m")      check_existing_release_command = "helm list --all-namespaces --short"     existing_releases = subprocess.check_output(         check_existing_release_command, shell=True, text=True)     if "database-backup" not in existing_releases:         original_directory = os.getcwd()         os.chdir("database_backup_helm_charts/database-backup-chart/")         run_command(             f"helm upgrade --install database-backup ./database-backup-chart --namespace {namespace_backup}")         print("\033[92mCronJob успешно cоздан\033[0m")         # Возвращаемся в исходную директорию         os.chdir(original_directory)         sys.exit()     else:         print("\033[93mCronJob уже существует.\033[0m") 

Данная функция предназначена для установки системы резервного копирования для СУБД PostgreSQL. Она взаимодействует с пользователем, собирая необходимую информацию, скачивает Helm charts, обрабатывает значения файлов YAML, устанавливает Helm, создает и обновляет Kubernetes ресурсы (Secret и CronJob) для обеспечения резервного копирования базы данных.

Шаги, выполняемые функцией:

  1. Получение параметров от пользователя:
    • Запрашивает у пользователя информацию о параметрах установки, таких как хост PostgreSQL, пароль, имя пользователя, база данных, ключи доступа к S3-системе хранения, URL и порт S3, регион, namespace и другие.
  2. Выбор частоты запуска кронджоба:
    • Вызывает функцию get_cron_schedule(), которая предоставляет пользователю выбор частоты запуска CronJob (регулярного задания).
  3. Проверка доступности кластера Kubernetes:
    • Вызывает функцию configure_kubernetes(), которая проверяет доступность кластера Kubernetes, используя указанный путь к файлу конфигурации.
  4. Установка Helm:
    • Загружает и устанавливает Helm.
  5. Скачивание и распаковка Helm charts:
    • Вызывает функцию download_and_unpack_helm_charts(), которая скачивает и распаковывает Helm charts из указанного реестра и с указанными именами чартов.
  6. Обработка values файла для секрета:
    • Вызывает функцию insert_values_yaml_secrets(), которая изменяет значения в файле values.yaml для создания секрета.
  7. Обработка values файла для CronJob:
    • Вызывает функцию insert_values_yaml_cj(), которая изменяет значения в файле values.yaml для создания CronJob.
  8. Проверка существующих релизов:
    • Проверяет существование релизов с помощью Helm.
  9. Установка секрета:
    • Если секрета еще нет, устанавливает его в Kubernetes.
      1. Установка CronJob:
    • Если CronJob еще не установлен, устанавливает его в Kubernetes.
      1. Вывод результатов:
    • Выводит информацию об успешном создании или об уже существующих ресурсах.
      1. Завершение программы:
    • Завершает выполнение программы.

Функция удаления системы резервного копирования.

def remove_backup_system():      print("Удаление системы резервного копирования...")     namespace_backup = input(         "Введите namespace в котором развернуто решение PostgreSQL. По умолчанию [backup]: ").strip() or "backup"     k8s_config_path = input("Введите путь к файлу конфигурации Kubernetes: ")     configure_kubernetes(k8s_config_path)     print("Удаление CronJob...")     check_existing_release_command = f"helm list --namespace {namespace_backup} --short"     existing_releases = subprocess.check_output(         check_existing_release_command, shell=True, text=True)     if "database-backup" in existing_releases:         run_command(             f"helm uninstall database-backup --namespace {namespace_backup}")         print("\033[92mCronJob успешно удален.\033[0m")     else:         print(             "\033[91mCronJob не может быть удален, так как не был найден.\033[0m")      print("Удаление системы резервного копирования...")     print("Удаление Секрета...")     check_existing_release_command = f"helm list --namespace {namespace_backup} --short"     existing_releases = subprocess.check_output(         check_existing_release_command, shell=True, text=True)     if "dockerconfigjson" in existing_releases:         run_command(             f"helm uninstall dockerconfigjson --namespace {namespace_backup}")         print("\033[92mСекрет успешно удален.\033[0m")     else:         print(             "\033[91mСекрет не может быть удален, так как не был найден.\033[0m")     sys.exit() 

Данная функция предназначена для удаления системы резервного копирования для СУБД PostgreSQL. Она взаимодействует с пользователем, собирая необходимую информацию, удаляет Helm charts, удаляет Helm релизы, связанные с CronJob и Secret.

Шаги, выполняемые функцией:

  1. Получение параметров от пользователя:
    • Запрашивает у пользователя информацию о параметрах удаления, таких как namespace, путь к файлу конфигурации Kubernetes и другие.
  2. Проверка доступности кластера Kubernetes:
    • Вызывает функцию configure_kubernetes(), которая проверяет доступность кластера Kubernetes, используя указанный путь к файлу конфигурации.
  3. Удаление CronJob:
    • Проверяет существование Helm релиза CronJob и, если он существует, удаляет его с помощью команды Helm. Выводит соответствующее сообщение.
  4. Удаление Secret:
    • Проверяет существование Helm релиза Secret и, если он существует, удаляет его с помощью команды Helm. Выводит соответствующее сообщение.
  5. Вывод результатов:
    • Выводит информацию об успешном удалении или об отсутствии найденных ресурсах.
  6. Завершение программы:
    • Завершает выполнение программы.

Функция вывода информации.

def view_backup_system_info():     print("Просмотр информации о системе резервного копирования...")     print("Резервное копирование PostgreSQL базы данных реализовано следующим образом:")     print("- Внутри Kubernetes-кластера поднимается контейнер, в котором настроены все необходимые параметры для создания резервной копии.")     print("- Заданный при установке скрипта расписание крон-джоба определяет, как часто выполнять процесс резервного копирования.")     print("- Контейнер подключается к PostgreSQL базе данных, выполняет процедуру бэкапа и сохраняет его в хранилище S3.")     print("- Параметры для подключения к PostgreSQL и настройки хранения бэкапов задаются при установке скрипта.") 

Данная функция предназначена для просмотра информации о системе резервного копирования для базы данных PostgreSQL. Функция предоставляет пользователю описание того, как реализован процесс резервного копирования после установки скрипта.

Функция запуска скрипта.

 def main():     while True:         display_menu()         choice = input("Введите номер действия (0-3): ")          if choice == "1":             install_backup_system()         elif choice == "2":             remove_backup_system()         elif choice == "3":             view_backup_system_info()         elif choice == "0":             print("Выход из программы.")             break         else:             print("Некорректный ввод. Пожалуйста, выберите действие снова.")  if __name__ == "__main__":     main() 

Функция main() представляет собой основной исполняемый блок программы, который обеспечивает взаимодействие пользователя с основными функциональными частями программы. Включает в себя бесконечный цикл, который предлагает пользователю выбор действия из меню и вызывает соответствующую функцию в зависимости от выбора.

Шаги выполнения:

  1. Цикл выбора:
    • Запускается бесконечный цикл, который предоставляет пользователю выбор из меню.
  2. Отображение меню:
    • В каждой итерации цикла вызывается функция display_menu(), которая отображает пользователю меню с возможными действиями.
  3. Выбор пользователя:
    • Пользователю предлагается ввести номер действия из меню (0 — выход, 1 — установка системы резервного копирования, 2 — удаление системы, 3 — просмотр информации).
    • Происходит считывание ввода пользователя (input()).
  4. Обработка выбора:
    • В зависимости от выбора пользователя выполняется соответствующая ветвь условия (if-elif-else).
    • Если выбрано 1, вызывается функция install_backup_system().
    • Если выбрано 2, вызывается функция remove_backup_system().
    • Если выбрано 3, вызывается функция view_backup_system_info().
    • Если выбрано 0, выводится сообщение о выходе, и цикл завершается (break).
  5. Некорректный ввод:
    • Если введенный номер действия не соответствует ни одному из вариантов, выводится сообщение о некорректном вводе, и цикл продолжается.

Завершение программы:

  • Если программа запущена напрямую (не импортирована как модуль), то выполнится последняя строка кода: if __name__ == "__main__": main(), что приведет к вызову функции main().

Окончание

Реализованное решение позволяет делать резервную копию СУБД в кластере Kubernetes. То что я здесь написал в целом достаточно спорно с какой стороны не посмотри. Но я искал подобное решение для проекта и несмотря на все минусы, оно справляется с поставленной задачей. Можно бесконечно улучшать его, например добавив в код проверки для каждой функции, нормально объединив Helm Charts и тд. Но как собранное в кратчайшие сроки, это решение имеет право на жизнь.


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


Комментарии

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

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