сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Linux и bash

Скрипты в проде: идемпотентность и секреты

Скрипты в проде

Написал наивное добавление строки в конфиг и запустил три раза - в файле три одинаковых строки. Переписал так, чтобы перед добавлением проверялось наличие, запустил пять раз - в файле одна строка. Разница в одной строке кода, а поведение принципиально разное: второй скрипт можно запускать сколько угодно и в любой момент.

Стержень: прод-скрипт можно запустить повторно без вреда, он громко падает и не светит секреты.

// Формулировки: «что такое идемпотентность?», «как передать пароль скрипту?», «скрипт отработал наполовину, что делать?»

Повторный запуск не должен вредить

Идемпотентность - это свойство «сколько ни повторяй, результат один и тот же». Мой замер показывает разницу буквально: три запуска наивной версии дали три строки, пять запусков аккуратной - одну.

Зачем это нужно на проде. Скрипт прерывается посреди работы чаще, чем кажется: оборвалась связь, кончилось время у задачи, кто-то нажал отмену. Если повторный запуск безопасен, лечение простое - запустить ещё раз. Если нет, начинается ручной разбор «что уже успело примениться», а это самый дорогой вид работы.

// Приёмы простые: перед добавлением проверять наличие, вместо «создать» использовать «создать, если нет», вместо дописывания в файл собирать его целиком из шаблона, а для шагов, которые нельзя повторить, вести отметку о выполнении. Ровно на этом стоят и инструменты описания инфраструктуры: они не выполняют шаги, а приводят состояние к описанному, поэтому повторный запуск для них норма.

идемпотентность
повторный запуск даёт тот же результат и не ломает сделанное
отметка о выполнении
запись, что неповторяемый шаг уже сделан

Секрет в аргументах виден всем

Проверил на этой машине. Запустил программу, передав пароль аргументом командной строки, и посмотрел, что видно снаружи. Аргументы процесса лежат в системном файле с правами на чтение ДЛЯ ВСЕХ: любой пользователь машины видит строку целиком вместе с паролем. Обычная команда просмотра процессов показывает ровно то же.

Для сравнения посмотрел на переменные окружения того же процесса: их файл доступен только владельцу. Отсюда практическая иерархия. Хуже всего - пароль аргументом. Приемлемо - переменная окружения, но с оговоркой: она наследуется дочерними процессами и часто попадает в аварийные снимки памяти и в описание контейнера, где её видно командой просмотра. Лучше - файл с правами только для владельца или запрос значения у хранилища секретов: это отдельная система, которая выдаёт пароли по правам и на ограниченное время.

// И отдельно: секреты не печатают в лог. Скрипт с включённым выводом всех команд напечатает и строку с паролем, а лог обычно уезжает в общее хранилище, где его читает вся команда.

аргументы процесса
командная строка запуска; читается любым пользователем машины
переменные окружения процесса
доступны только владельцу процесса, но наследуются потомками

Что ещё отличает прод-скрипт от домашнего

Первое - громкое падение. Скрипт, который упал молча и вернул нулевой код, хуже, чем не запускавшийся: планировщик считает задачу выполненной, а работа не сделана. Я это воспроизводил: опечатка в имени переменной дала код 0 и полное отсутствие результата.

Второе - предсказуемость при повторе, о которой выше. Третье - режим показа без изменений: возможность запустить и посмотреть, что скрипт СОБИРАЕТСЯ сделать. Для операций, которые удаляют или перезаписывают, это единственный способ проверить логику на проде, не сломав его.

// Четвёртое - проверки перед действием. Путь непустой, каталог тот самый, файл существует, свободного места хватает. Пять строк проверок в начале дешевле одного восстановления из резервной копии. И пятое - понятный вывод: что сделано, что пропущено и почему. Скрипт, который молча отработал, невозможно проверить, а скрипт, который написал «добавил 3 записи, 12 уже были на месте», проверяется за секунду.

режим показа
запуск, который печатает планируемые действия, но ничего не меняет
проверки перед действием
убедиться, что путь, каталог и место такие, как ожидалось

Как отвечать: «Как передать скрипту пароль от базы?»

Точно не аргументом командной строки. Я проверял: аргументы процесса лежат в системном файле, который читается любым пользователем машины, обычная команда просмотра процессов показывает строку запуска целиком вместе с паролем. Переменная окружения уже лучше - её файл доступен только владельцу процесса, - но и у неё есть цена: она наследуется всеми дочерними процессами и попадает в описание контейнера, где её видно одной командой. Поэтому рабочих варианта два: файл с правами только для владельца, который скрипт читает и сразу забывает, или запрос значения у хранилища секретов на время работы, с коротким сроком жизни. И отдельно слежу, чтобы значение не попало в лог: скрипт с включённым выводом всех команд напечатает и его тоже.

Ответ выстраивает иерархию вариантов с их ценой, а не называет один правильный. Замер про читаемость аргументов делает это разговором, а не пересказом.

На чём валятся

  • Передают пароль аргументом командной строки: его видит любой пользователь машины.
  • Пишут скрипты, которые нельзя запустить второй раз, и потом разбирают вручную, что успело примениться.
  • Возвращают ноль при неудаче и оставляют планировщик в уверенности, что всё хорошо.
  • Не делают режим показа для опасных операций и проверяют логику сразу на проде.
  • Включают вывод всех команд и печатают секреты в общий лог.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.

  1. #dvo_automation1 / 5
    Скрипт архивации молча выходит на первом файле и не печатает итог. Где баг?
    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)).

  2. #dvo_automation2 / 5
    Скрипт выкатки упал на середине. Что в первую очередь делает его безопасным для повторного запуска?
    A)Обёртка каждой команды в retry с экспоненциальной паузой
    B)Операции сформулированы как «привести к состоянию», а не «сделать шаг»
    C)Трап на EXIT, который откатывает всё сделанное
    D)Запуск под tmux, чтобы обрыв связи не прерывал работу
    показать ответ и разбор
    +B)Операции сформулированы как «привести к состоянию», а не «сделать шаг»

    // разбор: Идемпотентность — это когда повтор приводит к тому же состоянию: mkdir -p вместо mkdir, «создай, если нет» вместо «создай», сравнение с целевым конфигом вместо слепого append. Тогда после падения скрипт просто запускают заново. Ретраи и откаты полезны, но без идемпотентности они множат частично применённые изменения — и второй запуск дописывает строку в конфиг второй раз.

  3. #dvo_automation3 / 5
    В скрипте есть строка 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. Плюс не собирать разрушительные пути из непроверенных переменных.

  4. #dvo_automation4 / 5
    Скрипт качает артефакт: curl -sO "$URL", проверяет $?. При ответе 404 curl вернул 0, и скрипт поехал дальше с пустым файлом. Почему и как правильно?
    A)curl отдаёт HTTP-код прямо в $?, надо сравнить его с 404
    B)Проблема в -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 — ретраи на сетевых сбоях.

  5. #dvo_automation5 / 5
    Секрет перестали передавать аргументом (был виден в 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; и сброс переменной после использования.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.