Production-like учебно-портфельный проект по эксплуатации, доставке, наблюдаемости и восстановлению готового Java microservice.
Статус: проект завершён. Definition of Done выполнен на 53/55 пунктов; два пункта пропущены осознанным решением владельца (см. §44 и docs/dod-checklist.md). Репозиторий:
github.com/sulinivan/java-production-operations-lab(публичный).
Цель проекта — получить практический опыт полного жизненного цикла Java-приложения в production-like инфраструктуре:
source code
↓
GitHub
↓
CI
↓
Docker image
↓
Docker Hub
↓
CD
↓
Kubernetes / kind
↓
Java service
↓
PostgreSQL + S3/MinIO
↓
metrics + logs + health
↓
Prometheus + Loki + Grafana
↓
incident investigation
↓
backup / restore / rollback
Java-разработка не является целью проекта.
Java microservice используется как готовый объект эксплуатации.
Основной навык, который демонстрирует проект:
Умение доставить, развернуть, наблюдать, диагностировать, обновить и восстановить Java-приложение в production-like инфраструктуре.
В качестве application workload используется готовый Java/Spring Boot сервис CloudShare
(secure file sharing, upstream: Dhruv0306/cloudshare-app, MIT; поглощён монорепо — ADR-001).
Приложение разворачивается как есть, со всеми своими зависимостями (ADR-002):
Document
│
├── metadata ──────► PostgreSQL (Flyway миграции)
│
└── content ───────► S3 API / MinIO (зашифровано на стороне приложения)
Фактический состав зависимостей шире минимальной пары из исходной спецификации:
| Зависимость | Роль | Поведение при отказе |
|---|---|---|
| PostgreSQL 17 | authoritative metadata store | приложение деградирует |
| MinIO (S3 API) | содержимое файлов, bucket documents |
upload/download отказывают |
| Redis cache-aside | кэш, allkeys-lru |
потеря безопасна |
| Redis security-instance | blacklist токенов, step-up, noeviction |
fail-closed: login ломается |
| Redis rate-limit | счётчики окон, короткий timeout | fail-open |
| ClamAV | сканирование загружаемых файлов | upload невозможен |
Аутентификация — JWT (+ опциональная MFA); пароли всех трёх Redis и остальные
секреты жёстко валидируются при старте (SecretsStartupValidator, fail-closed).
Полный чек-лист DoD с фактическими доказательствами — docs/dod-checklist.md.
Кратко: инфраструктура поднимается автоматически (Terraform → Lima → Ansible → kind); CI/CD прогоняет цикл от git-тега до smoke-теста через Docker Hub и self-hosted runner; наблюдаемость собирает метрики трёх слоёв и логи восьми контейнеров; выполнен полный DR-цикл с потерей базы и восстановлением, проверенным через REST API; отработаны rollback'и двух видов. Пропущены два инцидентных сценария — MinIO-failure и resource-pressure (решение владельца; runbooks готовы).
Каждая технология существует только потому, что решает конкретную задачу. Ни одного компонента «для демонстрации инструмента» не добавлено — принцип закреплён и применялся через архитектурные решения docs/decisions.md.
Основные связи:
Terraform (guidoiaquinti/lima ~> 0.1.0)
↓
Lima VM java-prod-ops-lab (ubuntu 24.04, vz, aarch64, 6 CPU / 12GiB / 80GiB)
↓
Ansible
↓
Linux host: Docker, kind 'lab', kubectl, Helm, Nginx, GitHub Actions runner
↓
kind Kubernetes
↓
Helm → Java application
├──────────────► PostgreSQL ── backups ──► MinIO/postgres-backups
├──────────────► S3 API → MinIO/documents
├── Redis ×3, ClamAV
│
├── health ────────► Kubernetes probes
├── metrics ───────► Prometheus ← kube-state-metrics, node-exporter,
│ postgres-exporter
└── logs ──────────► Alloy → Loki → MinIO/loki
Prometheus ──────► Grafana ◄───── Loki
Client ──► Nginx (VM edge) ──► kind NodePort ──► Java Pod
Terraform отвечает только за создание инфраструктурной boundary:
Terraform (provider guidoiaquinti/lima ~> 0.1.0, patch-pin)
↓
Lima VM
↓
Linux VM
Terraform не управляет релизами приложения (это Helm), не конфигурирует хост (это Ansible). State хостится локально; провайдер до 1.0 запинен на patch-range.
Ansible приводит созданную Lima VM в состояние пригодное для эксплуатации.
Фактические роли: common (базовые пакеты, inotify sysctls), docker
(apt-репозиторий, ротация json-file логов), kind+helm (инструменты),
nginx (edge), runner (self-hosted GitHub Actions runner + CD-kubeconfig),
cluster (создание kind + платформенные зависимости), app (сборка образа +
деплой + smoke), monitoring (стек наблюдаемости).
Модель:
Terraform: «Создай мне машину»
Ansible: «Сделай эту машину пригодной для эксплуатации»
Lima предоставляет Linux environment всей лаборатории. Фактические параметры
(terraform/variables.tf, применены): Ubuntu 24.04 LTS, backend vz, aarch64,
6 vCPU / 12 GiB RAM / 80 GiB disk. Домашний каталог хоста не пробрасывается
в VM (mounts: null) — deployment target изолирован от developer machine (README-принцип §6).
Проброшены порты 80/443 для edge Nginx.
Developer machine (macOS, Apple M1)
│ terraform apply + ansible over ssh
▼
Lima VM java-prod-ops-lab
├── Docker 29.7.2
├── kind cluster 'lab' (k8s v1.36.4)
├── kubectl v1.36.4, Helm v4.2.4
├── Nginx 1.24 (edge)
└── self-hosted GitHub Actions runner
Инструментарий резолвится как latest stable в момент provisioning; обновление =
пересоздание VM (scripts/bootstrap.sh с нуля ~30 минут).
Репозиторий: github.com/sulinivan/java-production-operations-lab, публичный
(решение владельца; риски self-hosted runner компенсированы — см. §9 и
docs/security.md). Приложение поглощено каталогом без вложенного
.git; история upstream сохранена (ADR-001).
Работа с приложением идёт через Git:
branch → commit → pull request → CI → merge → tag → release → CD
Три workflow в .github/workflows/:
Триггеры: pull_request и push в main. Выполняется на GitHub-hosted
runner (ubuntu-latest) — PR-контекст никогда не касается self-hosted машины:
checkout
↓
mvn verify (unit + JaCoCo coverage gate)
↓
docker build (валидация сборки)
↓
Trivy scan CRITICAL/HIGH = fail (security gate)
Триггер: тег vMAJOR.MINOR.PATCH. Выполняется на self-hosted runner
(нативный linux/arm64, та же архитектура, что у kind-ноды):
checkout → docker login (GitHub Secrets) → build → push
docker.io/sulinivan/cloudshare:<semver>
Триггер: workflow_run после успешного Docker. Self-hosted runner внутри Lima VM
имеет прямой доступ к кластеру через выделенный ServiceAccount
(/opt/lab/kubeconfig-cd, не копия admin-конфига):
checkout head_sha → версия из git describe --tags
↓
helm upgrade --install --atomic (--set image.repository/tag, serviceMonitor.enabled)
↓
kubectl rollout status
↓
bash scripts/smoke-test.sh
Неудачный rollout или smoke = failed pipeline; --atomic откатывает релиз
автоматически (проверено: revision 24 failed → 25 rollback).
Runner является частью deployment environment. Факт:
- имя
lima-lab-runner, labelsself-hosted / Linux / ARM64; - выделенный непривилегированный пользователь
github-runner(группа docker); - systemd-юнит
actions.runner.sulinivan-java-production-operations-lab.lima-lab-runner.service; - регистрация одноразовым токеном через Ansible с
no_log(токен нигде не сохраняется); - CD-задачи работают через ServiceAccount
cd-deployer(kubeconfig/opt/lab/kubeconfig-cd, root:github-runner 0640) — отзыв доступа = удаление SA.
Security implications публичного репозитория + self-hosted runner задокументированы и компенсированы (docs/security.md): fork-PR требуют approval, CD триггерится только push/tag, секреты недоступны из fork-контекста.
Приложение упаковано в versioned multiarch-образ (linux/arm64 собран нативно):
sulinivan/cloudshare:0.1.0
sulinivan/cloudshare:0.1.1
latest не используется как deployment identifier; тег обязателен в шаблоне
(required "image.tag"). Базовый образ runtime — eclipse-temurin:17-jre-jammy
(multiarch; alpine-варианты amd64-only — единственное изменение приложения,
ADR-005). Non-root user, HEALTHCHECK на actuator liveness сохранены.
Registry: docker.io/sulinivan/cloudshare.
git tag v0.1.1 → Docker workflow (self-hosted) → Docker Hub
→ CD workflow → Kubernetes imagePull
Kubernetes не зависит от локальных образов: финальные деплои получают image из
Hub (в ручном dev-цикле возможен interim kind load — помечен в коде как
не-final путь).
Однонодовый kind-кластер lab (k8s v1.36.4). Осознанное ограничение лаборатории,
production-grade HA не заявляется. Демонстрировано: deployment, service discovery,
probes, resource limits, ConfigMap/Secret, rollout/rollback, logs, metrics.
Stateful-зависимости — Deployment + PVC (local-path), не StatefulSet — осознанное решение ADR-003 согласно этому разделу исходной спецификации. Размещение зафиксировано и не менялось между этапами.
Базовые ресурсы приложения: Namespace cloudshare, Deployment, Service (NodePort
30080), ConfigMap (checksum-аннотация → rolling restart), Secret (только ссылки —
ADR-009), ServiceMonitor. Edge-интеграция — Nginx хоста VM вместо Ingress-контроллера.
Helm — application release layer. Фактическая структура:
helm/cloudshare/
├── Chart.yaml # appVersion перезаписывается --set image.tag
├── values.yaml # конфиг по умолчанию
├── values-dev.yaml # диагностический профиль (plaintext DEBUG логи)
└── templates/
├── _helpers.tpl
├── configmap.yaml # checksum-аннотация → rolling update
├── deployment.yaml # probes, env/envFrom, resources
├── service.yaml # NodePort 30080
└── servicemonitor.yaml # включается флагом
Шаблоны secret.yaml и ingress.yaml отсутствуют намеренно (ADR-009):
chart ссылается на platform Secrets cloudshare-db/-minio/-app, поэтому
helm rollback и переустановка никогда не трогают credentials; edge обслуживает
nginx хоста VM, ingress controller без эксплуатационной задачи не ставился.
Продемонстрировано на живых ревизиях: helm install/upgrade/history/rollback
(revision history 23–28, включая failed upgrade и оба вида отката).
Nginx — edge reverse proxy на хосте Lima VM; не заменяет Kubernetes Service.
api.lab.local → 127.0.0.1:8080 → kind mapping :8080 → NodePort 30080 → Pod
grafana.lab.local → 127.0.0.1:3000 → kind mapping :3000 → NodePort 30300 → Grafana
Реализовано: reverse proxy, access/error логи per-vhost, upstream error handling (502 при недоступных апстримах — наблюдалось до подъёма кластера), client_max_body_size под multipart-лимиты приложения, WebSocket upgrade для Grafana Live. TLS termination не реализован (опциональный пункт исходной спецификации).
CloudShare — главный workload проекта. Приложение не изменялось без необходимости; единственное изменение за весь проект — замена базового образа Dockerfile на multiarch (ADR-005, ~3 строки). Изучается эксплуатация, а не разработка.
API используется для smoke tests, verification деплоя, incident reproduction.
Фактические операции (полная спека приложения — cloudshare-app/docs/system-design/api-spec.md):
POST /api/v1/auth/register # уникальный пользователь для каждого прогона
POST /api/v1/auth/login # → accessToken (JWT)
POST /api/v1/files/upload # multipart `file`; Tika MIME check + ClamAV
GET /api/v1/files?page=&size= # metadata list
GET /api/v1/files/{id}/download # расшифровка на лету
DELETE /api/v1/files/{id} # soft-delete (см. §22)
Smoke-цикл (scripts/smoke-test.sh, README §38): health → register → login →
upload → list → download (byte-compare) → delete → проверка обеих сторон хранилища.
Для upload используется валидный PNG: magic-byte MIME check отбраковывает случайные байты.
deployment → curl API → HTTP 200/201/204 → deployment successful
PostgreSQL хранит file metadata, пользователей, sharing, audit (партиционирован
приложением), relational state. As-built: postgres:17-alpine, Deployment + PVC 5Gi
(Recreate), БД cloudshare, application user cloudshare_user (владелец схемы,
пароль из Secret cloudshare-db), экспортёру выдан GRANT pg_monitor.
Отработано администрирование: создание пользователя/БД, роли, active sessions
(pg_stat_activity), database size, connection pool Hikari (лимит понижен до
лабораторного значения через chart values), логи, восстановление.
PostgreSQL → pg_dump → gzip → mc pipe → MinIO s3://postgres-backups/
postgres/scripts/backup.sh: имя файла несёт timestamp + identifier + pg-version —
cloudshare-20260825-221336-pg17.11.sql.gz
Backup считается невалидным без проверки восстановления (см. §19). В bucket лежат два артефакта реальных прогонов.
Обязательный сценарий выполнен полностью:
working database (пользователь + файл)
↓
backup → MinIO ✓
↓
DROP DATABASE ... WITH (FORCE) # намеренная потеря
↓
CONFIRM=yes restore.sh <backup> # guard от случайного запуска
↓
database recreated → dump applied → app restarted
↓
verify-restore.sh drill2@lab.local → «файлов у пользователя: 1» ✓
Недостаточно pg_restore completed successfully — доказано через REST API:
Java переподключилась, известный пользователь видит свои данные. Попутно
подтверждена точность point-in-time: попытка verify пользователя, созданного
ПОСЛЕ backup, честно вернула 401.
S3-compatible object storage, Deployment + PVC 20Gi. Buckets (создаются Job
minio-bucket-init, idempotent --ignore-existing):
documents # содержимое файлов приложения
postgres-backups # дампы PostgreSQL
loki # chunks + TSDB index бэкенда логов
Каждый bucket имеет отдельную ответственность; связь Loki→MinIO даёт вторую независимую нагрузку на object storage.
Приложение работает с S3-compatible API через MinIO client; концептуально backend
заменяем на AWS S3 без изменения бизнес-модели. Credentials приложения передаются
через Kubernetes Secret cloudshare-minio.
Upload/download/delete проверяются smoke-тестом с обеих сторон хранилища:
upload → metadata created AND объект появился в documents (счётчик объектов)
download → содержимое совпадает побайтово (расшифровка on-the-fly)
delete → metadata исчезла из list/download немедленно
Важное уточнение к исходной модели: удаление в приложении — soft-delete.
После DELETE /files/{id} (HTTP 204) объект MinIO остаётся до прогона
FilePurgeScheduler (app.scheduler.file-purge.cron, по умолчанию 02:00 UTC
ежедневно) — это документированное поведение приложения, а не рассинхрон
(ADR-010). Smoke-тест поэтому проверяет metadata-сторону строго (204 → нет в
list → download 404), а состояние объекта фиксирует информационно; полный цикл
delete→purge — отдельный ops-эксперимент с временным fast cron.
Actuator probes включены приложением; Kubernetes использует их так
(helm/cloudshare/values.yaml):
startupProbe → /actuator/health/liveness period 5s × 24 (JVM + Flyway)
livenessProbe → /actuator/health/liveness ТОЛЬКО livenessState
readinessProbe → /actuator/health/readiness группа включает db
Readiness включает состояние БД явно: MANAGEMENT_ENDPOINT_HEALTH_GROUP_READINESS_INCLUDE=readinessState,db.
Нюанс Boot 4: вариант additional-paths не связался — явный include надёжен.
Liveness сознательно НЕ зависит от БД: временный отказ PostgreSQL не рестартует pod.
Проверено контролируемой потерей БД: readiness DOWN → pod снят с эндпоинтов
Service (notReadyAddresses), liveness UP, restarts = 0.
Java: Micrometer /actuator/prometheus (JVM memory/GC/threads, HTTP rate/errors/latency)
Kubernetes: kube-state-metrics + node-exporter
PostgreSQL: postgres-exporter (v0.15.0; DATA_SOURCE_NAME из Secret, роль pg_monitor)
Pipeline: ServiceMonitor → Prometheus → Grafana
Все targets up; живые проверки: sum(jvm_memory_used_bytes) и count(kube_pod_info)
возвращают значения, счётчик запросов приложения растёт со smoke-трафиком.
Backend — Prometheus из kube-prometheus-stack 88.5.4 (Operator + Alertmanager +
Grafana + kube-state-metrics + node-exporter), namespace cloudshare (§41).
Скрейпы несуществующих в kind control-plane компонентов (etcd/scheduler/cm/proxy)
отключены — иначе вечные ложные алерты. Селекторы *NilUsesHelmValues: false,
чтобы оператор видел ServiceMonitor/PrometheusRule приложения.
Alert rules (monitoring/prometheus/rules/cloudshare-alerts.yaml) покрывают все
шесть требований: CloudshareAppUnavailable, HighHttpErrorRate (>5% 5m), HighLatencyP95
(>2s 10m), PodRestartStorm (>3 за 30m), ContainerMemoryNearLimit (>85% limit 10m),
PostgresUnavailable. Живое подтверждение конвейера: PodRestartStorm переходил в firing
на реальных рестартах exporter'а.
Логи приложения идут через stdout/stderr (JSON-энкодер logstash уже встроен в
non-dev профиль приложения — менять ничего не потребовалось). Collector — Alloy
DaemonSet: читает /var/log/pods ноды kind (ключевым оказался mount varlog),
labels namespace/pod/container, push → Loki.
Loki 7.3 (app 3.6.12), режим SingleBinary, schema v13 / store tsdb /
object_store s3. Backend — MinIO bucket loki: в бакете лежат реальные chunk-
файлы и TSDB-индексы. retention 168h. Три связи MinIO подтверждены:
MinIO: documents / postgres-backups / loki
Общая operational UI (NodePort 30300 + edge vhost grafana.lab.local). Дашборды
провижинены через sidecar из ConfigMap grafana-dashboards-cloudshare:
- CloudShare / Application & JVM — request rate, error rate, p50/p95 latency, heap, GC, threads
- CloudShare / Kubernetes — pod CPU/memory vs limit, restarts, pod phases
- CloudShare / PostgreSQL — pg_up, connections, db size, transactions
Datasources: Prometheus (из stack) + Loki (additionalDataSources). Поиск логов
работает: {namespace="cloudshare"} возвращает потоки восьми контейнеров.
Workflow закреплён и использовался в реальных разборах:
Alert → Metrics → affected service → Logs → error → Health checks
→ Fix / rollback / restore → Verify metrics → Verify API
Пример из практики проекта: PodRestartStorm (metrics) → рестарты exporter (k8s events) → причина найдена в логах → фикс образа → alert resolved.
Runbooks всех семи сценариев написаны по шаблону §46 (docs/incidents/).
Исполнение:
| Сценарий | Статус | Как доказан |
|---|---|---|
| Broken release (§30) | ✅ выполнен | CD run c несуществующим image.tag=main: rollout timeout → pipeline FAILED → авто-откат --atomic (rev 25) |
| PostgreSQL unavailable (§31) | ✅ выполнен | DROP DATABASE: readiness DOWN, pod снят с трафика, liveness UP, restarts 0 |
| PostgreSQL credentials broken (§32) | ◐ частично | механику покрывают смежные прогоны (рассинхрон секретов ловится валидатором старта); отдельного сценария смены пароля не проводилось |
| MinIO unavailable (§33) | ❌ пропущен | решение владельца; runbook готов |
| Data loss → restore (§34) | ✅ выполнен | полный DR-цикл с known-user проверкой через API (§19) |
| Bad configuration (§35) | ✅ выполнен | неверный config value через Helm values → отказ → диагностика → исправление конфигурацией; плюс фикс readiness-группы с наблюдением probes |
| Resource pressure (§36) | ❌ пропущен | решение владельца; runbook готов |
Подтверждено полным циклом релизов v0.1.0 и v0.1.1:
git push → CI passes → image created → pushed to Docker Hub
→ CD starts (workflow_run) → Helm upgrade → rollout
→ readiness successful → REST smoke test OK
Pipeline завершается ошибкой при неудаче любого шага — доказано реальным failed run (rollout timeout на несуществующем теге).
scripts/smoke-test.sh выполняется автоматически после каждого деплоя (внутри
CD job) и вручную:
health (liveness+readiness UP)
↓ create unique user → login
↓ upload (valid PNG) → object counter in MinIO +1
↓ list содержит файл
↓ download → byte-compare
↓ delete → 204, отсутствует в list, download 404
Два независимых доказательства:
- Автоматический —
--atomic: неудачный upgrade (rev 24) откатился сам (rev 25), сервис остался доступен. - Ручной —
helm rollbackпосле осознанного изменения конфигурации (rev 27 → 28), rollout успешен, readiness UP.
Фактическая структура репозитория:
java-production-operations-lab/
│
├── README.md # этот документ (as-built)
├── .env.lab.example # формат локальных секретов (реальные — вне Git)
│
├── docs/
│ ├── architecture.md # размещение компонентов, версии стека
│ ├── decisions.md # 10 ADR — все ключевые решения
│ ├── dod-checklist.md # итоговая верификация DoD 48/50
│ ├── deployment.md # процедуры bootstrap/deploy/backup/destroy
│ ├── operations.md # регламент эксплуатации
│ ├── security.md # модель секретов, риски public repo + runner
│ ├── troubleshooting.md # симптом → проверка → runbook
│ ├── backup-restore.md # политика и процедуры DR
│ ├── evidence/README.md # реестр доказательств (без скриншотов, §45)
│ └── incidents/ # 7 runbooks по шаблону §46
│
├── terraform/ # main/variables/outputs/versions.tf + lock
├── ansible/
│ ├── ansible.cfg
│ ├── inventory/hosts.ini.example
│ ├── playbooks/ # site, common, docker, kubernetes, nginx,
│ │ # runner, cluster, app, monitoring
│ └── roles/ # common docker kind helm nginx runner
│ # cluster app monitoring (+templates)
├── kind/cluster.yaml # extraPortMappings 8080→30080, 3000→30300
│
├── helm/cloudshare/ # Chart, values, values-dev, templates/*
│
├── kubernetes/ # platform deps: namespace, postgres,
│ # minio(+bucket-init), redis×3, clamav
│
├── postgres/scripts/ # backup.sh, restore.sh, verify-restore.sh
├── monitoring/ # prometheus/(values,rules), loki/, alloy/,
│ # grafana/dashboards/, postgres-exporter.yaml
├── scripts/ # bootstrap, gen-inventory, gen-secrets,
│ # apply-secrets, deploy, smoke-test, destroy
├── .github/workflows/ # ci.yml, docker.yml, cd.yml
│
└── cloudshare-app/ # готовый workload (Spring Boot 4, MIT);
# src, migrations, frontend, tests, docs
developer machine (macOS)
▼
Lima VM java-prod-ops-lab
├── kind cluster 'lab'
├── Nginx edge (host 80/443)
└── self-hosted runner
Внутри Kubernetes — один namespace cloudshare: Java, PostgreSQL, MinIO,
Redis ×3, ClamAV, Prometheus stack, Grafana, Loki, Alloy, postgres-exporter.
Таблица портов:
| Уровень | Порт | Назначение |
|---|---|---|
| macOS host | 80/443 | forwarded → VM nginx |
| VM loopback | 8080 / 3000 | kind extraPortMappings |
| kind node | 30080 / 30300 | NodePort cloudshare / Grafana |
Размещение stateful-зависимостей зафиксировано (ADR-003) и не менялось между этапами.
Ни один реальный credential не хранится в Git. Механизм: scripts/gen-secrets.sh
генерирует .env.lab (chmod 600, gitignored) → scripts/apply-secrets.sh
создаёт Kubernetes Secrets → chart только ссылается на них (ADR-009).
Фактический набор:
| Секрет | Хранение | Потребитель |
|---|---|---|
| POSTGRES_USERNAME/PASSWORD | .env.lab |
Secret cloudshare-db |
| MINIO_ACCESS_KEY/SECRET_KEY | .env.lab |
Secret cloudshare-minio |
| JWT_SECRET, CRYPTO_MASTER_KEK | .env.lab |
Secret cloudshare-app |
| REDIS_*_PASSWORD ×3 | .env.lab |
Secret cloudshare-app |
| GRAFANA_ADMIN_PASSWORD | .env.lab |
helm --set-string при установке |
| DOCKERHUB_USERNAME/TOKEN | GitHub Secrets | docker.yml push |
| GITHUB_RUNNER_TOKEN | одноразовый, env при установке | Ansible, не сохраняется |
Приложение дополнительно валидирует форму секретов на старте (fail-closed).
Три различаемых понятия на живых примерах:
Git tag: v0.1.0, v0.1.1
Docker tag: 0.1.0, 0.1.1 (= тег без префикса v)
Helm revision: 23…28 (инкремент каждого upgrade/rollback)
latest не используется для deployment; пустой image.tag в шаблоне запрещён.
Итог верификации: 53 из 55 пунктов выполнены, каждый подтверждён командой или прогоном pipeline. Детальная таблица с доказательствами — docs/dod-checklist.md.
Резюме по слоям:
Infrastructure 4/4 CI 6/6
CD 6/6 Application 8/8
PostgreSQL 6/6 Kubernetes 10/10
Observability 8/8 Operations 5/7*
* пропущены MinIO-failure и resource-pressure — решение владельца проекта; runbooks обоих сценариев написаны и готовы к исполнению.
По решению владельца проекта фаза скриншотов-доказательств пропущена.
Существует реестр ожидаемых артефактов с требованием формата
Problem/Action/Expected/Actual/Evidence — docs/evidence/README.md;
доказательства зафиксированы текстово в docs/dod-checklist.md.
Каждый operational scenario имеет runbook по шаблону (Symptoms → Impact → Detection → Investigation → Root cause → Recovery → Verification → Prevention):
docs/incidents/
├── broken-release.md # ✅ исполнен
├── postgres-failure.md # ✅ исполнен
├── postgres-credentials.md # ◐ частично
├── minio-failure.md # ❌ пропущен (runbook готов)
├── postgres-data-loss.md # ✅ исполнен (главный DR)
├── bad-configuration.md # ✅ исполнен
└── resource-pressure.md # ❌ пропущен (runbook готов)
Главный сценарий проекта выполнен дважды (v0.1.0, v0.1.1):
git tag v0.1.1 → push
→ CI (mvn verify, Trivy gate)
→ Docker workflow (arm64 build на self-hosted runner)
→ Docker Hub: docker.io/sulinivan/cloudshare:0.1.1
→ CD workflow: helm upgrade --atomic на self-hosted runner
→ Kubernetes rollout → SMOKE OK
→ в кластере крутится образ из Docker Hub
Затем намеренно ломался релиз:
bad release (image.tag=main) → rollout timeout → pipeline FAILED
→ auto-rollback (--atomic) → сервис жив
→ fix версии из git describe → повторный тег → deployed
И моделировалась потеря данных:
known user + file → backup → MinIO
→ DROP DATABASE → readiness DOWN, restarts 0
→ restore из MinIO → Java reconnect
→ REST verification: известный пользователь видит свои файлы
Это и есть главный результат проекта.
Проект демонстрирует не знание отдельных инструментов, а способность построить и эксплуатировать связанный delivery/runtime environment:
Infrastructure → Configuration → CI → Artifact → CD → Runtime
→ Dependencies → Observability → Incident response → Recovery
Каждый слой зависит от предыдущего и предоставляет основу следующему — вся цепочка проверена на практике.
Главная цель достигнута:
Take a ready-made Java service from source control to a reproducible production-like environment, operate it under normal and failure conditions, observe it, diagnose incidents, roll back releases, and recover persistent data.
Если компонент нельзя объяснить фразой:
«Без этого компонента конкретная эксплуатационная задача проекта не решается»
— компонент не добавляется.
Принцип соблюдён на протяжении всего проекта: ни Redis/Kafka/ArgoCD/Istio/Vault, ни StatefulSet, ни ingress-controller, ни secret.yaml в chart не появились «ради списка технологий». Каждое решение зафиксировано с обоснованием в docs/decisions.md (10 ADR), включая отказы от компонентов.