![Это - логотип Bash оболочки. Она сама и bash скрипт - это разные вещи. Это - логотип Bash оболочки. Она сама и bash скрипт - это разные вещи.](https://habrastorage.org/getpro/habr/upload_files/ec4/846/bba/ec4846bbad3ec0a1071e3d2c5d54dfae.jpg)
Приветствую, это Денис из команды 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
, который "утилизирует" вывод - если он Вам нужен, этот параметр необходимо убрать. Параметр -а
служит для добавления информации в конец файла без полной перезаписи. Ну а параметр <<EOL
говорит команде о том, что она должна записать в файл всё, что находится до символов "EOL"
# Шаг 2: Создание юнита для запуска селери
if [[ $CELERY = true ]]; then
sudo tee /etc/systemd/system/celery_$PROJECTNAME.service > /dev/null <<EOL
[Unit]
Description=Celery instance to serve $PROJECTNAME
After=network.target
After=$SERVICE_NAME.service
[Service]
User=$USER
WorkingDirectory=$PROJECTFOLDER/src
ExecStart=$PROJECTFOLDER/bin/start_celery.sh
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOL
fi
После записи новых юнитов, необходимо перечитать их как указано в 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 <<EOL
server {
listen 80;
listen [::]:80;
root /var/www/html;
server_name $SERVERNAME;
location /static/ {
autoindex on;
root $PROJECTFOLDER/src;
}
location /media/ {
autoindex on;
root $PROJECTFOLDER/src;
}
location / {
proxy_pass http://127.0.0.1:8005;
proxy_set_header X-Forwarded-Host \$server_name;
proxy_set_header X-Real-IP \$remote_addr;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
add_header Access-Control-Allow-Origin *;
}
}
EOL
sudo systemctl restart nginx
Далее всё просто. Если Вы скопируете мой код из репозитория, то в папке "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>|$PROJECTFOLDER|g" ./bin/start_gunicorn.sh
sed -i "s|<projectfolder>|$PROJECTFOLDER|g" ./bin/start_celery.sh
sed -i "s|<projectfolder>|$PROJECTFOLDER|g" ./src/gunicorn_config.py
sed -i "s|<user>|$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.
В данной статье я затрагиваю тему системных юнитов (демонов), но не распространяюсь. На этот счет на Хабре есть статьи, я лично читал эту. А целиком принцип именования и расположения файлов, а также способ развертывания позаимствован у неповторимого Алексея Голобурдина, конкретнее из этого видео.
Спасибо за внимание, надеюсь, статья была для Вас полезна :) Оставляйте отзывы, критику и пожелания в комментариях. Приятного дня :)
Комментарии (6)
alexxz
08.08.2024 07:38+6Приведённая автоматизация несколько похожа на форменное вредитльство в мире автоматизации. Идея хороша, а исполлнение плохое.
В частности - давайте представим, что какая-нибудь команда из последовательности завершилась с ошибкой. Следующие команды можно безопасно запускать? А ведь они будут запущены.
Или кто-то опечатался в имени переменной и вместо переменной молча подставилась пустая строка. Это - ожидаемое поведение? И надо запускать такую команду?
Или давайте представим, что кто-то подставил пробел или символ | в PROJECTNAME или PROJECTFOLDER в ваших скриптах всё правильно отработает?
Или кто-то использовал bash pipes не зная, что при падении первой команды, вы об этом не узнаете. Смотри про опцию pipefail.
Баш - классный язык, но он не полный и, на мой взгляд, не вполне переносимый между платформами. Если пишешь разовый скриптик под кокретную задачу и ошибки отсмотришь глазами - да. Так можно.
А вот в фоновые процессы Баш стоит писать только прочитав внимательно Bash pitfalls https://mywiki.wooledge.org/BashPitfalls и дописывая всегда в начале файла set -euo pipefail и кавычки к подстановкам переменных
BagrovChibirev Автор
08.08.2024 07:38Большое спасибо за Ваш полезный комментарий. Я, в целом, и не претендую на истину в первой инстанции мира автоматизации на баш, и написал статью, которую было бы самому полезно прочитать на тех этапах, когда я только начинал такой автоматизацией заниматься.
Полезны и нтересны Ваши замечания про " ","|" в названиях переменных, о чём я не подумал, а также всё про pipefail, а про безопасный запуск команд при падении я отвечу:
По сути, ничего ужасного с системой этот скрипт не делает, и если где-то он упадёт, то дальнейшие команды, например, включение юнита, если его запись упала (допустим, из-за отсутствий прав sudo), то systemctl сам отпишется об ошибке, которую будет видно в консоли, и это можно будет в дальнейшем отработать. Да и сам я намеренно запускал скрипт с ошибками, чтобы убедиться в его "безопасности", если использовать его по прямому назначению без модификаций :)
Вообще, данный скрипт я предоставил для ознакомления и доработки, ведь он не предназначен для непосредственного использования вне определенных условностей архитектуры построения проекта, которую навязывает Django, например, и подход к настройке gunicorn из примера упомянутого в статье "Диджитализируй"
feelamee
08.08.2024 07:38что вы предпочитаете для более серьезной автоматизации?
BagrovChibirev Автор
08.08.2024 07:38При развертывании проектов - хорошо настроенный Docker. Для всего остального на сервере - systemd, bash и cron.
Хотя тут можно поспекулировать на тему что такое "более серьёзная автоматизация", Вы можете раскрыть тему и рассказать, что думаете, хотя мне кажется, что это будет не в тематику данного поста.
motoroller95
ansible?
monpa
bashsible)