Сервер

На нашем сервере реализована система скриптов, написанных на Bash, которая позволяет произвести чистую установку всей инфраструктуры из Git репозиториев, в которых хранятся соответствующие скрипты установки и конфигурации Docker контейнеров.

Главная цель разработки — конфигурация сервера, которая должна представлять собой строго структурированную задокументированную систему, по принципу «сервер как код». Такая система дает возможность в случае любых нештатных ситуаций быстро перенести или восстановить всю инфраструктуру с нуля.

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

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

Требования

  • VPS cервер с чистой установкой Ubuntu Server 24.04 LTS
  • Локальный сервер разработки «dev», с которого будет выполняться первоначальный запуск и конфигурация. Кроме того, здесь может осуществляться резервное копирование с удаленных серверов.
  • Удаленный сервер репозиториев «origin». Здесь находится хранилище Git, а так же зашифрованное хранилище секретов.
  • Дополнительно используется сервер бэкапов, с которого при помощи rsync, запускаемого регулярно по cron, делаются резервные копии прочих серверов.

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

Описание основной структуры

Главная идея системы — набор максимально независимых друг от друга модулей, которые могут быть сконфигурированы на работу как единая система.

Основные принципы, которые делают систему единой:

  • Все модули находятся в директории /data
  • Локальные резервные копии данных модулей сохраняются в директорию /backups
  • Все секреты и конфигурация хранятся централизованно в папке /data/secrets
  • Данные всех модулей хранятся централизованно в директории /data/appdata
  • В папке с секретами /data/secrets конфигурация сохраняется в папку с названием хоста сервера. Это может быть доменное имя или IP адрес. Такой подход позволяет поддерживать несколько конфигураций для различных серверов при разработке на dev сервере.
  • В директории /data/utils находятся общие для всех модулей вспомогательны скрипты. Их список будет приведен позже.
  • В директории /data/install содержатся скрипты, использующиеся для установки системы.
  • Конфигурация каждого отдельного сервера определяется отдельным модулем, так же расположенным в папке /data. Имена этих модулей начинаются с префикса server-*. Например, на данный момент у меня в имеется четыре отдельные конфигурации: server-checkerwars, server-dev, server-origin, server-backup. Каждая такая конфигурация содержит в себе основной скрипт установки install.sh, директорию config (например, здесь располагается список заданий crontab cron.list), специфические для конкретного сервера скрипты, а так же README.md, в котором содержится описание системы.

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

В данном модуле имеются следующие основные компоненты:

  • init-server — первоначальная инициализация сервера
  • init-docker — установка Docker на Ubuntu 24.04 LTS
  • init-db — автоматизация создания базы данных в Docker контейнере с PostgreSQL
  • env-gen — добавление переменных в общую конфигурацию .env
  • global-env — добавление глобальных переменных на сервер
  • cron-list — добавление Cron-заданий из файла
  • ssh-port — изменение SSH порта
  • ssh-pw — отключение/включение входа по паролю через SSH

Описание работы

Давайте рассмотрим установку системы на чистый VPS сервер с доменным именем checkerwars.com

  1. Процесс установки начинается с запуска на локальном сервере инициирующего скрипта install-checkerwars.sh, находящегося в папке /data/server-checkerwars/dev.install:
Bash
#!/bin/bash

if [ "$(id -u)" != "0" ]; then
    echo -e "\033[31mThis script requires superuser rights.\033[0m"
    exit 0
fi

DEV_ENV="/data/secrets/dev.env"
INSTALL_DIR="/data/install"

trap 'echo -e "\033[31minstall-checkerwars.sh: Something went wrong\033[0m"; exit 1' ERR
set -e

export DEBIAN_FRONTEND=noninteractive

source $DEV_ENV

bash $INSTALL_DIR/remote.sh $SERVER_BEGETCW_HOST $SERVER_BEGETCW_PASSWORD

trap - ERR
echo "server-origin install complete"


В данном скрипте прописываются уникальные для каждого сервера переменные, которые задают параметры для подключения к удаленному серверу. Переменные с данными для доступа ко всем серверам находятся в одном файле /data/secrets/dev.env. Эти данные хранятся только на локальном сервере разработки, а после установки используется SSH доступ с использованием файлов ключей, а парольный доступ отключается.

В конечном счете, выполнения скрипта сводится к запуску другого локального скрипта /data/install/remote.sh с параметрами хоста и пароля:

Bash
#!/bin/bash

if [ "$(id -u)" != "0" ]; then
    echo -e "\033[31mThis script requires superuser rights.\033[0m"
    exit 0
fi

if [ -z "$1" ] || [ -z "$2" ]; then
  echo "Please provide SERVER_HOST & SERVER_PASSWORD as arguments"
  exit 1
fi

SSH_PORT=22
REMOTE_USER="root"
SERVER_HOST=$1
SERVER_PASSWORD=$2
SECRETS_DIR=/data/secrets
SECRETS_ENV=$SECRETS_DIR/$SERVER_HOST/$SERVER_HOST.env

DEV_ENV=$SECRETS_DIR/dev.env

source $SECRETS_ENV
source $DEV_ENV

SECRETS_SAFE=$SECRETS_DIR/safe
SECRETS_PATH=$SECRETS_SAFE/$SERVER_HOST.tar.gz

KEYS_FILE="$SECRETS_SAFE/keys.env"

trap 'echo -e "\033[31mremote.sh: Something went wrong\033[0m"; exit 1' ERR
set -e

export DEBIAN_FRONTEND=noninteractive

echo "Get SAFE_PASSWORD for decrypt secrets archive on target host..."
KEY_VAR=$(echo "$SERVER_HOST" | tr '.' '_')

if grep -q "^$KEY_VAR=" "$KEYS_FILE"; then
  SAFE_PASSWORD=$(grep "^$KEY_VAR=" "$KEYS_FILE" | cut -d '=' -f2)
else
  echo "SAFE_PASSWORD for $SERVER_HOST not found"
fi

cd /data/install

sudo bash secrets-push.sh $SERVER_HOST

echo "Reset known_hosts..."
ssh-keygen -f '/root/.ssh/known_hosts' -R $SERVER_HOST

echo "Install public SSH key from dev host to target host..."
sshpass -p $SERVER_PASSWORD ssh-copy-id -i ~/.ssh/id_ed25519.pub -p $SSH_PORT -o StrictHostKeyChecking=no $REMOTE_USER@$SERVER_HOST

echo "Copy install script from dev host to target host in tmp dir..."
scp -P $SSH_PORT install.sh $REMOTE_USER@$SERVER_HOST:/tmp/install.sh

echo "Copy secrets-decrypt script from dev host to target host in tmp dir..."
scp -P $SSH_PORT secrets-decrypt.sh $REMOTE_USER@$SERVER_HOST:/tmp/secrets-decrypt.sh

echo "Create secrets directory on target host..."
ssh -p $SSH_PORT $REMOTE_USER@$SERVER_HOST "mkdir -p $SECRETS_SAFE"

echo "Copy secret archive from origin host to target host..."
ssh $REMOTE_USER@$SERVER_ORIGIN_DOMAIN "cat $SECRETS_PATH" | ssh $REMOTE_USER@$SERVER_HOST -p $SSH_PORT "cat > $SECRETS_PATH"

echo "Run secrets-decrypt script for decrypt secrets archive on target host..."
ssh -p $SSH_PORT $REMOTE_USER@$SERVER_HOST "bash /tmp/secrets-decrypt.sh $SAFE_PASSWORD $SECRETS_PATH $SECRETS_DIR/$SERVER_HOST"

echo "Run install script on target host..."
ssh -p $SSH_PORT $REMOTE_USER@$SERVER_HOST "bash /tmp/install.sh $SERVER_HOST $SERVER_ORIGIN_PASSWORD $GITEA_API_ADD_SSH_KEY"

trap - ERR
echo "Remote install complete"


Этот скрипт выполняет целый набор важных подготовительных операций:

  1. Проверка прав пользователя: Скрипт проверяет, запущен ли он от имени суперпользователя (root). Если это не так, выводится сообщение об ошибке, и скрипт завершает выполнение.
  2. Проверка входных аргументов: Скрипт проверяет, были ли предоставлены SERVER_HOST и SERVER_PASSWORD в качестве аргументов. Если они отсутствуют, выводится сообщение об ошибке, и скрипт завершает выполнение.
  3. Инициализация переменных: Устанавливаются значения по умолчанию для SSH_PORT, REMOTE_USER, и пути к различным файлам и каталогам, включая пути к файлам окружения и секретам.
  4. Загрузка конфигураций: Два файла с переменными окружения (SECRETS_ENV и DEV_ENV) загружаются в текущий сеанс.
  5. Настройка обработчика ошибок: Устанавливается обработчик ошибок, который выводит сообщение об ошибке, если что-то идет не так, и завершает выполнение скрипта.
  6. Настройка тихого режима установки пакетов: Устанавливается переменная DEBIAN_FRONTEND в значение noninteractive для установки пакетов без взаимодействия с пользователем.
  7. Получение SAFE_PASSWORD: Определяется переменная SAFE_PASSWORD для расшифровки архива секретов на целевом хосте. Если пароль не найден в файле KEYS_FILE, выводится предупреждение.
  8. Переход в директорию /data/install: Переход в каталог, где размещены файлы для установки.
  9. Запуск скрипта secrets-push.sh: Вызов внешнего скрипта для передачи секретов на целевой сервер.
  10. Сброс ключей SSH: Удаление записи о целевом сервере в файле known_hosts, чтобы избежать предупреждений о смене ключей.
  11. Установка публичного ключа SSH на сервере: Использование ssh-copy-id для передачи публичного ключа SSH на целевой сервер.
  12. Копирование установочного скрипта: Передача файла install.sh на целевой сервер в каталог /tmp.
  13. Копирование скрипта для расшифровки: Передача файла secrets-decrypt.sh на целевой сервер в каталог /tmp.
  14. Создание директории для секретов на сервере: Создание указанного каталога для хранения секретов на целевом сервере.
  15. Копирование архива секретов: Передача архива секретов с исходного сервера на целевой сервер.
  16. Расшифровка архива секретов: Запуск скрипта secrets-decrypt.sh на целевом сервере для расшифровки переданного архива.
  17. Запуск установочного скрипта: Выполнение скрипта install.sh на целевом сервере для завершения процесса установки.
  18. Удаление обработчика ошибок: Сброс установленного ранее обработчика ошибок.
  19. Вывод сообщения об успешном завершении: Отображение сообщения, указывающего, что установка на удаленном сервере завершена.

После выполнения скрипта выше, процесс установки уже происходит полностью на удаленном сервере, где будет запущен install.sh:

Bash
#!/bin/bash

if [ "$(id -u)" != "0" ]; then
    echo -e "\033[31mThis script requires superuser rights.\033[0m"
    exit 0
fi

if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
  echo "Please provide SERVER_HOST SERVER_ORIGIN_PASSWORD GITEA_API_ADD_SSH_KEY as arguments"
  exit 1
fi

SERVER_HOST=$1
SERVER_ORIGIN_PASSWORD=$2
GITEA_API_ADD_SSH_KEY=$3

REMOTE_USER="root"
SECRETS_PATH=/data/secrets/$SERVER_HOST/$SERVER_HOST.env

trap 'echo -e "\033[31minstall.sh: Something went wrong\033[0m"; exit 1' ERR
set -e

export DEBIAN_FRONTEND=noninteractive

source $SECRETS_PATH

echo "Keygen SSH keys for $SERVER_NAME..."
if [ -f ~/.ssh/id_ed25519 ]; then
  echo "SSH key already exists: ~/.ssh/id_ed25519"
else
  ssh-keygen -t ed25519 -C "$SERVER_NAME" -f ~/.ssh/id_ed25519 -N ""
fi

echo "Install sshpass..."
apt-get update
apt-get install -y sshpass

echo "Install public SSH key from HOST to ORIGIN..."
sshpass -p $SERVER_ORIGIN_PASSWORD ssh-copy-id -i ~/.ssh/id_ed25519.pub -o StrictHostKeyChecking=no $REMOTE_USER@$SERVER_ORIGIN_DOMAIN


echo "Add public SSH key to gitea app..."
SSH_PUBLIC_KEY=$(cat ~/.ssh/id_ed25519.pub)

echo "Get SSH keys list from gitea API..."
keys=$(curl -X GET -H "Authorization: token $GITEA_API_ADD_SSH_KEY" "https://$GITEA_DOMAIN/api/v1/user/keys")

echo "Search SSH key in gitea..."
existing_key_id=$(echo $keys | jq -r ".[] | select(.title == \"$SERVER_NAME\") | .id")

echo "If already exist SSH key in gitea, remove it..."
if [ -n "$existing_key_id" ]; then
  curl -X DELETE -H "Authorization: token $GITEA_API_ADD_SSH_KEY" "https://$GITEA_DOMAIN/api/v1/user/keys/$existing_key_id"
  echo "SSH key removed"
fi

echo "Add new SSH public key in gitea..."
curl -X POST \
    "https://$GITEA_DOMAIN/api/v1/user/keys" \
    -H "Authorization: token $GITEA_API_ADD_SSH_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"title\": \"$SERVER_NAME\",
      \"key\": \"$SSH_PUBLIC_KEY\"
    }"


echo "Install utils from gitea origin..."
cd /data
echo "Connect to git repository on host: $GIT_SSH_DOMAIN"
echo "On username: $GIT_USER"
git clone git@"$GIT_SSH_DOMAIN":"$GIT_USER"/utils.git
cd /data/utils


echo "Run init-server script..."
bash init-server.sh

echo "create global environment vars..."
bash global-env.sh SERVER_DOMAIN $SERVER_HOST
bash global-env.sh ADMIN_EMAIL $ADMINEMAIL
bash global-env.sh SRV_START_DIR $SERVER_NAME

echo "Install $SERVER_NAME from gitea origin..."
cd /data
echo "Connect to git repository on host: $GIT_SSH_DOMAIN"
echo "On username: $GIT_USER"
git clone git@"$GIT_SSH_DOMAIN":"$GIT_USER"/$SERVER_NAME.git
cd /data/$SERVER_NAME

echo "Run $SERVER_NAME install.sh..."
bash /data/$SERVER_NAME/install.sh $SERVER_HOST

trap - ERR
echo "Install complete. Please exit and login again on port $SSH_PORT with ssh key "


Этот скрипт выполняет целый набор важных подготовительных операций:

  1. Проверка прав суперпользователя: Скрипт проверяет, запущен ли он от имени суперпользователя (root).
  2. Проверка аргументов: Проверяется наличие необходимых аргументов SERVER_HOST, SERVER_ORIGIN_PASSWORD и GITEA_API_ADD_SSH_KEY.
  3. Установка значений переменных: SERVER_HOST, SERVER_ORIGIN_PASSWORD и GITEA_API_ADD_SSH_KEY устанавливаются как переданные аргументы.
  4. Определение переменных: Устанавливаются значения для REMOTE_USER и SECRETS_PATH.
  5. Установка ловушки для ошибок: Определяется обработчик ошибок, который будет исполняться при возникновении ошибок.
  6. Настройка тихого режима установки пакетов: Устанавливается переменная DEBIAN_FRONTEND в значение noninteractive для установки пакетов без взаимодействия с пользователем.
  7. Загрузка секретов: Из файла, расположенного по пути, указанному в переменной SECRETS_PATH, загружаются необходимые данные.
  8. Генерация SSH-ключей: Если SSH-ключ отсутствует, он создается.
  9. Установка sshpass: Устанавливается утилита sshpass.
  10. Копирование публичного ключа на удаленный сервер: Публичный SSH-ключ копируется на сервер.
  11. Добавление публичного SSH-ключа в приложение Gitea:
    a. Получение существующих SSH-ключей из Gitea;
    b. Поиск существующего ключа по названию SERVER_NAME;
    c. Удаление существующего SSH-ключа, если он имеется;
    d. Добавление нового SSH-ключа.
  12. Клонирование репозитория utils: Переход в каталог /data, клонирование репозитория приложения полезных утилит.
  13. Исполнение скрипта init-server.sh: Запуск скрипта инициализации сервера.
  14. Создание глобальных переменных окружения: Создание необходимых переменных окружения для сервера.
  15. Клонирование основного репозитория сервера: Переход в каталог /data, клонирование основного репозитория по его названию.
  16. Исполнение скрипта установки: Запуск скрипта install.sh для развертывания сервера.
  17. Завершение работы скрипта: Снятие обработчика ошибок и вывод сообщения об успешной установке.

После выполнения скрипта выше, процесс установки основной конфигурации завершен, и далее производится установка и конфигурирование специфичных для конкретного сервера модулей:

Bash
#!/bin/bash

STARTDIR=server-origin

if [ "$(id -u)" != "0" ]; then
    echo -e "\033[31mThis script requires superuser rights.\033[0m"
    exit 0
fi

if [ -z "$1" ]; then
  echo "Please provide the DOMAIN_NAME as an argument"
  exit 1
fi

trap 'echo -e "\033[31minstall.sh: Something went wrong\033[0m"; exit 1' ERR
set -e

export DEBIAN_FRONTEND=noninteractive

DOMAIN=$1
SECRETS_PATH=/data/secrets/$DOMAIN/$DOMAIN.env
source $SECRETS_PATH

cd /data/utils
echo "Install docker..."
bash init-docker.sh

echo "Init cron list..."
sudo bash cron-list.sh

echo "Clone backup.git..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/backup.git

echo "Init traefik..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/traefik.git
cd traefik
sudo bash install.sh

echo "Init postgres..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/postgres.git
cd postgres
sudo bash install.sh

echo "Init pgadmin..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/pgadmin.git
cd pgadmin
sudo bash install.sh

echo "Init gitea..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/gitea.git
cd gitea
sudo bash install.sh

echo "Init mariadb..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/mariadb.git
cd mariadb
sudo bash install.sh


echo "Init checkers-backend..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/checkers-backend.git
cd checkers-backend
sudo bash install.sh
sudo bash run.prod.sh

echo "Init checkers-frontend..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/checkers-frontend.git
cd checkers-frontend
sudo bash run.prod.sh


echo "Init messenger-backend..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/messenger-backend.git
cd messenger-backend
sudo bash install.sh
sudo bash run.prod.sh

echo "Init messenger-frontend..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/messenger-frontend.git
cd messenger-frontend
sudo bash run.prod.sh


echo "Install blog-wp..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/blog-wp.git
cd blog-wp
sudo docker compose up -d

echo "Await 10s before blog-wp-install..."
sleep 10

echo "Install blog-wp-install..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/blog-wp-install.git
cd blog-wp-install
sudo bash install.sh

echo "Install tetris..."
cd /data
git clone git@$GIT_SSH_DOMAIN:$GIT_USER/tetris.git
cd tetris
sudo docker compose build
sudo docker compose up -d

trap - ERR
echo "Install server-origin complete"


Скрипт выполняет следующие операции:

  1. Устанавливается переменная STARTDIR со значением server-origin.
  2. Проверяется, что скрипт запускается с правами суперпользователя. Если нет, отображается сообщение об ошибке и скрипт завершает работу.
  3. Проверяется, передан ли аргумент DOMAIN_NAME при запуске скрипта. Если нет, отображается сообщение об ошибке и скрипт завершает работу.
  4. Устанавливается ловушка на случай ошибки, выводящая сообщение об ошибке и завершающая скрипт.
  5. Настройка тихого режима установки пакетов: Устанавливается переменная DEBIAN_FRONTEND в значение noninteractive для установки пакетов без взаимодействия с пользователем.
  6. Из командной строки извлекается доменное имя и сохраняется в переменную DOMAIN.
  7. Формируется путь до файла с секретами для данного домена и загружается содержимое этого файла.
  8. Переход в директорию /data/utils.
  9. Запускается скрипт init-docker.sh для установки Docker.
  10. Запускается скрипт cron-list.sh для инициализации списка задач cron.
  11. Клонируется репозиторий backup.git в директорию /data.
  12. Клонируется репозиторий traefik.git, начальная установка выполняется через скрипт install.sh.
  13. Клонируется репозиторий postgres.git, запускается скрипт install.sh.
  14. Клонируется репозиторий pgadmin.git, запускается скрипт install.sh.
  15. Клонируется репозиторий gitea.git, запускается скрипт install.sh.
  16. Клонируется репозиторий mariadb.git, запускается скрипт install.sh.
  17. Клонируется репозиторий checkers-backend.git, выполняется установка и запуск через соответствующие скрипты.
  18. Клонируется репозиторий checkers-frontend.git, запускается сценарий для production-окружения.
  19. Клонируется репозиторий messenger-backend.git, выполняется установка и запуск через соответствующие скрипты.
  20. Клонируется репозиторий messenger-frontend.git, запускается сценарий для продакшн-окружения.
  21. Клонируется репозиторий blog-wp.git, инициализируется через Docker.
  22. Осуществляется задержка на 10 секунд.
  23. Клонируется репозиторий blog-wp-install.git, запускается скрипт install.sh.
  24. Клонируется репозиторий tetris.git, инициализируется и запускается через Docker.
  25. Снимаются ловушки ошибок.
  26. Скрипт выводит сообщение о завершении установки server-origin.

Большинство указанных выше в скрипте репозтиториев можно найди здесь. На некоторые из них у нас есть описание в отдельных статьях.



Если вам интересен наш проект, есть вопросы, замечания, или предложения — оставляйте комментарии или пишите на почту: checkerwars@mail.ru

Кроме того, автор проекта ищет работу. Мое резюме.