Pull to refresh

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

Level of difficultyMedium
Reading time27 min
Views7.1K


Привет! У каждого из нас бывает что после какой-то задачи, ты хочешь чем-то поделиться. Но зачастую мотивации хватает только на поделиться в рамках внутреннего 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 и тд. Но как собранное в кратчайшие сроки, это решение имеет право на жизнь.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 5: ↑3 and ↓2+1
Comments9

Articles