Скрипты в проде: идемпотентность и секреты
Написал наивное добавление строки в конфиг и запустил три раза - в файле три одинаковых строки. Переписал так, чтобы перед добавлением проверялось наличие, запустил пять раз - в файле одна строка. Разница в одной строке кода, а поведение принципиально разное: второй скрипт можно запускать сколько угодно и в любой момент.
Стержень: прод-скрипт можно запустить повторно без вреда, он громко падает и не светит секреты.
// Формулировки: «что такое идемпотентность?», «как передать пароль скрипту?», «скрипт отработал наполовину, что делать?»
Повторный запуск не должен вредить
Идемпотентность - это свойство «сколько ни повторяй, результат один и тот же». Мой замер показывает разницу буквально: три запуска наивной версии дали три строки, пять запусков аккуратной - одну.
Зачем это нужно на проде. Скрипт прерывается посреди работы чаще, чем кажется: оборвалась связь, кончилось время у задачи, кто-то нажал отмену. Если повторный запуск безопасен, лечение простое - запустить ещё раз. Если нет, начинается ручной разбор «что уже успело примениться», а это самый дорогой вид работы.
// Приёмы простые: перед добавлением проверять наличие, вместо «создать» использовать «создать, если нет», вместо дописывания в файл собирать его целиком из шаблона, а для шагов, которые нельзя повторить, вести отметку о выполнении. Ровно на этом стоят и инструменты описания инфраструктуры: они не выполняют шаги, а приводят состояние к описанному, поэтому повторный запуск для них норма.
- идемпотентность
- повторный запуск даёт тот же результат и не ломает сделанное
- отметка о выполнении
- запись, что неповторяемый шаг уже сделан
Секрет в аргументах виден всем
Проверил на этой машине. Запустил программу, передав пароль аргументом командной строки, и посмотрел, что видно снаружи. Аргументы процесса лежат в системном файле с правами на чтение ДЛЯ ВСЕХ: любой пользователь машины видит строку целиком вместе с паролем. Обычная команда просмотра процессов показывает ровно то же.
Для сравнения посмотрел на переменные окружения того же процесса: их файл доступен только владельцу. Отсюда практическая иерархия. Хуже всего - пароль аргументом. Приемлемо - переменная окружения, но с оговоркой: она наследуется дочерними процессами и часто попадает в аварийные снимки памяти и в описание контейнера, где её видно командой просмотра. Лучше - файл с правами только для владельца или запрос значения у хранилища секретов: это отдельная система, которая выдаёт пароли по правам и на ограниченное время.
// И отдельно: секреты не печатают в лог. Скрипт с включённым выводом всех команд напечатает и строку с паролем, а лог обычно уезжает в общее хранилище, где его читает вся команда.
- аргументы процесса
- командная строка запуска; читается любым пользователем машины
- переменные окружения процесса
- доступны только владельцу процесса, но наследуются потомками
Что ещё отличает прод-скрипт от домашнего
Первое - громкое падение. Скрипт, который упал молча и вернул нулевой код, хуже, чем не запускавшийся: планировщик считает задачу выполненной, а работа не сделана. Я это воспроизводил: опечатка в имени переменной дала код 0 и полное отсутствие результата.
Второе - предсказуемость при повторе, о которой выше. Третье - режим показа без изменений: возможность запустить и посмотреть, что скрипт СОБИРАЕТСЯ сделать. Для операций, которые удаляют или перезаписывают, это единственный способ проверить логику на проде, не сломав его.
// Четвёртое - проверки перед действием. Путь непустой, каталог тот самый, файл существует, свободного места хватает. Пять строк проверок в начале дешевле одного восстановления из резервной копии. И пятое - понятный вывод: что сделано, что пропущено и почему. Скрипт, который молча отработал, невозможно проверить, а скрипт, который написал «добавил 3 записи, 12 уже были на месте», проверяется за секунду.
- режим показа
- запуск, который печатает планируемые действия, но ничего не меняет
- проверки перед действием
- убедиться, что путь, каталог и место такие, как ожидалось
Как отвечать: «Как передать скрипту пароль от базы?»
Точно не аргументом командной строки. Я проверял: аргументы процесса лежат в системном файле, который читается любым пользователем машины, обычная команда просмотра процессов показывает строку запуска целиком вместе с паролем. Переменная окружения уже лучше - её файл доступен только владельцу процесса, - но и у неё есть цена: она наследуется всеми дочерними процессами и попадает в описание контейнера, где её видно одной командой. Поэтому рабочих варианта два: файл с правами только для владельца, который скрипт читает и сразу забывает, или запрос значения у хранилища секретов на время работы, с коротким сроком жизни. И отдельно слежу, чтобы значение не попало в лог: скрипт с включённым выводом всех команд напечатает и его тоже.
Ответ выстраивает иерархию вариантов с их ценой, а не называет один правильный. Замер про читаемость аргументов делает это разговором, а не пересказом.
На чём валятся
- −Передают пароль аргументом командной строки: его видит любой пользователь машины.
- −Пишут скрипты, которые нельзя запустить второй раз, и потом разбирают вручную, что успело примениться.
- −Возвращают ноль при неудаче и оставляют планировщик в уверенности, что всё хорошо.
- −Не делают режим показа для опасных операций и проверяют логику сразу на проде.
- −Включают вывод всех команд и печатают секреты в общий лог.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Скрипт архивации молча выходит на первом файле и не печатает итог. Где баг?
set -e count=0 for f in *.log; do ((count++)) gzip "$f" done echo "сжато: $count"A)Цикл идёт в сабшелле, и count снаружи теряетсяB)gzip спотыкается на уже сжатом файле и роняет скриптC)((count++)) при count=0 возвращает 1, и set -e рубит скриптD)Глоб *.log не раскрывается без ls, цикл идёт по одной строкепоказать ответ и разбор
+C)((count++)) при count=0 возвращает 1, и set -e рубит скрипт// разбор: Арифметическая команда ((...)) отдаёт код 1, когда результат выражения равен нулю. Постфиксный count++ возвращает старое значение — на первой итерации это 0, значит код 1, и set -e молча гасит скрипт. Пишут count=$((count+1)) либо ((count++)) || true. Та же грабля у ((i--)) до нуля и у ((flag=0)).
- Скрипт выкатки упал на середине. Что в первую очередь делает его безопасным для повторного запуска?A)Обёртка каждой команды в retry с экспоненциальной паузойB)Операции сформулированы как «привести к состоянию», а не «сделать шаг»C)Трап на EXIT, который откатывает всё сделанноеD)Запуск под tmux, чтобы обрыв связи не прерывал работу
показать ответ и разбор
+B)Операции сформулированы как «привести к состоянию», а не «сделать шаг»// разбор: Идемпотентность — это когда повтор приводит к тому же состоянию: mkdir -p вместо mkdir, «создай, если нет» вместо «создай», сравнение с целевым конфигом вместо слепого append. Тогда после падения скрипт просто запускают заново. Ретраи и откаты полезны, но без идемпотентности они множат частично применённые изменения — и второй запуск дописывает строку в конфиг второй раз.
- В скрипте есть строка rm -rf "$WORKDIR"/*. Чем это опасно и как подстраховаться?A)Если WORKDIR пуст, сотрёт лишнее; страховка — ${WORKDIR:?}B)Опасности нет: двойные кавычки полностью защищают от ошибокC)Надо убрать кавычки, тогда путь раскроется как надоD)rm -rf безопасен, пока скрипт запущен не от root
показать ответ и разбор
+A)Если WORKDIR пуст, сотрёт лишнее; страховка — ${WORKDIR:?}// разбор: Если WORKDIR не задан или пуст, строка схлопывается в rm -rf /* — со всеми последствиями. Кавычки защищают от разбиения по пробелам, но не от пустого значения. Страховка: ${WORKDIR:?not set} (упасть на пустом), явная проверка [ -n "$WORKDIR" ] && [ -d "$WORKDIR" ], set -u. Плюс не собирать разрушительные пути из непроверенных переменных.
- Скрипт качает артефакт: curl -sO "$URL", проверяет $?. При ответе 404 curl вернул 0, и скрипт поехал дальше с пустым файлом. Почему и как правильно?A)curl отдаёт HTTP-код прямо в $?, надо сравнить его с 404B)Проблема в -s: он глушит вместе с прогрессом и код возвратаC)Нужно указать -O дважды, чтобы curl дождался полного файлаD)Без --fail curl принимает HTTP-ошибку за успех; спасает -f
показать ответ и разбор
+D)Без --fail curl принимает HTTP-ошибку за успех; спасает -f// разбор: curl завершается с кодом 0, если передача состоялась, — даже когда сервер ответил 4xx/5xx (он просто скачал страницу ошибки). Флаг --fail (-f) делает код возврата ненулевым на HTTP ≥400. Надёжный вызов: curl -fsSL --retry 3 -O "$URL": -f — падать на ошибке, -sS — без прогресс-бара, но с видимыми ошибками, -L — за редиректами, --retry — ретраи на сетевых сбоях.
- Секрет перестали передавать аргументом (был виден в ps) и положили в переменную окружения. Где он всё ещё может утечь?A)Переменная окружения полностью закрыта, новых путей утечки нетB)Через /proc/PID/environ и наследование дочерним процессамC)Только при включённом core dump процессаD)Только через историю шелла в ~/.bash_history
показать ответ и разбор
+B)Через /proc/PID/environ и наследование дочерним процессам// разбор: Переменные окружения лучше argv (их не видно в ps), но всё ещё утекаемы: /proc/PID/environ читает сам пользователь и root, а окружение наследуется каждым дочерним процессом, который может его случайно залогировать или сдампить. Надёжнее: файл с секретом и правами 0600, читаемый на старте; менеджер секретов (Vault); передача через stdin; и сброс переменной после использования.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.