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 codeNever: не перезапускать
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
Процесс:
- 5-10% трафика на новую версию
- Мониторинг метрик
- Постепенное увеличение доли трафика
- 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
- Декларативность — состояние системы описано декларативно в Git
- Version Control — все изменения отслеживаются и имеют историю
- Reviewed & Approved — изменения вносятся через pull requests
- 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: как это работает "под капотом"
-
Ansible читает playbook и инвентарь.
-
Для каждого хоста, которому нужно применить пьесу, Ansible устанавливает SSH-соединение.
-
Для каждой задачи Ansible:
- Загружает соответствующий модуль на целевую машину через SSH.
- Передаёт параметры задачи модулю.
- Выполняет модуль на целевом хосте.
- Читает результат (статус, вывод, флаг
changed). - Удаляет модуль с целевого хоста.
-
Если задача содержит
notify, добавляет обработчик в очередь. -
После всех задач запускает все обработчики, которые были добавлены в очередь (один раз каждый).
Вывод выполнения для каждой задачи включает:
- Имя задачи.
- Хосты, на которых она выполнялась.
- Статус:
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 применяются в порядке приоритета (от низкого к высокому):
- Role defaults (
roles/myrole/defaults/main.yml) — самый низкий приоритет. - Inventory variables — переменные из инвентаря (group_vars, host_vars).
- Playbook vars — переменные, определённые в playbook.
- Task vars — переменные, определённые в конкретной задаче.
- set_fact — переменные, установленные через модуль
set_fact. - Registered variables — результаты, сохранённые через
register. - 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"
Лучшие практики
- Используй роли — разбивай логику на переиспользуемые компоненты.
- Группируй хосты логически — webservers, databases, monitoring.
- Используй handlers — для перезагрузки сервисов после изменений.
- Разделяй окружения — dev, stage, prod в отдельных инвентариях.
- Документируй роли — добавляй README с параметрами и примерами.
- Используй переменные по умолчанию — defaults/main.yml с разумными значениями.
- Проверяй idempotency — запускай playbook дважды и убедись что ничего не изменится.
- Логируй выполнение — используй debug модуль для вывода переменных.
- Используй version control — храни все плейбуки в Git.
- Тестируй в 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-системе.
Типичный цикл запуска пайплайна
- Trigger: Разработчик делает push или создаёт PR.
- Checkout: Система клонирует репозиторий и переключается на нужную ветку/коммит.
- Setup: Установка Java, Gradle/Maven, зависимостей.
- Compile: Компиляция исходного кода.
- Unit Tests: Запуск быстрых тестов.
- Code Analysis: Проверка кодовой базы инструментами (SonarQube, Checkstyle, SpotBugs).
- Integration Tests: Запуск интеграционных тестов (если есть).
- Build Artifact: Сборка jar/war или Docker-образа.
- Publish: Публикация в registry (если нужна).
- Deploy: Развёртывание в тестовую/боевую среду (если нужно).
- 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:
- New Item → Multibranch Pipeline
- Branch Sources: GitHub / GitLab
- Credentials: GitHub/GitLab API token
- Repository URL: репозиторий
- 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:
- Разработчик делает commit с изменением версии образа в
deployment.yaml. - Argo CD подхватывает изменение из Git.
- Argo CD применяет манифест в кластер (kubectl apply).
- 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 полностью
Типичный полный процесс:
-
CI Pipeline (GitHub Actions / GitLab CI / Jenkins):
- Checkout
- Build
- Tests
- Code quality
- Build Docker image
- Push to registry
-
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
- Argo CD автоматически синхронизирует и развёртывает
или
-
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
Принципы эффективного кеширования:
- Копируйте dependency files перед исходниками
- Располагайте наименее изменяемые инструкции вверху
- Группируйте связанные команды в одну RUN инструкцию
- Используйте .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