Перейти к основному содержимому

DevOps Альфа-Банк - Middle 250+ тыс / Реальное собеседование.

· 107 мин. чтения

Сегодня мы разберем техническое собеседование на позицию DevOps/SRE-инженера, где команда Альфа-Банка тестирует реальный практический опыт кандидата в инфраструктуре: Linux, Ansible, Docker, Kubernetes, Git, сетевые политики и отказоустойчивый PostgreSQL. Диалог показывает не только глубину требований к инженеру платформенной команды, но и то, как собеседующие аккуратно вскрывают реальные границы его ответственности и понимания продакшен-инфраструктуры.

Вопрос 1. Чем вы занимаетесь на текущем месте работы и какие задачи находите наиболее интересными?

Таймкод: 00:05:20

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

Правильный ответ:

В текущей роли основная зона ответственности включает полный цикл разработки и сопровождения сервисов на Go, а также участие в построении инфраструктуры и процессов доставки. Ключевые направления работы:

  1. Разработка и эволюция сервисов:

    • Проектирование и реализация микросервисов на Go: REST/gRPC API, интеграции с внешними системами.
    • Работа с высоконагруженными компонентами: оптимизация производительности, снижение латентности, эффективная работа с памятью.
    • Внедрение устойчивых к сбоям решений: ретраи, circuit breaker, таймауты, backoff-стратегии, idempotency.
    • Код-ревью, поддержание единых стандартов кодовой базы, участие в техническом дизайне (дизайн-документы, ADR).

    Пример:

    • Разработка сервиса агрегации данных:
      • gRPC API для внутренних сервисов.
      • Кэширование в Redis.
      • Асинхронная обработка через Kafka.
      • Конфигурация через env/consul, graceful shutdown.
  2. Работа с базами данных и данными:

    • Проектирование схем и запросов к PostgreSQL/MySQL.
    • Оптимизация запросов: индексы, планы выполнения, нормализация/денормализация.
    • Использование миграций (golang-migrate, goose) и практики zero-downtime деплоя.
    • Реализация транзакционной логики и корректной работы с конкурентным доступом.

    Пример SQL:

    • Проектирование индекса для ускорения выборки по статусу и дате:
      • CREATE INDEX CONCURRENTLY idx_orders_status_created_at ON orders(status, created_at);
  3. Инфраструктура и CI/CD:

    • Настройка и поддержка GitLab CI/CD:
      • Многостадийные пайплайны (lint, unit tests, integration tests, build, security scan, deploy).
      • Reusable templates (.gitlab-ci.yml с anchors/includes).
      • Автоматический триггер деплоев по тегам/branсh policies.
    • Контейнеризация:
      • Docker-образы с минимальным пространством атаки (scratch, distroless).
      • Multi-stage builds для уменьшения размера образа.
    • Оркестрация:
      • Деплой в Kubernetes: манифесты/Helm чарты, конфигурация ресурсов, readiness/liveness пробы, авто-масштабирование.
    • Observability:
      • Метрики (Prometheus), логирование (structured logs), трейсы (OpenTelemetry).
      • Настройка алёртов и дашбордов для ключевых бизнес-метрик и системных показателей.
  4. Надежность, качество и инженерные практики:

    • Покрытие кода тестами: unit, integration, contract tests.
    • Использование mocking/stubs для внешних систем.
    • Code review как обязательная часть процесса.
    • Автоматизированные проверки качества: golangci-lint, staticcheck, secret scanning, SAST/DAST.
    • Участие в разборе инцидентов (postmortems), улучшение архитектуры и практик на основе реальных сбоев.
  5. Наиболее интересные задачи:

    • Проектирование архитектуры: как разбить домен на сервисы, определить границы контекстов, контракты API, схемы взаимодействия (sync/async).
    • Оптимизация производительности:
      • Анализ профилей (pprof), снижение аллокаций, тюнинг горутин и каналов.
      • Оптимизация тяжелых SQL-запросов и работы с кэшем.
    • Построение надежного и предсказуемого пайплайна доставки:
      • Быстрые обратимые деплои (blue-green, canary).
      • Минимизация ручных операций.
      • Гарантия, что из git до production путь прозрачен, воспроизводим и контролируем.

Краткий пример пайплайна GitLab CI для Go-сервиса:

stages:
- lint
- test
- build
- deploy

variables:
GO_VERSION: "1.22"

lint:
image: golang:${GO_VERSION}
stage: lint
script:
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
- golangci-lint run ./...
only:
- merge_requests
- branches

test:
image: golang:${GO_VERSION}
stage: test
script:
- go test ./... -coverprofile=coverage.out
artifacts:
reports:
coverage_report:
coverage_format: gocov
path: coverage.out

build:
image: golang:${GO_VERSION}
stage: build
script:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app ./cmd/service
artifacts:
paths:
- app
only:
- main
- tags

deploy_prod:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl apply -f k8s/
environment:
name: production
when: manual
only:
- tags

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

Вопрос 2. Какие конкретные инфраструктурные задачи вы выполняете: что разворачиваете и поддерживаете?

Таймкод: 00:05:55

Ответ собеседника: правильный. Кандидат описал установку и сопровождение приложений на Linux-серверах, использование Kubernetes, Docker Compose, Ansible и Helm, а также поддержку инфраструктуры с разными вариантами инсталляций.

Правильный ответ:

В рамках инфраструктурных задач фокус не только на "поднять и запустить", а на построение воспроизводимой, управляемой, безопасной и наблюдаемой среды для запуска Go-сервисов и сопутствующих компонентов. Основные направления:

  1. Управление Linux-серверами и базовой средой

    • Стандартизация окружений:
      • Единые образы/конфигурации для приложений (timezones, locales, логирование, системные пакеты).
      • Настройка системных лимитов для высоконагруженных сервисов:
        • ulimit, fs.inotify, net.core.somaxconn, настройки TCP (keepalive, backlog).
    • Безопасность:
      • Регулярные обновления, автоматизация security-патчей.
      • Настройка firewall (iptables, nftables, security groups), ограничение открытых портов.
      • Использование SSH-ключей, audit логов, минимизация доступа к прод-системам.
    • Мониторинг и логирование:
      • Установка и настройка агентов (Prometheus node_exporter, Loki/Fluent Bit, Vector).
      • Централизация логов приложений и системы.
  2. Контейнеризация и управление приложениями

    • Docker:
      • Создание оптимизированных Docker-образов для Go:
        • Multi-stage build, статически слинкованный бинарь, использование scratch/distroless.
      • Обеспечение минимального размера и поверхности атаки.
    • Docker Compose:
      • Описание окружений для локальной разработки и стейджингов:
        • Сборка: приложение + БД (PostgreSQL/MySQL) + Redis + Kafka/NATS.
        • Быстрая репликация production-like окружения для интеграционных тестов.

    Пример Dockerfile для Go-сервиса:

    FROM golang:1.22 AS builder
    WORKDIR /app
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o service ./cmd/service

    FROM gcr.io/distroless/base-debian12
    WORKDIR /app
    COPY --from=builder /app/service .
    USER nonroot:nonroot
    ENTRYPOINT ["/app/service"]
  3. Оркестрация на Kubernetes

    • Развертывание и сопровождение приложений в кластере:
      • Deployment, StatefulSet, Service, Ingress/Ingress Controller.
      • Конфигурирование ресурсов (requests/limits), affinity/anti-affinity.
      • Настройка readiness/liveness-проб для корректного управления трафиком.
    • Управление конфигурацией и секретами:
      • ConfigMap и Secret (в том числе через sealed-secrets или external secret store).
    • Масштабирование и отказоустойчивость:
      • Horizontal Pod Autoscaler (HPA), PodDisruptionBudget, стратегия обновлений (RollingUpdate, maxSurge/maxUnavailable).
    • Observability:
      • Интеграция Prometheus/Grafana, алёрты по latency/error rate, saturation ресурсов.
      • Трейсинг (Jaeger/Tempo, OpenTelemetry SDK в Go-сервисах).
  4. Helm как стандарт упаковки и деплоя

    • Создание и поддержка Helm-чартов для сервисов:
      • Параметризация ресурсов, конфигураций, environment-specific values.
      • Версионирование чартов, хранение в internal chart registry.
    • Единые шаблоны:
      • Типовые шаблоны Deployment/Service/Ingress/metrics для всех сервисов.
      • Встроенная поддержка probes, логирования, метрик, секьюрных дефолтов.

    Пример deployment-фрагмента в Helm-шаблоне:

    containers:
    - name: {{ .Chart.Name }}
    image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
    ports:
    - containerPort: {{ .Values.service.port }}
    readinessProbe:
    httpGet:
    path: /health/ready
    port: {{ .Values.service.port }}
    livenessProbe:
    httpGet:
    path: /health/live
    port: {{ .Values.service.port }}
  5. Ansible и автоматизация инфраструктуры

    • Описание инфраструктуры как кода:
      • Playbook для развертывания базовых компонентов (Docker, Kubernetes worker, nginx, runtime зависимостей).
      • Настройка пользователей, прав, системных параметров, ssh, firewall.
    • Zero-touch provisioning:
      • Возможность поднять новый сервер/нод/standalone-окружение одной командой.
    • Интеграция с CI/CD:
      • Запуск Ansible-плейбуков из GitLab CI для обновления окружений, миграции конфигураций.

    Пример: простой плейбук для установки Docker:

    - hosts: app_servers
    become: yes
    tasks:
    - name: Install Docker
    apt:
    name: docker.io
    state: present
    update_cache: yes

    - name: Enable and start Docker
    systemd:
    name: docker
    enabled: yes
    state: started
  6. Поддерживаемые инсталляции и среды

    • Разные варианты развертывания:
      • On-prem, частное облако, публичное облако (AWS/GCP/Azure).
      • Standalone-инсталляции для клиентов (single-tenant), multi-tenant SaaS.
    • Обновления и миграции:
      • Категоризация окружений (dev/stage/prod), канареечные/поштучные деплои.
      • Миграции БД с контролируемым откатом.
    • Диагностика и инциденты:
      • Поиск проблем: метрики, логи, трассировки, профилирование Go-сервисов.
      • Построение плана устранения системных bottleneck’ов (CPU, I/O, network, DB).

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

Вопрос 3. Как у вас организовано развертывание и администрирование PostgreSQL, из чего состоит отказоустойчивая конфигурация и какие компоненты используются?

Таймкод: 00:08:01

Ответ собеседника: правильный. PostgreSQL разворачивается с помощью Ansible-роли (форк из публичного репозитория), используется отказоустойчивая схема: мастер и реплика под управлением Patroni, для хранения конфигурации и координации применяется Consul; все работает на отдельных хостах без контейнеров.

Правильный ответ:

Развертывание и администрирование PostgreSQL в прод-среде должно обеспечивать:

  • предсказуемую установку (infrastructure as code),
  • отказоустойчивость и автоматический failover,
  • согласованность данных,
  • понятные и управляемые точки интеграции для приложений.

Типичная прод-конфигурация с Patroni и Consul выглядит так.

Основные компоненты:

  • PostgreSQL:
    • Основная СУБД.
  • Patroni:
    • Управляет кластером PostgreSQL, репликацией, лидерством и failover.
  • Consul (или Etcd/ZooKeeper):
    • Key-value хранилище для coordination/lock’ов и конфигурации.
  • HAProxy / pgbouncer (рекомендуется):
    • Маршрутизация к актуальному primary и/или connection pooling.
  • Ansible:
    • Автоматизированное развертывание и конфигурация всех вышеуказанных компонентов.
  1. Развертывание PostgreSQL через Ansible

Подход:

  • Вся конфигурация кластера (версии, параметры, пути, пользователи, репликация, Patroni) описана в Ansible-ролях и inventory.
  • Новая нода (primary или replica) может быть поднята автоматически из одной и той же роли.

Ключевые задачи роли:

  • Установка PostgreSQL нужной версии из официального/корпоративного репозитория.
  • Инициализация кластера (initdb) с нужными параметрами.
  • Настройка:
    • postgresql.conf (shared_buffers, work_mem, max_connections, wal_level, max_wal_senders, checkpoint_timeout и т.п.).
    • pg_hba.conf для безопасного доступа и репликации.
  • Создание пользователей, баз, прав:
    • Отдельный пользователь для приложений.
    • Отдельный пользователь для репликации.
  • Настройка бэкапов (например, wal-g / pgBackRest), ротации логов и мониторинга.

Краткий пример ansible-задач (упрощенный):

- name: Install PostgreSQL
apt:
name: postgresql-15
state: present

- name: Configure PostgreSQL
template:
src: postgresql.conf.j2
dest: /etc/postgresql/15/main/postgresql.conf
notify: Restart PostgreSQL
  1. Отказоустойчивая архитектура с Patroni

Patroni берет на себя:

  • управление жизненным циклом PostgreSQL-инстансов;
  • настройку synchronous/asynchronous репликации;
  • выбор лидера (primary);
  • автоматический failover при недоступности primary;
  • публикацию статуса через REST API.

Типовая схема:

  • 3+ PostgreSQL-ноды:
    • 1 primary.
    • 1-2+ replicas.
  • Patroni запущен на каждой ноде:
    • Читает конфигурацию (DSN, параметры PG, репликация).
    • Регистрирует своё состояние в Consul.
  • Consul:
    • Хранит информацию о текущем primary.
    • Используется для распределенного lock’а и координации.

Важные моменты:

  • Репликация:
    • Обычно streaming replication.
    • Можно настроить:
      • async для скорости;
      • synchronous_commit = remote_apply для критичных данных (но это увеличивает latency).
  • Failover:
    • Patroni отслеживает здоровье primary (через локальные проверки и через Consul).
    • При недоступности primary:
      • Получает lock на failover.
      • Продвигает одну из реплик в primary.
      • Обновляет информацию в Consul.
    • Кластер продолжает работать без ручного вмешательства (если сетевой и quorum-консенсус настроены корректно).

Упрощенный пример фрагмента patroni.yml:

scope: pg-cluster
name: pg-node-1

restapi:
listen: 0.0.0.0:8008
connect_address: 10.0.0.1:8008

consul:
host: consul.service.consul:8500
register_service: true

postgresql:
listen: 0.0.0.0:5432
connect_address: 10.0.0.1:5432
data_dir: /var/lib/postgresql/data
bin_dir: /usr/lib/postgresql/15/bin
authentication:
superuser:
username: postgres
password: "secure_password"
replication:
username: repl_user
password: "repl_password"
parameters:
wal_level: replica
max_wal_senders: 10
max_replication_slots: 10
synchronous_commit: "on"
  1. Роль Consul в отказоустойчивости

Consul используется как:

  • распределенное KV-хранилище для Patroni:
    • хранение информации о текущем лидере;
    • locks для операций failover;
  • сервис-дискавери:
    • запись сервиса "postgresql-primary" с адресом текущего primary.

Ключевые моменты:

  • Кластер Consul должен быть сам отказоустойчив (3–5 серверов).
  • Правильные ACL и политика безопасности (доступ к KV ограничен).
  1. Подключение приложений: стабильная точка входа

Чтобы приложениям не приходилось знать, кто сейчас primary, обычно используется:

  • HAProxy / pgbouncer, который:
    • смотрит на Consul/Patroni API,
    • проксирует трафик на текущий primary;
  • либо Consul DNS/Service Discovery для получения актуального адреса primary.

Пример (очень упрощенный) SQL-сценария работы:

  • Приложение всегда подключается к:
    • pg-primary.internal:5432 (HAProxy/pooler),
    • тот на основе Patroni/Consul знает, куда реально слать запросы.
  1. Операционка и администрирование

Критичные моменты, которые важно уметь и показать на интервью:

  • Бэкапы и восстановление:
    • Полные бэкапы + WAL (например, wal-g/pgBackRest).
    • Проверка восстановления (disaster recovery rehearsal).
  • Обновления:
    • Патчи PostgreSQL: rolling-перезапуск нод под управлением Patroni.
    • Мажорные апгрейды:
      • pg_upgrade / logical replication / новый кластер + переключение.
  • Наблюдаемость:
    • Метрики (pg_exporter, Patroni metrics, Consul health).
    • Алёрты:
      • replication lag,
      • недоступность ноды,
      • отказ попыток failover,
      • рост диска, autovacuum, bloat.
  1. Пример интеграции с Go-сервисом

В Go-сервисе важно:

  • использовать connection pooling (database/sql + pgx),
  • уметь переживать failover без падения всего приложения.

Упрощенный пример:

import (
"context"
"database/sql"
"time"

_ "github.com/jackc/pgx/v5/stdlib"
)

func NewDB() (*sql.DB, error) {
dsn := "postgres://app_user:pass@pg-primary.internal:5432/app_db?sslmode=disable"

db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}

db.SetMaxOpenConns(50)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, err
}

return db, nil
}

Таким образом, корректный ответ должен показывать понимание не только набора технологий (Patroni, Consul, Ansible), но и:

  • как именно обеспечивается автоматический failover;
  • как приложения находят актуальный primary;
  • как решаются вопросы бэкапов, обновлений и мониторинга;
  • почему используется bare-metal/VM без контейнеров для PostgreSQL (прозрачное управление storage, предсказуемая производительность, упрощенный доступ к ресурсам).

Вопрос 4. Как настраивается пул соединений и как выглядит процесс добавления новой базы данных в конфигурацию?

Таймкод: 00:10:04

Ответ собеседника: неполный. Упомянул использование отдельного пулера и конфигурацию через файлы, а также Git-центричный процесс (изменения сначала на PostgreSQL, затем в Git, проверка на тесте и деплой на прод), но не привел конкретный тип пулера и ключевые параметры настройки.

Правильный ответ:

Настройка пулера соединений и процесс добавления новой базы данных должны быть формализованы, повторяемы и безопасны. Ниже пример того, как это обычно организуется в зрелой инфраструктуре, с акцентом на PgBouncer/HAProxy (как наиболее частый стек) и GitOps-подход.

Основные цели пулера:

  • уменьшить количество прямых соединений к PostgreSQL;
  • стабилизировать нагрузку на кластер;
  • обеспечить единый точку входа для приложений, в том числе при failover;
  • дать гибкость в маршрутизации (разделение read/write, разные пулы для разных сервисов).
  1. Выбор и роль пулера

Чаще всего используются:

  • PgBouncer:
    • легковесный connection pooler;
    • держит постоянные соединения к PostgreSQL и переиспользует их между множеством краткоживущих клиентских коннектов;
    • режимы:
      • session pooling — изолированный сеанс (по умолчанию);
      • transaction pooling — максимальная эффективность для stateless-запросов;
      • statement pooling — редко, специфичный вариант.
  • HAProxy (или Envoy) в дополнение:
    • маршрутизирует к актуальному primary/replica (особенно в связке с Patroni/Consul);
    • может использоваться вместе с PgBouncer: HAProxy -> PgBouncer -> PostgreSQL.

Важно:

  • Для большинства backend-сервисов на Go с нормальной практикой работы с БД хорошо подходит PgBouncer в режиме transaction pooling или session pooling — выбор зависит от использования session state (temp tables, prepared statements, session GUC’и, читай ниже).
  1. Пример конфигурации PgBouncer

Конфигурация обычно хранится в Git и раскатывается через Ansible/Helm/k8s-манифесты. Типовая структура:

  • pgbouncer.ini — основные настройки;
  • userlist.txt — пользователи и креды (либо интеграция с auth_file/ldap/etc);
  • databases-секция — сопоставление логических имен с реальными серверами/базами.

Пример pgbouncer.ini (упрощенный):

[databases]
app_db = host=pg-primary.internal port=5432 dbname=app_db pool_size=50
billing_db = host=pg-primary.internal port=5432 dbname=billing_db pool_size=30
reports_readonly = host=pg-replica.internal port=5432 dbname=reports pool_size=20

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

pool_mode = transaction
server_tls_sslmode = disable
max_client_conn = 2000
default_pool_size = 50
min_pool_size = 5
reserve_pool_size = 20
reserve_pool_timeout = 5.0
server_idle_timeout = 600
server_lifetime = 3600
server_connect_timeout = 5.0
query_timeout = 30.0
idle_transaction_timeout = 60.0
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1

Ключевые моменты по настройке:

  • pool_mode:
    • transaction:
      • лучший вариант для типичных HTTP-запросов, когда приложение не хранит состояние в сессии БД;
      • нельзя использовать session-specific state: temp tables, alter role set, долгоживущие транзакции.
    • session:
      • если используются сложные сценарии (CURSOR, temp tables, session GUC).
  • max_client_conn:
    • ограничивает входящие соединения от приложений, предотвращая DDoS БД своими же сервисами.
  • default_pool_size / pool_size:
    • ограничивают количество одновременных соединений к PostgreSQL от пулера;
    • суммарно по всем базам/пользователям не должно превышать max_connections PostgreSQL с запасом.
  • timeouts:
    • обязательны для избежания залипания соединений и висящих транзакций.
  1. Интеграция с отказоустойчивостью

В связке с Patroni/Consul:

Варианты:

  • PgBouncer напрямую смотрит на виртуальный адрес/балансировщик (HAProxy), который уже знает актуальный primary.
  • Или перезапускается/переконфигурируется при смене primary (хуки Patroni/Consul), но это менее желательно.

Чаще:

  • HAProxy front:
    • следит за Patroni REST API / Consul, определяет, кто primary;
    • проксирует на соответствующий PostgreSQL.
  • PgBouncer:
    • подключается к HAProxy, а не к конкретной ноде:
    • host=pg-writer.internal (HAProxy VIP/Service), PgBouncer "видит" стабильную точку, failover прозрачен.
  1. Процесс добавления новой базы данных (GitOps-подход)

Цель — чтобы добавление новой БД или нового логического подключения было:

  • контролируемым,
  • воспроизводимым,
  • проверенным на тесте,
  • без ручного "правлю конфиг на проде по ssh".

Типичный поток:

  1. Создание базы на PostgreSQL:

    • через миграцию/скрипт/Ansible-роль:
    CREATE DATABASE app_new_db OWNER app_user;
    GRANT ALL PRIVILEGES ON DATABASE app_new_db TO app_user;
  2. Добавление конфигурации пулера в Git:

    • в репозитории инфраструктуры (например, infra-config):
    • правим шаблон pgbouncer.ini (или values.yaml для Helm-чарта):
    [databases]
    app_new_db = host=pg-primary.internal port=5432 dbname=app_new_db pool_size=40

    или в values.yaml:

    pgbouncer:
    databases:
    app_new_db:
    host: pg-primary.internal
    port: 5432
    dbname: app_new_db
    pool_size: 40
  3. Валидация и тест:

    • CI:
      • линтинг конфигураций;
      • проверка синтаксиса pgbouncer.ini / шаблонов;
      • возможно — тестовый деплой в stage-окружение.
    • В stage:
      • деплой новых конфигов через Ansible/Helm.
      • smoke-тест: Go-сервис подключается к новой БД через пулер и выполняет тестовые запросы.
  4. Применение на прод:

    • Merge PR -> GitLab CI/CD -> Ansible/Helm rollout.
    • Пулер перезагружается мягко:
      • для PgBouncer: RELOAD (без дропа коннектов) или аккуратный рестарт с удержанием активных коннекций.
    • Мониторинг:
      • метрики по соединениям к новой БД;
      • ошибки аутентификации, connection refused, timeouts.
  5. Использование со стороны приложений:

    • Приложение не ходит напрямую к PostgreSQL, а использует пулер:
    • пример DSN в Go:
    dsn := "postgres://app_user:pass@pgbouncer.internal:6432/app_new_db?sslmode=disable"
    db, err := sql.Open("pgx", dsn)
    • при этом maxOpenConns в приложении настраивается с учетом использования пулера:
      • часто: maxOpenConns <= pool_size на пулере для этого db/user.
  1. Важные инженерные нюансы
  • Согласованность лимитов:
    • PostgreSQL max_connections > суммарный server-пул PgBouncer;
    • PgBouncer max_client_conn > суммарное число клиентских коннектов, но с разумным запасом.
  • Правильный режим pooling:
    • если команда использует transaction pooling, то:
      • избегать session-level state;
      • не использовать session prepared statements (или включить соответствующую поддержку).
  • Безопасность:
    • креды не хардкодятся в конфиге в открытом виде:
      • используются vault/secrets management;
      • CI подставляет секреты при деплое.
  • Наблюдаемость:
    • метрики:
      • количество активных коннектов;
      • utilization пула;
      • rejected/failed connections;
    • логи ошибок пулера интегрируются в централизованное логирование.

Такой ответ показывает понимание не только того, что "есть пулер и есть Git", но и:

  • какую роль пулер играет в архитектуре;
  • как он связан с отказоустойчивостью и failover;
  • как формализован процесс добавления новой БД через GitOps;
  • какие параметры и практики критичны для стабильной работы Go-сервисов с PostgreSQL.

Вопрос 5. Где хранится конфигурационный файл пулера соединений и как он используется?

Таймкод: 00:11:20

Ответ собеседника: неполный. Упомянул, что конфигурация пулера находится в /etc/pgbouncer и называется pgbouncer.conf (по аналогии), но не уточнил точную структуру, связь с Git/CI/CD, процесс обновления и использования.

Правильный ответ:

Важен не только физический путь к конфигу пулера, но и то, как он управляется: через GitOps/инфраструктуру как код, как раскатывается на хосты, как безопасно обновляется и как его использует приложение.

На примере PgBouncer типичная организация выглядит так.

  1. Логическое хранение: Git как источник истины
  • Базовый принцип:
    • Конфигурация пулера не редактируется руками на проде.
    • Источником истины является репозиторий инфраструктуры:
      • например: infra/pgbouncer/pgbouncer.ini.j2 (Ansible шаблон),
      • или charts/pgbouncer/values.yaml (Helm),
      • либо k8s-манифесты с ConfigMap/Secret.
  • Все изменения:
    • идут через pull/merge-request,
    • проходят code review, CI-валидацию,
    • применяются на тестовом окружении,
    • затем автоматически раскатываются на прод.

Это гарантирует воспроизводимость, аудит и быстрый rollback.

  1. Физическое размещение на хосте

Для bare-metal/VM-инсталляций PgBouncer обычно:

  • Основной конфигурационный файл:
    • /etc/pgbouncer/pgbouncer.ini
  • Файл с пользователями:
    • /etc/pgbouncer/userlist.txt
  • Логи:
    • /var/log/pgbouncer/pgbouncer.log (или вывод в syslog/journal).

Пример структуры:

  • /etc/pgbouncer/
    • pgbouncer.ini
    • userlist.txt
    • databases.ini (иногда выносят базы в отдельный include-файл)

Часто используется include:

[databases]
%include /etc/pgbouncer/databases.ini

Фактический путь не критичен, важны:

  • единообразие для всех инсталляций;
  • автоматизированное управление (Ansible, Salt, Chef, Puppet, shell+CI).
  1. Как конфигурация попадает на хост

Сценарий с Ansible:

  • В Git хранятся шаблоны:
    • pgbouncer.ini.j2, userlist.txt.j2, databases.yml.
  • Playbook:
    • рендерит шаблоны с нужными переменными (адреса primary/replica, список баз, параметры пулов),
    • копирует на хосты в /etc/pgbouncer/,
    • выполняет проверку и безопасный reload/restart.

Условный пример:

- name: Deploy pgbouncer config
template:
src: pgbouncer.ini.j2
dest: /etc/pgbouncer/pgbouncer.ini
owner: pgbouncer
group: pgbouncer
mode: '0640'
notify: Reload pgbouncer

- name: Deploy userlist
template:
src: userlist.txt.j2
dest: /etc/pgbouncer/userlist.txt
owner: pgbouncer
group: pgbouncer
mode: '0640'

Handler для мягкого применения изменений:

- name: Reload pgbouncer
service:
name: pgbouncer
state: reloaded

Таким образом:

  • изменения в Git → CI проверил → Ansible применил → PgBouncer reload;
  • активные соединения, как правило, не рвутся (в отличие от жесткого рестарта).
  1. Как конфигурация используется приложениями

Приложения не работают с конфигом напрямую — они используют его результат:

  • PgBouncer поднимается:
    • читает /etc/pgbouncer/pgbouncer.ini,
    • открывает порт (например, 6432),
    • создает пулы под указанные базы.
  • Go-сервисы подключаются к пулеру:
    • host=pgbouncer.internal / port=6432 / dbname=имя_логической_базы
    • вместо прямого подключения к PostgreSQL.

Пример DSN в Go:

dsn := "postgres://app_user:pass@pgbouncer.internal:6432/app_db?sslmode=disable"
db, err := sql.Open("pgx", dsn)
if err != nil {
// обработка ошибки
}

Если конфигурация организована правильно:

  • приложение всегда ходит в стабильную точку входа (адрес пулера),
  • детали инфраструктуры (primary, replica, failover, реальные хосты PostgreSQL) скрыты внутри конфигурации пулера и балансировщиков,
  • изменения маршрутизации, добавление новых БД и т.п. делаются централизованно через обновление конфигов в Git.
  1. Ключевые инженерные моменты
  • Конфиг пулера:
    • управляется как код, версионируется, ревьюится;
    • на прод не меняется вручную.
  • Секреты:
    • пароли не хранятся в открытом виде в публичном Git;
    • используются Vault/secret management, шифрование или приватный репозиторий с ограниченным доступом.
  • Процесс обновления:
    • только через CI/CD + Ansible/Helm;
    • reload вместо рестарта, если возможно.

Такой ответ показывает понимание полного жизненного цикла конфигурации пулера: от Git-репозитория до используемого порта, через который ходят Go-сервисы, и как это помогает обеспечить стабильность и управляемость инфраструктуры.

Вопрос 6. В каком формате должен быть пароль для работы пулера подключений с базой данных и что делать при несоответствии формата?

Таймкод: 00:11:49

Ответ собеседника: неправильный. Упомянул MD5 и предложил просто скопировать "актуальный хэш" из базы в конфиг пулера, фактически игнорируя требования к формату/типу хэша на стороне PostgreSQL и пулера (например, PgBouncer), а также неправильно согласился менять хэш в базе без корректного анализа и выравнивания настроек аутентификации.

Правильный ответ:

Для пулеров соединений, работающих с PostgreSQL (чаще всего PgBouncer), принципиально важно:

  • какой метод аутентификации настроен в самом пулере;
  • в каком виде он ожидает пароль (plain/md5/scram и т.п.);
  • соответствует ли это формату пароля и настройкам аутентификации PostgreSQL.

Нельзя просто "рандомно скопировать хэш" из базы — нужно обеспечить согласованную конфигурацию по всей цепочке.

Разберём ключевые моменты.

Аутентификация в контексте PgBouncer

PgBouncer поддерживает несколько auth_type, среди них:

  • plain — пароль в виде открытого текста;
  • md5 — PostgreSQL-совместимый MD5-хэш;
  • scram-sha-256 (в новых версиях) при соответствующей сборке и настройке;
  • hba / pam / cert и др. (зависят от конфигурации).

Для классической конфигурации:

  • auth_type = md5
  • auth_file = /etc/pgbouncer/userlist.txt

PgBouncer ожидает пароли в PostgreSQL-стиле MD5:

  • формат: md5 + hex(MD5(password + username))
  • пример: md5e10adc3949ba59abbe56e057f20f883e

Важно:

  • Это не MD5 от просто пароля,
  • а MD5 от (пароль + имя_пользователя), закодированный в hex и с префиксом "md5".

Пример генерации MD5-пароля

Пусть:

  • user = app_user
  • password = S3cr3t

Тогда Pg-совместимый хэш:

md5_hex = MD5("S3cr3t" + "app_user")

Результат (пример):

md5e5f0d7f14f0b0e98c024aaa0dae68e8a

В userlist.txt запись будет:

"app_user" "md5e5f0d7f14f0b0e98c024aaa0dae68e8a"

или, если используется plain:

"app_user" "S3cr3t"

Как правильно согласовать форматы

Ключевой принцип:

  • Формат пароля и метод аутентификации должны быть согласованы:
    • на стороне PostgreSQL (password_encryption, pg_hba.conf),
    • на стороне PgBouncer (auth_type, auth_file),
    • на стороне клиента (Go-приложение), если оно ходит напрямую.

Типичные сценарии:

  1. PgBouncer с auth_type=md5

    • В userlist.txt должны быть PostgreSQL-style md5-хэши.
    • В PostgreSQL для соответствующего пользователя:
      • может быть пароль в md5 или scram, но:
        • при классической схеме аутентификация фактически завершается на PgBouncer, который прокидывает MD5-челлендж.
    • Правильное действие при "несоответствии":
      • не "перезаписывать хэш в базе, как попало",
      • а:
        • убедиться, что:
          • auth_type=md5 в PgBouncer;
          • userlist.txt содержит корректный md5-хэш формата md5 + MD5(password + username);
          • пароль, который знает PgBouncer, совпадает с тем, что настроен для этого пользователя в PostgreSQL.
        • при изменении пароля:
          • обновить пароль пользователя в PostgreSQL;
          • сгенерировать новый md5-хэш и обновить userlist.txt через Git/CI/CD;
          • перезагрузить PgBouncer (RELOAD).
  2. PostgreSQL с SCRAM-SHA-256, PgBouncer без поддержки SCRAM

    • Нельзя просто скопировать SCRAM-хэш в PgBouncer:
      • PgBouncer старых версий не умеет его обрабатывать.
    • Варианты решений:
      • включить поддержку SCRAM в PgBouncer (обновление версии/сборки + соответствующая конфигурация);
      • либо для пользователей, которые аутентифицируются через PgBouncer:
        • использовать md5-пароли в PostgreSQL (password_encryption='md5' для конкретных учёток или временно при установке пароля);
        • хранить md5-хэши в userlist.txt.
    • Недопустимо:
      • менять хэш "наугад" только в базе или только в PgBouncer — это ломает аутентификацию.
  3. plain-пароли (auth_type=plain)

    • PgBouncer хранит пароли в открытом виде.
    • Можно использовать, но:
      • только в сочетании с безопасным контуром (TLS, закрытая сеть, vault/secret management);
      • лучше избегать на проде, если есть альтернатива.

Что делать при несоответствии формата

Если пулер "не принимает" пароль, и вы видите расхождение форматов:

  1. Проверить конфиг пулера:

    • auth_type (md5/plain/scram/…);
    • путь к auth_file / userlist.txt;
    • формат записей (кавычки, префикс md5, отсутствие лишних пробелов).
  2. Проверить конфигурацию PostgreSQL:

    • как создан пользователь:
      • пароль задан как plain/md5/scram;
    • какие методы аутентификации в pg_hba.conf:
      • md5 / scram-sha-256 / trust / gssapi и т.д.
  3. Выравнивание (корректный сценарий):

    • выбрать целевой метод:
      • например: PgBouncer auth_type=md5.
    • Убедиться, что:
      • у пользователя есть пароль, совместимый с выбранным методом;
      • в auth_file пулера записан корректный хэш (md5 + MD5(password+username));
    • Обновить конфигурацию:
      • через Git → CI → деплой;
      • выполнить мягкий reload PgBouncer;
      • протестировать соединение.
  4. Чего делать нельзя:

    • "Править хэш в базе" без понимания:
      • PostgreSQL сам управляет форматом хранения пароля;
      • прямое редактирование pg_authid (через UPDATE) — грубая ошибка.
    • Смешивать SCRAM и md5 без понимания, умеет ли пулер выбранный формат.
    • Держать в пулере пароль, не совпадающий с реальным паролем пользователя в PostgreSQL.

Краткий пример проверки с Go

Если PgBouncer настроен корректно и пароль согласован:

dsn := "postgres://app_user:S3cr3t@pgbouncer.internal:6432/app_db?sslmode=disable"
db, err := sql.Open("pgx", dsn)
if err != nil {
panic(err)
}
if err := db.Ping(); err != nil {
panic(err) // если тут ошибка, проверяем auth_type/формат пароля/логи pgbouncer
}

Итоговая мысль:

  • Формат пароля — не произвольный "MD5", а конкретный PostgreSQL-совместимый формат (md5-password или SCRAM) в связке с настроенным auth_type.
  • При несоответствии нужно выровнять:
    • настройки PgBouncer,
    • способ хранения пароля в PostgreSQL,
    • содержимое auth_file,
    • а не хаотично "менять хэш" в одной из точек.

Вопрос 7. Какова зона ответственности при работе с отказоустойчивым кластером PostgreSQL: кто сопровождает и кто отвечает за инфраструктуру?

Таймкод: 00:12:36

Ответ собеседника: неполный. Указал на участие DBA и своё участие в инфраструктуре и Ansible-ролях, но не очертил чётко границы ответственности и глубину вовлечения.

Правильный ответ:

В контуре отказоустойчивого кластера PostgreSQL зона ответственности должна быть разделена так, чтобы:

  • эксплуатация была предсказуемой,
  • изменения были контролируемыми,
  • решения по схеме и параметрам СУБД принимались осознанно,
  • разработчики приложений понимали, "что можно", а "что нельзя" с точки зрения нагрузки и паттернов работы с БД.

Рациональная модель распределения выглядит следующим образом.

  1. Ответственность за инфраструктуру (инфра/кластерный слой)

Эта зона включает все, что связано с:

  • жизненным циклом нод и сервисов,
  • автоматизацией развертывания,
  • отказоустойчивостью и self-healing.

Типичные обязанности:

  • Проектирование архитектуры кластера PostgreSQL:
    • выбор топологии: primary + несколько реплик;
    • размещение по зонам доступности, сетевой топологии, дисковой подсистеме.
  • Развертывание и поддержка:
    • Ansible-ролями/terraform/helm-манифестами разворачиваются:
      • PostgreSQL;
      • Patroni (или другой кластер-менеджер);
      • Consul/Etcd/ZooKeeper;
      • PgBouncer/HAProxy;
      • мониторинг (Prometheus exporters, логирование, алёрты).
  • Инфраструктура как код:
    • все конфиги Patroni, PgBouncer, HAProxy, systemd, users, firewall и т.п. находятся в Git;
    • изменения идут через code review, CI/CD, с выкладкой сначала на stage.
  • Обеспечение отказоустойчивости:
    • корректная настройка quorum и election таймингов Patroni;
    • безопасный автоматический failover без split-brain;
    • тестирование сценариев отказа:
      • падение primary;
      • потеря ноды;
      • деградация сети;
      • восстановление реплик.
  • Обеспечение наблюдаемости:
    • метрики по состоянию кластера, lag репликации, ошибкам, connection pool;
    • дашборды и алёрты, понятные SRE/infra-команде и DBA.

Роль здесь — отвечать за то, чтобы кластер как система "жил", автоматически восстанавливался и был управляемым. Любое изменение (например, новый узел реплики, обновление версии PostgreSQL, настройка Patroni, конфиги пулера/балансировщика) — через инфраструктурный пайплайн.

  1. Ответственность DBA (эксплуатация и логика внутри PostgreSQL)

DBA фокусируется не на "поднять сервис", а на:

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

Ключевые обязанности DBA:

  • Настройка PostgreSQL:
    • тонкая настройка параметров (shared_buffers, work_mem, effective_cache_size, autovacuum, wal_*).
    • выбор и настройка режима репликации (sync/async, synchronous_standby_names).
  • Схемы и запросы:
    • ревью и оптимизация схем, индексных стратегий;
    • помощь командам разработки в написании эффективных запросов;
    • анализ планов выполнения, поиск узких мест.
  • Управление бэкапами и восстановлением:
    • выбор инструмента (pgBackRest, wal-g);
    • проверка, что бэкапы не только делаются, но и восстанавливаются;
    • разработка DR-сценариев.
  • Участие в релизах:
    • сопровождение мажорных апгрейдов PostgreSQL;
    • оценка риска изменений (например, массовые миграции схем, тяжелые миграции).
  1. Ответственность инженера, работающего с инфраструктурой PostgreSQL (как в ответе на интервью)

Компетентный инженер в этой зоне должен:

  • Не перекладывать всё на DBA, а совместно с ними:
    • проектировать инфраструктурную оболочку кластера;
    • гарантировать, что инфраструктура не противоречит требованиям СУБД.
  • Конкретные зоны ответственности могут включать:
    • Разработка и поддержка Ansible-ролей/Helm-чартов:
      • развертывание PostgreSQL-нод;
      • установка Patroni, Consul, PgBouncer, HAProxy;
      • единые шаблоны конфигураций, возможность быстро поднять новый кластер.
    • Встроенные механизмы health-check и failover:
      • интеграция Patroni с балансировщиками (HAProxy) и сервис-дискавери;
      • корректные лейблы/сервисы, чтобы приложения всегда попадали на актуальный primary.
    • Интеграция с CI/CD:
      • автоматическое применение изменений конфигов;
      • безопасные window’ы для конфигурационных изменении;
      • проверки на stage перед rollout-ом в prod.
    • Инструменты диагностики:
      • доступные дашборды для latency, throughput, ошибок, lag, bloat, autovacuum activity.

Если говорить как о корректном личном ответе, он должен звучать так:

  • Я отвечал за:
    • автоматизацию развертывания кластера PostgreSQL (Ansible-рокли/скрипты);
    • конфигурацию и эксплуатацию Patroni, Consul, PgBouncer/балансировщиков;
    • интеграцию этого стека с CI/CD;
    • настройку мониторинга и алёртинга по состоянию кластера;
    • тестирование сценариев отказа и регламенты по восстановлению.
  • DBA отвечали за:
    • схемы данных, индексы, сложные запросы, performance-тюнинг;
    • стратегию бэкапов, контроль консистентности данных;
    • рекомендации по параметрам PostgreSQL, которые мы дальше оборачивали в инфраструктурные роли.
  • Взаимодействие:
    • любые изменения в параметрах PostgreSQL или в архитектуре кластера согласовывались с DBA;
    • любые инфраструктурные изменения проходили через меня/команду инфраструктуры, с обязательным тестированием.
  1. Почему важна четкая граница ответственности

Такая модель:

  • исключает "ручные правки" на проде;
  • дает прозрачный ownership:
    • если падает Patroni/балансировщик — это зона инфраструктуры;
    • если запросы медленные, индексы неправильные — это зона DBA/разработки;
  • позволяет быстро эволюционировать:
    • смена версии PostgreSQL,
    • изменение топологии,
    • добавление новых кластеров, шардов, read-replicas.

Таким образом, корректный ответ должен показать:

  • понимание того, что кластер PostgreSQL — это совместная зона ответственности;
  • ясное разделение:
    • инфраструктура (кластерный слой, автоматизация, HA, мониторинг),
    • СУБД-слой (схемы, запросы, тюнинг, бэкапы);
  • собственную роль в разработке, поддержке и эволюции инфраструктуры вокруг отказоустойчивого кластера.

Вопрос 8. Что вы делали на проекте в Neoflex с точки зрения инфраструктуры и какие технологии использовали?

Таймкод: 00:13:02

Ответ собеседника: правильный. Описал сопровождение разработки: поднимал виртуальные машины в Proxmox, устанавливал ОС, разворачивал сервисы, автоматизировал через Ansible, использовал Terraform для управления Proxmox, применял Docker и Docker Compose.

Правильный ответ:

На таком проекте важно показать не просто "поднимал VM и ставил Docker", а системный подход к инфраструктуре как коду, средам разработки и эксплуатации сервисов, а также к интеграции этого всего с процессом разработки на Go.

Ключевые направления работ могли выглядеть так.

Инфраструктура на базе Proxmox + Terraform

  • Использование Proxmox в качестве платформы виртуализации:
    • Создание и управление виртуальными машинами под различные среды:
      • dev, stage, demo, pre-prod, prod (если проект доходит до боевого контура).
    • Стандартизация базовых образов:
      • минимальные образы Linux (часто Debian/Ubuntu), подготовленные для автоматической конфигурации.
  • Использование Terraform для управления Proxmox:
    • Описание инфраструктуры в HCL:

      • какие VM нужны, с какими ресурсами (CPU/RAM/Disk/Network),
      • привязка к конкретным нодам/стореджам/сетям.
    • Пример (упрощённый) Terraform-конфигурации для Proxmox:

      provider "proxmox" {
      pm_api_url = "https://proxmox.local:8006/api2/json"
      pm_user = "terraform@pve"
      pm_password = var.pm_password
      pm_tls_insecure = true
      }

      resource "proxmox_vm_qemu" "app_vm" {
      name = "app-dev-01"
      target_node = "pve-node1"
      clone = "base-template-ubuntu"
      cores = 4
      memory = 8192

      network {
      model = "virtio"
      bridge = "vmbr0"
      }

      disk {
      size = "50G"
      type = "scsi"
      storage = "local-lvm"
      }

      ssh_user = "ubuntu"
      os_type = "cloud-init"
      ipconfig0 = "ip=10.0.10.11/24,gw=10.0.10.1"
      }
    • Выгоды:

      • воспроизводимость окружений;
      • возможность поднять целый стенд "по описанию" без ручных действий.

Автоматизация конфигурации через Ansible

  • После создания VM — полная конфигурация через Ansible:

    • установка необходимых пакетов (Docker, Docker Compose, агенты мониторинга, логирования);
    • настройка пользователей, ssh-доступа, firewall;
    • деплой сервисов и зависимостей (Nginx, PostgreSQL, Redis, message broker, внутренние сервисы).
  • Разделение ролей:

    • base-os: системные настройки, security hardening;
    • docker-host: установка Docker и настройка daemon;
    • app-stack: деплой конкретных сервисов проекта.
  • Пример (упрощённый) Ansible playbook:

    - hosts: app_vms
    become: yes
    roles:
    - role: base-os
    - role: docker
    - role: monitoring-agent
    - role: app-stack
  • Бонус:

    • Любая новая VM после запуска автоматически приводится к стандартному состоянию;
    • минимум "ручных" шагов, меньше дрейфа конфигураций.

Контейнеризация: Docker и Docker Compose

  • Использование Docker для упаковки приложений:

    • Go-сервисы, вспомогательные утилиты, тестовые стенды.
    • Multi-stage-сборка Go-приложений:
      • уменьшение size образов;
      • быстрые деплои.

    Пример Dockerfile для Go-сервиса:

    FROM golang:1.22 AS builder
    WORKDIR /app
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o service ./cmd/service

    FROM gcr.io/distroless/base-debian12
    WORKDIR /app
    COPY --from=builder /app/service .
    USER nonroot:nonroot
    ENTRYPOINT ["/app/service"]
  • Docker Compose для сборки окружений:

    • локальная разработка и интеграционные стенды:
      • сервисы приложения;
      • PostgreSQL/Redis/Kafka/NATS;
      • вспомогательные сервисы (pgadmin, jaeger и т.п.).

    Пример docker-compose.yml (упрощённый):

    version: "3.9"

    services:
    app:
    image: registry.local/app:latest
    depends_on:
    - db
    environment:
    - DB_DSN=postgres://app:pass@db:5432/app?sslmode=disable
    ports:
    - "8080:8080"

    db:
    image: postgres:15
    environment:
    POSTGRES_DB: app
    POSTGRES_USER: app
    POSTGRES_PASSWORD: pass
    volumes:
    - db_data:/var/lib/postgresql/data

    volumes:
    db_data:

Роль в процессе разработки и эксплуатации

Правильный акцент в ответе:

  • Обеспечение среды для команды разработки:
    • Быстрое поднятие окружений под фичи/тесты.
    • Единообразные dev/stage окружения, максимально близкие к прод-стеку.
  • Интеграция с CI/CD:
    • Terraform + Ansible как часть пайплайнов:
      • создание/обновление VM;
      • автоматический деплой сервисов.
    • Docker-образы собираются в CI:
      • go test / линтеры;
      • сборка;
      • загрузка в приватный registry;
      • деплой на нужные VM через Ansible или GitLab CI runners.

Инженерные практики и ответственность

  • Инфраструктура как код:
    • Terraform (Proxmox), Ansible (конфигурация), Docker (упаковка).
  • Повторяемость:
    • Любое окружение можно пересоздать из Git-репозитория.
  • Безопасность и контроль:
    • нет ручного "накликивания" инфраструктуры;
    • любые изменения проходят через MR и ревью.
  • Поддержка команд разработки:
    • предоставление готовых шаблонов docker-compose/конфигов;
    • помощь в интеграции Go-сервисов с БД, кэшем, очередями в стандартной инфраструктуре.

Такой ответ демонстрирует:

  • владение полным циклом: от гипервизора и VM до контейнеризации приложений;
  • умение строить воспроизводимую инфраструктуру вокруг Go-сервисов;
  • практическое применение Terraform, Ansible, Docker и Docker Compose в реальном проекте, а не на уровне "знаю по документации".

Вопрос 9. Как использовался Terraform в инфраструктуре на базе Proxmox?

Таймкод: 00:13:49

Ответ собеседника: правильный. Сообщил, что Terraform применялся для управления виртуальными машинами Proxmox и автоматизации их создания и конфигурации.

Правильный ответ:

Использование Terraform с Proxmox должно демонстрировать инфраструктуру как код, воспроизводимость окружений и минимизацию ручных действий. В зрелой конфигурации Terraform не просто "создаёт VM", а:

  • описывает все окружения декларативно;
  • стандартизирует конфигурации;
  • интегрируется с Ansible/конфигурационным менеджментом и CI/CD;
  • позволяет безопасно вносить изменения, видеть diff и делать rollbacks.

Ключевые аспекты использования Terraform с Proxmox.

  1. Terraform как источник истины для виртуальных машин
  • Все виртуальные машины описываются в Terraform-конфигурации (HCL):
    • имя VM;
    • ресурсы (vCPU, RAM, диск);
    • сетевые настройки;
    • базовый template/образ;
    • теги, которые далее используют Ansible/мониторинг.
  • Нет ручного "накликивания" VM в Proxmox WebUI для prod/stage:
    • любые изменения в составе или параметрах VM проходят через Git → MR → review → terraform plan/apply.

Пример описания VM (упрощенно):

provider "proxmox" {
pm_api_url = "https://proxmox.local:8006/api2/json"
pm_user = "terraform@pve"
pm_password = var.pm_password
pm_tls_insecure = true
}

resource "proxmox_vm_qemu" "app_vm" {
name = "app-stage-01"
target_node = "pve-node1"
clone = "ubuntu-22.04-cloudinit-template"

cores = 4
memory = 8192

disk {
type = "scsi"
storage = "local-lvm"
size = "50G"
}

network {
model = "virtio"
bridge = "vmbr0"
}

ipconfig0 = "ip=10.0.20.11/24,gw=10.0.20.1"
ssh_user = "ubuntu"
}

Преимущества:

  • воспроизводимость (одинаковые машины для dev/stage/prod с параметризацией);
  • возможность массово масштабировать или пересоздавать инфраструктуру по описанию.
  1. Стандартизация окружений через модули
  • Создаются Terraform-модули для типовых ролей:
    • модуль для app-сервера;
    • модуль для базы данных;
    • модуль для utility/monitoring нод.
  • Для каждого окружения:
    • используется один и тот же модуль с разными переменными.

Пример модуля вызова:

module "app_stage" {
source = "./modules/proxmox-app-vm"
name_prefix = "app-stage"
count = 3
cpu = 4
memory = 8192
disk_size = "50G"
network_cidr = "10.0.20.0/24"
}

Это уменьшает ручные ошибки и дрейф конфигураций.

  1. Связка Terraform + Ansible

Terraform отвечает за:

  • создание VM,
  • присвоение IP, ресурсов, метаданных.

Ansible отвечает за:

  • установку ПО (Docker, агенты, runtime),
  • деплой сервисов, конфигураций.

Типичный pipeline:

  • terraform apply:
    • создал/обновил набор VM;
  • Terraform выводит inventory (outputs) для Ansible:
    • IP-адреса, хостнеймы, теги ролей;
  • Ansible запускается:
    • конфигурирует созданные машины.

Пример outputs, которые затем используются Ansible:

output "app_stage_ips" {
value = [for vm in proxmox_vm_qemu.app_vm : vm.ssh_host]
}
  1. GitOps и CI/CD для инфраструктуры
  • Terraform-конфиги хранятся в отдельном репозитории (например, infra-proxmox).
  • Процесс изменений:
    • разработчик инфраструктуры меняет HCL;
    • открывает MR;
    • CI выполняет terraform fmt, validate, plan;
    • план ревьюится (явно видно, какие VM будут созданы/изменены/удалены);
    • после approve — terraform apply из CI/CD с ограниченными credentials.
  • Это обеспечивает:
    • прозрачность и аудит всех изменений;
    • отсутствие "серых" VM, созданных вручную.
  1. Использование Terraform для жизненного цикла окружений

Terraform позволяет:

  • поднимать временные стенды под фичи:
    • отдельный workspace или отдельные prefix’ы;
    • после завершения задачи окружение уничтожается terraform destroy.
  • масштабировать:
    • изменение count/ресурсов → terraform apply;
  • мигрировать:
    • перемещение VM на другие ноды/сторедж;
    • изменение параметров без ручных действий.
  1. Интеграция с Go-командой и сервисами

Косвенно это влияет на разработку на Go:

  • dev/stage окружения стандартизированы:
    • Go-сервисы всегда попадают в предсказуемую инфраструктуру;
    • одинаковые DNS/сети/порты, что уменьшает "работает у меня локально, не работает на стенде".
  • Легко поднимать полноценные end-to-end окружения:
    • Terraform создаёт VM,
    • Ansible разворачивает базу, кэш, брокеры, сервисы,
    • разработчики и QA тестируют реальное поведение.

Такой ответ демонстрирует:

  • осознанное использование Terraform не как игрушки, а как ключевого инструмента управления инфраструктурой на Proxmox;
  • понимание практик IaC, GitOps, модульности и интеграции с Ansible и CI/CD;
  • влияние инфраструктурных решений на качество и предсказуемость среды для сервисов (в том числе написанных на Go).

Вопрос 10. Как автоматизировалось развёртывание Zabbix и почему первоначальный подход оказался неудачным?

Таймкод: 00:15:00

Ответ собеседника: правильный. Сначала использовал Python-скрипты и Zabbix API для автоматизации настройки, затем перешёл к декларативной конфигурации через Ansible-модуль Zabbix, как более поддерживаемый и читаемый подход. Показал умение переоценивать решения и выбирать более подходящий инструмент.

Правильный ответ:

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

Основные моменты.

Императивный подход: Python + Zabbix API

Изначально автоматизация часто строится так:

  • Пишутся Python-скрипты, которые:
    • создают хосты в Zabbix;
    • привязывают шаблоны;
    • настраивают триггеры, группы, медиа-тип, действия;
    • синхронизируют конфигурацию с актуальной инфраструктурой.

Примерно (упрощённо):

from pyzabbix import ZabbixAPI

z = ZabbixAPI("https://zabbix.example.com")
z.login("api_user", "password")

z.host.create(
host="app-stage-01",
interfaces=[{
"type": 1,
"main": 1,
"useip": 1,
"ip": "10.0.20.11",
"dns": "",
"port": "10050"
}],
groups=[{"groupid": "2"}],
templates=[{"templateid": "10001"}]
)

Почему это со временем оказывается проблемным:

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

Вывод: для масштабируемой и командной работы это слабый формат "monitoring as code".

Переход к декларативному подходу: Ansible + Zabbix модули

Более зрелое решение — описывать конфигурацию Zabbix декларативно:

  • использовать официальные/сообщественные Ansible-модули:
    • zabbix_host, zabbix_hostgroup, zabbix_template, zabbix_media, zabbix_action и т.д.;
  • хранить desired state в Git;
  • применять изменения через CI/CD.

Преимущества:

  • Декларативность:
    • явно видно, какие хосты, группы, шаблоны, триггеры должны существовать.
  • Идемпотентность:
    • Ansible-модули сами проверяют, нужно ли изменить объект;
    • повторный запуск не ломает конфигурацию и не создает дубликаты.
  • Читаемость:
    • YAML понятно читается любым инженером;
    • легче ревьюить, легче онбордить новых людей.
  • Интеграция с инфраструктурой:
    • можно генерировать список хостов из Terraform/инвентори:
      • всё согласовано с реальной инфраструктурой.

Упрощенный пример Ansible-конфигурации:

- name: Configure Zabbix hosts
hosts: localhost
connection: local
vars:
zabbix_url: "https://zabbix.example.com"
zabbix_user: "api_user"
zabbix_password: "password"
tasks:
- name: Ensure app host group exists
community.zabbix.zabbix_hostgroup:
server_url: "{{ zabbix_url }}"
login_user: "{{ zabbix_user }}"
login_password: "{{ zabbix_password }}"
state: present
name: "App servers"

- name: Ensure app-stage-01 host is present
community.zabbix.zabbix_host:
server_url: "{{ zabbix_url }}"
login_user: "{{ zabbix_user }}"
login_password: "{{ zabbix_password }}"
state: present
host: "app-stage-01"
groups:
- "App servers"
interfaces:
- type: 1
main: 1
useip: 1
ip: "10.0.20.11"
dns: ""
port: "10050"
templates:
- "Template App Service"

Почему первоначальный подход оказался неудачным

Формулируя на уровне зрелого инженера:

  • Скрипты на Python через Zabbix API:
    • давали быстрый старт,
    • но:
      • были императивными,
      • плохо масштабирующимися,
      • сложными для поддержки и ревью.
  • Проблемы:
    • отсутствие чёткого декларативного desired state;
    • высокая сложность онбординга;
    • логика изменений "зашита" в коде;
    • больше рисков при модификациях (сложно предсказать diff).

Почему Ansible оказался лучше:

  • Позволяет описывать мониторинг так же, как остальную инфраструктуру:
    • "monitoring as code" рядом с "infrastructure as code";
  • Даёт:
    • идемпотентность;
    • декларативность;
    • интеграцию с CI/CD;
    • единый стек для управления: те же подходы, что для Proxmox, PostgreSQL, приложений.

Правильный акцент в ответе:

  • Сначала был императивный API-driven подход на Python — рабочий, но тяжело поддерживаемый.
  • Затем сознательно был сделан шаг к декларативному управлению через Ansible:
    • конфигурация Zabbix стала:
      • прозрачной,
      • версионируемой,
      • предсказуемой;
    • это уменьшило технический долг и риски, ускорило изменения.

Такой ответ показывает не только знание инструментов, но и умение:

  • критически оценивать свои решения;
  • переходить к более зрелым практикам (декларативность, IaC, GitOps);
  • выравнивать мониторинг по тем же стандартам, что и остальную инфраструктуру.

Вопрос 11. Как с помощью Python автоматизировался перенос проектов между инстансами GitLab?

Таймкод: 00:15:42

Ответ собеседника: правильный. Кратко описал использование python-gitlab: настройка подключений к двум инстансам и циклический перенос проектов, отметил, что задача была технически несложной.

Правильный ответ:

Даже если задача кажется простой, корректный подход к миграции проектов между GitLab-инстансами должен учитывать:

  • аутентификацию и права,
  • структуру групп и namespace,
  • перенос репозиториев (HTTP/SSH URL, зеркалирование или export/import),
  • перенос metadata (issues, labels, wiki, pipelines history — если требуется),
  • идемпотентность и возможность повторного запуска.

Оптимальное решение на базе python-gitlab выглядит так.

Основная идея

  • Используем библиотеку python-gitlab для работы с обоими инстансами:
    • source_gitlab — старый инстанс (откуда переносим).
    • target_gitlab — новый инстанс (куда переносим).
  • Для каждого проекта:
    • определяем целевой namespace/группу;
    • создаем группу на target, если её ещё нет;
    • переносим проект:
      • либо через встроенный GitLab Project Import/Export;
      • либо через зеркалирование репозитория (для кода) + дополнительные шаги, если нужны issues/merge requests.

Базовый пример скрипта (упрощенный)

import gitlab

SOURCE_URL = "https://gitlab-old.example.com"
TARGET_URL = "https://gitlab-new.example.com"
SOURCE_TOKEN = "source-token"
TARGET_TOKEN = "target-token"

source_gl = gitlab.Gitlab(SOURCE_URL, private_token=SOURCE_TOKEN)
target_gl = gitlab.Gitlab(TARGET_URL, private_token=TARGET_TOKEN)

source_gl.auth()
target_gl.auth()

def ensure_group(target, full_path):
"""
Гарантирует существование группы (namespace) на целевом инстансе.
full_path вида "team/backend" или "org/project".
"""
parts = full_path.split("/")
current_path = ""
parent_id = None
for part in parts:
current_path = part if not current_path else f"{current_path}/{part}"
try:
group = target.groups.get(current_path)
except gitlab.exceptions.GitlabGetError:
group = target.groups.create({
"name": part,
"path": part,
"parent_id": parent_id,
})
parent_id = group.id
return target.groups.get(current_path)

def migrate_project(project):
# Получаем исходные данные
namespace_full_path = project.namespace["full_path"]
project_name = project.path
project_full_path = f"{namespace_full_path}/{project_name}"

print(f"Migrating {project_full_path}...")

# Гарантируем, что namespace существует на target
target_group = ensure_group(target_gl, namespace_full_path)

# Проверяем, нет ли уже проекта на target
try:
target_gl.projects.get(project_full_path)
print(f"Project {project_full_path} already exists on target, skipping.")
return
except gitlab.exceptions.GitlabGetError:
pass

# Вариант 1: используем импорт по HTTPS URL (переносим только git-историю)
# Требует токена/доступа с target к source
new_project = target_gl.projects.create({
"name": project_name,
"path": project_name,
"namespace_id": target_group.id,
"import_url": project.http_url_to_repo.replace(SOURCE_URL, f"oauth2:{SOURCE_TOKEN}@{SOURCE_URL[8:]}"),
# В проде лучше использовать отдельный тех. токен/юзера и аккуратную подстановку
})

print(f"Created project {new_project.path_with_namespace} on target.")

# Пример: миграция всех проектов определённой группы
group = source_gl.groups.get("team/backend")
for p in group.projects.list(all=True):
project = source_gl.projects.get(p.id)
migrate_project(project)

Что важно подчеркнуть в правильном подходе

  1. Воспроизводимость и идемпотентность:

    • Скрипт можно запускать повторно:
      • если проект уже есть на target — пропускаем или валидируем.
    • Структура групп создается автоматически на основе source.
  2. Сохранение структуры namespace:

    • Не просто "слить все в одну группу".
    • Мигрировать в соответствии с исходной организационной структурой:
      • это критично для прав доступа, CI/CD, зависимостей.
  3. Управление доступом:

    • Используются отдельные access tokens с ограниченными правами:
      • source: read_api/read_repository;
      • target: api для создания групп/проектов и запуска import.
    • Не хардкодить секреты в коде:
      • использовать переменные окружения или vault.
  4. Подход к содержимому:

    • В простом варианте:
      • переносится только Git-история.
    • В более полном:
      • использовать встроенный экспорт/импорт GitLab:
        • экспорт проекта на source (через API),
        • загрузка архива на target (через API),
        • это позволяет перенести issues, labels, merge requests и т.п.
    • Выбор способа зависит от требований к миграции.
  5. Интеграция с CI/CD:

    • Скрипт оборачивается в job:
      • конфиги GitLab (source/target URL, токены) берутся из CI variables;
      • миграция фиксируется логами и артефактами (отчёт о перенесённых проектах).
  6. Контроль и безопасность:

    • Логирование:
      • какие проекты перенесены, какие пропущены, какие упали с ошибкой.
    • Возможность dry-run:
      • сначала показать план (список создаваемых групп/проектов), затем выполнять.

Такой ответ показывает:

  • понимание использования python-gitlab не просто как "цикла по проектам",
  • умение учитывать структуру namespace, идемпотентность и требования к доступу,
  • подход к миграции как к инженерной задаче с контролем, повторяемостью и безопасностью, а не как к одноразовому скрипту.

Вопрос 12. Как в Linux определить размер директории на диске?

Таймкод: 00:17:17

Ответ собеседника: правильный. Предложил использовать команду du с ключами -s для суммарного размера и -h для удобного форматирования.

Правильный ответ:

Для определения занимаемого места директорией в Linux стандартно используют команду du (disk usage).

Базовый и достаточный вариант:

du -sh /path/to/dir

Расшифровка:

  • du — утилита для оценки использования диска.
  • -s (summarize) — выводить только общий итог по директории, без перечисления поддиректорий.
  • -h (human-readable) — форматирует размер в удобном виде (K, M, G).

Дополнительные полезные варианты:

  • Посмотреть размеры поддиректорий первого уровня:
    du -sh /path/to/dir/*
  • Отфильтровать самые "тяжелые" директории:
    du -sh /path/to/dir/* | sort -h

Важно помнить:

  • du показывает занятое пространство с учетом жёстких ссылок и особенностей файловой системы;
  • для точного анализа прод-систем сочетайте du с df (для проверки свободного места на файловой системе) и инструментами мониторинга.

Вопрос 13. Как проверить доступность сервиса на сервере, к которому нет SSH-доступа, только по порту?

Таймкод: 00:17:55

Ответ собеседника: правильный. Предложил использовать netcat (nc) с ключами -zv, указав IP и порт, запускать команду с доступного хоста; упомянул netstat/ss, но корректно уточнил, что на целевой сервер зайти нельзя и в итоге оставил nc как валидное решение.

Правильный ответ:

Когда к серверу нет SSH-доступа и нужно проверить доступность сервиса по конкретному порту, задача сводится к проверке сетевой доступности (TCP/UDP) с доступного хоста. Основные корректные подходы:

  1. netcat (nc) — прямой и удобный способ

Для проверки TCP-порта:

nc -zv 203.0.113.10 443

Где:

  • -z — режим "probe", не отправлять данные, только проверить возможность соединения.
  • -v — подробный вывод (покажет успешное или неуспешное подключение).
  • 203.0.113.10 — IP сервера.
  • 443 — порт сервиса.

Интерпретация:

  • succeeded / open — порт доступен, TCP-соединение установлено.
  • failed / connection refused / timed out — сервис недоступен или порт закрыт/фильтруется.
  1. telnet — старый, но рабочий вариант

Если nc нет под рукой:

telnet 203.0.113.10 443
  • Успешное подключение (Connected) говорит о доступности порта.
  • Hang/timeout или Connection refused — проблема с доступностью.
  1. curl — если это HTTP/HTTPS или похожий протокол

Для HTTP/HTTPS-сервисов логичнее сразу проверять не только порт, но и ответ:

curl -vk https://203.0.113.10:443/health
  • Покажет:
    • устанавливается ли TCP/TLS-соединение;
    • что возвращает endpoint (например, /health).
  1. nmap — для более детальной проверки

Если доступен nmap и нужны подробности:

nmap -p 443 203.0.113.10

Выдаст состояние порта: open / closed / filtered.

  1. Практический акцент
  • Проверка всегда выполняется с того сегмента сети, где ожидается использование сервиса (тот же VPC, тот же офис, тот же Kubernetes namespace и т.д.).
  • Если порт недоступен:
    • проверять маршрутизацию, firewall, security groups, ACL, load balancer’ы.
  • На интервью важно явно проговорить:
    • к целевому хосту не заходим, проверяем соединение снаружи;
    • netcat/telnet/curl/nmap — инструменты клиента, а не сервера.

Вопрос 14. Как определить, какие файлы в данный момент использует процесс в Linux?

Таймкод: 00:19:31

Ответ собеседника: правильный. Указал использование lsof с фильтрацией по PID и просмотр информации в /proc/<PID>/; путь и детали не досказал, но направление корректное.

Правильный ответ:

Для определения, какие файлы использует конкретный процесс в Linux, есть два основных, корректных и широко применяемых способа:

  1. Использование lsof

Наиболее простой и удобный инструмент:

lsof -p <PID>

Где:

  • <PID> — идентификатор процесса.

Что делает команда:

  • выводит список всех открытых файлов процессом:
    • обычные файлы;
    • директории;
    • сокеты (TCP/UDP);
    • pipe;
    • устройства;
    • библиотеки.

Полезные варианты:

  • Показать только файлы (без сокетов и т.п.):

    lsof -p <PID> | grep REG
  • Найти, кто держит конкретный файл:

    lsof /path/to/file

Это часто используют, чтобы понять:

  • кто держит lock-файл,
  • почему нельзя отмонтировать FS,
  • кто держит удалённый, но всё ещё занимающий место файл (deleted).
  1. Использование /proc/<PID>/fd

Более низкоуровневый и универсальный способ, не зависящий от lsof:

  • В Linux для каждого процесса есть директория:
    • /proc/<PID>/
  • Внутри:
    • /proc/<PID>/fd/ — список файловых дескрипторов процесса.
      • Каждый дескриптор — это симлинк на реальный объект.

Пример:

ls -l /proc/<PID>/fd

Результат покажет ссылки вида:

0 -> /dev/pts/2
1 -> /var/log/app.log
2 -> /var/log/app.err
3 -> socket:[12345]
4 -> /data/app/data.db (deleted)

Через это можно увидеть:

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

Практический акцент:

  • Для быстрой диагностики:
    • lsof -p <PID> — первый инструмент.
  • Для системного дебага или в минимальных окружениях:
    • анализ /proc/<PID>/fd.
  • При проблемах с диском:
    • искать "deleted" файлы, которые всё ещё держит процесс:
      lsof | grep deleted

Такой подход демонстрирует уверенное владение инструментами диагностики в Linux и понимание того, как процессы работают с файловой системой на уровне ОС.

Вопрос 15. Что означает запись сети в формате CIDR с маской /22 и сколько адресов она включает?

Таймкод: 00:20:32

Ответ собеседника: неправильный. Пытался объяснить CIDR, но запутался в расчетах и не дал корректного числа адресов и ясного объяснения.

Правильный ответ:

CIDR (Classless Inter-Domain Routing) — это способ записи сетей в формате:

ip_address/prefix_length

Где:

  • prefix_length (например, /22) — количество бит, принадлежащих сети;
  • остальные биты используются для адресов внутри сети (хостовых адресов).

Для IPv4 всего 32 бита.

Разберем /22:

  • /22 означает:
    • 22 бита — сеть.
    • 32 - 22 = 10 бит — хостовая часть.
  • Количество адресов в сети:
    • 2^(количество бит хостовой части) = 2^10 = 1024 адреса.

Пример:

  • Сеть: 192.168.4.0/22
  • Маска /22 в десятичном виде:
    • 255.255.252.0
    • (так как 252 = 11111100₂, это 6 бит в последнем октете, итого 8+8+8+6 = 30? Проверим правильно:)
      • Для /22:
        • первые 8 бит: 255
        • вторые 8 бит: 255
        • третьи 6 бит: 11111100 → 252
        • итого маска: 255.255.252.0

Диапазон адресов для 192.168.4.0/22:

  • Network address: 192.168.4.0
  • Broadcast address: 192.168.7.255
  • Общее количество адресов: 1024 (от .4.0 до .7.255 включительно)

Если говорить о пригодных для хостов адресах в классическом понимании (без учета современных нюансов):

  • usable-адреса: 1024 - 2 = 1022
    • вычитаем:
      • network address,
      • broadcast address.

Ключевые моменты, которые важно уверенно объяснить:

  • Маска /22 — это:
    • "шире", чем /24 (сеть больше),
    • объединяет 4 последовательные /24 подсети.
  • Формула:
    • количество адресов в сети = 2^(32 - prefix_length).
    • для /22 → 2^(32 - 22) = 2^10 = 1024.
  • Умение быстро прикинуть:
    • /24 → 256 адресов,
    • /23 → 512,
    • /22 → 1024,
    • /21 → 2048,
    • и т.д.

Такое объяснение показывает понимание базовой сетевой арифметики, необходимой для работы с инфраструктурой и сетями.

Вопрос 16. Корректна ли запись сети с недопустимым октетом и когда используется маска 0.0.0.0/0?

Таймкод: 00:22:12

Ответ собеседника: частично правильный. Верно указал, что октет 275 некорректен (диапазон 0–255), и что 0.0.0.0/0 используется для разрешения доступа с любого IP (например, в firewall), но сформулировал неструктурированно.

Правильный ответ:

Разберем по двум частям: валидность IP-адреса и смысл записи 0.0.0.0/0.

Некорректный октет в IPv4-адресе

  • IPv4-адрес — это 32 бита, обычно записывается в виде четырех десятичных октетов:
    • A.B.C.D, где каждый из A, B, C, D:
      • целое число в диапазоне от 0 до 255 включительно.
  • Любое значение октета за пределами 0–255 делает адрес синтаксически недопустимым.
    • Например: 10.0.275.1 — некорректный IP-адрес:
      • 275 > 255, такой адрес не существует в рамках IPv4.

Вывод:

  • Запись сети или хоста с октетом 275 (или любым >255) — некорректна и не должна использоваться ни в маршрутизации, ни в firewall, ни в конфигурации сервисов.

Что означает 0.0.0.0/0

Запись 0.0.0.0/0 в CIDR — это:

  • маршрут или правило, охватывающее весь IPv4-пространство адресов;
  • "любой IP-адрес", без ограничений по подсети.

Интерпретация:

  • prefix /0 означает:
    • 0 бит фиксировано под сеть,
    • 32 бита доступны под адреса,
    • значит диапазон: от 0.0.0.0 до 255.255.255.255.
  • Это не "один адрес", а маска, обозначающая все возможные IPv4-адреса.

Где используется 0.0.0.0/0 (типичные случаи):

  1. Маршрутизация (default route):

    • В таблице маршрутизации:
      • 0.0.0.0/0 → default gateway.
    • Это означает:
      • "весь трафик к сетям, для которых нет более специфичных маршрутов, отправлять сюда".
    • Пример:
      ip route
      default via 192.168.1.1 dev eth0
      Логически соответствует маршруту 0.0.0.0/0 → 192.168.1.1.
  2. Firewall, ACL, security-группы:

    • В правилах доступа:
      • 0.0.0.0/0 значит "разрешить (или запретить) для любого источника" (или назначения).
    • Пример:
      • Правило в cloud security group:
        • Allow: 0.0.0.0/0 → TCP/80
        • Означает: открыть HTTP-доступ с любого IP в интернете.
    • Важно:
      • это потенциально небезопасно, особенно для чувствительных портов (SSH, БД, админ-панели).
  3. Слушающий адрес сервиса:

    • Часто путают:

      • 0.0.0.0 как listen address (в конфиге сервиса) — "слушать на всех интерфейсах";
      • и 0.0.0.0/0 как CIDR в маршрутизации/ACL.
    • Пример в Go:

      http.ListenAndServe("0.0.0.0:8080", handler)

      Это значит: принимать соединения на порту 8080 на всех доступных интерфейсах.

Ключевые моменты, которые важно уверенно проговаривать:

  • IP с октетом > 255 — всегда некорректен.
  • 0.0.0.0/0:
    • это валидная CIDR-запись, обозначающая все IPv4-адреса;
    • в маршрутизации — default route;
    • в firewall/ACL — "любой адрес" (часто источник/назначение без ограничений);
    • требует осознанного использования с точки зрения безопасности.

Вопрос 17. Что означает идемпотентность в контексте Ansible?

Таймкод: 00:23:03

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

Правильный ответ:

Идемпотентность в контексте Ansible означает, что:

  • задачи и роли описывают желаемое состояние системы декларативно;
  • повторный запуск одних и тех же плейбуков над одним и тем же окружением:
    • не приводит к лишним изменениям;
    • не ломает текущее состояние;
    • не создает "побочных эффектов";
    • при уже достигнутом нужном состоянии Ansible сообщает state=OK (changed: false).

Ключевые аспекты идемпотентности в Ansible:

  1. Desired state вместо императивных действий

Идемпотентность базируется на том, что модули Ansible:

  • описывают "что должно быть", а не "что сделать":

Примеры:

  • Пакет должен быть установлен:

    - name: Ensure nginx is installed
    apt:
    name: nginx
    state: present

    Повторный запуск:

    • если nginx уже установлен — изменений не будет.
  • Файл должен иметь конкретное содержимое и права:

    - name: Deploy config
    template:
    src: app.conf.j2
    dest: /etc/app/app.conf
    owner: app
    group: app
    mode: '0644'

    Повторный запуск:

    • если содержимое и права не изменились — файл не перезаписывается, changed: false;
    • если конфиг поменялся — будет changed: true и (важно!) предсказуемое изменение.
  1. Идемпотентность как гарантия безопасного многократного запуска

Идеальный плейбук:

  • можно запускать:
    • при первом разворачивании,
    • при обновлении,
    • при восстановлении,
    • в cron/CI по расписанию,
  • и каждый раз он:
    • либо аккуратно доводит систему до нужного состояния,
    • либо констатирует, что всё уже ок.

Это критично для:

  • отказоустойчивых кластеров (PostgreSQL, Redis, etc.);
  • массового управления инфраструктурой;
  • GitOps-подхода, когда конфигурация регулярно "синхронизируется" с репозитория.
  1. Практические нюансы и ошибки

Грамотный инженер следит за тем, чтобы задачи оставались идемпотентными:

  • Избегать голых shell/command, если есть модуль:

    • Плохо:
      - command: apt-get install -y nginx
      Такая команда:
      • не знает текущего состояния,
      • всегда может считаться changed,
      • сложнее поддерживать.
    • Хорошо:
      - apt:
      name: nginx
      state: present
  • Если shell/command неизбежны:

    • использовать creates, removes или проверки, чтобы сделать их условно идемпотентными.

    Пример:

    - name: Initialize DB schema
    command: /usr/local/bin/migrate up
    args:
    creates: /var/lib/app/.schema_initialized
  • Следить за тем, чтобы роли не вносили "дрейф":

    • не генерировали случайные значения на каждый запуск;
    • не перезапускали сервисы без причины.
  1. Почему это важно в реальных системах

Идемпотентность:

  • делает инфраструктуру предсказуемой;
  • позволяет безопасно автоматизировать:
    • конфигурацию БД-кластеров,
    • деплой pooler’ов, балансировщиков, мониторинга, логирования;
  • упрощает аудит и отладку:
    • видно, какие изменения реально произошли при прогоне плейбука;
    • можно различать "конфиг уже в нужном состоянии" и "только что что-то изменили".

В связке с Go-сервисами и прод-инфраструктурой это означает:

  • можно многократно прогонять одни и те же роли (PostgreSQL, PgBouncer, Zabbix, Proxmox VM, сервисы),
  • будучи уверенным, что они не "сломают" уже настроенную систему, а только приведут её к описанному в коде состоянию.

Вопрос 18. Как выполнить только определённые задачи из плейбука Ansible?

Таймкод: 00:24:00

Ответ собеседника: правильный. Упомянул использование тегов для выборочного запуска задач и условных конструкций/фактов для управления выполнением, что отражает корректное понимание подходов.

Правильный ответ:

Основной и правильный способ выборочно выполнять задачи в Ansible — использовать теги. Дополнительно можно применять условия (when), разбиение на отдельные плейбуки/роли и ограничения по хостам/паттернам. Важно не превращать плейбук в набор "if’ов", а структурировать его так, чтобы выборочное выполнение было предсказуемым и управляемым.

Ключевые подходы:

  1. Теги — основной инструмент

Теги позволяют:

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

Пример:

- hosts: app_servers
become: yes
tasks:
- name: Install nginx
apt:
name: nginx
state: present
tags: [nginx, setup]

- name: Deploy app config
template:
src: app.conf.j2
dest: /etc/nginx/conf.d/app.conf
tags: [nginx, config]

- name: Restart nginx
service:
name: nginx
state: restarted
tags: [nginx, reload]

Запуск только задач с тегом nginx:

ansible-playbook site.yml --tags nginx

Запуск только конфигурационных задач:

ansible-playbook site.yml --tags config

Исключение тегов:

ansible-playbook site.yml --skip-tags nginx

Рекомендации:

  • заранее проектировать теги:
    • по компонентам (nginx, postgres, app, monitoring),
    • по типу действия (install, config, migrate, restart),
    • по окружению или цели (init, upgrade, cleanup).
  • не дублировать хаотично, а придерживаться единой схемы тегирования.
  1. Ограничение по хостам и паттернам

Можно выполнить задачи только для части инфраструктуры:

ansible-playbook site.yml -l app_servers
ansible-playbook site.yml -l app-stage-01

Комбинация с тегами:

ansible-playbook site.yml -l app-stage-01 --tags nginx

Это особенно полезно для точечных изменений или отладки.

  1. Условия (when) и факты

Условия нужны для управления выполнением на уровне логики, но не являются заменой тегам:

- name: Run DB migration only in production
command: /usr/local/bin/migrate up
when: ansible_hostname == "app-prod-01"
tags: [migrate]

Использовать when:

  • для различий между окружениями, ОС, версиями;
  • для завязки на факты (ansible_os_family, ansible_distribution, custom facts).

Не стоит злоупотреблять when как "ручным селектором задач" вместо тегов.

  1. Разбиение на роли и отдельные плейбуки

Для крупных проектов:

  • выносить логические части в роли (db, app, monitoring, proxy);
  • иметь отдельные entrypoint-плейбуки:
    • db.yml, app.yml, infra.yml.

Тогда выборочное выполнение — это запуск нужного плейбука или роли:

ansible-playbook app.yml
ansible-playbook db.yml
  1. Практический инженерный акцент

Зрелый подход:

  • теги — основной механизм "адресного" запуска;
  • роли и структура плейбуков — так спроектированы, чтобы не нужны были хаотичные костыли;
  • условия when используются точечно и осмысленно;
  • все сценарии (init, upgrade, hotfix, только-config, только-migrate) заранее формализованы через комбинацию:
    • тегов,
    • паттернов хостов,
    • entrypoint-плейбуков.

Это позволяет:

  • безопасно выполнять только нужные части конфигурации;
  • быстро отлаживать проблемы (запускать отдельные блоки без полного прогона);
  • встроить Ansible в CI/CD, где пайплайны вызывают конкретные теги/плейбуки под конкретные операции.

Вопрос 19. Как ограничить набор хостов, на которых будет выполняться плейбук Ansible?

Таймкод: 00:25:05

Ответ собеседника: правильный. Указал использование групп в inventory, параметра --limit при запуске и указание нужных хостов/групп в самом плейбуке.

Правильный ответ:

Ограничение набора хостов в Ansible — базовый, но критически важный навык для безопасного управления инфраструктурой. Корректный подход сочетает:

  • грамотную структуру inventory,
  • явное указание целевых хостов в плейбуках,
  • использование --limit для точечных запусков.

Основные способы.

  1. Указание хостов или групп в самом плейбуке

В плейбуке явно задается, на каких хостах выполняются задачи:

- name: Configure app servers
hosts: app_servers
become: yes
tasks:
- name: Ensure app is installed
apt:
name: my-app
state: present

Варианты для директивы hosts:

  • имя группы из inventory: app_servers, db_servers;
  • конкретный хост: app-01;
  • паттерны: app_, db_.

Это первый уровень ограничения: плейбук не уйдет на "лишние" хосты.

  1. Использование групп в inventory

Грамотная структура inventory позволяет удобно таргетировать:

Пример inventory:

[app_servers]
app-01
app-02

[db_servers]
db-01
db-02

[stage]
app-01
db-01

[prod]
app-02
db-02

Таким образом:

  • можно в плейбуке использовать hosts: app_servers;
  • а при запуске дополнительно уточнять окружение через --limit.
  1. Параметр --limit при запуске плейбука

--limit позволяет сузить список хостов на уровне выполнения, без изменения плейбука.

Примеры:

  • Запустить только на одном хосте:

    ansible-playbook site.yml --limit app-01
  • Только на группе:

    ansible-playbook site.yml --limit app_servers
  • Комбинации (через двоеточие):

    ansible-playbook site.yml --limit "app_servers:db_servers"
  • Исключение (через !):

    ansible-playbook site.yml --limit "prod:!db_servers"

--limit всегда пересекается с тем, что указано в hosts внутри плейбука:

  • Итоговый таргет = hosts из плейбука ∩ фильтр из --limit.
  1. Комбинация практик

Зрелый подход:

  • inventory структурирован по:
    • ролям (app, db, cache),
    • окружениям (dev, stage, prod),
    • локациям/AZ (dc1, dc2).
  • плейбуки ориентированы на логические группы:
    • hosts: app_servers, hosts: db_servers.
  • для операций (релиз, хотфикс, проверка) используется --limit:
    • чтобы сузить выполнение до:
      • конкретного окружения (например, --limit stage),
      • конкретного хоста (для отладки),
      • подмножества (blue/green и т.п.).

Это:

  • снижает риск случайного запуска задач на проде;
  • повышает управляемость и предсказуемость;
  • позволяет легко интегрировать Ansible в CI/CD (где окружение/группа задаются параметрами).

Вопрос 20. Есть ли опыт написания шаблонов Jinja2 для Ansible и для чего они использовались?

Таймкод: 00:25:39

Ответ собеседника: правильный. Указал, что использовал шаблоны Jinja2 для генерации конфигурационных файлов с подстановкой значений, зависящих от хоста или окружения.

Правильный ответ:

Шаблоны Jinja2 в Ansible — ключевой инструмент для декларативной и переиспользуемой конфигурации. Они позволяют генерировать конфиги и манифесты на основе переменных, фактов, условий и циклов, обеспечивая:

  • единый шаблон для разных окружений;
  • отсутствие дублирования;
  • предсказуемость и идемпотентность.

Типичные сценарии использования Jinja2 с Ansible.

  1. Генерация конфигурационных файлов сервисов

Основной и правильный кейс — формировать конфиги из шаблонов с учетом:

  • окружения (dev/stage/prod),
  • роли хоста (app/db/cache),
  • параметров производительности,
  • адресов сервисов (PostgreSQL, Redis, брокеры, API и т.п.).

Пример шаблона для приложения на Go (config.yaml.j2):

server:
addr: "{{ app_listen_addr | default('0.0.0.0:8080') }}"
read_timeout: {{ app_read_timeout | default(5) }}
write_timeout: {{ app_write_timeout | default(5) }}

database:
dsn: "postgres://{{ db_user }}:{{ db_password }}@{{ db_host }}:{{ db_port }}/{{ db_name }}?sslmode={{ db_sslmode | default('disable') }}"

logging:
level: "{{ log_level | default('info') }}"

feature_flags:
enable_cache: {{ enable_cache | default(true) | lower }}
enable_tracing: {{ enable_tracing | default(false) | lower }}

Задача Ansible:

- name: Render app config
template:
src: config.yaml.j2
dest: /etc/myapp/config.yaml
owner: myapp
group: myapp
mode: '0644'
notify: Restart myapp

Так один шаблон покрывает разные окружения и хосты через переменные group_vars/host_vars.

  1. Конфигурация Nginx, PgBouncer, HAProxy, systemd

Jinja2 удобно использовать для типовых инфраструктурных компонентов:

  • Nginx vhost’ы:
    • разные upstream’ы и домены в зависимости от окружения.
  • PgBouncer:
    • динамический список баз и пулов;
  • HAProxy:
    • список backend’ов, health-check’ов;
  • systemd unit’ы:
    • параметры среды, пути, лимиты.

Пример PgBouncer-шаблона (pgbouncer.ini.j2):

[databases]
{% for db_name, cfg in pgbouncer_databases.items() %}
{{ db_name }} = host={{ cfg.host }} port={{ cfg.port }} dbname={{ cfg.dbname }} pool_size={{ cfg.pool_size | default(50) }}
{% endfor %}

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
max_client_conn = {{ pgbouncer_max_client_conn | default(2000) }}
default_pool_size = {{ pgbouncer_default_pool_size | default(50) }}
  1. Использование условий и циклов

Jinja2 позволяет выразить логику без копипасты:

  • разные настройки по окружениям:
log_level = {{ 'debug' if env == 'dev' else 'info' }}
  • генерация блоков по спискам/словарам:
{% for upstream in upstreams %}
server {{ upstream.host }}:{{ upstream.port }};
{% endfor %}

Ключевые моменты:

  • логика должна быть читабельной;
  • тяжелую бизнес-логику лучше выносить в vars/role, минимизируя сложность шаблона.
  1. Интеграция с фактами Ansible

В шаблонах можно использовать:

  • ansible_hostname, ansible_fqdn;
  • IP-адреса интерфейсов;
  • кастомные факты.

Пример:

node_name = "{{ ansible_hostname }}"

Это позволяет генерировать конфиги, завязанные на реальные параметры хоста.

  1. Проверка и надежность

Для прод-подхода важно:

  • шаблоны хранятся в Git, проходят review;
  • используется ansible-lint для проверки;
  • изменения проверяются на stage перед prod;
  • шаблоны пишутся идемпотентно:
    • одинаковые входные данные → одинаковый вывод;
    • без рандома и побочных эффектов.
  1. Связь с Go и инфраструктурой

Для сервисов на Go Jinja2 используется как часть инфраструктурного пайплайна:

  • один бинарь приложения;
  • разные config.yaml, сформированные через Jinja2:
    • разные DSN PostgreSQL,
    • разные endpoints внешних сервисов,
    • разные уровни логирования/feature flags.

Это:

  • упрощает деплой;
  • минимизирует различия в коде между окружениями;
  • позволяет управлять поведением сервисов декларативно.

Таким образом, корректный ответ:

  • подтверждает опыт работы с Jinja2;
  • подчёркивает его использование для генерации конфигов инфраструктурных и прикладных сервисов;
  • показывает понимание декларативности, переиспользования и интеграции с Ansible и GitOps-подходом.

Вопрос 21. Есть ли опыт написания и использования собственных и внешних ролей Ansible и какой подход к заимствованным ролям?

Таймкод: 00:26:29

Ответ собеседника: правильный. Подтвердил опыт создания ролей и использования внешних (Galaxy/GitHub), описал практику форка внешних ролей в свой репозиторий и их доработки с пониманием ответственности за сопровождение.

Правильный ответ:

Зрелый подход к Ansible-ролям включает:

  • системное проектирование собственных ролей;
  • осознанное использование внешних ролей;
  • контроль качества, версионирование и интеграцию с общим GitOps/IaC-подходом.

Оптимальная модель выглядит так.

Опыт разработки собственных ролей

Собственные роли используются для:

  • критичных компонент инфраструктуры;
  • доменно-специфичных вещей, уникальных для компании/проекта;
  • случаев, когда внешний код не удовлетворяет требованиям по качеству, безопасности или структуре.

Ключевые принципы собственных ролей:

  • Четкая ответственность роли:
    • role: postgres_cluster — установка и базовая конфигурация PostgreSQL/Patroni/экспортеров.
    • role: pgbouncer — установка, конфиг, перезапуск.
    • role: app_service — деплой Go-сервисов (binary + systemd + конфиг).
  • Идемпотентность:
    • каждый task приводит систему к детерминированному состоянию;
    • повторные запуски безопасны.
  • Минимальная логика в shell:
    • использовать core/official модули Ansible;
    • shell/command — только с creates/removes/when и понятными гарантиями.
  • Переиспользуемость:
    • параметры через defaults/vars;
    • без хардкода окружений, секретов, конкретных хостов.
  • Стандарты структуры:
    • tasks/, templates/, files/, handlers/, vars/, defaults/, meta/;
    • документация в README.md роли.

Пример: роль деплоя Go-сервиса

  • Сборка образа/артефакта в CI.
  • Роль:
    • кладет бинарь,
    • рендерит config (Jinja2),
    • создает systemd unit,
    • делает daemon-reload и рестарт при изменениях.

Фрагмент systemd unit-шаблона:

[Unit]
Description={{ app_name }} service
After=network.target

[Service]
User={{ app_user }}
Group={{ app_group }}
ExecStart=/usr/local/bin/{{ app_name }} -config {{ app_config_path }}
Restart=on-failure
Environment=GODEBUG=madvdontneed=1

[Install]
WantedBy=multi-user.target

Использование внешних ролей (Galaxy/GitHub)

Внешние роли уместно применять для:

  • широко используемых компонентов:
    • nginx, node_exporter, postgres_exporter, docker, prometheus, grafana и т.п.
  • стандартных задач, где нет необходимости "изобретать велосипед".

Лучшие практики работы с внешними ролями:

  • Фиксация версий:
    • в requirements.yml:
      - src: geerlingguy.nginx
      version: 0.18.0
    • никаких "latest" на проде.
  • Предварительный аудит:
    • читаем код роли:
      • идемпотентность;
      • отсутствие опасных команд;
      • отсутствие хардкода;
      • адекватная поддержка параметров.
  • Инкапсуляция:
    • не "зашиваем" внешнюю роль в бизнес-логику;
    • оборачиваем, если надо, своей тонкой ролью/плейбуком, чтобы не зависеть от внутренних деталей.

Подход к форку и доработке внешних ролей

Форк внешней роли — нормальная практика, если:

  • нужно:
    • адаптировать под свои стандарты (security, layout, company policies);
    • добавить поддержку специфичных окружений;
    • починить баги, которые критичны здесь и сейчас.

Но это включает ответственность:

  • Форк переносится в собственный Git-репозиторий:
    • например: internal-ansible-roles/postgres, internal-ansible-roles/pgbouncer.
  • Вводится версионирование:
    • тегирование релизов ролей;
    • фиксация версий в requirements.yml.
  • Периодический rebase/merge с апстримом:
    • отслеживание security-патчей;
    • аккуратное подтягивание изменений, если они актуальны.
  • Документирование:
    • чем форк отличается от оригинала;
    • какие поведенческие изменения внесены.

Критично:

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

Интеграция с GitOps/IaC

Роли — часть единого контура:

  • Все роли (собственные и форкнутые) хранятся в Git.
  • Используется:
    • ansible-lint,
    • molecule (по возможности) для тестирования ролей;
    • CI:
      • проверка синтаксиса,
      • тестовые прогоны.
  • В проде применяются только через пайплайны:
    • без ручных правок ролей на серверах.

Итоговый акцент в корректном ответе:

  • Да, есть опыт написания собственных ролей:
    • для сервисов, БД, пулеров, мониторинга, системных настроек.
  • Есть опыт использования и адаптации внешних ролей:
    • роли из Galaxy/GitHub сначала ревьюятся, фиксируются по версии.
    • при необходимости форка — выносятся в свой репозиторий, дорабатываются и поддерживаются как часть внутреннего IaC-стека.
  • Понимание, что с момента форка ответственность за обновления и безопасность лежит на команде, а не на внешнем авторе.

Вопрос 22. Как поддерживать форкнутую внешнюю Ansible-роль, если она сильно разошлась с исходным репозиторием?

Таймкод: 00:27:35

Ответ собеседника: неполный. Предложил при необходимости заново взять актуальную версию из исходного репозитория и вручную перенести свои изменения, задокументировав их. Осознает проблему, но не описывает системный подход (upstream remote, регулярный merge/rebase, семантическое версионирование, управление изменениями).

Правильный ответ:

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

  • есть upstream (оригинальная роль),
  • есть ваш форк как продукт с собственным lifecycle,
  • есть стратегия, как подтягивать изменения из upstream и не ломать свои.

Оптимальный подход можно описать так.

  1. Структурировать форк как полноценный проект

С самого начала форк должен быть:

  • в отдельном репозитории (например, git@your-org/internal-ansible-role-pgbouncer.git);
  • с:
    • README, где явно указано, от какого проекта форкнуто;
    • описанием отличий;
    • версионированием (теги, changelog);
    • CI (ansible-lint, molecule/tests).

Ключевая идея:

  • вы не просто "скопировали роль", вы теперь владеете её жизненным циклом.
  1. Настроить связь с upstream через git remote

Правильный технический фундамент — не "качать zip раз в год", а:

  • добавить оригинальный репозиторий как upstream-remote:
git remote add upstream https://github.com/original/ansible-role.git
git fetch upstream

Дальше два стандартных сценария интеграции:

  • merge upstream/main → ваш main;
  • либо rebase на новые коммиты upstream.

Пример:

# Обновляем сведения об upstream
git fetch upstream

# Сливаем изменения оригинала в свой main
git checkout main
git merge upstream/main

# Разруливаем конфликты, запускаем тесты, тегируем релиз

Преимущества:

  • прозрачно видно diff между вашей ролью и upstream;
  • можно выборочно переносить изменения (cherry-pick критичных фиксов).
  1. Использовать семантическое версионирование

Сильно разошедшийся форк должен жить по своим версиям:

  • v1.x.y — синхронен ранним версиям upstream (минимальные правки);
  • v2.x.y — несовместимые изменения (breaking changes), адаптация под вашу инфраструктуру.

Практика:

  • использовать semantic versioning:
    • MAJOR — несовместимые изменения,
    • MINOR — новая функциональность без ломания внешнего интерфейса,
    • PATCH — багфиксы, security-патчи.
  • фиксировать версии роли в requirements.yml ваших Ansible-проектов:
- src: git@gitlab.your-org/internal-ansible-role-pgbouncer.git
scm: git
version: "2.3.1"
name: yourorg.pgbouncer

Так вы избегаете внезапных изменений при очередном обновлении.

  1. Минимизировать "drift" по архитектуре роли

Чем больше вы произвольно меняете структуру роли, тем сложнее подтягивать upstream.

Хорошая практика:

  • Не ломать базовую архитектуру без крайней необходимости:
    • сохранять знакомую структуру tasks/templates/vars;
    • не превращать роль в монолитный Frankenstein.
  • Свои изменения:
    • выносить в:
      • дополнительные параметры (defaults/main.yml),
      • отдельные tasks-файлы,
      • включаемые подзадачи (import_tasks/include_tasks).
  • Если нужен совсем другой behavior:
    • разумнее создать свою роль "с нуля", чем мучить форк.
  1. Регулярно подтягивать upstream, а не раз в год

Типичная ошибка — ждать, пока форк и upstream разъедутся так, что merge становится болью.

Зрелый подход:

  • Раз в некоторое время (например, раз в 1–3 месяца):
    • git fetch upstream
    • анализ релизов upstream (особенно security и багфиксы);
    • аккуратный merge или выборочный cherry-pick;
    • запуск тестов (molecule/интеграционные прогоны);
    • выпуск новой версии вашей роли.

Это:

  • снижает риск накопленных конфликтов;
  • позволяет быстро получать важные фиксы.
  1. Управлять отличиями осознанно

Если форк "живёт своей жизнью", важно:

  • Явно задокументировать ключевые отличия:
    • параметры, которые добавлены/изменены;
    • поведение по умолчанию (например, более строгая безопасность);
    • несовместимости с оригинальной ролью.
  • Для сложных изменений — рассмотреть:
    • контрибьют назад в upstream:
      • если ваши изменения полезны в общем случае, лучше внести их в оригинал и уйти от форка;
    • либо официально признать:
      • это отдельный продукт (own namespace: yourorg.postgresql_cluster), и не пытаться "сравниваться" с upstream каждую неделю.
  1. Что не является хорошей практикой
  • Периодически "выкинуть всё и взять свежую версию, а затем руками слияние по памяти":
    • высокий риск потери важных изменений;
    • отсутствие трассируемости;
    • боль при отладке.
  • Изменять внешнюю роль прямо в vendor-каталоге без отдельного репо:
    • невозможно повторно использовать,
    • сложно обновлять,
    • невозможно нормально ревьюить.

Кратко, как должен звучать зрелый ответ:

  • Форк оформляется как отдельный поддерживаемый проект в нашем namespace.
  • Оригинальный репозиторий подключен как upstream remote.
  • Периодически подтягиваем изменения из upstream через merge/rebase/cherry-pick.
  • Используем свои версии (semver), фиксируем их в requirements.yml.
  • Документируем отличия и минимизируем хаотичный drift.
  • Где возможно — отправляем улучшения в upstream, чтобы сократить необходимость в форке.

Такой подход показывает не только знание git/Ansible, но и понимание инженерной ответственности за заимствованный код и его эволюцию.

Вопрос 23. Как объединить несколько последних коммитов в один в рамках одной ветки без merge в другую ветку?

Таймкод: 00:29:52

Ответ собеседника: неполный. Указал на использование git rebase и HEAD~3 для переписи истории и объединения изменений, но не дал точной пошаговой команды, не отделил interactive rebase и squash, не акцентировал важные нюансы.

Правильный ответ:

Корректный и контролируемый способ объединить несколько последних коммитов в один в рамках одной ветки — использовать интерактивный rebase с последующим squash/fixup.

Ключевые моменты:

  • Операция переписывает историю (изменяет хэши коммитов).
  • Выполнять это безопасно:
    • до того, как коммиты ушли в общий origin и используются другими;
    • либо осознанно, предупредив команду (force-push нарушит их историю).

Базовый сценарий: объединить N последних коммитов в один

Допустим, нужно объединить последние 3 коммита текущей ветки в один.

  1. Запускаем интерактивный rebase:
git rebase -i HEAD~3

Смысл:

  • HEAD~3 — "показать последние 3 коммита для редактирования" (точнее: rebase начнётся от родителя этого диапазона).
  • Откроется редактор с чем-то вроде:
pick a1b2c3d Коммит 1
pick e4f5g6h Коммит 2
pick i7j8k9l Коммит 3
  1. Задаём стратегию squash:
  • Первый коммит оставить как базовый (pick);
  • Остальные "прижать" к нему через squash (s) или fixup (f):
pick a1b2c3d Коммит 1
squash e4f5g6h Коммит 2
squash i7j8k9l Коммит 3

Разница:

  • squash — позволит объединить комментарии и отредактировать итоговое сообщение;
  • fixup — тихо сольёт коммит в первый, не сохранив его сообщение.
  1. Сохраняем и закрываем редактор
  • Git выполнит rebase:
    • объединит изменения всех трёх коммитов;
    • предложит отредактировать итоговое сообщение (для squash).

В результате:

  • на месте 3 коммитов появится 1 новый;
  • история ветки станет чище.
  1. Обновление удалённой ветки (если нужно)

Если ветка уже пушилась в удалённый репозиторий:

git push --force-with-lease

Рекомендация:

  • использовать именно --force-with-lease, а не голый --force:
    • это защитит от случайного затирания чужих коммитов.

Важно про безопасность и практику

  • Такой squash допустим:
    • для локальных/фича-веток до merge request;
    • в рамках практики "чистая история перед merge".
  • Нежелателен:
    • для веток, по которым уже работают другие;
    • в main/master/prod-ветках и long-lived ветках без жёсткой дисциплины.

Альтернативные варианты (кратко):

  • Если нужно объединить не только последние N, а произвольный диапазон:
    • также через git rebase -i с указанием нужной точки (например, от общего родителя).
  • Если хотите "один итоговый коммит из всей ветки":
    • часто делают:
      • обычные рабочие коммиты;
      • перед merge — rebase -i и squash в один/несколько логичных.

Типичные ошибки, которых стоит избегать:

  • Использовать git merge --squash вместо rebase, если задача — переписать историю в уже существующей ветке (merge --squash уместен для объединения при вливании в другую ветку).
  • Делать squash публичных коммитов без понимания последствий для коллег.
  • Путать количество коммитов:
    • git rebase -i HEAD3 открывает 3 последних коммита (точнее: редактируется диапазон после HEAD3), важно считать правильно.

Итого, коротко:

  • Используем интерактивный rebase:
    • git rebase -i HEAD~N
    • первый коммит — pick, остальные — squash/fixup.
  • После успешного rebase — при необходимости пушим с --force-with-lease.
  • Делаем это до того, как ветку активно используют другие участники.

Вопрос 24. Как синхронизировать функциональную ветку с обновлённым master, если в master появились изменения в тех же файлах?

Таймкод: 00:31:01

Ответ собеседника: правильный. Предложил смержить master в рабочую ветку, разрешить конфликты в ней, а затем выполнять финальный merge. Это корректный и практичный подход.

Правильный ответ:

Задача: в master появились новые изменения (в том числе в тех же файлах, что и в вашей feature-ветке), и нужно:

  • подтянуть их в рабочую ветку,
  • решить конфликты заранее,
  • сделать так, чтобы финальный merge в master был чистым и предсказуемым.

Есть два основных корректных подхода: merge master в feature-ветку или rebase feature-ветки на master. Оба допустимы, выбор зависит от командной политики и требований к истории.

  1. Подход 1: Merge master → feature (самый безопасный и часто рекомендуемый)

Этот вариант не переписывает историю feature-ветки (важно, если ветка уже шарится между разработчиками).

Шаги:

  1. Обновляем master:
git checkout master
git pull origin master
  1. Переходим в feature-ветку:
git checkout feature/my-task
  1. Мержим master в feature:
git merge master
  1. Разрешаем конфликты:
  • Git пометит конфликтующие файлы.
  • Вручную выбираем правильные изменения.
  • После исправления:
git add <files>
git commit

В результате:

  • feature-ветка теперь содержит все изменения master;
  • конфликты уже разрешены в вашей ветке;
  • финальный merge feature → master обычно fast-forward или простой merge без конфликтов.

Плюсы:

  • История не переписывается.
  • Безопасно для общих веток.
  • Хорошо видно точку, где подтягивали master.

Минусы:

  • История может содержать несколько merge-коммитов (визуальный "шум", но это честная история).
  1. Подход 2: Rebase feature на master (для линейной истории)

Используется, когда команда предпочитает "чистую" линейную историю и допускает переписывание истории в feature-ветках (обычно до merge request).

Шаги:

  1. Обновляем master:
git checkout master
git pull origin master
  1. Переходим в feature-ветку:
git checkout feature/my-task
  1. Перебазируемся на master:
git rebase master
  1. При конфликтах:
  • Git остановится на проблемном коммите.
  • Разрешаем конфликты вручную.
  • Затем:
git add <files>
git rebase --continue
  1. После успешного rebase:
  • при необходимости обновляем удалённую ветку:
git push --force-with-lease

Плюсы:

  • Линейная история без merge-коммитов.
  • Удобно для чтения логов и анализа.

Минусы:

  • Переписывает историю:
    • нельзя делать rebase публичной ветки, по которой уже работают другие, без координации;
    • нужен дисциплинированный процесс.
  1. Практические рекомендации
  • Если feature-ветка уже расшарена или долгая:
    • предпочтителен git merge master в feature.
  • Если ветка короткая и контролируемая (локальная или в MR до ревью):
    • можно использовать git rebase master для линейной истории.
  • Всегда:
    • разрешать конфликты в рабочей ветке (feature), а не в master;
    • прогонять тесты после синхронизации;
    • использовать git push --force-with-lease вместо голого --force при rebase.
  1. Типичные ошибки, которых стоит избегать
  • Делать rebase master (общей ветки) на feature.
  • Делать force-push в master.
  • Игнорировать конфликты и "затирать" изменения другой стороны без осознания.
  • Не прогонять тесты после сложного мерджа/rebase.

Итого корректный, зрелый ответ:

  • "Подтянуть изменения master в функциональную ветку (через merge или rebase), разрешить конфликты там, убедиться что всё собирается и тесты проходят, и только после этого делать финальный merge feature-ветки в master."

Вопрос 25. Как организовать работу команды, чтобы несколько разработчиков могли совместно коммитить в одну ветку разработки и иметь актуальный код?

Таймкод: 00:32:25

Ответ собеседника: неполный. Уходит в обсуждение git-flow и индивидуальных веток от develop, акцентирует культуру и изоляцию изменений, но не отвечает прямо на сценарий, когда несколько разработчиков сознательно работают в одной общей ветке без форков и частых rebase.

Правильный ответ:

Совместная работа нескольких разработчиков в одной ветке (например, общей feature-ветке или long-running "integration" ветке) — допустимый сценарий, если он организован как управляемый процесс, а не хаос:

  • все работают с одним remote-бранчем;
  • изменения синхронизируются часто и предсказуемо;
  • минимизируются конфликтные практики (случайные rebase, force-push без координации);
  • есть понятные правила коммитов и интеграции.

Ниже — практичный, инженерно корректный подход.

Основные принципы

  1. Один общий удаленный бранч как "интеграционная" ветка:

    • Например: feature/payments-refactor.
    • Все разработчики работают в нем как в общем пространстве.
  2. Запрет на переписывание истории без явного согласия:

    • Никаких git rebase origin/branch с последующим force-push "по настроению".
    • История общая — значит, rebase и squash допустимы только:
      • до публикации,
      • либо строго по регламенту и с синхронизацией команды.
  3. Частая синхронизация:

    • Разработчики регулярно подтягивают изменения:
      • git pull --ff-only или git pull --rebase (по договоренности, см. ниже).
    • Чем меньше "отставание", тем меньше конфликтов.

Рабочая модель без лишних rebase и форков

Вариант 1: Только merge, без переписывания истории

Подходит, если важнее стабильность, чем "идеальная" история.

  • Все разработчики:

    • работают в одной ветке feature/xyz;
    • делают небольшие, логичные коммиты;
    • перед пушем:
      • git pull --ff-only origin feature/xyz
        • если fast-forward невозможен → значит, кто-то уже запушил несовместимую историю:
          • нужно локально разрулить (merge), затем пушнуть.
  • Если есть локальные коммиты и на сервере появились новые:

    • git pull (по умолчанию сделает merge-commit) или:
      • git fetch origin
      • git merge origin/feature/xyz
    • Разрешаем конфликты у себя локально.
    • Затем git push.

Особенности:

  • история может содержать merge-коммиты;
  • но нет force-push, нет неожиданного изменения истории;
  • все участники всегда могут просто git pull.

Вариант 2: Регламентированный pull --rebase (аккуратный вариант)

Можно использовать git pull --rebase, чтобы убрать лишние merge-коммиты, но:

  • каждый разработчик:
    • перед пушем делает:
      • git pull --rebase origin feature/xyz
    • локальные коммиты "поверх" актуального состояния ветки.
  • Затем:
    • git push (без force).

Условие:

  • никто не делает rebase уже опубликованных коммитов с последующим force-push, который ломает историю другим.

Ключевое правило:

  • rebase — только локальный относительно origin/feature/xyz.
  • Не переписывать уже запушенную общую историю.

Правила для командной работы в одной ветке

Чтобы это реально работало, нужны конкретные правила:

  1. Никакого git push --force в общую ветку.

    • Если очень нужно (массовый squash/cleanup):
      • делается один раз по регламенту:
        • назначенный ответственный,
        • окно времени,
        • уведомление команды,
        • все после этого делают git fetch + git reset --hard origin/branch.
    • В обычном процессе — запрещено.
  2. Мелкие, осмысленные коммиты

    • Каждый коммит должен:
      • собираться,
      • проходить базовые тесты (как минимум локально),
      • не ломать общий код.
    • Это снижает вероятность того, что один человек "роняет" ветку для всех.
  3. Частый pull

    • Не копить большой отрыв.
    • Перед началом работы:
      • git pull origin feature/xyz
    • Перед пушем:
      • git pull (merge или rebase, в зависимости от правила команды),
      • решить конфликты локально,
      • затем git push.
  4. CI на общую ветку

    • На общий бранч обязательно натравлен CI:
      • сборка,
      • тесты,
      • линтеры.
    • Если CI "красный":
      • никто не добавляет сверху "ещё коммитов лишь бы запушить";
      • сначала чинится причина.
  5. Зонирование ответственности внутри ветки

    • По возможности:
      • договориться, кто трогает какие модули/файлы.
    • При работе над общей задачей:
      • ранний обмен: "я правлю domain/service.go, ты — http/handler.go".
    • Это снижает конфликтность.
  6. Код-ревью даже в общей ветке

    • Вариант:
      • работать через merge request в общий feature-бранч от личных коротких веток.
      • Но вопрос по условию — "без форков и лишних ребейзов".
    • Минимум:
      • коллеги смотрят ключевые изменения;
      • не мешать друг другу "напрямую" пушить опасный код.

Типичный практичный workflow

  • Разработчик A:

    • git checkout feature/xyz
    • git pull
    • делает правки, коммиты;
    • git pull --rebase origin feature/xyz (или просто git pull);
    • git push origin feature/xyz.
  • Разработчик B:

    • до начала работы:
      • git pull origin feature/xyz
    • пишет код;
    • перед пушем:
      • git pull --rebase (или merge),
      • решает конфликты, если есть,
      • git push.

Никто не делает force-push, поэтому:

  • история может быть "шумной", но:
    • она консистентна;
    • каждый всегда может безопасно сделать git pull.

Краткая формулировка корректного ответа

  • Разрешить совместную работу в одной ветке можно, если:
    • запретить force-push и переписывание истории;
    • договориться о стратегии синхронизации (git pull --rebase или git pull с merge);
    • часто подтягивать изменения и решать конфликты локально;
    • держать коммиты маленькими и стабильными;
    • защитить ветку через CI и branch protection.
  • Это позволяет нескольким разработчикам коммитить в одну ветку и всегда иметь актуальный, работоспособный код без хаоса в истории.

Вопрос 26. В чём принципиальное отличие виртуализации от контейнеризации?

Таймкод: 00:35:59

Ответ собеседника: неполный. Упомянул эмуляцию блочных устройств при виртуализации и шаринг ресурсов при контейнеризации, но не раскрыл чётко уровни изоляции, роль гипервизора, общий kernel, user-space, overhead и практические последствия для архитектуры.

Правильный ответ:

Принципиальное отличие:

  • Виртуализация эмулирует полноценные виртуальные машины с собственными ОС поверх гипервизора.
  • Контейнеризация изолирует процессы в рамках одного ядра хостовой ОС, разделяя kernel, но предоставляя отдельные user-space окружения.

Это различие по уровню изоляции и архитектуре определяет:

  • накладные расходы,
  • скорость запуска,
  • безопасность,
  • плотность размещения,
  • операционные практики.

Разберём по слоям.

Виртуализация (VM)

Идея:

  • Каждая виртуальная машина — это "почти физический сервер":
    • своя гостевая ОС (kernel + user space),
    • свои драйверы,
    • свои системные службы,
    • полная изоляция на уровне виртуализированного "железа".

Ключевые компоненты:

  • Hypervisor:
    • тип 1 (bare-metal): ESXi, Hyper-V, Xen;
    • тип 2 (hosted): VirtualBox, VMware Workstation.
  • Каждый VM:
    • получает виртуальные CPU, RAM, диски, сетевые интерфейсы;
    • устанавливается полноценная ОС (Linux/Windows/…);
    • поверх ОС ставятся приложения.

Последствия:

  • Изоляция:
    • сильная, почти как между физическими машинами;
    • пробить границу VM-хост сложнее (при корректной конфигурации).
  • Накладные расходы:
    • больше:
      • каждый гость — свой kernel, background-сервисы;
      • дополнительный слой виртуализации I/O.
  • Скорость:
    • запуск VM — секунды/десятки секунд;
    • снапшоты/миграции тяжелее.
  • Плотность:
    • меньше приложений на железо при тех же ресурсах.
  • Применение:
    • смесь разных ОС;
    • строгая изоляция (compliance, security);
    • кластеры СУБД, legacy-приложения, мульти-tenant с жёсткими границами.

Контейнеризация

Идея:

  • Контейнер — это изолированный процесс (или группа процессов), разделяющий kernel хоста, но имеющий:
    • свой root-фс (образ),
    • свои namespaces (pid, net, mnt, ipc, uts, user),
    • свои cgroups для лимитов CPU/RAM/IO.

Ключевые технологии:

  • Linux namespaces:
    • изоляция PID, сети, файловой системы, hostname и т.д.
  • cgroups:
    • ограничения и учет ресурсов.
  • Образы:
    • слоёные файловые системы (OverlayFS и др.).
  • Рантаймы:
    • containerd, runc, CRI-O, Docker Engine как оркестратор.

Последствия:

  • Общий kernel:
    • все контейнеры работают на одном ядре:
      • нет собственного kernel внутри контейнера;
      • нельзя запустить ядро другой ОС (например, полноценный Windows внутри Linux-контейнера).
  • Изоляция:
    • легче, чем у VM:
      • при ошибках/уязвимостях в namespaces/cgroups/kernel возможны контейнер-эскейпы;
      • требует строгих политик (Seccomp, AppArmor/SELinux, rootless контейнеры).
  • Накладные расходы:
    • минимальные:
      • нет второго ядра;
      • общий ресурсный слой;
    • высокая плотность.
  • Скорость:
    • запуск контейнера — доли секунды;
    • масштабирование реактивное.
  • Применение:
    • микросервисы, Go/Java/Node API;
    • CI/CD, ephemeral-окружения;
    • Kubernetes-орchestration;
    • быстрая доставка и откаты.

Сравнение по ключевым осям

  • Уровень изоляции:
    • ВМ: железо → гипервизор → гостевая ОС → приложение.
    • Контейнер: хост ОС (kernel) → контейнер (user-space) → приложение.
  • Безопасность:
    • ВМ чаще рассматривают как более "жёсткую" границу.
    • Контейнеры нужно дополнительно укреплять политиками безопасности.
  • Гибкость:
    • ВМ — можно разные ОС.
    • Контейнеры — тот же тип ядра (Linux-контейнеры на Linux, Windows-контейнеры на Windows).
  • Производительность:
    • ВМ — накладные расходы больше, но предсказуемы.
    • Контейнеры — ближе к bare-metal для CPU-bound/IO-bound задач, при правильной настройке.

Практические выводы для инженерной архитектуры

  1. Go-сервисы и микросервисы:
  • Контейнеризация — основной выбор:
    • один бинарник, минимальный образ (scratch/distroless),
    • быстрые деплои,
    • Kubernetes/Helm для оркестрации.

Пример Dockerfile для Go:

FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app ./cmd/app

FROM gcr.io/distroless/base-debian12
WORKDIR /app
COPY --from=builder /app/app .
USER nonroot:nonroot
ENTRYPOINT ["/app/app"]
  1. Критичные stateful-сервисы (PostgreSQL, Kafka, etc.):
  • Решение зависит от требований:
    • часто — ВМ (или bare-metal) из-за контроля дисков, сети, предсказуемости IO и security;
    • иногда — контейнеры под Kubernetes с правильно спроектированным storage и операторами;
  • Важно понимать trade-off:
    • контейнеризация stateful-сервисов усложняет операционный контур.
  1. Multi-tenant и compliance:
  • Высокие требования изоляции (финансовые, гос-системы):
    • слои:
      • физика → ВМ → контейнеры;
    • контейнеры внутри ВМ как дополнительный уровень, а не замена.

Краткая формулировка для интервью:

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

Вопрос 27. За счёт каких механизмов ядра происходит изоляция контейнеров друг от друга?

Таймкод: 00:36:42

Ответ собеседника: правильный. Указал на использование namespace как механизма ядра для разделения процессов и ресурсов.

Правильный ответ:

Изоляция контейнеров в Linux обеспечивается несколькими фундаментальными механизмами ядра, которые в совокупности позволяют запускать процессы так, чтобы они выглядели как отдельные "мини-системы", хотя используют общее ядро.

Ключевые механизмы:

  1. Namespaces — пространственная изоляция

Namespaces скрывают от процесса всё, что за пределами его "мира". Основные типы:

  • pid namespace:

    • Изолирует дерево процессов.
    • Процесс внутри контейнера видит свои PID’ы, часто начиная с 1.
    • Процессы вне namespace не видны.
  • mnt (mount) namespace:

    • Изолирует файловую систему.
    • Контейнер получает свой rootfs:
      • слои образа + volume’ы.
    • Монтирования внутри контейнера не влияют на хост и другие контейнеры.
  • net namespace:

    • Изолирует сетевой стек:
      • свои интерфейсы (veth, lo),
      • свои IP, маршруты, iptables.
    • Позволяет давать контейнеру отдельный сетевой контур.
  • uts namespace:

    • Изолирует hostname и domainname.
    • Контейнер может иметь свой hostname, не влияя на хост.
  • ipc namespace:

    • Изолирует межпроцессное взаимодействие:
      • System V IPC, POSIX message queues.
    • Исключает пересечения с другими контейнерами.
  • user namespace:

    • Разводит UID/GID внутри контейнера и на хосте.
    • Процесс может быть "root" внутри контейнера, но отображаться в непривилегированного пользователя на хосте (важно для безопасности).

В совокупности namespaces дают "отдельный мир" для процессов: свои процессы, сеть, файловую систему, hostname и т.п.

  1. cgroups (Control Groups) — управление и лимиты ресурсов

cgroups не про "видимость", а про "сколько можно съесть":

  • Ограничивают использование:
    • CPU (доли, квоты),
    • RAM (лимиты, OOM),
    • дискового IO,
    • сетевых ресурсов,
    • количества процессов (pids cgroup).
  • Обеспечивают:
    • предотвращение "выжирания" всех ресурсов одним контейнером;
    • предсказуемость и изоляцию по производительности.

Пример эффекта:

  • Один контейнер не может "повесить" весь хост, забрав всю память, если заданы лимиты;
  • Go-сервис в контейнере должен учитывать cgroup-лимиты (например, для настройки GOMAXPROCS лучше читать реальные лимиты CPU).
  1. Дополнительные механизмы безопасности

Поверх namespaces и cgroups в прод-средах важно включать:

  • capabilities:
    • тонкая настройка привилегий процессов;
    • контейнеру можно отобрать опасные привилегии root.
  • seccomp:
    • фильтрация системных вызовов;
    • запрет потенциально опасных syscall’ов.
  • AppArmor / SELinux:
    • мандатные политики безопасности;
    • ограничение доступа процессов к ресурсам даже внутри контейнера.

Они не являются механизмами изоляции контейнеров в базовом определении "namespaces + cgroups", но критичны для реальной многопользовательской и multi-tenant безопасности.

  1. Практический акцент

Современные контейнерные рантаймы и оркестраторы (Docker, containerd, CRI-O, Kubernetes):

  • конфигурируют namespaces для изоляции окружения;
  • создают cgroups для лимитов и учета ресурсов;
  • настраивают capabilities, seccomp и MAC-политики для усиления защиты.

Понимание этого важно при:

  • построении безопасной инфраструктуры;
  • объяснении, почему контейнер — не "лёгкая VM":
    • у него общий kernel с хостом, изоляция реализована через механизмы ядра, а не полноценной виртуализацией железа.

Вопрос 28. За счёт чего задаются ограничения по ресурсам для контейнеров, например по памяти?

Таймкод: 00:37:02

Ответ собеседника: правильный. Указал на использование cgroups для ограничения ресурсов контейнера.

Правильный ответ:

Ограничения ресурсов контейнеров (память, CPU, количество процессов, I/O и т.д.) задаются через механизм ядра Linux под названием cgroups (Control Groups).

Ключевые моменты.

Что такое cgroups

  • cgroups — подсистема ядра, позволяющая:
    • ограничивать (limit),
    • приоритизировать,
    • учитывать (account),
    • изолировать использование ресурсов группой процессов.
  • Контейнер по сути — набор процессов, помещённых в определённый namespace + cgroup.

Основные типы ограничений, важные для контейнеров:

  1. Память

Через cgroup (v1: memory, v2: unified cgroup) можно:

  • задать:
    • верхний лимит памяти (memory limit),
    • лимит по swap,
  • управлять поведением при превышении:
    • при достижении лимита контейнер может получить OOM (Out Of Memory) и его процесс(ы) будут убиты OOM-killer’ом внутри cgroup.

Пример на уровне Docker:

docker run -m 512m --memory-swap=512m myapp

Это создаст для контейнера cgroup, где:

  • максимум 512 МБ памяти;
  • без дополнительного swap (в данном примере memory == memory-swap).

В Kubernetes (через kubelet → cgroups):

resources:
requests:
memory: "256Mi"
limits:
memory: "512Mi"
  • kubelet создаст cgroup с лимитом памяти 512Mi для контейнера/Pod’а.
  1. CPU

cgroups позволяют:

  • ограничивать долю CPU:
    • через cpu.shares, cpu.cfs_quota_us, cpu.cfs_period_us (v1),
    • или аналогичные параметры в cgroup v2.
  • Пример (Docker):
docker run --cpus=1.5 myapp
  1. Другие ресурсы
  • pids: ограничение количества процессов:
    • чтобы контейнер не "заспавнил" тысячи процессов.
  • blkio: ограничение I/O к дискам.
  • net_cls, net_prio: управление сетевыми приоритетами.

Почему это важно для приложений (включая Go-сервисы)

  • Приложение внутри контейнера видит только свою cgroup-реальность.

  • Для корректной работы (особенно для Go, JVM и т.п.) важно:

    • использовать параметры, учитывающие cgroups:
      • современные версии Go (1.20+) корректнее учитывают cgroup-лимиты при выборе GOMAXPROCS;
    • понимать, что:
      • "видимый" объём памяти и CPU внутри контейнера управляется лимитами,
      • превышение лимитов ведёт к OOM внутри cgroup, а не "на всём хосте".

Практическое резюме:

  • Изоляция окружения контейнера: namespaces.
  • Ограничение и учёт ресурсов контейнера: cgroups.
  • В проде лимиты должны быть заданы осознанно:
    • чтобы один контейнер не мог "высосать" весь CPU/память/IO и не уронить соседние сервисы.

Вопрос 29. Можно ли увидеть процессы контейнера с хоста без docker exec и как?

Таймкод: 00:37:27

Ответ собеседника: правильный. Указал, что процессы контейнера на хосте видны как обычные процессы, что верно (хотя без явного упоминания PID namespace и отображения PID’ов).

Правильный ответ:

Да, процессы контейнера видны на хост-системе как обычные процессы, потому что контейнеры в Linux — это процессы в отдельных namespace’ах и cgroup’ах, но всё ещё под управлением общего ядра хоста.

Основные способы посмотреть процессы контейнера с хоста (без docker exec):

  1. Обычный ps/top на хосте

Контейнерные процессы — это обычные процессы с точки зрения хоста:

ps aux | grep myapp
ps -eo pid,ppid,user,cmd | grep docker

Вы увидите процессы контейнера с их PID на хосте. Внутри контейнера тот же процесс может иметь другой PID из-за PID namespace.

  1. Связка с Docker: docker top

Если можно пользоваться Docker CLI (но не заходить внутрь контейнера):

docker ps
docker top <container_id>

docker top показывает процессы контейнера с точки зрения хоста, без запуска интерактивной сессии внутри контейнера.

  1. Через cgroups и /proc (более низкоуровневый способ)

Контейнерные процессы привязаны к cgroup’ам. Можно:

  • найти cgroup контейнера,
  • посмотреть PIDs через /sys/fs/cgroup или /proc.

Пример (упрощённо):

# Найти процессы контейнера по имени/ID в списке процессов
ps aux | grep <container_id_or_name>

# Или посмотреть cgroup процесса
cat /proc/<PID>/cgroup

По записям в cgroup можно понять, что процесс принадлежит контейнеру.

  1. В Kubernetes

На ноде Kubernetes контейнерные процессы также видны как обычные процессы:

  • Можно использовать:
    • crictl ps
    • crictl inspect
  • Или системные утилиты:
    • ps aux | grep <pod_name> / <image_name>.

Ключевая идея:

  • Контейнер — не отдельная виртуальная машина, а процессы в отдельном наборе namespace’ов и cgroup’ов.
  • Поэтому:
    • с хоста вы всегда можете увидеть процессы контейнеров;
    • внутри контейнера — видите урезанную картину по PID namespace.

Это важно понимать при отладке, профилировании и расследовании инцидентов на уровне узла.

Вопрос 30. Где на хосте найти логи контейнера Docker?

Таймкод: 00:37:52

Ответ собеседника: частично правильный. Верно указал, что стандартный подход — логировать в stdout/stderr контейнера и что файлы логов лежат в каталоге /var/lib/docker/containers/<container-id>/*.log, но путь и детали сформулировал неточно и не затронул нюансы лог-драйверов.

Правильный ответ:

По умолчанию Docker не "прячет" логи внутри контейнера, а пишет то, что процесс выводит в stdout/stderr, в лог-файлы на хосте. Но точное расположение и формат зависят от драйвера логирования.

Базовый случай: json-file (дефолт для большинства инсталляций)

Если используется стандартный лог-драйвер json-file (значение по умолчанию во многих дистрибутивах Docker), логи хранятся на хосте по пути:

  • /var/lib/docker/containers/<container-id>/<container-id>-json.log

Где:

  • <container-id> — полный ID контейнера (первые символы того, что вы видите в docker ps).

Пример:

docker ps
# допустим, ID = a1b2c3d4e5f6

ls /var/lib/docker/containers/a1b2c3d4e5f6/
# ...
# a1b2c3d4e5f6-json.log

Этот файл содержит все строки, которые приложение писало в stdout/stderr, в формате JSON-объектов. Docker команда:

docker logs <container-id or name>

по сути читает этот файл (или соответствующий источник, если драйвер другой).

Важные нюансы:

  1. Логи завязаны на лог-драйвер

Если лог-драйвер изменен, путь и место хранения могут быть другими:

  • json-file:
    • локальные файлы: /var/lib/docker/containers/<id>/<id>-json.log
  • local:
    • оптимизированный бинарный формат рядом с контейнером.
  • syslog, journald:
    • логи уходят в системный журнал:
      • journalctl,
      • syslog (/var/log/syslog, /var/log/messages и т.п.).
  • fluentd, gelf, awslogs, splunk и т.д.:
    • логи отправляются во внешнюю систему, локальных файлов в /var/lib/docker/containers может не быть (или они будут неполными).

Проверить лог-драйвер контейнера:

docker inspect --format='{{.HostConfig.LogConfig.Type}}' <container-id>
  1. Не редактировать логи руками
  • Нельзя бездумно чистить/удалять *.log файлы в /var/lib/docker/containers:
    • это может поломать работу docker logs;
    • если чистите — делайте это осознанно и лучше через ротацию.

Для json-file доступны настройки ротации в /etc/docker/daemon.json, например:

{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}
  1. Правильная практика логирования для приложений

Современный прод-подход:

  • Приложение (например, на Go) пишет структурированные логи в stdout/stderr.
  • Docker/Kubernetes/рантайм:
    • собирает эти потоки;
    • отдаёт их в:
      • json-file/local,
      • journald/syslog,
      • либо в системy логирования (ELK, Loki, etc.).
  • Внутри контейнера:
    • лучше не писать лог-файлы напрямую в /var/log/... без необходимости;
    • это усложняет сбор логов и управление ими.

Минимальный пример для Go:

log.Printf("level=info msg=\"starting server\" port=%d", port)

Эти строки окажутся в docker logs и, при default-драйвере, в /var/lib/docker/containers/<id>/<id>-json.log.

Кратко:

  • При стандартном json-file:
    • логи контейнера лежат на хосте в:
      • /var/lib/docker/containers/<container-id>/<container-id>-json.log
  • Но всегда нужно учитывать:
    • какой лог-драйвер настроен;
    • что в прод-средах логи часто уезжают в journald или внешние системы, а не живут только в json-файлах.

Вопрос 31. Где искать логи etcd в Kubernetes-кластере, если etcd запускается в контейнере без Docker?

Таймкод: 00:38:34

Ответ собеседника: неполный. Предположил использование journalctl или логов контейнерного рантайма (containerd и т.п.), но без конкретики по стандартным сценариям и командам.

Правильный ответ:

Нужно исходить из реальной схемы запуска etcd в Kubernetes-кластере. Обычно etcd для control-plane работает:

  • либо как static Pod (через kubelet) на master-ноде;
  • либо как отдельный systemd-сервис или внешний кластер.

При этом "без Docker" чаще означает использование containerd/CRI-O как контейнерного рантайма. Логи в таких случаях ищутся следующим образом.

  1. Если etcd работает как static Pod (наиболее типичный случай в современных кластерах)

В Kubernetes-кластерах, созданных kubeadm, etcd обычно запускается как static Pod на control-plane ноде, а kubelet использует containerd или другой CRI-рантайм.

В этом случае:

  • kubelet пишет логи контейнеров в стандартный путь:
    • /var/log/containers/
    • /var/log/pods/
  • Логи etcd будут доступны:

Через kubectl (рекомендуемый и универсальный способ):

kubectl -n kube-system get pods | grep etcd
kubectl -n kube-system logs <etcd-pod-name>

Например:

kubectl -n kube-system logs etcd-master-1

Это абстрагирует от конкретного рантайма (containerd/CRI-O).

На ноде, напрямую (если нужно без kubectl):

  • Логи контейнера etcd (symlink-и, завязанные на CRI):

    • /var/log/containers/etcd-<node-name>kube-system*.log
    • /var/log/pods/kube-system_etcd-<node-name>_/etcd/.log

Пример:

ls /var/log/containers | grep etcd
cat /var/log/containers/etcd-master-1_kube-system_etcd-*.log
  • При использовании containerd:
    • "сырые" логи контейнера обычно лежат в:
      • /var/log/pods/... (через kubelet),
      • либо в data-директории containerd (но в нормальном кластере вам почти всегда достаточно /var/log/containers + kubectl logs).

Ключевой момент:

  • Если etcd — это Pod, правильный путь — через kubectl logs и стандартные Kubernetes-лог директории, а не искать "docker logs".
  1. Если etcd запускается как systemd-сервис (без контейнеров)

Некоторые инсталляции (особенно старые/кастомные) поднимают etcd как родной бинарник:

  • В этом случае логи обычно идут в journald или в файл.

Проверка:

systemctl status etcd
journalctl -u etcd -f

Если в юните прописан вывод в файл /var/log/etcd/..., смотреть там.

  1. Если etcd в контейнере под containerd/CRI-O без kubeadm-стека

Если etcd запущен вручную как контейнер через CRI-рантайм:

  • Используем crictl для доступа к логам:
crictl ps | grep etcd
crictl logs <container-id>

Это прямой аналог docker logs, но для CRI.

  1. Практический приоритет действий

Если вопрос в контексте нормального Kubernetes-кластера с etcd как частью control-plane:

  • Сначала:
    • kubectl -n kube-system logs etcd-<node-name>
  • Если нет доступа через API или есть подозрение на проблемы control-plane:
    • на соответствующей control-plane ноде:
      • смотреть /var/log/containers и /var/log/pods для etcd-*;
      • при systemd-режиме — journalctl -u etcd.

Краткая, прикладная формулировка:

  • В кластерах с etcd как static Pod:
    • логи etcd смотрим через:
      • kubectl -n kube-system logs <etcd-pod>;
      • либо на ноде: /var/log/containers/etcd-kube-system.log.
  • В кластерах, где etcd как systemd-сервис:
    • journalctl -u etcd.
  • При использовании containerd/CRI-O:
    • kubectl logs или crictl logs вместо docker logs.

Важно не путать:

  • отсутствие Docker не означает отсутствие логов;
  • логирование идёт через kubelet/CRI и стандартные пути/инструменты, а доступ к логам делается через kubectl или crictl, а не через docker exec.

Вопрос 32. Какие основные бинарные компоненты входят в Kubernetes (control plane и worker)?

Таймкод: 00:40:39

Ответ собеседника: правильный. Перечислил kube-apiserver, controller-manager, scheduler, kubelet и kube-proxy, показав базовое понимание архитектуры.

Правильный ответ:

Корректный ответ должен не только перечислять компоненты, но и пояснять их роль и взаимодействие. Базовый набор ключевых бинарей Kubernetes делится на компоненты control plane и компоненты на worker-нодах.

Control plane (управляющая плоскость)

Основные компоненты:

  1. kube-apiserver

    • Центральная точка входа во все операции Kubernetes.
    • Принимает запросы от:
      • kubectl, контроллеров, операторов, внешних систем, внутренних компонентов.
    • Реализует:
      • аутентификацию (AuthN),
      • авторизацию (AuthZ),
      • admission-логику,
      • валидацию и мутацию объектов.
    • Работает поверх etcd как над "истинным состоянием" кластера.
    • Вся коммуникация внутри кластера (по модели control plane) строится через API-server: это ключевой инвариант.
  2. kube-controller-manager

    • Запускает набор контроллеров, реализующих "control loops":
      • Node controller (отслеживание статуса нод),
      • Replication/Deployment controller,
      • Endpoint/ServiceAccount/Tokens controller и др.
    • Каждый контроллер:
      • читает desired state из etcd через kube-apiserver,
      • сравнивает его с текущим состоянием,
      • предпринимает действия для достижения желаемого состояния (создать/удалить Pod, обновить endpoints, и т.п.).
    • Основа reconcile-модели Kubernetes.
  3. kube-scheduler

    • Отвечает за принятие решения, на какую ноду поставить новый Pod.
    • Основные шаги:
      • фильтрация (подходящие ноды по ресурсам, taints/tolerations, nodeSelector/nodeAffinity, constraints),
      • scoring (выбор лучшей ноды с учетом нагрузки, распределения и т.д.).
    • Устанавливает binding через kube-apiserver.
    • Сам не запускает контейнеры; он только назначает, где их запустить.
  4. etcd (как часть control plane)

    • Не Kubernetes-бинарь, но критичный компонент.
    • Хранит:
      • все состояние кластера:
        • манифесты объектов,
        • статусы,
        • конфигурацию.
    • Требует:
      • отказоустойчивости,
      • резервного копирования,
      • правильной конфигурации (TLS, кворм).

Worker-ноды (data plane)

Основные компоненты:

  1. kubelet

    • Агент на каждой ноде (control-plane и worker, если они совмещены).
    • Обязанности:
      • следит за Pod’ами, которые должны быть запущены на ноде (через API-server);
      • взаимодействует с контейнерным рантаймом (containerd, CRI-O и т.п.) по CRI;
      • управляет lifecycle Pod’ов и контейнеров:
        • запуск, перезапуск, остановка;
      • управляет readiness/liveness/startup пробами;
      • монтирует volume’ы;
      • репортит статус обратно в kube-apiserver.
    • kubelet — исполнитель решений, принятых control plane.
  2. kube-proxy

    • Сетевой компонент, работающий на каждой ноде.
    • Реализует абстракцию Service:
      • следит за объектами Service и EndpointSlice;
      • настраивает правила маршрутизации трафика к Pod’ам.
    • В разных режимах:
      • iptables,
      • IPVS,
      • (ранее userspace).
    • Фактически отвечает за то, чтобы:
      • трафик на ClusterIP/NodePort корректно доходил до нужных Pod’ов.
  3. Контейнерный рантайм (не kubernetes-бинарь, но критично)

    • containerd, CRI-O (или dockershim в старых кластерах).
    • kubelet через CRI отдаёт команды рантайму:
      • запустить/остановить контейнер,
      • получить статус, логи и т.п.

Уточнения и практические нюансы

  • На control-plane нодах помимо kube-apiserver, kube-scheduler и kube-controller-manager обычно также работают:
    • kubelet,
    • kube-proxy (если нода также может запускать Pod’ы).
  • В продуктивной конфигурации control plane обычно:
    • высокодоступен (несколько реплик api-server, etcd cluster, controller-manager, scheduler).
  • Многие дополнительные компоненты (CoreDNS, Ingress Controller, CNI-плагины, Metrics Server, Operators) работают как обычные Pod’ы и не относятся к "основным бинарям", но критичны для полноценного кластера.

Краткий ответ, который ожидается на уровне уверенного инженера:

  • Control plane:
    • kube-apiserver — API и точка входа;
    • kube-controller-manager — контроллеры reconcile-логики;
    • kube-scheduler — планирование Pod’ов;
    • etcd — хранилище состояния кластера.
  • Worker:
    • kubelet — агент, управляющий Pod’ами на ноде;
    • kube-proxy — реализация сервисной сетевой абстракции.
  • Контейнерный рантайм (containerd/CRI-O) — нижний слой исполнения контейнеров, с которым работает kubelet.

Вопрос 33. Как запустить по одному экземпляру сервиса (например, сборщика логов) на каждом узле кластера Kubernetes?

Таймкод: 00:41:18

Ответ собеседника: правильный. Указал использование DaemonSet как подходящей абстракции.

Правильный ответ:

Для гарантированного запуска по одному (или по одному на условие) экземпляру Pod’а на каждом узле Kubernetes используется объект DaemonSet.

Ключевая идея:

  • Deployment масштабируется по количеству реплик и не "привязан" к каждому node.
  • DaemonSet гарантирует, что на каждом подходящем узле кластера будет запущен один Pod:
    • при добавлении нового узла — Pod автоматически там создаётся;
    • при удалении узла — Pod удаляется;
    • при падении Pod’а на узле — он пересоздаётся на том же узле (если узел жив).

Типичные сценарии использования DaemonSet:

  • агенты сбора логов:
    • Fluent Bit, Fluentd, Vector, Logstash-агенты;
  • агенты мониторинга:
    • node-exporter, cAdvisor, агенты APM;
  • сетевые и инфраструктурные демоны:
    • CNI-плагины, storage-агенты, node-level сервисы.

Базовый пример DaemonSet для лог-агента:

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-collector
namespace: kube-system
spec:
selector:
matchLabels:
app: log-collector
template:
metadata:
labels:
app: log-collector
spec:
serviceAccountName: log-collector
containers:
- name: log-collector
image: my-registry/log-collector:latest
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "256Mi"
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
type: Directory

Важные моменты:

  • DaemonSet работает поверх стандартного планировщика:
    • но с особой логикой: Pod’ы размещаются по одному на ноду.
  • Можно таргетировать не все ноды:
    • через nodeSelector, nodeAffinity, taints/tolerations:
      • например, запускать сборщик логов только на worker-нодах.
  • При использовании для инфраструктурных агентов:
    • стоит тщательно задать ресурсы (requests/limits);
    • учесть безопасность (под каким пользователем, какие hostPath монтируются).

Таким образом, правильное и ожидаемое решение: использовать DaemonSet для сервиса, который должен присутствовать на каждом (или определённых) узле кластера.

Вопрос 34. Как добиться запуска пода только на определённом узле Kubernetes?

Таймкод: 00:41:56

Ответ собеседника: правильный. Указал использование nodeSelector/лейблов и affinity/anti-affinity для ограничения размещения pod’ов на нужных нодах; детали названий полей вспоминал неуверенно, но подход верный.

Правильный ответ:

Запуск Pod’а только на определённых узлах Kubernetes достигается за счёт сочетания:

  • лейблов на нодах,
  • селекторов/аффинити в спецификации Pod’а/Deployment,
  • при необходимости — taints/tolerations.

Базовый и рекомендуемый способ — пометить нужные ноды label’ами и использовать nodeSelector или nodeAffinity.

  1. nodeSelector + labels на нодах (простой и наглядный способ)

Шаги:

  1. Пометить нужный узел:
kubectl label nodes node-1 role=logs
  1. В манифесте Pod/Deployment указать nodeSelector:
apiVersion: v1
kind: Pod
metadata:
name: log-agent
spec:
nodeSelector:
role: logs
containers:
- name: log-agent
image: my-registry/log-agent:latest

Результат:

  • Pod может быть запущен только на нодах, у которых есть label role=logs.
  • Если таких нод нет или они недоступны — Pod останется Pending.

Плюсы:

  • максимально простой,
  • хорошо читается.

Минус:

  • жёсткое равенство (только exact match по ключ-значению).
  1. nodeAffinity (более гибкий и выразительный способ)

Для более сложных условий (несколько нод, зоны, типы, исключения) используют nodeAffinity:

apiVersion: v1
kind: Pod
metadata:
name: log-agent
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: role
operator: In
values: ["logs"]
containers:
- name: log-agent
image: my-registry/log-agent:latest

Ключевые моменты:

  • requiredDuringSchedulingIgnoredDuringExecution:
    • жесткое требование: под не будет запущен, если условие не выполнено.
  • operator: In, NotIn, Exists, DoesNotExist и т.д.:
    • позволяет гибко описывать условия.
  • Это более мощный аналог nodeSelector:
    • nodeSelector по сути — синтаксический сахар над простым вариантом nodeAffinity.
  1. Taints и tolerations (чтобы ограничить узлы "только для своих")

Если нужно не только "привязать" pod к конкретным нодам, но и защитить эти ноды от чужих pod’ов:

  1. Отметить ноду taint’ом:
kubectl taint nodes node-1 dedicated=logs:NoSchedule
  1. В Pod добавить toleration:
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "logs"
effect: "NoSchedule"
nodeSelector:
role: logs

В результате:

  • На node-1 смогут запускаться только Pod’ы с toleration + (желательно) правильным nodeSelector.
  • Это типичный паттерн для:
    • выделенных нод под логирование, мониторинг, stateful-нагрузку.
  1. Практические рекомендации
  • Для задачи "запустить Pod только на конкретном узле":
    • минимум:
      • label на ноде,
      • nodeSelector или nodeAffinity в PodSpec.
  • Для защищённых/выделенных нод:
    • комбинировать:
      • nodeSelector/nodeAffinity,
      • taints/tolerations.
  • Не хардкодить nodeName, если это не особый случай.

(Допустимый, но менее гибкий вариант — spec.nodeName)

В редких случаях можно явно указать nodeName:

spec:
nodeName: node-1

Но:

  • это жестко "прибивает" Pod к конкретному имени ноды;
  • не учитывает планировщик;
  • плохо масштабируется и не рекомендуется как основной механизм.

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

  • Использовать labels на нодах + nodeSelector или nodeAffinity для выбора нужных узлов.
  • При необходимости ограничения — добавлять taints/tolerations.
  • Избегать ручного nodeName, кроме специфических случаев (отладка, очень специализированные Pod’ы).

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

Таймкод: 00:44:01

Ответ собеседника: правильный. Описал использование pod affinity/anti-affinity с правилами, которые препятствуют размещению нескольких pod’ов одного приложения на одной ноде, оставляя возможность "нарушить" правило при дефиците ресурсов.

Правильный ответ:

Задача: при масштабировании приложения (через Deployment/ReplicaSet/StatefulSet) добиться распределения реплик по нодам так, чтобы:

  • по возможности на одной ноде не оказывалось больше одной реплики;
  • но при недостатке нод или ресурсов кластер мог всё-таки разместить дополнительные экземпляры на тех же нодах, а не держал Pods в Pending.

Для этого в Kubernetes используются:

  • podAntiAffinity (предпочтительно),
  • topology spread constraints (современный и более универсальный механизм).

Оба подхода стоит знать и уметь объяснить.

Подход 1: podAntiAffinity

podAntiAffinity позволяет задать правило:

  • "Не размещай Pod этого приложения на ноде, где уже есть Pod с такими же метками".

Рекомендуется использовать preferredDuringSchedulingIgnoredDuringExecution (мягкое правило), чтобы:

  • кластер сначала пытался распределить Pods по разным нодам;
  • но при невозможности — мог всё же поставить несколько на одну ноду.

Пример Deployment с podAntiAffinity:

apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 4
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: ["my-app"]
topologyKey: "kubernetes.io/hostname"
containers:
- name: app
image: my-registry/my-app:latest

Ключевые моменты:

  • labelSelector:
    • выбирает Pods этого же приложения (по метке app: my-app).
  • topologyKey: kubernetes.io/hostname:
    • правило действует на уровне ноды:
      • т.е. не класть одинаковые Pods на один hostname, если есть альтернатива.
  • preferredDuring...:
    • "мягкое" ограничение:
      • планировщик будет стараться раскидать Pods по разным нодам;
      • если нод мало или ресурсы ограничены — допустит несколько Pods на одной ноде.

Если требуется строгое правило (обычно аккуратнее, но может привести к Pending):

  • используем requiredDuringSchedulingIgnoredDuringExecution:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["my-app"]
topologyKey: "kubernetes.io/hostname"

В этом случае:

  • пока есть достаточное количество нод:
    • будет ровно по одному Pod на ноду;
  • если реплик больше, чем нод:
    • лишние останутся в Pending (что может быть нежелательно).

Подход 2: TopologySpreadConstraints (рекомендуется для современных кластеров)

TopologySpreadConstraints дают более декларативный и гибкий способ "равномерно" распределять Pods.

Пример:

apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 4
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: "kubernetes.io/hostname"
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: my-app
containers:
- name: app
image: my-registry/my-app:latest

Пояснения:

  • topologyKey: kubernetes.io/hostname:
    • балансируем по нодам.
  • labelSelector:
    • учитываем только Pods текущего приложения.
  • maxSkew: 1:
    • разница в количестве Pods между нодами не должна превышать 1.
  • whenUnsatisfiable: ScheduleAnyway:
    • если идеальное распределение невозможно, всё равно размещать Pods (аналог мягкого правила).

Преимущества topologySpreadConstraints:

  • Более гибкий и проговоренный способ равномерного распределения;
  • Не только "не больше одного", а контролируемый skew;
  • Можно комбинировать несколько уровней:
    • по hostname,
    • по zone (failure-domain.beta.kubernetes.io/zone или topology.kubernetes.io/zone),
    • строить отказоустойчивость.

Практические рекомендации:

  • Для "по возможности по одному Pod на ноду":
    • podAntiAffinity с preferredDuringSchedulingIgnoredDuringExecution
    • или topologySpreadConstraints с maxSkew=1 и whenUnsatisfiable=ScheduleAnyway.
  • Для строгого правила "один Pod на ноду, иначе не запускать":
    • podAntiAffinity с requiredDuringSchedulingIgnoredDuringExecution
    • но понимать, что часть Pods может остаться Pending, если нод мало.
  • Для прод-сценариев (особенно критичных сервисов):
    • combined-подход:
      • spread constraints по zone/hostname,
      • affinity/anti-affinity,
      • разумные лимиты ресурсов, чтобы планировщик реально мог равномерно размещать Pods.

Краткий ответ, который ожидается:

  • Использовать podAntiAffinity или topologySpreadConstraints, настроив их так, чтобы scheduler распределял реплики по нодам, стараясь не ставить более одной реплики на одну ноду, но позволял отклонения, если ресурсов или нод недостаточно.

Вопрос 36. Какие виды affinity/anti-affinity использовать, чтобы избежать размещения нескольких реплик одного приложения на одной ноде?

Таймкод: 00:46:05

Ответ собеседника: правильный. Уточнил, что для разведения реплик по нодам используется podAntiAffinity, корректно сопоставив механизм с задачей.

Правильный ответ:

Для предотвращения (или минимизации вероятности) размещения нескольких реплик одного и того же приложения на одной ноде используют podAntiAffinity. Именно podAntiAffinity описывает правила взаиморазмещения Pod’ов относительно других Pod’ов, в отличие от nodeAffinity, который работает с лейблами нод.

Ключевые моменты:

  • podAntiAffinity:
    • опирается на метки Pod’ов (labelSelector),
    • использует topologyKey, чтобы задать уровень "разведения" (например, нода, зона),
    • позволяет задавать жесткие (required) и мягкие (preferred) правила.
  1. Мягкое правило: preferredDuringSchedulingIgnoredDuringExecution

Используется, когда:

  • хотим максимально равномерно распределить Pods по нодам;
  • но не хотим, чтобы при дефиците ресурсов Pods оставались в Pending.

Пример:

affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: ["my-app"]
topologyKey: "kubernetes.io/hostname"

Семантика:

  • topologyKey = "kubernetes.io/hostname":
    • планировщик старается не размещать несколько Pod’ов my-app на одном hostname;
  • preferred:
    • если нельзя выполнить условие идеально, Pods всё равно будут размещены (ScheduleAnyway по сути).

Это практичный выбор для большинства приложений: "раскидывай по нодам, но не ломай кластер, если мест мало".

  1. Жесткое правило: requiredDuringSchedulingIgnoredDuringExecution

Используется, если:

  • нужно строго гарантировать:
    • Pod не должен быть запущен на ноде, где уже есть Pod того же приложения;
  • и вы осознаёте, что при недостатке нод Pods могут зависнуть в Pending.

Пример:

affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["my-app"]
topologyKey: "kubernetes.io/hostname"

Семантика:

  • На одной ноде — не более одного Pod с label app=my-app.
  • Если реплик больше, чем нод, часть Pod’ов не будет запущена.
  1. Почему именно podAntiAffinity, а не nodeAffinity
  • nodeAffinity:
    • управляет тем, на каких нодах Pod "может" или "должен" запускаться (по лейблам нод);
    • не умеет учитывать уже размещённые Pod’ы этого приложения.
  • podAntiAffinity:
    • непосредственно решает задачу "не ставь меня рядом с Pod’ами, у которых такие-то метки";
    • поэтому это правильный инструмент для разведения реплик по нодам.
  1. Практические рекомендации
  • Для прод-приложений:
    • настраивать podAntiAffinity (или topologySpreadConstraints) для:
      • повышения отказоустойчивости (реплики не на одной ноде),
      • равномерного распределения нагрузки.
  • Выбор:
    • preferredDuringScheduling — разумный дефолт.
    • requiredDuringScheduling — когда важнее строгая изоляция, чем 100% запуск всех реплик.

Итого: для предотвращения размещения нескольких реплик одного приложения на одной ноде используют podAntiAffinity (required или preferred) с topologyKey = "kubernetes.io/hostname" и селектором по меткам самого приложения.

Вопрос 37. Как настроить доступ: системе CI/CD дать полный контроль над деплоем в нескольких namespaces, а разработчикам выдать только права на чтение состояния?

Таймкод: 00:46:59

Ответ собеседника: правильный. Предложил использовать RBAC: определить роли с нужными правами, привязать их к ServiceAccount через Role/ClusterRoleBinding, дать CI расширенные права на деплой, а разработчикам — доступ только к просмотру подов, логов и статусов; упомянул GitOps (Argo CD) как вариант.

Правильный ответ:

Задача: грамотно разграничить права в Kubernetes:

  • CI/CD-системе:
    • полный контроль над деплоем в выбранных namespaces (создание/обновление/удаление ресурсов);
  • разработчикам:
    • read-only доступ:
      • смотреть Pods, Events, Logs, Deployments, Services и т.п.,
      • но не модифицировать ресурсы и не деплоить вручную.

Это классический кейс для использования Kubernetes RBAC (Role-Based Access Control) в связке с сервис-аккаунтами и (опционально) GitOps-инструментами.

Ключевые принципы:

  1. Никаких "admin" прав всем подряд.
  2. Чёткое разделение:
    • сервисные аккаунты CI/CD с минимально достаточными полномочиями;
    • человеко-пользователи (dev) с read-only.
  3. Привязка прав к конкретным namespaces.

Разберём практическую реализацию.

  1. Полные права на деплой для CI/CD в конкретных namespaces

Для CI/CD (GitLab CI, GitHub Actions, Jenkins, Argo CD и т.п.) создаём:

  • отдельный ServiceAccount в кластере,
  • Role или ClusterRole с нужными правами,
  • RoleBinding/ClusterRoleBinding для привязки.

Если нужно управлять несколькими конкретными namespaces (и только ими), лучше:

  • использовать ClusterRole с набором правил,
  • и RoleBinding в каждом целевом namespace.

Пример ClusterRole для CI/CD (упрощённо):

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ci-deployer
rules:
- apiGroups: ["", "apps", "batch", "extensions", "networking.k8s.io"]
resources:
- pods
- pods/log
- services
- configmaps
- secrets
- deployments
- statefulsets
- daemonsets
- jobs
- cronjobs
- ingresses
- replicasets
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

Далее в нужных namespaces создаём RoleBinding, который привязывает этот ClusterRole к ServiceAccount CI:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer-binding
namespace: app-namespace
subjects:
- kind: ServiceAccount
name: ci-deployer-sa
namespace: ci-namespace
roleRef:
kind: ClusterRole
name: ci-deployer
apiGroup: rbac.authorization.k8s.io

Комментарии:

  • CI получает:
    • полный контроль над ресурсами деплоя в указанных namespaces;
    • при этом не имеет прав на cluster-wide операции (узлы, CRD, чужие namespaces и т.п.), если специально не выданы.
  • Токен этого ServiceAccount используется в CI:
    • сохраняется как secret/variable в CI-системе;
    • kubectl/helm/Argo CD используют его для применения манифестов.
  1. Read-only права для разработчиков

Для разработчиков нужно:

  • разрешить:
    • смотреть Pods, Deployments, StatefulSets, Services, Ingress, ConfigMaps, Secrets (только meta/значения, если политика позволяет),
    • смотреть логи pod’ов,
    • смотреть Events;
  • запретить любые изменения (create/update/patch/delete).

Это можно сделать через:

  • ClusterRole с набором read-only правил;
  • RoleBinding/ClusterRoleBinding для конкретных групп пользователей (через интеграцию с OIDC/LDAP/Groups).

Пример read-only ClusterRole для приложенческих namespaces:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: dev-readonly
rules:
- apiGroups: ["", "apps", "batch", "extensions", "networking.k8s.io"]
resources:
- pods
- pods/log
- services
- endpoints
- configmaps
- secrets
- deployments
- statefulsets
- daemonsets
- jobs
- cronjobs
- ingresses
- replicasets
- events
verbs: ["get", "list", "watch"]

Привязка к разработчикам (пример для группы):

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-readonly-binding
namespace: app-namespace
subjects:
- kind: Group
name: dev-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: dev-readonly
apiGroup: rbac.authorization.k8s.io

Итог:

  • Пользователи из группы dev-team:
    • могут использовать kubectl/k9s:
      • kubectl get pods, logs, describe и т.п.;
    • не могут изменять ресурсы: apply, delete, scale, edit будут запрещены.
  1. GitOps-подход как усиление модели

Если используется GitOps (Argo CD, Flux):

  • CI/CD не обязательно должен напрямую иметь широкий доступ:
    • можно ограничить его пушем в Git-репозиторий;
    • а Argo CD (как контроллер в кластере) сам применяет манифесты.

При этом:

  • для Argo CD настраивается отдельный ServiceAccount с нужными правами на target namespaces (аналогично ci-deployer);
  • разработчики:
    • создают/меняют манифесты через Git (MR, code review);
    • в кластере по-прежнему имеют read-only для runtime-состояния.

Это усиливает безопасность:

  • изменения в кластере только через Git + контроллер;
  • любой прямой kubectl apply "от человека" можно запретить.
  1. Важные инженерные моменты
  • Не выдавать cluster-admin CI или разработчикам "по привычке":
    • это быстро превращается в дыру в безопасности.
  • Разделять:
    • технические аккаунты (ServiceAccount для CI/GitOps),
    • человеческие аккаунты (OIDC/LDAP → RBAC).
  • Минимизировать права:
    • CI:
      • только те namespaces и ресурсы, которыми он реально управляет;
    • Dev:
      • только чтение, без права менять прод-ресурсы.
  • Использовать защищённые ветки и code review:
    • чтобы доступ к прод-конфигурации контролировался на уровне Git.

Краткая формулировка:

  • Через RBAC:
    • создать ServiceAccount для CI/CD и ClusterRole/RoleBinding с правами на create/update/delete ресурсов деплоя в нужных namespaces;
    • создать read-only ClusterRole и RoleBinding для команд разработчиков, ограничив их verbs набором get/list/watch (и, при необходимости, logs);
    • опционально использовать GitOps (Argo CD/Flux), чтобы CI/CD управлял только Git, а применение манифестов делал контроллер с чётко определёнными правами.

Вопрос 38. Какие объекты RBAC использовать, чтобы система деплоя могла работать во множестве namespaces?

Таймкод: 00:49:37

Ответ собеседника: правильный. Указал на использование ClusterRole и ClusterRoleBinding для назначения прав, действующих сразу в нескольких namespaces, корректно отличив их от Role/RoleBinding, ограниченных конкретным namespace.

Правильный ответ:

Если системе деплоя (CI/CD, GitOps-контроллеру) нужно управлять ресурсами в нескольких namespaces, важно:

  • не дублировать одинаковые роли в каждом namespace;
  • централизованно описать набор прав;
  • привязать их к сервисному аккаунту так, чтобы они действовали во всех нужных namespaces.

Для этого используются:

  • ClusterRole — определяет права на уровне кластера (может охватывать ресурсы из разных namespaces и/или cluster-scoped ресурсы).
  • ClusterRoleBinding — привязывает ClusterRole к субъекту (ServiceAccount/пользователь/группа) на уровне всего кластера.

Базовый паттерн

  1. Создаём ClusterRole для системы деплоя

Пример: система деплоя должна уметь создавать/обновлять/удалять стандартные ресурсы приложений (Deployment, StatefulSet, Service, ConfigMap и т.д.) во множестве namespaces:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ci-deployer
rules:
- apiGroups: ["", "apps", "batch", "networking.k8s.io"]
resources:
- pods
- services
- configmaps
- secrets
- deployments
- statefulsets
- daemonsets
- jobs
- cronjobs
- ingresses
- replicasets
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

Эта ClusterRole:

  • сама по себе никому права не даёт;
  • описывает, что "кто-то" сможет полноценно оперировать этими ресурсами во всех namespaces, где будет привязка.
  1. Привязываем ClusterRole к ServiceAccount через ClusterRoleBinding

Чтобы система деплоя могла использовать эти права во всех целевых namespaces, создаём ClusterRoleBinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ci-deployer-binding
subjects:
- kind: ServiceAccount
name: ci-deployer-sa
namespace: ci-system
roleRef:
kind: ClusterRole
name: ci-deployer
apiGroup: rbac.authorization.k8s.io

Результат:

  • ServiceAccount ci-deployer-sa в namespace ci-system получает права, описанные в ClusterRole ci-deployer;
  • эти права действуют сразу во всех namespaces кластера (для перечисленных ресурсов).

Это удобно, когда:

  • один GitOps-контроллер (например, Argo CD) или CI-агент деплоит во множество namespaces;
  • не нужно создавать отдельные Role/RoleBinding в каждом namespace.

Ограничение прав по namespaces

Если нужно сузить набор namespaces (а не "во всём кластере"), есть варианты:

  • Использовать отдельные RoleBinding в нужных namespaces, ссылающиеся на ClusterRole:
    • ClusterRole общая;
    • RoleBinding в каждом namespace определяет, где именно этот ClusterRole "включён" для сервисного аккаунта.

Пример (ограничение только на app-namespace и app-staging):

# В namespace app-namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer-binding
namespace: app-namespace
subjects:
- kind: ServiceAccount
name: ci-deployer-sa
namespace: ci-system
roleRef:
kind: ClusterRole
name: ci-deployer
apiGroup: rbac.authorization.k8s.io

Тот же RoleBinding создаётся в других разрешённых namespaces.

Такой подход:

  • даёт централизованное описание прав (ClusterRole),
  • но разрешает их действие только в конкретных namespaces через RoleBinding,
  • что безопаснее, чем "во всём кластере без ограничений".

Краткий вывод:

  • Если системе деплоя нужны одинаковые права во многих namespaces:
    • описываем права в ClusterRole;
    • привязываем их к ServiceAccount через ClusterRoleBinding (для всех namespaces) или через набор RoleBinding (чтобы ограничить конкретные namespaces).
  • Role/RoleBinding — для одного namespace;
  • ClusterRole/ClusterRoleBinding — для прав, потенциально охватывающих несколько namespaces или весь кластер.

Вопрос 39. Как изолировать сетевой трафик между двумя namespace в Kubernetes, запретив взаимный доступ?

Таймкод: 00:50:07

Ответ собеседника: правильный. После подсказки указал на использование NetworkPolicy для описания сетевых правил и отметил, что их фактическое применение зависит от CNI-плагина.

Правильный ответ:

По умолчанию во многих Kubernetes-кластерах (в зависимости от CNI) трафик между Pod’ами разных namespace может быть разрешён. Чтобы изолировать два namespace друг от друга (и вообще контролировать L3/L4-трафик), используют:

  • NetworkPolicy — объект Kubernetes для декларативного описания сетевых правил;
  • CNI-плагин, который умеет эти политики применять (Calico, Cilium, Weave Net, etc.).

Ключевые идеи:

  1. NetworkPolicy — это "фильтр", а не firewall по умолчанию
  • Политики работают по принципу "разрешить явно, остальное запретить", но:
    • только для Pod’ов, подпадающих под podSelector/namespaceSelector в политиках;
    • только если CNI поддерживает NetworkPolicy.
  • Если для Pod’ов в namespace не существует ни одной policy, они обычно остаются "открытыми" (allow all), в зависимости от реализации.

Поэтому для реальной изоляции нужно:

  • Явно ввести политики, ограничивающие ingress/egress.
  1. Задача: два namespace, запретить взаимный доступ

Допустим:

  • namespace: team-a
  • namespace: team-b
  • Требование:
    • Pod’ы из team-a не могут ходить в team-b;
    • Pod’ы из team-b не могут ходить в team-a;
    • при этом внутри namespace трафик может быть разрешён (если нужно).

Пример решения:

Шаг 1: Включить "deny by default" для ingress/egress в каждом namespace.

Для namespace team-a:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-from-other-namespaces
namespace: team-a
spec:
podSelector: {} # применимо ко всем Pod'ам в namespace
ingress:
- from:
- podSelector: {} # разрешить трафик только от Pod'ов в этом же namespace
egress:
- to:
- podSelector: {} # разрешить исходящий трафик только внутри namespace

Для namespace team-b — аналогичная политика.

Семантика:

  • ingress:
    • разрешён только трафик от Pod’ов в том же namespace (так как namespaceSelector не указан, а podSelector: {} — локальный).
    • все обращения из других namespace блокируются.
  • egress:
    • исходящий трафик разрешен только к Pod’ам того же namespace;
    • обращения в другие namespace блокируются.

Это создаёт строгую изоляцию между team-a и team-b (и всеми другими namespace, если не заданы отдельные разрешения).

Шаг 2 (более гибкий вариант): Явно ограничить только меж-namespace трафик

Если нужно:

  • разрешить egress в интернет или к общим инфраструктурным сервисам (DNS, ingress, monitoring),
  • но запретить прямой трафик между team-a и team-b,

можно использовать namespaceSelector.

Пример: запретить ingress из других namespaces в team-a, но не трогать egress:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-cross-namespace-ingress
namespace: team-a
spec:
podSelector: {}
ingress:
- from:
- podSelector: {} # только pod'ы внутри того же namespace

Аналогично для team-b.

  1. Роль CNI-плагина

NetworkPolicy — это декларация. Чтобы она работала:

  • установлен CNI-плагин с поддержкой NetworkPolicy:
    • Calico, Cilium, Weave Net, Kube-router и т.п.;
  • если используется простейший CNI без поддержки policy (или встроенный "bridge" без enforcement), NetworkPolicy не будет применяться.

Проверка:

  • документация кластера/CNI;
  • тест: создать deny-all policy и проверить, что трафик действительно блокируется.
  1. Практические рекомендации
  • Стартовый шаблон для изоляции:

    • На каждый "tenant"/project namespace сразу вешать базовую NetworkPolicy:
      • deny ingress снаружи;
      • опционально — ограничить egress.
  • Для межсервисного трафика:

    • явно описывать разрешения через labelSelector и namespaceSelector.
  • Не полагаться на "случайное поведение" CNI:

    • всегда формализовать требования в policy.

Краткий ответ, который ожидается:

  • Использовать NetworkPolicy для описания ingress/egress-правил на уровне Pod/namespace.
  • На каждый из двух namespace повесить политики, разрешающие трафик только внутри своего namespace и блокирующие трафик из/в другие.
  • Убедиться, что выбранный CNI-плагин поддерживает и применяет NetworkPolicy.

Вопрос 40. С какими CNI-плагинами и service mesh решениями стоит уметь работать и как они используются на практике?

Таймкод: 00:51:32

Ответ собеседника: неполный. Упомянул Calico и Cilium, подтвердил знакомство с Istio; отметил использование Istio для blue/green и canary, но без технических деталей и примеров конфигурации, без раскрытия практических сценариев и особенностей.

Правильный ответ:

Для зрелой работы с Kubernetes-сетями и сервис-мешами важно:

  • понимать архитектуру и возможности CNI-плагинов;
  • уметь применять NetworkPolicy, IPAM, особенности маршрутизации;
  • понимать, когда и зачем нужен service mesh, и как он интегрируется с приложениями (в том числе на Go);
  • уметь минимально конфигурировать типовые сценарии: mTLS, canary, blue/green, observability.

Ниже — концентрированный разбор на примере Calico, Cilium и Istio.

CNI-плагины

  1. Calico

Calico — один из самых распространённых CNI-плагинов для production-кластеров.

Ключевые возможности:

  • L3-сеть без оверлея (BGP-пиринги с Underlay) или с VXLAN/IPIP-туннелями при необходимости.
  • Поддержка Kubernetes NetworkPolicy + собственные расширенные политики (GlobalNetworkPolicy).
  • Поддержка IPAM:
    • пулов адресов,
    • per-namespace / per-node аллокаций.
  • Масштабируемость и предсказуемость маршрутизации:
    • реальный routable IP для Pod’ов (в режимах без оверлея).

Практическое применение:

  • Изоляция трафика:
    • жесткие NetworkPolicy между namespace и приложениями.
  • Multi-tenant сценарии:
    • разделение трафика разных команд/продуктов.
  • Интеграция с "железной" сетью:
    • через BGP анонсировать подсети Pod’ов на физические маршрутизаторы.

Пример базовой политики на Calico (совместимой с k8s NetworkPolicy):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-cross-namespace
namespace: team-a
spec:
podSelector: {}
ingress:
- from:
- podSelector: {} # только внутри team-a
  1. Cilium

Cilium — CNI следующего поколения, основанный на eBPF.

Ключевые преимущества:

  • eBPF вместо iptables:
    • высокая производительность,
    • гибкость обработки трафика на уровне ядра.
  • Расширенные NetworkPolicy:
    • L3/L4/L7 (HTTP, gRPC-aware) политика;
    • можно ограничивать доступ не только по IP/порту, но и по HTTP-путям, gRPC-методам.
  • Встроенные функции:
    • kube-proxy replacement (балансировка без iptables),
    • Hubble — observability (traffic flow, service map),
    • интеграция с service mesh-подобными возможностями без sidecar (cluster mesh, mTLS и др. в новых версиях).

Практическое применение:

  • Нагрузочные и безопасные кластера:
    • когда важна наблюдаемость и тонкий контроль трафика.
  • Service-aware политики:
    • ограничить, например:
      • что Pod’ы могут ходить только на конкретные HTTP API внутри кластера.

Пример (идея): запретить доступ к /admin endpoint, разрешая /api/v1 — в Cilium это можно делать через L7 policy, в отличие от обычного CNI.

Выбор CNI: кратко

  • Calico:
    • стабильно, предсказуемо, отлично для "классической" сетевой модели.
  • Cilium:
    • для продвинутых сценариев, высокой наблюдаемости, L7-политик, перфоманса.
  • Оба:
    • поддерживают NetworkPolicy;
    • интегрируются с современными Kubernetes-стеками.

Service Mesh (Istio как типичный пример)

Service mesh решает не задачу "подключить Pod к сети", а задачи уровня взаимодействия сервисов:

  • mTLS между сервисами;
  • canary/blue-green/traffic shifting;
  • ретраи, timeouts, circuit breaking;
  • централизованная observability (metrics, traces, logs);
  • политика доступа на уровне сервиса (RBAC, AuthorizationPolicy).
  1. Istio: архитектура и ключевые элементы

Основные компоненты:

  • Istiod:
    • control-plane:
      • конфигурация,
      • discovery,
      • распределение политик и маршрутов.
  • Sidecar proxy (Envoy) в каждом Pod:
    • перехватывает входящий и исходящий трафик;
    • применяет правила маршрутизации, mTLS, ретраи и т.п.

В современных режимах (Ambient mesh) Istio движется к снижению зависимости от sidecar, но классическая модель остаётся базовой для понимания.

  1. Типовые сценарии использования Istio

a) mTLS "по умолчанию"

  • Прозрачное шифрование трафика между сервисами.
  • Минимум конфигов со стороны приложения (Go-код не знает о TLS внутри кластера).

Пример включения STRICT mTLS в namespace:

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: my-namespace
spec:
mtls:
mode: STRICT

b) Canary / blue-green / traffic shifting

Разделение трафика по версиям сервиса без изменения клиента:

  1. Описываем VirtualService и DestinationRule.

Пример: 90% трафика на v1, 10% на v2:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: my-app
namespace: my-namespace
spec:
host: my-app
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: my-app
namespace: my-namespace
spec:
hosts:
- my-app
http:
- route:
- destination:
host: my-app
subset: v1
weight: 90
- destination:
host: my-app
subset: v2
weight: 10

Применение:

  • Go-сервис-клиент просто обращается к my-app.
  • Istio на уровне sidecar/Envoy делит трафик согласно правилам.

c) Политики доступа на уровне сервиса

Можно задать, какой сервис имеет право звать какой:

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-from-frontend
namespace: my-namespace
spec:
selector:
matchLabels:
app: my-app
rules:
- from:
- source:
principals: ["cluster.local/ns/my-namespace/sa/frontend-sa"]
  1. Связь с Go-сервисами

Правильная интеграция:

  • Приложение остаётся максимально "тупым" сетево:
    • обычный HTTP/gRPC без внутренней логики маршрутизации;
    • timeouts/ретраи всё равно на уровне приложения нужны, но mesh может добавить политики.
  • Istio:
    • добавляет observability:
      • метрики (Prometheus),
      • трассировки (Jaeger/Tempo/OpenTelemetry),
      • логирование на уровне sidecar.
  • Go-код:
    • хорошо сочетается с mesh:
      • легко добавить correlation-id,
      • экспортировать метрики,
      • соблюдать timeouts, которые mesh дополняет retry/policy-механиками.

Критичные моменты, которые важно понимать и уметь объяснить:

  • CNI решает "как Pod’ы общаются на L3/L4".
  • NetworkPolicy (на базе CNI) — "кто с кем может общаться".
  • Service Mesh (Istio, Linkerd, Consul, Kuma):
    • решает L7-задачи:
      • маршрутизация, canary, mTLS, авторизация, наблюдаемость.
  • Cilium частично размывает границу, предлагая L7-политики и mesh-функции на базе eBPF без sidecar — это тренд, который стоит отслеживать.

Хороший ответ на интервью:

  • Конкретно называет:
    • Calico — как основной CNI с поддержкой NetworkPolicy и BGP.
    • Cilium — как eBPF-решение с L3–L7 политиками и observability.
    • Istio — как service mesh для mTLS, traffic shifting, canary/blue-green, централизованной сетевой политики между сервисами.
  • Показывает понимание, что:
    • CNI и mesh — разные уровни;
    • mesh не заменяет CNI, а работает поверх него (или рядом, интегрируясь с ним).