Это - логотип Bash оболочки. Она сама и 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, который "утилизирует" вывод - если он Вам нужен, этот параметр необходимо убрать. Параметр служит для добавления информации в конец файла без полной перезаписи. Ну а параметр <<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)


  1. motoroller95
    08.08.2024 07:38
    +1

    ansible?


    1. monpa
      08.08.2024 07:38
      +4

      bashsible)


  1. alexxz
    08.08.2024 07:38
    +6

    Приведённая автоматизация несколько похожа на форменное вредитльство в мире автоматизации. Идея хороша, а исполлнение плохое.

    В частности - давайте представим, что какая-нибудь команда из последовательности завершилась с ошибкой. Следующие команды можно безопасно запускать? А ведь они будут запущены.

    Или кто-то опечатался в имени переменной и вместо переменной молча подставилась пустая строка. Это - ожидаемое поведение? И надо запускать такую команду?

    Или давайте представим, что кто-то подставил пробел или символ | в PROJECTNAME или PROJECTFOLDER в ваших скриптах всё правильно отработает?

    Или кто-то использовал bash pipes не зная, что при падении первой команды, вы об этом не узнаете. Смотри про опцию pipefail.

    Баш - классный язык, но он не полный и, на мой взгляд, не вполне переносимый между платформами. Если пишешь разовый скриптик под кокретную задачу и ошибки отсмотришь глазами - да. Так можно.

    А вот в фоновые процессы Баш стоит писать только прочитав внимательно Bash pitfalls https://mywiki.wooledge.org/BashPitfalls и дописывая всегда в начале файла set -euo pipefail и кавычки к подстановкам переменных


    1. BagrovChibirev Автор
      08.08.2024 07:38

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

      Полезны и нтересны Ваши замечания про " ","|" в названиях переменных, о чём я не подумал, а также всё про pipefail, а про безопасный запуск команд при падении я отвечу:

      По сути, ничего ужасного с системой этот скрипт не делает, и если где-то он упадёт, то дальнейшие команды, например, включение юнита, если его запись упала (допустим, из-за отсутствий прав sudo), то systemctl сам отпишется об ошибке, которую будет видно в консоли, и это можно будет в дальнейшем отработать. Да и сам я намеренно запускал скрипт с ошибками, чтобы убедиться в его "безопасности", если использовать его по прямому назначению без модификаций :)

      Вообще, данный скрипт я предоставил для ознакомления и доработки, ведь он не предназначен для непосредственного использования вне определенных условностей архитектуры построения проекта, которую навязывает Django, например, и подход к настройке gunicorn из примера упомянутого в статье "Диджитализируй"


    1. feelamee
      08.08.2024 07:38

      что вы предпочитаете для более серьезной автоматизации?


      1. BagrovChibirev Автор
        08.08.2024 07:38

        При развертывании проектов - хорошо настроенный Docker. Для всего остального на сервере - systemd, bash и cron.

        Хотя тут можно поспекулировать на тему что такое "более серьёзная автоматизация", Вы можете раскрыть тему и рассказать, что думаете, хотя мне кажется, что это будет не в тематику данного поста.