Учебник веб-разработки
Разделы учебника
На этой странице

XI. DevOps

Linux: процессы, права и SSH

Оглавление · Жизненный цикл сервиса · Деплой Atmanki

Задача: понять, кто запускает приложение, к каким файлам оно имеет доступ и как удалённая команда попадает на VPS. Нужны основы JS и работа с терминалом. Linux-команды ниже предназначены для учебной машины; macOS отличается устройством служб и некоторыми утилитами. Production-настройки выполняются штатными скриптами.

Программа и процесс

Файл server.js — программа; запущенный Node — процесс с PID, памятью, рабочим каталогом, окружением и открытыми ресурсами. Два запуска одного файла создают разные процессы. Перезапуск теряет память: переменная JS не заменяет запись в БД. Веб-сервер живёт между запросами; обработчик запроса не получает новый процесс автоматически. Это важная граница при переходе от привычной модели PHP-запросов.

Исходник схемы
flowchart LR
  Shell["Shell: команда и окружение"] --> Process["Процесс Node: PID, память, cwd"]
  Process --> Streams["stdin / stdout / stderr"]
  Process --> Resources["Файлы, сокеты, соединения"]
  Process --> Child["Дочерний процесс: отдельная память"]

Shell разбирает командную строку: кавычки, подстановки и перенаправления. > пишет stdout в файл, 2> — stderr, | соединяет stdout одной команды со stdin другой. Успешный exit code означает успех по правилам самой программы: он не доказывает готовность сайта. В bash set -o pipefail позволяет учитывать ошибку внутри pipeline; без него статус обычно определяется последней командой. Не печатайте полное окружение ради диагностики: в нём могут быть токены.

Для наблюдения на Linux достаточно начать с небольшого набора:

pwd
id
ps -eo pid,ppid,user,stat,comm
ss -ltn
systemctl status docker --no-pager

ps показывает процессы, ss — слушающие TCP-сокеты, systemctl — службу Docker на хосте с systemd. Это разные вопросы. Наличие слушающего порта ещё не проверяет HTTP-ответ, а состояние Docker daemon не проверяет приложения внутри контейнеров. Не добавляем отдельный systemd-unit для Next: в Atmanki его запуском управляет Docker.

Сигнал — запрос к процессу

SIGTERM обычно просит завершиться; обработчик может закрыть сервер и соединения. SIGINT обычно приходит от Ctrl+C. SIGKILL нельзя перехватить: процесс не выполнит свой cleanup. kill отправляет сигнал, а не обязательно немедленно уничтожает процесс. Сигналы Linux.

Корректное завершение требует ограниченного времени: прекратить приём новых запросов, закончить или отменить текущие, освободить ресурсы, выйти. Если операция в БД уже зафиксирована, сигнал не отменяет её. Если клиент не получил ответ, результат может быть неизвестен — см. идемпотентность.

Следующий пример проверяет только доставку сигнала своему дочернему процессу. Запустите из терминала с Node 24; он не открывает порт и не обращается к серверу.

node --input-type=module <<'JS'
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { once } from "node:events";

const child = spawn(process.execPath, ["-e",
  'setInterval(() => {}, 1000); process.on("SIGTERM", () => process.exit(0)); process.send("ready");',
], { stdio: ["ignore", "ignore", "ignore", "ipc"] });
const controller = new AbortController();
const deadline = setTimeout(() => {
  controller.abort();
  child.kill("SIGKILL");
}, 5000);
try {
  const [message] = await once(child, "message", { signal: controller.signal });
  assert.equal(message, "ready");
  const exited = once(child, "exit", { signal: controller.signal });
  child.kill("SIGTERM");
  const [code, signal] = await exited;
  assert.equal(code, 0);
  assert.equal(signal, null);
  console.log("SIGTERM обработан; exit code 0");
} finally {
  clearTimeout(deadline);
  if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
}
JS

Таймер удерживает дочерний процесс; IPC-канал сообщает, что обработчик уже установлен. Это убирает гонку «послать SIGTERM до готовности». Deadline ограничивает ожидание; SIGKILL в cleanup относится только к процессу, созданному примером. Лаборатория не подтверждает graceful shutdown Next/Strapi: его проверяют с реальными запросами в изолированной среде, отдельно от production.

Права — отношение процесса к объекту

У процесса есть числовые идентификаторы пользователя и групп; у файла — владелец, группа и права. Имена пользователей удобны людям, но проверка доступа опирается на идентификаторы. Учётные данные процесса.

В базовой Unix-модели выбирается класс owner, group или other, затем проверяется нужное действие. Разрешения не складываются по всем трём классам. ACL, capabilities и политики безопасности могут добавлять правила; здесь рассматриваем обычные mode bits.

БитДля обычного файлаДля каталога
r = 4Читать содержимоеПолучать список имён
w = 2Изменять содержимоеИзменять записи каталога при наличии x
x = 1ИсполнятьПроходить через каталог, обращаться к известному имени

640 — owner rw, group r, other ничего; 750 — owner rwx, group rx, other ничего. Чтобы открыть файл, нужны права прохода через каждый каталог пути. Чтобы удалить файл, обычно важны права родительского каталога, а не право записи в сам файл; sticky bit и другие правила могут ограничивать удаление. Разрешение пути в Linux.

Практика без секретов — отдельный временный каталог:

lesson_dir=$(mktemp -d)
printf 'учебный текст\n' > "$lesson_dir/example.txt"
chmod 700 "$lesson_dir"
chmod 600 "$lesson_dir/example.txt"
ls -ld "$lesson_dir" "$lesson_dir/example.txt"
rm "$lesson_dir/example.txt"
rmdir "$lesson_dir"

Ожидайте drwx------ у каталога и -rw------- у файла. Не применяйте chmod 777 к настоящим секретам ради устранения Permission denied. Сначала проверьте, какой UID обращается к файлу и где именно не хватает доступа.

В контейнере USER node не означает, что пользователь хоста с тем же именем имеет тот же UID. Для bind mount нужны совместимые числовые права. В Atmanki web и Strapi работают под node; их Dockerfile и монтирования — источник истины. Изменение владельца большого дерева данных не заменяет диагностику одного пути.

SSH проверяет две стороны

SSH создаёт защищённое соединение. Сначала клиент должен установить идентичность сервера по host key, затем сервер аутентифицирует пользователя. Публичный ключ в authorized_keys разрешает вход владельцу соответствующего приватного ключа; приватный ключ остаётся на стороне клиента. OpenSSH ssh.

Исходник схемы
sequenceDiagram
  participant Client as SSH-клиент
  participant Server as VPS
  Client->>Server: Установить соединение
  Server-->>Client: Host key сервера
  Note over Client: Проверить ожидаемый ключ
  Client->>Server: Подтвердить владение ключом пользователя
  Note over Server: Проверить authorized_keys и ограничения
  Server-->>Client: Выполнить разрешённую команду

known_hosts хранит известные ключи серверов. Новый fingerprint сверяют через доверенный канал, например консоль провайдера; ssh-keyscan сам по себе только получает предъявленный ключ. Изменение host key требует объяснения: переустановка или другой сервер возможны, но автоматическое удаление записи скрывает причину. StrictHostKeyChecking управляет этой проверкой. OpenSSH ssh_config.

Учебная команда после настройки своей машины, с вымышленным доменом:

ssh -o BatchMode=yes -o StrictHostKeyChecking=yes \
  -i ~/.ssh/lesson_ed25519 lesson@example.invalid 'id; pwd'

BatchMode отключает интерактивные запросы; без заранее известного host key строгая проверка завершится ошибкой. Одинарные кавычки передают id; pwd удалённому shell. Сложные команды с локальными и удалёнными подстановками лучше оформлять проверяемым скриптом. Не включайте agent forwarding без потребности.

bootstrap.sh создаёт atmanki-deploy, каталог .ssh с mode 0700 и authorized_keys с 0600; ключ ограничен запретом forwarding и PTY. Это не sandbox: пользователь состоит в группе docker, которая даёт полномочия уровня root через Docker daemon. Права группы docker. Доступ к релизу — административная возможность, даже без интерактивного терминала.

Проверьте понимание

  1. Процесс жив, но порт не слушается. Что подтверждает ps и что проверять дальше?
  2. У файла 600, у родителя нет x для процесса. Почему чтение не удаётся?
  3. После перезапуска исчез список в переменной JS. Где должно храниться такое состояние?
  4. SSH сообщил об изменившемся host key. Как получить подтверждение нового ключа?
  5. Почему успешная лаборатория SIGTERM не доказывает завершение активного HTTP-запроса?

Дальше — контейнер, готовность и релиз.