Initial commit: interview prep repo (resume, legend, stack cases, Kubernetes course)

This commit is contained in:
Tot Maxim
2026-07-18 00:37:41 +03:00
commit 9bb7f6263f
47 changed files with 3548 additions and 0 deletions

148
stack/ansible/CASE.md Normal file
View File

@@ -0,0 +1,148 @@
# Кейс: роль Ansible + тестирование в Molecule
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «АО ТНИИС», автоматизация установки/настройки серверов, роли и плейбуки, тестирование через Molecule перед CI (TeamCity).
## Что нужно реально сделать (домашний стенд)
Написать Ansible-роль, разворачивающую и конфигурирующую Nginx, и протестировать её через Molecule (Docker-драйвер) — без реальных серверов, но с реальным прогоном роли и проверкой результата.
### 1. Структура
```
ansible-lab/
├── ansible.cfg
├── inventory/
│ └── hosts.ini
├── playbook.yml
└── roles/
└── nginx_deploy/
├── defaults/main.yml
├── tasks/main.yml
├── handlers/main.yml
├── templates/nginx.conf.j2
└── molecule/
└── default/
├── molecule.yml
├── converge.yml
└── verify.yml
```
### 2. roles/nginx_deploy/defaults/main.yml
```yaml
nginx_worker_connections: 1024
nginx_server_name: lab.local
```
### 3. roles/nginx_deploy/tasks/main.yml
```yaml
- name: Установить nginx
ansible.builtin.package:
name: nginx
state: present
- name: Развернуть конфиг nginx из шаблона
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: "0644"
notify: reload nginx
- name: Убедиться, что nginx запущен и в автозагрузке
ansible.builtin.service:
name: nginx
state: started
enabled: true
```
### 4. roles/nginx_deploy/handlers/main.yml
```yaml
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
```
### 5. roles/nginx_deploy/templates/nginx.conf.j2
```nginx
events {
worker_connections {{ nginx_worker_connections }};
}
http {
server {
listen 80;
server_name {{ nginx_server_name }};
location / {
return 200 'ok';
}
}
}
```
### 6. molecule/default/molecule.yml
```yaml
driver:
name: docker
platforms:
- name: nginx-instance
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
provisioner:
name: ansible
verifier:
name: ansible
```
### 7. molecule/default/converge.yml
```yaml
- name: Converge
hosts: all
roles:
- role: nginx_deploy
```
### 8. molecule/default/verify.yml
```yaml
- name: Verify
hosts: all
tasks:
- name: Проверить, что nginx слушает 80 порт
ansible.builtin.wait_for:
port: 80
timeout: 5
- name: Проверить HTTP-ответ
ansible.builtin.uri:
url: http://localhost/
status_code: 200
```
### 9. Шаги воспроизведения
1. `pip install molecule molecule-plugins[docker] ansible`.
2. `cd roles/nginx_deploy && molecule test` — это прогоняет полный цикл: create (поднять Docker-инстанс) → converge (применить роль) → idempotence (повторный прогон роли не должен ничего менять) → verify (проверки из verify.yml) → destroy.
3. Специально сломать что-то в шаблоне (например, опечатку в директиве nginx) и посмотреть, как `molecule test` падает на этапе converge или verify — это и есть демонстрация того, зачем нужен этот шаг перед CI.
4. Убедиться, что повторный прогон роли (idempotence check) не репортит изменений — если репортит, значит роль не идемпотентна, и это тоже частый вопрос на собеседовании.
### 10. Что это даёт в разговоре с интервьюером
- Реальное понимание, зачем нужен Molecule: тестировать роль в изолированном контейнере до того, как она попадёт в реальную инфраструктуру через CI.
- Понимание идемпотентности — краеугольного принципа Ansible.
- Практика с handlers, templates, defaults — стандартной структурой роли.
- Понимание, как Molecule встраивается в CI-конвейер (у меня локально `molecule test`, в TeamCity — тот же шаг, но в build step с прогоном перед деплоем).
## Нагрузка и цифры
Ansible сам по себе не «нагрузочная» технология в смысле RPS/TPS — релевантная метрика тут время прогона плейбука на N хостов (параллелизм через `forks` в `ansible.cfg`). Цифры не зафиксированы отдельно — общий подход к честным цифрам масштаба (три слоя: публичные ориентиры / лабные замеры / реальные факты с работы) описан в [../../legend/CAPACITY.md](../../legend/CAPACITY.md).
## Как это ложится в легенду
В реальной работе (АО ТНИИС) — 15 скриптов/ролей Ansible с управлением конфигурациями через роли и модули, тестирование в CI/CD через Molecule и TeamCity. Домашний кейс воспроизводит тот же цикл в миниатюре на одной роли вместо полного набора инфраструктурных ролей.

View File

@@ -0,0 +1,59 @@
# Вопросы: Ansible
Опираются на кейс: [CASE.md](CASE.md).
### Что такое идемпотентность и почему это важно в Ansible?
Идемпотентность — свойство операции давать один и тот же результат при повторном запуске, не ломая и не дублируя изменения. В Ansible это значит, что повторный прогон плейбука на уже настроенном хосте не должен ничего менять (задачи должны показывать `ok`, а не `changed`). Я это проверял на своей роли nginx_deploy через `molecule test` — там есть отдельный шаг idempotence, который прогоняет converge второй раз и падает, если что-то опять помечено как changed.
### Чем роль (role) отличается от плейбука (playbook)?
Плейбук — это конкретный сценарий: какие хосты, какие роли/задачи применить и в каком порядке. Роль — переиспользуемый, самодостаточный блок автоматизации со своей структурой папок (tasks, handlers, templates, defaults, vars), который можно подключать в разные плейбуки. У меня playbook.yml просто вызывает роль nginx_deploy — так удобнее переиспользовать её в других плейбуках без копирования кода.
### Зачем нужны handlers и чем они отличаются от обычных задач?
Handler — задача, которая выполняется только по уведомлению (`notify`) от другой задачи, и только один раз в конце плейбука, даже если её нотифицировали несколько раз. Типичный пример — перезагрузка сервиса только если поменялся конфиг: в моей роли задача `template` нотифицирует хендлер `reload nginx`, и nginx перезагружается только тогда, когда конфиг реально изменился, а не при каждом прогоне.
### Что такое defaults/vars и в чём разница между ними по приоритету?
`defaults/main.yml` — переменные с самым низким приоритетом, задуманы как значения по умолчанию, которые легко переопределить снаружи роли (в inventory, playbook, group_vars). `vars/main.yml` — переменные роли с более высоким приоритетом, обычно используются для внутренних констант роли, которые не предполагается менять снаружи. В своей роли я вынес `nginx_worker_connections` и `nginx_server_name` в defaults именно потому, что это то, что вызывающий плейбук может захотеть переопределить.
### Зачем нужен Molecule, если можно просто запустить playbook на тестовом сервере?
Molecule автоматизирует весь цикл проверки роли: поднимает изолированное окружение (у меня — Docker-контейнер), прогоняет роль, проверяет идемпотентность, гоняет verify-тесты и уничтожает окружение — и всё это одной командой, без ручного создания и удаления тестовых серверов. Это то, что можно встроить в CI (в моей практике — TeamCity), чтобы роль проверялась автоматически на каждый коммит до попадания в прод.
### Чем отличается модуль `command`/`shell` от специализированных модулей вроде `service`, `package`, `template`?
Специализированные модули декларативны и идемпотентны из коробки — они сами проверяют текущее состояние и меняют его только при необходимости (например, `service` не будет перезапускать уже запущенный сервис). `command`/`shell` выполняют произвольную команду безусловно и по умолчанию всегда репортят `changed`, что ломает идемпотентность, если не добавлять вручную `creates`/`changed_when`. В своей роли я использовал `package`, `template`, `service` именно чтобы не терять идемпотентность.
### Как Ansible понимает, какие хосты — цель для выполнения?
Через inventory — статический файл (INI/YAML со списком хостов и групп) или динамический inventory-скрипт/плагин, который генерирует список из внешнего источника (облако, CMDB и т.п.). В playbook секция `hosts:` указывает, на какую группу/хост из inventory применять роль. В лабе у меня Molecule сам генерирует временный inventory на Docker-инстанс, в проде — обычно статический или динамический inventory отдела.
### Как безопасно хранить секреты (пароли, токены) в Ansible?
Через `ansible-vault` — шифрование файлов с чувствительными переменными, которые потом расшифровываются на лету при прогоне плейбука с паролем/ключом vault. Секреты не должны лежать в открытом виде в репозитории с ролями.
### Что произойдёт, если задача в роли завершится с ошибкой на середине выполнения?
По умолчанию Ansible останавливает выполнение плейбука на этом хосте (fail fast) — дальнейшие задачи для этого хоста не выполняются, но выполнение на других хостах продолжается независимо (если запуск на несколько хостов). Это поведение можно переопределить через `ignore_errors`, `block/rescue` для обработки ошибок или `any_errors_fatal` для остановки всего прогона.
### Как вы тестировали конкретно свою роль и что бы произошло, если бы шаблон был некорректным?
Я специально ломал `nginx.conf.j2` (опечатка в директиве) и запускал `molecule test` — прогон падал на этапе converge, потому что Ansible применяет конфиг, а nginx не стартует/не резолвит синтаксис, что видно в выводе `service` модуля. Это и есть основная ценность прогона перед CI — ошибка находится сразу, а не после деплоя на реальный сервер.
### Где в Ansible живут переменные — в ролях, плейбуках или инвентарях, и зачем это разделение?
Переменные могут жить на всех трёх уровнях, и у каждого — своя роль. В роли (`defaults/main.yml`, `vars/main.yml`) — значения, специфичные для самой роли (см. вопрос про defaults/vars выше). В плейбуке (`vars:` секция) — значения, специфичные для конкретного сценария применения роли. В inventory (`group_vars/`, `host_vars/`) — значения, специфичные для конкретной группы хостов или отдельного хоста (например, разный `nginx_server_name` для разных окружений). Разделение нужно, чтобы одна и та же роль оставалась переиспользуемой: логика роли не меняется, а конкретные значения приходят снаружи в зависимости от того, где и для чего роль применяется.
### Как переопределить переменную роли?
Проще всего — передать её на более высоком приоритете, чем `defaults/main.yml` (самый низкий приоритет намеренно, чтобы легко переопределять): через `group_vars`/`host_vars` в inventory, через `vars:` в самом плейбуке при вызове роли, или через `-e` в командной строке (`ansible-playbook playbook.yml -e "nginx_worker_connections=2048"``-e` имеет наивысший приоритет из всех источников). В своей роли `nginx_deploy` я именно поэтому вынес `nginx_worker_connections` в `defaults`, а не в `vars` — чтобы вызывающий плейбук/inventory мог его свободно переопределить без правки самой роли.
### Могут ли роль и плейбук существовать по отдельности, и что будет, если убрать один из них?
Плейбук без роли — вполне рабочий сценарий: можно писать задачи прямо в плейбуке (`tasks:` напрямую), просто без переиспользуемой структуры роли — так делают для одноразовых/простых сценариев. Роль без плейбука сама по себе не выполнится — роль не имеет собственного понятия «на каких хостах запускаться», это просто набор задач/шаблонов/дефолтов; ей обязательно нужен плейбук (или `ansible-console`/adhoc-вызов через `include_role` из другого плейбука), который укажет `hosts:` и подключит роль. То есть роль — переиспользуемый строительный блок, плейбук — то, что решает, где и когда этот блок применить.
### Разбор команды: `ansible -m shell -i inventory.yml {{ansible_hostname}}` — что делает эта команда с ключами поэтапно?
По шагам: `ansible` — adhoc-режим (разовая команда без плейбука, в отличие от `ansible-playbook`); `-i inventory.yml` — указывает, какой inventory-файл использовать для определения хостов и их переменных; `-m shell` — указывает модуль `shell` (выполнить произвольную команду через шелл на целевом хосте, в отличие от специализированных идемпотентных модулей — см. вопрос про command/shell выше); `{{ ansible_hostname }}` в этом месте выглядит некорректно синтаксически как есть — Jinja2-конструкции `{{ }}` предназначены для использования внутри YAML/шаблонов и аргументов модулей, а не как позиционный аргумент-паттерн хостов в CLI-вызове `ansible` (туда обычно подставляется паттерн хостов из inventory, например `all` или имя группы). На такой вопрос честный ответ — указать на это несоответствие и уточнить у интервьюера, что именно должно было стоять на этом месте, а не пытаться выдумать интерпретацию.

95
stack/ci-cd/CASE.md Normal file
View File

@@ -0,0 +1,95 @@
# Кейс: CI/CD (Git, Gitea, GitLab, TeamCity)
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — GitLab (ОКБ СУХОЙ, хранение тестовых скриптов), Gitea + TeamCity (АО ТНИИС, хранение Ansible-ролей и CI-конвейер с Molecule).
## Что нужно реально сделать (домашний стенд)
Поднять Gitea локально, запушить туда роль Ansible из [../ansible/CASE.md](../ansible/CASE.md), и настроить пайплайн через GitHub Actions (доступная бесплатная альтернатива TeamCity для домашней практики — принцип pipeline/stage/job идентичен, конкретный синтаксис отличается; на собеседовании это стоит явно проговаривать как «дома тренировался на GitHub Actions, на текущей работе — TeamCity, принцип тот же»).
### 1. Gitea в Docker Compose
```yaml
version: "3.8"
services:
gitea:
image: gitea/gitea:1.22
container_name: gitea
environment:
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__ROOT_URL=http://localhost:3001/
ports:
- "3001:3000"
- "2222:22"
volumes:
- gitea_data:/data
volumes:
gitea_data:
```
```bash
docker compose up -d
# зайти на localhost:3001, создать администратора, создать репозиторий "ansible-nginx-role"
git remote add gitea http://localhost:3001/<user>/ansible-nginx-role.git
git push gitea main
```
### 2. Пайплайн, прогоняющий molecule test на каждый push
```yaml
# .github/workflows/molecule.yml (для GitHub как доступной альтернативы; на реальном месте — эквивалент в TeamCity)
name: Molecule Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install ansible molecule molecule-plugins[docker] docker
- name: Run molecule test
run: molecule test
```
Смысл именно в том, чтобы это был реальный прогон уже готовой роли из [../ansible/CASE.md](../ansible/CASE.md): пайплайн скачивает код, ставит зависимости и выполняет тот же `molecule test`, который до этого прогонялся вручную — то есть проверка идемпотентности и корректности роли теперь происходит автоматически при каждом push, без ручного запуска.
### 3. Модель веток
```bash
git checkout -b feature/add-ssl-support
# правки роли
git push gitea feature/add-ssl-support
# создать Pull Request в Gitea UI: feature/add-ssl-support -> main
```
Protected branch (`main`) в настройках репозитория Gitea — запрет прямого push, обязательное прохождение пайплайна (CI status check) и минимум одного ревью перед merge. Смысл: код не может попасть в основную ветку, минуя автоматическую проверку и код-ревью.
### 4. Базовые операции Git на практике
```bash
git rebase main # переносит коммиты ветки поверх актуального main, история линейная
git merge main # создаёт merge-коммит, сохраняет реальную историю параллельной разработки
git cherry-pick <commit-hash> # переносит один конкретный коммит в текущую ветку
git bisect start
git bisect bad # текущий коммит содержит баг
git bisect good <старый-коммит> # этот коммит был рабочим
# git автоматически предлагает коммиты между ними для бинарного поиска, где баг появился
```
## Что это даёт в разговоре с интервьюером
- Реально запушенный в Gitea код и реально прогнанный через CI пайплайн, проверяющий Ansible-роль — а не абстрактное «умею писать YAML».
- Понимание разницы rebase/merge не как определения, а с пониманием, когда какой подход уместен (rebase — для чистки локальной истории перед PR, merge — для сохранения факта параллельной разработки).
- Практическое понимание protected branches как механизма, а не просто слов.
- Умение нарисовать и объяснить типовой pipeline: lint → test/molecule → deploy — то, что реально спрашивали на нескольких собеседованиях (см. [../../interview/real-interviews.md](../../interview/real-interviews.md)).
## Как это ложится в легенду
Git-хостинги и CI — сквозная инфраструктура вокруг уже проработанного Ansible-кейса. Здесь не отдельный «продукт» для тестирования — кейс встраивает существующую роль в реальный пайплайн, замыкая рассказ «написал роль → протестировал локально через Molecule → теперь это же автоматически проверяется в CI на каждый push».

31
stack/ci-cd/QUESTIONS.md Normal file
View File

@@ -0,0 +1,31 @@
# Вопросы: CI/CD
Опираются на кейс: [CASE.md](CASE.md). Источники — [Реалист банк.md](../../interview/Реалист%20банк.md), [Bi.Zone.md](../../interview/Bi.Zone.md), [Alfa-Bank.md](../../interview/Alfa-Bank.md).
### В чём разница между CI и CD?
CI (Continuous Integration) — автоматическая проверка кода при каждом изменении: сборка, линтинг, тесты — цель поймать проблему как можно раньше, до слияния с основной веткой. CD (Continuous Delivery/Deployment) — автоматизация доставки уже проверенного кода до среды исполнения (staging/production) — сборка артефакта/образа, публикация в registry, применение на целевой инфраструктуре. Граница на практике: CI заканчивается там, где код признан «годным» (все тесты прошли, собран артефакт), CD начинается с того, что этот артефакт доставляется дальше.
### Нарисуй CI/CD и как проливается код через GitLab CI — что будет в процессе CI и что в CD (с этапа, когда код уже запушен в GitHub, а образы в Nexus)?
От push в GitHub: (1) webhook/триггер запускает pipeline в GitLab CI; (2) CI-часть — стадии `lint` (статический анализ) → `test` (юнит/интеграционные тесты, в моей практике — `molecule test` для Ansible-ролей) → `build` (сборка Docker-образа); (3) стадия публикации — образ пушится в Nexus как container registry с тегом (обычно commit SHA или семантическая версия); (4) CD-часть начинается с деплоя — pipeline берёт собранный образ именно из Nexus (не пересобирает заново) и применяет его на целевом окружении (`kubectl apply`/`helm upgrade` при K8s, или через Ansible-плейбук деплоя при классической инфраструктуре) — сначала обычно на staging, дальше по ручному approve или автоматически на production. Ключевой принцип: то, что задеплоено в prod — это тот же самый бинарный артефакт (образ), что прошёл все проверки CI, без пересборки на каждом этапе.
### Что такое pipeline/stage/job?
Pipeline — весь процесс целиком, от триггера (push/MR) до конечного результата (успешный деплой или явный fail). Stage — логическая фаза внутри pipeline (например, `build`, `test`, `deploy`) — stages выполняются последовательно. Job — конкретная выполняемая задача внутри stage (например, в stage `test` могут быть параллельно запущены job `unit-tests` и job `molecule-test`) — job'ы внутри одного stage обычно выполняются параллельно, если нет явных зависимостей.
### Зачем нужны protected branches?
Защита основной ветки (обычно `main`/`master`) от прямых push и слияний без прохождения обязательных проверок — требует, чтобы изменения проходили через Merge/Pull Request с обязательным прохождением CI (status checks) и, как правило, минимум одного ревью. Смысл — предотвратить попадание непроверенного или сломанного кода прямо в ветку, с которой разворачивается прод.
### Как откатить плохой деплой?
Зависит от инфраструктуры: при K8s — `kubectl rollout undo deployment/<name>` (возврат к предыдущей ревизии Deployment) или `helm rollback <release> <revision>` при Helm-релизах. При классическом деплое через Ansible/образы — передеплоить предыдущий проверенный тег образа (тот самый принцип «в prod идёт конкретный собранный артефакт из registry» из вопроса про GitLab CI выше — откат это просто повторный деплой более старого тега). Важно, чтобы откат был таким же автоматизированным процессом через тот же pipeline, а не ручными правками на проде.
### Чем отличается git merge --no-ff от fast-forward merge?
Fast-forward merge происходит, когда в целевой ветке не было новых коммитов после создания feature-ветки — Git просто «продвигает» указатель ветки вперёд, без создания отдельного merge-коммита, история выглядит линейной, как будто feature-ветки не было вовсе. `git merge --no-ff` принудительно создаёт merge-коммит, даже если fast-forward был бы возможен — это сохраняет в истории явный факт, что была отдельная ветка разработки, что удобно для читаемости истории и для возможности откатить весь набор изменений одним `revert` merge-коммита.
### Как выглядит типичный pipeline для инфраструктурного репозитория?
`lint` (например, `ansible-lint` для проверки стиля и распространённых ошибок в ролях) → `test` (`molecule test` — идемпотентность и корректность роли в изолированном окружении, см. [../ansible/CASE.md](../ansible/CASE.md)) → `deploy` (применение роли/плейбука на целевую инфраструктуру, обычно с разделением на staging и production stage с ручным approve перед production). Именно эту цепочку я воспроизвёл в [CASE.md](CASE.md) — начиная с `push` в Gitea и заканчивая автоматическим прогоном `molecule test` в пайплайне.

90
stack/databases/CASE.md Normal file
View File

@@ -0,0 +1,90 @@
# Кейс: PostgreSQL (репликация и базовые запросы)
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «Домашняя лаборатория / pet-проект». SQL как язык запросов уже вписан в легенду как сквозной инструмент (работа с данными сопровождаемых сервисов в АО ТНИИС), но отдельного опыта администрирования/репликации самой СУБД в рабочей практике не было — это тоже честный pet-кейс поверх рабочего опыта с запросами.
## Что нужно реально сделать (домашний стенд)
Поднять master + одну streaming-реплику PostgreSQL в Docker Compose, попробовать синхронный и асинхронный режим репликации, прогнать `pgbench` для базовых цифр производительности и потренировать SQL-запросы на простой учебной схеме.
### 1. docker-compose.yml — master + replica
```yaml
version: "3.8"
services:
pg-master:
image: postgres:16
container_name: pg-master
environment:
POSTGRES_PASSWORD: labpass
POSTGRES_USER: labuser
POSTGRES_DB: labdb
command: >
postgres
-c wal_level=replica
-c max_wal_senders=5
-c max_replication_slots=5
-c synchronous_commit=on
-c synchronous_standby_names='FIRST 1 (replica1)'
volumes:
- ./master-init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
pg-replica:
image: postgres:16
container_name: pg-replica
environment:
POSTGRES_PASSWORD: labpass
PGUSER: labuser
depends_on:
- pg-master
ports:
- "5433:5432"
entrypoint: >
bash -c "
until pg_basebackup -h pg-master -D /var/lib/postgresql/data -U labuser -Fp -Xs -P -R --slot=replica1 --create-slot;
do echo waiting for master; sleep 2; done;
echo \"primary_conninfo = 'host=pg-master port=5432 user=labuser application_name=replica1'\" >> /var/lib/postgresql/data/postgresql.auto.conf;
exec postgres"
```
### 2. master-init.sql — учебная схема
```sql
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT REFERENCES customers(id),
amount NUMERIC(10,2),
status TEXT
);
INSERT INTO customers (name) VALUES ('Иванов'), ('Петров'), ('Сидоров');
INSERT INTO orders (customer_id, amount, status)
VALUES (1, 1500.00, 'paid'), (1, 300.00, 'pending'), (2, 4200.00, 'paid');
```
### 3. Шаги воспроизведения
1. `docker compose up -d` — поднять master, дождаться, пока replica пройдёт `pg_basebackup` и подключится (`docker compose logs -f pg-replica`).
2. Проверить репликацию: `psql -h localhost -p 5432 -U labuser labdb -c "INSERT INTO customers (name) VALUES ('Новый клиент')"`, затем `psql -h localhost -p 5433 -U labuser labdb -c "SELECT * FROM customers"` — строка должна появиться на реплике.
3. Проверить статус на master: `SELECT * FROM pg_stat_replication;` — увидеть `replica1`, `state = streaming`, `sync_state = sync` (при заданном `synchronous_standby_names`).
4. Переключить на асинхронный режим: убрать `synchronous_standby_names` из command, перезапустить master, повторить `pg_stat_replication``sync_state` сменится на `async`. Разница на практике: при `sync` транзакция на master не считается закоммиченной, пока реплика не подтвердила запись (гарантия нуля потерянных данных при падении master ценой задержки commit); при `async` master коммитит сразу, не дожидаясь реплики (быстрее, но при падении master возможна потеря последних транзакций).
5. Погонять `pgbench` для базовых цифр: `pgbench -h localhost -p 5432 -U labuser -i labdb` (инициализация), `pgbench -h localhost -p 5432 -U labuser -c 10 -j 2 -T 30 labdb` (10 клиентов, 30 секунд) — записать TPS в [../../legend/CAPACITY.md](../../legend/CAPACITY.md).
6. Потренировать запросы на схеме `customers`/`orders`: JOIN, агрегации, `WHERE id = N` — см. [QUESTIONS.md](QUESTIONS.md).
## Что это даёт в разговоре с интервьюером
- Практическое понимание разницы sync/async репликации не как определения, а как наблюдаемого поведения (`pg_stat_replication`, разная задержка commit).
- Понимание streaming-репликации через WAL и `pg_basebackup` — как реплика вообще получает данные.
- Базовые цифры TPS со своей лабы — честная отправная точка для разговора про производительность (см. [../../legend/CAPACITY.md](../../legend/CAPACITY.md)).
- Уверенное владение основными типами JOIN и агрегатными запросами на конкретной схеме.
## Как это ложится в легенду
PostgreSQL как СУБД — pet-кейс, не приписанный к опыту в компаниях. SQL как язык запросов к данным сопровождаемых сервисов остаётся в легенде как рабочий навык (см. [../../legend/LEGEND.md](../../legend/LEGEND.md)); этот кейс добавляет к нему более глубокое, честно обозначенное pet-понимание того, как устроена сама СУБД под капотом.

View File

@@ -0,0 +1,43 @@
# Вопросы: PostgreSQL / базы данных
Опираются на кейс: [CASE.md](CASE.md). Источники реальных вопросов — [Реалист банк.md](../../interview/Реалист%20банк.md), [Alfa-Bank.md](../../interview/Alfa-Bank.md), [VK Cloud.md](../../interview/VK%20Cloud.md).
### Расскажи про синхронный и асинхронный режим работы PostgreSQL — в чём разница?
При синхронной репликации (`synchronous_commit=on` + `synchronous_standby_names`) master не подтверждает клиенту commit транзакции, пока хотя бы одна указанная реплика не подтвердила запись WAL — это гарантирует нулевую потерю данных при падении master ценой более медленного commit. При асинхронной репликации master коммитит сразу и отправляет WAL реплике фоном — быстрее, но при падении master прямо перед репликацией последние транзакции можно потерять. У себя в стенде я явно переключал этот параметр и видел разницу в `pg_stat_replication.sync_state` (`sync``async`), а не просто читал про это.
### Как будешь разворачивать PostgreSQL на две ноды?
Классическая схема — master + streaming replica: реплика инициализируется через `pg_basebackup` с master и дальше получает изменения через WAL-стриминг по replication slot. Дальше поверх этого нужен failover-механизм (например, Patroni + etcd/Consul для автоматического переключения при падении master) — сам failover-контроллер я не поднимал, но принцип базовой репликации прогнал в Docker Compose на две ноды (master + replica) и вижу, из каких частей состоит более сложная HA-схема поверх этого.
### Какие виды JOIN существуют и как они работают?
`INNER JOIN` — только строки, где есть совпадение в обеих таблицах. `LEFT JOIN` — все строки левой таблицы + совпадения справа (NULL, если совпадения нет). `RIGHT JOIN` — зеркально, все строки правой таблицы. `FULL OUTER JOIN` — все строки из обеих таблиц, с NULL там, где нет совпадения. На своей учебной схеме `customers`/`orders`: `SELECT c.name, o.amount FROM customers c LEFT JOIN orders o ON o.customer_id = c.id` покажет всех клиентов, включая тех, у кого нет заказов (у меня в примере таких не было, но проверял именно так, добавляя клиента без заказов).
### Что такое кластер (в контексте баз данных)?
В контексте PostgreSQL термин многозначный: (1) «кластер БД» на уровне процесса — это весь набор баз данных, которыми управляет один экземпляр `postgres` (директория данных, `initdb`); (2) в контексте отказоустойчивости — группа из нескольких инстансов (master + реплики), обеспечивающая доступность и/или масштабирование чтения. У себя в лабе я поднял именно второй случай — master + reплика как единый логический кластер с общими данными.
### Как происходит репликация базы данных?
Master пишет все изменения в WAL (write-ahead log) до применения к данным. Реплика подключается к master через replication slot, получает поток WAL-записей (streaming replication) и применяет их у себя, воспроизводя то же состояние с небольшой задержкой (или без задержки — при sync-режиме). Начальное состояние реплика получает через `pg_basebackup` — полную копию данных на момент старта, дальше — только дельты через WAL.
### Что такое quorum в конфигурации master-master (или в отказоустойчивом кластере вообще)?
Кворум — минимальное количество узлов, которое должно быть согласно/доступно, чтобы кластер считал своё решение валидным (обычно больше половины от общего числа узлов). Нужен, чтобы избежать одновременного принятия противоречащих решений разными частями кластера при разрыве сети — при потере кворума меньшая часть кластера не имеет права принимать решения (например, выбирать нового master), что и предотвращает split brain.
### Расскажи про split brain и почему кворум помогает?
Split brain — ситуация, когда из-за разрыва сети кластер разделяется на две части, и обе считают себя главными/рабочими одновременно (например, две ноды одновременно думают, что они master и принимают записи) — это ведёт к расхождению данных. Кворум решает эту проблему: только та часть кластера, где узлов больше половины от общего числа, имеет право принимать решения (выбирать нового лидера, подтверждать транзакции); меньшая часть автоматически переходит в read-only или отказывает в обслуживании, не считая себя валидным большинством.
### Есть таблица orders — какая команда выведет количество записей, где id = 15?
```sql
SELECT COUNT(*) FROM orders WHERE id = 15;
```
Так как `id` — первичный ключ, результат либо 0, либо 1; если бы вопрос был про количество заказов конкретного клиента, это было бы `SELECT COUNT(*) FROM orders WHERE customer_id = 15;`.
### Что такое скоринг и кредитный конвейер? (контекст банковских вакансий)
Скоринг — процесс автоматической оценки кредитоспособности клиента по набору параметров (доход, кредитная история и т.д.), результат — числовой балл или решение одобрить/отклонить. Кредитный конвейер — сквозной автоматизированный процесс обработки заявки на кредит от подачи до решения: сбор данных → проверки (антифрод, скоринг, внешние бюро) → решение → оформление. Для инженера сопровождения/DevOps практический смысл в том, что это высоконагруженный и критичный по SLA процесс — отсюда повышенные требования к мониторингу и отказоустойчивости систем, которые его обслуживают.

118
stack/docker/CASE.md Normal file
View File

@@ -0,0 +1,118 @@
# Кейс: Docker
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «АО ТНИИС», контейнеризация сервисов мониторинга и вспомогательных инструментов. Практика опирается на уже готовые стенды [../monitoring/CASE.md](../monitoring/CASE.md) и [../nginx/CASE.md](../nginx/CASE.md) — оба используют Docker Compose.
## Что нужно реально сделать
### 1. Dockerfile для собственного простого сервиса (multi-stage build)
```dockerfile
# build stage
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY . .
RUN go build -o /app ./...
# итоговый образ — только бинарник, без всей тулчейна сборки
FROM alpine:3.19
COPY --from=build /app /app
ENTRYPOINT ["/app"]
```
```
# .dockerignore
.git
*.md
node_modules
```
Multi-stage build: первый этап содержит весь тяжёлый тулчейн сборки (компилятор, зависимости), второй — только финальный артефакт. Итоговый образ в разы меньше и не тащит в прод лишние инструменты сборки. Слои кешируются построчно — `COPY . .` перед `RUN build` означает, что при изменении любого файла кеш слоя сборки инвалидируется; на реальных проектах порядок команд специально выстраивают так, чтобы редко меняющиеся зависимости копировались раньше исходного кода.
### 2. Volumes: bind mount vs named volume
```bash
# bind mount — конкретный путь на хосте, удобно для конфигов и разработки
docker run -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro nginx
# named volume — управляется самим Docker, удобно для данных (БД, персистентное состояние)
docker volume create pgdata
docker run -v pgdata:/var/lib/postgresql/data postgres
```
Bind mount используется в кейсах [../monitoring/CASE.md](../monitoring/CASE.md) и [../nginx/CASE.md](../nginx/CASE.md) для конфигов (`prometheus.yml`, `nginx.conf`) — удобно редактировать файл на хосте и сразу видеть изменения в контейнере. Named volume лучше подходит для данных, которые не нужно редактировать руками с хоста и которые должны переживать пересоздание контейнера (в [../databases/CASE.md](../databases/CASE.md) для этого пригодился бы именно named volume под `/var/lib/postgresql/data`).
### 3. Сети Docker: как контейнеры находят друг друга
```bash
docker network ls
docker network inspect <compose-project>_default
```
В Docker Compose по умолчанию создаётся отдельная bridge-сеть на проект, и все сервисы внутри неё резолвят друг друга по имени сервиса через встроенный DNS Docker — именно поэтому в [../monitoring/CASE.md](../monitoring/CASE.md) Prometheus обращается к `node-exporter:9100`, а не по IP: имя сервиса из `docker-compose.yml` работает как hostname внутри этой сети.
### 4. Ограничение ресурсов и что происходит при превышении
```yaml
services:
app:
image: myapp
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
```
```bash
docker run --memory=256m --cpus=0.5 myapp
```
При превышении лимита памяти ядро (через cgroups, см. [../linux-bash/QUESTIONS.md](../linux-bash/QUESTIONS.md)) убивает процесс через OOM killer — контейнер завершается с кодом 137 (128 + сигнал 9 SIGKILL). Воспроизвести: запустить в контейнере с `--memory=50m` процесс, который выделяет память сверх лимита (`stress --vm 1 --vm-bytes 100M`), и увидеть код выхода 137 через `docker inspect <container> --format '{{.State.ExitCode}}'`.
### 5. Диагностика упавшего контейнера
```bash
docker logs <container>
docker logs --tail 50 -f <container>
docker exec -it <container> sh
docker inspect <container>
docker inspect <container> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
```
### 6. Внутренности: виртуализация vs контейнеризация, containerd, PID контейнера
Виртуализация (VM) — эмулирует полное отдельное «железо» с собственным ядром ОС через гипервизор (VMware/KVM/Hyper-V) — тяжелее, но полная изоляция вплоть до ядра. Контейнеризация — все контейнеры на хосте используют одно и то же ядро хоста, изоляция обеспечивается namespaces (что процесс видит) и ограничение ресурсов — cgroups (сколько он может использовать), без отдельного гостевого ядра — отсюда контейнеры легче и стартуют быстрее VM.
`containerd` — низкоуровневый контейнерный рантайм, которым Docker Engine пользуется под капотом для реального управления жизненным циклом контейнеров (запуск, остановка, управление образами) — сам Docker CLI/демон — более высокоуровневая обвязка поверх containerd с удобным UX (`docker build`, `docker compose` и т.п.). Docker понимает, что контейнер «умер», потому что containerd отслеживает главный процесс контейнера (PID 1 внутри контейнера) — как только этот процесс завершается (сам или из-за ошибки), containerd фиксирует это событие и помечает контейнер как `Exited`.
```bash
docker inspect <container> --format '{{.State.Pid}}' # PID процесса контейнера на хосте
ps aux | grep <тот же PID> # виден и на хосте — контейнер это просто изолированный процесс хоста
```
### 7. Entrypoint vs CMD
```dockerfile
ENTRYPOINT ["myapp"]
CMD ["--config", "/etc/myapp/default.yml"]
```
`ENTRYPOINT` задаёт неизменяемую (без явного `--entrypoint` при запуске) главную команду контейнера. `CMD` задаёт аргументы по умолчанию к ней, которые легко переопределить при `docker run myapp --config /other.yml` — эта строка заменит именно `CMD`, не трогая `ENTRYPOINT`. Если задан только `CMD` без `ENTRYPOINT` — вся строка `CMD` целиком заменяется аргументами `docker run`.
### 8. Копирование файлов в работающий контейнер
```bash
docker cp ./local-file.txt <container>:/app/file.txt
docker cp <container>:/app/output.log ./output.log
```
## Что это даёт в разговоре с интервьюером
- Понимание, что контейнер технически — обычный процесс хоста с изоляцией через namespaces/cgroups, а не мини-VM.
- Практика multi-stage build и осознанного слоёного кеширования, а не просто «Dockerfile работает».
- Знание разницы bind mount/named volume не абстрактно, а с привязкой к тому, где какой тип реально применён в других кейсах репозитория.
- Воспроизведённый вживую OOM kill (код 137) — понимание на практике, а не только в теории.
## Как это ложится в легенду
В реальной работе (АО ТНИИС) Docker упоминается в стеке отдела как средство контейнеризации сервисов сопровождения. Этот кейс показывает собственный опыт сборки, сетевого взаимодействия и диагностики контейнеров поверх уже готовых стендов мониторинга и nginx.

58
stack/docker/QUESTIONS.md Normal file
View File

@@ -0,0 +1,58 @@
# Вопросы: Docker
Опираются на кейс: [CASE.md](CASE.md). Источники — [VK Cloud.md](../../interview/VK%20Cloud.md), [Реалист банк.md](../../interview/Реалист%20банк.md).
### В чём разница между образом (image) и контейнером?
Образ — неизменяемый шаблон: слоёная файловая система + метаданные (какая команда запускается по умолчанию, какие переменные окружения и т.д.), результат `docker build`. Контейнер — запущенный (или остановленный) экземпляр образа с добавленным поверх write-слоем, где живут любые runtime-изменения — по сути процесс на хосте плюс собственная изолированная файловая система, сеть и т.д. Один образ может породить много независимых контейнеров.
### В чём отличия виртуализации от контейнеризации?
Виртуализация эмулирует отдельное «железо» через гипервизор — у каждой VM своё полноценное ядро ОС, изоляция максимальная, но накладные расходы выше (полный загруженный образ ОС, медленнее старт). Контейнеризация — все контейнеры хоста делят одно и то же ядро хоста; изоляция достигается через namespaces (что процесс видит — свои PID, сеть, файловую систему) и ограничение ресурсов через cgroups (сколько он может использовать), см. [../linux-bash/QUESTIONS.md](../linux-bash/QUESTIONS.md). Контейнеры легче и стартуют за секунды, но изоляция менее строгая (общее ядро — потенциальная поверхность атаки при уязвимостях в ядре).
### Как Docker понимает, что контейнер умер, и помечает его как exit?
Внутри Docker Engine работает `containerd` — реальный рантайм, управляющий процессами контейнеров. Он отслеживает главный процесс контейнера (PID 1 внутри контейнерного namespace); как только этот процесс завершается (штатно или с ошибкой), это фиксируется как событие завершения, и статус контейнера меняется на `Exited` с соответствующим exit-кодом (`docker inspect --format '{{.State.ExitCode}}'`).
### Что такое Docker runtime и зачем нужен containerd?
Runtime — компонент, который непосредственно создаёт и управляет изолированными процессами контейнеров на уровне ОС (namespaces, cgroups, файловая система из слоёв образа). `containerd` — низкоуровневый рантайм, на котором построен сам Docker Engine: Docker CLI/демон — это удобная надстройка (сборка образов, compose, registry-взаимодействие) поверх `containerd`, который непосредственно исполняет работу по управлению жизненным циклом контейнеров. Это разделение позволяет другим системам (например, Kubernetes через CRI) использовать `containerd` напрямую, без полного Docker Engine.
### Как зайти внутрь контейнера?
```bash
docker exec -it <container> sh # или bash, если есть в образе
```
`exec` запускает новый процесс внутри уже работающего контейнера (в отличие от `docker run`, который создаёт новый контейнер). Если контейнер уже остановлен, `exec` не сработает — нужно либо запустить его заново, либо использовать `docker cp` для файлов без запуска процесса внутри.
### Как скопировать файл в работающий контейнер?
```bash
docker cp ./local-file.txt <container>:/path/in/container
docker cp <container>:/path/in/container ./local-file.txt # и в обратную сторону, из контейнера на хост
```
### Если убить процесс Docker (сам демон) — запущенные контейнеры сдохнут?
Нет, не обязательно — сами контейнеры управляются `containerd`, а не напрямую демоном `dockerd`; при перезапуске `dockerd` уже запущенные контейнеры продолжают работать (это осознанное архитектурное решение, начиная с версии Docker, где Docker Engine был разделён с `containerd` — раньше, в старых версиях, где всё было завязано на монолитный демон, перезапуск демона действительно ронял контейнеры). После рестарта `dockerd` он заново подключается к уже работающим контейнерам через `containerd` и восстанавливает их видимость в `docker ps`.
### Как работает сеть в Docker?
По умолчанию Docker создаёт bridge-сеть (`docker0` для отдельных контейнеров, отдельная сеть на проект в Docker Compose), контейнеры получают виртуальные сетевые интерфейсы, подключённые к этому мосту, и приватные IP внутри него. Docker поднимает встроенный DNS внутри пользовательских (non-default) bridge-сетей — контейнеры резолвят друг друга по имени сервиса/контейнера, а не по IP. Наружу трафик пробрасывается через `-p host_port:container_port`, что настраивает NAT-правила (iptables) для перенаправления с порта хоста на IP контейнера внутри моста.
### Много ли процессов внутри одного контейнера? У контейнеров есть PID?
Технически контейнер может запускать сколько угодно процессов (например, если ENTRYPOINT — скрипт, который стартует несколько дочерних процессов), но общепринятая практика — один основной процесс на контейнер (принцип "один контейнер — одна ответственность"), чтобы жизненный цикл контейнера чётко соответствовал жизненному циклу этого процесса. У контейнера есть PID — как у любого обычного процесса хоста (см. `docker inspect --format '{{.State.Pid}}'` в кейсе), плюс внутри контейнера, благодаря PID namespace, главный процесс видит себя как PID 1 в своём изолированном пространстве.
### Entrypoint vs CMD — в чём разница?
`ENTRYPOINT` — фиксированная главная команда контейнера, `CMD` — аргументы к ней по умолчанию, которые можно переопределить, просто передав другие аргументы в `docker run image <новые аргументы>`. Если задан только `CMD` без `ENTRYPOINT`, вся команда целиком переопределяется тем, что передано в `docker run`. Практическая польза разделения — можно сделать образ, у которого фиксирована сама программа (`ENTRYPOINT ["myapp"]`), но легко менять флаги запуска без пересборки образа.
### Разница docker ps, docker ps -a, docker logs, docker images?
`docker ps` — только работающие контейнеры. `docker ps -a` — вообще все контейнеры, включая остановленные/завершившиеся. `docker logs <container>` — стандартный вывод (stdout/stderr) процесса внутри контейнера, накопленный с момента старта (`-f` — читать в реальном времени, как `tail -f`). `docker images` — список локально скачанных/собранных образов (в отличие от контейнеров — это шаблоны, а не запущенные экземпляры).
### Как сохранить логи из контейнера?
`docker logs <container> > output.log` — перенаправить stdout вывод команды в файл на хосте. Если логи пишутся не в stdout, а в файл внутри контейнера — `docker cp <container>:/path/to/log.file ./log.file`. Для постоянного сохранения логов за пределами жизни контейнера в проде обычно настраивают logging driver (например, `json-file` с ротацией, или отправку в централизованную систему — см. [../elk/CASE.md](../elk/CASE.md)) вместо ручного копирования.

100
stack/elk/CASE.md Normal file
View File

@@ -0,0 +1,100 @@
# Кейс: Elastic Stack (ELK)
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «АО ТНИИС», централизованный сбор и анализ логов в дополнение к метрикам из Prometheus.
## Что нужно реально сделать (домашний стенд)
Docker Compose стенд Elasticsearch + Kibana + Filebeat, собирающий логи уже готового кейса [../nginx/CASE.md](../nginx/CASE.md) — реальная связка «есть логи → собрали → увидели в Kibana», а не абстрактный пример.
### 1. docker-compose.yml
```yaml
version: "3.8"
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
kibana:
image: docker.elastic.co/kibana/kibana:8.13.0
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
ports:
- "5601:5601"
depends_on:
- elasticsearch
filebeat:
image: docker.elastic.co/beats/filebeat:8.13.0
container_name: filebeat
user: root
volumes:
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
- nginx_logs:/var/log/nginx:ro
depends_on:
- elasticsearch
volumes:
nginx_logs:
```
Чтобы подключить реальные логи из кейса [../nginx/CASE.md](../nginx/CASE.md), в его `docker-compose.yml` у сервиса `proxy` нужно смонтировать тот же named volume `nginx_logs` на `/var/log/nginx` — тогда Filebeat читает те же файлы логов, что пишет живой nginx.
### 2. filebeat.yml
```yaml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/nginx/access.log
fields:
service: nginx-lab
output.elasticsearch:
hosts: ["elasticsearch:9200"]
index: "nginx-logs-%{+yyyy.MM.dd}"
setup.template.name: "nginx-logs"
setup.template.pattern: "nginx-logs-*"
```
### 3. Шаги воспроизведения
1. В [../nginx/CASE.md](../nginx/CASE.md) настроить `access_log` в формате, откуда легко выделить статус-код (стандартный combined-формат nginx это уже делает).
2. `docker compose up -d` для обоих стендов (или объединить в один docker-compose.yml).
3. Сгенерировать трафик через nginx-стенд (`curl` в цикле из шагов [../nginx/CASE.md](../nginx/CASE.md)), чтобы появились строки в access.log.
4. Проверить, что данные дошли: `curl http://localhost:9200/nginx-logs-*/_search?pretty`.
5. В Kibana (`localhost:5601`) создать index pattern `nginx-logs-*`, зайти в Discover — увидеть живые записи логов.
6. Построить простую визуализацию: количество запросов по HTTP-статус-коду (парсинг статуса из строки лога через встроенный `nginx` module Filebeat или простой grok-подобный dissect-фильтр).
### 4. Простой parse-фильтр для нестандартных логов
```yaml
processors:
- dissect:
tokenizer: '%{client_ip} - - [%{@timestamp}] "%{method} %{url} %{http_version}" %{status_code} %{bytes}'
field: "message"
target_prefix: ""
```
`dissect` — более простой и быстрый аналог `grok` для логов со стабильным, предсказуемым форматом (не требует регулярных выражений, работает через явные разделители) — здесь разбирает стандартную строку access-лога nginx на отдельные поля (`status_code`, `method`, `url`), которые дальше можно фильтровать и агрегировать в Kibana.
## Что это даёт в разговоре с интервьюером
- Реально собранная связка «источник логов (nginx) → shipper (Filebeat) → хранилище/поиск (Elasticsearch) → визуализация (Kibana)».
- Понимание, зачем нужен index pattern и что такое shard/index на базовом уровне (см. [QUESTIONS.md](QUESTIONS.md)).
- Понимание разницы Filebeat (лёгкий shipper, просто пересылает строки/файлы) vs Logstash (тяжёлый обработчик с богатыми фильтрами — grok, mutate, конвертация форматов).
- Практика простого dissect/parse-фильтра для структурирования сырых текстовых логов.
## Как это ложится в легенду
ELK упоминается в стеке отдела АО ТНИИС как часть инфраструктуры сопровождения, дополняющая метрики. Кейс замыкает цепочку с уже проработанным Nginx-кейсом — те же логи, которые нужно было бы разбирать вручную, теперь централизованно собираются и доступны для поиска и визуализации.

35
stack/elk/QUESTIONS.md Normal file
View File

@@ -0,0 +1,35 @@
# Вопросы: Elastic Stack (ELK)
Опираются на кейс: [CASE.md](CASE.md). Источники — [Реалист банк.md](../../interview/Реалист%20банк.md), [Bi.Zone.md](../../interview/Bi.Zone.md).
### Зачем нужен централизованный сбор логов?
Без централизации при инциденте пришлось бы вручную заходить на каждый сервер и грепать локальные файлы — на масштабе больше пары серверов это нереально быстро. Централизованный сбор (Filebeat/Logstash → Elasticsearch → Kibana) даёт единую точку поиска по всем источникам сразу, с фильтрами, временными диапазонами и агрегациями — я это прочувствовал даже на своём мини-стенде: логи одного nginx-контейнера искать через Kibana удобнее, чем `docker logs | grep` вручную, а на масштабе десятков сервисов разница становится критичной.
### Что такое index и shard в Elasticsearch на базовом уровне?
Index — логическая единица хранения данных в Elasticsearch, аналог таблицы/базы в привычных СУБД (у меня — `nginx-logs-2026.07.17`, с ротацией по дням). Shard — физическая часть индекса: Elasticsearch разбивает индекс на несколько shard'ов, которые могут распределяться по разным нодам кластера — это даёт горизонтальное масштабирование (шардов больше — можно параллельно обрабатывать запросы/индексацию на нескольких машинах) и отказоустойчивость через replica-шарды (копии основных шардов на других нодах). На однонодовом dev-стенде я это не мог продемонстрировать в полную силу (реплики некуда класть при single-node), но структуру index → shards видел через `_cat/indices` и `_cat/shards`.
### В чём разница между Logstash и Filebeat?
Filebeat — лёгкий shipper: минимум ресурсов, задача — надёжно прочитать файл/поток и переслать данные дальше (в Elasticsearch напрямую или через Logstash), с минимальной трансформацией (у меня — простой `dissect` для разбора строки access-лога). Logstash — тяжёлый обработчик с богатым набором фильтров (`grok`, `mutate`, `date`, конвертация форматов, обогащение из внешних источников) — уместен, когда нужна серьёзная трансформация/нормализация разнородных логов перед индексацией. Типичная практика — Filebeat на источнике (лёгкий агент на каждом хосте) → опционально Logstash в середине для тяжёлой обработки → Elasticsearch. В своём стенде обошёлся без Logstash, потому что формат nginx access-лога стабильный и `dissect` в самом Filebeat справился.
### Чем отличается ELK от OpenSearch?
OpenSearch — форк Elasticsearch и Kibana (под открытой лицензией, после того как Elastic сменил лицензирование части своего стека) — архитектурно и по API очень близок к Elasticsearch на момент форка, дальше эти проекты развиваются раздельно и постепенно расходятся в фичах. С точки зрения повседневной эксплуатации на базовом уровне (индексация, поиск, дашборды) отличия для рядового пользователя минимальны — выбор между ними чаще определяется лицензионной политикой компании (открытая лицензия OpenSearch vs текущее лицензирование Elastic), а не техническими возможностями на базовом уровне.
### Ты мониторил стандартными службами мониторинга сам стек ELK? Если да, то как?
Elasticsearch отдаёт собственные метрики (использование JVM heap, длина очередей индексации, состояние шардов, задержки запросов) через встроенный API (`_cluster/health`, `_nodes/stats`) — это можно собирать тем же Prometheus через `elasticsearch_exporter` и визуализировать в Grafana, аналогично стенду [../monitoring/CASE.md](../monitoring/CASE.md). Сам ELK-стек — это тоже сервис, которому нужен мониторинг (не только логи снаружи собирает, но и сам может деградировать — например, при переполнении диска или нехватке heap), поэтому в проде разумно смотреть на него теми же инструментами, что и на остальную инфраструктуру, а не только через собственный Kibana Stack Monitoring.
### Разворачивал ELK с нуля с агентами?
Да, стенд в [CASE.md](CASE.md) — Elasticsearch + Kibana + Filebeat с нуля через Docker Compose, с Filebeat, читающим реальные логи работающего nginx-контейнера из отдельного кейса, а не тестовые/синтетические данные. Настроил index pattern в Kibana и простую визуализацию по статус-кодам запросов nginx.
### Сколько Гб данных логов будет в ELK, если в Prometheus уже 15 Гб метрик?
Однозначного числа тут нет — вопрос проверяет понимание порядка и природы различия между логами и метриками, а не конкретную формулу. Метрики — компактные числовые ряды с фиксированной структурой (timestamp + значение + лейблы), которые к тому же хорошо сжимаются за счёт повторяемости паттернов (Prometheus TSDB использует специальное сжатие временных рядов). Логи — произвольный текст, часто многословный (стектрейсы, JSON с вложенностью, человекочитаемые сообщения), без такой плотной структуры — при сопоставимом количестве событий объём логов обычно на порядок (иногда на два) больше объёма метрик. Отсюда и практический вывод, который я бы озвучил на этом вопросе: если стоит выбор, что мониторить в первую очередь при ограниченных ресурсах хранения — метрики обычно дешевле держать долго и по ним быстрее строить алерты по порогам, а логи — глубже для расследования причины конкретного инцидента, но дороже в хранении; поэтому разумная схема — метрики для алертинга и трендов + логи с более коротким retention для расследований (см. также [../monitoring/QUESTIONS.md](../monitoring/QUESTIONS.md) про выбор между логами и метриками для конкретного инцидента).
### Что такое retention логов и зачем он нужен?
Retention — период, в течение которого логи хранятся, прежде чем автоматически удаляются/архивируются. Нужен, потому что логи растут в объёме быстрее метрик (см. вопрос выше) и бесконечное хранение экономически и технически нецелесообразно — retention настраивают через Index Lifecycle Management в Elasticsearch (например, hot-фаза несколько дней на быстрых дисках для активного поиска, потом warm/cold фазы на более дешёвом хранилище, потом delete). Конкретный срок — компромисс между стоимостью хранения и требованиями (иногда регуляторными) держать историю логов для расследования инцидентов задним числом.

81
stack/kafka/CASE.md Normal file
View File

@@ -0,0 +1,81 @@
# Кейс: Kafka (домашняя однонодовая лаба)
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «Домашняя лаборатория / pet-проект». Kafka упоминалась только в требованиях вакансии Альфа-Банка и в вопросах Bi.Zone — в рабочем стеке компаний из резюме её нет, честный pet-кейс.
## Что нужно реально сделать (домашний стенд)
Однонодовый Kafka-брокер в режиме KRaft (без ZooKeeper — так проще для домашней лабы и это актуальная архитектура в новых версиях) в Docker, создать топик, погонять продюсера/консюмера через встроенные CLI-утилиты и замерить пропускную способность через `kafka-producer-perf-test`.
### 1. docker-compose.yml
```yaml
version: "3.8"
services:
kafka:
image: apache/kafka:3.7.0
container_name: kafka-lab
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-lab:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
```
### 2. Создать топик и погонять продюсера/консюмера
```bash
docker compose up -d
docker exec -it kafka-lab /opt/kafka/bin/kafka-topics.sh \
--create --topic orders-events --bootstrap-server localhost:9092 \
--partitions 3 --replication-factor 1
# консюмер в одном терминале
docker exec -it kafka-lab /opt/kafka/bin/kafka-console-consumer.sh \
--topic orders-events --bootstrap-server localhost:9092 --group demo-group
# продюсер в другом терминале
docker exec -it kafka-lab /opt/kafka/bin/kafka-console-producer.sh \
--topic orders-events --bootstrap-server localhost:9092
```
Набрать несколько сообщений в продюсере — увидеть их в консюмере в реальном времени. Открыть второй консюмер с той же `--group demo-group` — увидеть, как партиции топика (3 штуки) распределяются между консюмерами внутри одной consumer group (каждое сообщение читается только одним консюмером из группы — это и есть горизонтальное масштабирование обработки).
### 3. Партиции и оффсеты
```bash
docker exec -it kafka-lab /opt/kafka/bin/kafka-topics.sh \
--describe --topic orders-events --bootstrap-server localhost:9092
docker exec -it kafka-lab /opt/kafka/bin/kafka-consumer-groups.sh \
--describe --group demo-group --bootstrap-server localhost:9092
```
Второй вывод показывает `CURRENT-OFFSET`/`LOG-END-OFFSET`/`LAG` на партицию — практическое понимание, как Kafka отслеживает, что консюмер уже прочитал, а что ещё нет.
### 4. Замер пропускной способности
```bash
docker exec -it kafka-lab /opt/kafka/bin/kafka-producer-perf-test.sh \
--topic orders-events --num-records 100000 --record-size 200 \
--throughput -1 --producer-props bootstrap.servers=localhost:9092
```
Записать полученные `records/sec` и `MB/sec` в [../../legend/CAPACITY.md](../../legend/CAPACITY.md) — честная лабная цифра для разговора про пропускную способность.
## Что это даёт в разговоре с интервьюером
- Понимание топика, партиции, consumer group, оффсета не как определений, а как наблюдаемого поведения (`--describe` показывает реальное распределение и лаг).
- Понимание, зачем нужны партиции — параллельная обработка внутри одной consumer group.
- Лабная цифра пропускной способности как честная база для разговора о производительности.
## Как это ложится в легенду
Kafka — pet-кейс, не приписанный к опыту в компаниях. Упоминается в резюме только в навыках как самостоятельно проработанная технология, без привязки к конкретному рабочему проекту.

27
stack/kafka/QUESTIONS.md Normal file
View File

@@ -0,0 +1,27 @@
# Вопросы: Kafka
Опираются на кейс: [CASE.md](CASE.md). Источники — [Bi.Zone.md](../../interview/Bi.Zone.md), [Alfa-Bank.md](../../interview/Alfa-Bank.md) (упомянута в требованиях вакансии).
### Что такое Kafka?
Распределённая платформа для потоковой передачи событий (event streaming) — по сути, отказоустойчивый распределённый лог сообщений. Продюсеры пишут сообщения в топики, консюмеры читают их независимо друг от друга и в своём темпе; в отличие от классической очереди сообщения не удаляются сразу после прочтения, а хранятся заданное время (retention), поэтому несколько разных консюмеров могут читать один и тот же поток данных с разной скоростью или начинать читать заново.
### С какими версиями Kafka работал?
В домашней лабе поднимал актуальную версию (3.7) в режиме KRaft — то есть без ZooKeeper, это более новая архитектура, где консенсус по метаданным кластера реализован внутри самой Kafka через `controller`-роль вместо отдельного ZooKeeper-кластера. Более старые продовые инсталляции (до Kafka 3.x/4.x) чаще всего используют классическую схему с ZooKeeper — если на конкретном месте работы Kafka завязана на ZooKeeper, стоит явно уточнить это у интервьюера, чтобы не путать архитектуры.
### Что такое партиции и оффсеты?
Топик физически делится на партиции — это единица параллелизма и хранения. Каждое сообщение внутри партиции получает последовательный номер — оффсет, и порядок сообщений гарантирован только внутри одной партиции (не между партициями топика). Консюмер отслеживает, до какого оффсета он уже прочитал каждую партицию — это позволяет ему продолжить с того же места после перезапуска. У себя в лабе создавал топик с 3 партициями и через `kafka-consumer-groups.sh --describe` явно видел `CURRENT-OFFSET`/`LOG-END-OFFSET`/`LAG` по каждой партиции.
### Что такое consumer group и как балансируются партиции внутри неё?
Consumer group — логическое объединение нескольких консюмеров, которые вместе обрабатывают топик так, что каждое сообщение читается только одним консюмером из группы (а не всеми) — это механизм горизонтального масштабирования обработки. Партиции топика распределяются между консюмерами группы; если консюмеров больше, чем партиций, часть консюмеров останется без партиций и будет простаивать. У себя в лабе с топиком на 3 партиции и двумя консюмерами в одной группе увидел, как каждый консюмер забрал себе часть партиций.
### Что такое AT/ET CI/CD в контексте Kafka? (формулировка с собеседования Bi.Zone)
В контексте связки Kafka с CI/CD-пайплайнами это, по всей видимости, отсылка к семантике доставки сообщений — **at-least-once** (сообщение гарантированно доставлено, но может задублироваться при retry) и **exactly-once** (доставка ровно один раз, более строгая и дорогая гарантия, у Kafka реализуется через идемпотентного продюсера и транзакции). В пайплайнах обработки данных выбор между этими семантиками определяет, нужна ли дедупликация на стороне консюмера. Честно: на самом собеседовании формулировка вопроса была нестандартной, и это моя интерпретация вероятного смысла — по этой конкретной формулировке лучше переспросить интервьюера, что именно имеется в виду, а не гадать молча.
### Как замерить пропускную способность Kafka?
Встроенная утилита `kafka-producer-perf-test.sh` — задаёшь число записей, размер записи и лимит throughput (`-1` — без ограничения, максимальная скорость) и получаешь `records/sec`/`MB/sec` на конкретном железе. Я гонял такой замер у себя на однонодовом брокере — конкретные цифры и то, как они соотносятся с публично известными продовыми показателями, зафиксированы в [../../legend/CAPACITY.md](../../legend/CAPACITY.md).

169
stack/kubernetes/CASE.md Normal file
View File

@@ -0,0 +1,169 @@
# Кейс: Kubernetes (домашний pet-кластер)
Если K8s незнаком — сначала пройти учебный курс с нуля [LEARNING.md](LEARNING.md), этот файл — компактный конспект результата для повторения перед собеседованием, без пошаговых объяснений «зачем».
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «Домашняя лаборатория / pet-проект». **Важно: K8s не входит в рабочий стек ни АО ТНИИС, ни ОКБ СУХОЙ** — это честный pet-проект поверх рабочего опыта с Docker/Ansible/мониторингом, а не часть легенды о работе в конкретной компании. На собеседовании так и позиционируется: «в проде на текущей работе K8s не используем, но поднимал себе дома кластер, чтобы разобраться» (этот же приём в разборе [ВТБ](../../interview/Bank%20VTB.md) сработал в плюс — см. [../../interview/real-interviews.md](../../interview/real-interviews.md)).
## Что нужно реально сделать (домашний стенд)
Локальный кластер через `kind` (Kubernetes IN Docker) — не требует отдельной виртуалки, поднимается поверх уже установленного Docker. Цель — своими руками пройти путь «манифест → под → сервис → доступ снаружи», задеплоить кусок стека мониторинга через Helm и прогнать диагностические команды, которые реально спрашивали на собеседованиях.
### 1. Установка и создание кластера
```bash
# kind и kubectl (Windows, через choco; либо скачать бинарники с GitHub releases)
choco install kind kubernetes-cli kubernetes-helm
# кластер из 1 control-plane + 2 worker-нод — чтобы было что рисовать при вопросе про архитектуру
```
```yaml
# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
```
```bash
kind create cluster --name lab --config kind-config.yaml
kubectl cluster-info
kubectl get nodes -o wide
```
### 2. Деплой простого приложения: Deployment + Service + ConfigMap
```yaml
# app.yaml
apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: demo
data:
GREETING: "Привет из ConfigMap"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: demo
type: Opaque
stringData:
API_KEY: "lab-secret-value"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
namespace: demo
spec:
replicas: 3
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: demo-app
image: nginxdemos/hello:latest
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret
---
apiVersion: v1
kind: Service
metadata:
name: demo-app-svc
namespace: demo
spec:
selector:
app: demo-app
ports:
- port: 80
targetPort: 80
type: ClusterIP
```
```bash
kubectl apply -f app.yaml
kubectl -n demo get pods -o wide
kubectl -n demo rollout status deployment/demo-app
```
### 3. Доступ снаружи: port-forward и Ingress
```bash
# быстрый способ проверить сервис
kubectl -n demo port-forward svc/demo-app-svc 8080:80
curl http://localhost:8080
```
Для полноценного Ingress — установить NGINX Ingress Controller (kind предоставляет готовый манифест под себя) и завести `Ingress`-ресурс с host-based роутингом — так на собеседовании про Реалист спрашивали про «два ЦОД под K8s с балансировщиком»: балансировщик снаружи (в лабе — сам kind/NGINX Ingress) → Service (ClusterIP, виртуальный балансировщик по подам через kube-proxy) → поды на worker-нодах.
### 4. Helm: разворачиваем kube-prometheus-stack
Практическая связка с уже готовым кейсом [../monitoring/CASE.md](../monitoring/CASE.md) — тот же принцип «Prometheus + Grafana», но теперь через Helm-чарт в кластере, а не через голый docker-compose.
```bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace
kubectl -n monitoring get pods
kubectl -n monitoring port-forward svc/monitoring-grafana 3000:80
```
Это даёт прямой ответ на вопрос Bi.Zone «как задеплоишь мониторинг на 300 000 серверов»: не поднимать вручную, а Helm-чартом с overrides под масштаб (`values.yaml`: количество реплик, remote_write в Mimir/Thanos, ресурсы). См. цифры и архитектуру масштаба в [../../legend/CAPACITY.md](../../legend/CAPACITY.md).
### 5. Диагностика руками (то, что реально спрашивали «показать в терминале»)
```bash
kubectl -n demo get pods
kubectl -n demo describe pod <pod-name>
kubectl -n demo logs <pod-name>
kubectl -n demo logs <pod-name> --previous # логи упавшего предыдущего инстанса контейнера
kubectl -n demo exec -it <pod-name> -- sh
kubectl -n demo get events --sort-by=.lastTimestamp
kubectl get daemonset -A # например, kube-proxy — daemonset на каждой ноде
```
Специально сломать под (например, указать несуществующий образ в `image:`) и посмотреть на `ImagePullBackOff`/`CrashLoopBackOff` через `describe` — реальная практика диагностики, а не просто описание.
### 6. Разбор компонентов кластера (для вопроса «из чего состоит K8s»)
- **control-plane**: `kube-apiserver` (единая точка входа для всех запросов), `etcd` (хранилище состояния кластера, key-value), `kube-scheduler` (назначает поды на ноды), `kube-controller-manager` (следит, что желаемое состояние совпадает с реальным).
- **worker-нода**: `kubelet` (агент, который управляет подами на ноде), `kube-proxy` (сетевые правила для Service), container runtime (`containerd`).
- **etcd отдельно от control-plane или нет** — в проде для отказоустойчивости etcd часто выносят на отдельные ноды (нечётное количество, 3/5, из-за кворума), в маленьких/pet-инсталляциях (как kind) он живёт на той же control-plane ноде. В `kind` это видно напрямую: `kubectl get pods -n kube-system` покажет `etcd-lab-control-plane`.
## Что это даёт в разговоре с интервьюером
- Понимание базовых объектов: Pod, Deployment, Service, Namespace, ConfigMap, Secret, Ingress — и что каждый из них реально содержит (проверено `kubectl get -o yaml`).
- Понимание разницы ConfigMap (несекретные конфиги) и Secret (base64-кодированные чувствительные данные — не шифрование, а кодирование; для реальной защиты нужны внешние решения типа Sealed Secrets/Vault, см. [../vault/CASE.md](../vault/CASE.md)).
- Практика с Helm — установка чарта, `values.yaml`, `helm upgrade`.
- Живой опыт диагностики через `kubectl describe`/`logs`/`events`.
- Честная граница: это pet-кластер из 3 нод на своей машине, а не эксплуатация продового HA-кластера — расширение до multi-datacenter/etcd-кворума описывается на уровне понимания архитектуры, не личного опыта эксплуатации на этом масштабе.
## Как это ложится в легенду
K8s **не** приписывается к опыту в АО ТНИИС или ОКБ СУХОЙ — там его не было и в резюме он не должен появляться в блоках компаний. В «Ключевые навыки» резюме и в легенду он входит как отдельный, честно обозначенный pet-опыт: практическое знакомство с базовыми объектами и Helm поверх домашнего кластера, дополняющее рабочий опыт с Docker и Ansible.

View File

@@ -0,0 +1,833 @@
# Учебный курс: Kubernetes с нуля
Это учебный документ, а не легенда — здесь нет утверждений «делал на работе». Цель: пройти путь от нуля до уверенного pet-уровня, объясняя себе на каждом шаге «что это и зачем», а не просто копируя команды. Итоговая позиция на собеседовании не меняется: K8s — честная домашняя практика поверх рабочего опыта с Docker/Ansible/мониторингом (см. [../../legend/LEGEND.md](../../legend/LEGEND.md) → «Домашняя лаборатория / pet-проект»).
**Как этим пользоваться:** проходить модули по порядку, не пропускать практику — каждый следующий модуль опирается на состояние кластера, оставленное предыдущим. Когда весь курс пройден руками один раз, для повторения перед конкретным собеседованием используй более компактный [CASE.md](CASE.md) — он про то же самое, но без объяснений, как шпаргалка. Вопросы с готовыми ответами — в [QUESTIONS.md](QUESTIONS.md).
**Формат каждого модуля:** зачем это → теория-минимум → практика руками → самопроверка → как это в реальных проектах.
**Окружение курса** (одно на все модули, то же, что в CASE.md): Windows + Docker Desktop, кластер `kind` с именем `lab`, 1 control-plane + 2 worker. Не пересоздавай кластер между модулями без необходимости — большинство модулей продолжают работать в одном и том же кластере и в одних и тех же namespace (`demo`, `monitoring`), это ближе к тому, как выглядит реальная работа с уже существующим кластером.
---
## Модуль 0. Установка инструментов
### Зачем это
Три разных инструмента решают три разные задачи, и важно не путать их роли: **Docker** — где физически крутятся контейнеры, **kind** — как за секунды поднять тестовый K8s-кластер поверх Docker без отдельных виртуалок, **kubectl** — как разговаривать с любым K8s-кластером (хоть pet, хоть прод), **Helm** — как ставить готовые наборы манифестов одной командой вместо ручного `kubectl apply` на десяток файлов.
### Теория-минимум
`kind` = Kubernetes IN Docker: каждая «нода» кластера — это на самом деле Docker-контейнер, внутри которого запущен полноценный K8s. Это не то, как выглядит прод (там ноды — реальные VM/железо), но API и поведение объектов идентичны — то, что выучено на kind, переносится на любой кластер.
### Практика руками
```bash
# Docker Desktop должен быть уже установлен и запущен — это фундамент, без него ничего не работает
# Windows, через choco (одной командой ставит все три инструмента)
choco install kind kubernetes-cli kubernetes-helm
# проверить, что каждый инструмент реально встал
docker --version
kind --version
kubectl version --client
helm version
```
### Самопроверка
- Все четыре команды `--version` отвечают без ошибок.
- Понимаешь своими словами разницу между Docker, kind, kubectl и Helm — если нет, перечитай «Зачем это» выше.
### Как это в реальных проектах
В проде kind не используют (это чисто локальный/CI инструмент для тестов) — реальные кластеры разворачивают managed-сервисами облаков (EKS/GKE/managed K8s) или `kubeadm`/аналогами on-premise. kubectl и Helm — те же самые, что и в pet-кластере, это и есть смысл разработки на kind: инструменты один в один.
---
## Модуль 1. Зачем вообще Kubernetes
### Зачем это
Прежде чем учить объекты K8s, важно понимать проблему, которую он решает — иначе все дальнейшие абстракции выглядят как искусственная сложность ради сложности.
### Теория-минимум
Без оркестратора эксплуатация выглядит так: `docker run` руками на конкретной VM, и если контейнер упал — никто, кроме человека или самопального скрипта, его не перезапустит; если VM легла — сервис просто недоступен, пока кто-то не отреагирует. K8s переворачивает модель: администратор описывает **desired state** («хочу 3 реплики этого образа, слушающие порт 8080») декларативно в YAML, а control-plane кластера постоянно сверяет фактическое состояние с желаемым и сам исправляет расхождение — это и называется **self-healing**. Ключевая идея: не «выполни команду один раз», а «поддерживай вот такое состояние постоянно».
### Практика руками
```bash
kind create cluster --name lab
kubectl run demo --image=nginxdemos/hello --restart=Always
kubectl get pods
# демо self-healing: убиваем под руками
kubectl delete pod demo
kubectl get pods
# NAME ... — под пересоздан автоматически (другой RESTARTS/AGE), потому что desired state требует, чтобы под существовал
```
### Самопроверка
- Своими словами: что такое desired state и чем декларативный подход отличается от «выполнить команду»?
- Почему после `kubectl delete pod demo` под появился снова?
*(Забегая вперёд: на самом деле голый `kubectl run` пересоздаётся не всегда — это будет явно разобрано в модуле 4 про Deployment. Здесь цель — просто увидеть идею self-healing на практике.)*
### Как это в реальных проектах
Эта идея — причина, почему K8s вообще существует: чем больше сервисов и нод, тем дороже человеку вручную следить за их состоянием. В резюме и легенде это прямая параллель с системами вроде systemd (`Restart=always` — тот же self-healing, но на уровне одной VM, не кластера) — see [../linux-bash/CASE.md](../linux-bash/CASE.md).
---
## Модуль 2. Архитектура кластера
### Зачем это
Вопрос «из чего состоит Kubernetes» — один из самых частых на собеседованиях (см. [QUESTIONS.md](QUESTIONS.md)). Нельзя понимать объекты (Pod, Service и т.д.), не понимая, кто внутри кластера отвечает за их существование.
### Теория-минимум
Кластер делится на **control-plane** (мозг) и **worker-ноды** (мышцы, где реально крутятся приложения):
- `kube-apiserver` — единственная точка входа для всех запросов (в том числе от `kubectl`); всё остальное общается только через него, напрямую в обход API никто ничего не меняет.
- `etcd` — key-value хранилище, единственный источник правды о состоянии кластера (все объекты, их спеки и статусы).
- `kube-scheduler` — решает, на какую worker-ноду поставить новый под (по ресурсам, тегам, ограничениям).
- `kube-controller-manager` — набор контроллеров, каждый из которых следит за своим типом объектов и подгоняет реальность к желаемому состоянию (это и есть механизм self-healing из модуля 1).
- `kubelet` (на каждой worker-ноде) — агент, который реально запускает/останавливает контейнеры на своей ноде через container runtime.
- `kube-proxy` (на каждой worker-ноде) — настраивает сетевые правила, чтобы трафик до Service доходил до нужных подов.
- `containerd` — сам container runtime, который непосредственно тянет образы и стартует контейнеры (аналог Docker engine, но легче).
### Практика руками
```bash
# пересоздаём кластер с явной топологией — 1 control-plane + 2 worker, чтобы было что показать на схеме
cat > kind-config.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
kind delete cluster --name lab
kind create cluster --name lab --config kind-config.yaml
kubectl cluster-info
kubectl get nodes -o wide
# все компоненты control-plane живут как поды в системном namespace kube-system
kubectl get pods -n kube-system -o wide
```
Найди в выводе `kube-apiserver-lab-control-plane`, `etcd-lab-control-plane`, `kube-scheduler-lab-control-plane`, `kube-controller-manager-lab-control-plane` — все живьём, не абстракция. `kube-proxy` увидишь как DaemonSet (по одному поду на каждую ноду — забегая вперёд к модулю 9 про DaemonSet).
### Самопроверка
- Назвать вслух 4 компонента control-plane и 2 компонента worker-ноды, объяснить роль каждого без подсматривания.
- Почему `etcd` в pet-кластере находится на той же ноде, что и control-plane, а в проде его часто выносят отдельно? (Кворум при отказоустойчивости — нечётное число нод: 3 или 5, при потере части живой кворум продолжает работать, если жива больше половины.)
### Как это в реальных проектах
В managed-кластерах (EKS/GKE) весь control-plane скрыт от пользователя облаком — вы видите только worker-ноды. В on-premise-кластерах (ближе к тому, что было бы в закрытом контуре без облаков) control-plane разворачивается и обслуживается вручную или через `kubeadm`, и вопрос про размещение `etcd` становится реальным архитектурным решением, а не абстракцией.
---
## Модуль 3. Pod и kubectl-база
### Зачем это
Pod — минимальная единица деплоя в K8s. Нельзя запустить «просто контейнер» напрямую через API K8s — всегда через Pod. Здесь же нарабатывается базовый набор команд `kubectl`, которым пользуешься постоянно.
### Теория-минимум
Pod — это один или несколько контейнеров, которые всегда живут и умирают вместе, на одной ноде, с общей сетью (localhost между контейнерами внутри пода) и опционально общими volume. В большинстве случаев в поде один контейнер — многоконтейнерные поды (sidecar-паттерн) нужны реже, когда вспомогательный процесс должен жить бок о бок с основным (например, агент логирования).
Анатомия любого манифеста K8s — четыре ключа верхнего уровня:
- `apiVersion` — версия API, к которой относится объект (у Pod это `v1`, у более сложных объектов часто `apps/v1` и т.д.).
- `kind` — тип объекта (`Pod`, `Deployment`, `Service`...).
- `metadata` — имя, namespace, labels, аннотации — «паспорт» объекта.
- `spec` — собственно желаемое состояние: что именно нужно запустить.
### Практика руками
```yaml
# pod.yaml
apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: v1
kind: Pod
metadata:
name: demo-pod
namespace: demo
labels:
app: demo
spec:
containers:
- name: hello
image: nginxdemos/hello:latest
ports:
- containerPort: 80
```
```bash
kubectl apply -f pod.yaml
kubectl -n demo get pods # список подов
kubectl -n demo get pods -o wide # + на какой ноде, IP пода
kubectl -n demo describe pod demo-pod # полная инфа: события, статус, причина проблем
kubectl -n demo logs demo-pod # логи контейнера
kubectl -n demo exec -it demo-pod -- sh # зайти внутрь контейнера интерактивно
kubectl -n demo get pod demo-pod -o yaml # реальный полный манифест объекта (K8s дополнил своими полями)
kubectl -n demo delete pod demo-pod # удалить
```
### Самопроверка
- Из четырёх ключей манифеста назвать, какой отвечает за «что мы хотим получить», а какой — за «как объект называется и где живёт».
- Чем `logs` отличается от `describe`? (logs — вывод приложения; describe — метаданные/события/статус от самого K8s, годится для диагностики, почему под не стартует).
- Что покажет `kubectl -n demo get pod demo-pod -o yaml` такого, чего нет в исходном `pod.yaml`? (K8s подставляет статус, IP, дополнительные системные поля — разница между тем, что ты запросил, и тем, что реально существует).
### Как это в реальных проектах
Голые Pod'ы в проде почти никогда не создают напрямую (кроме короткоживущих задач) — если под упадёт, его никто не пересоздаст, потому что за него никто не отвечает (в отличие от модуля 1, где `kubectl run` создавал под через неявный контроллер). Правильный способ — через Deployment, это модуль 4.
---
## Модуль 4. Deployment и ReplicaSet
### Зачем это
Это самый частый способ запускать stateless-приложения в K8s — и то, что даёт настоящий self-healing и rolling-обновления без даунтайма.
### Теория-минимум
**Deployment** управляет **ReplicaSet**, а ReplicaSet управляет подами — трёхуровневая иерархия. Deployment описывает: сколько реплик (`replicas`), как найти «свои» поды (`selector`), и шаблон, из которого их создавать (`template`, внутри — точно такой же `spec`, как у голого Pod). Если поменять образ в Deployment, он не убивает все поды разом — создаёт новый ReplicaSet и постепенно переключает трафик со старого на новый (rolling update), это и есть обновление без даунтайма.
### Практика руками
```yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
namespace: demo
spec:
replicas: 3
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: demo-app
image: nginxdemos/hello:latest
ports:
- containerPort: 80
```
```bash
kubectl apply -f deployment.yaml
kubectl -n demo get deployment demo-app
kubectl -n demo get replicaset # Deployment создал ReplicaSet
kubectl -n demo get pods -o wide # ReplicaSet создал 3 пода
# масштабирование
kubectl -n demo scale deployment demo-app --replicas=5
kubectl -n demo get pods
# self-healing по-настоящему: убиваем один под
kubectl -n demo delete pod <имя-любого-пода>
kubectl -n demo get pods # ReplicaSet тут же пересоздал под — реплик снова 5
# rolling update
kubectl -n demo set image deployment/demo-app demo-app=nginxdemos/hello:plain-text
kubectl -n demo rollout status deployment/demo-app
kubectl -n demo rollout history deployment/demo-app
# откат, если что-то пошло не так
kubectl -n demo rollout undo deployment/demo-app
```
### Самопроверка
- Нарисовать иерархию Deployment → ReplicaSet → Pod своими словами.
- Что произойдёт с количеством ReplicaSet после `set image`? (появится новый ReplicaSet под новую версию, старый останется с 0 репликами — это и даёт `rollout undo`).
- Чем rolling update отличается от простого «убить все старые поды и поднять новые»? (нет даунтайма — старые поды продолжают обслуживать трафик, пока новые не станут готовы).
### Как это в реальных проектах
Deployment — рабочая лошадка для 90% приложений в проде (stateless-сервисы). Rolling update с постепенным переключением — прямая параллель с тем, зачем в принципе нужен `readiness probe` (модуль 8): K8s не переключает трафик на новый под, пока тот не сообщит о готовности.
---
## Модуль 5. Service и сеть
### Зачем это
У подов IP-адреса нестабильны (под пересоздали — IP другой). Нужен стабильный способ достучаться до набора подов, не завязываясь на их конкретные IP — это и есть Service.
### Теория-минимум
**Service** — это стабильный виртуальный IP + DNS-имя, стоящее перед набором подов, отобранных по `selector` (по тем же labels, что использует Deployment). Три основных типа:
- `ClusterIP` (по умолчанию) — доступен только внутри кластера, для внутреннего взаимодействия сервисов.
- `NodePort` — открывает порт на каждой worker-ноде наружу кластера, для простого внешнего доступа без облачного балансировщика.
- `LoadBalancer` — просит облако выдать внешний балансировщик (в pet-кластере kind не работает без эмуляции, поэтому для локального внешнего доступа используем `port-forward` или Ingress из модуля 6).
Внутри кластера любой под может обратиться к Service по DNS-имени вида `<service>.<namespace>.svc.cluster.local` (или просто `<service>`, если в том же namespace) — `kube-proxy` на каждой ноде транслирует это имя в конкретный IP пода по правилам iptables/IPVS, с балансировкой между всеми подходящими подами.
### Практика руками
```yaml
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: demo-app-svc
namespace: demo
spec:
selector:
app: demo-app
ports:
- port: 80
targetPort: 80
type: ClusterIP
```
```bash
kubectl apply -f service.yaml
kubectl -n demo get svc demo-app-svc
kubectl -n demo get endpoints demo-app-svc # реальные IP подов, на которые сейчас указывает Service
# проверка снаружи через port-forward
kubectl -n demo port-forward svc/demo-app-svc 8080:80
curl http://localhost:8080 # в отдельном терминале
# проверка балансировки изнутри кластера — заходим в один под и стучимся в Service несколько раз
kubectl -n demo run curl-test --image=curlimages/curl -it --rm -- sh
# внутри: curl demo-app-svc.demo.svc.cluster.local, повторить несколько раз
```
### Самопроверка
- Почему нельзя просто обращаться напрямую к IP пода в проде? (IP меняется при каждом пересоздании пода — Service даёт стабильную точку входа).
- Чем `endpoints` Service отличаются от самого Service? (endpoints — динамический список реальных IP подходящих подов, обновляется автоматически при масштабировании/пересоздании).
- В чём разница ClusterIP / NodePort / LoadBalancer одним предложением каждая.
### Как это в реальных проектах
Это ровно то, что подразумевается под вопросом «как работает трафик в K8s» (см. [QUESTIONS.md](QUESTIONS.md)): Service + kube-proxy — обязательный слой между внешним балансировщиком/Ingress и конкретными подами, в любой архитектуре, от pet-кластера до мультидатацентрового прода.
---
## Модуль 6. Ingress
### Зачем это
NodePort/port-forward годятся для лабы, но не масштабируются на реальный внешний трафик с доменными именами и путями (`/api`, `/app` на одном IP). Ingress — стандартный способ завести весь HTTP(S)-трафик в кластер через один вход с роутингом по host/path.
### Теория-минимум
**Ingress** — это правило роутинга (host/path → Service), но само по себе оно ничего не делает — нужен **Ingress Controller** (отдельное приложение внутри кластера, например NGINX Ingress Controller), которое читает Ingress-ресурсы и реально проксирует трафик. Полная цепочка запроса: клиент → Ingress Controller (по внешнему IP/порту) → на основе host/path выбирает нужный Service → Service выбирает под по `selector` → под отвечает.
### Практика руками
```bash
# kind поставляется с готовым манифестом NGINX Ingress Controller под себя
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl -n ingress-nginx wait --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=120s
```
```yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-ingress
namespace: demo
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: demo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-app-svc
port:
number: 80
```
```bash
kubectl apply -f ingress.yaml
kubectl -n demo get ingress
# добавить demo.local в hosts-файл (Windows: C:\Windows\System32\drivers\etc\hosts) → 127.0.0.1
curl http://demo.local
```
### Самопроверка
- Своими словами: чем Ingress отличается от Ingress Controller? (Ingress — декларативное правило-манифест; Controller — программа, которая это правило исполняет).
- Нарисовать полную цепочку «клиент → под» через Ingress, включая все промежуточные звенья.
### Как это в реальных проектах
Это прямой ответ на реальный вопрос с собеседования «нарисуй два ЦОД под K8s с балансировщиком» (Реалист банк, см. [QUESTIONS.md](QUESTIONS.md)): внешний L4/L7-балансировщик перед ЦОДами → внутри каждого ЦОД Ingress Controller/Service → поды. Личная параллель с рабочим опытом — Nginx как балансировщик перед сервисами (см. [../nginx/CASE.md](../nginx/CASE.md), [../../legend/STORY.md](../../legend/STORY.md) → nginx.conf для x3 Mimir): Ingress Controller здесь и есть, по сути, Nginx, только управляемый декларативно через K8s-объекты, а не ручным редактированием `nginx.conf`.
---
## Модуль 7. ConfigMap и Secret
### Зачем это
Образ контейнера должен быть одинаковым во всех окружениях (dev/stage/prod) — конфигурация, которая между окружениями отличается, не должна быть зашита в образ. ConfigMap и Secret — стандартный способ передать конфиг снаружи образа.
### Теория-минимум
**ConfigMap** — для несекретных данных (URL, флаги, конфиг-файлы), **Secret** — для чувствительных значений (пароли, токены, ключи). Оба можно подключить к поду двумя способами: как переменные окружения (`envFrom`/`env`) или как смонтированный файл (`volumeMounts`). Важный технический нюанс: Secret по умолчанию хранит значения в **base64 — это кодирование, а не шифрование**. Любой, у кого есть доступ к API/etcd, может декодировать значение одной командой — реальная защита требует либо шифрования etcd at rest, либо внешних систем вроде Vault (см. [../vault/CASE.md](../vault/CASE.md)) или Sealed Secrets.
### Практика руками
```yaml
# config-secret.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: demo
data:
GREETING: "Привет из ConfigMap"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: demo
type: Opaque
stringData:
API_KEY: "lab-secret-value"
```
Подключаем к Deployment из модуля 4 через `envFrom`:
```yaml
# добавить в spec.template.spec.containers[0] demo-app из deployment.yaml
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret
```
```bash
kubectl apply -f config-secret.yaml
kubectl apply -f deployment.yaml # обновлённый с envFrom
# проверка, что переменные реально попали в под
kubectl -n demo exec -it <имя-пода> -- env | grep -E "GREETING|API_KEY"
# ключевая демонстрация: base64 — не шифрование
kubectl -n demo get secret app-secret -o yaml
# скопировать значение data.API_KEY и декодировать:
echo "<base64-значение>" | base64 -d
# получится исходный "lab-secret-value" — открытым текстом
```
### Самопроверка
- Почему нельзя просто зашить конфиг в Dockerfile/образ? (тогда под каждое окружение нужен свой образ — теряется смысл «один образ везде»).
- Объяснить вслух, почему `kubectl get secret -o yaml` + `base64 -d` — не взлом, а штатная возможность любого, у кого есть доступ к API.
### Как это в реальных проектах
Прямая параллель с рабочим опытом: `ansible-vault` шифрует секреты в Ansible-ролях (см. [../ansible/CASE.md](../ansible/CASE.md)) примерно с той же целью, ради которой в проде K8s-секреты часто заводят не как голый `Secret`, а через внешний Vault-интеграцию (модуль pet — [../vault/CASE.md](../vault/CASE.md)) или `Sealed Secrets`, чтобы решить проблему base64 ≠ шифрование.
---
## Модуль 8. Probes и ресурсы
### Зачем это
K8s не знает, «готово» ли приложение внутри контейнера, просто по факту, что процесс запущен — процесс может быть жив, но зависшим, или ещё прогревающимся. Probes дают K8s способ реально спросить приложение о его состоянии.
### Теория-минимум
Три вида проверок:
- **livenessProbe** — «жив ли процесс вообще?» Если проверка не проходит несколько раз подряд — K8s убивает и пересоздаёт контейнер.
- **readinessProbe** — «готов ли принимать трафик прямо сейчас?» Если не проходит — под не убивают, но Service временно перестаёт слать в него трафик (именно это делает rolling update из модуля 4 безопасным).
- **startupProbe** — для медленно стартующих приложений: пока не пройдёт, liveness/readiness не проверяются (чтобы долгий старт не приняли за зависание).
**Ресурсы** (`requests`/`limits`): `requests` — сколько CPU/RAM гарантированно резервируется под контейнер (scheduler использует это число, чтобы решить, на какую ноду его поставить), `limits` — потолок, выше которого контейнер не может подняться (превышение по памяти → **OOMKilled**, превышение по CPU → троттлинг, не убийство). Комбинация requests/limits определяет **QoS-класс** пода (`Guaranteed`/`Burstable`/`BestEffort`) — от него зависит, что убьют первым при нехватке ресурсов на ноде.
### Практика руками
```yaml
# probes.yaml — добавить в spec.template.spec.containers[0] demo-app
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"
```
```bash
kubectl apply -f deployment.yaml
kubectl -n demo describe pod <имя-пода> # секция Liveness/Readiness видна прямо в describe
# демо OOMKilled: временно поставить memory limit заведомо ниже, чем нужно приложению
# (например memory: "8Mi"), применить, посмотреть:
kubectl -n demo get pods
kubectl -n demo describe pod <имя-пода> # Last State: Terminated, Reason: OOMKilled
```
### Самопроверка
- В чём разница между liveness и readiness одним предложением каждая — и что K8s делает в каждом случае при провале проверки.
- Что произойдёт, если задать `limits.memory` меньше, чем реально требуется приложению? (контейнер убьют по OOM, и он уйдёт в CrashLoopBackOff при повторных попытках — мост к модулю 9).
### Как это в реальных проектах
Правильно настроенные probes — то, что отличает «работающий на бумаге» деплой от реально безопасного rolling update: без readinessProbe K8s может начать слать трафик на под, который ещё не прогрелся, и получить ошибки у пользователей в момент релиза.
---
## Модуль 9. Диагностика поломок
### Зачем это
Это самый практический навык для собеседования: «покажи, как будешь диагностировать» — частый формат реальных вопросов (см. [QUESTIONS.md](QUESTIONS.md)). Здесь специально ломаем всё, что можно сломать, чтобы увидеть диагностику вживую, а не в теории.
### Теория-минимум
Три частых состояния поломанного пода:
- **ImagePullBackOff** — K8s не может скачать указанный образ (опечатка в имени, несуществующий тег, нет доступа к registry).
- **CrashLoopBackOff** — контейнер стартует и тут же падает, K8s пытается перезапустить снова и снова с растущей паузой между попытками.
- **Pending** — под не может быть запланирован ни на одну ноду (не хватает ресурсов под `requests`, или не подходит ни одна нода по ограничениям).
### Практика руками
```bash
# 1. ImagePullBackOff — несуществующий образ
kubectl -n demo run broken1 --image=nginxdemos/this-image-does-not-exist:latest
kubectl -n demo get pods
kubectl -n demo describe pod broken1 # Events внизу прямо назовут причину
# 2. CrashLoopBackOff — команда, которая тут же завершается с ошибкой
kubectl -n demo run broken2 --image=busybox --restart=Always -- sh -c "exit 1"
kubectl -n demo get pods -w # смотрим, как RESTARTS растёт
kubectl -n demo logs broken2 --previous # логи упавшего перед текущим рестартом контейнера
kubectl -n demo describe pod broken2
# 3. Pending — запросить заведомо нереальные ресурсы
kubectl -n demo run broken3 --image=nginx --requests='cpu=100,memory=100Gi' --restart=Always
kubectl -n demo get pods # STATUS: Pending
kubectl -n demo describe pod broken3 # Events: "0/3 nodes are available: insufficient cpu/memory"
# общая хронология по namespace — полезно, когда непонятно, с чего начать
kubectl -n demo get events --sort-by=.lastTimestamp
# уборка после экспериментов
kubectl -n demo delete pod broken1 broken2 broken3
```
### Самопроверка
Чек-лист «под не работает — что смотреть по порядку» (проговорить вслух на реальном примере из практики выше):
1. `kubectl get pods` — какой статус? (ImagePullBackOff / CrashLoopBackOff / Pending / Running-но-не-Ready).
2. `kubectl describe pod` — секция Events внизу почти всегда прямо называет причину.
3. Если контейнер стартовал и упал — `kubectl logs <pod> --previous`, посмотреть, что вывело само приложение перед падением.
4. Если Pending — смотреть Events на предмет нехватки ресурсов или несовпадения ограничений с нодами.
5. Если ничего не понятно — `kubectl get events --sort-by=.lastTimestamp` по всему namespace, искать связанные события.
### Как это в реальных проектах
Это прямая параллель с рабочим инцидент-флоу из [../../legend/STORY.md](../../legend/STORY.md) → «Как решается инцидент»: там формула «дашборд → логи (Kibana) → диагностика (systemctl/journalctl) → рестарт/эскалация», здесь та же логика, но инструмент — `kubectl describe`/`logs`/`events` вместо `systemctl`/`journalctl`. Смысл вопроса на собеседовании «как будешь диагностировать под» — не проверить знание конкретной команды, а проверить, есть ли системный порядок действий.
---
## Модуль 10. Хранилище: PV, PVC и StatefulSet
### Зачем это
Поды по умолчанию **эфемерны** — при пересоздании пода локальные данные внутри контейнера теряются. Для приложений с состоянием (базы данных, очереди) это неприемлемо, и K8s даёт отдельный слой абстракций для постоянного хранилища.
### Теория-минимум
- `emptyDir` — временный volume, живущий столько же, сколько под (переживает рестарт контейнера внутри пода, но не пересоздание самого пода) — полезен для промежуточных данных между контейнерами одного пода.
- `PersistentVolume` (PV) — реальный кусок хранилища в кластере (диск/NFS/облачный volume), `PersistentVolumeClaim` (PVC) — запрос пода «дай мне столько-то места», который K8s сопоставляет с подходящим PV. `StorageClass` — шаблон, по которому PV создаются автоматически по запросу PVC (динамическое провижининг вместо ручного создания PV заранее).
- **StatefulSet** vs **Deployment**: Deployment создаёт взаимозаменяемые поды с случайными именами и общим PVC (или без него) — годится для stateless. StatefulSet даёт каждому поду стабильное имя (`app-0`, `app-1`, ...) и **свой собственный** PVC, который переживает пересоздание конкретного пода — обязательно для баз данных и любых систем, где реплики не взаимозаменяемы (например, знают свою роль master/replica).
### Практика руками
```yaml
# statefulset-demo.yaml — учебный пример на минимальном приложении, не на реальной БД
apiVersion: v1
kind: Service
metadata:
name: demo-headless
namespace: demo
spec:
clusterIP: None # headless-сервис — нужен StatefulSet для стабильных DNS-имён подов
selector:
app: demo-stateful
ports:
- port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: demo-stateful
namespace: demo
spec:
serviceName: demo-headless
replicas: 2
selector:
matchLabels:
app: demo-stateful
template:
metadata:
labels:
app: demo-stateful
spec:
containers:
- name: demo
image: nginxdemos/hello:latest
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Mi
```
```bash
kubectl apply -f statefulset-demo.yaml
kubectl -n demo get pods -l app=demo-stateful # demo-stateful-0, demo-stateful-1 — стабильные имена, не случайные
kubectl -n demo get pvc # у каждого пода свой PVC
kubectl -n demo get pv
# показать, что PVC переживает пересоздание своего пода
kubectl -n demo delete pod demo-stateful-0
kubectl -n demo get pods -l app=demo-stateful # пересоздался с тем же именем demo-stateful-0 и тем же PVC
```
### Самопроверка
- Своими словами: почему для базы данных в K8s используют StatefulSet, а не Deployment?
- Что произойдёт с данными в `emptyDir`, если под пересоздан целиком (не просто перезапущен контейнер внутри него)? (данные потеряются — emptyDir живёт не дольше самого пода).
### Как это в реальных проектах
Это прямой ответ на реальный вопрос с собеседования «как будешь разворачивать PostgreSQL на две ноды K8s» (см. [QUESTIONS.md](QUESTIONS.md)): StatefulSet + PVC на каждую реплику + обычно готовый оператор (CloudNativePG, Zalando Postgres Operator), который поверх StatefulSet настраивает master-replica топологию и Service, разделяющий трафик на запись/чтение. Принцип репликации сам по себе прогнан отдельно в Docker Compose — см. [../databases/CASE.md](../databases/CASE.md); K8s здесь — просто уровень оркестрации поверх той же логики.
---
## Модуль 11. Helm, часть 1 — пользователь чартов
### Зачем это
Реальные приложения (особенно готовые open-source системы вроде Prometheus/Grafana) состоят из десятков манифестов. Helm — золотой стандарт для их установки и обновления одной командой вместо ручного `kubectl apply -f` на каждый файл.
### Теория-минимум
- **Chart** — упакованный набор шаблонизированных манифестов K8s (аналог пакета в apt/npm).
- **Release** — конкретный установленный в кластер экземпляр чарта (можно поставить один и тот же чарт несколько раз под разными именами релиза — например, две независимые инсталляции Prometheus).
- **Repository** — источник, откуда качаются чарты (аналог apt-репозитория или npm registry).
- `values.yaml` — файл с параметрами, которые чарт подставляет в свои шаблоны при установке — то, чем конкретная инсталляция отличается от дефолтной.
Ключевые команды: `helm repo add/update` (подключить и обновить репозиторий), `helm search repo` (найти чарт), `helm install` (поставить новый релиз), `helm upgrade` (обновить существующий релиз новыми values/версией), `helm rollback` (откатить релиз к прошлой ревизии — аналог `kubectl rollout undo`, но на уровне всего чарта), `helm uninstall` (удалить релиз).
### Практика руками
```bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm search repo prometheus-community/kube-prometheus-stack
# посмотреть дефолтные values перед установкой — не ставить вслепую
helm show values prometheus-community/kube-prometheus-stack > default-values.yaml
# ставим с переопределением — уменьшаем ресурсы под pet-кластер
cat > my-values.yaml << 'EOF'
grafana:
adminPassword: "lab-admin"
prometheus:
prometheusSpec:
resources:
requests:
cpu: 200m
memory: 512Mi
EOF
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
-f my-values.yaml
kubectl -n monitoring get pods
helm list -n monitoring
helm status monitoring -n monitoring
# обновление релиза — меняем values и применяем поверх существующей инсталляции
helm upgrade monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--set grafana.adminPassword=new-lab-admin \
-f my-values.yaml
helm history monitoring -n monitoring
# при необходимости: helm rollback monitoring 1 -n monitoring
kubectl -n monitoring port-forward svc/monitoring-grafana 3000:80
```
### Самопроверка
- Своими словами объяснить разницу между Chart и Release (пакет vs конкретная установленная копия пакета).
- Зачем перед `helm install` смотреть `helm show values`, а не сразу ставить с дефолтами вслепую?
- Чем `helm upgrade` отличается от повторного `helm install`? (upgrade применяет изменения к существующему релизу с сохранением истории ревизий, install создаст конфликт с уже существующим релизом).
### Как это в реальных проектах
Это прямой ответ на реальный вопрос с собеседования Bi.Zone «как задеплоишь мониторинг на 300 000 серверов» (см. [../../legend/CAPACITY.md](../../legend/CAPACITY.md)): не вручную, а Helm-чартом с `values.yaml`, где параметризуются количество реплик, ресурсы, remote_write в Mimir — тот же принцип «Prometheus + Grafana», что и в рабочем стеке (см. [../monitoring/CASE.md](../monitoring/CASE.md)), но развёрнутый декларативно и параметризуемо, а не через docker-compose с ручными правками.
---
## Модуль 12. Helm, часть 2 — автор чарта
### Зачем это
Пользоваться готовыми чартами (модуль 11) — половина навыка. Уметь упаковать своё приложение в чарт — то, что отличает «ставил через Helm» от «понимаю, как Helm устроен изнутри», и именно это спрашивают на более глубоких технических собеседованиях.
### Теория-минимум
Структура чарта, созданного `helm create`:
- `Chart.yaml` — метаданные чарта (имя, версия чарта, версия приложения).
- `values.yaml` — дефолтные параметры, доступные в шаблонах как `.Values.*`.
- `templates/` — сами манифесты K8s, но с шаблонизацией Go-template: `{{ .Values.replicaCount }}` подставит значение из values.yaml, `{{ include "chart.fullname" . }}` — переиспользуемый фрагмент из `_helpers.tpl`, `{{- if .Values.ingress.enabled }}...{{- end }}` — условное включение блока манифеста.
- `_helpers.tpl` — вспомогательные именованные шаблоны (чтобы не дублировать логику вроде генерации имени релиза в каждом файле).
- `NOTES.txt` — текст, который Helm печатает пользователю сразу после `helm install` (подсказка, как получить доступ к приложению).
### Практика руками
```bash
helm create demo-chart
```
Разобрать построчно, что `helm create` сгенерировал по умолчанию — открыть `demo-chart/Chart.yaml`, `demo-chart/values.yaml`, `demo-chart/templates/deployment.yaml`, `demo-chart/templates/_helpers.tpl` и сопоставить с манифестами, которые писались руками в модулях 37: `templates/deployment.yaml` — это тот же Deployment из модуля 4, но `replicas`, `image.repository`, `image.tag` заменены на `{{ .Values.replicaCount }}`, `{{ .Values.image.repository }}` и т.д.
```bash
# проверка шаблона без реальной установки — что синтаксис корректен
helm lint demo-chart
# рендер шаблонов локально — увидеть итоговый YAML, который получится после подстановки values
helm template demo-chart
# рендер с переопределёнными values — увидеть разницу
helm template demo-chart --set replicaCount=3 --set image.repository=nginxdemos/hello
```
Адаптировать `values.yaml` и `templates/deployment.yaml` под приложение из модулей 37 (образ `nginxdemos/hello`, свой ConfigMap/Secret) и поставить своим чартом вместо ручных `kubectl apply`:
```bash
helm install demo-release ./demo-chart --namespace demo --create-namespace
kubectl -n demo get all -l app.kubernetes.io/instance=demo-release
helm uninstall demo-release -n demo
```
### Самопроверка
- Открыть `templates/deployment.yaml` сгенерированного чарта и найти в нём 3 места, где вместо жёстко заданного значения подставляется `.Values.*` — объяснить, зачем каждое из них параметризовано.
- Чем `helm template` отличается от `helm install`? (template — просто рендерит YAML локально, ничего не применяет к кластеру; install — рендерит и сразу применяет).
- Зачем нужен `_helpers.tpl`, если можно было бы просто продублировать `{{ .Release.Name }}-demo-chart` в каждом файле шаблонов?
### Как это в реальных проектах
Собственные чарты пишут, когда приложение — своё (не готовый open-source продукт вроде Prometheus), и его нужно раскатывать в несколько окружений с разными параметрами (dev/stage/prod) без копипаста манифестов — это прямая параллель с тем, как в рабочем стеке Ansible-роли параметризуются через переменные вместо копирования плейбуков под каждое окружение (см. [../ansible/CASE.md](../ansible/CASE.md)): тот же принцип «шаблон + параметры», другой уровень (конфигурация ОС/сервисов vs манифесты K8s).
---
## Модуль 13. Как это устроено в реальных проектах
### Зачем это
Финальный модуль — не новая техническая тема, а сборка всех модулей в целостную картину «как это выглядит в реальной команде», плюс честная граница pet-опыта.
### Теория-минимум
Типичный процесс в компании, использующей K8s+Helm:
- Чарты (свои и сторонние) хранятся в git-репозитории вместе с манифестами инфраструктуры — это прямая параллель с рабочим опытом хранения Ansible-ролей в Gitea (см. [../../legend/LEGEND.md](../../legend/LEGEND.md)).
- Раскатка идёт не руками с ноутбука разработчика, а через CI/CD-пайплайн (`helm upgrade` как шаг джобы) — параллель с TeamCity-раскаткой ролей/дашбордов из рабочего опыта (см. [../../legend/STORY.md](../../legend/STORY.md) → «Версионирование дашбордов»).
- Разные `values-dev.yaml` / `values-staging.yaml` / `values-prod.yaml` под одно и то же приложение — тот же чарт, разные параметры на окружение.
- **GitOps** (инструменты ArgoCD/Flux) — следующий шаг развития идеи: вместо того чтобы CI сам выполнял `helm upgrade`, в git-репозитории просто описывается желаемое состояние кластера, а отдельный контроллер внутри кластера постоянно сверяет его с реальностью и сам подтягивает изменения (тот же принцип desired state из модуля 1, но применённый к самому процессу деплоя). Здесь достаточно понимания идеи, не практики — pet-кластер для этого не поднимался.
- **Umbrella-чарт** — чарт, который сам ничего не разворачивает напрямую, а просто объявляет зависимости от нескольких других чартов (например, «наше приложение» + «его конфигурация Redis» одним релизом) — используется, когда несколько связанных сервисов нужно ставить и версионировать вместе.
### Практика руками
Практики в этом модуле нет — это сборочный/рефлексивный модуль. Вместо неё: выписать на бумаге для своего pet-кластера полный путь «от `git commit` в репозитории чарта до работающего пода», используя реальные названия компонентов, которые прошли в модулях 012 (Deployment, Service, Helm release и т.д.) вместо абстрактных слов.
### Самопроверка
- Объяснить своими словами идею GitOps и чем она отличается от «CI-пайплайн просто выполняет helm upgrade».
- Честно сформулировать для себя одним предложением, что из модуля 13 — реальный pet-опыт (структура чартов, values, helm upgrade/rollback), а что — только понимание на уровне «знаю, что это и зачем» (ArgoCD/Flux, продовые GitOps-пайплайны).
### Как это в реальных проектах
Это тот самый честный формат ответа на собеседовании, уже принятый в этом репозитории для других инструментов «на границе опыта» (см. [../../legend/DEPT_STACK.md](../../legend/DEPT_STACK.md) → «Как этим пользоваться на собеседовании»): по GitOps — «сам не разворачивал, но понимаю идею и зачем она нужна», без попытки выдать это за практический опыт.
---
## Выходной контроль
Курс пройден, когда пройдены оба пункта:
1. **Устный прогон [QUESTIONS.md](QUESTIONS.md)** — вслух, без подглядывания в готовые ответы; если запинаешься — вернуться к соответствующему модулю выше (не к готовому ответу), разобраться и сформулировать заново своими словами.
2. **Две схемы на бумаге без подсказок**: архитектура кластера (control-plane/worker, модуль 2) и путь трафика (клиент → под, модуль 56).
### Таблица соответствия: вопрос из QUESTIONS.md → модуль курса
| Вопрос из QUESTIONS.md | Модуль |
|---|---|
| Из чего состоит Kubernetes? | 2 |
| etcd будет на одной виртуалке с K8s или отдельно? | 2 |
| Что такое DaemonSet? | 2 (kube-proxy как DaemonSet) |
| Нарисуй два ЦОД под K8s с балансировщиком | 5, 6 |
| Как будешь разворачивать PostgreSQL на две ноды K8s? | 10 |
| Что такое Namespace и зачем он нужен? | 3 |
| Чем отличается ConfigMap от Secret? | 7 |
| Какой у тебя опыт с Helm и разворотом кластеров K8s? | 11, 12 |
| Как работает трафик в K8s? | 5, 6 |
| Как проверить логи и поды в кластере? | 3, 9 |
После прохождения курса — переходи к [CASE.md](CASE.md) как к компактному конспекту для повторения перед конкретным собеседованием.

View File

@@ -0,0 +1,43 @@
# Вопросы: Kubernetes
Опираются на кейс: [CASE.md](CASE.md). Источники реальных вопросов — [Реалист банк.md](../../interview/Реалист%20банк.md), [Bi.Zone.md](../../interview/Bi.Zone.md), [Alfa-Bank.md](../../interview/Alfa-Bank.md), [VK Cloud.md](../../interview/VK%20Cloud.md), см. сводку в [../../interview/real-interviews.md](../../interview/real-interviews.md).
### Из чего состоит Kubernetes?
Control-plane — `kube-apiserver` (единая точка входа для всех операций), `etcd` (хранилище состояния кластера), `kube-scheduler` (распределяет поды по нодам), `kube-controller-manager` (следит за соответствием реального состояния желаемому). На worker-нодах — `kubelet` (управляет подами на своей ноде) и `kube-proxy` (сетевые правила доступа к Service), плюс container runtime (`containerd`). У себя в кластере (`kind`, 1 control-plane + 2 worker) я это видел напрямую через `kubectl get pods -n kube-system`.
### etcd будет на одной виртуалке с K8s или отдельно?
В маленьких/pet-инсталляциях (у меня в `kind`) etcd живёт на той же control-plane ноде. В проде для отказоустойчивости etcd обычно выносят на отдельные ноды нечётным числом (3 или 5) — так работает кворум: при потере части узлов кластер etcd продолжает принимать решения, если жива больше половины. Consul в этом контексте — альтернатива service discovery/конфиг-хранилища, не замена etcd как хранилища состояния самого K8s.
### Что такое DaemonSet?
Контроллер, который гарантирует, что на каждой (или на подмножестве по селектору) ноде кластера запущена ровно одна копия пода — используется для агентов уровня ноды: `kube-proxy`, CNI-плагин, node-exporter для мониторинга. В отличие от Deployment, DaemonSet не масштабируется числом реплик — он масштабируется вместе с числом нод: добавили ноду — на ней автоматически появился под DaemonSet.
### Нарисуй два ЦОД под K8s с балансировщиком — на каких виртуалках это будет работать?
Внешний балансировщик (L4/L7, например HAProxy/облачный LB) перед двумя ЦОД → в каждом ЦОД — набор worker-нод под control-plane (control-plane лучше растянуть между ЦОД нечётным числом инстансов ради кворума etcd, например 3: 2 в одном ЦОД + 1 в другом, или 3+2 при пяти) → внутри кластера трафик до пода идёт через Service (ClusterIP/NodePort) и `kube-proxy`, который резолвит запрос в конкретный под по iptables/IPVS-правилам. Каждый ЦОД — набор физических/виртуальных worker-нод с kubelet и containerd. Я такую схему целиком не эксплуатировал (моя лаба — 3 ноды на одной машине), но принцип балансировки и роль каждого компонента понимаю и воспроизвёл в миниатюре.
### Как будешь разворачивать PostgreSQL на две ноды K8s?
Через `StatefulSet` (не Deployment — под'ам БД нужны стабильные имена и отдельные PersistentVolume на каждую реплику) с master-replica топологией: обычно готовый оператор (например, CloudNativePG или Zalando Postgres Operator) заводит StatefulSet, PersistentVolumeClaim на каждую ноду и Service, разделяющий трафик на запись (к master) и на чтение (к репликам). Сам оператор в кластере не поднимал — но принцип репликации PostgreSQL прогнан отдельно в Docker Compose, см. [../databases/CASE.md](../databases/CASE.md); K8s тут просто уровень оркестрации поверх той же логики репликации.
### Что такое Namespace и зачем он нужен?
Логическая изоляция ресурсов внутри одного кластера — разделение по окружениям (dev/stage/prod) или командам, без поднятия отдельных кластеров. Объекты в разных namespace могут называться одинаково и не конфликтуют; ресурсы вроде Node и PersistentVolume — не namespaced, они общие на весь кластер. В своей лабе завёл отдельный namespace `demo` под тестовое приложение и отдельный `monitoring` под kube-prometheus-stack — чтобы не мешать их друг другу и явно видеть границу через `kubectl -n <namespace>`.
### Чем отличается ConfigMap от Secret?
ConfigMap — для несекретных конфигурационных данных (переменные окружения, конфиг-файлы), Secret — для чувствительных значений (пароли, токены, ключи). Технически Secret по умолчанию хранит значения в base64 — это кодирование, а не шифрование, то есть сам по себе Secret не «защищает» данные от того, кто имеет доступ к API/etcd. Для реальной защиты нужны либо шифрование etcd at rest, либо внешние системы вроде Vault (см. [../vault/CASE.md](../vault/CASE.md)) или Sealed Secrets. В своём манифесте завёл и ConfigMap, и Secret и явно проверил разницу через `kubectl get secret app-secret -o yaml` — значение там закодировано base64, не зашифровано.
### Какой у тебя опыт с Helm и разворотом кластеров K8s?
Helm — пакетный менеджер для K8s: чарт описывает набор шаблонизированных манифестов с параметрами в `values.yaml`, `helm install`/`upgrade` разворачивает/обновляет релиз одной командой вместо ручного `kubectl apply` на десяток файлов. У себя разворачивал `kube-prometheus-stack` через `helm install` в отдельный namespace — это дало сразу Prometheus + Grafana + Alertmanager + нужные ServiceMonitor-ресурсы с рабочими дефолтами, которые я после точечно переопределял через `--set`/`values.yaml`.
### Как работает трафик в K8s?
Клиент → внешний балансировщик/Ingress-контроллер (роутинг по host/path) → Service (стабильный виртуальный IP/DNS-имя, абстракция над множеством подов) → `kube-proxy` на нодах транслирует обращение к Service в адрес конкретного пода по правилам iptables/IPVS, с балансировкой между подами, которые матчатся по `selector` в Service. Между подами внутри кластера — плоская сеть через CNI-плагин, любой под может обратиться к любому другому по его Pod IP или через Service DNS-имя (`<service>.<namespace>.svc.cluster.local`).
### Как проверить логи и поды в кластере?
`kubectl get pods -A` — все поды по всем namespace; `kubectl -n <ns> logs <pod>` — логи контейнера, `--previous` — логи упавшего перед текущим рестартом контейнера; `kubectl -n <ns> describe pod <pod>` — события и причина, если под не стартует (`ImagePullBackOff`, `CrashLoopBackOff`); `kubectl get events --sort-by=.lastTimestamp` — хронология событий по всему namespace. Специально ронял под (указывал несуществующий образ) и разбирал `describe`, чтобы увидеть, как выглядит диагностика в реальности, а не в теории.

22
stack/languages/CASE.md Normal file
View File

@@ -0,0 +1,22 @@
# Кейс: вспомогательные языки (Python, SQL, C/C++)
> Заготовка. Статус: TODO — требует полной проработки по правилам [../../AI_GUIDELINES.md](../../AI_GUIDELINES.md). Эти технологии не имеют отдельной линии опыта в резюме — они вписаны в легенду как сквозные инструменты (см. [../../legend/LEGEND.md](../../legend/LEGEND.md), раздел «Технологии без прямого кейса в опыте работы»). Кейсы здесь должны конкретно привязываться к задачам из уже проработанных категорий, а не существовать отдельно.
## Python — автоматизация
- [ ] Написать реальный скрипт, который решает задачу из уже сделанных кейсов: например, скрипт на Python, который парсит логи Nginx из [../nginx/CASE.md](../nginx/CASE.md) и считает количество 5xx ответов, либо скрипт, дёргающий Prometheus HTTP API из [../monitoring/CASE.md](../monitoring/CASE.md) и печатающий текущее значение метрики.
- [ ] Вопросы: разница списков и кортежей, GIL на базовом уровне, работа с виртуальным окружением (venv), как оформлен typical requirements.txt.
## SQL — работа с данными
- [ ] Поднять локально PostgreSQL/MySQL в Docker, создать простую таблицу (например, лог инцидентов: id, дата, сервис, описание), написать несколько реальных запросов: выборка с фильтром и сортировкой, `GROUP BY` с агрегатом (количество инцидентов по сервису), `JOIN` с таблицей сервисов.
- [ ] Вопросы: разница INNER/LEFT JOIN, что такое индекс и когда он не помогает, разница WHERE и HAVING, что такое транзакция и ACID на базовом уровне.
## C/C++ — чтение кода
- [ ] Не писать production-код с нуля (не основной инструмент), но разобрать небольшой пример на C/C++ (например, простую программу с багом — segfault от null pointer или утечкой памяти) и показать, что кандидат может прочитать код, найти проблему и объяснить её словами — это то, что реально требовалось тестировщику embedded ПО в ОКБ СУХОЙ.
- [ ] Вопросы: разница между стеком и кучей, что такое segmentation fault, зачем нужен valgrind/аналог, разница между указателем и ссылкой.
## Как это ложится в легенду
Все три языка — не основная специализация, а обвязка вокруг основных задач (мониторинг, тестирование embedded ПО). Кейсы должны это подчёркивать: не «я Python-разработчик», а «я писал скрипты на Python, чтобы автоматизировать конкретную рутину сопровождения».

146
stack/linux-bash/CASE.md Normal file
View File

@@ -0,0 +1,146 @@
# Кейс: Linux (Astra/CentOS) и Bash — диагностика и внутренности
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — Astra Linux (АО ТНИИС) как базовая ОС серверного парка, CentOS (ОКБ СУХОЙ) как ОС тестовых стендов, Bash — повседневная автоматизация в обеих ролях.
## Что нужно реально сделать
Практикум на Linux-контейнере/WSL: живые bash-скрипты для обслуживания и прогон диагностических сценариев, которые реально спрашивали на собеседованиях («сервер тормозит — с чего начнёшь», «упал сервер — что делать»).
### 1. Реальные bash-скрипты для обслуживания
**Ротация и архивация логов:**
```bash
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="/var/log/myapp"
ARCHIVE_DIR="/var/log/myapp/archive"
DAYS_TO_KEEP=7
mkdir -p "$ARCHIVE_DIR"
find "$LOG_DIR" -maxdepth 1 -name "*.log" -mtime +1 -exec gzip {} \;
find "$LOG_DIR" -maxdepth 1 -name "*.log.gz" -exec mv {} "$ARCHIVE_DIR" \;
find "$ARCHIVE_DIR" -name "*.log.gz" -mtime +"$DAYS_TO_KEEP" -delete
```
**Проверка места на диске с алертом в лог:**
```bash
#!/usr/bin/env bash
set -euo pipefail
THRESHOLD=85
USAGE=$(df -h / | awk 'NR==2 {gsub("%","",$5); print $5}')
if [ "$USAGE" -ge "$THRESHOLD" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') WARNING: disk usage ${USAGE}% >= ${THRESHOLD}%" >> /var/log/disk-check.log
exit 1
fi
```
**Обвязка перезапуска стенда из [../monitoring/CASE.md](../monitoring/CASE.md):**
```bash
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
docker compose down
docker compose up -d
sleep 5
docker compose ps --filter "status=running" | grep -q prometheus || {
echo "prometheus не поднялся" >&2
exit 1
}
```
### 2. Systemd: unit-файл + таймер вместо cron
```ini
# /etc/systemd/system/disk-check.service
[Unit]
Description=Проверка места на диске
[Service]
Type=oneshot
ExecStart=/usr/local/bin/disk-check.sh
```
```ini
# /etc/systemd/system/disk-check.timer
[Unit]
Description=Запуск disk-check каждые 15 минут
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
```
```bash
systemctl daemon-reload
systemctl enable --now disk-check.timer
systemctl list-timers | grep disk-check
journalctl -u disk-check.service
```
### 3. Диагностический сценарий «сервер тормозит — первые 35 команд»
Порядок, который реально прогонял на своих стендах:
1. `uptime` — load average за 1/5/15 минут, первое ощущение масштаба проблемы.
2. `top` (или `htop`) — какой процесс ест CPU/память прямо сейчас.
3. `free -h` — сколько реально свободной памяти (`available`, а не `free`, см. QUESTIONS.md).
4. `df -h` — не закончилось ли место на диске (частая причина зависаний записи).
5. `iostat -x 1` / `vmstat 1` — I/O-нагрузка на диск, если `top` не показал явного виновника по CPU.
### 4. Диагностический сценарий «df показывает, что место закончилось, а du — что всё в порядке»
Воспроизвести самому:
```bash
# терминал 1: занимаем место большим файлом внутри работающего процесса
dd if=/dev/zero of=/tmp/bigfile bs=1M count=2000 &
PID=$!
sleep 1
rm /tmp/bigfile # удаляем файл, пока процесс его ещё держит открытым
df -h /tmp # место всё ещё занято
du -sh /tmp # du не видит файл — его уже нет в дереве каталогов
ls -l /proc/$PID/fd | grep bigfile # но дескриптор в /proc/<pid>/fd всё ещё жив
wait $PID
df -h /tmp # только теперь место освобождается
```
Причина: `du` считает по дереву каталогов (файла там уже нет — он удалён), а `df` считает по факту занятых блоков на файловой системе, которые освобождаются только когда закрывается последний файловый дескриптор, ссылающийся на inode. Пока процесс держит файл открытым, место физически занято, хотя ссылки на файл в каталоге уже нет.
### 5. Права, владение, поиск в логах
```bash
chmod 755 /usr/local/bin/disk-check.sh
chown appuser:appgroup /var/log/myapp
# поиск всех строк с ошибкой в большом логе
grep -c "connection timeout" /var/log/myapp/app.log
awk '/connection timeout/ {print $0}' /var/log/myapp/app.log | tail -20
```
### 6. Сигналы и процессы
```bash
ps aux | grep myapp
kill -15 <pid> # SIGTERM — вежливая просьба завершиться
kill -9 <pid> # SIGKILL — принудительное убийство, процесс не может его перехватить/игнорировать
pkill -f myapp # найти и убить по имени/паттерну команды
```
## Что это даёт в разговоре с интервьюером
- Реальные, лично написанные и прогнанные bash-скрипты для обслуживания, а не абстрактные примеры.
- Практический опыт systemd unit + timer как альтернативы cron.
- Воспроизведённый вживую «фокус» df vs du — конкретный, запоминающийся ответ вместо зазубренной теории.
- Уверенная диагностическая последовательность на вопрос «сервер тормозит».
## Как это ложится в легенду
Linux и Bash — сквозной инструмент в обеих ролях (тестировщик на CentOS, инженер сопровождения на Astra). Скрипты и диагностика в этом кейсе — обобщённая практика повседневного обслуживания серверов, характерная для роли инженера сопровождения, без привязки к вымышленным деталям конкретных внутренних систем компаний.

View File

@@ -0,0 +1,79 @@
# Вопросы: Linux и Bash
Опираются на кейс: [CASE.md](CASE.md). Источники — [VK Cloud.md](../../interview/VK%20Cloud.md) (основной объём), [Dit Moscow.md](../../interview/Dit%20Moscow.md), [Alfa-Bank.md](../../interview/Alfa-Bank.md).
### Есть load average в Linux — как считается?
Скользящее экспоненциально взвешенное среднее числа процессов в состоянии "runnable" (готовы выполняться/выполняются) плюс процессов в непрерываемом ожидании ввода-вывода (D-state), усреднённое за 1/5/15 минут. Число выше количества ядер не всегда значит перегрузку CPU — часто виноват диск или сеть (процессы висят в D-state), поэтому смотрю load average вместе с `top`/`vmstat`, а не отдельно.
### Free и available в выводе free — чем отличаются, и если free почти 0, а available много — это плохо?
`free` — буквально свободная, ничем не занятая память. `available` — память, реально доступная для новых приложений без свопинга, с учётом того, что ядро может быстро освободить часть кеша страниц (page cache) и буферов при необходимости. Linux агрессивно использует свободную память под кеш файловой системы — это не потеря, а разумное использование ресурса. Поэтому если `free` близко к 0, а `available` большое — это нормально и даже хорошо: система эффективно использует память под кеш, который мгновенно отдаст приложениям по запросу. Плохой признак — низкий именно `available` вместе с активным использованием swap.
### Кейс: сервер тормозит, с каких 3-5 команд начнёшь диагностику?
`uptime` (load average, масштаб проблемы) → `top`/`htop` (кто ест CPU/память прямо сейчас) → `free -h` (память) → `df -h` (не кончилось ли место, особенно если есть запись логов) → `iostat -x 1`/`vmstat 1` (I/O, если по CPU и памяти виновник не найден). Порядок именно такой — от общей картины к частностям, чтобы не тратить время на глубокую диагностику не в том направлении.
### Как найти pid нужного процесса и убить его?
`ps aux | grep <имя>` или `pgrep -f <паттерн>` — найти pid. Убить: `kill -15 <pid>` (SIGTERM, вежливое завершение, процесс может перехватить сигнал и корректно закрыть ресурсы) или `kill -9 <pid>` (SIGKILL, безусловное принудительное убийство ядром — процесс не может его ни перехватить, ни проигнорировать). Второй способ — `pkill -f <паттерн>` находит и убивает сразу по паттерну имени команды, без отдельного поиска pid.
### Команда kill и её популярные сигналы, 2 способа убить процесс
Основные сигналы: `SIGTERM` (15, по умолчанию у `kill`) — просьба завершиться корректно; `SIGKILL` (9) — принудительное безусловное завершение ядром; `SIGHUP` (1) — исторически «повесить трубку», многие демоны используют его как сигнал перечитать конфиг без полного рестарта; `SIGINT` (2) — то же, что Ctrl+C. Два способа убить: `kill <pid>` (по идентификатору процесса) и `pkill -f <паттерн>` (по имени/паттерну командной строки, без предварительного поиска pid).
### Что делают df и du — с подвохом (df говорит место закончилось, du — что всё в порядке)
`df` считает занятое место по факту использованных блоков файловой системы. `du` считает по дереву каталогов, суммируя размеры файлов, которые там реально видны. Расхождение возникает, когда файл удалён (`rm`), но его всё ещё держит открытым какой-то процесс — файла больше нет в дереве каталогов (`du` его не видит), но место физически не освобождается, пока не закроется последний файловый дескриптор, ссылающийся на inode этого файла (`df` показывает место как занятое). Я воспроизвёл это вживую: `dd` создаёт большой файл в фоне, `rm` его удаляет, пока процесс жив — `df` и `du` расходятся, пока процесс не завершится (`ls -l /proc/<pid>/fd` при этом показывает "живой" дескриптор на уже удалённый файл).
### Что покажет ps -aux / ps -ef?
Оба показывают список всех процессов в системе, но в разном формате: `ps aux` — BSD-стиль (USER, PID, %CPU, %MEM, VSZ, RSS, TTY, STAT, START, TIME, COMMAND), `ps -ef` — System V-стиль (UID, PID, PPID — родительский процесс явно виден, C, STIME, TTY, TIME, CMD). `ps -ef` удобнее, когда нужно явно видеть иерархию процессов через PPID; `aux` привычнее для быстрой оценки потребления ресурсов.
### Как назначить владельца папки в Linux? Как выдаются права, что такое 777 и 755?
`chown user:group /path/to/dir` — сменить владельца (пользователя) и группу; `chown -R` — рекурсивно для содержимого. Права задаются тремя цифрами (владелец/группа/остальные), каждая — сумма: read=4, write=2, execute=1. `755` = владелец rwx (7), группа и остальные r-x (5) — типично для исполняемых скриптов и директорий, куда нужен доступ на чтение всем. `777` = rwx всем без ограничений — практически всегда небезопасно на проде, разрешает любому пользователю системы читать, менять и исполнять файл.
### Что делает awk и варианты использования?
Текстовый процессор, работающий построчно и разбивающий каждую строку на поля по разделителю (по умолчанию — пробел/таб): `$1`, `$2` — первое/второе поле, `$0` — вся строка, `NR` — номер текущей строки. Типичное использование: `awk '{print $1}' file` — вывести первую колонку, `awk -F: '{print $1}' /etc/passwd` — сменить разделитель на `:`, `awk 'NR==2 {print $5}'` — взять конкретное поле конкретной строки (я так парсил вывод `df -h` в скрипте disk-check из [CASE.md](CASE.md)), `awk '/pattern/ {print}'` — фильтрация строк по регулярке, аналог grep, но с доступом к полям.
### У тебя большой лог — как найти все строки с ошибкой connection timeout?
`grep -c "connection timeout" file.log` — посчитать количество вхождений; `grep -n "connection timeout" file.log` — вывести с номерами строк; для действительно больших файлов и потоковой обработки в реальном времени — `tail -f file.log | grep --line-buffered "connection timeout"`. Если нужен контекст вокруг ошибки — `grep -B2 -A2 "connection timeout" file.log` (2 строки до и после).
### Что такое cgroups и namespaces в Linux?
Namespaces — механизм ядра, изолирующий, что процесс видит: свой PID namespace (процесс видит только «свои» процессы, начиная нумерацию с 1), сетевой namespace (свои интерфейсы, таблицы маршрутизации), mount namespace (своя файловая система), UTS (своё hostname) и т.д. Cgroups (control groups) — механизм, ограничивающий и учитывающий, сколько ресурсов процесс может использовать (CPU, память, I/O). Вместе это и есть технический фундамент контейнеризации: контейнер — это обычный процесс на хосте, которому через namespaces ограничили видимость системы, а через cgroups — доступные ресурсы (без отдельной виртуализации ядра, в отличие от полноценных VM). Подробнее в контексте Docker — [../docker/QUESTIONS.md](../docker/QUESTIONS.md).
### Что такое inode?
Структура данных файловой системы, хранящая метаданные файла (владелец, права, размер, время изменения, указатели на блоки данных на диске) — но не само имя файла. Имя файла — это просто запись-ссылка в каталоге, указывающая на inode. Отсюда и разница между df/du из вопроса выше: пока хотя бы один процесс держит открытым файловый дескриптор, указывающий на inode, место на диске не освобождается, даже если все имена (ссылки в каталогах) на этот inode уже удалены.
### Что такое дескрипторы и reference count в файловом дескрипторе?
Файловый дескриптор — небольшое целое число, которым процесс ссылается на открытый файл/сокет/пайп внутри ядра (0, 1, 2 — stdin/stdout/stderr зарезервированы). Reference count в контексте inode — счётчик количества ссылок на него: это и жёсткие ссылки в каталогах (`ln`), и открытые файловые дескрипторы процессов. Данные на диске реально освобождаются только тогда, когда reference count падает до нуля — то есть удалены все имена (hard links) и закрыты все процессы, державшие файл открытым.
### Ссылки жёсткие и мягкие (hard/soft) — в чём разница и что происходит при удалении файла?
Жёсткая ссылка (`ln`) — ещё одно имя, указывающее прямо на тот же inode: у файла и его hard link одинаковый inode-номер, и оба «равноправны» — удаление одного из имён не трогает данные, пока жива хотя бы одна ссылка (см. reference count выше). Мягкая/символическая ссылка (`ln -s`) — отдельный маленький файл, который просто хранит путь к другому файлу; если оригинал удалить, symlink останется, но будет указывать в никуда («битая ссылка»). Hard link не может пересекать границы файловых систем и не может указывать на директорию (за редкими системными исключениями), symlink может — это одно из отличий на практике.
### Как проверить, что порт открыт/слушается, и как найти порт нужного процесса?
`ss -tulpn` (современный аналог `netstat`) — покажет все слушающие TCP/UDP порты и какой процесс их держит. Для конкретного процесса: `ss -tulpn | grep <pid или имя>` либо через `/proc/<pid>/net/tcp` (низкоуровнево). Для проверки доступности порта на удалённом хосте: `nc -zv host port` или `curl -v telnet://host:port`.
### Как будешь менять параметры ядра Linux?
Через `sysctl`: временно — `sysctl -w vm.swappiness=10` (действует до перезагрузки), постоянно — правка `/etc/sysctl.conf` или файла в `/etc/sysctl.d/*.conf` с последующим `sysctl -p` для применения без перезагрузки. Типичные параметры, которые встречаются на практике: `vm.swappiness` (насколько активно система уходит в своп), `net.core.somaxconn` (размер очереди входящих TCP-соединений), `fs.file-max` (лимит открытых файловых дескрипторов на систему).
### Что такое systemd? Зачем нужна директория /etc/systemd/system?
Systemd — современная система инициализации и менеджер служб в большинстве дистрибутивов Linux (включая Astra и современные CentOS/RHEL) — управляет запуском, остановкой, зависимостями и мониторингом состояния сервисов через unit-файлы. `/etc/systemd/system` — стандартное место для unit-файлов, добавленных администратором вручную (в отличие от `/usr/lib/systemd/system`, куда unit-файлы кладут пакеты дистрибутива) — сюда я клал свои unit и timer для скрипта disk-check из [CASE.md](CASE.md).
### Что такое zombie/orphan процесс?
Zombie — процесс, который уже завершился, но его родитель ещё не вызвал `wait()`, чтобы забрать его код возврата — запись о процессе висит в таблице процессов (виден в `ps` со статусом `Z`), но сам процесс уже не потребляет ресурсов кроме этой записи. Orphan — процесс, чей родитель завершился раньше него; такой процесс автоматически «усыновляется» init-процессом (PID 1 или ближайшим subreaper), который и заберёт его код возврата, когда тот завершится — то есть orphan сам по себе не проблема, в отличие от накопления zombie-процессов, что говорит об ошибке в родительском процессе (не вызывает wait()).
### Что будешь делать, если упал сервер? Если сервер на VMware — как будешь спасать VM и какую диагностику проведёшь?
Порядок действий: сначала проверить, отвечает ли хост вообще (ping, SSH) — если нет, для VM это часто означает проблему на уровне гипервизора, а не гостевой ОС. Дальше — зайти через консоль гипервизора (у VMware это vSphere/ESXi console, доступ к которой не зависит от сетевого стека самой VM) и посмотреть состояние виртуалки: зависла ли она (нет отклика на клавиатуру в консоли), ушла в панику ядра (виден текст паники на консоли), или процесс просто не отвечает по сети из-за исчерпания ресурсов. Дальнейшая диагностика по возможности зайти в саму гостевую ОС: `dmesg`/`journalctl -xb` на предмет OOM killer или паники ядра, проверка диска (не забился ли под 100%, что часто останавливает запись логов и сервисов), проверка сети гипервизора (не отвалился ли виртуальный сетевой адаптер). Если гостевая ОС совсем не отвечает — крайняя мера восстановления это перезапуск VM средствами гипервизора (soft reset через ACPI-сигнал, если возможно, иначе hard reset) с последующим разбором причины по логам, которые сохранились до сбоя. Личного опыта администрирования именно VMware у меня нет — это разбор на уровне общей логики диагностики сбоя виртуальной машины, применимой к любому гипервизору.

142
stack/monitoring/CASE.md Normal file
View File

@@ -0,0 +1,142 @@
# Кейс: стек мониторинга (Prometheus + Grafana + Mimir)
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «АО ТНИИС». Задача инженера сопровождения — собирать метрики, визуализировать их и держать масштабируемое хранилище для истории метрик.
## Что нужно реально сделать (домашний стенд)
Разворачиваем локально Prometheus + Mimir + Grafana + node-exporter через Docker Compose. Цель — своими руками пройти путь «метрика с хоста → Prometheus → remote_write в Mimir → дашборд в Grafana», чтобы честно говорить об этом на собеседовании.
### 1. Структура стенда
```
monitoring-lab/
├── docker-compose.yml
├── prometheus/
│ └── prometheus.yml
├── mimir/
│ └── mimir.yml
└── grafana/
└── provisioning/
└── datasources/
└── datasource.yml
```
### 2. docker-compose.yml
```yaml
version: "3.8"
services:
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
ports:
- "9100:9100"
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- "--config.file=/etc/prometheus/prometheus.yml"
ports:
- "9090:9090"
depends_on:
- node-exporter
mimir:
image: grafana/mimir:latest
container_name: mimir
command: ["-config.file=/etc/mimir/mimir.yml"]
volumes:
- ./mimir/mimir.yml:/etc/mimir/mimir.yml
ports:
- "9009:9009"
grafana:
image: grafana/grafana:latest
container_name: grafana
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
ports:
- "3000:3000"
depends_on:
- mimir
- prometheus
```
### 3. prometheus/prometheus.yml
```yaml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "node"
static_configs:
- targets: ["node-exporter:9100"]
remote_write:
- url: "http://mimir:9009/api/v1/push"
```
### 4. mimir/mimir.yml (минимальный single-binary конфиг для лабы)
```yaml
target: all
blocks_storage:
backend: filesystem
filesystem:
dir: /data/blocks
bucket_store:
sync_dir: /data/tsdb-sync
compactor:
data_dir: /data/compactor
ingester:
ring:
replication_factor: 1
limits:
ingestion_rate: 100000
```
### 5. grafana/provisioning/datasources/datasource.yml
```yaml
apiVersion: 1
datasources:
- name: Mimir
type: prometheus
access: proxy
url: http://mimir:9009/prometheus
isDefault: true
```
### 6. Шаги воспроизведения
1. `docker compose up -d` — поднять весь стенд.
2. Открыть Prometheus UI (`localhost:9090/targets`) — убедиться, что target `node` в статусе `UP`.
3. Открыть Grafana (`localhost:3000`, admin/admin), проверить, что источник данных `Mimir` отвечает (Explore → запрос `up`).
4. Создать дашборд вручную: панель CPU (`rate(node_cpu_seconds_total{mode="idle"}[5m])`), панель памяти (`node_memory_MemAvailable_bytes`), панель дисков.
5. Настроить alert rule в Grafana (например, «CPU idle < 20% в течение 5 минут») и проверить, что алерт срабатывает при нагрузке (`stress` внутри контейнера или на хосте).
6. Остановить `node-exporter` и убедиться, что Prometheus помечает target как `DOWN`, а в Grafana это видно на дашборде (пропуск данных) так на практике выглядит инцидент.
### 7. Что это даёт в разговоре с интервьюером
- Понимание разницы **Prometheus vs Mimir**: Prometheus сбор + локальное краткосрочное хранение + alerting-движок; Mimir горизонтально масштабируемое долгосрочное хранилище метрик, совместимое с PromQL, принимает данные через `remote_write`.
- Понимание модели pull (Prometheus сам ходит в `/metrics`) в отличие от push-систем.
- Практическое понимание, что такое target, job, scrape_interval, label.
- Опыт настройки datasource и дашборда в Grafana руками, а не только просмотр готовых.
## Нагрузка и цифры
Один node-exporter не показатель нагрузки; честная оценка масштаба и вопросы про «как мониторить 300 000 серверов» разобраны в [../../legend/CAPACITY.md](../../legend/CAPACITY.md). На этом стенде можно замерить число активных series (`curl localhost:9090/api/v1/status/tsdb`) и вписать результат в таблицу лабных замеров там же.
## Как это ложится в легенду
В реальной работе (АО ТНИИС) 50 дашбордов в Grafana и настройка Mimir для масштабирования, унификация метрик из разных источников. Домашний кейс воспроизводит тот же путь данных в миниатюре: один источник (node-exporter) вместо десятков, но тот же принцип Prometheus scrape remote_write Mimir Grafana dashboard.

View File

@@ -0,0 +1,67 @@
# Вопросы: мониторинг (Prometheus / Grafana / Mimir)
Опираются на кейс: [CASE.md](CASE.md).
### Чем Prometheus отличается от Zabbix/Nagios?
Prometheus тянет метрики сам (pull-модель) с эндпоинта `/metrics` у сервиса, хранит их как временные ряды с лейблами и даёт мощный язык запросов PromQL. Классические Zabbix/Nagios исторически больше про push-агентов и чек-скрипты с бинарным статусом «ок/не ок». Я поднимал у себя стенд с node-exporter — Prometheus сам ходит и забирает метрики по расписанию (`scrape_interval`), это и есть pull.
### Что такое target, job, label в Prometheus?
Target — конкретный адрес, откуда собираются метрики (например, `node-exporter:9100`). Job — логическая группа таргетов одного типа (в моём конфиге — job `node`). Label — пара ключ-значение, которая добавляется к метрике и позволяет фильтровать/группировать в PromQL (`instance`, `job`, и кастомные).
### Зачем нужен remote_write и чем Mimir отличается от обычного Prometheus?
Prometheus по умолчанию хранит данные локально и недолго (по умолчанию TSDB держит порядка 15 дней). Если нужно долгосрочное хранение и горизонтальное масштабирование под много Prometheus-инстансов, метрики отправляют через `remote_write` во внешнее хранилище — у меня это Mimir. Mimir совместим с PromQL, поэтому Grafana обращается к нему так же, как к обычному Prometheus-источнику, но данные хранятся отдельно и масштабируемо (у меня — на файловой системе, в проде обычно объектное хранилище типа S3).
### Как устроен путь метрики от хоста до дашборда?
node-exporter отдаёт метрики на `/metrics` → Prometheus по scrape_interval их забирает → одновременно пушит их через remote_write в Mimir → Grafana ходит в Mimir как в datasource и рисует панели через PromQL-запросы.
### Что делать, если target в Prometheus в статусе DOWN?
Проверить: доступен ли хост/порт сетевым способом (в моём случае — контейнер node-exporter упал или сеть Docker недоступна), отдаёт ли эндпоинт `/metrics` корректный ответ, не изменился ли путь/порт в конфиге scrape_config. Я специально гасил node-exporter в стенде и смотрел, как это отражается в Prometheus targets и на дашборде (пропуски данных).
### Как написать alert rule и куда он «стреляет»?
Alert rule — это условие на PromQL-выражение с длительностью (например, «CPU idle < 20% в течение 5 минут» алерт). В Grafana я настраивал такое правило на дашборде; в проде алерты обычно уходят дальше в Alertmanager/мессенджеры этого в лабе я не поднимал, но принцип понятен: правило проверяется на интервале evaluation_interval, и если условие держится дольше `for`, алерт переходит из pending в firing.
### Чем отличается rate() от irate() в PromQL?
Обе функции считают скорость роста counter-метрики, но `rate()` усредняет по всему интервалу окна (более гладкий график, подходит для алертов и дашбордов), а `irate()` считает по двум последним точкам (более «дёрганый», подходит для быстрого реагирования на всплески). В своих запросах CPU я использовал `rate()`, потому что нужен был сглаженный тренд, а не мгновенный всплеск.
### Зачем нужна унификация форматов метрик, если данные идут из разных источников?
Если разные экспортеры называют одну и ту же сущность по-разному (разные названия лейблов, разные единицы измерения), дашборды и алерты становится сложно переиспользовать и сравнивать между сервисами. Унификация это соглашение об именовании метрик и лейблов, чтобы один и тот же дашборд можно было применить к разным сервисам без переделки запросов.
### Как организовать доступ к дашбордам для команды?
В Grafana это роли и права на уровне организации/папок дашбордов плюс подключение аутентификации (LDAP/OAuth/встроенные пользователи). Права разграничиваются так, чтобы просмотр был у всех, а редактирование у ограниченного круга, чтобы не ломали чужие дашборды.
### В чём разница между метриками и логами и почему нужны оба?
Метрики агрегированные числовые ряды во времени (быстро смотреть тренды, ставить алерты по порогам). Логи детальные текстовые события конкретного момента (нужны, чтобы понять, что именно произошло внутри инцидента). Метрика в Grafana покажет, что вырос p99 задержки, но причину чаще всего покажут логи поэтому мониторинг обычно закрывается связкой метрики (Prometheus/Mimir) + логи (ELK, см. [../elk/](../elk/)).
### Как задеплоил бы мониторинг архитектурно для очень большого парка серверов (например, 300 000)?
Grafana + Prometheus + Mimir + объектное хранилище (MinIO/S3) под блоки Mimir. Один Prometheus не рассчитан на такой масштаб (публичный ориентир до ~12 млн активных series на инстанс, см. [../../legend/CAPACITY.md](../../legend/CAPACITY.md)), поэтому схема множество Prometheus-инстансов, шардированных по группам серверов/сервисов (например, по датацентру или по команде), каждый scrape'ит свою часть парка и отправляет данные через `remote_write` в Mimir. Mimir горизонтально масштабируется (ingester/distributor/querier как отдельные компоненты) и хранит блоки в объектном хранилище это снимает ограничение на объём долгосрочного хранения метрик с одного диска. Grafana обращается к Mimir как к единому PromQL-совместимому источнику, не зная о шардировании снизу. У себя в лабе я воспроизвёл этот же принцип в миниатюре Prometheus remote_write Mimir Grafana (см. [CASE.md](CASE.md)), просто с одним источником вместо тысяч, и в кластере [../kubernetes/CASE.md](../kubernetes/CASE.md), где тот же стек развёрнут Helm-чартом `kube-prometheus-stack`.
### У тебя есть 300 000 метрик от кастомных агентов в формате JSON — как будешь их собирать?
Раз агенты сами по себе пушат данные и их формат (JSON) нельзя поменять, два реалистичных варианта: (1) pull-модель написать текстовый файл-адаптер для `node_exporter` через его textfile collector: отдельный процесс парсит входящие JSON (`jq` или скрипт на Python) и периодически перезаписывает `.prom`-файл в формате метрик Prometheus, который `node_exporter` дальше просто отдаёт по scrape; (2) push-модель если агенты сами инициируют отправку, а не ждут scrape, использовать Pushgateway: агент (или обвязка вокруг него) конвертирует JSON в формат Prometheus и пушит через HTTP API Pushgateway, откуда данные уже забирает Prometheus обычным scrape. Выбор между вариантами зависит от того, кто инициирует передачу если агенты только отдают данные по запросу, то pull через textfile collector; если сами шлют данные без внешнего опроса Pushgateway.
### К тебе пришёл разработчик с жалобой на HTTP 400/500 — как будешь мониторить и почему?
Начал бы с метрик они быстрее покажут масштаб и динамику проблемы: в Grafana смотрю долю 4xx/5xx от общего числа запросов во времени (`rate` по статус-коду, если это есть в лейблах метрики например, у nginx через `nginx-prometheus-exporter` или у самого приложения) это отвечает на вопросы «когда началось», «это массово или единичный случай», «на каком именно сервисе». Дальше для выяснения точной причины конкретных ошибок логи (ELK): по временному окну, где метрики показали всплеск, ищу в логах конкретные запросы с этим статус-кодом и смотрю стектрейс/сообщение об ошибке. То есть метрики для обнаружения и оценки масштаба, логи для диагностики конкретной причины; в стенде [../nginx/CASE.md](../nginx/CASE.md) я специально проверял, как выглядит 503 от `limit_req` в логах nginx и как это же отразилось бы на метрике доли ошибочных ответов.
### Работал с Zabbix? Чем он отличается от Prometheus и что такое зонтичный мониторинг?
Практического опыта с Zabbix у меня нет, но принцип понимаю: Zabbix классическая push/агентская модель мониторинга с центральным сервером, который получает данные от агентов на хостах (или сам их опрашивает по SNMP/скриптам), и с готовым UI из коробки, включая triggers/actions для алертинга исторически более «всё в одном», чем связка Prometheus+Grafana, которая собирается из отдельных модульных компонентов. Зонтичный мониторинг верхнеуровневая система, агрегирующая сигналы (алерты/статусы) от нескольких нижележащих систем мониторинга разных команд/доменов в единую панель для дежурных/руководства не подменяет специализированные системы мониторинга конкретных сервисов, а сводит их в общую картину состояния на уровне всей организации.
### Что такое Monq?
Российская платформа для зонтичного (umbrella) мониторинга и AIOps агрегирует события/алерты из нижележащих систем мониторинга разных команд (включая Zabbix, Prometheus и другие источники) в единую панель, с возможностью писать сценарии обработки событий (корреляция, автоматические действия по алертам). Личного опыта работы с конкретно Monq у меня нет знаю его как класс продукта (зонтичный мониторинг, см. вопрос про Zabbix выше) по описанию вакансии, а не по практике; на собеседовании честно обозначил бы это и попросил рассказать подробнее о том, как именно он используется в их процессах.
### Что такое переменные в Grafana?
Параметры дашборда, которые можно менять через выпадающий список сверху (например, `$instance`, `$job`, `$datacenter`), не редактируя сами запросы панелей сами PromQL-запросы ссылаются на переменную (`up{instance="$instance"}`), и при смене значения в выпадающем списке весь дашборд пересчитывается под выбранный контекст. Это то, что позволяет держать один шаблонный дашборд вместо копии на каждый сервис/инстанс прямое продолжение темы унификации метрик, которая уже разбиралась выше.

68
stack/networking/CASE.md Normal file
View File

@@ -0,0 +1,68 @@
# Кейс: сетевая база (практикум команд)
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «Домашняя лаборатория / pet-проект». Сети как отдельная тема не входили ни в резюме, ни в рабочий стек явно — но это базовые знания, без которых не воспринимается ни один инфраструктурный собес (весь собес [VK Cloud](../../interview/VK%20Cloud.md) построен на сетевой базе). Здесь не docker-стенд, а практикум: реальные команды, прогнанные на своей машине, с записанным выводом.
## Что нужно реально сделать
Не разворачивать отдельную инфраструктуру, а вживую прогнать диагностические команды и зафиксировать, что именно они показывают — это то, что на собеседовании часто просят показать в терминале.
### 1. DNS: резолвинг и разница authoritative/recursive
```bash
dig vk.com # полный ответ: ANSWER SECTION, TTL, тип записи A
dig +trace vk.com # путь от корневых DNS-серверов до authoritative-сервера домена
nslookup vk.com
```
Рекурсивный резолвер (например, DNS провайдера или публичный 8.8.8.8) сам обходит цепочку: корневые сервера → TLD-сервера (.com) → authoritative-сервер зоны vk.com, и возвращает клиенту готовый ответ, кешируя его на своё TTL. Authoritative-сервер — тот, у кого хранится реальная зона домена и который выдаёт финальный, «истинный» ответ по этому домену.
### 2. Traceroute — как получает IP промежуточных хостов
```bash
traceroute 8.8.8.8 # Linux/macOS
tracert 8.8.8.8 # Windows
```
Traceroute шлёт пакеты с постепенно увеличивающимся TTL (1, 2, 3...). Каждый маршрутизатор на пути, получив пакет с TTL, уменьшает его на 1; когда TTL становится 0, маршрутизатор отбрасывает пакет и отправляет обратно ICMP Time Exceeded с указанием своего IP. Так по очереди «подсвечивается» каждый хоп на пути — сначала ближайший роутер (TTL=1 истекает у него), потом следующий (TTL=2) и так далее, пока пакет не дойдёт до цели.
### 3. HTTP/S, TLS, методы
```bash
curl -v https://vk.com # -v показывает весь handshake: TCP connect, TLS handshake, заголовки запроса/ответа
curl -I https://vk.com # только заголовки ответа
openssl s_client -connect vk.com:443 -brief # детали TLS-сертификата и версии протокола
```
Прогнать вручную запросы разными методами на тестовый сервис (например, `httpbin.org`): `curl -X GET/POST/PUT/DELETE/PATCH https://httpbin.org/anything` — увидеть, как сервис получает метод и тело запроса.
### 4. Порты и процессы
```bash
ss -tulpn # какие порты слушаются и каким процессом (Linux)
netstat -ano | findstr :80 # Windows-аналог
Test-NetConnection -ComputerName example.com -Port 443 # PowerShell: проверка доступности порта на удалённом хосте
nc -zv example.com 443 # аналог на Linux
```
### 5. Load average и сетевые утилиты диагностики
```bash
uptime # load average за 1/5/15 минут
ping -c 4 8.8.8.8
```
Load average — среднее число процессов, находящихся в состоянии "runnable" (выполняются или ждут CPU) либо в непрерываемом ожидании ввода-вывода (D-state), усреднённое за 1/5/15 минут. Число выше количества ядер CPU не всегда означает перегрузку процессора — часто это диск/сеть (процессы висят в D-state в ожидании I/O), поэтому load average нужно смотреть вместе с `top`/`vmstat`, а не изолированно.
### 6. Прогон полного пути запроса
Разобрать по шагам, что происходит при вводе `vk.com` в браузере и Enter — от DNS-резолвинга до отрисовки страницы (см. ответ в [QUESTIONS.md](QUESTIONS.md)) — и свериться с реальным выводом `curl -v` из пункта 3, где видны все те же этапы вживую (TCP connect, TLS handshake, HTTP-запрос/ответ).
## Что это даёт в разговоре с интервьюером
- Уверенное владение диагностическими утилитами (`dig`, `traceroute`, `curl -v`, `ss`) — не только знание теории, но и что конкретно смотреть в выводе команды.
- Понимание TCP/UDP, HTTP/TLS не абстрактно, а с привязкой к реальному выводу `curl -v`/`openssl s_client`.
- Способность разобрать сетевой путь запроса по шагам под живым вопросом на собеседовании.
## Как это ложится в легенду
Сетевая база — pet-проработка теории, не привязанная к конкретному эпизоду в компаниях (сети явно не выделены как отдельная строка стека ни в АО ТНИИС, ни в ОКБ СУХОЙ). На собеседовании это честно позиционируется как фундаментальные знания, которые нарабатывались параллельно с администрированием Linux-серверов (Astra/CentOS) в обеих ролях — там сетевая диагностика неизбежно возникала как часть повседневной работы, здесь она отдельно систематизирована и углублена.

View File

@@ -0,0 +1,58 @@
# Вопросы: сети
Опираются на кейс: [CASE.md](CASE.md). Источник — почти целиком [VK Cloud.md](../../interview/VK%20Cloud.md), см. сводку в [../../interview/real-interviews.md](../../interview/real-interviews.md).
### В чём разница авторитетного и рекурсивного DNS?
Рекурсивный резолвер (обычно у провайдера или публичный, например 8.8.8.8) берёт на себя всю работу: получает запрос от клиента и сам обходит цепочку от корневых серверов до нужной зоны, кешируя результат. Авторитетный сервер — тот, у кого реально хранится зона конкретного домена; он не занимается рекурсией для чужих запросов, а просто отдаёт финальный, «истинный» ответ по своей зоне. Я это проверял через `dig +trace vk.com` — видно всю цепочку: root → `.com` TLD → авторитетный сервер зоны `vk.com`.
### Как Traceroute получает IP промежуточных хостов?
Отправляет пакеты с постепенно растущим TTL, начиная с 1. Каждый маршрутизатор на пути уменьшает TTL на 1; когда TTL достигает 0, маршрутизатор отбрасывает пакет и шлёт обратно ICMP Time Exceeded со своим IP-адресом в качестве отправителя. Так каждый следующий хоп «раскрывается» по очереди — TTL=1 покажет первый роутер, TTL=2 — второй и так далее, до момента, когда пакет наконец дойдёт до цели.
### Что такое vlan и vxlan?
VLAN (Virtual LAN) — логическое разделение одной физической L2-сети на несколько изолированных широковещательных доменов через тегирование кадров (802.1Q, VLAN ID в заголовке), без физического разделения проводов/свитчей. VXLAN — способ инкапсулировать L2-трафик внутрь UDP-пакетов поверх L3-сети, что снимает ограничение VLAN на 4096 идентификаторов (24-битный VNI даёт ~16 млн сегментов) и позволяет растягивать один логический L2-сегмент через маршрутизируемую сеть, в том числе между дата-центрами — характерно для облачных/контейнерных платформ, где нужно много изолированных виртуальных сетей поверх общей физической инфраструктуры.
### Как считается load average в Linux?
Это скользящее экспоненциально взвешенное среднее числа процессов в состоянии "runnable" (готовы выполняться или уже выполняются) плюс процессов в непрерываемом ожидании ввода-вывода (D-state), усреднённое за интервалы 1/5/15 минут. Важный нюанс: число выше количества ядер CPU не автоматически значит «процессор перегружен» — часто это диск или сеть (много процессов ждут I/O в D-state), поэтому load average нужно интерпретировать вместе с `top`/`vmstat`/`iostat`, а не как самостоятельный показатель нагрузки CPU.
### Что такое curl -v? Чем отличается от wget?
`curl -v` — verbose-режим curl, показывает весь процесс запроса пошагово: разрешение DNS, установку TCP-соединения, TLS handshake (если HTTPS), отправленные и полученные заголовки. `wget` — тоже инструмент для HTTP(S)/FTP-запросов из терминала, но исторически ориентирован на скачивание файлов (умеет рекурсивно скачивать по ссылкам, докачивать прерванную закачку из коробки), тогда как `curl` изначально заточен под гибкую работу с самим протоколом (произвольные методы, заголовки, тело запроса) и чаще используется для отладки API и HTTP-взаимодействия, а не просто для сохранения файла на диск.
### Как работают HTTP/S и TLS/SSL, зачем нужны? Методы HTTP?
HTTP — прикладной протокол запрос-ответ поверх TCP: клиент отправляет метод + путь + заголовки (+ тело), сервер отвечает статус-кодом + заголовками (+ телом). HTTPS — тот же HTTP, но поверх TLS-соединения: сначала происходит TLS handshake (обмен сертификатами, согласование шифров, выработка сессионного ключа), и уже внутри зашифрованного канала идёт обычный HTTP-обмен — это защищает от перехвата и подмены данных по пути. Методы: GET (получить ресурс, без побочных эффектов), POST (создать/отправить данные), PUT (полностью заменить ресурс), PATCH (частично обновить), DELETE (удалить). Проверял вживую через `curl -v https://vk.com` — там виден отдельно TCP connect, отдельно TLS handshake, отдельно сам HTTP-запрос/ответ.
### Что такое TCP/UDP и как устанавливается соединение?
TCP — соединение-ориентированный протокол с гарантией доставки и порядка байт: соединение устанавливается через three-way handshake (SYN → SYN-ACK → ACK), дальше идёт передача с подтверждениями (ACK) и повторной отправкой потерянных сегментов. UDP — протокол без установления соединения и без гарантий: пакеты (датаграммы) отправляются «как есть», без подтверждения доставки и порядка — используется там, где важна скорость больше надёжности (стриминг, DNS-запросы, некоторые виды мониторинга).
### Что такое MTU?
Maximum Transmission Unit — максимальный размер пакета (в байтах), который может быть передан за один раз без фрагментации на конкретном сетевом интерфейсе/канале. Стандартный Ethernet MTU — 1500 байт. Если пакет больше MTU канала, он либо фрагментируется (IPv4), либо отбрасывается с ICMP «Fragmentation needed» при выставленном флаге DF (актуально, например, при туннелировании — VPN/VXLAN добавляют заголовки поверх, съедая часть MTU, и это частая причина странных обрывов соединений именно на больших пакетах).
### Что такое пакет, кадр, фрейм?
Кадр (frame) — единица данных на канальном уровне (L2), с MAC-адресами отправителя/получателя (Ethernet-кадр). Пакет (packet) — единица данных на сетевом уровне (L3), с IP-адресами, инкапсулируется внутрь кадра при передаче по конкретному каналу. Термин «фрейм» обычно синоним кадра (тот же L2). Ha практике: один и тот же IP-пакет при прохождении через разные сетевые сегменты каждый раз заново упаковывается в новый кадр L2 (например, Ethernet-кадр в локальной сети, PPP-кадр на другом участке), а сам пакет остаётся неизменным до конечного получателя.
### На каком протоколе работает ping и на каком уровне модели OSI?
Ping использует ICMP (Internet Control Message Protocol) — протокол сетевого уровня (L3 в модели OSI), формально «поверх» IP, но не транспортный протокол в привычном смысле (нет портов, как у TCP/UDP). Отправляет Echo Request и ждёт Echo Reply от целевого хоста.
### Какая есть динамическая маршрутизация, что это?
Динамическая маршрутизация — протоколы, по которым маршрутизаторы автоматически обмениваются информацией о доступных сетях и строят/обновляют таблицы маршрутизации без ручной настройки каждого маршрута. Основные: OSPF и IS-IS (link-state протоколы внутри автономной системы, строят полную карту топологии), BGP (протокол между автономными системами — то, на чём фактически держится маршрутизация в интернете), RIP (более старый, distance-vector, для небольших сетей).
### Ввожу vk.com и нажимаю Enter — опиши весь путь до открытия страницы
1. Браузер проверяет DNS-кеш; если пусто — идёт запрос к рекурсивному резолверу, который резолвит домен в IP через цепочку root → TLD → authoritative-сервер.
2. Браузер устанавливает TCP-соединение с полученным IP на порт 443 (three-way handshake: SYN → SYN-ACK → ACK).
3. Поверх TCP — TLS handshake: обмен сертификатами, проверка цепочки доверия, согласование шифров, выработка сессионного ключа.
4. Внутри зашифрованного канала браузер отправляет HTTP-запрос (GET /), сервер отвечает статус-кодом и телом (HTML).
5. Браузер парсит HTML, находит ссылки на дополнительные ресурсы (CSS, JS, картинки) и повторяет для них аналогичный цикл (возможно, переиспользуя уже установленное TCP/TLS-соединение через keep-alive/HTTP2 multiplexing).
6. Браузер рендерит DOM, применяет CSS, выполняет JS — страница становится видимой и интерактивной.
Каждый из этих этапов я лично видел раздельно через `curl -v` — команда явно печатает моменты TCP connect, TLS handshake и HTTP-обмена по отдельности.

111
stack/nginx/CASE.md Normal file
View File

@@ -0,0 +1,111 @@
# Кейс: Nginx как reverse proxy и балансировщик нагрузки
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «АО ТНИИС», конфигурационные файлы Nginx для маршрутизации и балансировки нагрузки перед сервисами.
## Что нужно реально сделать (домашний стенд)
Поднять два бэкенд-сервиса и один Nginx перед ними, который распределяет нагрузку и отдаёт статус бэкендов, чтобы своими руками увидеть балансировку, health-check-подобное поведение и базовую защиту (rate limiting).
### 1. Структура
```
nginx-lab/
├── docker-compose.yml
├── nginx/
│ └── nginx.conf
└── backend/
└── index.html
```
### 2. docker-compose.yml
```yaml
version: "3.8"
services:
backend1:
image: nginx:alpine
container_name: backend1
volumes:
- ./backend/index.html:/usr/share/nginx/html/index.html
environment:
- INSTANCE=backend1
backend2:
image: nginx:alpine
container_name: backend2
volumes:
- ./backend/index.html:/usr/share/nginx/html/index.html
environment:
- INSTANCE=backend2
proxy:
image: nginx:alpine
container_name: proxy
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "8080:80"
depends_on:
- backend1
- backend2
```
### 3. nginx/nginx.conf
```nginx
events {
worker_connections 1024;
}
http {
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=5r/s;
upstream backend_pool {
least_conn;
server backend1:80 max_fails=3 fail_timeout=10s;
server backend2:80 max_fails=3 fail_timeout=10s;
}
server {
listen 80;
location / {
limit_req zone=req_limit burst=10 nodelay;
proxy_pass http://backend_pool;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /health {
return 200 'ok';
add_header Content-Type text/plain;
}
}
}
```
### 4. Шаги воспроизведения
1. `docker compose up -d`.
2. `curl http://localhost:8080/` несколько раз подряд — убедиться, что ответы приходят от backend1 и backend2 попеременно (балансировка `least_conn`; чтобы увидеть разницу в теле ответа, положить в `index.html` разные значения через `INSTANCE` или просто проверить по логам `docker compose logs backend1 backend2`, какой контейнер получил запрос).
3. Остановить один бэкенд (`docker compose stop backend1`) и убедиться, что Nginx продолжает отдавать 200, перенаправляя весь трафик на backend2 — практика `max_fails`/`fail_timeout` в деле.
4. Прогнать быстрый цикл запросов (`for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/; done`) и увидеть `503` от `limit_req`, когда запросы превышают `rate=5r/s` — понять, как работает rate limiting на практике.
5. Проверить `curl http://localhost:8080/health` — отдельный location без проксирования, как обычно делают health-check эндпоинт.
### 5. Что это даёт в разговоре с интервьюером
- Понимание `upstream`-блока и алгоритмов балансировки (`round-robin` по умолчанию, `least_conn`, `ip_hash`).
- Понимание, зачем нужны `proxy_set_header` (чтобы бэкенд видел реальный IP и Host клиента, а не адрес самого Nginx).
- Практика rate limiting (`limit_req_zone`/`limit_req`) как базовой защиты от перегрузки/abuse.
- Понимание `max_fails`/`fail_timeout` как механизма пассивного health-check upstream-серверов.
## Нагрузка и цифры
Замерить пропускную способность своего стенда: `hey -n 10000 -c 50 http://localhost:8080/` (или `wrk -t2 -c50 -d30s http://localhost:8080/`) — записать полученный RPS и задержку p50/p99. Сравнение с публичными ориентирами по nginx (десятки тысяч RPS на статике, тысячи на проксировании — сильно зависит от конфигурации) и таблица для собственных замеров — в [../../legend/CAPACITY.md](../../legend/CAPACITY.md).
## Как это ложится в легенду
В реальной работе (АО ТНИИС) — конфиги Nginx для маршрутизации и балансировки перед сервисами, в том числе перед компонентами мониторинга. Домашний кейс воспроизводит тот же принцип на двух простых бэкендах вместо реального внутреннего сервиса.

39
stack/nginx/QUESTIONS.md Normal file
View File

@@ -0,0 +1,39 @@
# Вопросы: Nginx
Опираются на кейс: [CASE.md](CASE.md).
### Чем reverse proxy отличается от обычного proxy?
Обычный (forward) proxy работает на стороне клиента — скрывает клиента от сервера. Reverse proxy стоит перед сервером(ами) и скрывает их от клиента — клиент обращается к одному адресу, а Nginx уже сам решает, на какой бэкенд отправить запрос. В своём кейсе я как раз настроил Nginx как reverse proxy перед двумя бэкендами через `upstream` + `proxy_pass`.
### Какие алгоритмы балансировки нагрузки есть в Nginx и в чём разница?
По умолчанию — round-robin (запросы по очереди). `least_conn` — на сервер с наименьшим числом активных соединений (я использовал именно его в лабе, чтобы не заливать один инстанс, если он медленнее отвечает). `ip_hash` — привязывает клиента к одному и тому же серверу по хешу IP, что полезно для sticky-сессий, если приложение не умеет в shared-сессии.
### Зачем нужны proxy_set_header в конфиге?
По умолчанию бэкенд видит IP и Host самого Nginx, а не реального клиента, потому что запрос проксируется. `proxy_set_header X-Real-IP $remote_addr` и `X-Forwarded-For` передают реальный IP клиента дальше, а `Host $host` сохраняет исходный заголовок Host — это важно, если бэкенд как-то зависит от домена или логирует IP клиентов.
### Как Nginx понимает, что бэкенд «упал», и что делает в этом случае?
Через пассивные health-check параметры в `upstream`: `max_fails` — сколько неудачных попыток допустимо, `fail_timeout` — на сколько сервер помечается недоступным после превышения max_fails. В моём кейсе я гасил один из бэкендов и видел, что Nginx перестаёт направлять на него трафик и весь поток идёт на оставшийся, без ошибок для клиента.
### Что такое rate limiting и как он настроен в Nginx?
`limit_req_zone` объявляет зону в разделяемой памяти, ключ (обычно `$binary_remote_addr`, то есть IP клиента) и допустимую скорость запросов (`rate=5r/s` в моём случае). `limit_req` в конкретном location применяет эту зону, а `burst` разрешает кратковременный всплеск сверх лимита (запросы ставятся в очередь) — `nodelay` убирает искусственную задержку для burst-запросов, но всё равно возвращает 503 при превышении burst. Я специально прогонял 20 запросов подряд и видел 503 после исчерпания лимита.
### В чём разница между location / и отдельным location /health?
`location` определяет, как обрабатывается конкретный путь URL. Я вынес `/health` в отдельный блок, который отвечает статикой (`return 200`) без проксирования на бэкенд — так делают health-check эндпоинты, чтобы проверка живости самого Nginx/прокси-слоя не зависела от состояния бэкендов.
### Чем отличается $host от $http_host и от server_name?
`server_name` — значение в конфиге, по которому Nginx выбирает нужный `server`-блок для запроса. `$host` — переменная времени выполнения: значение из заголовка Host запроса (без порта), либо server_name, если заголовка нет. `$http_host` — заголовок Host как есть, включая порт, если он был передан клиентом. На практике для проксирования обычно используют `$host`, чтобы не тащить порт туда, где он не нужен.
### Как бы вы отдавали статику через Nginx, а не проксировали на бэкенд?
Через `location` с директивой `root`/`alias`, указывающей на директорию со статикой, вместо `proxy_pass` — тогда Nginx сам отдаёт файлы, не дёргая бэкенд, что быстрее и снимает нагрузку с приложения.
### Что произойдёт, если в upstream все сервера окажутся недоступны?
Nginx вернёт ошибку (обычно 502 Bad Gateway или 504 Gateway Timeout, в зависимости от того, где именно произошёл сбой — на этапе соединения или ожидания ответа) — в моём кейсе, если бы я остановил оба backend1 и backend2, curl к proxy начал бы получать 502.

21
stack/testing/CASE.md Normal file
View File

@@ -0,0 +1,21 @@
# Кейс: тестирование (Selenium, тест-дизайн)
> Заготовка. Статус: TODO — требует полной проработки по правилам [../../AI_GUIDELINES.md](../../AI_GUIDELINES.md).
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «ОКБ СУХОЙ», тестовые сценарии, автоматизированные модульные и интеграционные тесты, работа на CentOS-стендах.
## Что нужно проработать
- [ ] Написать простой Selenium-тест (Python + selenium + pytest) на любой публичный/локальный тестовый сайт (например, свой же локальный сервис из [../nginx/CASE.md](../nginx/CASE.md) с простой HTML-страницей).
- [ ] Структурировать тест по Page Object Model — паттерн, который часто спрашивают отдельно.
- [ ] Написать тест-кейсы (тест-дизайн) на бумаге/в markdown для гипотетической фичи — с шагами, ожидаемым результатом, приоритетом — чтобы показать не только автоматизацию, но и ручной тест-дизайн, упомянутый в резюме.
- [ ] Разобрать разницу unit / integration / e2e тестов применительно к своему опыту (модульные и интеграционные тесты в резюме).
- [ ] Оформить пример баг-репорта (шаги воспроизведения, ожидаемое/фактическое поведение, окружение) — это то, что реально делал инженер-тестировщик.
## QUESTIONS.md
- [ ] Составить вопросы: что такое Page Object Model и зачем он нужен, разница smoke/regression тестирования, как приоритизировать тесты при ограниченном времени, что такое flaky test и как с ним бороться, разница между verification и validation.
## Как это ложится в легенду
Роль в ОКБ СУХОЙ — тестировщик встроенного ПО. Кейс должен показать связку ручного тест-дизайна и автоматизации, а не только Selenium-скрипт в отрыве от контекста.

67
stack/vault/CASE.md Normal file
View File

@@ -0,0 +1,67 @@
# Кейс: HashiCorp Vault (dev-режим + интеграция с CI)
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «Домашняя лаборатория / pet-проект». Vault спрашивали в Альфа-Банке («как работает Hashicorp Vault: как вкладываются секреты, синхронизация с CI/CD») — в рабочем стеке компаний из резюме секреты хранятся через `ansible-vault` (см. [../ansible/QUESTIONS.md](../ansible/QUESTIONS.md)), Vault — отдельный честный pet-кейс поверх этого опыта.
## Что нужно реально сделать (домашний стенд)
Vault в dev-режиме (single binary, in-memory storage — не для прода, но для понимания механики более чем достаточно), положить секрет через KV-движок, прочитать его через CLI и через HTTP API, завести простую политику доступа, и на уровне паттерна разобрать, как секрет из Vault попадает в CI/CD-пайплайн.
### 1. Запуск
```bash
docker run --cap-add=IPC_LOCK -d --name vault-lab \
-p 8200:8200 \
-e 'VAULT_DEV_ROOT_TOKEN_ID=lab-root-token' \
hashicorp/vault server -dev
```
Dev-режим сразу распечатан (unsealed), с готовым root-токеном — специально упрощённый режим для локальной практики, в проде Vault разворачивается с реальным storage backend (Consul/Raft) и требует ручного unseal через Shamir's Secret Sharing (несколько ключей, из которых нужен кворум для распечатывания).
### 2. Положить и прочитать секрет через KV
```bash
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='lab-root-token'
vault kv put secret/ci/db-credentials username=labuser password=labpass
vault kv get secret/ci/db-credentials
vault kv get -field=password secret/ci/db-credentials
```
### 3. То же самое через HTTP API (то, что реально дёргает CI-раннер)
```bash
curl -s -H "X-Vault-Token: lab-root-token" \
http://127.0.0.1:8200/v1/secret/data/ci/db-credentials | jq .data.data
```
### 4. Политика доступа (принцип наименьших привилегий)
```hcl
# ci-readonly-policy.hcl
path "secret/data/ci/*" {
capabilities = ["read"]
}
```
```bash
vault policy write ci-readonly ci-readonly-policy.hcl
vault token create -policy="ci-readonly" -ttl=1h
```
Полученным ограниченным токеном (не root) повторить `vault kv get` — сработает (есть право read); попробовать `vault kv put` этим же токеном — должно быть отказано (Permission denied), так как в политике только `read`. Это ядро модели Vault: не «все или ничего», а точечные политики на конкретные пути.
### 5. Паттерн интеграции с CI/CD (концептуально, без реального раннера)
Типичная схема: CI-раннер (GitLab CI/TeamCity) на старте job аутентифицируется в Vault не статическим токеном в переменных окружения, а через auth-метод (например, AppRole или JWT/OIDC-аутентификация от самого CI, если поддерживается) — получает короткоживущий токен с ограниченной политикой → читает нужные секреты через API → использует их в рамках job → токен истекает сам по TTL. Ключевое отличие от простого хранения секрета в CI-переменных: секрет не лежит статично в конфигурации пайплайна, доступ временный и аудируемый (Vault логирует каждое обращение).
## Что это даёт в разговоре с интервьюером
- Практическое понимание KV secrets engine — как секрет реально «вкладывается» и читается, а не абстрактно.
- Понимание политик доступа Vault (HCL, path-based capabilities) на реальном примере — не просто «там есть права», а конкретный сценарий read-only токена.
- Понимание разницы между dev-режимом (для лабы) и продовым Vault (unseal, storage backend, HA) — честная граница того, что реально прогнано.
- Осмысленное представление о паттерне интеграции с CI/CD через short-lived токены, а не статичные секреты в переменных.
## Как это ложится в легенду
Vault — pet-кейс, не приписанный к опыту в компаниях. В резюме и легенде реальный опыт хранения секретов зафиксирован через `ansible-vault` в АО ТНИИС; Vault добавляется отдельно как самостоятельно проработанная технология в навыках, без утверждения, что она использовалась на рабочем месте.

19
stack/vault/QUESTIONS.md Normal file
View File

@@ -0,0 +1,19 @@
# Вопросы: HashiCorp Vault
Опираются на кейс: [CASE.md](CASE.md). Источник — [Alfa-Bank.md](../../interview/Alfa-Bank.md).
### Как работает Hashicorp Vault: как вкладываются секреты?
Секреты хранятся через secrets engine — самый базовый и распространённый вариант KV (key-value): секрет кладётся по конкретному пути (`vault kv put secret/ci/db-credentials username=... password=...`) и читается тем же путём (`vault kv get`). Под капотом Vault шифрует данные перед записью в storage backend (в моей лабе — in-memory dev-режим, в проде обычно Raft или Consul) собственным мастер-ключом, который в продовом режиме получается через unseal-процесс (Shamir's Secret Sharing — несколько частей ключа, кворум которых нужен, чтобы распечатать хранилище после старта). Помимо статичного KV есть и динамические secrets engines (например, для БД) — Vault может сам генерировать короткоживущие учётные данные на лету, но этого я на своей лабе не поднимал, только статичный KV.
### Как наполняется файл секрета?
Секрет не «файл» в файловой системе в привычном смысле — это запись в хранилище Vault по определённому пути, доступ к которой идёт через CLI (`vault kv put/get`) или HTTP API. Если приложению/пайплайну нужен именно файл (например, `.env` или конфиг с паролем), это обычно результат отдельного шага: агент/раннер запрашивает секрет через API и материализует его во временный файл на своей стороне непосредственно перед использованием, не храня его в репозитории или в постоянном хранилище.
### Как идёт синхронизация с CI/CD?
CI-раннер аутентифицируется в Vault (в идеале — не статичным root-токеном, а через auth-метод вроде AppRole или через встроенную OIDC/JWT-аутентификацию от самой CI-системы, если она поддерживается), получает короткоживущий токен с ограниченной политикой доступа, читает нужные секреты через API в рамках выполнения job, и токен истекает сам по TTL после использования. Я на своей лабе прогнал часть этой схемы вручную: завёл политику `ci-readonly` с правом только `read` на путь `secret/data/ci/*`, выписал по ней ограниченный токен и убедился, что им можно читать секрет, но нельзя его перезаписать (`vault kv put` тем же токеном отдаёт Permission denied) — сам CI-раннер (GitLab CI/TeamCity) я в эту схему не встраивал, это на уровне понимания паттерна.
### Чем это отличается от того, что секрет просто лежит в переменных CI?
Секрет в переменных CI-системы статичен: один раз задан — лежит вечно, доступен всем job без разбора, изменения/ротация требуют ручного вмешательства, и нет отдельного лога, кто и когда именно его читал. Vault даёт: точечные политики доступа (кто и к какому пути может обращаться), короткоживущие токены вместо постоянных секретов, аудит-лог обращений, и возможность динамически генерировать секреты вместо хранения статичных значений.