DevOps-инженер: от администрирования Linux к автоматизации инфраструктуры

Курс формирует системный подход к управлению IT-системами, начиная с основ Linux и заканчивая созданием полноценного CI/CD конвейера. Вы освоите методологию DevOps через практическое применение инструментов контейнеризации, оркестрации и автоматизации.

Философия DevOps и жизненный цикл разработки ПО

Философия DevOps и жизненный цикл разработки ПО

Представьте: пятница, 17:00. Команда разработчиков радостно передает системным администраторам новую версию приложения и уходит на выходные. Администраторы разворачивают код на сервере, и сайт мгновенно падает. Разработчики в понедельник пожимают плечами: «На моем компьютере всё работает!». Администраторы злятся: «А на боевом сервере — нет!». Этот классический конфликт стоил IT-индустрии миллионов долларов и терабайтов сожженных нервов. Именно для его решения и был придуман DevOps.

DevOps — это не название должности и не конкретная программа. Это философия, культура и набор инженерных практик, которые объединяют разработку (Development) и эксплуатацию (Operations) в единый непрерывный процесс.

Стена непонимания: почему Dev и Ops враждуют

Исторически IT-отделы были разделены на два изолированных лагеря, между которыми стояла невидимая «стена непонимания» (Wall of Confusion).

У каждого лагеря была своя, противоположная другой, цель:

  1. Dev (Разработчики): Их главная задача — создавать новые функции и как можно быстрее доставлять их пользователям. Их метрика успеха — скорость изменений.
  2. Ops (Системные администраторы): Их главная задача — обеспечивать бесперебойную работу серверов и баз данных. Любое изменение в системе — это риск сбоя. Их метрика успеха — стабильность.

Когда разработчик пишет код, он тестирует его в своей изолированной среде. Но боевой сервер (production) устроен сложнее: там другие версии библиотек, другие настройки сети, реальные пользователи и высокие нагрузки. Передача кода через «стену» без совместной ответственности приводила к тому, что релизы происходили редко (раз в несколько месяцев), сопровождались простоями и откатами версий.

Концепция DevOps разрушает эту стену. Она говорит: разработка и эксплуатация — это одна команда с общей целью. Разработчики начинают думать о том, как их код будет работать под нагрузкой, а администраторы внедряют инструменты, позволяющие разработчикам безопасно и быстро обновлять систему.

Эволюция жизненного цикла ПО (SDLC)

Жизненный цикл разработки программного обеспечения (Software Development Life Cycle, SDLC) — это путь, который проходит идея от задумки до работающего продукта. Подход к SDLC менялся вместе с индустрией.

Подход Как это работало Главный недостаток
Waterfall (Каскад) Строго последовательно: месяцы на проектирование, месяцы на код, затем долгое тестирование и один большой релиз. Если на этапе тестирования находили фундаментальную ошибку, приходилось переделывать всё с нуля.
Agile (Гибкая разработка) Работу разбили на короткие циклы (спринты) по 2–4 недели. Код стали писать и тестировать быстро. Разработчики ускорились, но администраторы (Ops) по-прежнему разворачивали код вручную. Образовалось «бутылочное горлышко».
DevOps Логичное продолжение Agile. Автоматизация не только написания кода, но и его сборки, тестирования и доставки на сервер. Требует высокой технической культуры и полного пересмотра процессов.

Бесконечный цикл DevOps

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

Давайте проследим, как крошечная задача — например, исправление опечатки на главной странице сайта — проходит через весь этот цикл:

  1. Plan (Планирование): Менеджер создает задачу в трекере (например, Jira): «Исправить опечатку в заголовке».
  2. Code (Написание кода): Разработчик скачивает код на свой ноутбук, исправляет опечатку и сохраняет изменения в системе контроля версий (Git).
  3. Build (Сборка): Специальный сервер автоматически замечает новый код и собирает из него готовое к запуску приложение.
  4. Test (Тестирование): Автоматические скрипты проверяют, не сломало ли исправление опечатки другие части сайта.
  5. Release (Релиз): Проверенный код упаковывается в артефакт (например, Docker-образ), готовый к отправке на сервер.
  6. Deploy (Развертывание): Система автоматизации (например, Ansible) незаметно для пользователей обновляет приложение на боевом сервере.
  7. Operate (Эксплуатация): Обновленный сайт работает и обслуживает реальных клиентов.
  8. Monitor (Мониторинг): Системы наблюдения собирают данные. Если после обновления нагрузка на процессор выросла или пользователи начали получать ошибки, система отправляет алерт (уведомление), и цикл начинается заново с этапа Plan.

Основы культуры: фреймворк CALMS

Чтобы цикл работал, недостаточно просто купить новые программы. DevOps опирается на пять столпов, известных как аббревиатура CALMS:

  • C — Culture (Культура): Общая ответственность. Если сайт упал, виноват не конкретный админ или программист, проблема ищется в процессе.
  • A — Automation (Автоматизация): Всё, что делается вручную больше одного раза, должно быть автоматизировано (настройка серверов, тестирование, деплой).
  • L — Lean (Бережливое производство): Избавление от лишних процессов. Задачи разбиваются на мелкие части. Выкатить 10 маленьких обновлений за день безопаснее, чем одно гигантское за месяц.
  • M — Measurement (Измерения): Все решения принимаются на основе данных. Важнейшая метрика здесь — доступность сервиса (Uptime).

Она вычисляется по формуле: Uptime=TworkTtotal×100%Uptime = \frac{T_{work}}{T_{total}} \times 100\% Где TworkT_{work} — время фактической работы сервиса без сбоев, а TtotalT_{total} — общее время в рассматриваемом периоде (например, месяц). Если мы не измеряем систему, мы не знаем, улучшают ли наши действия её работу.

  • S — Sharing (Обмен знаниями): Разработчики делятся пониманием архитектуры кода, а администраторы обучают разработчиков основам работы сетей и операционных систем.

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

Именно поэтому наш курс начинается не с облачных платформ и сложных пайплайнов. Чтобы автоматизировать инфраструктуру, нужно сначала глубоко понимать, как она работает изнутри. Сердцем 90% серверов в мире является операционная система Linux. И именно с фундаментальных принципов управления Linux мы начнем наше практическое погружение в следующем модуле.

Системное администрирование Linux: управление процессами, пользователями и правами доступа

Системное администрирование Linux: управление процессами, пользователями и правами доступа

Пятничный деплой завершился успешно, код доставлен на сервер, но приложение почему-то не отвечает. Вы заходите на сервер и видите пустую консоль. Что произошло? Закончилась оперативная память и система «убила» ваше приложение? Или у программы просто нет прав на чтение конфигурационного файла с паролем от базы данных? В методологии DevOps этап Operate (эксплуатация) невозможен без понимания того, как операционная система управляет ресурсами. В мире Linux всё сводится к двум фундаментальным сущностям: файлам и процессам.

Анатомия процессов: кто потребляет ресурсы

Каждая запущенная программа в Linux — от системного демона до вашего Python-скрипта — это процесс. Операционная система должна отличать их друг от друга, поэтому при запуске каждому процессу выдается уникальный идентификатор — PID (Process ID).

Процессы не возникают из ниоткуда. Каждый новый процесс создается уже существующим. Образуется строгая иерархия, где у каждого процесса есть родитель — PPID (Parent Process ID). На вершине этого дерева всегда находится процесс с PID 1 (обычно это systemd или init), который запускается самим ядром при загрузке системы.

Чтобы увидеть, что происходит в системе прямо сейчас, инженеры используют утилиты мониторинга. Базовая команда ps aux выведет статичный снимок всех процессов, но в реальной практике чаще применяют динамические инструменты, такие как top или htop.

Ключевые метрики процесса в top:

  • %CPU — сколько процессорного времени потребляет задача.
  • %MEM — доля используемой оперативной памяти.
  • STAT — текущее состояние (R — выполняется, S — спит в ожидании события, Z — «зомби», процесс завершился, но родитель не считал его статус).

Как остановить процесс: искусство сигналов

Если процесс завис или потребляет слишком много памяти, его нужно остановить. Для этого используется команда kill. Вопреки названию, она не «убивает» процесс напрямую, а отправляет ему сигнал.

Два самых важных сигнала, которые должен знать DevOps-инженер:

  1. SIGTERM (сигнал 15) — мягкое завершение. Отправляется по умолчанию командой kill <PID>. Процесс получает уведомление: «Тебя просят закрыться». У программы есть время корректно сохранить данные, закрыть соединения с базой и завершить дочерние процессы.
  2. SIGKILL (сигнал 9) — жесткое убийство (kill -9 <PID>). Сигнал перехватывается самим ядром ОС, и процесс уничтожается мгновенно. Программа не успевает ничего сохранить.

Использование kill -9 — это антипаттерн в DevOps, который применяется только в крайнем случае, когда процесс намертво завис и не реагирует на SIGTERM. Жесткое завершение базы данных почти гарантированно приведет к повреждению данных.

Пользователи и принцип наименьших привилегий

Процессы не существуют в вакууме — каждый процесс запускается от имени определенного пользователя и наследует его права.

В Linux есть абсолютный монарх — пользователь root. Ему разрешено всё: удалять системные файлы, менять пароли других пользователей, останавливать любые процессы. Исторически системные администраторы часто работали прямо из-под root, что приводило к катастрофическим ошибкам (знаменитое случайное удаление всего диска командой rm -rf /).

В DevOps применяется принцип наименьших привилегий (Principle of Least Privilege): каждый сервис и каждый инженер должен иметь ровно столько прав, сколько необходимо для выполнения задачи, и ни битом больше.

Вместо того чтобы сидеть под root, инженеры работают под обычными учетными записями, а для выполнения привилегированных команд используют утилиту sudo (Superuser DO). Она позволяет выполнить конкретную команду с правами администратора, оставляя запись об этом в логах аудита.

Для приложений создаются специальные сервисные пользователи. Если вы разворачиваете веб-сервер Nginx, он не должен работать от root. Вы создаете пользователя nginx без права логина в систему: useradd -r -s /bin/false nginx Если хакер найдет уязвимость в веб-сервере, он получит права только пользователя nginx, а не всей системы.

Матрица доступа: кто и что может делать с файлами

Поскольку в Linux «всё есть файл» (включая конфигурации, логи и даже оборудование), безопасность системы сводится к тому, чтобы правильно настроить связь между пользователями и файлами.

Каждый файл и директория в Linux имеет владельца (User) и группу-владельца (Group). Права доступа всегда назначаются для трех категорий:

  1. u (User) — права владельца файла.
  2. g (Group) — права группы.
  3. o (Others) — права всех остальных пользователей системы.

Для каждой из этих категорий можно разрешить или запретить три действия:

  • r (Read) — чтение содержимого файла (или просмотр списка файлов в директории).
  • w (Write) — изменение файла (или создание/удаление файлов в директории).
  • x (Execute) — запуск файла как программы (или право войти в директорию).

Числовая магия chmod

В скриптах автоматизации права доступа почти всегда записываются числами (восьмеричной системой). Каждому праву присвоен свой вес:

  • Чтение (r) = 4
  • Запись (w) = 2
  • Выполнение (x) = 1

Чтобы получить итоговое право для категории, веса складываются. Например, если владельцу нужно дать права на чтение и запись, но не на выполнение: 4+2+0=64 + 2 + 0 = 6. Если нужны вообще все права: 4+2+1=74 + 2 + 1 = 7.

Итоговая маска состоит из трех цифр: для владельца, для группы и для остальных.

Для изменения прав используется команда chmod (Change Mode), а для изменения владельца — chown (Change Owner).

Собираем воедино: анатомия безопасного деплоя

Давайте свяжем процессы, пользователей и права в единый сценарий, с которым вы будете сталкиваться постоянно. Нам нужно развернуть бэкенд-приложение на сервере.

  1. Изоляция пользователя: Мы не запускаем приложение от своего имени или от root. Мы создаем пользователя backend_user.
  2. Подготовка файловой системы: Копируем код приложения в папку /opt/app.
  3. Передача прав: Делаем нашего сервисного пользователя владельцем этой папки: sudo chown -R backend_user:backend_user /opt/app
  4. Настройка безопасности: Ограничиваем доступ. Приложение должно читать и писать в своей папке, но посторонние не должны видеть исходный код. Мы задаем права 750 (владельцу можно всё, группе только чтение и запуск, остальным — ничего): sudo chmod -R 750 /opt/app
  5. Запуск процесса: Теперь мы запускаем приложение от имени backend_user. В списке процессов (ps aux) мы увидим, что наша программа работает изолированно и безопасно.

Сейчас мы рассмотрели, как делать это руками в консоли. Но философия DevOps требует автоматизации. Вводить эти команды на ста серверах вручную — путь к ошибкам и выгоранию. В следующих главах мы начнем превращать эти ручные действия в воспроизводимый код, начав с написания Bash-скриптов.

Сетевые протоколы и работа с файловой системой в Linux для инженеров

Сетевые протоколы и работа с файловой системой в Linux для инженеров

Изолированный, безопасно запущенный процесс с правильными правами доступа — это лишь половина дела. Если этот процесс не знает, откуда прочитать свой конфигурационный файл, куда записать информацию об ошибке и как принять запрос от пользователя, он абсолютно бесполезен. Серверное приложение оживает только тогда, когда начинает взаимодействовать с файловой системой и сетью.

Навигация по стандарту: где живут данные

В Linux всё начинается с корневого каталога /. В отличие от Windows с её дисками C: и D:, в Linux все физические диски и логические разделы монтируются в единое дерево. Чтобы инженеры и программы не путались, где что лежит, существует FHS (Filesystem Hierarchy Standard).

Для DevOps-инженера критически важны четыре директории, с которыми приходится работать каждый день:

Директория Назначение Практический смысл для инженера
/etc Конфигурационные файлы Здесь лежат настройки системных служб. Если нужно изменить порт Nginx или настройки SSH, вы идете в /etc/nginx или /etc/ssh. Процессы обычно имеют сюда права только на чтение.
/var/log Журналы (логи) Сюда приложения пишут информацию о своей работе. Если процесс упал, первым делом проверяем логи здесь.
/opt Стороннее ПО Место для монолитных приложений, которые не разбиваются по системным папкам. Сюда часто деплоят кастомные бинарные файлы бэкенда.
/tmp Временные файлы Директория для временных данных. Важно: она очищается при перезагрузке сервера. Запускать отсюда production-код — грубая ошибка.

Когда мы создаем сервисного пользователя для запуска приложения, мы должны явно выдать ему права на чтение из нужной папки в /etc и права на запись в конкретную папку внутри /var/log.

Точка контакта: как процесс выходит в сеть

Философия Linux гласит: «Всё есть файл». Сетевое соединение — не исключение. Когда процесс хочет общаться по сети, он создает специальный файловый дескриптор, который называется сокетом (socket).

Чтобы трафик из интернета нашел путь к конкретному процессу (PID) на вашем сервере, используется связка двух координат:

  1. IP-адрес — это адрес самого сервера (как номер дома).
  2. Порт — это числовой идентификатор, к которому привязывается процесс (как номер квартиры). Доступны порты от 0 до 65535.

Системные службы обычно занимают порты до 1024 (например, HTTP использует порт 80, SSH — 22). Чтобы процесс мог «слушать» порт с номером меньше 1024, он должен быть запущен с правами root. Именно поэтому современные веб-приложения часто запускают от обычного пользователя на портах вроде 8080 или 3000, а затем перенаправляют на них трафик.

Анатомия соединения: почему TCP важен для инженера

Большинство надежных веб-сервисов работают поверх протокола TCP (Transmission Control Protocol). Его главная фишка — гарантия доставки. Прежде чем передать хоть один байт полезных данных (например, HTML-страницу), клиент и сервер обязаны установить соединение.

Этот процесс называется тройным рукопожатием (TCP 3-way handshake):

  1. SYN (Synchronize): Клиент стучится в порт сервера и говорит: «Я хочу установить соединение, мой начальный номер пакета X».
  2. SYN-ACK (Synchronize-Acknowledge): Сервер отвечает: «Я получил твой X, готов общаться. Мой начальный номер Y».
  3. ACK (Acknowledge): Клиент подтверждает: «Я получил твой Y, начинаем передачу данных».

Понимание этого механизма спасает часы дебага. Если на этапе SYN сервер молчит (пакет теряется), клиент будет долго ждать и в итоге выдаст ошибку Connection timeout. Это значит, что сервер выключен или трафик блокирует файрвол. Если же сервер отвечает специальным пакетом RST (Reset), соединение сбрасывается мгновенно с ошибкой Connection refused. Это значит, что сервер доступен, но на этом порту нет запущенного процесса, который мог бы ответить.

Практика: набор инструментов для траблшутинга

В DevOps 70% времени уходит не на первичную настройку, а на выяснение причин, почему «оно не работает». Рассмотрим базовый набор утилит.

1. ss (Socket Statistics)

Показывает, какие порты сейчас слушаются процессами на сервере. Заменяет устаревшую утилиту netstat. Самая частая комбинация флагов: ss -tuln

  • -t — показать TCP-порты.
  • -u — показать UDP-порты.
  • -l — показать только те порты, которые находятся в режиме прослушивания (LISTEN).
  • -n — показывать номера портов цифрами, а не пытаться преобразовать их в имена сервисов.

Если вы запустили приложение, но ss -tuln не показывает нужный порт — приложение упало или зависло при старте.

2. curl

Утилита для отправки HTTP-запросов и проверки доступности портов. Команда curl -v http://localhost:8080 (флаг -v означает verbose, подробный вывод) покажет весь процесс: от попытки установить TCP-соединение до полученных HTTP-заголовков. Это идеальный способ проверить, отвечает ли ваш процесс локально, исключив влияние внешних сетей.

3. ping

Проверяет базовую сетевую связность между хостами. Важно помнить: ping работает по протоколу ICMP, у которого нет портов. Если ping проходит, значит, сервер включен и сеть работает. Но это не гарантирует, что на сервере запущен ваш веб-сервер и открыт порт 80.

Сборка картины: расследование инцидента

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

  1. Проверяем процесс: ps aux | grep backend — жив ли процесс вообще? Если нет, он упал.
  2. Проверяем логи: Идем в /var/log/backend/error.log. Видим ошибку Permission denied.
  3. Проверяем конфигурацию и права: Читаем конфиг в /etc/backend/config.yaml. Видим, что приложение пытается слушать порт 80.
  4. Находим причину: Приложение запущено от сервисного пользователя, а порты до 1024 требуют прав root.
  5. Исправляем: Меняем порт в конфиге на 8080, перезапускаем процесс.
  6. Проверяем сеть: Выполняем ss -tuln — видим порт 8080 в состоянии LISTEN. Делаем curl -v http://localhost:8080 и получаем успешный ответ.

Мы прошли путь от запуска изолированного бинарника до работающего сетевого сервиса. Следующий шаг — перестать выполнять эти рутинные команды руками и заставить сервер настраивать себя самостоятельно.

Автоматизация рутины: написание Bash-скриптов для системных задач

Автоматизация рутины: написание Bash-скриптов для системных задач

Представьте ситуацию: пятничный вечер, выкатывается обновление бэкенда. Вам нужно зайти на сервер, остановить текущий процесс, скопировать новую сборку в /opt, назначить правильного владельца, запустить сервис и проверить, слушает ли он нужный порт. Ввод этих команд вручную занимает три минуты. Если серверов десять — полчаса. Если вы опечатаетесь в команде chown — сервис не запустится, и вы потратите еще час на чтение логов. Ручное выполнение повторяющихся действий — главный враг DevOps-инженера.

Мы уже умеем управлять процессами, правами и сетью в интерактивном режиме. Теперь наша задача — превратить эти знания в воспроизводимый код. Bash-скрипты — это первый и самый доступный уровень автоматизации, клей, на котором держится инфраструктура до внедрения тяжеловесных инструментов конфигурации.

От командной строки к сценарию

Bash-скрипт — это просто текстовый файл, содержащий набор команд Linux, которые выполняются последовательно. Но чтобы система поняла, как именно читать этот файл, ему нужны два атрибута: Shebang и права на исполнение.

Создадим файл deploy.sh. Его первая строка всегда должна выглядеть так:

#!/bin/bash

Эта конструкция называется Shebang (от символов hash # и bang !). Она указывает ядру операционной системы, какой интерпретатор использовать для выполнения файла. Без неё система попытается выполнить файл с помощью оболочки по умолчанию, что может привести к ошибкам совместимости.

Далее мы добавляем команды. Например, создадим директорию и скопируем туда архив:

#!/bin/bash
mkdir -p /opt/myapp
cp release.tar.gz /opt/myapp/

Если мы попытаемся запустить этот файл командой ./deploy.sh, система выдаст ошибку Permission denied. Вспоминаем концепцию прав доступа: по умолчанию текстовые файлы создаются без права на исполнение (Execute). Чтобы скрипт заработал, мы должны явно разрешить его запуск:

chmod +x deploy.sh

Теперь скрипт можно выполнять. Но пока он делает действия «вслепую». Настоящая автоматизация требует работы с данными и обратной связи от системы.

Переменные и управление потоками вывода

В скриптах не принято жестко прописывать пути и имена файлов (хардкод). Если завтра директория изменится с /opt/myapp на /opt/backend, придется переписывать весь код. Для гибкости используются переменные.

В Bash переменная объявляется без пробелов вокруг знака равенства, а вызывается со знаком доллара:

APP_DIR="/opt/myapp"
ARCHIVE_NAME="release.tar.gz"

mkdir -p $APP_DIR
cp $ARCHIVE_NAME $APP_DIR/

Когда скрипт работает, команды генерируют текст: успешные сообщения или ошибки. В Linux каждый процесс имеет три стандартных потока данных:

  1. stdin (0) — стандартный ввод (то, что мы печатаем с клавиатуры).
  2. stdout (1) — стандартный вывод (результат успешной работы команды).
  3. stderr (2) — стандартный вывод ошибок.

При автоматизации нам часто нужно «спрятать» лишний успешный вывод, чтобы он не засорял терминал, а ошибки, наоборот, сохранить в лог-файл в директории /var/log. Для этого используется перенаправление потоков символом >.

  • > file.log перенаправляет только успешный вывод (stdout).
  • 2> error.log перенаправляет только ошибки (stderr).
  • > output.log 2>&1 перенаправляет оба потока в один файл (сначала stdout направляется в файл, а затем stderr направляется туда же, куда и stdout).

Если вывод команды нам вообще не нужен, его отправляют в системную «черную дыру» — устройство /dev/null:

# Распаковываем архив тихо, ошибки отправляем в лог
tar -xzf $ARCHIVE_NAME -C $APP_DIR > /dev/null 2>> /var/log/deploy_errors.log

Как скрипт понимает, что всё сломалось: Exit Codes

Главное правило автоматизации: скрипт должен уметь останавливаться, если что-то пошло не так. Если команда mkdir не смогла создать директорию (например, нет прав sudo), следующая команда cp попытается скопировать файлы в несуществующее место, что приведет к каскадному сбою.

Каждая программа в Linux при завершении отправляет системе числовой код — Exit Code (код возврата).

  • Код 0 означает, что программа завершилась успешно.
  • Любой код от 1 до 255 означает ошибку.

Сразу после выполнения команды её код возврата сохраняется в специальной системной переменной $?.

Вместо того чтобы вручную проверять $? после каждой строки, в Bash используют логические операторы && (И) и || (ИЛИ) для связывания команд в цепочки.

  • команда1 && команда2 — вторая команда выполнится только если первая завершилась успешно (вернула 0).
  • команда1 || команда2 — вторая команда выполнится только если первая завершилась с ошибкой (вернула не 0).

Пример безопасного перезапуска сервиса:

# Убить процесс ТОЛЬКО если удалось найти его PID
pkill nginx && echo "Nginx остановлен" || echo "Процесс Nginx не найден"

Условия и циклы на службе сисадмина

Цепочки && удобны для коротких проверок, но для сложной логики используются полноценные условные конструкции if.

Частая задача — проверить, существует ли нужный файл, прежде чем с ним работать. Для этого в Bash есть специальные флаги проверок, например -f (существует ли файл) или -d (существует ли директория).

if [ -d "/opt/myapp" ]; then
    echo "Директория существует. Начинаем обновление."
else
    echo "Директория не найдена. Создаем..."
    mkdir -p /opt/myapp
fi

Еще один инструмент борьбы с рутиной — циклы. Если вам нужно изменить права доступа не для одного файла, а для десятка конфигурационных файлов, используется цикл for. Он берет список элементов и выполняет для каждого из них заданный блок команд.

# Меняем владельца для всех лог-файлов в папке
for LOG_FILE in /var/log/myapp/*.log; do
    chown nginx:nginx $LOG_FILE
    chmod 640 $LOG_FILE
done

Практический кейс: Скрипт проверки и ротации

Соберем изученные концепции в единый скрипт. Задача: написать инструмент, который проверяет, работает ли наше приложение на порту 8080. Если порт свободен (приложение упало), скрипт должен заархивировать старые логи, чтобы освободить место, и попытаться запустить приложение заново.

Здесь мы объединяем сетевую диагностику (утилита ss), работу с файловой системой и логику Bash.

#!/bin/bash

# 1. Задаем переменные
APP_PORT=8080
LOG_DIR="/var/log/myapp"
BACKUP_DIR="/opt/backups"

# 2. Проверяем, слушает ли кто-то порт (перенаправляем вывод в null, нам нужен только Exit Code)
ss -tuln | grep ":$APP_PORT" > /dev/null

# 3. Анализируем результат
if [ $? -eq 0 ]; then
    echo "Сервис работает в штатном режиме."
else
    echo "Внимание! Порт $APP_PORT свободен. Сервис упал."

    # 4. Ротация логов перед перезапуском
    echo "Архивируем старые логи..."
    mkdir -p $BACKUP_DIR

    # Цикл по всем логам
    for FILE in $LOG_DIR/*.log; do
        # Архивируем файл и в случае успеха удаляем оригинал
        tar -czf "$BACKUP_DIR/$(basename $FILE).tar.gz" $FILE && rm -f $FILE
    done

    # 5. Запуск сервиса от имени сервисного пользователя
    echo "Запускаем сервис..."
    sudo -u myappuser /opt/myapp/start.sh > /dev/null 2>&1 &

    echo "Процесс восстановления запущен."
fi

Этот скрипт — классический пример автоматизации рутины. Он принимает решения на основе состояния системы (занят ли порт), обрабатывает файлы пакетно (через цикл) и управляет процессами, пряча лишний вывод.

Постепенно такие скрипты обрастают новыми функциями, становятся сложнее и длиннее. В какой-то момент поддерживать тысячи строк Bash-кода становится тяжело. Именно тогда на сцену выходят декларативные инструменты вроде Ansible или контейнеризация с Docker, где среда выполнения описывается еще проще. Но под капотом этих современных инструментов всё равно лежат те же самые базовые принципы взаимодействия с ядром Linux, переменные и потоки вывода.

Контейнеризация с Docker: изоляция приложений и управление образами

Контейнеризация с Docker: изоляция приложений и управление образами

Вы написали идеальный Bash-скрипт для деплоя бэкенда. Он безупречно работает на вашем сервере с Ubuntu 22.04, корректно направляет логи в /var/log и запускает процесс. Но когда коллега пытается развернуть приложение на CentOS, скрипт падает: в системе установлена другая версия Python, отсутствуют нужные системные библиотеки, а пути к конфигурациям отличаются. Проблема «на моем компьютере это работает» — главный кошмар эксплуатации. Чтобы её решить, нужно упаковать приложение не просто в архив с кодом, а вместе со всей его файловой системой и зависимостями.

Контейнер — это просто изолированный процесс

Исторически для изоляции приложений использовали виртуальные машины (VM). Виртуальная машина эмулирует «железо» и запускает полноценную гостевую операционную систему со своим ядром. Это надежно, но избыточно: чтобы запустить веб-сервер, требующий 50 МБ оперативной памяти, вам приходится запускать целую ОС, которая потребляет еще 1 ГБ и тратит минуты на загрузку.

Контейнеризация предлагает другой подход.

Контейнер не эмулирует оборудование и не запускает собственную ОС. Под капотом Linux контейнер — это обычный процесс, имеющий свой PID, точно такой же, какими мы управляли с помощью утилит top и kill. Вся магия Docker заключается в использовании встроенных механизмов ядра Linux:

  • Namespaces (пространства имен) изолируют процесс так, что он «видит» только себя, свою собственную файловую систему и свою виртуальную сеть.
  • Cgroups (контрольные группы) жестко ограничивают процесс в ресурсах: ему можно выделить ровно 1 CPU и 512 МБ памяти, и он не сможет «съесть» ресурсы соседних приложений.

Поскольку контейнер использует ядро хост-машины, он запускается за миллисекунды и почти не создает накладных расходов.

Образ и контейнер: теория неизменяемости

Чтобы запустить изолированный процесс, ему нужно подсунуть файловую систему, в которой лежат нужные библиотеки и код. В Docker эта файловая система называется образом (Image).

Образ — это неизменяемый шаблон (read-only). Это слепок файловой системы в конкретный момент времени. Вы не можете изменить готовый образ, вы можете только собрать новый.

Контейнер — это запущенный экземпляр образа. Когда Docker запускает контейнер, он берет неизменяемый образ и добавляет поверх него тонкий временный слой для записи (read-write layer). Приложение внутри контейнера думает, что может писать файлы на диск, но все эти изменения сохраняются только во временном слое.

Главное правило контейнеризации: Контейнеры эфемерны. Любые данные, записанные внутри контейнера, будут безвозвратно уничтожены при его удалении. Контейнер можно остановить, удалить и пересоздать в любой момент из исходного образа — и он должен работать точно так же.

Dockerfile — декларативный рецепт сборки

Если Bash-скрипт — это императивный набор команд («сделай шаг 1, затем шаг 2»), то Dockerfile — это декларативный рецепт создания образа. Вы описываете, как должна выглядеть файловая система, а Docker сам выполняет сборку.

Рассмотрим типичный Dockerfile для простого веб-приложения:

# 1. Задаем базовый образ (уже содержит минимальную ОС и Python)
FROM python:3.10-slim

# 2. Устанавливаем рабочую директорию внутри будущего образа
WORKDIR /opt/myapp

# 3. Копируем файл зависимостей с хост-машины внутрь образа
COPY requirements.txt .

# 4. Выполняем команду установки зависимостей (результат сохранится в образ)
RUN pip install -r requirements.txt

# 5. Копируем остальной исходный код
COPY . .

# 6. Указываем команду, которая будет запущена при старте контейнера
CMD ["python", "app.py"]

Инструкции, изменяющие файловую систему (например, RUN, COPY), создают новый слой в образе. Docker кэширует эти слои. Если вы измените только исходный код (шаг 5), Docker не будет заново скачивать базовый образ и переустанавливать зависимости (шаги 1-4) — он возьмет их из кэша, что делает сборку невероятно быстрой.

Практика: сборка, запуск и сеть

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

docker build -t my-python-app:v1 .

Флаг -t задает имя (тег) образа, а точка в конце указывает, что Dockerfile и файлы проекта находятся в текущей директории.

Теперь запустим контейнер из созданного образа:

docker run -d -p 8080:80 my-python-app:v1

Разберем ключевые флаги:

  • -d (detach) — запускает контейнер в фоновом режиме. Если не указать этот флаг, процесс привяжется к вашему stdout/stderr, и при закрытии терминала контейнер остановится (аналог отправки SIGTERM/SIGHUP).
  • -p 8080:80 (publish) — проброс портов.

Остановимся на сети подробнее. Внутри своего изолированного пространства имен (namespace) приложение слушает порт 80. Но этот порт существует только в виртуальной сети контейнера. Хост-машина ничего о нем не знает. Правило -p ХОСТ:КОНТЕЙНЕР связывает порт вашей реальной машины с портом контейнера. Когда внешний запрос приходит на порт 8080 вашего сервера, Docker перенаправляет его на порт 80 внутрь контейнера.

Управление данными: Volumes

Мы выяснили, что данные внутри контейнера исчезают при его удалении. Но как быть с базами данных или логами, которые мы хотим анализировать в /var/log?

Для постоянного хранения данных используются Volumes (тома) и Bind mounts (привязки). Самый простой способ — примонтировать конкретную директорию с хост-машины (bind mount) прямо внутрь контейнера с помощью флага -v.

docker run -d -p 5432:5432 -v /opt/postgres_data:/var/lib/postgresql/data postgres:15

В этом примере СУБД PostgreSQL работает внутри контейнера и пишет свои файлы в директорию /var/lib/postgresql/data. Однако благодаря флагу -v, физически эти файлы сохраняются на вашем сервере в директории /opt/postgres_data. Если контейнер с базой данных упадет, будет удален или заменен на новую версию, данные на диске сервера останутся в сохранности. При запуске нового контейнера вы просто снова примонтируете эту директорию, и база данных продолжит работу с того же места.

Docker меняет парадигму администрирования. Вам больше не нужно настраивать окружение на сервере, разрешать конфликты версий библиотек и писать сложные Bash-скрипты для деплоя. Серверу достаточно иметь установленный Docker, а всё приложение вместе со своей экосистемой приезжает в виде единого, готового к запуску образа.

Инфраструктура как код (IaC): управление конфигурациями с помощью Ansible

Инфраструктура как код (IaC): управление конфигурациями с помощью Ansible

Представьте, что написанный ранее Bash-скрипт для настройки сервера нужно выполнить не на одной машине, а на пятидесяти. На семнадцатом сервере моргнула сеть, и скрипт оборвался на середине: пользователь создан, но права на директорию не выданы, а сервис не запущен. Что делать дальше? Запустить скрипт заново? Но тогда команда useradd выдаст ошибку «пользователь уже существует», и выполнение снова прервётся. Это главная ловушка ручной автоматизации: скрипты отлично работают на чистой системе, но ломаются, если состояние системы изменилось.

Чтобы управлять десятками и сотнями серверов, индустрия перешла к концепции Infrastructure as Code (IaC) — инфраструктура как код. Вместо того чтобы писать инструкции как настраивать сервер, инженеры описывают, каким он должен быть в итоге.

Императивный и декларативный подходы

Разница между Bash-скриптами и инструментами IaC заключается в подходе к управлению состоянием.

Характеристика Императивный подход (Bash) Декларативный подход (Ansible)
Суть Описание пошаговых действий Описание желаемого конечного состояния
Пример из жизни «Пройди 100 метров, поверни направо, зайди в дверь» «Мне нужен адрес: ул. Ленина, 10»
Пример в коде mkdir /opt/app && chown nginx /opt/app «Директория /opt/app должна существовать, владелец — nginx»
Обработка ошибок Приходится писать проверки (if [ ! -d ... ]) Инструмент сам проверяет текущее состояние

Фундамент декларативного подхода — идемпотентность. В математике операция называется идемпотентной, если многократное её применение даёт тот же результат, что и однократное: f(f(x))=f(x)f(f(x)) = f(x).

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

Архитектура Ansible: автоматизация без агентов

Ansible — один из самых популярных инструментов управления конфигурациями. Его главное архитектурное преимущество — Agentless (безагентность).

Многие системы мониторинга или управления требуют установки специальной программы-шпиона (агента) на каждый целевой сервер. Ansible это не нужно. Ему достаточно стандартного протокола SSH, который уже есть в любом дистрибутиве Linux.

В экосистеме Ansible есть две роли:

  1. Control Node (Управляющий узел) — машина, на которой установлен сам Ansible (это может быть ваш ноутбук или выделенный CI-сервер). Отсюда запускаются команды.
  2. Managed Nodes (Управляемые узлы) — целевые серверы. На них не нужно устанавливать Ansible, нужен только открытый порт 22 (SSH) и Python (для выполнения сгенерированного кода).

Чтобы Control Node знал, куда подключаться, используется Inventory (инвентарный файл). Это простой текстовый список IP-адресов, сгруппированных по смыслу.

[webservers]
192.168.1.10
192.168.1.11

[databases]
192.168.1.20

Playbooks и Modules: пишем инфраструктуру

Код в Ansible пишется в формате YAML. Файл с описанием конфигурации называется Playbook (книга сценариев).

Playbook состоит из Plays (сценариев), которые привязывают группу серверов из Inventory к списку Tasks (задач). Каждая задача использует определённый Module — готовую абстракцию, которая умеет выполнять конкретную работу идемпотентно.

Вместо того чтобы писать сложные комбинации useradd, chmod и chown, мы используем модули. Сравним создание сервисного пользователя:

Было (Bash):

if ! id -u nginx > /dev/null 2>&1; then
    useradd -r -s /bin/false nginx
fi

Стало (Ansible, модуль user):

- name: Убедиться, что сервисный пользователь nginx существует
  user:
    name: nginx
    system: yes
    shell: /bin/false
    state: present

Ansible берёт готовый код модуля (написанный на Python), передаёт в него параметры из YAML, отправляет по SSH на целевой сервер, выполняет, возвращает результат (Exit Code и JSON-отчёт) и удаляет временные файлы.

Практика: деплой веб-сервера

Соберём полученные знания в единый Playbook, который подготовит сервер для веб-приложения. Нам нужно установить Nginx, закинуть конфигурационный файл и убедиться, что сервис запущен и переживёт перезагрузку системы.

---
- name: Настройка веб-серверов
  hosts: webservers
  become: yes # Выполнять задачи с правами sudo (root)

  tasks:
    - name: Установить пакет nginx
      apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Скопировать кастомный конфиг
      copy:
        src: ./my_nginx.conf
        dest: /etc/nginx/sites-available/default
        owner: root
        mode: '0644'

    - name: Убедиться, что Nginx запущен и добавлен в автозагрузку
      systemd:
        name: nginx
        state: started
        enabled: yes

Разбор модулей:

  • apt — управляет пакетами в Debian/Ubuntu. state: present гарантирует, что пакет установлен.
  • copy — берёт файл с Control Node (src) и кладёт на Managed Node (dest), попутно выставляя владельца и числовую маску прав доступа.
  • systemd — управляет процессами-демонами. Заменяет ручной ввод команд вроде systemctl start nginx.

Когда вы запускаете этот Playbook командой ansible-playbook -i inventory.ini deploy.yml, Ansible проверяет каждый шаг. Если Nginx уже установлен, он пометит задачу зелёным статусом ok и ничего не будет скачивать. Если конфиг изменился на вашей машине, он пометит задачу жёлтым статусом changed и перезапишет файл на сервере.

Инфраструктура стала предсказуемой. Теперь конфигурация серверов — это текстовые файлы, которые можно хранить в Git вместе с исходным кодом приложения, отслеживать историю изменений и откатываться к предыдущим версиям. Следующий шаг на пути DevOps — сделать так, чтобы эти Playbooks запускались автоматически при каждом обновлении кода.

Непрерывная интеграция и доставка (CI/CD): построение пайплайнов в GitLab CI

Непрерывная интеграция и доставка (CI/CD): построение пайплайнов в GitLab CI

Представьте, что разработчик исправил опечатку на главной странице сайта и отправил код в репозиторий. Чтобы это изменение попало на сервер, вам нужно зайти в терминал, собрать новый Docker-образ, отправить его в хранилище, а затем запустить Ansible Playbook для обновления приложения. На это уходит 10–15 минут. Если в команде десять разработчиков, и каждый вносит изменения пять раз в день, вы проведете весь рабочий день, просто нажимая стрелочку «вверх» в истории Bash и нажимая Enter. Мы уже автоматизировали сами действия с помощью Docker и Ansible, но запуск этих инструментов всё ещё остался ручным. Пришло время замкнуть цикл и заставить инфраструктуру обновляться автоматически при каждом обновлении кода.

От рутины к конвейеру: концепция CI/CD

В основе современной автоматизации разработки лежат две неразрывные практики, которые превращают написание кода и его развертывание в единый заводской конвейер.

Continuous Integration (CI) — Непрерывная интеграция. Это процесс, при котором код, написанный разработчиками, автоматически сливается в общую ветку, после чего сразу же собирается и проверяется. Главная цель CI — как можно раньше обнаружить ошибку. На этом этапе мы запускаем линтеры (проверку стиля кода), прогоняем автотесты и собираем артефакты (например, компилируем бинарный файл или собираем Docker-образ).

Continuous Delivery / Deployment (CD) — Непрерывная доставка / развертывание. Если этап CI прошел успешно, в дело вступает CD. Это автоматический перенос собранного артефакта на целевые серверы (тестовые или продуктовые). Разница между Delivery и Deployment заключается лишь в одной кнопке: при Delivery готовый релиз ждет ручного подтверждения от инженера для выкатки на «прод», а при Deployment процесс идет полностью автоматически до самого конца.

Вместе они образуют Pipeline (пайплайн, или конвейер). Пайплайн состоит из стадий (Stages), которые выполняются строго последовательно. Если на стадии тестирования код выдает ошибку (Exit Code отличен от нуля), конвейер немедленно останавливается.

Кто выполняет работу: GitLab Server и GitLab Runner

GitLab — это не просто хранилище Git-репозиториев. Это мощный комбайн, в котором встроен свой CI/CD-движок. Однако важно понимать архитектурное разделение: сам сервер GitLab не выполняет ваши скрипты и сборки.

Если бы сервер GitLab сам собирал Docker-образы сотен проектов, он бы быстро исчерпал процессорное время и оперативную память, а выполнение чужого кода на главном сервере создало бы огромную дыру в безопасности.

Для выполнения задач используются GitLab Runners — отдельные серверы или виртуальные машины, на которых установлена специальная программа-агент.

  1. Разработчик делает git push в GitLab.
  2. GitLab видит изменения и создает задачу (Job) в очереди.
  3. Runner, который постоянно опрашивает GitLab («Есть для меня работа?»), забирает эту задачу.
  4. Runner выполняет Bash-команды, собирает образы, деплоит их и отправляет логи обратно в GitLab.

Чаще всего Runner использует Docker-экзекьютор. Это значит, что для выполнения каждой задачи Runner создает временный изолированный контейнер, выполняет в нем команды, а затем уничтожает его. Это гарантирует, что сборки не будут влиять друг на друга и засорять файловую систему (вспомните концепцию эфемерности контейнеров).

Анатомия .gitlab-ci.yml

Чтобы объяснить GitLab, как именно должен выглядеть наш конвейер, используется декларативный подход. В корне репозитория создается файл .gitlab-ci.yml. Как и в случае с Ansible, используется синтаксис YAML.

Рассмотрим базовую структуру:

stages:
  - build
  - test
  - deploy

build_image:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    - echo "Начинаем сборку Docker-образа..."
    - docker build -t myapp:latest .
    - docker push myapp:latest

Разберем ключевые элементы:

  • stages — определяет порядок этапов. Задачи из этапа test не начнутся, пока не завершатся все задачи из build.
  • build_image — произвольное имя конкретной задачи (Job).
  • image — указывает Runner-у, какой Docker-образ использовать в качестве среды для выполнения этой задачи. В данном случае мы используем образ, внутри которого уже установлен Docker-клиент.
  • services — запускает дополнительные фоновые контейнеры. Здесь docker:dind (Docker-in-Docker) необходим, чтобы внутри изолированного Runner-а работал демон Docker для успешной сборки образов.
  • script — массив обычных Bash-команд. Все, что вы делали руками в терминале (перенаправление потоков, вызов утилит), пишется здесь.

Объединяем Docker и Ansible в пайплайне

Теперь свяжем знания из предыдущих глав. Наша цель: при обновлении кода автоматически собрать новый Docker-образ и с помощью Ansible развернуть его на сервере.

У нас возникает проблема безопасности. Чтобы Ansible мог подключиться к целевому серверу (Managed Node), ему нужен SSH-ключ. Хранить приватный SSH-ключ прямо в репозитории в открытом виде — грубейшее нарушение безопасности. Любой, кто получит доступ к коду, получит доступ к серверам.

Для решения этой проблемы используются CI/CD Variables (переменные). В настройках репозитория в веб-интерфейсе GitLab мы можем создать переменную типа File, например SSH_PRIVATE_KEY, и вставить туда содержимое ключа. Обычную текстовую переменную с многострочным ключом пометить как "Masked" (чтобы скрыть из логов) не получится из-за строгих ограничений GitLab на пробелы и переносы строк, поэтому тип File — это стандартный и самый безопасный способ передачи таких секретов. Во время выполнения пайплайна GitLab передаст эту переменную Runner-у в виде временного файла.

Посмотрим на готовый пайплайн деплоя:

stages:
  - build
  - deploy

# Стадия 1: Сборка образа
build_docker:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    # Авторизуемся во встроенном хранилище GitLab
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY/app:$CI_COMMIT_SHORT_SHA .
    - docker push $CI_REGISTRY/app:$CI_COMMIT_SHORT_SHA

# Стадия 2: Деплой через Ansible
deploy_ansible:
  stage: deploy
  image: willhallonline/ansible:2.15-ubuntu
  before_script:
    # Подготавливаем SSH-ключ из переменной типа File для работы Ansible
    - mkdir -p ~/.ssh
    - cp "$SSH_PRIVATE_KEY" ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - export ANSIBLE_HOST_KEY_CHECKING=False
  script:
    # Запускаем Playbook, передавая ему новую версию образа
    - ansible-playbook -i inventory.ini deploy.yml -e "app_version=$CI_COMMIT_SHORT_SHA"

В этом коде мы использовали встроенные переменные GitLab: $CI_REGISTRY для доступа к хранилищу образов (Container Registry) и $CI_COMMIT_SHORT_SHA (короткий хэш Git-коммита) в качестве тега для Docker-образа. Это гарантирует, что каждый образ уникален и привязан к конкретному изменению в коде.

На этапе deploy мы используем образ, в котором предустановлен Ansible. В блоке before_script (выполняется до основного скрипта) мы берем секретный SSH-ключ из переменной (для типа File переменная содержит путь к временному файлу), копируем его в стандартную директорию SSH и устанавливаем правильные права доступа 600 (вспомните систему прав Linux — только владелец может читать файл ключа). После этого запускается ansible-playbook, который идемпотентно обновляет состояние сервера.

Внедрив этот файл, мы замыкаем цикл DevOps. Разработчику достаточно отправить код в ветку, а GitLab CI и Runner возьмут на себя всю рутину по сборке и доставке, используя написанные нами Dockerfile и Playbook.