Время от времени появляется задача: сделать скрипт для публикации, который нужно обновлять но невозможно изменять. Например, это может быть скрипт инициализации, зашитый внутрь образа виртуальной машины или скрипт для установки движка сайта (публикуемый разработчиком движка).
В статье я расскажу о приемах, которые применяю для создания таких скриптов, они помогут избежать некоторых граблей, сохранить простоту и гибкость скриптов. Подход подойдет для тех скриптов, поведение которых должно меняться в зависимости от потребностей автора, обновлений и т.п. Подход НЕ подойдет для скриптов, которые должны работать автономно (без связи с системой автора).
Я использую такой подход в bash-скриптах, но общий принцип можно применять независимо от языка.
Несколько лет назад стал делать шаблоны VDS-серверов для массового использования клиентами. После создания шаблона поменять его уже не получится, т.е. единственный путь исправления ошибки: публикация нового шаблона, что стоит времени и много гигабайтов места (шаблон + его копии на всех серверах). За эти несколько лет получилось совершить несколько неудобных ошибок, в результате теперь еще много лет придется поддерживать шаблоны с неудобным поведением на старте.
Похожая ситуация может получиться и со скриптами установки/настройки панелей управления, движков сайтов, просто программ которые должны по одной команде скачаться, поставиться и настроиться. При том что сама программа может обновляться, а доступа к исправлению скрипта установки у всех кто его скачал раньше уже нет.
Часть из того что я использую было увидено в похожих скриптах, например установщики ispsystem, brew. Часть подсказали коллеги и часть нажита собственным горьким опытом.
Каждый раз загружать и выполнять весь код со своего сервера.
Публикуемый скрипт — только загружает первый файл исполняемого кода, больше ничего не делат.
Первый файл исполняемого кода — сразу делает то что нужно (в простых случаях) или определяет что-то общее и загружает новые файлы
Остальные файлы — организованы любым удобным образом, это можно будет менять уже в процессе работы.
Публикуемый скрипт ни в коем случае не должен выполнять ту задачу для которой он публикуется и даже не должен иметь намёка на её решение. Единственная задача этого скрипта — найти способ подключения к серверу разработчика и скачать оттуда код для выполнения. Этот код должен быть максимально простым с минимумом внешних зависимостей, т.к. их придется поддерживать всё время жизни скрипта.
Этот код я помещаю в шаблон с заранее известным окружением, в частности я точно знаю что там есть curl, а много попыток нужно т.к. при старте сервера сеть может не работать или http-сервер может временно выдавать ошибку. Другие скрипты могут работать в разных окружениях и curl там может не быть. Это хорошее место чтобы попробовать подключиться к серверу разными способами, при необходимости много раз.
Обязательно нужно проверить что код для выполнения загрузился целиком — длинная строка с if делает именно проверку: проверяется что скрипт начинается с #!/bin/bash и заканчивается #BashScriptEnd. Так можно быть уверенным, что не выполнится код HTML-ошибки или полскрипта, обрезав rm -rf /tmp/my-downloads до rm -rf /
Делать более сложные проверки я в этом месте не стал намеренно — TCP в данном случае дает приемлимую защиту от повреждения данных, а любое усложнение внешнего интерфейса потом придется поддерживать вечно.
У этого скрипта только одна внешняя зависимость — URL-адрес. В дальнейшем даже его потом пришлось поменять — при усовершенствовании организации кода, но зависимость настолько простая, что поддерживать её просто. Более того в новых скриптах тоже используется именно этот путь, сложившийся исторически, а не новый «правильный». Потому что в случае изменений в будущем пришлось бы поддерживать уже два URL и т.п.
В скрипте не должно ��ыть никаких попыток определить окружение и загрузить например init_linux.sh или init_freebsd.sh вместо init.sh — такая попытка тоже была, оказалось что это неудобно и теперь приходится поддерживать заглушки для старых версию скриптов.
Тут свободы уже больше его можно будет менять не трогая уже опубликованной части. Так что если всё просто — сюда можно поместит сразу тот код который будет выполняться. Если что-то усложнится — это легко поменять в будущем.
При сложности выполняемого кода больше, чем 1 файл я рекомендую поместить сюда:
1. Функцию для загрузки новых файлов. Она заново определит как именно подключаться к серверу и во всех скриптах нужно использовать именно её. Она не подходит для общей библиотеки, т.к. та еще не загружена.
2. Вызвать эту функцию один/несколько раз для подключения и выполнения всех нужных файлов: общая библиотека кода, специфический код для найденного окружения и т.п. Может быть посмотреть какая команда передана в аргументах и подгрузить код для выполнения этой команды.
В статье я расскажу о приемах, которые применяю для создания таких скриптов, они помогут избежать некоторых граблей, сохранить простоту и гибкость скриптов. Подход подойдет для тех скриптов, поведение которых должно меняться в зависимости от потребностей автора, обновлений и т.п. Подход НЕ подойдет для скриптов, которые должны работать автономно (без связи с системой автора).
Я использую такой подход в bash-скриптах, но общий принцип можно применять независимо от языка.
Короткая предистория (можно пропустить):
Несколько лет назад стал делать шаблоны VDS-серверов для массового использования клиентами. После создания шаблона поменять его уже не получится, т.е. единственный путь исправления ошибки: публикация нового шаблона, что стоит времени и много гигабайтов места (шаблон + его копии на всех серверах). За эти несколько лет получилось совершить несколько неудобных ошибок, в результате теперь еще много лет придется поддерживать шаблоны с неудобным поведением на старте.
Похожая ситуация может получиться и со скриптами установки/настройки панелей управления, движков сайтов, просто программ которые должны по одной команде скачаться, поставиться и настроиться. При том что сама программа может обновляться, а доступа к исправлению скрипта установки у всех кто его скачал раньше уже нет.
Часть из того что я использую было увидено в похожих скриптах, например установщики ispsystem, brew. Часть подсказали коллеги и часть нажита собственным горьким опытом.
Общая суть
Каждый раз загружать и выполнять весь код со своего сервера.
Общая структура кода
Публикуемый скрипт — только загружает первый файл исполняемого кода, больше ничего не делат.
Первый файл исполняемого кода — сразу делает то что нужно (в простых случаях) или определяет что-то общее и загружает новые файлы
Остальные файлы — организованы любым удобным образом, это можно будет менять уже в процессе работы.
Что должен делать и что должен НЕ делать публикуемый скрипт
Публикуемый скрипт ни в коем случае не должен выполнять ту задачу для которой он публикуется и даже не должен иметь намёка на её решение. Единственная задача этого скрипта — найти способ подключения к серверу разработчика и скачать оттуда код для выполнения. Этот код должен быть максимально простым с минимумом внешних зависимостей, т.к. их придется поддерживать всё время жизни скрипта.
Вот код к которому я пришел в итоге
function try()
{
# Выполнить команду, если вернулась ошибка - продолжать пробовать столько раз, сколько указано в первом параметре
... код немного громоздкий, опущен для упрощения
}
# Execute init.sh from panel
function execute_init()
{
local EVAL_CODE=`curl http://panel.1gb.ru/minimal/init.sh`
if [ "${EVAL_CODE#\#\!/bin/bash}" != "$EVAL_CODE" ] && [ "${EVAL_CODE%\#BashScriptEnd}" != "$EVAL_CODE" ]; then
eval "$EVAL_CODE"
return 0
else
return 1
fi
}
try 100000 execute_init
Этот код я помещаю в шаблон с заранее известным окружением, в частности я точно знаю что там есть curl, а много попыток нужно т.к. при старте сервера сеть может не работать или http-сервер может временно выдавать ошибку. Другие скрипты могут работать в разных окружениях и curl там может не быть. Это хорошее место чтобы попробовать подключиться к серверу разными способами, при необходимости много раз.
Обязательно нужно проверить что код для выполнения загрузился целиком — длинная строка с if делает именно проверку: проверяется что скрипт начинается с #!/bin/bash и заканчивается #BashScriptEnd. Так можно быть уверенным, что не выполнится код HTML-ошибки или полскрипта, обрезав rm -rf /tmp/my-downloads до rm -rf /
Делать более сложные проверки я в этом месте не стал намеренно — TCP в данном случае дает приемлимую защиту от повреждения данных, а любое усложнение внешнего интерфейса потом придется поддерживать вечно.
У этого скрипта только одна внешняя зависимость — URL-адрес. В дальнейшем даже его потом пришлось поменять — при усовершенствовании организации кода, но зависимость настолько простая, что поддерживать её просто. Более того в новых скриптах тоже используется именно этот путь, сложившийся исторически, а не новый «правильный». Потому что в случае изменений в будущем пришлось бы поддерживать уже два URL и т.п.
В скрипте не должно ��ыть никаких попыток определить окружение и загрузить например init_linux.sh или init_freebsd.sh вместо init.sh — такая попытка тоже была, оказалось что это неудобно и теперь приходится поддерживать заглушки для старых версию скриптов.
Что поместить в загружаемый скрипт
Тут свободы уже больше его можно будет менять не трогая уже опубликованной части. Так что если всё просто — сюда можно поместит сразу тот код который будет выполняться. Если что-то усложнится — это легко поменять в будущем.
При сложности выполняемого кода больше, чем 1 файл я рекомендую поместить сюда:
1. Функцию для загрузки новых файлов. Она заново определит как именно подключаться к серверу и во всех скриптах нужно использовать именно её. Она не подходит для общей библиотеки, т.к. та еще не загружена.
2. Вызвать эту функцию один/несколько раз для подключения и выполнения всех нужных файлов: общая библиотека кода, специфический код для найденного окружения и т.п. Может быть посмотреть какая команда передана в аргументах и подгрузить код для выполнения этой команды.
На что обратить внимание
- Всегда проверять загруженный код перед выполнением. Иначе можно выполнить полскрипта или какую-то ошибку
- В публикуемом скрипте минимум (в идеале одна) внешняя зависимость — адрес с которого загружается основной код. Всегда один и тот же
- Всегда делать несколько попыток загрузить каждый файл. Даже если работа идет в локальной сети — могут быть временные ошибки сервера когда он вернет что-то не то, например ошибку. Или вообще подключение не примет. Если у вас один загружаемый файл и команды выполняются вручную — это может быть не страшно. Но если команды будут встроены в автоматику и подключаемых файлов много — вероятность что ошибка произойдет вполне реальна. Так же реальна и загрузка файла не целиком.
- Перед публикацией скрипта обязательно его проверить — это обидно, когда опечатался в одном символе и из-за этого нужно переделывать кучу работы по подготовке/проверке шаблона или взаимодействовать с теми, кому недавно отдал скрипт
Недостатки и точка зрения на них
- Необходимость подключения к сети (или интернету). В контексте применения этого подхода всё равно что-то придется скачивать с сервера разработчика (в т.ч. со своего если система используется внутри) и сеть всё равно потребуется. ��крипт не сможет настроить локальную сеть если она не работает или задать пароль доступа если не сможет его из сети получить. Так же скрипт не сможет развернуть ПО/сайт с сервера разработчика если он недоступен
- Выполнение заранее неопределенного кода. Если это свой код, то всё понятно. Если это чей-то чужой код — всё равно это будет запускаться/выполняться на том уровне доверия который ему положен. Если это установка клиенского приложения — в клиентской среде и всё равно разработчик внутри своего ПО может делать что угодно и весь код проверять никто не будет (если кто-то будет — они врядли будут вообще использовать авторазвертывание со стороних ресурсов — только свои коды со своих серверов)
