Kubernetes: Основы и архитектура


Введение в Kubernetes

Что такое Kubernetes и зачем он нужен

Kubernetes — это open-source платформа для оркестрации контейнеризированных приложений, разработанная Google и поддерживаемая Cloud Native Computing Foundation (CNCF). В эпоху микросервисной архитектуры, когда приложение состоит из десятков или сотен сервисов, запущенных в контейнерах, ручное управление этими контейнерами становится невозможным.

Kubernetes решает эту проблему, автоматизируя:

  • Развертывание (Deployment): описание желаемого состояния приложения, Kubernetes автоматически создает и управляет контейнерами
  • Масштабирование (Scaling): автоматическое увеличение или уменьшение количества реплик приложения в зависимости от нагрузки
  • Управление ресурсами: распределение CPU и памяти между контейнерами на доступных узлах
  • Self-healing: автоматический перезапуск упавших контейнеров и замена неработающих узлов
  • Service Discovery: автоматическое обнаружение сервисов в сети без необходимости жесткой конфигурации IP-адресов
  • Load Balancing: распределение трафика между репликами одного приложения
  • Rolling Updates: плавное обновление приложения без простоев

Kubernetes позволяет объявить желаемое состояние приложения, а платформа автоматически приводит текущее состояние к желаемому и поддерживает его в таком состоянии. Это декларативный подход, где описывается «что» нужно достичь, а не «как» это сделать.


Архитектура и компоненты

Control Plane: мозг кластера

Kubernetes кластер состоит из двух основных типов компонентов:

Control Plane (Master Node) — управляет кластером и принимает решения о распределении workloads.

API Server

  • Центральная точка управления Kubernetes
  • Все операции (создание Pod'ов, Services, и т.д.) идут через REST API
  • Валидирует и обрабатывает все запросы к кластеру
  • Для Java разработчиков: это то, с чем взаимодействует kubectl и все client-библиотеки (Fabric8, Spring Cloud Kubernetes)

etcd

  • Распределенное key-value хранилище состояния всего кластера
  • Хранит конфигурацию всех объектов Kubernetes (Pod'ы, Services, Deployments, и т.д.)
  • Критически важно для целостности данных — при потере etcd теряется вся информация о кластере
  • На production: использовать несколько реплик etcd для высокой доступности

Scheduler

  • Компонент, решающий на какой Worker Node разместить новый Pod
  • Анализирует требования Pod'а (resource requests), constraints (affinity, taints), и текущее состояние узлов
  • Выбирает оптимальный узел для размещения
  • Для Java разработчиков важно: понимать как работает scheduling при установке resource requests/limits

Controller Manager

  • Запускает множество control loops, каждый отвечает за определенный аспект:

    • Deployment Controller — отслеживает Deployments и создает/удаляет ReplicaSets
    • ReplicaSet Controller — поддерживает нужное количество Pod'ов
    • Service Controller — управляет Service'ами
    • Node Controller — отслеживает состояние узлов

Каждый controller постоянно сравнивает текущее состояние (what is) с желаемым состоянием (what should be) и предпринимает действия для приведения их в соответствие. Это основа reconciliation loop.

Worker Nodes: рабочие узлы

Worker Node — это машина (VM или физический сервер), на которой запускаются контейнеризированные приложения.

Kubelet

  • Агент, запущенный на каждом узле
  • Следит за Pod'ами, назначенными API Server'ом этому узлу
  • Запускает и останавливает контейнеры через container runtime
  • Отправляет статус Pod'ов обратно в API Server
  • Выполняет health checks (проверяет liveness и readiness probes)

Container Runtime

  • Программное обеспечение, ответственное за запуск контейнеров
  • Классический выбор: Docker
  • Современные альтернативы: containerd, CRI-O (более лёгкие и с лучшей интеграцией с Kubernetes)

kube-proxy

  • Сетевой компонент на каждом узле
  • Реализует Service networking
  • Настраивает iptables/IPVS правила для маршрутизации трафика к Pod'ам
  • Для Java разработчиков: отвечает за то, что ваше приложение может общаться с другими Service'ами по DNS-имени и порту

Основные объекты и ресурсы

Pod: базовая единица развертывания

Pod — минимальная развертываемая единица в Kubernetes. Это не контейнер, а обертка вокруг одного или нескольких контейнеров.

Хотя обычно в Pod'е только один контейнер (best practice), Kubernetes был спроектирован с поддержкой multi-container Pod'ов. Основной причиной существования Pod'а является предоставление shared resources:

  • Network namespace: все контейнеры в Pod'е разделяют один IP-адрес и network interface. Они могут общаться через localhost
  • Storage volumes: контейнеры в Pod'е могут разделять общие volumes для обмена данными

Pod Lifecycle

Pending → ContainerInit → Running → Succeeded/Failed

Если контейнер падает, Kubelet перезапускает его согласно restart policy:

  • Always (по умолчанию): всегда перезапускать контейнер
  • OnFailure: перезапускать только при non-zero exit code
  • Never: не перезапускать

Deployment: управление Pod'ами

Deployment — это декларативное описание желаемого состояния приложения. Он управляет ReplicaSet'ами, которые в свою очередь управляют Pod'ами.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: java-app
  template:
    metadata:
      labels:
        app: java-app
    spec:
      containers:

      - name: app
        image: my-java-app:1.0
        ports:

        - containerPort: 8080
        resources:
          requests:
            cpu: 500m
            memory: 512Mi
          limits:
            cpu: 1000m
            memory: 1024Mi

Deployment автоматически:

  • Создает нужное количество Pod'ов (replicas)
  • Заменяет поврежденные Pod'ы
  • Выполняет rolling updates при изменении image версии
  • Позволяет откатиться на предыдущую версию если возникли проблемы

Service: сетевая абстракция

Service предоставляет stable DNS имя и virtual IP для доступа к Pod'ам. Без Service'ов, Pod'ы были бы недоступны для других приложений, так как у них нестабильные IP адреса.

apiVersion: v1
kind: Service
metadata:
  name: java-app-service
spec:
  type: ClusterIP
  selector:
    app: java-app
  ports:

  - protocol: TCP
    port: 80
    targetPort: 8080

Типы Service'ов:

  • ClusterIP (по умолчанию): доступен только внутри кластера
  • NodePort: доступен через порт на каждом узле
  • LoadBalancer: облачный load balancer для external access
  • ExternalName: маппит на external DNS имя

StatefulSet: для приложений с состоянием

StatefulSet используется для приложений, требующих stable network identity и persistent storage:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:

      - name: postgres
        image: postgres:13
        ports:

        - containerPort: 5432
        volumeMounts:

        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:

  - metadata:
      name: postgres-storage
    spec:
      accessModes:

      - ReadWriteOnce
      resources:
        requests:
          storage: 10Gi

StatefulSet обеспечивает:

  • Stable network identity (pod-0, pod-1, и т.д.)
  • Ordered deployment и scaling
  • Stable persistent storage

Namespaces: логическая изоляция

Namespace в Kubernetes — это виртуальный кластер внутри одного физического кластера. Используется для:

  • Multi-tenancy: каждый tenant получает свой namespace
  • Окружения: dev, staging, prod как отдельные namespaces
  • Teams: каждой team свой namespace
kubectl create namespace production
kubectl apply -f deployment.yaml -n production

Встроенные namespaces:

  • default: namespace по умолчанию
  • kube-system: системные компоненты Kubernetes
  • kube-public: публичные ресурсы
  • kube-node-lease: управление node heartbeats

Labels и Selectors: организация ресурсов

Labels — это key-value метки, прикрепляемые к ресурсам для организации и запросов.

metadata:
  labels:
    app: java-app
    environment: production
    version: "1.0"
    team: backend

Service использует selector для выбора Pod'ов:

spec:
  selector:
    app: java-app
    environment: production

Ключевые концепции для Java разработчиков

Resource Requests и Limits

Всегда устанавливайте resource requests и limits:

resources:
  requests:
    cpu: 500m        # Гарантированно получит
    memory: 512Mi
  limits:
    cpu: 1000m       # Максимум не превышает
    memory: 1024Mi
  • Requests: минимум ресурсов, гарантированный Kubernetes при scheduling
  • Limits: максимум ресурсов, контейнер будет throttled или killed при превышении

Для Java приложений:

  • Heap size (-Xmx) должен быть < 75% от memory limit
  • CPU requests должны отражать типичную нагрузку, limits — peak нагрузку

Health Checks

livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  • Liveness: контейнер жив и может выполнять работу?
  • Readiness: Pod готов к приему трафика?

Без правильных health checks, Kubernetes может направить трафик на неготовый Pod или не перезапустит упавший контейнер.

Graceful Shutdown

При удалении Pod'а Kubernetes отправляет SIGTERM, приложение имеет terminationGracePeriodSeconds (по умолчанию 30) для graceful shutdown:

spec:
  terminationGracePeriodSeconds: 60

Spring Boot автоматически обрабатывает SIGTERM для graceful shutdown.


Заключение

Понимание архитектуры Kubernetes, компонентов Control Plane, Worker Nodes, и основных объектов (Pod, Deployment, Service, StatefulSet) является фундаментом для эффективной работы с платформой. Для Java разработчиков критически важно понимать, как resource requests/limits, health checks и graceful shutdown влияют на поведение приложений в production окружении.

Kubernetes: Конфигурация и управление ресурсами


Введение

Эффективная конфигурация и управление ресурсами является основой для стабильной и масштабируемой работы приложений в Kubernetes. Этот раздел охватывает конфигурационные инструменты, управление ресурсами, health checks, автомасштабирование и оптимизацию Java приложений.


ConfigMap и Secrets: управление конфигурацией

ConfigMap: public конфигурация

ConfigMap служит основным инструментом для управления конфигурационными данными в Kubernetes. Этот объект хранит конфигурацию в виде key-value пар в plain text формате.

ConfigMap может быть использован несколькими способами:

  • Environment variables — данные из ConfigMap могут быть внедрены как переменные окружения в контейнер
  • Command-line аргументы — значения могут быть переданы как аргументы при запуске приложения
  • Volume-mounted файлы — ConfigMap может быть смонтирован как директория с файлами внутри Pod'а
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: production
data:
  LOG_LEVEL: "INFO"
  DATABASE_POOL_SIZE: "20"
  application.properties: |
    server.port=8080
    spring.application.name=my-app

Secrets: чувствительные данные

Secrets предоставляют механизм для безопасного хранения чувствительных данных, таких как пароли, токены доступа, SSH ключи.

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: YWRtaW4=  # base64-encoded
  password: cGFzc3dvcmQxMjM=

Kubernetes поддерживает несколько типов Secrets:

  • Opaque (по умолчанию) — использует base64 кодирование
  • kubernetes.io/service-account-token — автоматически создаваемые токены сервис-аккаунтов
  • kubernetes.io/dockercfg — учетные данные Docker registry

Spring Cloud Kubernetes интеграция

Spring Cloud Kubernetes предоставляет глубокую интеграцию Spring приложений с Kubernetes:

  • Автоматическое обнаружение Service'ов — вместо ручного конфигурирования URL'ов других микросервисов, Spring Cloud Kubernetes автоматически обнаруживает доступные сервисы в кластере
  • Загрузка конфигурации — ConfigMap и Secrets автоматически подгружаются как PropertySource в Spring Environment
  • Hot-reload конфигурации — при изменении ConfigMap, Spring Cloud Kubernetes может автоматически перезагрузить изменившиеся свойства без перезапуска контейнера

Resource Requests и Limits: управление ресурсами

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

Requests представляют собой минимальное гарантированное количество ресурсов, которые контейнер получит при запуске. Scheduler использует эти значения для решения, на какой Node разместить Pod.

Limits задают максимальное количество ресурсов, которое может использовать контейнер. При превышении CPU limit применяется throttling, при превышении memory limit контейнер получит OOMKilled сигнал и будет перезапущен.

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 1000m
    memory: 1024Mi

Quality of Service (QoS) классы

Правильное установление requests и limits влияет на QoS класс Pod'а, что влияет на порядок eviction при нехватке ресурсов:

  • Guaranteed — requests равны limits (самый высокий приоритет, никогда не будет удален)
  • Burstable — requests задаются, но меньше limits
  • BestEffort — ни requests ни limits не заданы (самый низкий приоритет, first to evict)

Для production приложений используйте Guaranteed или Burstable классы.

Resource Quotas и LimitRanges

Для multi-tenant окружений используйте Resource Quotas для контроля total usage в namespace:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: team-a
spec:
  hard:
    requests.cpu: "10"
    requests.memory: "20Gi"
    limits.cpu: "20"
    limits.memory: "40Gi"
    pods: "100"

LimitRanges устанавливают default и максимальные values для отдельных контейнеров:

apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
spec:
  limits:

  - max:
      memory: "1Gi"
      cpu: "500m"
    default:
      memory: "512Mi"
      cpu: "250m"
    type: Container

Health Checks: Liveness, Readiness, Startup Probes

Liveness Probe

Определяет, жив ли контейнер и может ли он работать. Если liveness probe выходит в failed статус, Kubernetes перезапускает контейнер.

Readiness Probe

Определяет, готов ли Pod принимать входящий трафик. Если readiness probe отказывает, Service исключает этот Pod из endpoint'ов.

Startup Probe

Задерживает запуск liveness и readiness проверок для slow-starting приложений, особенно Java приложений с долгим warm-up периодом.

startupProbe:
  httpGet:
    path: /health/startup
    port: 8080
  failureThreshold: 30
  periodSeconds: 10
  
livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5

Для Java приложений используйте Spring Boot Actuator /actuator/health endpoints.


Horizontal Pod Autoscaler (HPA)

HPA автоматически масштабирует количество Pod'ов на основе метрик.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: java-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: java-app
  minReplicas: 2
  maxReplicas: 10
  metrics:

  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:

      - type: Percent
        value: 50
        periodSeconds: 15
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:

      - type: Percent
        value: 100
        periodSeconds: 15

HPA требует Metrics Server для сбора метрик о ресурсах (CPU, память).

Custom Metrics

Для более сложных сценариев используйте custom metrics (например, масштабирование по количеству сообщений в очереди):

metrics:

- type: Pods
  pods:
    metric:
      name: queue_length
    target:
      type: AverageValue
      averageValue: "100"

Vertical Pod Autoscaler (VPA)

VPA рекомендует и автоматически обновляет resource requests/limits на основе actual usage:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: java-app-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: java-app
  updatePolicy:
    updateMode: "Auto"  # Auto, Recreate, Initial, Off

VPA полезна для определения оптимальных resource requests, особенно на начальных этапах развития приложения.


Init Containers и Lifecycle Hooks

Init Containers

Init containers запускаются перед основными контейнерами, полезны для инициализации:

spec:
  initContainers:

  - name: migration
    image: my-app-migration:latest
    command: ["./migrate.sh"]
  containers:

  - name: app
    image: my-app:latest

Graceful Shutdown и Pod Disruption Budgets

Graceful Shutdown

При удалении Pod'а Kubernetes отправляет SIGTERM, приложение имеет terminationGracePeriodSeconds (по умолчанию 30) для graceful shutdown:

spec:
  terminationGracePeriodSeconds: 60
  containers:

  - name: app
    image: my-app:latest

Spring Boot автоматически обрабатывает SIGTERM для graceful shutdown и drain incoming requests.

Pod Disruption Budgets (PDB)

PDB гарантирует минимальное количество доступных Pod'ов при планируемых операциях (обновления узлов, масштабирование):

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: java-app-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: java-app

Java оптимизация в Kubernetes

Container Awareness

Java 10+ автоматически определяет container limits благодаря UseContainerSupport флагу. Установите:

-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0

Это предотвращает OOM ошибки, которые могут возникнуть, если JVM видит всю host memory вместо container limits.

Startup Optimization

Для быстрого scaling используйте:

  • Class Data Sharing (CDS) — уменьшает startup time за счёт предкомпилированных classfiles
  • GraalVM native images — кардинально уменьшают время запуска и потребление памяти
  • Quarkus framework — специально оптимизирован для Kubernetes

Heap Size Configuration

-Xms512m  # Initial heap
-Xmx768m  # Maximum heap (должен быть < 75% от memory limit)

Для Java приложений с memory limit 1024Mi установите -Xmx768m.


Quarkus и GraalVM для облачных приложений

Quarkus Framework

Quarkus специально разработан для облачных native приложений в Kubernetes:

  • Minimal startup time — контейнеры стартуют за 0.1-0.2 секунд
  • Low memory footprint — 50-100 MB для простых приложений
  • Live reload — для development

Пример Quarkus приложения:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: quarkus-app
spec:
  replicas: 3
  template:
    spec:
      containers:

      - name: app
        image: my-quarkus-app:latest
        resources:
          requests:
            cpu: 100m
            memory: 64Mi
          limits:
            cpu: 250m
            memory: 128Mi

Заметьте, как низкие лимиты памяти в сравнении с традиционными Java приложениями!

GraalVM Native Images

GraalVM native images предоставляют сумасшедше быстрый startup:

FROM ghcr.io/graalvm/native-image:latest as builder
COPY . /app
WORKDIR /app
RUN native-image -jar app.jar

FROM frolvlad/alpine-glibc
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]

Environment Variables и конфигурация

Способы передачи переменных окружения

env:

- name: LOG_LEVEL
  value: "DEBUG"
- name: DATABASE_URL
  valueFrom:
    configMapKeyRef:
      name: app-config
      key: database.url
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: app-secrets
      key: password

Массовая загрузка конфигурации

envFrom:

- configMapRef:
    name: app-config
- secretRef:
    name: app-secrets

Заключение

Правильная конфигурация и управление ресурсами критически важны для production приложений в Kubernetes. От правильного установления resource requests/limits до реализации proper health checks и graceful shutdown, каждый компонент влияет на стабильность и производительность системы.

Для Java разработчиков особенно важно понимать, как контейнер-awareness, Quarkus/GraalVM и startup optimization влияют на поведение приложений в облачной среде.

Kubernetes: Безопасность


Введение

Безопасность в Kubernetes требует многоуровневого подхода. RBAC контролирует доступ на уровне API, Network Policies обеспечивают сетевую изоляцию, Pod Security Standards определяют базовую конфигурацию безопасности, а правильное управление Secrets защищает чувствительные данные.


Role-Based Access Control (RBAC)

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

RBAC контролирует доступ к Kubernetes API на основе ролей. Это механизм авторизации, который определяет, какие действия могут выполнять пользователи или приложения в кластере.

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

Role и ClusterRole — объекты, которые определяют набор разрешений (permissions). Они содержат правила (rules), каждое из которых указывает:

  • verbs (действия): get, list, create, update, delete, patch, watch и другие
  • resources (ресурсы): pods, services, deployments, configmaps, secrets и т.д.
  • resourceNames (опционально): конкретные имена ресурсов

RoleBinding и ClusterRoleBinding — объекты, которые связывают Role или ClusterRole с subjects (получатели разрешений).

Role ограничен одним namespace, ClusterRole действует cluster-wide.

Практическое применение

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: default
rules:

- apiGroups: [""]
  resources: ["pods", "pods/logs"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["pods/exec"]
  verbs: ["create"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-reader-binding
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: pod-reader
subjects:

- kind: ServiceAccount
  name: my-app
  namespace: default

Principle of Least Privilege (PoLP)

Ключевой принцип безопасности — предоставлять минимально необходимые разрешения.

Практические рекомендации:

  • Избегайте cluster-admin — используйте её только в исключительных случаях и для ограниченного числа администраторов
  • Используйте множество мелких ролей вместо одной крупной
  • Специфицируйте ресурсы и namespace — ограничьте доступ к определённым namespace'ам или конкретным ресурсам

Network Policies

Концепция Network Policies

Network Policies функционируют как firewall rules на уровне Pod'ов. Они контролируют трафик, входящий в Pod'ы (ingress) и выходящий из Pod'ов (egress), на основе pod selectors, namespace selectors и IP blocks.

По умолчанию в Kubernetes все Pod'ы могут взаимодействовать друг с другом без ограничений. Network Policies позволяют реализовать модель "deny-by-default".

Примеры Network Policies

Deny all ingress traffic:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-ingress
spec:
  podSelector: {}
  policyTypes:

  - Ingress

Allow traffic from specific namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-frontend
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes:

  - Ingress
  ingress:

  - from:

    - podSelector:
        matchLabels:
          tier: frontend
    - namespaceSelector:
        matchLabels:
          name: production
    ports:

    - protocol: TCP
      port: 8080

Network Policy требования

Network Policies требуют, чтобы в кластере был установлен network plugin, поддерживающий Network Policies (например, Calico, Weave, Cilium).


Pod Security Standards

Pod Security Standards определяют три уровня безопасности для Pod'ов.

1. Privileged

Наименее ограничивающий уровень, позволяет:

  • Привилегированные контейнеры
  • Host networking, Host PID, Host IPC
  • Запуск от root пользователя
  • Любые Linux capabilities

Используйте только для системных компонентов.

2. Baseline

Промежуточный уровень, запрещает:

  • Привилегированные контейнеры
  • hostNetwork, hostPID, hostIPC

Подходит для большинства приложений.

3. Restricted

Наиболее ограничивающий уровень:

  • Запрещает привилегированные контейнеры
  • Запрещает запуск от root
  • Удаляет все Linux capabilities
  • Запрещает allowPrivilegeEscalation

Идеален для multi-tenant окружений.

Enforcement

Pod Security Standards могут быть enforced через Pod Security Admission controller (встроен в Kubernetes 1.23+):

apiVersion: v1
kind: Namespace
metadata:
  name: secure-namespace
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Service Accounts и Security Context

Service Accounts

ServiceAccount — это идентичность для Pod'ов при взаимодействии с Kubernetes API Server.

Каждый ServiceAccount имеет associated authentication token, который монтируется в Pod как volume в /var/run/secrets/kubernetes.io/serviceaccount/.

Security Context

Security context определяет привилегии и параметры управления доступом для Pod или Container.

spec:
  securityContext:
    runAsUser: 1000
    runAsNonRoot: true
    fsGroup: 2000
  containers:

  - name: app
    image: my-app:latest
    securityContext:
      capabilities:
        drop:

        - ALL
        add:

        - NET_BIND_SERVICE
      allowPrivilegeEscalation: false

Основные параметры:

  • runAsUser — UID пользователя, от которого запускается процесс
  • runAsNonRoot — boolean флаг для запрета запуска от root
  • fsGroup — GID группы для доступа к файловой системе
  • capabilities — Linux capabilities
  • allowPrivilegeEscalation — может ли процесс получить больше привилегий

Secrets Management Best Practices

Проблемы с дефолтным хранением Secrets

По умолчанию Kubernetes Secrets хранятся в etcd в base64-encoded формате, который легко может быть декодирован. Это создаёт серьёзный риск безопасности.

Encryption at Rest

Включите encryption at rest для Secrets в etcd. Kubernetes поддерживает несколько encryption providers:

  • aescbc — AES-CBC encryption
  • aesgcm — AES-GCM encryption
  • kms — Key Management Service (AWS KMS, GCP KMS)

External Secret Managers

Вместо хранения Secrets в Kubernetes, используйте external secret managers:

  • HashiCorp Vault — популярное решение с encryption, audit logging, и policy enforcement
  • AWS Secrets Manager — управляемый сервис Amazon
  • Google Secret Manager — аналогичный сервис от Google Cloud
  • Azure Key Vault — сервис от Microsoft

Access Control через RBAC

Ограничивайте доступ к Secrets через RBAC:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
rules:

- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["my-app-secret"]
  verbs: ["get"]

Image Security

Image Scanning

Сканируйте образы на уязвимости перед развертыванием в production.

Популярные инструменты:

  • Trivy — быстрый сканер от Aqua Security
  • Clair — компонент Docker Distribution для хранения информации об уязвимостях
  • Anchore — комплексное решение для управления образами

Private Registries

Используйте private registries вместо публичных Docker Hub для:

  • Большего контроля над образами
  • Сканирования перед использованием
  • Лучшей производительности
  • Соответствия security compliance
imagePullSecrets:

- name: regcred

Image Signing

Image signing (Notary, Cosign) обеспечивает integrity и authenticity образов:

  • Образы подписываются приватным ключом
  • Кластер может проверить подпись перед использованием
  • Предотвращает использование поддельных образов

RBAC Best Practices

Регулярный аудит

kubectl get rolebindings --all-namespaces
kubectl get clusterrolebindings

Policy Management

Используйте policy-as-code инструменты (OPA, Kyverno) для автоматизации enforcement RBAC policies.

Monitoring доступа

Включайте audit logging для отслеживания всех запросов к API и анализируйте логи на предмет подозрительной активности.


Заключение

Безопасность в Kubernetes требует систематического подхода. RBAC контролирует доступ на уровне API, Network Policies обеспечивают сетевую изоляцию, Pod Security Standards определяют базовую конфигурацию, а правильное управление Secrets и Image Security защищает приложения от компрометации.

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

  • Применяйте Principle of Least Privilege
  • Используйте Network Policies для изоляции
  • Enforce Pod Security Standards
  • Храните Secrets в external managers
  • Сканируйте образы перед развертыванием
  • Регулярно аудируйте доступ и разрешения

Kubernetes: Развертывание и CI/CD


Введение

Эффективное управление жизненным циклом приложений требует правильного выбора стратегий развертывания и автоматизации CI/CD процессов. Этот раздел охватывает deployment strategies, инструменты пакетирования, GitOps парадигму и управление релизами.


Deployment Strategies

Rolling Updates

Rolling update — наиболее распространённая стратегия развертывания новых версий с обеспечением zero-downtime.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-app
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    # pod template

Параметры:

  • maxSurge — максимальное количество дополнительных Pod'ов во время update
  • maxUnavailable — максимальное количество недоступных Pod'ов

Управление процессом:

kubectl rollout status deployment/java-app
kubectl rollout history deployment/java-app
kubectl rollout undo deployment/java-app

Blue-Green Deployment

Blue-Green deployment поддерживает две идентичные среды: blue (текущая) и green (новая версия).

Новая версия полностью развертывается в green и тестируется. После успеха трафик переключается через изменение Service selector.

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

  • Мгновенный rollback
  • Полное тестирование перед переключением

Недостатки:

  • Требует удвоенных ресурсов

Canary Deployment

Canary deployment постепенно вводит новую версию для небольшой части пользователей.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: java-app
spec:
  hosts:

  - java-app
  http:

  - match:

    - uri:
        prefix: /
    route:

    - destination:
        host: java-app-v1
      weight: 90
    - destination:
        host: java-app-v2
      weight: 10

Процесс:

  1. 5-10% трафика на новую версию
  2. Мониторинг метрик
  3. Постепенное увеличение доли трафика
  4. Rollback если возникли проблемы

Helm Charts: пакетирование приложений

Структура Helm Chart

my-app-chart/
├── Chart.yaml
├── values.yaml
├── values-prod.yaml
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    └── configmap.yaml

Chart.yaml — метаданные диаграммы:

apiVersion: v2
name: my-app
description: Java backend application
version: 1.0.0
appVersion: "1.0"

values.yaml — конфигурируемые значения:

replicaCount: 3

image:
  repository: my-registry.com/my-app
  tag: "1.0"
  pullPolicy: IfNotPresent

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 1000m
    memory: 1024Mi

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

Helm Commands

# Создать package
helm package my-app-chart/

# Установить chart
helm install my-release my-app-chart/

# Обновить deployment
helm upgrade my-release my-app-chart/

# Просмотреть history
helm history my-release

# Откатиться
helm rollback my-release 1

GitOps и декларативная конфигурация

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

  1. Декларативность — состояние системы описано декларативно в Git
  2. Version Control — все изменения отслеживаются и имеют историю
  3. Reviewed & Approved — изменения вносятся через pull requests
  4. Automatic Sync — система автоматически приводит кластер в соответствие с Git

ArgoCD: Continuous Deployment

ArgoCD мониторит Git репозиторий и автоматически синхронизирует состояние кластера.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: java-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/k8s-manifests
    targetRevision: HEAD
    path: java-app
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

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

  • Audit trail всех изменений
  • Easy rollbacks через git revert
  • Disaster recovery из Git
  • Collaboration через Git

CI/CD Pipelines

Jenkins + Kubernetes

Jenkins может использовать Kubernetes Pod'ы как agents:

pipeline {
  agent {
    kubernetes {
      yaml '''
        apiVersion: v1
        kind: Pod
        spec:
          containers:

          - name: docker
            image: docker:latest
            command:

            - cat
            tty: true
            volumeMounts:

            - name: docker-sock
              mountPath: /var/run/docker.sock
          volumes:

          - name: docker-sock
            hostPath:
              path: /var/run/docker.sock
      '''
    }
  }
  stages {
    stage('Build') {
      steps {
        container('docker') {
          sh 'docker build -t my-app:${BUILD_NUMBER} .'
          sh 'docker push registry.com/my-app:${BUILD_NUMBER}'
        }
      }
    }
    stage('Deploy') {
      steps {
        sh 'kubectl set image deployment/java-app app=my-app:${BUILD_NUMBER}'
      }
    }
  }
}

GitLab CI/CD

GitLab предоставляет встроенный CI/CD, оптимизированный для Kubernetes:

stages:

  - build
  - test
  - deploy

build_image:
  stage: build
  image: docker:latest
  script:

    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

deploy_to_k8s:
  stage: deploy
  image: bitnami/kubectl:latest
  script:

    - kubectl set image deployment/java-app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:

    - main

Operators и Custom Resources

Kubernetes Operators

Operators автоматизируют управление сложными приложениями через Custom Resource Definitions (CRD).

apiVersion: database.example.com/v1
kind: PostgreSQL
metadata:
  name: my-database
spec:
  version: "13"
  replicas: 3
  storage: 100Gi

Operator автоматически:

  • Создает и конфигурирует инстансы
  • Управляет резервными копиями
  • Выполняет обновления и масштабирование

CRD (Custom Resource Definition)

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: postgresdatabases.database.example.com
spec:
  names:
    kind: PostgreSQL
    plural: postgresdatabases
  scope: Namespaced
  versions:

  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              version:
                type: string
              replicas:
                type: integer
              storage:
                type: string

Версионирование и управление релизами

Semantic Versioning

Используйте semantic versioning (MAJOR.MINOR.PATCH):

myapp:2.3.1
  ↓   ↓  ↓
  │   │  └─ PATCH: исправления ошибок
  │   └───── MINOR: новые функции, обратно совместимые
  └───────── MAJOR: breaking changes

Tagging Стратегия

Используйте несколько тегов для Docker образов:

docker tag myapp:latest myapp:stable
docker tag myapp:latest myapp:v2.3.1
docker tag myapp:latest myapp:prod-ready
  • latest — самая свежая версия
  • stable — стабильная production-ready версия
  • version-specific tags — например v2.3.1

Helm Version Management

# История releases
helm history my-app

# Откат на конкретную версию
helm rollback my-app 3

# Просмотр версий Chart'а
helm search repo my-app --versions

Progressive Delivery с Argo Rollouts

Argo Rollouts расширяет Kubernetes deployment capabilities для advanced deployment strategies:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: java-app
spec:
  replicas: 3
  strategy:
    canary:
      steps:

      - setWeight: 10
      - pause: {duration: 5m}
      - setWeight: 50
      - pause: {duration: 5m}
  selector:
    matchLabels:
      app: java-app
  template:
    # pod template

Artifact и Container Registry Management

Private Container Registry

Используйте private registry вместо Docker Hub:

imagePullSecrets:

- name: registry-credentials

containers:

- name: app
  image: my-registry.com/my-app:1.0

Image Tagging Strategy

# Build и tag образ
docker build -t myapp:latest .
docker tag myapp:latest myapp:${GIT_COMMIT_SHA:0:8}
docker tag myapp:latest myapp:${VERSION}

# Push все tags
docker push myapp:latest
docker push myapp:${GIT_COMMIT_SHA:0:8}
docker push myapp:${VERSION}

Заключение

Современные deployment strategies и CI/CD инструменты предоставляют мощный набор возможностей для управления приложениями. От rolling updates для большинства сценариев до canary deployments для high-risk изменений, выбор правильной стратегии критичен для стабильной работы production систем.

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

  • Выбирайте deployment strategy в зависимости от требований
  • Автоматизируйте всё через CI/CD pipelines
  • Используйте GitOps для контроля конфигурации
  • Версионируйте правильно (semantic versioning)
  • Implement progressive delivery для сложных changes

Kubernetes: Observability и production operations


Введение

Эффективная эксплуатация production Kubernetes кластеров требует глубокого владения инструментами мониторинга, логирования, отладки и обслуживания. Этот раздел охватывает observability stack, troubleshooting, production patterns и cost optimization.


Prometheus и мониторинг

Prometheus: сбор метрик

Prometheus — один из самых популярных инструментов мониторинга в экосистеме Kubernetes. Его основная задача — сбор time-series метрик через процесс scraping (обращение к HTTP endpoints, предоставляющим метрики).

Kubernetes интегрируется с Prometheus через Service Discovery: Prometheus автоматически находит все новые Pod'ы, Service'ы и ноды.

PromQL: язык запросов

# Суммарная скорость HTTP запросов за 5 минут
sum(rate(http_requests_total{namespace="production"}[5m]))

# CPU utilization по pod'ам
sum(rate(container_cpu_usage_seconds_total[5m])) by (pod_name)

# Memory usage в percentage
(container_memory_usage_bytes / container_spec_memory_limit_bytes) * 100

Alertmanager

Alertmanager обрабатывает алерты от Prometheus:

apiVersion: v1
kind: PrometheusRule
metadata:
  name: app-alerts
spec:
  groups:

  - name: java-app
    rules:

    - alert: HighCPUUsage
      expr: (sum(rate(container_cpu_usage_seconds_total[5m])) by (pod)) > 0.9
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "High CPU usage on {{ $labels.pod }}"
    
    - alert: HighErrorRate
      expr: (rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m])) > 0.05
      for: 5m
      labels:
        severity: critical

Grafana: визуализация метрик

Grafana — UI платформа для визуализации метрик из Prometheus (и других источников):

  • Dashboards — графики и таблицы для ключевых метрик
  • Alerting — уведомления в Slack, Telegram, Email
  • Annotations — маркировка важных событий на графиках

Готовые dashboards для Kubernetes:

  • Cluster overview
  • Pod/container metrics
  • Node metrics

Logging: централизованный сбор логов

Structured Logging

Логируйте в JSON для простоты парсинга и анализа:

{
  "timestamp": "2025-01-15T10:30:45.123Z",
  "level": "ERROR",
  "logger": "com.example.service",
  "message": "Database connection failed",
  "correlationId": "req-123-456",
  "service": "user-service",
  "span_id": "span-789",
  "trace_id": "trace-123"
}

ELK/EFK Stack

Elasticsearch — хранилище логов
Logstash/Fluentd — сбор и обработка логов
Kibana — UI для поиска и анализа

Loki: lightweight logging

Loki от Grafana предоставляет lightweight альтернативу ELK:

apiVersion: v1
kind: ConfigMap
metadata:
  name: loki-config
data:
  loki-config.yaml: |
    auth_enabled: false
    ingester:
      chunk_idle_period: 3m
    limits_config:
      enforce_metric_name: false
      reject_old_samples: true
      reject_old_samples_max_age: 168h

Distributed Tracing: отслеживание запросов

OpenTelemetry

OpenTelemetry — стандарт для сбора метрик, логов и трейсов:

// Spring Boot with OpenTelemetry
implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter'

Jaeger / Zipkin

apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-config
data:
  config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
    exporters:
      jaeger:
        endpoint: jaeger-collector:14250
    service:
      pipelines:
        traces:
          receivers: [otlp]
          exporters: [jaeger]

Troubleshooting и debugging

kubectl debugging commands

# Проверить статус Pod'ов
kubectl get pods -o wide

# Подробная информация о Pod'е (events, conditions)
kubectl describe pod <pod-name>

# Логи контейнера
kubectl logs <pod-name>
kubectl logs -f <pod-name>  # tail -f
kubectl logs <pod-name> --previous  # логи crashed контейнера

# Interactive shell в контейнере
kubectl exec -it <pod-name> -- /bin/bash

Common Pod Issues

CrashLoopBackOff — контейнер падает при запуске:

  • Проверьте Events в kubectl describe pod
  • Смотрите последние строки в kubectl logs --previous
  • Проверьте exit code

ImagePullBackOff — ошибка при скачивании образа:

  • Проверьте имя образа
  • Проверьте доступ к private registry
  • Проверьте imagePullSecrets

Pending — Pod не может быть размещен:

  • Недостаточно ресурсов (check requests)
  • Taints/tolerations mismatch
  • PVC не может быть смонтирован

Node Debugging

# Информация о узле
kubectl describe node <node-name>

# Check node pressure
kubectl top nodes

# Logs from node
kubectl get events -n kube-system | grep <node-name>

Service Mesh: Istio vs Linkerd

Istio

Полнофункциональное решение с богатым набором возможностей:

  • Advanced traffic management
  • Canary deployments, traffic splitting
  • Circuit breaking
  • Mutual TLS (mTLS) для service-to-service коммуникации

Требует больше ресурсов и более сложна в операции.

Linkerd

Более лаконичное решение, построенное на Rust:

  • Простота и производительность
  • Меньше ресурсов
  • Быстрый setup

Выбор зависит от требований проекта.


Production Best Practices

High Availability

Control Plane HA:

  • Минимум 3 узла (лучше 5)
  • Распределенные по availability zones
  • Несколько replicas etcd

Application HA:

  • Минимум 2-3 replicas для production
  • Pod Disruption Budgets
  • Resource requests/limits

Multi-cluster Management

Для enterprise-приложений с требованиями к disaster recovery:

  • Geographic distribution — разные регионы
  • Kubernetes Federation — централизованное управление
  • Service mesh — cross-cluster communication
  • Backup & Restore — Velero для автоматизированных backups

Cost Optimization

Rightsizing:

  • Мониторьте actual usage
  • Adjust requests/limits

Autoscaling:

  • HPA для Pod'ов
  • Cluster Autoscaler для nodes
  • VPA для рекомендаций по resources

Spot Instances:

  • Используйте для non-critical workloads
  • Дешевле, но могут быть прерваны

etcd Management

etcd Backup и Restore

# Backup etcd
ETCDCTL_API=3 etcdctl --endpoints=https://localhost:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /backup/etcd-backup.db

# Restore etcd
ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-backup.db \
  --data-dir=/var/lib/etcd-restored

etcd Performance Tuning

  • Используйте SSD для etcd storage
  • Установите heartbeat и election timeouts
  • Мониторьте операции latency

Database Deployment в Kubernetes

StatefulSet для Databases

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:

      - name: postgres
        image: postgres:13
        ports:

        - containerPort: 5432
        volumeMounts:

        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:

  - metadata:
      name: postgres-storage
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 100Gi

Operators для Databases

PostgreSQL Operator:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres-cluster
spec:
  instances: 3
  storage:
    size: 100Gi

MySQL Operator, MongoDB Operator — аналогичные решения.


Kubernetes Upgrade Strategies

Rolling Node Upgrade

# Drain узла (переместить pods)
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data

# Upgrade компонентов на узле
# (kubeadm upgrade, kubelet, etc.)

# Возвращение узла в service
kubectl uncordon <node>

Blue-Green Cluster

  • Создать новый кластер с новой версией
  • Миграция workloads
  • Переключение traffic
  • Быстрый rollback при проблемах

Best Practices

  • Upgrade один minor version at a time
  • Тестировать в non-production first
  • Имать backup и rollback plan
  • Мониторить API deprecations

Disaster Recovery

Backup Strategy

Компоненты для backup:

  • etcd (состояние кластера)
  • PersistentVolumes (данные приложений)
  • ConfigMaps, Secrets
  • Custom resources

Velero для Backup/Restore

apiVersion: velero.io/v1
kind: Backup
metadata:
  name: daily-backup
spec:
  ttl: 720h0m0s
  includedNamespaces:

  - production
  includedResources:

  - deployments
  - services
  - persistentvolumes

RTO/RPO

RTO (Recovery Time Objective) — максимальное допустимое время для восстановления
RPO (Recovery Point Objective) — максимально допустимое количество data loss

Для critical systems: RTO < 1 hour, RPO < 15 minutes.


Java-specific Production Patterns

JVM Tuning для Kubernetes

-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200

Graceful Shutdown

Spring Boot автоматически:

  • Обрабатывает SIGTERM
  • Останавливает приём новых запросов
  • Завершает in-flight запросы
  • Перекрывает connections
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Performance Monitoring

Используйте:

  • Prometheus для метрик
  • Grafana для дашбордов
  • OpenTelemetry для трейсинга
  • ELK для логов

Заключение

Production Kubernetes требует системного подхода к observability, troubleshooting, upgrade strategies и disaster recovery. Мониторинг через Prometheus/Grafana, логирование через ELK/Loki, и distributed tracing через OpenTelemetry/Jaeger обеспечивают полную видимость системы.

Для Java разработчиков критично понимать:

  • Как JVM взаимодействует с container limits
  • Graceful shutdown механизмы
  • Health check implementations
  • Performance monitoring и tuning

Правильная конфигурация и мониторинг обеспечивают надёжную и эффективную работу production систем в Kubernetes.

Ansible: базовые концепции

Ansible и его место в DevOps

Ansible — инструмент для автоматизации конфигурации и развёртывания. Для backend-разработчика это означает, что ты можешь описать состояние серверов (установка ПО, конфигурация сервисов, развёртывание приложения) в коде и применить это состояние к множеству машин одной командой.

Ключевое отличие Ansible от других инструментов (Chef, Puppet): он не требует агента на целевых машинах, работает по SSH и не оставляет следов. Это упрощает развёртывание в облаках и локальных окружениях.

Архитектурные компоненты

Управляющий узел (control node) — машина, где запускается Ansible. Обычно это твоя рабочая машина, сервер CI/CD или выделенная машина.

Управляемые узлы (managed nodes) — целевые серверы, на которых выполняются команды. Требуется только SSH-доступ и Python (часто уже есть на Linux).

Инвентарь (inventory) — список управляемых узлов с их адресами и переменными.

Playbook — YAML-файл, описывающий, что нужно сделать на серверах. Playbook содержит одну или несколько пьес (play).

Пьеса (play) — блок инструкций, определяющий целевые хосты и список задач.

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

Модуль (module) — Python-скрипт на управляемом узле, который выполняет конкретное действие. Ansible поставляется с сотнями модулей (apt, yum, copy, template, service и др.).

Роль (role) — организованная структура для переиспользования кода: группировка задач, переменных, шаблонов и обработчиков по логическому признаку.

Обработчик (handler) — задача, которая запускается только если произошло изменение (changed), и запускается один раз в конце пьесы, даже если её триггировало несколько задач.

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

Идемпотентность — ключевое свойство Ansible. Это означает, что повторный запуск playbook не должен вызвать нежелательные изменения, если целевое состояние уже достигнуто.

Пример:

- name: Install nginx
  apt:
    name: nginx
    state: present

При первом запуске этот модуль установит nginx. При втором запуске он проверит, что nginx уже установлен, и не будет повторно его ставить.

Идемпотентность позволяет безопасно запускать playbook многократно, не боясь побочных эффектов. Модули, которые соответствуют этому принципу, сообщают об изменении через статус changed: true/false.

Модули, которые НЕ идемпотентны (например, shell, command по умолчанию):

- name: This will always report changed
  shell: echo "hello"

Для обеспечения идемпотентности при использовании shell или command используется аргумент creates:

- name: Create file if it doesn't exist
  shell: touch /tmp/myfile
  args:
    creates: /tmp/myfile

Инвентарь: структура и типы

INI-формат

[webservers]
web1.example.com
web2.example.com ansible_port=2222

[databases]
db1.example.com ansible_user=dbadmin

[all:vars]
ansible_user=ubuntu
ansible_private_key_file=~/.ssh/id_rsa

YAML-формат

all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
          ansible_port: 2222
    databases:
      hosts:
        db1.example.com:
          ansible_user: dbadmin
  vars:
    ansible_user: ubuntu
    ansible_private_key_file: ~/.ssh/id_rsa

Переменные инвентаря

Переменные хостов — специфичны для конкретной машины:

webserver1:
  ansible_host: 192.168.1.10
  ansible_user: deploy
  app_port: 8080

Переменные групп — применяются ко всем хостам в группе:

all:
  vars:
    ntp_server: ntp.ubuntu.com

Playbook: структура и выполнение

Базовая структура playbook:

---
- name: Deploy web application
  hosts: webservers
  become: yes
  gather_facts: yes
  vars:
    app_version: "1.0.0"
  tasks:

    - name: Install dependencies
      apt:
        name: "{{ item }}"
        state: present
      loop:

        - nginx
        - curl
    
    - name: Copy application config
      template:
        src: app.conf.j2
        dest: /etc/app/config.conf
      notify: restart app
  
  handlers:

    - name: restart app
      service:
        name: app
        state: restarted

Хосты — к каким машинам применить пьесу (по имени или группе из инвентаря).

become — выполнять задачи с повышением привилегий (sudo). Часто нужно для установки ПО и изменения конфигов.

gather_facts — собрать информацию о системе (OS, IP, установленное ПО и т.д.). Доступна в переменной ansible_facts. Отключи, если не нужна — экономит время.

vars — переменные, доступные в пьесе.

tasks — список задач для выполнения по порядку.

handlers — задачи, триггируемые другими задачами через notify.

Модули: примеры для backend-разработчика

apt / yum — установка пакетов:

- name: Install Java
  apt:
    name: openjdk-17-jdk
    state: present

copy — копирование файлов на целевой хост:

- name: Copy application jar
  copy:
    src: target/app.jar
    dest: /opt/app/app.jar
    owner: app
    group: app
    mode: '0755'

template — копирование файла с обработкой Jinja2-переменных:

- name: Generate application.properties
  template:
    src: application.properties.j2
    dest: /opt/app/application.properties
  notify: restart app

service — управление системными сервисами:

- name: Start nginx
  service:
    name: nginx
    state: started
    enabled: yes

command / shell — выполнение команд. Используй command по умолчанию (безопаснее), shell только если нужны pipe/редирект:

- name: Get Java version
  command: java -version
  register: java_version
  changed_when: false

- name: Run database migrations
  shell: |
    cd /opt/app
    ./gradlew migrate
  environment:
    DB_URL: "{{ database_url }}"

docker_container — управление контейнерами:

- name: Run application container
  docker_container:
    name: myapp
    image: myapp:latest
    state: started
    ports:

      - "8080:8080"
    env:
      LOG_LEVEL: INFO

get_url — скачивание файлов:

- name: Download JAR
  get_url:
    url: https://repo.example.com/app-1.0.jar
    dest: /opt/app/app.jar
    checksum: "sha256:abc123..."

Выполнение playbook: как это работает "под капотом"

  1. Ansible читает playbook и инвентарь.

  2. Для каждого хоста, которому нужно применить пьесу, Ansible устанавливает SSH-соединение.

  3. Для каждой задачи Ansible:

    • Загружает соответствующий модуль на целевую машину через SSH.
    • Передаёт параметры задачи модулю.
    • Выполняет модуль на целевом хосте.
    • Читает результат (статус, вывод, флаг changed).
    • Удаляет модуль с целевого хоста.
  4. Если задача содержит notify, добавляет обработчик в очередь.

  5. После всех задач запускает все обработчики, которые были добавлены в очередь (один раз каждый).

Вывод выполнения для каждой задачи включает:

  • Имя задачи.
  • Хосты, на которых она выполнялась.
  • Статус: ok (уже в желаемом состоянии), changed (произошло изменение), failed, skipped.

Facts и переменные во время выполнения

Facts — информация о целевом хосте, собранная модулем setup:

- name: Show facts
  debug:
    var: ansible_facts

Доступны переменные:

  • ansible_os_family — семейство ОС (Debian, RedHat и т.д.).
  • ansible_distribution — конкретная ОС (Ubuntu, CentOS и т.д.).
  • ansible_interfaces — список сетевых интерфейсов.
  • ansible_memtotal_mb — объём памяти.

Используй facts для условной логики:

- name: Install nginx (Ubuntu)
  apt:
    name: nginx
    state: present
  when: ansible_os_family == "Debian"

- name: Install nginx (CentOS)
  yum:
    name: nginx
    state: present
  when: ansible_os_family == "RedHat"

Переменные вычисляются один раз в начале выполнения пьесы и доступны во всех задачах. Используй register для сохранения вывода задачи в переменную и применения её позже:

- name: Check if app is running
  command: systemctl is-active myapp
  register: app_status
  changed_when: false
  failed_when: false

- name: Restart app if not running
  service:
    name: myapp
    state: restarted
  when: app_status.rc != 0

Команды для работы с Ansible

ansible-playbook — запуск playbook:

ansible-playbook site.yml
ansible-playbook site.yml --inventory hosts.ini
ansible-playbook site.yml -i hosts.ini -e "env=prod"

ansible — выполнение ad-hoc команд на хостах:

ansible webservers -m apt -a "name=curl state=present" -b
ansible all -m setup

Флаги:

  • -i — файл инвентаря.
  • -e — переменные для переопределения (-e "key=value").
  • -b — использовать become (sudo).
  • -v — verbosity (можно повторять: -vvv для максимума).
  • --check — сухой запуск (dry-run), без реальных изменений.
  • --diff — показывает различия в файлах.
  • --limit — ограничить хосты (--limit webservers).
  • --tags — запустить только задачи с определённым тегом.
  • --start-at-task — начать с конкретной задачи.

Сухой запуск с diff:

ansible-playbook site.yml --check --diff

Логирование и отладка

Установи логирование в конфиге ansible.cfg:

[defaults]
log_path = /var/log/ansible.log

Используй модуль debug для вывода переменных:

- name: Debug variables
  debug:
    msg: "App version: {{ app_version }}"

Выполни playbook с максимальным логированием:

ansible-playbook site.yml -vvv

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

ansible-playbook site.yml --check --diff --limit db1.example.com

Ключевые выводы

  • Ansible работает без агентов, используя SSH и Python.
  • Идемпотентность — гарантирует безопасность повторных запусков.
  • Инвентарь описывает целевые хосты и их переменные.
  • Playbook — YAML-файл с задачами, которые нужно выполнить.
  • Модули — переиспользуемые компоненты для конкретных действий.
  • Handlers запускаются в конце пьесы, только если произошли изменения.
  • Facts содержат информацию о целевом хосте и используются для условной логики.

Ansible: инвентарь, группы, переменные и организация окружений

Инвентарь и управление несколькими окружениями

Для backend-разработчика типичная структура окружений:

inventory/
├── dev
│   ├── hosts.yml
│   └── group_vars/
├── stage
│   ├── hosts.yml
│   └── group_vars/
└── prod
    ├── hosts.yml
    └── group_vars/

Команда для применения playbook к конкретному окружению:

ansible-playbook deploy.yml -i inventory/dev/hosts.yml
ansible-playbook deploy.yml -i inventory/stage/hosts.yml
ansible-playbook deploy.yml -i inventory/prod/hosts.yml

Иерархия переменных (variable precedence)

Переменные в Ansible применяются в порядке приоритета (от низкого к высокому):

  1. Role defaults (roles/myrole/defaults/main.yml) — самый низкий приоритет.
  2. Inventory variables — переменные из инвентаря (group_vars, host_vars).
  3. Playbook vars — переменные, определённые в playbook.
  4. Task vars — переменные, определённые в конкретной задаче.
  5. set_fact — переменные, установленные через модуль set_fact.
  6. Registered variables — результаты, сохранённые через register.
  7. Extra vars (-e флаг) — самый высокий приоритет.

Пример иерархии:

---
- name: Example hierarchy
  hosts: webservers
  vars:
    timeout: 30
  tasks:

    - name: Set fact
      set_fact:
        timeout: 60

    - name: Print timeout
      debug:
        msg: "Timeout: {{ timeout }}"
      vars:
        timeout: 90

При запуске с ansible-playbook -e "timeout=120" выведет 120 (extra vars имеют максимальный приоритет).

Группы хостов и их организация

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

all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
    databases:
      hosts:
        db1.example.com:
          ansible_host: 10.0.1.5
        db2.example.com:
          ansible_host: 10.0.1.6
    cache:
      hosts:
        redis1.example.com:
    monitoring:
      hosts:
        prometheus.example.com:

Хост может быть в нескольких группах одновременно. Ansible также создаёт группы:

  • all — все хосты.
  • ungrouped — хосты, не входящие ни в какую группу.

Группы используются в playbook как целевые хосты:

- name: Deploy web app
  hosts: webservers
  tasks:

    - name: Install web server
      apt:
        name: nginx
        state: present

- name: Configure database
  hosts: databases
  tasks:

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

Для запуска задач на подмножестве хостов используй --limit:

ansible-playbook site.yml --limit webservers
ansible-playbook site.yml --limit web1.example.com

Group vars и host vars

Group vars — переменные для всех хостов в группе. Файл group_vars/имя_группы.yml или папка group_vars/имя_группы/main.yml.

inventory/prod/
├── hosts.yml
└── group_vars/
    ├── webservers.yml
    ├── databases.yml
    └── all.yml

Содержимое inventory/prod/group_vars/webservers.yml:

# Переменные для всех хостов в группе webservers
app_user: deploy
app_group: deploy
app_port: 8080
nginx_worker_processes: 4
log_level: info

Содержимое inventory/prod/group_vars/databases.yml:

postgres_version: 14
postgres_max_connections: 200
backup_retention_days: 30

Содержимое inventory/prod/group_vars/all.yml:

# Глобальные переменные для всех хостов
environment: prod
ntp_server: ntp.ubuntu.com
syslog_server: logs.example.com

Host vars — переменные для конкретного хоста. Файл host_vars/имя_хоста.yml.

inventory/prod/
├── hosts.yml
└── host_vars/
    ├── web1.example.com.yml
    ├── web2.example.com.yml
    └── db1.example.com.yml

Содержимое inventory/prod/host_vars/web1.example.com.yml:

# Специфичные переменные для конкретного хоста
ansible_port: 2222
app_instance_id: web1
custom_flag: true
local_disk_size: 500GB

Использование групп для условной логики

В playbook обращайся к группам через groups переменную:

- name: Configure web server to know about database
  hosts: webservers
  tasks:

    - name: Generate database connection string
      set_fact:
        db_host: "{{ groups['databases'][0] }}"
    
    - name: Create app config
      template:
        src: app.conf.j2
        dest: /etc/app/app.conf
      vars:
        database_hosts: "{{ groups['databases'] }}"
        redis_host: "{{ groups['cache'][0] }}"

Условное выполнение задач для определённых групп:

- name: Apply monitoring configuration
  hosts: all
  tasks:

    - name: Install Prometheus node exporter
      apt:
        name: prometheus-node-exporter
        state: present
      when: inventory_hostname in groups['monitoring']

Динамический инвентарь

Динамический инвентарь — скрипт или API, который генерирует список хостов. Полезно при использовании облачных сервисов (AWS EC2, Google Cloud, DigitalOcean).

Пример скрипта на Python, возвращающего инвентарь в JSON:

#!/usr/bin/env python3
import json
import sys

# Здесь код для получения хостов (запрос к облаку, БД и т.д.)
inventory = {
    "all": {
        "hosts": [
            "web1.example.com",
            "web2.example.com"
        ],
        "vars": {
            "ansible_user": "ubuntu"
        }
    },
    "webservers": {
        "hosts": [
            "web1.example.com",
            "web2.example.com"
        ]
    },
    "databases": {
        "hosts": [
            "db1.example.com"
        ]
    }
}

if len(sys.argv) > 1 and sys.argv[1] == '--list':
    print(json.dumps(inventory))
elif len(sys.argv) > 1 and sys.argv[1] == '--host':
    host = sys.argv[2]
    print(json.dumps({}))
else:
    print(json.dumps({"error": "Usage: inventory.py --list or --host <hostname>"}))
    sys.exit(1)

Скрипт должен быть исполняемым:

chmod +x inventory.py

Используй динамический инвентарь в playbook:

ansible-playbook deploy.yml -i ./inventory.py

Структуризация playbook для разных окружений

Типичная структура проекта:

ansible/
├── ansible.cfg
├── inventories/
│   ├── dev/
│   │   ├── hosts.yml
│   │   └── group_vars/
│   ├── stage/
│   │   ├── hosts.yml
│   │   └── group_vars/
│   └── prod/
│       ├── hosts.yml
│       └── group_vars/
├── roles/
│   ├── common/
│   ├── webserver/
│   └── database/
├── site.yml
├── deploy.yml
└── maintenance.yml

site.yml — playbook для конфигурации серверов:

---
- name: Configure common settings
  hosts: all
  roles:

    - common

- name: Configure web servers
  hosts: webservers
  roles:

    - webserver

- name: Configure databases
  hosts: databases
  roles:

    - database

deploy.yml — playbook для развёртывания приложения:

---
- name: Deploy application
  hosts: webservers
  vars:
    app_version: "{{ version }}"
  roles:

    - deploy

Запуск для разных окружений:

ansible-playbook site.yml -i inventories/dev
ansible-playbook deploy.yml -i inventories/stage -e "version=1.2.0"
ansible-playbook deploy.yml -i inventories/prod -e "version=1.2.0"

Переменные через командную строку

Переопредели переменные через -e:

ansible-playbook deploy.yml -e "env=prod app_version=1.2.3"

Загрузи переменные из JSON-файла:

ansible-playbook deploy.yml -e @vars.json

Содержимое vars.json:

{
  "app_version": "1.2.3",
  "replicas": 3,
  "log_level": "info"
}

Загрузка переменных из внешних файлов

Используй модуль include_vars:

- name: Load environment-specific variables
  include_vars:
    file: "vars/{{ env }}.yml"
    name: env_vars

- name: Show loaded variables
  debug:
    var: env_vars

Или в начале playbook:

---
- name: Deploy application
  hosts: webservers
  vars_files:

    - "vars/common.yml"
    - "vars/{{ env }}.yml"
  tasks:

    - name: Install app
      apt:
        name: myapp
        state: present

Структура:

vars/
├── common.yml
├── dev.yml
├── stage.yml
└── prod.yml

Использование фактов для выбора конфигурации

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

- name: Install Docker
  hosts: webservers
  tasks:

    - name: Add Docker repository (Ubuntu)
      apt_repository:
        repo: ppa:docker/ubuntu
        state: present
      when: ansible_distribution == "Ubuntu"

    - name: Add Docker repository (CentOS)
      yum_repository:
        name: docker-ce
        baseurl: https://download.docker.com/linux/centos/
        gpgcheck: yes
      when: ansible_os_family == "RedHat"

    - name: Install Docker
      package:
        name: docker-ce
        state: present

Логирование переменных и debug

Вывести все переменные для хоста:

- name: Show all variables
  debug:
    var: hostvars[inventory_hostname]

Вывести специфичные переменные:

- name: Show app configuration
  debug:
    msg: |
      App: {{ app_name }}
      Version: {{ app_version }}
      Port: {{ app_port }}
      Environment: {{ environment }}

Запусти playbook в debug-режиме:

ansible-playbook site.yml -vvv

Интеграция инвентаря в CI/CD

Типичная структура для CI/CD (например, GitHub Actions):

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:

      - uses: actions/checkout@v2
      - name: Deploy to staging
        run: |
          ansible-playbook deploy.yml \
            -i inventories/stage \
            -e "version=${{ github.sha }}" \
            --vault-password-file vault-pass.txt

Сохрани SSH-ключ как GitHub Secret и используй его:

      - name: Add SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

Ключевые выводы

  • Инвентарь определяет хосты, группы и переменные.
  • Group vars используются для конфигурации на уровне группы.
  • Host vars — для уникальных параметров отдельных хостов.
  • Переменные имеют иерархию приоритета; extra vars имеют максимальный приоритет.
  • Динамический инвентарь позволяет получать хосты из облака или других источников.
  • Факты о системе используются для условной логики и выбора конфигурации.
  • Структурируй инвентарь по окружениям (dev, stage, prod) для удобства.

Ansible: роли, handlers, организация проекта и best practices

Структура роли

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

Структура роли:

roles/
└── webserver/
    ├── tasks/
    │   └── main.yml           # Основные задачи
    ├── handlers/
    │   └── main.yml           # Обработчики (перезагрузка сервисов)
    ├── templates/
    │   ├── nginx.conf.j2      # Jinja2-шаблоны
    │   └── app.conf.j2
    ├── files/
    │   └── security.conf      # Статичные файлы
    ├── vars/
    │   └── main.yml           # Переменные роли
    ├── defaults/
    │   └── main.yml           # Значения по умолчанию
    ├── meta/
    │   └── main.yml           # Зависимости, метаинформация
    └── README.md              # Документация

Написание роли: пример для Java-приложения

Создай роль javaapp:

roles/javaapp/defaults/main.yml:

app_name: myapp
app_version: 1.0.0
app_user: appuser
app_group: appuser
app_home: /opt/myapp
app_port: 8080
app_jvm_memory: 1024m
log_level: INFO

roles/javaapp/vars/main.yml:

app_jar_url: "https://repo.example.com/{{ app_name }}-{{ app_version }}.jar"
app_service_name: "{{ app_name }}"
systemd_unit: "/etc/systemd/system/{{ app_service_name }}.service"

roles/javaapp/meta/main.yml:

---
dependencies:

  - role: java
    vars:
      java_version: 17
  - role: common

roles/javaapp/tasks/main.yml:

---
- name: Create application user
  user:
    name: "{{ app_user }}"
    group: "{{ app_group }}"
    home: "{{ app_home }}"
    shell: /bin/false
    system: yes
  register: app_user_info

- name: Create application directories
  file:
    path: "{{ item }}"
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: '0755'
  loop:

    - "{{ app_home }}"
    - "{{ app_home }}/bin"
    - "{{ app_home }}/config"
    - "{{ app_home }}/logs"

- name: Download application JAR
  get_url:
    url: "{{ app_jar_url }}"
    dest: "{{ app_home }}/bin/{{ app_name }}.jar"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: '0755'
  notify: restart app

- name: Create application config from template
  template:
    src: application.properties.j2
    dest: "{{ app_home }}/config/application.properties"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: '0644'
  notify: restart app

- name: Create systemd unit file
  template:
    src: app.service.j2
    dest: "{{ systemd_unit }}"
    mode: '0644'
  notify: systemd daemon reload

- name: Enable and start application service
  service:
    name: "{{ app_service_name }}"
    enabled: yes
    state: started

- name: Wait for application to be ready
  wait_for:
    port: "{{ app_port }}"
    delay: 2
    timeout: 30

roles/javaapp/handlers/main.yml:

---
- name: restart app
  service:
    name: "{{ app_service_name }}"
    state: restarted

- name: systemd daemon reload
  systemd:
    daemon_reload: yes

Handlers: правильное использование

Handlers — задачи, которые выполняются только при изменении других задач. Триггер через notify:

- name: Install nginx
  apt:
    name: nginx
    state: present
  notify: restart nginx

- name: Update nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: reload nginx

Определи handlers в handlers/main.yml:

---
- name: restart nginx
  service:
    name: nginx
    state: restarted

- name: reload nginx
  service:
    name: nginx
    state: reloaded

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

  • Handlers запускаются только один раз в конце пьесы, даже если их триггировало несколько задач.
  • Используй listen для группировки нескольких действий под одним триггером:
- name: Restart web services
  listen: restart web
  service:
    name: "{{ item }}"
    state: restarted
  loop:

    - nginx
    - php-fpm

- name: Check web services
  listen: restart web
  wait_for:
    port: 80
    delay: 2
    timeout: 10

Затем в задаче используй notify:

- name: Update web config
  template:
    src: web.conf.j2
    dest: /etc/web/config.conf
  notify: restart web
  • Используй force_handlers: yes в playbook, чтобы запустить handlers даже если playbook завершился с ошибкой:
---
- name: Deploy application
  hosts: webservers
  force_handlers: yes
  tasks:

    - name: Stop old service
      service:
        name: myapp
        state: stopped
      notify: restart app
    # Если следующая задача упадёт, handler всё равно запустится
    - name: Deploy new version
      copy:
        src: myapp.jar
        dest: /opt/myapp/

Зависимые роли

Если роль зависит от другой роли, определи в meta/main.yml:

---
dependencies:

  - role: java
    vars:
      java_version: 17
  - role: docker
    vars:
      docker_version: latest
  - role: postgresql
    vars:
      postgres_version: 14

При включении роли webserver, Ansible автоматически выполнит её зависимости.

Pre/post tasks и включение ролей

Используй pre_tasks для выполнения задач до ролей и post_tasks для выполнения после:

---
- name: Deploy application
  hosts: webservers
  pre_tasks:

    - name: Check connectivity
      ping:

    - name: Get system info
      setup:
        filter: ansible_os_family

  roles:

    - java
    - postgresql
    - webserver

  post_tasks:

    - name: Run tests
      shell: curl http://localhost:8080/health

    - name: Notify team
      mail:
        host: smtp.example.com
        to: team@example.com
        subject: "Deployment completed"

Включение задач из других файлов

Для переиспользования задач без создания полной роли используй include_tasks:

- name: Include common tasks
  include_tasks: common_setup.yml

- name: Configure firewall
  include_tasks: firewall.yml
  vars:
    firewall_ports:

      - 8080
      - 8443

Best practices по организации проекта

Структура репозитория

ansible-playbooks/
├── ansible.cfg                    # Конфигурация Ansible
├── .gitignore
├── inventories/                   # Инвентари по окружениям
│   ├── dev/
│   │   ├── hosts.yml
│   │   └── group_vars/
│   ├── stage/
│   └── prod/
├── roles/                         # Роли для переиспользования
│   ├── common/
│   ├── java/
│   ├── docker/
│   ├── postgresql/
│   ├── webserver/
│   └── monitoring/
├── site.yml                       # Основной playbook для конфигурации
├── deploy.yml                     # Playbook для развёртывания
├── maintenance.yml                # Playbook для поддержки
├── group_vars/                    # Глобальные переменные для групп
│   └── all.yml
├── host_vars/                     # Переменные для хостов
├── library/                       # Кастомные модули
├── plugins/                       # Плагины
├── README.md
└── requirements.yml               # Зависимости (ansible-galaxy)

Конфиг Ansible

ansible.cfg:

[defaults]
inventory = inventories/dev
roles_path = ./roles
host_key_checking = False
retry_files_enabled = False
log_path = /var/log/ansible.log

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True

Соглашения по именованию

  • Роли: common, webserver, database (нижние подчёркивания для многословных имён).
  • Переменные: app_name, db_host, service_port (snake_case).
  • Файлы playbook: site.yml, deploy.yml, maintenance.yml.
  • Теги: install, configure, deploy, rollback.

Использование тегов

Пометь задачи тегами для выборочного выполнения:

- name: Install dependencies
  apt:
    name: "{{ item }}"
    state: present
  loop:

    - nginx
    - curl
  tags:

    - install

- name: Configure application
  template:
    src: app.conf.j2
    dest: /etc/app/config.conf
  tags:

    - configure
    - deploy

- name: Start services
  service:
    name: "{{ item }}"
    state: started
  loop:

    - nginx
    - app
  tags:

    - deploy

Запуск с конкретным тегом:

ansible-playbook site.yml --tags install
ansible-playbook deploy.yml --tags deploy
ansible-playbook site.yml --skip-tags install

Тестирование playbook

Используй dry-run для проверки:

ansible-playbook site.yml --check

Используй diff для видения изменений:

ansible-playbook deploy.yml --check --diff

Запусти только одну задачу для отладки:

ansible-playbook site.yml --start-at-task "Task name"

Лучшие практики

  1. Используй роли — разбивай логику на переиспользуемые компоненты.
  2. Группируй хосты логически — webservers, databases, monitoring.
  3. Используй handlers — для перезагрузки сервисов после изменений.
  4. Разделяй окружения — dev, stage, prod в отдельных инвентариях.
  5. Документируй роли — добавляй README с параметрами и примерами.
  6. Используй переменные по умолчанию — defaults/main.yml с разумными значениями.
  7. Проверяй idempotency — запускай playbook дважды и убедись что ничего не изменится.
  8. Логируй выполнение — используй debug модуль для вывода переменных.
  9. Используй version control — храни все плейбуки в Git.
  10. Тестируй в dev перед prod — используй staging окружение.

Версионирование зависимостей

requirements.yml для Ansible Galaxy (внешних ролей):

---
collections:

  - name: community.docker
    version: ">=1.9.0"
  - name: ansible.posix
    version: ">=1.2.0"

roles:

  - name: geerlingguy.java
    version: 1.10.0
  - name: geerlingguy.postgresql
    version: 3.4.0

Установи зависимости:

ansible-galaxy install -r requirements.yml

Мониторинг и отладка playbook

Используй модуль assert для проверок:

- name: Verify application is running
  assert:
    that:

      - app_status.rc == 0
      - app_version.stdout == expected_version
    fail_msg: "Application verification failed"
    success_msg: "Application is healthy"

Используй блоки с rescue для обработки ошибок:

- name: Try to deploy
  block:

    - name: Copy application
      copy:
        src: app.jar
        dest: /opt/app/

    - name: Start service
      service:
        name: app
        state: started
  
  rescue:

    - name: Rollback previous version
      command: /opt/app/scripts/rollback.sh

    - name: Notify team about failure
      mail:
        host: smtp.example.com
        to: team@example.com
        subject: "Deployment failed and rolled back"

Ключевые выводы

  • Роли — основной способ организации автоматизации в Ansible.
  • Handlers запускаются в конце пьесы, только при изменениях.
  • Зависимые роли автоматически выполняются перед основной ролью.
  • Используй теги для выборочного запуска задач.
  • Dry-run и diff помогают проверить изменения перед применением.
  • Структурируй проект по окружениям и ролям для масштабируемости.

Ansible: Jinja2 шаблоны, переменные

Jinja2 шаблоны для конфигураций

Jinja2 — язык шаблонизации в Ansible. Используется для генерации конфигурационных файлов на основе переменных.

Модуль template копирует файл с обработкой Jinja2:

- name: Generate application config
  template:
    src: application.properties.j2
    dest: /etc/app/application.properties
    owner: app
    group: app
    mode: '0600'

Базовый синтаксис Jinja2

Вывод переменной:

# application.properties.j2
app.name={{ app_name }}
app.version={{ app_version }}
app.port={{ app_port }}

Условия:

{% if environment == "prod" %}
app.debug=false
log.level=ERROR
{% elif environment == "stage" %}
app.debug=false
log.level=INFO
{% else %}
app.debug=true
log.level=DEBUG
{% endif %}

Циклы:

# nginx-upstream.conf.j2
upstream backend {
{% for host in groups['webservers'] %}
    server {{ host }}:{{ app_port }};
{% endfor %}
}

Фильтры:

# Преобразование строк
app.name.uppercase={{ app_name | upper }}
app.name.lowercase={{ app_name | lower }}

# JSON-кодирование
database.config={{ db_config | to_json }}

# Стандартные фильтры
list.count={{ items | length }}
first.item={{ items[0] | default('none') }}

# Соединение элементов списка
allowed.hosts={{ allowed_hosts | join(', ') }}

Установка переменных в шаблоне:

{% set app_url = "http://" + app_host + ":" + app_port | string %}
app.url={{ app_url }}

Практический пример: Jinja2 для конфигурации Spring Boot

roles/javaapp/templates/application.properties.j2:

# Spring Boot Configuration
spring.application.name={{ app_name }}
server.port={{ app_port }}
server.servlet.context-path=/api

# Database Configuration
spring.datasource.url=jdbc:postgresql://{{ db_host }}:{{ db_port }}/{{ db_name }}
spring.datasource.username={{ db_user }}
spring.datasource.password={{ db_password }}
spring.jpa.hibernate.ddl-auto={{ db_ddl_auto | default('validate') }}

# Logging
logging.level.root={{ log_level | default('INFO') }}
logging.level.com.myapp={{ app_log_level | default('DEBUG') }}
logging.file.name={{ app_home }}/logs/application.log

# Server Configuration
server.tomcat.threads.max={{ tomcat_max_threads | default('200') }}
server.connection-timeout=20000

# Application-specific
app.environment={{ environment }}
app.version={{ app_version }}
app.replicas={{ groups['webservers'] | length }}

# Cache Configuration
{% if enable_cache %}
spring.cache.type=redis
spring.redis.host={{ redis_host }}
spring.redis.port={{ redis_port }}
{% else %}
spring.cache.type=simple
{% endif %}

# Monitoring
management.endpoints.web.exposure.include=health,metrics,info
management.endpoint.health.show-details=when-authorized

Использование в playbook:

- name: Deploy Java application
  hosts: webservers
  vars:
    app_name: myapp
    app_port: 8080
    db_host: "{{ groups['databases'][0] }}"
    db_port: 5432
    db_name: myapp_db
    db_user: appuser
    log_level: INFO
    enable_cache: yes
    redis_host: "{{ groups['cache'][0] }}"
  tasks:

    - name: Generate application properties
      template:
        src: application.properties.j2
        dest: "{{ app_home }}/config/application.properties"
      notify: restart app

Docker Compose через Jinja2

roles/docker/templates/docker-compose.yml.j2:

version: '3.8'

services:
  app:
    image: {{ docker_registry }}/{{ app_name }}:{{ app_version }}
    container_name: {{ app_name }}
    ports:

      - "{{ app_port }}:8080"
    environment:

      - APP_ENV={{ environment }}
      - LOG_LEVEL={{ log_level }}
      - DB_HOST={{ db_host }}
      - DB_PORT={{ db_port }}
      - DB_NAME={{ db_name }}
      - DB_USER={{ db_user }}
      - DB_PASSWORD={{ db_password }}
      - CACHE_HOST={{ redis_host }}
    volumes:

      - {{ app_home }}/config:/app/config:ro
      - {{ app_home }}/logs:/app/logs
    depends_on:
{% for service in docker_dependencies %}
      - {{ service }}
{% endfor %}
    restart: always
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3

{% if enable_postgresql %}
  postgres:
    image: postgres:{{ postgres_version }}
    container_name: {{ app_name }}-db
    environment:

      - POSTGRES_DB={{ db_name }}
      - POSTGRES_USER={{ db_user }}
      - POSTGRES_PASSWORD={{ db_password }}
    volumes:

      - postgres_data:/var/lib/postgresql/data
    restart: always
{% endif %}

{% if enable_redis %}
  redis:
    image: redis:{{ redis_version }}-alpine
    container_name: {{ app_name }}-cache
    ports:

      - "{{ redis_port }}:6379"
    restart: always
{% endif %}

volumes:
{% if enable_postgresql %}
  postgres_data:
{% endif %}

Systemd unit файл через Jinja2

roles/javaapp/templates/app.service.j2:

[Unit]
Description={{ app_description | default(app_name ~ ' application') }}
After=network.target
Wants=network-online.target

[Service]
Type=simple
User={{ app_user }}
Group={{ app_group }}
WorkingDirectory={{ app_home }}

# Переменные окружения
EnvironmentFile=-{{ app_home }}/config/env
Environment="APP_HOME={{ app_home }}"
Environment="LOG_LEVEL={{ log_level }}"
Environment="JAVA_OPTS=-Xmx{{ app_jvm_memory }} -Xms{{ app_jvm_memory }}"

# Запуск приложения
ExecStart={{ java_bin | default('/usr/bin/java') }} \
    -Xmx{{ app_jvm_memory }} \
    -Xms{{ app_jvm_memory }} \
    -Dspring.config.location=file:{{ app_home }}/config/application.properties \
    -jar {{ app_home }}/bin/{{ app_name }}.jar

# Политика перезагрузки
Restart=on-failure
RestartSec=10
StartLimitInterval=60
StartLimitBurst=3

# Логирование
StandardOutput=journal
StandardError=journal
SyslogIdentifier={{ app_name }}

# Безопасность
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths={{ app_home }}

[Install]
WantedBy=multi-user.target

Ansible Vault: управление секретами

Vault позволяет шифровать конфиденциальные данные (пароли, ключи, токены).

Создание зашифрованного файла

ansible-vault create inventories/prod/group_vars/all/secrets.yml

Вводишь пароль и редактируешь файл. Содержимое автоматически зашифруется.

Содержимое secrets.yml:

---
db_password: "superSecurePassword123!"
redis_password: "redisPassword"
api_key: "sk-1234567890abcdef"
ssl_certificate: |
  -----BEGIN CERTIFICATE-----
  MIIDXTCCAkWgAwIBAgIJAJL3...
  -----END CERTIFICATE-----

Использование переменных из Vault

В playbook или group_vars используй переменные как обычно:

- name: Configure database
  postgresql_query:
    db: "{{ db_name }}"
    login_user: "{{ db_user }}"
    login_password: "{{ db_password }}"  # из secrets.yml
    query: "..."

Запусти playbook с расшифровкой Vault:

ansible-playbook site.yml --ask-vault-pass

или с файлом пароля:

ansible-playbook site.yml --vault-password-file ~/.vault_pass

Просмотр зашифрованного файла

ansible-vault view secrets.yml

Редактирование зашифрованного файла

ansible-vault edit secrets.yml

Шифрование существующего файла

ansible-vault encrypt vars.yml

Расшифровка файла

ansible-vault decrypt secrets.yml

Практика: Vault для разных окружений

Структура:

inventories/
├── dev/
│   └── group_vars/
│       ├── all.yml
│       └── all/
│           └── secrets.yml
├── stage/
│   └── group_vars/
│       ├── all.yml
│       └── all/
│           └── secrets.yml
└── prod/
    └── group_vars/
        ├── all.yml
        └── all/
            └── secrets.yml

inventories/dev/group_vars/all/secrets.yml:

---
db_password: "dev_password_123"
redis_password: "dev_redis_pass"
api_key: "dev_api_key"

inventories/prod/group_vars/all/secrets.yml:

---
db_password: "prod_complex_password_xyz789"
redis_password: "prod_redis_secure_pass"
api_key: "prod_api_key_secret"

Используй переменные в group_vars/all.yml:

---
db_user: appuser
db_host: "{{ groups['databases'][0] }}"
# db_password загружается из secrets.yml

redis_host: "{{ groups['cache'][0] }}"
# redis_password загружается из secrets.yml

Комбинирование Vault с extra vars

Для интеграции с CI/CD передавай чувствительные переменные через extra vars:

ansible-playbook deploy.yml \
  -i inventories/prod \
  -e "db_password=$DB_PASSWORD" \
  -e "api_key=$API_KEY" \
  --vault-password-file ~/.vault_pass

в GitHub Actions:

- name: Deploy with secrets
  run: |
    ansible-playbook deploy.yml \
      -i inventories/prod \
      -e "db_password=${{ secrets.DB_PASSWORD }}" \
      -e "api_key=${{ secrets.API_KEY }}" \
      --vault-password-file $VAULT_PASS_FILE
  env:
    VAULT_PASS_FILE: /tmp/vault_pass

Сохрани пароль Vault в файл перед запуском:

      - name: Create vault password file
        run: |
          echo "${{ secrets.VAULT_PASSWORD }}" > /tmp/vault_pass
          chmod 600 /tmp/vault_pass

Проверка синтаксиса и валидация

Проверь синтаксис playbook перед выполнением:

ansible-playbook site.yml --syntax-check

Используй lint для проверки best practices:

ansible-lint site.yml

Установи ansible-lint:

pip install ansible-lint

Фильтры Jinja2 для форматирования

Преобразование типов:

port_number={{ app_port | int }}
flag_value={{ enable_feature | bool }}

Работа со списками:

first_host={{ groups['webservers'] | first }}
last_host={{ groups['webservers'] | last }}
random_host={{ groups['webservers'] | random }}
sorted_hosts={{ groups['webservers'] | sort }}
unique_ids={{ all_ids | unique }}

Манипуляция строками:

joined_hosts={{ groups['webservers'] | join(', ') }}
base64_encoded={{ secret_key | b64encode }}
base64_decoded={{ encoded_key | b64decode }}

Математика:

sum_values={{ [1, 2, 3, 4] | sum }}
max_value={{ metrics | max }}
min_value={{ metrics | min }}

Ключевые выводы

  • Jinja2 шаблоны генерируют конфигурационные файлы на основе переменных.
  • Используй условия и циклы в шаблонах для гибкости.
  • Ansible Vault шифрует чувствительные данные.
  • Разделяй secrets по окружениям (dev, stage, prod).
  • Extra vars имеют наивысший приоритет для переопределения переменных.
  • Используй фильтры Jinja2 для преобразования и форматирования данных.
  • Всегда проверяй синтаксис перед применением playbook в production.

Ansible: практические сценарии

Сценарий 1: Настройка Linux-сервера под Java/Kotlin приложение

Роль prepare_java_server для установки всех необходимых зависимостей.

roles/prepare_java_server/defaults/main.yml:

java_version: 17
docker_version: latest
docker_compose_version: latest
system_packages:

  - build-essential
  - curl
  - wget
  - git
  - htop
  - net-tools
  - vim
  - unzip
  - jq
timezone: UTC
swap_size_gb: 2

roles/prepare_java_server/tasks/main.yml:

---
- name: Update system packages
  apt:
    update_cache: yes
    cache_valid_time: 3600

- name: Install system packages
  apt:
    name: "{{ system_packages }}"
    state: present

- name: Set timezone
  timezone:
    name: "{{ timezone }}"

- name: Configure swap
  block:

    - name: Check if swap file exists
      stat:
        path: /swapfile
      register: swap_file

    - name: Create swap file
      command: dd if=/dev/zero of=/swapfile bs=1G count={{ swap_size_gb }}
      when: not swap_file.stat.exists

    - name: Set swap file permissions
      file:
        path: /swapfile
        mode: '0600'

    - name: Make swap
      command: mkswap /swapfile
      when: not swap_file.stat.exists

    - name: Mount swap
      command: swapon /swapfile
      when: not swap_file.stat.exists

    - name: Add swap to fstab
      lineinfile:
        path: /etc/fstab
        line: "/swapfile swap swap defaults 0 0"
        state: present

- name: Install OpenJDK {{ java_version }}
  apt:
    name: "openjdk-{{ java_version }}-jdk"
    state: present

- name: Set JAVA_HOME environment variable
  lineinfile:
    path: /etc/environment
    line: 'JAVA_HOME="/usr/lib/jvm/java-{{ java_version }}-openjdk-amd64"'
    state: present

- name: Install Docker prerequisites
  apt:
    name:

      - apt-transport-https
      - ca-certificates
      - curl
      - gnupg
      - lsb-release
    state: present

- name: Add Docker GPG key
  apt_key:
    url: https://download.docker.com/linux/ubuntu/gpg
    state: present

- name: Add Docker repository
  apt_repository:
    repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
    state: present

- name: Install Docker
  apt:
    name: "docker-ce"
    state: present

- name: Install Docker Compose
  get_url:
    url: "https://github.com/docker/compose/releases/latest/download/docker-compose-Linux-x86_64"
    dest: /usr/local/bin/docker-compose
    mode: '0755'

- name: Add current user to docker group
  user:
    name: "{{ ansible_user_id }}"
    groups: docker
    append: yes

- name: Enable Docker service
  systemd:
    name: docker
    enabled: yes
    state: started

- name: Verify Java installation
  command: java -version
  register: java_check
  changed_when: false

- name: Verify Docker installation
  command: docker --version
  register: docker_check
  changed_when: false

- name: Show installation summary
  debug:
    msg: |
      Java installation: {{ java_check.stderr_lines[0] }}
      Docker: {{ docker_check.stdout }}

Использование:

---
- name: Prepare server for Java application
  hosts: webservers
  become: yes
  roles:

    - prepare_java_server

Сценарий 2: Развёртывание Spring Boot приложения

Playbook для развёртывания Spring Boot приложения с использованием systemd.

roles/deploy_spring_boot/defaults/main.yml:

app_name: myapp
app_version: 1.0.0
app_user: appuser
app_group: appuser
app_home: /opt/myapp
app_port: 8080
app_jvm_memory: 1024m
artifact_repository: "https://repo.example.com/releases"
log_level: INFO
health_check_url: "http://localhost:{{ app_port }}/actuator/health"
health_check_timeout: 30

roles/deploy_spring_boot/tasks/main.yml:

---
- name: Create application user
  user:
    name: "{{ app_user }}"
    group: "{{ app_group }}"
    home: "{{ app_home }}"
    shell: /bin/false
    system: yes
  register: app_user_info

- name: Create application directories
  file:
    path: "{{ item }}"
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: '0755'
  loop:

    - "{{ app_home }}"
    - "{{ app_home }}/bin"
    - "{{ app_home }}/config"
    - "{{ app_home }}/logs"
    - "{{ app_home }}/previous"

- name: Check current version
  stat:
    path: "{{ app_home }}/bin/{{ app_name }}.jar"
  register: current_jar

- name: Backup current version
  copy:
    src: "{{ app_home }}/bin/{{ app_name }}.jar"
    dest: "{{ app_home }}/previous/{{ app_name }}.jar.{{ ansible_date_time.iso8601_basic_short }}"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    remote_src: yes
  when: current_jar.stat.exists

- name: Stop application for update
  systemd:
    name: "{{ app_name }}"
    state: stopped
  ignore_errors: yes
  when: current_jar.stat.exists

- name: Download new application version
  get_url:
    url: "{{ artifact_repository }}/{{ app_name }}-{{ app_version }}.jar"
    dest: "{{ app_home }}/bin/{{ app_name }}.jar"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: '0755'
  register: download_result

- name: Generate application configuration
  template:
    src: application.properties.j2
    dest: "{{ app_home }}/config/application.properties"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: '0640'
  register: config_result

- name: Create systemd service unit
  template:
    src: app.service.j2
    dest: "/etc/systemd/system/{{ app_name }}.service"
    mode: '0644'
  register: service_result

- name: Reload systemd daemon
  systemd:
    daemon_reload: yes
  when: service_result.changed

- name: Start application service
  systemd:
    name: "{{ app_name }}"
    enabled: yes
    state: started

- name: Wait for application to be healthy
  uri:
    url: "{{ health_check_url }}"
    method: GET
    status_code: 200
  register: health_check
  until: health_check.status == 200
  retries: 10
  delay: 3
  ignore_errors: yes

- name: Health check result
  debug:
    msg: "Application health check: {{ health_check.status | default('failed') }}"

- name: Verify service is running
  command: systemctl is-active {{ app_name }}
  register: service_status
  changed_when: false

- name: Show deployment summary
  debug:
    msg: |
      Application: {{ app_name }}:{{ app_version }}
      Location: {{ app_home }}
      Service: {{ app_name }}
      User: {{ app_user }}
      Port: {{ app_port }}
      Status: {{ service_status.stdout }}
      Health: {{ health_check.status | default('unknown') }}

roles/deploy_spring_boot/templates/application.properties.j2:

# Application Configuration
spring.application.name={{ app_name }}
server.port={{ app_port }}

# Database
spring.datasource.url={{ db_url }}
spring.datasource.username={{ db_user }}
spring.datasource.password={{ db_password }}

# Logging
logging.level.root={{ log_level }}
logging.file.name={{ app_home }}/logs/application.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n

# Actuator (Health & Metrics)
management.endpoints.web.exposure.include=health,metrics,info
management.endpoint.health.show-details=when-authorized

roles/deploy_spring_boot/templates/app.service.j2:

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

[Service]
Type=simple
User={{ app_user }}
Group={{ app_group }}
WorkingDirectory={{ app_home }}

ExecStart=/usr/bin/java \
    -Xmx{{ app_jvm_memory }} \
    -Xms{{ app_jvm_memory }} \
    -Dspring.config.location=file:{{ app_home }}/config/application.properties \
    -jar {{ app_home }}/bin/{{ app_name }}.jar

Restart=on-failure
RestartSec=10

StandardOutput=journal
StandardError=journal
SyslogIdentifier={{ app_name }}

[Install]
WantedBy=multi-user.target

Playbook для использования:

---
- name: Deploy Spring Boot application
  hosts: webservers
  become: yes
  vars:
    app_name: myapp
    app_version: "{{ deploy_version }}"
    db_url: "jdbc:postgresql://{{ groups['databases'][0] }}:5432/myapp_db"
    db_user: appuser
    db_password: "{{ vault_db_password }}"
  roles:

    - deploy_spring_boot

Запуск:

ansible-playbook deploy.yml \
  -i inventories/prod \
  -e "deploy_version=1.2.3" \
  --vault-password-file ~/.vault_pass

Сценарий 3: Развёртывание через Docker Compose

Playbook для развёртывания микросервисов через Docker Compose.

roles/deploy_docker_compose/defaults/main.yml:

app_name: microservice
docker_registry: registry.example.com
app_version: latest
compose_dir: /opt/docker-compose
services:

  - app
  - postgres
  - redis

roles/deploy_docker_compose/tasks/main.yml:

---
- name: Create compose directory
  file:
    path: "{{ compose_dir }}"
    state: directory
    mode: '0755'

- name: Stop running containers
  docker_compose:
    project_src: "{{ compose_dir }}"
    state: absent
  ignore_errors: yes

- name: Generate docker-compose.yml
  template:
    src: docker-compose.yml.j2
    dest: "{{ compose_dir }}/docker-compose.yml"
    mode: '0644'

- name: Create .env file with configuration
  template:
    src: .env.j2
    dest: "{{ compose_dir }}/.env"
    mode: '0600'

- name: Pull latest images
  docker_image:
    name: "{{ docker_registry }}/{{ app_name }}"
    tag: "{{ app_version }}"
    source: pull
    force_source: yes

- name: Start services with docker-compose
  docker_compose:
    project_src: "{{ compose_dir }}"
    state: present
    pull: yes
    recreate: smart

- name: Wait for services to be ready
  pause:
    seconds: 5

- name: Verify containers are running
  docker_container_info:
    name: "{{ compose_dir.split('/')[-1] }}_{{ item }}_1"
  register: container_status
  loop: "{{ services }}"
  failed_when: container_status.containers | length == 0 or not container_status.containers[0].State.Running

- name: Health check
  uri:
    url: "http://localhost:{{ app_port }}/health"
    status_code: 200
  retries: 5
  delay: 2
  ignore_errors: yes

- name: Show deployment status
  debug:
    msg: |
      Services deployed: {{ services | join(', ') }}
      Location: {{ compose_dir }}
      Docker Compose file updated

roles/deploy_docker_compose/templates/docker-compose.yml.j2:

version: '3.8'

services:
  app:
    image: {{ docker_registry }}/{{ app_name }}:{{ app_version }}
    container_name: {{ app_name }}
    ports:

      - "{{ app_port }}:8080"
    environment:

      - APP_ENV={{ environment }}
      - DB_HOST=postgres
      - REDIS_HOST=redis
    depends_on:

      - postgres
      - redis
    restart: unless-stopped

  postgres:
    image: postgres:14-alpine
    container_name: {{ app_name }}-db
    environment:

      - POSTGRES_DB={{ db_name }}
      - POSTGRES_USER={{ db_user }}
      - POSTGRES_PASSWORD={{ db_password }}
    volumes:

      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: {{ app_name }}-cache
    restart: unless-stopped

volumes:
  postgres_data:

Сценарий 4: Отладка и проверка конфигурации

Утилиты для диагностики проблем.

maintenance.yml:

---
- name: Debug and verify configuration
  hosts: webservers
  gather_facts: yes
  tasks:

    - name: Show system information
      debug:
        msg: |
          OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
          Kernel: {{ ansible_kernel }}
          Architecture: {{ ansible_machine }}
          Python: {{ ansible_python_version }}

    - name: Check connectivity to database
      wait_for:
        host: "{{ db_host }}"
        port: 5432
        timeout: 5
      ignore_errors: yes
      register: db_check

    - name: Database connectivity result
      debug:
        msg: "Database accessible: {{ db_check is succeeded }}"

    - name: Check application service status
      service_facts:

    - name: Show service status
      debug:
        var: ansible_facts.services["{{ app_name }}.service"]

    - name: Get application logs
      command: "journalctl -u {{ app_name }} -n 50 --no-pager"
      register: app_logs

    - name: Show recent logs
      debug:
        msg: "{{ app_logs.stdout_lines }}"

    - name: Check disk space
      shell: df -h | grep -E "/$|/opt|/var"
      register: disk_space

    - name: Show disk usage
      debug:
        msg: "{{ disk_space.stdout_lines }}"

    - name: Check memory usage
      shell: free -h
      register: memory

    - name: Show memory
      debug:
        msg: "{{ memory.stdout_lines }}"

Запуск для отладки:

ansible-playbook maintenance.yml -i inventories/prod --limit web1.example.com -vvv

Проверка и валидация перед production

Перед развёртыванием в production запусти проверку:

# Синтаксис
ansible-playbook deploy.yml --syntax-check

# Dry-run с diff
ansible-playbook deploy.yml -i inventories/stage --check --diff

# Lint
ansible-lint deploy.yml

# Запуск на одном хосте
ansible-playbook deploy.yml -i inventories/prod --limit web1.example.com

# Логирование
ansible-playbook deploy.yml -i inventories/prod -vvv > deploy.log

Откат версии

Playbook для отката на предыдущую версию:

---
- name: Rollback application
  hosts: webservers
  become: yes
  tasks:

    - name: Find previous version
      find:
        paths: "{{ app_home }}/previous"
        patterns: "{{ app_name }}.jar.*"
      register: previous_versions

    - name: Get latest backup
      set_fact:
        latest_backup: "{{ previous_versions.files | sort(attribute='mtime', reverse=true) | first }}"

    - name: Stop current service
      systemd:
        name: "{{ app_name }}"
        state: stopped

    - name: Restore previous version
      copy:
        src: "{{ latest_backup.path }}"
        dest: "{{ app_home }}/bin/{{ app_name }}.jar"
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        remote_src: yes

    - name: Start application
      systemd:
        name: "{{ app_name }}"
        state: started

    - name: Verify rollback
      uri:
        url: "http://localhost:{{ app_port }}/health"
        status_code: 200
      retries: 5
      delay: 2

Ключевые выводы

  • Используй роли для переиспользования кода на разных проектах.
  • Всегда проверяй health endpoints после развёртывания.
  • Используй systemd для управления приложениями, Docker Compose для микросервисов.
  • Сохраняй backup перед обновлением версии для возможности отката.
  • Разделяй плейбуки на конфигурацию, развёртывание и поддержку.
  • Используй dry-run и diff перед production развёртыванием.
  • Логируй всё для отладки проблем.

CI/CD: базовые принципы, структура пайплайна и ключевые сущности

Определения и назначение

Continuous Integration (CI) — это процесс автоматической сборки, тестирования и валидации кода при каждом коммите или pull request. Задача CI: обнаружить ошибки как можно раньше, до попадания кода в основную ветку.

Continuous Delivery (CD) — это автоматизация подготовки кода к релизу: сборка артефакта, создание образа, размещение в registry, готовность к развёртыванию. Код остаётся под контролем и может быть развёрнут в любой момент (обычно вручную).

Continuous Deployment — полная автоматизация, включая автоматический деплой в production после прохождения всех проверок (используется реже в Enterprise).

Для backend-разработчика обычно важны CI и подготовка к CD; сам деплой часто делегирован DevOps.

Структура типичного пайплайна

Пайплайн состоит из последовательности стадий (stages), каждая из которых содержит одну или несколько задач (jobs/steps):

Trigger (push, PR, manual)
  ↓
Checkout → получение кода из репозитория
  ↓
Build → компиляция, сборка артефакта или jar/war
  ↓
Unit Tests → тестирование отдельных компонентов
  ↓
Code Quality → статический анализ (SonarQube, linters)
  ↓
Integration Tests → тестирование взаимодействия компонентов
  ↓
Build Docker Image → упаковка приложения в контейнер
  ↓
Push to Registry → публикация образа в Docker Registry/ECR/GCR
  ↓
Deploy to Dev/Stage → разворачивание в тестовую среду (опционально)
  ↓
Deploy to Production → разворачивание в боевую среду (опционально, часто вручную)

Для pull request пайплайны обычно содержат только ранние стадии (Build, Unit Tests, Code Quality). Для коммитов в main/master ветку — полный набор с деплоем.

Ключевые сущности CI/CD-систем

Pipeline (Workflow) — полный процесс от триггера до финального результата. В GitHub Actions называется workflow, в GitLab CI — pipeline, в Jenkins — job/pipeline.

Stage (Phase) — логическое группирование связанных задач. Stages выполняются последовательно, jobs внутри stage могут выполняться параллельно.

Job (Task/Step) — атомарная единица работы: команда, скрипт или вызов готового action/plugin. Step — более мелкая единица (в GitHub Actions step входит в job).

Runner/Agent — физический или виртуальный компьютер, на котором выполняются команды. Может быть:

  • Управляемый хостом (GitHub-hosted runners, GitLab shared runners) — простой, медленнее.
  • Самоуправляемый (self-hosted runner, private agent) — быстрее, требует обслуживания.

Артефакты — результаты выполнения job (собранные jar/war, отчёты тестов, Docker образы, логи). Артефакты передаются между stages и сохраняются для анализа.

Кеш — промежуточные данные, сохраняющиеся между запусками пайплайна для ускорения: загруженные dependency (Maven, Gradle), кешированные слои Docker. Кеш восстанавливается на основе ключей.

Environment (Окружение) — набор переменных окружения, секретов и конфигурации для конкретного этапа развёртывания (dev, staging, production). Разные environment'ы могут использовать разные credentials и настройки.

Trigger (Триггер) — событие, запускающее пайплайн:

  • push — коммит в ветку.
  • pull_request — создание или обновление PR.
  • schedule — периодический запуск по расписанию (крон).
  • manual — ручной запуск через UI.
  • webhook — внешнее событие (например, от другого сервиса).

Требования к Java/Kotlin-проекту

Build tool (Gradle/Maven)

Проект должен:

  • Иметь корректный build.gradle или pom.xml с зависимостями.
  • Поддерживать сборку из CLI без GUI (не требовать IDE).
  • Быть настроен так, чтобы сборка была воспроизводима: одинаковые версии зависимостей при повторных запусках.

Типичная сборка:

./gradlew clean build  # или mvn clean install

Структура проекта

project-root/
├── src/main/java (или src/main/kotlin)
├── src/test/java (или src/test/kotlin)
├── build.gradle (или pom.xml)
├── Dockerfile (для сборки образа)
├── .github/workflows/ (GitHub Actions)
├── .gitlab-ci.yml (GitLab CI)
├── Jenkinsfile (Jenkins)
└── docker-compose.yml (локальная разработка)

Конфигурация тестов

Тесты должны:

  • Быть разделены на unit тесты (быстрые, in-memory) и интеграционные (требуют БД, кешей и т.п.).
  • Помечены аннотациями @Tag (JUnit 5) или категориями (JUnit 4) для раздельного запуска.
  • Иметь порядок выполнения, независимый от других тестов (идемпотентность).
./gradlew test                    # только unit тесты
./gradlew integrationTest         # интеграционные тесты

Артефакты и версионирование

Собранный jar или war должен:

  • Иметь чёткую версию: app-1.2.3.jar.
  • Содержать манифест с метаданными.
  • Быть готовым к публикации в artifact repository (Nexus, Artifactory).

Версия часто берётся из git-tag или версии в build.gradle:

version = '1.2.3'

или генерируется из commit SHA:

VERSION="1.2.3+git.${GIT_SHA:0:7}"

Логирование и мониторинг

Код должен:

  • Логировать значимые события (стартап, ошибки, запросы) в структурированном виде (JSON).
  • Поддерживать запуск с разными уровнями логирования (DEBUG, INFO, WARN, ERROR).
  • Содержать health-check endpoint (GET /actuator/health), на котором CI может проверить готовность.

Переменные окружения

Приложение должно конфигурироваться через переменные окружения или properties-файлы:

# application.properties
app.name=${APP_NAME:my-service}
app.port=${APP_PORT:8080}
db.url=${DB_URL:localhost:5432}

Секреты не должны быть в коде; они передаются через переменные окружения в CI/CD-системе.

Типичный цикл запуска пайплайна

  1. Trigger: Разработчик делает push или создаёт PR.
  2. Checkout: Система клонирует репозиторий и переключается на нужную ветку/коммит.
  3. Setup: Установка Java, Gradle/Maven, зависимостей.
  4. Compile: Компиляция исходного кода.
  5. Unit Tests: Запуск быстрых тестов.
  6. Code Analysis: Проверка кодовой базы инструментами (SonarQube, Checkstyle, SpotBugs).
  7. Integration Tests: Запуск интеграционных тестов (если есть).
  8. Build Artifact: Сборка jar/war или Docker-образа.
  9. Publish: Публикация в registry (если нужна).
  10. Deploy: Развёртывание в тестовую/боевую среду (если нужно).
  11. Report: Сбор отчётов (тесты, покрытие, анализ) и уведомление разработчика.

Каждый шаг может завершиться ошибкой, что остановит весь пайплайн. Результат доступен в UI системы CI/CD, уведомления отправляются в Slack/email.

Типичные проблемы в пайплайнах и их решения

Проблема Причина Решение
Долгая сборка (>10 мин) Медленные тесты, много зависимостей Кеширование, распараллеливание, разделение на unit/integration
Непредсказуемые падения Race conditions в тестах, зависимость от порядка Изоляция тестов, правильная инициализация/cleanup, testcontainers
Проблемы с artifact Разные версии при разных запусках Зафиксировать версии зависимостей, использовать BOM
Отказ в доступе к registry Неверные credentials Проверить secrets в CI/CD, использовать механизм OIDC (вместо токенов)
Пайплайн зависит от external API Внешний сервис может быть недоступен Использовать mock'и в тестах, testcontainers, или пропускать тесты с флагом

CI/CD: GitHub Actions и GitLab CI — git-based пайплайны для Java/Kotlin

GitHub Actions: основные концепции

GitHub Actions встроена в GitHub и использует YAML-конфигурацию в .github/workflows/*.yml. Основные сущности:

Workflow — файл .github/workflows/ci.yml, описывающий весь процесс автоматизации.

Event (Триггер) — что запускает workflow:

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:

    - cron: '0 2 * * *'  # каждый день в 02:00 UTC
  workflow_dispatch:  # ручной запуск

Job — независимая единица работы, выполняется на отдельном runner'е. Jobs могут выполняться параллельно или последовательно (через needs).

Step — отдельная команда или action внутри job'а. Steps выполняются последовательно.

Action — переиспользуемый блок кода, написанный на JavaScript, Docker или составной. Примеры: actions/checkout@v3, actions/setup-java@v3.

Runner — машина, на которой выполняется job. Встроенные: ubuntu-latest, windows-latest, macos-latest. Самоуправляемые: self-hosted.

Пример GitHub Actions для Java/Kotlin сервиса

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:

      - uses: actions/checkout@v3
      
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle  # кеширование gradle зависимостей
      
      - name: Build with Gradle
        run: ./gradlew clean build -x test
      
      - name: Run unit tests
        run: ./gradlew test
      
      - name: Run integration tests
        run: ./gradlew integrationTest
      
      - name: SonarQube scan
        run: ./gradlew sonarqube
        env:
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
          SONAR_LOGIN: ${{ secrets.SONAR_LOGIN }}
      
      - name: Upload test results
        if: always()  # выполнить даже если тесты упали
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: build/test-results/
      
      - name: Publish to artifact repository
        if: github.ref == 'refs/heads/main'
        run: ./gradlew publish
        env:
          NEXUS_URL: ${{ secrets.NEXUS_URL }}
          NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
          NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}

  docker-build:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:

      - uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      - name: Extract version
        id: version
        run: |
          VERSION=$(cat build.gradle | grep "version = " | sed "s/.*version = '\([^']*\)'.*/\1/")
          echo "version=$VERSION" >> $GITHUB_OUTPUT
      
      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: |
            ${{ secrets.DOCKER_USERNAME }}/my-service:${{ steps.version.outputs.version }}
            ${{ secrets.DOCKER_USERNAME }}/my-service:latest
          cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/my-service:buildcache
          cache-to: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/my-service:buildcache,mode=max

GitLab CI: основные концепции

GitLab CI использует .gitlab-ci.yml в корне репозитория. Концепции частично пересекаются с GitHub Actions, но есть отличия.

Pipeline — весь процесс, аналогично workflow в GitHub.

Stage — логическое группирование jobs, выполняются последовательно.

Job — задача внутри stage'а. Jobs в одном stage выполняются параллельно.

Runner — машина, выполняющая job. Обычно самоуправляемый, но есть shared runners GitLab.

Artifacts — файлы, передающиеся между jobs и stages (явно указываются).

Cache — данные, кешируемые между запусками (зависимости, промежуточные результаты).

Variables — переменные окружения, могут быть глобальные, на уровне job или группы.

Пример GitLab CI для Java/Kotlin сервиса

variables:
  GRADLE_OPTS: "-Dorg.gradle.daemon=false"
  DOCKER_DRIVER: overlay2
  DOCKER_BUILDKIT: 1

stages:

  - build
  - test
  - quality
  - package
  - deploy

build:
  stage: build
  image: gradle:7.6-jdk17
  script:

    - ./gradlew clean build -x test
  artifacts:
    paths:

      - build/libs/
      - build/classes/
    expire_in: 1 hour
  cache:
    key: gradle-cache
    paths:

      - .gradle/
  only:

    - main
    - develop
    - merge_requests

unit_tests:
  stage: test
  image: gradle:7.6-jdk17
  script:

    - ./gradlew test
  artifacts:
    reports:
      junit: build/test-results/test/*.xml
    paths:

      - build/reports/tests/test/
    expire_in: 30 days
  cache:
    key: gradle-cache
    paths:

      - .gradle/
  coverage: '/Test execution finished with \d+ failures, \d+ skipped. Total coverage: ([0-9.]+)%/'
  dependencies:

    - build

integration_tests:
  stage: test
  image: gradle:7.6-jdk17
  services:

    - postgres:14
    - redis:7
  variables:
    POSTGRES_DB: test_db
    POSTGRES_USER: test_user
    POSTGRES_PASSWORD: test_password
    REDIS_HOST: redis
  script:

    - ./gradlew integrationTest
  artifacts:
    reports:
      junit: build/test-results/integrationTest/*.xml
    paths:

      - build/reports/tests/integrationTest/
    expire_in: 30 days
  cache:
    key: gradle-cache
    paths:

      - .gradle/
  dependencies:

    - build
  only:

    - main
    - merge_requests

sonarqube:
  stage: quality
  image: gradle:7.6-jdk17
  script:

    - ./gradlew sonarqube -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN
  cache:
    key: gradle-cache
    paths:

      - .gradle/
  dependencies:

    - build
    - unit_tests
  only:

    - main
    - merge_requests
  allow_failure: true

docker_build:
  stage: package
  image: docker:20.10
  services:

    - docker:20.10-dind
  before_script:

    - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
  script:

    - VERSION=$(cat build.gradle | grep "version = " | sed "s/.*version = '\([^']*\)'.*/\1/")
    - docker build -t $DOCKER_USERNAME/my-service:$VERSION .
    - docker tag $DOCKER_USERNAME/my-service:$VERSION $DOCKER_USERNAME/my-service:latest
    - docker push $DOCKER_USERNAME/my-service:$VERSION
    - docker push $DOCKER_USERNAME/my-service:latest
  dependencies:

    - build
  only:

    - main

deploy_dev:
  stage: deploy
  image: alpine:latest
  before_script:

    - apk add --no-cache openssh-client
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - ssh-keyscan -H $DEV_SERVER_HOST >> ~/.ssh/known_hosts
  script:

    - VERSION=$(cat build.gradle | grep "version = " | sed "s/.*version = '\([^']*\)'.*/\1/")
    - ssh $DEV_USER@$DEV_SERVER_HOST "docker pull $DOCKER_USERNAME/my-service:$VERSION && docker-compose -f /opt/services/docker-compose.yml up -d my-service"
  dependencies:

    - docker_build
  only:

    - main
  environment:
    name: development
    url: https://dev.example.com

Сравнение GitHub Actions и GitLab CI

Аспект GitHub Actions GitLab CI
Конфиг .github/workflows/*.yml .gitlab-ci.yml
Параллелизм Jobs параллельны, можно контролировать Jobs в stage параллельны, stages последовательны
Artifacts Сохраняются отдельно, expires Встроены, expires, передаются между jobs
Кеш Встроен, простой механизм Встроен, более гибкий
Secrets Environment secrets, organization secrets Variables, masked variables
Runners GitHub-hosted (удобно) или self-hosted Обычно self-hosted (требует обслуживания)
Цена Бесплатно для public, лимиты для private Бесплатно для public/private (самоуправляемые runner)
Интеграция Только с GitHub Только с GitLab

Общие паттерны для CI пайплайнов Java/Kotlin

Линтеры и статический анализ

# GitHub Actions
- name: Run Checkstyle
  run: ./gradlew checkstyleMain

- name: Run SpotBugs
  run: ./gradlew spotbugsMain

- name: Run ktlint
  run: ./gradlew ktlintCheck
# GitLab CI
lint:
  stage: quality
  image: gradle:7.6-jdk17
  script:

    - ./gradlew checkstyleMain spotbugsMain ktlintCheck
  cache:
    key: gradle-cache
    paths:

      - .gradle/

Раздельный запуск unit и интеграционных тестов

// build.gradle
sourceSets {
    integrationTest {
        java {
            compileClasspath += main.output + test.output
            runtimeClasspath += main.output + test.output
        }
    }
}

task integrationTest(type: Test) {
    testClassesDirs = sourceSets.integrationTest.output.classesDirs
    classpath = sourceSets.integrationTest.runtimeClasspath
}

Условное выполнение jobs

GitHub Actions:

- name: Publish
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  run: ./gradlew publish

GitLab CI:

publish:
  stage: package
  script:

    - ./gradlew publish
  only:

    - main
  except:

    - schedules

Матричное тестирование (несколько версий JDK)

GitHub Actions:

strategy:
  matrix:
    java-version: [11, 17, 21]
steps:

  - uses: actions/setup-java@v3
    with:
      java-version: ${{ matrix.java-version }}

GitLab CI:

.test_template: &test_template
  image: gradle:7.6-jdk${JAVA_VERSION}
  script:

    - ./gradlew test

test_jdk11:
  <<: *test_template
  variables:
    JAVA_VERSION: "11"

test_jdk17:
  <<: *test_template
  variables:
    JAVA_VERSION: "17"

test_jdk21:
  <<: *test_template
  variables:
    JAVA_VERSION: "21"

Использование Docker Compose для зависимостей

# GitLab CI
services:

  - name: postgres:14
    alias: postgres
  - name: redis:7
    alias: redis

variables:
  POSTGRES_DB: test_db
  POSTGRES_USER: test_user
  POSTGRES_PASSWORD: password
  POSTGRES_HOST_AUTH_METHOD: trust
  REDIS_HOST: redis

script:

  - ./gradlew integrationTest
# GitHub Actions
services:
  postgres:
    image: postgres:14
    env:
      POSTGRES_DB: test_db
      POSTGRES_USER: test_user
      POSTGRES_PASSWORD: password
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5
  redis:
    image: redis:7
    options: >-
      --health-cmd "redis-cli ping"
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

Оптимизация пайплайнов

Кеширование зависимостей: Gradle и Maven автоматически кешируют скачанные зависимости. В GitHub Actions используйте cache: gradle, в GitLab CI — явно описывайте пути кеша.

Parallelization: Запускайте unit тесты, линтеры и static анализ параллельно (если runner'ов достаточно).

Условное выполнение: Не запускайте дорогостоящие jobs (Docker build, deploy) для PR'ов или веток, отличных от main/master.

Ранний фейл: Запускайте быстрые проверки (lint, unit tests) раньше, чтобы не тратить время на долгие stages, если уже есть ошибки.

Динамическое определение версии: Извлекайте версию из git-tag или build.gradle один раз и используйте в других jobs:

GitHub Actions:

- id: version
  run: echo "version=$(cat build.gradle | grep version | sed ...)" >> $GITHUB_OUTPUT
- uses: docker/build-push-action@v4
  with:
    tags: myrepo/myapp:${{ steps.version.outputs.version }}

CI/CD: Jenkins для backend-разработчика — declarative pipelines и Jenkinsfile

Основные понятия Jenkins

Jenkins Controller (Master) — центральный сервер, хранит конфигурацию пайплайнов, управляет запусками, хранит логи и результаты.

Jenkins Agent (Node, Slave) — рабочая машина, на которой выполняются job'ы. Controller распределяет задачи между agent'ами. Agent'ы могут быть постоянные (self-hosted) или временные (Docker-контейнеры, облачные инстансы).

Pipeline — сценарий, описывающий процесс CI/CD. Есть два формата:

  • Scripted Pipeline: Groovy-скрипт, полная свобода, сложнее в обслуживании.
  • Declarative Pipeline: Структурированный формат YAML-подобного синтаксиса, проще, рекомендуется.

Jenkinsfile — файл с описанием pipeline'а, обычно находится в корне репозитория.

Stage — логический этап пайплайна (Build, Test, Quality, Package, Deploy).

Step — отдельный action/команда внутри stage'а.

Post — блок, выполняющийся после завершения stage'а или всего pipeline'а (успешно или с ошибкой).

When — условие, при котором выполняется stage или step.

Структура Declarative Pipeline

pipeline {
    agent { /* где выполнять */ }
    
    options { /* глобальные опции */ }
    
    parameters { /* входные параметры */ }
    
    environment { /* переменные окружения */ }
    
    stages {
        stage('Stage Name') {
            when { /* условие */ }
            steps {
                // команды и действия
            }
            post {
                success { }
                failure { }
                always { }
            }
        }
    }
    
    post {
        success { /* если всё ок */ }
        failure { /* если ошибка */ }
        always { /* всегда */ }
    }
}

Agent: где выполняется job

// На любом доступном agent'е
agent any

// На конкретном agent'е по label'у
agent {
    label 'linux && java17'
}

// В Docker контейнере
agent {
    docker {
        image 'gradle:7.6-jdk17'
        args '-v /var/run/docker.sock:/var/run/docker.sock'  // доступ к Docker daemon'у
    }
}

// Kubernetes pod (если Jenkins работает в K8s)
agent {
    kubernetes {
        yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:

  - name: gradle
    image: gradle:7.6-jdk17
    command: ['cat']
    tty: true
        '''
    }
}

// Без agent (используется controller)
agent none

Пример Jenkinsfile для Java/Kotlin сервиса

pipeline {
    agent {
        docker {
            image 'gradle:7.6-jdk17'
            args '-v /var/run/docker.sock:/var/run/docker.sock -e GRADLE_OPTS="-Dorg.gradle.daemon=false"'
        }
    }

    options {
        timestamps()  // добавить временные метки к логам
        timeout(time: 1, unit: 'HOURS')  // максимальное время выполнения
        disableConcurrentBuilds()  // не запускать несколько build'ов параллельно
        buildDiscarder(logRotator(numToKeepStr: '30'))  // хранить только 30 последних build'ов
    }

    parameters {
        booleanParam(
            name: 'SKIP_TESTS',
            defaultValue: false,
            description: 'Skip unit and integration tests'
        )
        choice(
            name: 'DEPLOY_ENV',
            choices: ['dev', 'staging', 'production'],
            description: 'Deploy environment'
        )
    }

    environment {
        GRADLE_HOME = "${env.WORKSPACE}/.gradle"
        APP_VERSION = sh(script: "cat build.gradle | grep version | sed \"s/.*version = '\\([^']*\\)'.*/\\1/\"", returnStdout: true).trim()
        DOCKER_REGISTRY = 'docker.io'
        DOCKER_IMAGE = "${DOCKER_REGISTRY}/mycompany/my-service"
        NEXUS_URL = credentials('nexus-url')
        NEXUS_CREDENTIALS = credentials('nexus-credentials')
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    echo "Repository: ${env.GIT_URL}"
                    echo "Branch: ${env.GIT_BRANCH}"
                    echo "Commit: ${env.GIT_COMMIT}"
                }
            }
        }

        stage('Compile') {
            steps {
                script {
                    echo "Building with Gradle ${APP_VERSION}"
                    sh './gradlew clean build -x test --no-daemon'
                }
            }
        }

        stage('Unit Tests') {
            when {
                expression { params.SKIP_TESTS == false }
            }
            steps {
                script {
                    echo "Running unit tests"
                    sh './gradlew test --no-daemon'
                }
            }
            post {
                always {
                    junit 'build/test-results/test/*.xml'
                    publishHTML([
                        reportDir: 'build/reports/tests/test',
                        reportFiles: 'index.html',
                        reportName: 'Unit Test Report'
                    ])
                }
            }
        }

        stage('Code Quality Analysis') {
            when {
                expression { params.SKIP_TESTS == false }
            }
            steps {
                script {
                    echo "Running Checkstyle, SpotBugs, and ktlint"
                    sh './gradlew checkstyleMain spotbugsMain ktlintCheck --no-daemon'
                }
            }
            post {
                always {
                    checkstyle(
                        pattern: 'build/reports/checkstyle/main.xml'
                    )
                    recordIssues(
                        enabledForFailure: true,
                        tool: spotBugs(
                            pattern: 'build/reports/spotbugs/main.xml'
                        )
                    )
                }
            }
        }

        stage('Integration Tests') {
            when {
                expression { params.SKIP_TESTS == false }
            }
            steps {
                script {
                    echo "Running integration tests"
                    sh './gradlew integrationTest --no-daemon'
                }
            }
            post {
                always {
                    junit 'build/test-results/integrationTest/*.xml'
                    publishHTML([
                        reportDir: 'build/reports/tests/integrationTest',
                        reportFiles: 'index.html',
                        reportName: 'Integration Test Report'
                    ])
                }
            }
        }

        stage('SonarQube Analysis') {
            when {
                expression { params.SKIP_TESTS == false && env.GIT_BRANCH == 'main' }
            }
            steps {
                script {
                    withSonarQubeEnv('SonarQube') {
                        sh './gradlew sonarqube'
                    }
                }
            }
        }

        stage('Build Artifact') {
            steps {
                script {
                    echo "Publishing artifact to Nexus"
                    sh './gradlew publish --no-daemon'
                }
            }
        }

        stage('Build Docker Image') {
            when {
                expression { env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'develop' }
            }
            steps {
                script {
                    echo "Building Docker image: ${DOCKER_IMAGE}:${APP_VERSION}"
                    sh '''
                        docker build \
                            --build-arg VERSION=${APP_VERSION} \
                            -t ${DOCKER_IMAGE}:${APP_VERSION} \
                            -t ${DOCKER_IMAGE}:latest \
                            .
                    '''
                }
            }
        }

        stage('Push to Registry') {
            when {
                expression { env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'develop' }
            }
            steps {
                script {
                    withCredentials([usernamePassword(
                        credentialsId: 'docker-registry-credentials',
                        usernameVariable: 'DOCKER_USER',
                        passwordVariable: 'DOCKER_PASS'
                    )]) {
                        sh '''
                            echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin
                            docker push ${DOCKER_IMAGE}:${APP_VERSION}
                            docker push ${DOCKER_IMAGE}:latest
                        '''
                    }
                }
            }
        }

        stage('Deploy to Dev') {
            when {
                expression { env.GIT_BRANCH == 'develop' && currentBuild.result != 'FAILURE' }
            }
            steps {
                script {
                    echo "Deploying to Dev environment"
                    sh '''
                        ansible-playbook \
                            -i inventories/dev \
                            -e docker_image=${DOCKER_IMAGE}:${APP_VERSION} \
                            playbooks/deploy.yml
                    '''
                }
            }
        }

        stage('Deploy to Staging') {
            when {
                expression { env.GIT_BRANCH == 'main' && params.DEPLOY_ENV in ['staging', 'production'] }
            }
            input {
                message "Deploy to Staging?"
                ok "Deploy"
                submitter "admin,devops"
            }
            steps {
                script {
                    echo "Deploying to Staging"
                    sh '''
                        ansible-playbook \
                            -i inventories/staging \
                            -e docker_image=${DOCKER_IMAGE}:${APP_VERSION} \
                            playbooks/deploy.yml
                    '''
                }
            }
        }

        stage('Deploy to Production') {
            when {
                expression { env.GIT_BRANCH == 'main' && params.DEPLOY_ENV == 'production' }
            }
            input {
                message "Deploy to Production? This is a critical action!"
                ok "Deploy to Prod"
                submitter "admin"
            }
            steps {
                script {
                    echo "Deploying to Production"
                    sh '''
                        ansible-playbook \
                            -i inventories/production \
                            -e docker_image=${DOCKER_IMAGE}:${APP_VERSION} \
                            playbooks/deploy.yml
                    '''
                }
            }
        }
    }

    post {
        always {
            // Архивировать логи
            archiveArtifacts artifacts: 'build/logs/**/*.log', allowEmptyArchive: true
            
            // Очистить рабочую директорию
            cleanWs()
        }
        success {
            script {
                echo "Pipeline completed successfully"
                // Отправить уведомление в Slack
                slackSend(
                    channel: '#ci-notifications',
                    color: 'good',
                    message: "✅ Build #${env.BUILD_NUMBER} Success\nService: ${DOCKER_IMAGE}\nVersion: ${APP_VERSION}\n${env.BUILD_URL}"
                )
            }
        }
        failure {
            script {
                echo "Pipeline failed"
                slackSend(
                    channel: '#ci-notifications',
                    color: 'danger',
                    message: "❌ Build #${env.BUILD_NUMBER} Failed\n${env.BUILD_URL}"
                )
            }
        }
    }
}

Работа с credentials в Jenkins

Secrets хранятся в Jenkins Credentials Store и не должны находиться в коде.

Использование credentials в pipeline'е

// Username/Password
withCredentials([usernamePassword(
    credentialsId: 'my-credentials',
    usernameVariable: 'USERNAME',
    passwordVariable: 'PASSWORD'
)]) {
    sh 'echo $USERNAME:$PASSWORD'
}

// Secret text
withCredentials([string(
    credentialsId: 'api-token',
    variable: 'API_TOKEN'
)]) {
    sh 'curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com'
}

// SSH Key
withCredentials([file(
    credentialsId: 'deploy-ssh-key',
    variable: 'SSH_KEY'
)]) {
    sh 'ssh -i $SSH_KEY user@host "docker pull..."'
}

// Встроенные credentials (для SonarQube, Maven Repository и т.п.)
withSonarQubeEnv('SonarQube') {
    sh './gradlew sonarqube'
}

// Environment credentials (для REST API)
withEnv(['NEXUS_URL=' + credentials('nexus-url')]) {
    sh './gradlew publish'
}

Интеграция с GitHub и GitLab

Webhook в Jenkins для автоматического запуска

GitHub:

  • URL: https://jenkins.example.com/github-webhook/
  • События: Push events, Pull request events

GitLab:

  • URL: https://jenkins.example.com/project/my-project
  • Events: Push events, Merge request events

Multibranch Pipeline (автоматический pipeline для каждой ветки)

// Jenkinsfile в каждой ветке разный, Jenkins автоматически запускает нужный
// При push в main → запускается Jenkinsfile из main ветки
// При push в feature/x → запускается Jenkinsfile из feature/x ветки

Конфигурация Multibranch Pipeline:

  1. New Item → Multibranch Pipeline
  2. Branch Sources: GitHub / GitLab
  3. Credentials: GitHub/GitLab API token
  4. Repository URL: репозиторий
  5. Behaviors: Auto-register webhook, Discover PR'ы, основные ветки

When: условное выполнение

stage('Deploy') {
    when {
        // Выполнить только на main ветке
        branch 'main'
    }
    steps { sh 'deploy.sh' }
}

stage('Deploy') {
    when {
        // Выполнить на main или develop
        branch pattern: "^(main|develop)$", comparator: "REGEXP"
    }
    steps { sh 'deploy.sh' }
}

stage('Deploy') {
    when {
        // Выполнить только если предыдущий stage успешен
        expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
    }
    steps { sh 'deploy.sh' }
}

stage('Deploy') {
    when {
        // Выполнить только если параметр равен определённому значению
        expression { params.DEPLOY_ENV == 'production' }
    }
    steps { sh 'deploy.sh' }
}

stage('Deploy') {
    when {
        // Выполнить только если tag
        tag "v*"
    }
    steps { sh 'deploy.sh' }
}

stage('Deploy') {
    when {
        // Выполнить только если это не Pull Request
        not { changeRequest() }
    }
    steps { sh 'deploy.sh' }
}

stage('Deploy') {
    when {
        // Выполнить если были изменения в определённых файлах
        changeset "src/main/**"
    }
    steps { sh 'deploy.sh' }
}

Parameters: входные параметры для ручного запуска

parameters {
    string(
        name: 'ENVIRONMENT',
        defaultValue: 'dev',
        description: 'Target deployment environment'
    )
    
    choice(
        name: 'LOG_LEVEL',
        choices: ['DEBUG', 'INFO', 'WARN', 'ERROR'],
        description: 'Application log level'
    )
    
    booleanParam(
        name: 'RUN_TESTS',
        defaultValue: true,
        description: 'Run tests during build'
    )
    
    text(
        name: 'ADDITIONAL_FLAGS',
        defaultValue: '',
        description: 'Additional Gradle flags'
    )
}

stages {
    stage('Build') {
        steps {
            sh '''
                ./gradlew clean build \
                    -Dlog.level=${LOG_LEVEL} \
                    ${ADDITIONAL_FLAGS}
            '''
        }
    }
}

Обработка ошибок и retries

stage('Test') {
    steps {
        retry(3) {  // переповторить до 3 раз при ошибке
            sh './gradlew test'
        }
    }
}

stage('Deploy') {
    steps {
        timeout(time: 30, unit: 'MINUTES') {  // таймаут для step'а
            sh './deploy.sh'
        }
    }
}

stage('Network call') {
    steps {
        catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
            // Ошибка не остановит pipeline, но отметит stage как UNSTABLE
            sh 'curl -f https://unreliable-api.com'
        }
    }
}

Интеграция с artifact repositories

environment {
    NEXUS_URL = credentials('nexus-url')
    NEXUS_CREDENTIALS = credentials('nexus-credentials')
}

stages {
    stage('Publish') {
        steps {
            sh '''
                ./gradlew publish \
                    -Pnexus.url=${NEXUS_URL} \
                    -Pnexus.username=${NEXUS_CREDENTIALS_USR} \
                    -Pnexus.password=${NEXUS_CREDENTIALS_PSW}
            '''
        }
    }
}

CI/CD: Continuous Delivery — стратегии деплоя и инструменты CD

Стратегии деплоя

Rolling Deployment (Постепенная замена)

Новая версия приложения развёртывается постепенно, заменяя старые инстансы один за другим. Старая и новая версии работают одновременно.

Схема: v1 → (v1, v1, v2) → (v1, v2, v2) → (v2, v2, v2)

Преимущества: Нулевой downtime, можно откатиться, ресурсы не удваиваются.

Недостатки: Две версии одновременно (совместимость API, схема БД), откат сложнее, более долгий процесс.

# Kubernetes Rolling Deployment (по умолчанию)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # на 1 pod больше во время обновления
      maxUnavailable: 0    # всегда минимум replicas доступны
  selector:
    matchLabels:
      app: my-service
  template:
    metadata:
      labels:
        app: my-service
    spec:
      containers:

      - name: my-service
        image: docker.io/mycompany/my-service:v2.0.0
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/ready
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

Blue-Green Deployment (Синий-зелёный)

Две идентичные среды (Blue и Green). На Blue работает текущая версия (v1). Green подготавливается с новой версией (v2). После успешной проверки трафик переключается на Green полностью и мгновенно.

Схема: (Blue: v1) → (Blue: v1, Green: v2) → (Blue: old, Green: v2 active) → откат возможен (Green: v1)

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

Недостатки: Требует вдвое больше ресурсов, нужна синхронизация данных между средами, более сложная логика маршрутизации.

#!/bin/bash
# Пример Blue-Green деплоя на Docker Compose

# 1. Текущая версия (Blue)
docker-compose -f docker-compose.blue.yml up -d

# 2. Развернуть новую версию (Green)
docker-compose -f docker-compose.green.yml up -d

# 3. Проверить здоровье нового сервиса
health_check() {
    for i in {1..30}; do
        if curl -f http://localhost:8081/actuator/health > /dev/null 2>&1; then
            echo "Green is ready"
            return 0
        fi
        sleep 2
    done
    echo "Green failed health check"
    return 1
}

if health_check; then
    # 4. Переключить nginx/load balancer на Green
    sed -i 's/upstream backend { server localhost:8080; }/upstream backend { server localhost:8081; }/' /etc/nginx/nginx.conf
    nginx -s reload
    echo "Switched to Green (port 8081)"
    
    # 5. Можно остановить Blue после промежутка времени
    sleep 300
    docker-compose -f docker-compose.blue.yml down
else
    # Откатиться: остановить Green
    docker-compose -f docker-compose.green.yml down
    exit 1
fi

Canary Deployment (канареечный)

Новая версия развёртывается для небольшого процента пользователей (например, 5-10%), в то время как основной трафик идёт на старую версию. Если ошибок нет, процент постепенно увеличивается.

Схема: 95% v1 + 5% v2 → 80% v1 + 20% v2 → 50% v1 + 50% v2 → 0% v1 + 100% v2

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

Недостатки: Сложная логика маршрутизации, нужен мониторинг, более долгий процесс, версии должны быть совместимы.

# Kubernetes Canary с Argo Rollouts или Flagger
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: my-service
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-service
  progressDeadlineSeconds: 60
  service:
    port: 8080
  analysis:
    interval: 1m
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    metrics:

    - name: request-success-rate
      thresholdRange:
        min: 99
      interval: 1m
  skipAnalysis: false

Деплой Docker-образов

Публикация в Registry

# Собрать образ
docker build -t docker.io/mycompany/my-service:1.2.3 .
docker tag docker.io/mycompany/my-service:1.2.3 docker.io/mycompany/my-service:latest

# Залогиниться в registry
docker login docker.io -u $USERNAME -p $PASSWORD

# Отправить в registry
docker push docker.io/mycompany/my-service:1.2.3
docker push docker.io/mycompany/my-service:latest

# Использовать Docker Buildx для мультиплатформенных образов
docker buildx build \
    --platform linux/amd64,linux/arm64 \
    -t docker.io/mycompany/my-service:1.2.3 \
    --push .

Деплой на сервер через SSH

#!/bin/bash
# deploy.sh

set -e

DOCKER_IMAGE="docker.io/mycompany/my-service:${VERSION}"
DEPLOY_HOST="deploy@app.example.com"
DEPLOY_PATH="/opt/services"

echo "Deploying $DOCKER_IMAGE to $DEPLOY_HOST"

# 1. SSH на сервер и обновить образ
ssh $DEPLOY_HOST "cd $DEPLOY_PATH && docker pull $DOCKER_IMAGE"

# 2. Остановить старый контейнер и запустить новый
ssh $DEPLOY_HOST "
    docker stop my-service || true
    docker rm my-service || true
    docker run -d \
        --name my-service \
        -p 8080:8080 \
        -e JAVA_OPTS='-Xmx512m -Xms256m' \
        -e APP_ENV=production \
        -e LOG_LEVEL=INFO \
        --restart unless-stopped \
        --health-cmd='curl -f http://localhost:8080/actuator/health || exit 1' \
        --health-interval=30s \
        --health-timeout=3s \
        --health-retries=3 \
        $DOCKER_IMAGE
"

# 3. Проверить здоровье
ssh $DEPLOY_HOST "
    for i in {1..30}; do
        if curl -f http://localhost:8080/actuator/health; then
            echo 'Service is healthy'
            exit 0
        fi
        sleep 2
    done
    echo 'Service failed health check'
    exit 1
"

echo "Deployment successful"

Деплой с Ansible

# playbooks/deploy.yml
---
- hosts: app_servers
  become: yes
  vars:
    docker_image: "{{ docker_image }}"
    app_version: "{{ docker_image | regex_search(':(.+)$', '\\1') }}"
    app_port: 8080
    app_env: "{{ deploy_env | default('dev') }}"
  
  tasks:

    - name: Create app directory
      file:
        path: /opt/my-service
        state: directory
        mode: '0755'
    
    - name: Copy docker-compose.yml
      template:
        src: docker-compose.yml.j2
        dest: /opt/my-service/docker-compose.yml
        mode: '0644'
    
    - name: Pull latest image
      shell: docker pull {{ docker_image }}
      register: pull_result
    
    - name: Stop current service
      shell: |
        cd /opt/my-service
        docker-compose down --remove-orphans
      ignore_errors: yes
    
    - name: Start service with new image
      shell: |
        cd /opt/my-service
        docker-compose up -d
    
    - name: Wait for service to be healthy
      uri:
        url: "http://localhost:{{ app_port }}/actuator/health"
        status_code: 200
      register: result
      until: result.status == 200
      retries: 30
      delay: 2
    
    - name: Run smoke tests
      shell: |
        curl -f http://localhost:{{ app_port }}/api/test || exit 1
      register: smoke_test
      changed_when: false
    
    - name: Rollback on failure
      shell: |
        cd /opt/my-service
        docker-compose down
        docker-compose up -d
      when: smoke_test.rc != 0
      failed_when: true

# docker-compose.yml.j2
version: '3.9'
services:
  my-service:
    image: {{ docker_image }}
    container_name: my-service
    ports:

      - "{{ app_port }}:{{ app_port }}"
    environment:
      JAVA_OPTS: "-Xmx512m -Xms256m -XX:+UseG1GC"
      APP_ENV: {{ app_env }}
      LOG_LEVEL: INFO
      DB_URL: "jdbc:postgresql://postgres-db.example.com:5432/{{ app_env }}_db"
      REDIS_HOST: "redis.example.com"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:{{ app_port }}/actuator/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 30s
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "5"
    networks:

      - app-network

networks:
  app-network:
    driver: bridge

Деплой с docker-compose на сервер

version: '3.9'

services:
  my-service:
    image: docker.io/mycompany/my-service:${SERVICE_VERSION}
    container_name: my-service
    ports:

      - "8080:8080"
    environment:
      JAVA_OPTS: "-Xmx1g -Xms512m -XX:+UseG1GC"
      SPRING_DATASOURCE_URL: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
      SPRING_DATASOURCE_USERNAME: ${DB_USER}
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: ${REDIS_HOST}
      REDIS_PORT: ${REDIS_PORT}
      LOG_LEVEL: INFO
    depends_on:

      - db
      - redis
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    volumes:

      - /var/log/my-service:/var/log
    networks:

      - app-network
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "10"
  
  db:
    image: postgres:14-alpine
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:

      - pg-data:/var/lib/postgresql/data
    restart: unless-stopped
    networks:

      - app-network
  
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:

      - redis-data:/data
    networks:

      - app-network

volumes:
  pg-data:
  redis-data:

networks:
  app-network:
    driver: bridge

GitOps и Argo CD

GitOps — подход, при котором Git репозиторий является источником истины для конфигурации и состояния инфраструктуры. CD-инструмент постоянно синхронизирует состояние в кластере с конфигурацией в Git.

Argo CD — инструмент для GitOps-деплоя в Kubernetes. Хранит манифесты в Git, автоматически применяет их в кластер.

# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-service
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/mycompany/my-service-deployment.git
    targetRevision: main
    path: kubernetes/
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true      # удалить ресурсы, которые удалены из Git
      selfHeal: true   # автоматически синхронизировать, если кластер отличается от Git
    syncOptions:

      - CreateNamespace=true
  revisionHistoryLimit: 10

Поток с Argo CD:

  1. Разработчик делает commit с изменением версии образа в deployment.yaml.
  2. Argo CD подхватывает изменение из Git.
  3. Argo CD применяет манифест в кластер (kubectl apply).
  4. Kubernetes rolling update обновляет pods.
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-service
    spec:
      serviceAccountName: my-service
      containers:

      - name: my-service
        image: docker.io/mycompany/my-service:1.2.3  # версия обновляется здесь
        ports:

        - containerPort: 8080
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/ready
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
  namespace: production
spec:
  type: LoadBalancer
  ports:

  - port: 80
    targetPort: 8080
  selector:
    app: my-service

Automation: CI → CD полностью

Типичный полный процесс:

  1. CI Pipeline (GitHub Actions / GitLab CI / Jenkins):

    • Checkout
    • Build
    • Tests
    • Code quality
    • Build Docker image
    • Push to registry
  2. Update manifest (скрипт в пайплайне):

# Обновить версию в deployment.yaml
sed -i "s|image:.*|image: docker.io/mycompany/my-service:${VERSION}|" kubernetes/deployment.yaml
git commit -m "Update my-service to ${VERSION}"
git push origin main
  1. Argo CD автоматически синхронизирует и развёртывает

или

  1. CD Pipeline (Jenkins):

    • Pull image
    • Health check
    • Deploy на сервер

Требования к приложению для CD

  • Health checks: endpoint /actuator/health (Spring Boot).
  • Graceful shutdown: приложение должно корректно завершиться при получении SIGTERM.
  • Логирование: структурированное, доступно для мониторинга.
  • Метрики: Prometheus metrics для мониторинга и автоскейлинга.
  • Database migrations: автоматические миграции при старте (Flyway, Liquibase).
  • Конфигурация: через environment variables, без хардкода.

CI/CD: практические сценарии и шаблоны пайплайнов для Java/Kotlin

Типовой CI-пайплайн для монолитного Java/Kotlin сервиса

Пайплайн разрабатывается под следующие предположения:

  • Проект собирается Gradle.
  • Код покрыт unit-тестами (>70% покрытия).
  • Есть интеграционные тесты (в отдельном source set).
  • Статический анализ: Checkstyle, SpotBugs, ktlint/Detekt.
  • Деплоить нужно в Docker-контейнере.
# .github/workflows/ci.yml (GitHub Actions)
name: CI-Pipeline

on:
  push:
    branches: [main, develop, 'release/**']
  pull_request:
    branches: [main, develop]

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 45
    
    steps:

      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # для правильного определения версии
      
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle
      
      - name: Cache SonarQube packages
        uses: actions/cache@v3
        with:
          path: ~/.sonar/cache
          key: ${{ runner.os }}-sonar
      
      - name: Build application
        run: ./gradlew clean assemble -x test --no-daemon
        env:
          ORG_GRADLE_PROJECT_version: ${{ github.sha }}
      
      - name: Run unit tests
        run: ./gradlew test --no-daemon
      
      - name: Run code analysis
        run: ./gradlew checkstyleMain spotbugsMain ktlintCheck --no-daemon
        continue-on-error: true  # не падать, если анализ нашёл проблемы
      
      - name: Run integration tests
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: ./gradlew integrationTest --no-daemon
      
      - name: Generate coverage report
        run: ./gradlew jacocoTestReport --no-daemon
      
      - name: Publish test results
        if: always()
        uses: EnricoMi/publish-unit-test-result-action@v2
        with:
          files: build/test-results/**/*.xml
      
      - name: SonarQube analysis
        if: github.event_name == 'push'
        run: |
          ./gradlew sonarqube \
            -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }} \
            -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \
            -Dsonar.login=${{ secrets.SONAR_TOKEN }} \
            --no-daemon
        continue-on-error: true
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./build/reports/jacoco/test/jacocoTestReport.xml
          fail_ci_if_error: false
      
      - name: Build Docker image
        if: github.event_name == 'push'
        run: |
          VERSION=$(cat build.gradle | grep "version =" | head -1 | sed "s/.*= '\([^']*\)'.*/\1/")
          docker build -t localhost/my-service:${VERSION} -t localhost/my-service:latest .
      
      - name: Push to Docker registry
        if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
        run: |
          VERSION=$(cat build.gradle | grep "version =" | head -1 | sed "s/.*= '\([^']*\)'.*/\1/")
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
          docker tag localhost/my-service:${VERSION} docker.io/mycompany/my-service:${VERSION}
          docker tag localhost/my-service:latest docker.io/mycompany/my-service:latest
          docker push docker.io/mycompany/my-service:${VERSION}
          docker push docker.io/mycompany/my-service:latest

  security-scan:
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    timeout-minutes: 15
    
    steps:

      - uses: actions/checkout@v3
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'
      
      - name: Upload Trivy results to GitHub Security
        uses: github/codeql-action/upload-sarif@v2
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'
      
      - name: Run OWASP Dependency-Check
        run: |
          mkdir -p build/reports
          ./gradlew dependencyCheck --no-daemon
        continue-on-error: true

CI-пайплайн для микросервисов (монорепозиторий)

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

Структура:

monorepo/
├── services/
│   ├── user-service/
│   │   ├── build.gradle
│   │   ├── Dockerfile
│   │   └── src/
│   ├── order-service/
│   │   ├── build.gradle
│   │   ├── Dockerfile
│   │   └── src/
│   └── payment-service/
├── shared-lib/
├── build.gradle (root)
└── gradle.properties
# .github/workflows/ci-microservices.yml
name: CI-Microservices

on:
  push:
    branches: [main, develop]
    paths:

      - 'services/**'
      - 'shared-lib/**'
      - '.github/workflows/ci-microservices.yml'
  pull_request:
    branches: [main, develop]

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      user-service: ${{ steps.changes.outputs.user-service }}
      order-service: ${{ steps.changes.outputs.order-service }}
      payment-service: ${{ steps.changes.outputs.payment-service }}
    
    steps:

      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            user-service:

              - 'services/user-service/**'
              - 'shared-lib/**'
            order-service:

              - 'services/order-service/**'
              - 'shared-lib/**'
            payment-service:

              - 'services/payment-service/**'
              - 'shared-lib/**'

  build-user-service:
    needs: changes
    if: needs.changes.outputs.user-service == 'true'
    runs-on: ubuntu-latest
    
    steps:

      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle
      
      - name: Build and test user-service
        run: ./gradlew :services:user-service:build --no-daemon
      
      - name: Build Docker image
        run: |
          cd services/user-service
          VERSION=$(grep "version =" build.gradle | sed "s/.*= '\([^']*\)'.*/\1/")
          docker build -t docker.io/mycompany/user-service:${VERSION} .
      
      - name: Push to registry
        if: github.event_name == 'push'
        run: |
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
          cd services/user-service
          VERSION=$(grep "version =" build.gradle | sed "s/.*= '\([^']*\)'.*/\1/")
          docker push docker.io/mycompany/user-service:${VERSION}

  build-order-service:
    needs: changes
    if: needs.changes.outputs.order-service == 'true'
    runs-on: ubuntu-latest
    
    steps:

      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle
      
      - name: Build and test order-service
        run: ./gradlew :services:order-service:build --no-daemon
      
      - name: Build Docker image
        run: |
          cd services/order-service
          VERSION=$(grep "version =" build.gradle | sed "s/.*= '\([^']*\)'.*/\1/")
          docker build -t docker.io/mycompany/order-service:${VERSION} .
      
      - name: Push to registry
        if: github.event_name == 'push'
        run: |
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
          cd services/order-service
          VERSION=$(grep "version =" build.gradle | sed "s/.*= '\([^']*\)'.*/\1/")
          docker push docker.io/mycompany/order-service:${VERSION}

  build-payment-service:
    needs: changes
    if: needs.changes.outputs.payment-service == 'true'
    runs-on: ubuntu-latest
    
    steps:

      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle
      
      - name: Build and test payment-service
        run: ./gradlew :services:payment-service:build --no-daemon
      
      - name: Build Docker image
        run: |
          cd services/payment-service
          VERSION=$(grep "version =" build.gradle | sed "s/.*= '\([^']*\)'.*/\1/")
          docker build -t docker.io/mycompany/payment-service:${VERSION} .
      
      - name: Push to registry
        if: github.event_name == 'push'
        run: |
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
          cd services/payment-service
          VERSION=$(grep "version =" build.gradle | sed "s/.*= '\([^']*\)'.*/\1/")
          docker push docker.io/mycompany/payment-service:${VERSION}

  integration-tests:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
    services:
      postgres:
        image: postgres:14
        env:
          POSTGRES_DB: integration_test
          POSTGRES_PASSWORD: password
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:

      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle
      
      - name: Run integration tests
        run: ./gradlew integrationTest --no-daemon
        env:
          DATABASE_URL: jdbc:postgresql://postgres:5432/integration_test
          DATABASE_USER: postgres
          DATABASE_PASSWORD: password

Pull Request пайплайн (быстрая версия)

Для PR нужно запустить только быстрые проверки, чтобы дать feedback разработчику за 5-10 минут.

# .github/workflows/pr-checks.yml
name: PR Checks

on:
  pull_request:
    branches: [main, develop]

jobs:
  quick-checks:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    
    steps:

      - uses: actions/checkout@v3
      
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle
      
      - name: Compile
        run: ./gradlew compileJava compileKotlin --no-daemon
      
      - name: Lint
        run: ./gradlew checkstyleMain ktlintCheck --no-daemon
        continue-on-error: true
      
      - name: Unit tests (fast path)
        run: ./gradlew test --no-daemon -x integrationTest
        timeout-minutes: 10
      
      - name: Comment PR with results
        if: always()
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '✅ PR checks completed'
            })

Main-пайплайн: полные проверки и деплой

Для main-ветки запускаются все стадии и деплой.

# .github/workflows/main-deploy.yml
name: Main Deploy

on:
  push:
    branches: [main]
    tags: [v*]

jobs:
  full-ci:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    
    steps:

      - uses: actions/checkout@v3
      
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle
      
      - name: Full build with all tests
        run: ./gradlew build --no-daemon
      
      - name: Code quality checks
        run: ./gradlew checkstyleMain spotbugsMain ktlintCheck --no-daemon
        continue-on-error: true
      
      - name: SonarQube analysis
        run: |
          ./gradlew sonarqube \
            -Dsonar.projectKey=my-service \
            -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \
            -Dsonar.login=${{ secrets.SONAR_TOKEN }} \
            --no-daemon
        continue-on-error: true
      
      - name: Build and push Docker image
        run: |
          VERSION=$(cat build.gradle | grep "version =" | sed "s/.*= '\([^']*\)'.*/\1/")
          TAG="${{ github.ref_type == 'tag' && github.ref_name || 'latest' }}"
          
          docker build -t docker.io/mycompany/my-service:${VERSION} .
          docker tag docker.io/mycompany/my-service:${VERSION} docker.io/mycompany/my-service:${TAG}
          
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
          docker push docker.io/mycompany/my-service:${VERSION}
          docker push docker.io/mycompany/my-service:${TAG}
      
      - name: Update ArgoCD manifest
        run: |
          VERSION=$(cat build.gradle | grep "version =" | sed "s/.*= '\([^']*\)'.*/\1/")
          sed -i "s|image:.*my-service.*|image: docker.io/mycompany/my-service:${VERSION}|" kubernetes/deployment.yaml
          
          git config user.email "ci@example.com"
          git config user.name "CI Bot"
          git add kubernetes/deployment.yaml
          git commit -m "chore: update image to ${VERSION}"
          git push origin main

Jenkins: template с переиспользованием

Для снижения дублирования кода в Jenkinsfile'ах используются shared libraries.

// vars/standardCIPipeline.groovy
def call(Map config) {
    pipeline {
        agent {
            docker {
                image config.javaImage ?: 'gradle:7.6-jdk17'
                args '-v /var/run/docker.sock:/var/run/docker.sock'
            }
        }
        
        options {
            timestamps()
            timeout(time: config.timeout ?: 60, unit: 'MINUTES')
            buildDiscarder(logRotator(numToKeepStr: '20'))
        }
        
        environment {
            GRADLE_HOME = "${env.WORKSPACE}/.gradle"
            APP_VERSION = sh(script: "cat build.gradle | grep version | sed \"s/.*= '\\([^']*\\)'.*/\\1/\"", returnStdout: true).trim()
        }
        
        stages {
            stage('Checkout') {
                steps {
                    checkout scm
                }
            }
            
            stage('Build') {
                steps {
                    sh './gradlew clean build -x test --no-daemon'
                }
            }
            
            stage('Unit Tests') {
                steps {
                    sh './gradlew test --no-daemon'
                }
                post {
                    always {
                        junit 'build/test-results/test/*.xml'
                    }
                }
            }
            
            stage('Code Quality') {
                steps {
                    sh './gradlew checkstyleMain spotbugsMain ktlintCheck --no-daemon'
                }
                post {
                    always {
                        checkstyle pattern: 'build/reports/checkstyle/*.xml'
                    }
                }
            }
            
            stage('Build Docker') {
                when {
                    branch pattern: "^(main|develop)$", comparator: "REGEXP"
                }
                steps {
                    sh '''
                        docker build -t ${DOCKER_REPO}/${APP_NAME}:${APP_VERSION} .
                        docker tag ${DOCKER_REPO}/${APP_NAME}:${APP_VERSION} ${DOCKER_REPO}/${APP_NAME}:latest
                    '''
                }
            }
            
            stage('Push Docker') {
                when {
                    branch pattern: "^(main|develop)$", comparator: "REGEXP"
                }
                steps {
                    script {
                        withCredentials([usernamePassword(
                            credentialsId: 'docker-credentials',
                            usernameVariable: 'DOCKER_USER',
                            passwordVariable: 'DOCKER_PASS'
                        )]) {
                            sh '''
                                echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin
                                docker push ${DOCKER_REPO}/${APP_NAME}:${APP_VERSION}
                                docker push ${DOCKER_REPO}/${APP_NAME}:latest
                            '''
                        }
                    }
                }
            }
            
            stage('Deploy') {
                when {
                    branch pattern: "^(main|develop)$", comparator: "REGEXP"
                    expression { config.deployEnabled ?: false }
                }
                steps {
                    script {
                        String env = env.BRANCH_NAME == 'main' ? 'staging' : 'dev'
                        sh '''
                            ansible-playbook \
                                -i inventories/${ENV_NAME} \
                                -e docker_image=${DOCKER_REPO}/${APP_NAME}:${APP_VERSION} \
                                playbooks/deploy.yml
                        '''
                    }
                }
            }
        }
        
        post {
            always {
                archiveArtifacts artifacts: 'build/**/*.jar', allowEmptyArchive: true
                cleanWs()
            }
            success {
                echo "Pipeline succeeded"
            }
            failure {
                echo "Pipeline failed"
            }
        }
    }
}
// Jenkinsfile использует shared library
@Library('shared-pipeline-library') _

standardCIPipeline {
    javaImage = 'gradle:7.6-jdk17'
    timeout = 60
    deployEnabled = true
}

Распространённые ошибки и их решения

Проблема Причина Решение
Flaky tests (иногда падают) Зависимость от времени, race conditions, незаизолированное состояние Использовать @DirtiesContext, testcontainers, фиксировать таймауты
Медленная сборка (>15 мин) Много тестов, медленные интеграционные тесты Разделить unit/integration, кешировать зависимости, параллелизм
Out of memory в Docker Недостаточно памяти контейнеру Увеличить heap: -Xmx1g, лимит памяти контейнера
Нестабильный registry Network issues при push/pull Добавить retry logic, использовать кеш слоёв Docker
Credentials leak Hardcoded passwords в логах Использовать secrets, mask output, проверять логи перед коммитом
Долгое время запуска пайплайна Очередь runner'ов, медленный runner Добавить runner'ов, оптимизировать сборку, использовать self-hosted

Docker

Что такое Docker

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

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

Image (Образ) — read-only шаблон для создания контейнеров. Содержит файловую систему, runtime, библиотеки, переменные окружения и конфигурацию.

Container (Контейнер) — запущенный экземпляр образа. Изолированная среда выполнения с собственной файловой системой, сетью и процессами.

Dockerfile — текстовый файл с инструкциями для сборки образа. Описывает каждый шаг создания образа как отдельный layer.

Layer (Слой) — промежуточный образ, созданный каждой инструкцией в Dockerfile. Layers кешируются и переиспользуются.

Registry — хранилище Docker образов (Docker Hub, AWS ECR, Google Container Registry).

Преимущества контейнеризации

  • Consistency — одинаковое поведение в dev, test и production
  • Isolation — изоляция приложений друг от друга
  • Portability — запуск на любой платформе с Docker
  • Scalability — легкое масштабирование и orchestration
  • Efficiency — быстрый старт, меньше ресурсов чем VMs

Пояснение: Docker решает проблему "у меня работает" — если приложение работает в контейнере локально, оно будет работать везде.


Создание Dockerfile

Базовая структура

# Базовый образ
FROM openjdk:17-jdk-slim

# Метаданные
LABEL maintainer="developer@company.com"
LABEL version="1.0"
LABEL description="Spring Boot application"

# Рабочая директория
WORKDIR /app

# Копирование файлов
COPY target/myapp.jar app.jar

# Переменные окружения
ENV SPRING_PROFILES_ACTIVE=docker
ENV SERVER_PORT=8080

# Открытие порта
EXPOSE 8080

# Команда запуска
CMD ["java", "-jar", "app.jar"]

Инструкции Dockerfile

FROM — определяет базовый образ:

# Официальный OpenJDK образ
FROM openjdk:17-jdk-slim

# Многоступенчатая сборка
FROM maven:3.8-openjdk-17 AS build
FROM openjdk:17-jre-slim AS runtime

WORKDIR — устанавливает рабочую директорию:

WORKDIR /app
# Все последующие команды выполняются в /app

COPY vs ADD:

# COPY - простое копирование (рекомендуется)
COPY src/ /app/src/
COPY target/app.jar app.jar

# ADD - расширенное копирование (автоматическая распаковка архивов)
ADD https://example.com/file.tar.gz /tmp/
ADD archive.tar.gz /extracted/  # Автоматически распакует

RUN — выполнение команд при сборке:

# Установка зависимостей
RUN apt-get update && apt-get install -y \
    curl \
    vim \
    && rm -rf /var/lib/apt/lists/*

# Создание пользователя
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Maven сборка
RUN mvn clean package -DskipTests

ENV — переменные окружения:

ENV JAVA_OPTS="-Xmx512m -Xms256m"
ENV DATABASE_URL="jdbc:postgresql://db:5432/myapp"
ENV SPRING_PROFILES_ACTIVE="docker"

EXPOSE — документирует порты:

EXPOSE 8080 8443  # HTTP и HTTPS порты
# Не открывает порты, только документирует

USER — переключение пользователя:

# Создание non-root пользователя для безопасности
RUN addgroup --system appuser && adduser --system --group appuser
USER appuser

CMD vs ENTRYPOINT:

# CMD - команда по умолчанию (можно переопределить)
CMD ["java", "-jar", "app.jar"]

# ENTRYPOINT - точка входа (нельзя переопределить)
ENTRYPOINT ["java", "-jar", "app.jar"]

# Комбинация ENTRYPOINT + CMD
ENTRYPOINT ["java"]
CMD ["-jar", "app.jar"]  # Можно переопределить параметры

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


Multi-stage Build

Зачем нужны многоступенчатые сборки

Проблемы single-stage builds:

  • Большой размер образа (build tools в production образе)
  • Security risks (компиляторы и исходники в production)
  • Сложность maintenance (все в одном образе)

Multi-stage build позволяет:

  • Использовать разные базовые образы для build и runtime
  • Копировать только необходимые артефакты
  • Значительно уменьшить размер финального образа

Maven/Gradle приложения

# === BUILD STAGE ===
FROM maven:3.8-openjdk-17-slim AS build

# Копируем pom.xml для dependency caching
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Копируем исходники и собираем
COPY src src
RUN mvn clean package -DskipTests

# === RUNTIME STAGE ===
FROM openjdk:17-jre-slim AS runtime

# Создаем non-root пользователя
RUN addgroup --system appuser && adduser --system --group appuser

# Рабочая директория
WORKDIR /app

# Копируем только JAR из build stage
COPY --from=build target/*.jar app.jar

# Переключаемся на non-root пользователя
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

Node.js приложения

# === BUILD STAGE ===
FROM node:18-alpine AS build

WORKDIR /app

# Package files для dependency caching
COPY package*.json ./
RUN npm ci --only=production

# Исходники и сборка
COPY . .
RUN npm run build

# === RUNTIME STAGE ===
FROM node:18-alpine AS runtime

# Безопасность
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

WORKDIR /app

# Копируем production dependencies
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./package.json

USER nextjs

EXPOSE 3000

CMD ["npm", "start"]

Оптимизированная сборка с кешированием

# === DEPENDENCIES STAGE ===
FROM maven:3.8-openjdk-17-slim AS deps

WORKDIR /app
COPY pom.xml .

# Загружаем зависимости (кешируется если pom.xml не изменился)
RUN mvn dependency:go-offline -B

# === BUILD STAGE ===
FROM deps AS build

# Копируем исходники
COPY src src

# Сборка приложения
RUN mvn clean package -DskipTests -o  # -o = offline mode

# === TEST STAGE (опционально) ===
FROM build AS test
RUN mvn test

# === RUNTIME STAGE ===
FROM gcr.io/distroless/java17-debian11 AS runtime

WORKDIR /app

# Копируем только JAR файл
COPY --from=build /app/target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

Пояснение: Distroless образы содержат только runtime и ваше приложение, без OS utilities. Это максимально безопасно и минимально по размеру.


Оптимизация образов

Layer Caching Strategy

Принципы эффективного кеширования:

  1. Копируйте dependency files перед исходниками
  2. Располагайте наименее изменяемые инструкции вверху
  3. Группируйте связанные команды в одну RUN инструкцию
  4. Используйте .dockerignore для исключения ненужных файлов
# ❌ Плохо - пересборка всего при изменении кода
FROM maven:3.8-openjdk-17-slim
COPY . /app
WORKDIR /app
RUN mvn clean package

# ✅ Хорошо - кеширование зависимостей
FROM maven:3.8-openjdk-17-slim
WORKDIR /app

# 1. Копируем dependency файлы (изменяются редко)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 2. Копируем исходники (изменяются часто)
COPY src src
RUN mvn clean package -DskipTests -o

.dockerignore файл

# Версионирование
.git
.gitignore
.github

# IDE файлы
.idea
.vscode
*.iml
.settings
.project
.classpath

# Build артефакты
target/
build/
*.jar
*.war

# OS файлы
.DS_Store
Thumbs.db

# Logs
*.log
logs/

# Временные файлы
*.tmp
*.temp
*.cache

# Dependencies (для Node.js)
node_modules/
npm-debug.log

# Documentation
README.md
docs/
*.md

# Docker файлы
Dockerfile
docker-compose.yml
.dockerignore

# Environment файлы (безопасность)
.env
.env.local
secrets/

Пояснение: .dockerignore работает как .gitignore, но для Docker context. Уменьшает размер context и ускоряет сборку.

Выбор базовых образов

Размер образов по типам:

# Full OS images (избегайте в production)
ubuntu:20.04           # ~72MB
centos:8              # ~200MB

# Slim variants (хороший баланс)
openjdk:17-jdk-slim   # ~400MB
node:18-slim          # ~180MB
python:3.9-slim       # ~45MB

# Alpine variants (минимальный размер)
openjdk:17-jdk-alpine # ~350MB
node:18-alpine        # ~110MB
python:3.9-alpine     # ~25MB

# Distroless (максимальная безопасность)
gcr.io/distroless/java17  # ~250MB
gcr.io/distroless/nodejs  # ~150MB

Оптимизация слоев

# ❌ Плохо - много слоев
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*

# ✅ Хорошо - один слой
RUN apt-get update && \
    apt-get install -y \
        curl \
        vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# ❌ Плохо - оставляет кеш в слое
RUN wget https://example.com/large-file.tar.gz && \
    tar -xzf large-file.tar.gz && \
    rm large-file.tar.gz  # Файл все еще в слое!

# ✅ Хорошо - очистка в том же слое
RUN wget https://example.com/large-file.tar.gz && \
    tar -xzf large-file.tar.gz && \
    rm large-file.tar.gz

Анализ размера образа

# Просмотр слоев образа
docker history myapp:latest

# Анализ размера с помощью dive
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive:latest myapp:latest

# Сравнение размеров
docker images | grep myapp

Docker Compose для локальной разработки

Что такое Docker Compose

Docker Compose — инструмент для определения и запуска multi-container Docker приложений. Использует YAML файл для конфигурации services, networks и volumes.

Преимущества для разработки:

  • Одна команда для запуска всех зависимостей
  • Изоляция окружений разработчиков
  • Consistent environment для команды
  • Easy integration testing

Базовая структура docker-compose.yml

version: '3.8'

services:
  app:
    build: .
    ports:

      - "8080:8080"
    environment:

      - SPRING_PROFILES_ACTIVE=docker
    depends_on:

      - db
      - redis
    networks:

      - app-network

  db:
    image: postgres:14-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:

      - "5432:5432"
    volumes:

      - postgres_data:/var/lib/postgresql/data
    networks:

      - app-network

volumes:
  postgres_data:

networks:
  app-network:
    driver: bridge

Полная инфраструктура для микросервисов

version: '3.8'

services:
  # === DATABASES ===
  postgres:
    image: postgres:14-alpine
    environment:
      POSTGRES_DB: userdb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:

      - "5432:5432"
    volumes:

      - postgres_data:/var/lib/postgresql/data
      - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: orderdb
      MYSQL_USER: user
      MYSQL_PASSWORD: password
    ports:

      - "3306:3306"
    volumes:

      - mysql_data:/var/lib/mysql
      - ./docker/mysql/conf.d:/etc/mysql/conf.d
    command: --default-authentication-plugin=mysql_native_password

  # === MESSAGE BROKERS ===
  rabbitmq:
    image: rabbitmq:3.11-management-alpine
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: admin
    ports:

      - "5672:5672"   # AMQP port
      - "15672:15672" # Management UI
    volumes:

      - rabbitmq_data:/var/lib/rabbitmq

  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on:

      - zookeeper
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    ports:

      - "9092:9092"

  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000

  # === CACHING ===
  redis:
    image: redis:7-alpine
    ports:

      - "6379:6379"
    volumes:

      - redis_data:/data
    command: redis-server --appendonly yes --requirepass redis123

  # === MONITORING ===
  prometheus:
    image: prom/prometheus:latest
    ports:

      - "9090:9090"
    volumes:

      - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus

  grafana:
    image: grafana/grafana:latest
    ports:

      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    volumes:

      - grafana_data:/var/lib/grafana
      - ./docker/grafana/dashboards:/etc/grafana/provisioning/dashboards

  # === SEARCH ===
  elasticsearch:
    image: elasticsearch:8.8.0
    environment:

      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:

      - "9200:9200"
    volumes:

      - elasticsearch_data:/usr/share/elasticsearch/data

  # === TESTING TOOLS ===
  wiremock:
    image: wiremock/wiremock:latest
    ports:

      - "8090:8080"
    volumes:

      - ./docker/wiremock/mappings:/home/wiremock/mappings
      - ./docker/wiremock/__files:/home/wiremock/__files

volumes:
  postgres_data:
  mysql_data:
  rabbitmq_data:
  redis_data:
  prometheus_data:
  grafana_data:
  elasticsearch_data:

networks:
  default:
    name: microservices-network

Profiles для разных окружений

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    profiles: ["app"]
    depends_on:

      - db

  db:
    image: postgres:14-alpine
    # Всегда включен (без profile)
    
  redis:
    image: redis:7-alpine
    profiles: ["cache"]

  monitoring:
    image: prometheus:latest
    profiles: ["monitoring"]

# Запуск разных комбинаций
# docker-compose up                    # Только db
# docker-compose --profile app up      # db + app  
# docker-compose --profile cache up    # db + redis
# docker-compose --profile monitoring up # db + monitoring

Environment-specific конфигурации

# docker-compose.override.yml (автоматически применяется)
version: '3.8'

services:
  app:
    environment:

      - DEBUG=true
      - LOG_LEVEL=debug
    volumes:

      - .:/app  # Live reload для разработки

# docker-compose.prod.yml
version: '3.8'

services:
  app:
    image: myapp:latest  # Pre-built образ
    restart: unless-stopped
    environment:

      - SPRING_PROFILES_ACTIVE=production

# Запуск с specific файлом
# docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

Управление секретами

Переменные окружения (ENV)

# В Dockerfile
ENV DATABASE_PASSWORD=default_password
ENV API_KEY=placeholder
# В docker-compose.yml
services:
  app:
    environment:

      - DATABASE_PASSWORD=${DB_PASSWORD:-default}
      - API_KEY=${API_KEY}
    env_file:

      - .env
      - .env.local
# .env файл
DB_PASSWORD=secret123
API_KEY=abc123def456
SPRING_PROFILES_ACTIVE=docker

Проблемы ENV подхода:

  • Переменные видны через docker inspect
  • Могут попасть в logs
  • Сложно ротировать
  • Нет encryption at rest

Docker Secrets (Swarm mode)

# docker-compose.yml для Docker Swarm
version: '3.8'

services:
  app:
    image: myapp:latest
    secrets:

      - db_password
      - api_key
    environment:

      - DATABASE_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    external: true  # Управляется через Docker CLI

# Создание secret
# echo "my_secret_password" | docker secret create db_password -
// Чтение secret в приложении
@Component
public class SecretManager {
    
    public String getSecret(String secretName) {
        try {
            Path secretPath = Paths.get("/run/secrets/" + secretName);
            return Files.readString(secretPath).trim();
        } catch (IOException e) {
            throw new RuntimeException("Failed to read secret: " + secretName, e);
        }
    }
}

Volumes для секретов

services:
  app:
    volumes:

      - secrets_volume:/app/secrets:ro  # Read-only mount
    environment:

      - SECRETS_PATH=/app/secrets

volumes:
  secrets_volume:
    driver: local
    driver_opts:
      type: tmpfs  # В памяти, не на диске
      device: tmpfs
      o: size=100m,uid=1000
// Чтение секретов из volume
@ConfigurationProperties(prefix = "app")
public class AppConfig {
    
    @Value("${secrets.path:/app/secrets}")
    private String secretsPath;
    
    @PostConstruct
    public void loadSecrets() {
        try {
            Path dbPasswordFile = Paths.get(secretsPath, "db_password");
            if (Files.exists(dbPasswordFile)) {
                String password = Files.readString(dbPasswordFile).trim();
                System.setProperty("spring.datasource.password", password);
            }
        } catch (IOException e) {
            log.error("Failed to load secrets", e);
        }
    }
}

Интеграция с HashiCorp Vault

# docker-compose.yml с Vault
services:
  vault:
    image: vault:latest
    ports:

      - "8200:8200"
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: myroot
      VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
    cap_add:

      - IPC_LOCK

  app:
    depends_on:

      - vault
    environment:

      - VAULT_ADDR=http://vault:8200
      - VAULT_TOKEN=${VAULT_TOKEN}
// Spring Cloud Vault интеграция
@Configuration
@VaultPropertySource("secret/myapp")
public class VaultConfig {
    
    @Bean
    public VaultTemplate vaultTemplate() {
        VaultEndpoint endpoint = new VaultEndpoint();
        endpoint.setHost("vault");
        endpoint.setPort(8200);
        endpoint.setScheme("http");
        
        TokenAuthentication auth = new TokenAuthentication(vaultToken);
        
        return new VaultTemplate(endpoint, auth);
    }
}

Init containers для секретов

services:
  secret-fetcher:
    image: vault:latest
    command: |
      sh -c "
        vault auth -method=aws
        vault read -field=password secret/db > /shared/db_password
        vault read -field=key secret/api > /shared/api_key
      "
    volumes:

      - shared_secrets:/shared
    environment:

      - VAULT_ADDR=https://vault.company.com

  app:
    depends_on:

      - secret-fetcher
    volumes:

      - shared_secrets:/app/secrets:ro

volumes:
  shared_secrets:
    driver: tmpfs

Пояснение: Init containers загружают секреты до запуска основного приложения. Секреты остаются в shared volume только в памяти.


Безопасность контейнеров

Non-root пользователи

# Создание dedicated пользователя
FROM openjdk:17-jre-slim

# Создаем группу и пользователя
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Создаем директории с правильными permissions
RUN mkdir -p /app/data && chown -R appuser:appuser /app

WORKDIR /app

# Копируем файлы под root
COPY --chown=appuser:appuser target/app.jar app.jar

# Переключаемся на non-root пользователя
USER appuser

CMD ["java", "-jar", "app.jar"]

Security scanning

# .github/workflows/security.yml
name: Container Security Scan

on: [push, pull_request]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:

      - uses: actions/checkout@v2
      
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
      
      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v1
        with:
          sarif_file: 'trivy-results.sarif'

Runtime security

# docker-compose.yml с security constraints
services:
  app:
    image: myapp:latest
    security_opt:

      - no-new-privileges:true  # Запрет privilege escalation
    cap_drop:

      - ALL                     # Убираем все capabilities
    cap_add:

      - NET_BIND_SERVICE        # Добавляем только необходимые
    read_only: true             # Read-only root filesystem
    tmpfs:

      - /tmp                    # Writable tmpfs для временных файлов
      - /var/cache
    ulimits:
      memlock: -1
      nofile: 65536

Практические команды

Основные Docker команды

# === ОБРАЗЫ ===
# Сборка образа
docker build -t myapp:latest .
docker build -t myapp:v1.0 --target production .  # Multi-stage

# Просмотр образов
docker images
docker image ls --filter "reference=myapp*"

# Удаление образов
docker rmi myapp:latest
docker image prune  # Удаление неиспользуемых образов

# === КОНТЕЙНЕРЫ ===
# Запуск контейнера
docker run -d --name myapp -p 8080:8080 myapp:latest
docker run -it --rm myapp:latest bash  # Интерактивный режим

# Просмотр контейнеров
docker ps          # Запущенные
docker ps -a       # Все

# Логи контейнера
docker logs myapp
docker logs -f myapp  # Follow режим

# Выполнение команд в контейнере
docker exec -it myapp bash
docker exec myapp cat /app/config.properties

# Остановка и удаление
docker stop myapp
docker rm myapp
docker container prune  # Удаление остановленных контейнеров

# === СИСТЕМНЫЕ КОМАНДЫ ===
# Использование дискового пространства
docker system df

# Очистка всего неиспользуемого
docker system prune -a --volumes

# Информация о Docker
docker info
docker version

Docker Compose команды

# === УПРАВЛЕНИЕ SERVICES ===
# Запуск всех services
docker-compose up
docker-compose up -d  # Detached режим

# Запуск specific services
docker-compose up db redis
docker-compose up --scale app=3  # Масштабирование

# Остановка
docker-compose stop
docker-compose down          # Остановка и удаление
docker-compose down -v       # + удаление volumes

# === СБОРКА ===
# Пересборка образов
docker-compose build
docker-compose up --build    # Сборка и запуск

# Сборка без cache
docker-compose build --no-cache

# === ЛОГИ И МОНИТОРИНГ ===
# Просмотр логов
docker-compose logs
docker-compose logs -f app   # Follow конкретного service

# Статус services
docker-compose ps
docker-compose top           # Процессы в контейнерах

# === ВЫПОЛНЕНИЕ КОМАНД ===
# Выполнение команды в service
docker-compose exec app bash
docker-compose exec db psql -U postgres

# Одноразовый контейнер
docker-compose run --rm app bash

Production deployment

# === CI/CD PIPELINE ===
# Сборка для production
docker build -t myapp:${BUILD_NUMBER} \
  --target production \
  --build-arg VERSION=${BUILD_NUMBER} .

# Тегирование
docker tag myapp:${BUILD_NUMBER} myapp:latest
docker tag myapp:${BUILD_NUMBER} registry.company.com/myapp:${BUILD_NUMBER}

# Push в registry
docker push registry.company.com/myapp:${BUILD_NUMBER}
docker push registry.company.com/myapp:latest

# === DEPLOYMENT ===
# Pull на production сервере
docker pull registry.company.com/myapp:${BUILD_NUMBER}

# Rolling update
docker service update --image registry.company.com/myapp:${BUILD_NUMBER