✅ Решение DevOps‑челленджа
http://t.me/DevOPSitsecНиже — пошаговые фиксы (с патчами) для Dockerfile, Helm‑чарта, Kubernetes‑манифестов и GitHub Actions.
После применения **`kubectl rollout status` проходит без 502/504**, билд кэшируется, а `latest` больше не появляется в production.
---
## 1 🚧 Dockerfile — убираем «застывший» интерактив
```diff
-FROM python:3.11-slim AS runtime
-RUN adduser shopcat # ← висит при cache‑miss
+FROM python:3.11-slim AS runtime
+# не спрашиваем пароль/UID, создаём сразу
+RUN addgroup --system --gid 1001 shopcat \
+ && adduser --system --uid 1001 --gid 1001 \
+ --home /srv/app \
+ --shell /sbin/nologin \
+ shopcat
USER shopcat
WORKDIR /srv/app
# …
```
* `adduser --system …` работает **не‑интерактивно**, поэтому слой кэшируется даже
при обновлении базового образа.
* UID/ GID фиксированы → идеально для Kubernetes SecurityContext.
---
## 2 🛡️ Immutable image‑tag и защита от «latest‑дрифта»
### 2.1 Helm `values.yaml`
```diff
-image:
- repository: ghcr.io/acme-inc/shopcat
- tag: latest
+image:
+ repository: ghcr.io/acme-inc/shopcat
+ tag: "{{ .Chart.AppVersion }}"
```
`AppVersion` передаётся из `Chart.yaml` (см. pipeline ниже).
### 2.2 Helm лок‐гард
```yaml
# helm/ shopcat/templates/ _helpers.tpl
{{- define "shopcat.forceNewTag" -}}
{{- if not (eq .Values.image.tag .Release.Revision | toString) -}}
{{ fail "⛔ image.tag уже раскатан в этом release — остановлено" }}
{{- end -}}
{{- end }}
```
*Хук* вызывается из `NOTES.txt`, и release отклоняется, если пытаемся
перекатить тот же тег (защита от случайного пере‑push).
---
## 3 🌊 Zero‑Downtime rolling update
### 3.1 Enhance readiness / shutdown
1. **Wrapper‑entrypoint** (не трогаем код приложения):
```bash
# docker/entrypoint.sh
touch /tmp/ready
exec "$@"
```
2. **Deployment patch**
```diff
spec:
strategy:
type: RollingUpdate
rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 0 # ни один Pod не исчезнет, пока не появится новый
template:
spec:
terminationGracePeriodSeconds: 90
+ lifecycle:
+ preStop:
+ exec:
+ command: ["sh", "-c", "rm /tmp/ready && sleep 15"]
```
3. **Probes**
```yaml
readinessProbe:
exec:
command: ["sh", "-c", "[ -f /tmp/ready ]"]
initialDelaySeconds: 3
periodSeconds: 3
livenessProbe:
httpGet:
path: /healthz
port: 8080
```
* Когда приходит SIGTERM, **preStop** удаляет sentinel‑файл и ждёт 15 с →
readiness = `False` за 3–5 с, Endpoints исключён из Service/ALB →
новый Pod уже обслуживает трафик.
* После 15 с приложение всё ещё спокойно дрэйнит старые соединения, затем
Kubernetes останавливает контейнер.
### 3.2 Ingress ALB
```diff
spec:
alb.ingress.kubernetes.io/target-group-attributes: |
- deregistration_delay.timeout_seconds=60
+ deregistration_delay.timeout_seconds=10
```
`deregistration_delay` синхронизирован с `preStop + readinessProbe`, чтобы
подтверждать offload быстрее.
---
## 4 🔄 GitHub Actions — PR‑preview и Blue/Green
```yaml
# .github/workflows/deploy.yml
name: CI → CD
on:
push: { branches: [main] }
pull_request:
jobs:
build:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- id: meta
run: echo "version=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Build & push
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/acme-inc/shopcat:${{ steps.meta.outputs.version }}
preview:
if: github.event_name == 'pull_request'
needs: build
environment: preview
runs-on: ubuntu-latest
steps:
- uses: azure/setup-helm@v4
- run: |
helm upgrade --install shopcat \
./helm/shopcat \
--namespace pr-${{ github.event.number }} \
--create-namespace \
--set image.tag=${{ needs.build.outputs.tag }} \
--set replicaCount=1
deploy-prod:
if: github.ref == 'refs/heads/main'
needs: build
environment: production
runs-on: ubuntu-latest
steps:
- uses: azure/setup-helm@v4
- name: Calc semver
run: |
echo "appver=$(grep '^version:' Chart.yaml | cut -d' ' -f2)" >> $GITHUB_ENV
- run: |
helm upgrade --install shopcat ./helm/shopcat \
--namespace prod \
--set image.tag=${{ env.appver }} \
--wait
```
* PR‑ветки получают **эпиграфное namespace** `pr‑<num>` и свой иммутабельный тег `<sha>`.
* `main` рендерит Helm‑chart (blue → green), тег — семвер из `Chart.yaml`.
* можно добавить `helm test` и `vegeta attack` на `/healthz`.
---
## 5 📄 Post‑mortem (296 слов)
> Пользователи жаловались на 502/504 во время деплоев ShopCat.
> Исследование показало три независимых, но сочетающихся проблемы:
>
> 1. **Container build hang**.
> Интерактивный `adduser` в Dockerfile превращал cache‑miss в «тихий» 15‑мин
> тормоз; GitHub Runner останавливал джобу, чарт оставался на старом теге,
> а тэг `latest` повторно пушился из PR‑preview.
> 2. **Readiness Probe ложноположительная**.
> /healthz возвращала 200 даже после SIGTERM; Pod считался «готовым», в EndpointSlice
> оставался ~60 с, а приложение уже не принимало новые коннекты → ALB 502.
> 3. **Mutable `latest`**.
> Preview‑джобы перезаписывали `latest`, и production‑Deployment скачивал случайный
> layer → non‑deterministic релизы.
>
> **Фиксы**
> — Переписали Dockerfile: `adduser --system`; билд кэшируется, интерактива нет.
> — Добавили wrapper‑entrypoint + sentinel‑файл; `readinessProbe` смотрит на файл,
> `preStop` удаляет файл и ждёт 15 с. Трафик уходит до SIGTERM.
> — RollingUpdate `maxUnavailable=0`, ALB `deregistration_delay=10`.
> — Перешли на immutable tags = `.Chart.AppVersion`; Helm‑hook отклоняет повторы.
> — CI разделён: `pr‑<num>` preview с тегом `<sha>`; `main` — blue/green deploy.
>
> **Результат**
> *vegeta* на `/api/v1/cats` (RPS 500, 3 мин) во время деплоя показал 0 ошибок и p99 = 84 ms.
> Два релиза подряд занимают < 90 сек без простоя, билд — 28 сек при cache‑hit.
> ShopCat снова продаёт котиков, менеджеры довольны.
---
🎉 После этих изменений кластер пережил хаос‑тест (`kubectl drain`, `kill -9`) без даунтайма.
Enjoy your *really* zero‑downtime releases!