Простая автоматизация с Bash для новичков
Это — логотип Bash оболочки. Она сама и bash скрипт — это разные вещи.
Приветствую, это Денис из команды BagrovChibirev, и в статье я на простом примере расскажу об автоматизации процессов в Linux с помощью bash скриптов (сценариев командной строки).
Этот материал для тех, кто только рассматривает для себя инструменты автоматизации рутинных процессов. Я не буду вдаваться в подробности работы оболочки или терминологию (на знания чего и не претендую), но я пошагово пройдусь по написанному скрипту и расскажу своё мнение почему вообще их стоит использовать. Приятного чтения :)
Рассматривать я буду свой минималистичный скрипт для разворачивания простого python Django проекта при помощи системных юнитов (демонов) на удалённом сервере. Для тех, кто не в курсе: демоны — это специальные системные сервисы, которые следят за состоянием сторонних процессов и поддерживают их работоспособность. В современном мире для таких целей на микросервисах применяется Docker, но когда проект небольшой и состоит из пары-тройки процессов, их намного легче, проще и дешевле для системы (в разы), развернуть при помощи встроенных в линукс демонов.
Полностью скрипт доступен здесь
Начнём с того, что баш скрипт доступен почти всегда: Bash предустановлен на большинстве машин с Linux. Я использую скрипты на удалённых VPS, где в большинстве случаев используется либо Ubuntu 18+, либо Debian 10+, и, зайдя в систему, очень удобно просто закинуть в директорию сценарий, который произведет действия по обновлению, установке необходимого ряда пакетов, настройке пользователей и ssh доступов сам, без необходимости в очередной раз лезть туда руками.
Сам по себе язык сценариев bash очень прост, и исполняется построчно, что даёт систему управления похожую на скрипты Python, или просто последовательное выполнение команд в терминале оболочки сервера, если в скрипте нет ветвления или отработки ошибок.
Чтобы запустить такой сценарий, необходимо его непосредственно написать или импортировать любым образом в нужную Вам директорию, и задать ему права на исполнение. Начинается почти любой скрипт с объявления «шебанга»: специальных символов »#!
», за которыми следует путь к «интерпретатору», который будет использоваться для выполнения этого сценария.
#!/bin/bash
В целом, для оболочки bash эта строка необязательна, но только в случаях, когда Вы явно указываете «интерпретатор», например, выполняя файл через команду ниже. Также работает, например, когда Вы выполняете программы на Python, явно вызывая его в качестве интерпретатора для сценария.
bash ./startup.sh
python -m ./startup.py
Но всё же, поскольку скрипт может быть вызван из другого места, или без использования явного указания оболочки, начинается он с шебанга и указания пути до оболочки bash.
Далее, поскольку скрипт создаёт несколько текстовых файлов с переменными, которые могут разниться от клиента к клиенту, я решил сделать обработку входящих аргументов. И поскольку аргументы могут быть переданы неправильно, мне также нужна отработка ошибок, для чего я написал функцию.
Ниже сразу я объявил функцию usage (), объявлять функции можно с круглыми скобками и без, но фигурные скобки после названия обязательны. Применение этой функции делает вывод помощи в консоль и прерывает дальнейшее выполнение.Важно понимать, что, поскольку Bash читает файл построчно, то вызов функции ДО её объявления вызовет уведомление об ошибке (скрипт продолжит работать), а переопределение функции полностью перезапишет её.
#!/bin/bash
# Принт справки помощи
usage() {
echo "Usage: $0 -p projectname [-s servername] [-c] [-help]"
echo " -s servername Set the server name (default: project)"
echo " -p projectname Set the project name"
echo " -c Include Celery service setup"
echo " -h Print this help message"
exit 1
}
Далее мне необходимо задать именованные параметры, которые будут использоваться ниже в коде, что я сделал через присвоение, и «встроенную» функцию getopts
. Bash распознаёт различные инструменты языка при помощи пробелов и табуляции, поэтому здесь важно где Вы ставите пробел, а где нет. Например, CELERY=false
— это присвоение, а вот CELERY = false
— это уже сравнение.Ну, а обработка аргументов происходит при помощи оператора while
, который проходится по всем вариантам функции getopts
из заданного фиксированного списка. getopts
перебирает переданный список аргументов и передаёт аргумент с его значением (если его нет, но он указан, то попадёт true
) в переменную. В данном случае, в $opt
попадает сама «переменная» аргумента, а в $OPTARG
его значение.Уже внутри while
, когда в $opt
лежит значение, можно перебрать его с помощью оператора case. В конце, для завершения цикла case ставится оператор esac
(также работает и для if
— fi
), а для завершения while
ставится оператор done
.
# Парсит переданные аргументы
CELERY=false
while getopts "cs:p:" opt; do
case $opt in
c)
CELERY=true
;;
s)
SERVERNAME=$OPTARG
;;
p)
PROJECTNAME=$OPTARG
;;
h)
usage
;;
?)
usage
;;
esac
done
Далее, распарсив аргументы, необходимо удостовериться, что они не пусты, а если и пусты, то передать в них стандартное значение. С полученными итоговыми значениями, скрипт наконец может спокойно заняться основным делом — созданием конфигурационных файлов. А по пути я еще задаю русскую локаль для сервера.
# Объявление переменных для подстановки
PROJECTFOLDER=$(pwd)
PROJECTNAME=${PROJECTNAME:-project}
SERVERNAME=${SERVERNAME:-_}
# Шаг 0: Конфигурация русской локали
if ! grep -q '^ru_RU.UTF-8' /etc/locale.gen; then
echo "ru_RU locale is not configured. Configuring..."
echo 'ru_RU.UTF-8 UTF-8' | sudo tee -a /etc/locale.gen
sudo locale-gen
echo "ru_RU locale configured successfully."
else
echo "ru_RU locale is already configured."
fi
Поскольку мы не знаем пуст ли аргумент, то мы можем задать стандартное значение для него, произведя манипуляцию, похожую на тернарный оператор в Python. Здесь в его качестве выступает конструкция :-
, стандартная при назначении значений переменных: если параметр не задан, задать ему указанное после оператора значение. Далее, при конфигурации локали и создании файла, используется конструкция if then fi
: в общем случае, указанного перед этим синтаксиса достаточно для того, чтобы выполнять простые условия, но в качестве условий могут быть как выполнены команды (как в случае с локалью), так и произведены вычисления и сравнения.
После объявления условий выполнения хорошим тоном будет поставить ;
, чтобы облегчить интерпретатору понимание кода, и гарантировать отсутствие ошибок типа пропущенного синтаксиса. Существует несколько разных способов объявления условий, и, в примере, использованы два: без скобок и с двойными скобками (подробнее здесь). Скобки можно не отбрасывать вообще, но можно и отбросить, если условие — это выполнение сторонней команды или результат работы функции. Двойные же квадратные скобки служат для того, чтобы обеспечить, простыми словами, более «буквальное» выполнение написанного внутри кода и обойтись без употребления кавычек вокруг переменных.Здесь же, в локали, используется оператор !
, обозначающий эквивалент not
для дальнейшего условия, а команда grep -q
производит поиск заданной строки в заданном затем файле в «тихом» (-q = --quiet = --silent
) режиме — без вывода информации в консоль. Соответственно, если результат выполнения команды — провал, то мы записываем в файл конфигурации локалей строку с необходимой локалью с помщью команды tee -a
, и вызываем их генерацию через sudo
.Чуть больше про tee
. Здесь можно было бы воспользоваться стандартным echo >> file
, но tee
позволяет увидеть в терминале что было записано в файл, поэтому стандартно я пользуюсь ей, хотя в данном случае вывод в консоль перекрыт параметром > /dev/null
, который «утилизирует» вывод — если он Вам нужен, этот параметр необходимо убрать. Параметр -а
служит для добавления информации в конец файла без полной перезаписи. Ну, а параметр <
# Шаг 2: Создание юнита для запуска селери
if [[ $CELERY = true ]]; then
sudo tee /etc/systemd/system/celery_$PROJECTNAME.service > /dev/null <
После записи новых юнитов, необходимо перечитать их как указано в 3 м шаге, за чем следует простая команда на установку nginx (здесь apt-get
сам отработает ситуацию, когда nginx уже установлен), и также через tee
для него прописывается конфиг. Если у вас все ещё стандартный nginx.conf, и порт 80 не занят, всё будет работать. Обратить внимание здесь необходимо на proxy_pass
конфигурации nginx. Он рассчитан на конфигурацию запуска gunicorn, которая у Вас может отличаться. Nginx, также, требует перезапуска.
# Шаг 3: Обновить конфигурацию системного демона
sudo systemctl daemon-reload
# Шаг 4: Установить и обновить конфигурацию Nginx
sudo apt-get install -y nginx
sudo tee /etc/nginx/sites-enabled/$PROJECTNAME > /dev/null <
Далее всё просто. Если Вы скопируете мой код из репозитория, то в папке «bin» у Вас будут лежать два скрипта, которые будут запускаться юнитами, поэтому мы должны выдать им права на выполнение chmod +x
. Затем, устанавливается редис как очередь для селери, и python-venv. (Сейчас вместо стандартного python-venv я рекомендую использовать современный uv или, хотя бы, pip-tools, но поскольку скрипт сделан не только для себя, но и для клиента, и для минимальной реализации, используем python-venv: он всем прост и понятен в установке.)
# Шаг 5: Задание прав на выполненияе для скриптов запуска селери и гуникорна
chmod +x ./bin/*
# Шаг 6: Установка сервера редис (очереди для задач)
sudo apt-get update
sudo apt-get install -y redis-server python3-venv
После этого, скрипт создаёт, активирует, и устанавливает зависимости в окружение. Как можно увидеть, этого я добиваюсь просто прописав одна за другой команды, как я писал в начале, как будто сам нахожусь в терминале оболочки.Шаг 9 нужен для того, чтобы сделать этот скрипт универсальным. Здесь я подставляю полученные в начале переменные в код других скриптов в папках »/bin» и »/src». С помощью команды sed -i
я подставляю значения $PROJECTFOLDER
вместо прямо в потоке чтения (в данном случае, sed
читает из файла) с аттрибутом --in-place
. После чего я просто выполняю пошагово миграции и загружаю фикстуры, а также загружаю и запускаю работу демонов через утилиту systemctl
.
# Шаг 7: создание и запуск виртуального окружения python
python3 -m venv ./venv
source ./venv/bin/activate
# Шаг 8: Устоновка необходимых зависимостей
python -m pip install -r requirements.txt
# Шаг 9: Подстановка пользователя и директории в исполняемые файлы
sed -i "s||$PROJECTFOLDER|g" ./bin/start_gunicorn.sh
sed -i "s||$PROJECTFOLDER|g" ./bin/start_celery.sh
sed -i "s||$PROJECTFOLDER|g" ./src/gunicorn_config.py
sed -i "s||$USER|g" ./src/gunicorn_config.py
# Шаг 10: Запуск миграций джанго на пустую базу данных
cd ./src
python manage.py makemigrations
python manage.py migrate
python manage.py loaddata fixtures/initial.json
python manage.py collectstatic --noinput
# Шаг 11: Запуск проекта с помощью юнита
sudo systemctl enable celery_$PROJECTNAME
sudo systemctl enable $PROJECTNAME
sudo systemctl start $PROJECTNAME
sudo systemctl start celery_$PROJECTNAME
Для корректной работы этого скрипта, ему самому необходимо выдать права на выполнение от пользователя, которым вы являетесь, или от суперпользователя через команду chmod +x ./startup.sh
, и запустить его ./startup.sh
.
Рассчитывается, что этот файл будет лежать на уровень выше корневой директории Вашего Django проекта, на уровне с виртуальным окружением и папкой /bin.
В данной статье я затрагиваю тему системных юнитов (демонов), но не распространяюсь. На этот счет на Хабре есть статьи, я лично читал эту. А целиком принцип именования и расположения файлов, а также способ развертывания позаимствован у неповторимого Алексея Голобурдина, конкретнее из этого видео.
Спасибо за внимание, надеюсь, статья была для Вас полезна :) Оставляйте отзывы, критику и пожелания в комментариях. Приятного дня :)