Деякі інциденти в продакшені починаються з великого вогню. Більшість починаються з розводження плечима: «Цікаво… на моєму ноутбуку працювало». Ви відправляєте контейнер, він проходить CI, працює в стейджингу, а в проді ніби прокинувся в іншому всесвіті.
Зазвичай так і є. Не тому, що Docker ненадійний. А через людей. Конкретніше: люди, які поводяться з тегами образів так, ніби це номери версій, тоді як насправді це прізвиська. Прізвиська брешуть.
Єдине правило: ніколи не деплойте плаваючі теги
Ось правило, яке закінчує більшість драм «працює на моїй машині» з контейнерами:
Деплойте по дайджесту, а не за змінним тегом.
Не «latest». Не «main». Навіть не «1.2». У продакшені (а бажано й у стейджингу) ви деплойте незмінні ідентифікатори: image@sha256:….
Що вважається «плаваючим» тегом?
Будь-який тег, який може переміститися без вашого відома. А це… більшість тегів.
latestплаває за замовчуванням.main,stable,prod,candidateплавають, бо люди їх переміщують.1.2плаває, якщо ви перебудували той самий тег після патчу базового образу або «лише виправили Dockerfile».- Навіть
1.2.3плаває, якщо ваш регістр дозволяє перезапис тегів і ваш процес цього не запобігає.
Який практичний ефект деплойту по дайджесту?
Ви робите «що запущено» відповіданим одним рядком. Ролбеки стають детерміністичними. Ви вбиваєте клас інцидентів, коли той самий Kubernetes YAML деплойтить різні байти в різні дні.
Є й культурний бік: це змушує розглядати «збірку» і «деплой» як окремі події. Збірка виробляє артефакт. Деплой вибирає цей артефакт. Якщо ваш крок «деплой» тригерить перебудову, ви не деплойте артефакти; ви граєтеся з компілятором.
Жарт сухого гумору #1: Тег «latest» — як молоко в офісному холодильнику: технічно марковане, емоційно небезпечне і ніколи не те, чим здається.
Коли дозволено використовувати теги?
Теги підходять для людей і робочих процесів:
- У CI: «ця збірка створила тег
pr-1847». - У пайплайнах промоції: «перемістити
candidate, щоб вказував на дайджест X». - У деві: «запустити
:latestлокально, якщо ви любите сюрпризи».
Але в продакшені мають працювати дайджести. Якщо ви обов’язково хочете зберегти теги в маніфестах для зручності, тоді увімкніть іммутабельність тегів у регістрі і все одно записуйте дайджест, який ви деплойнули. Інакше ви знову довірятимете прізвиську.
Чому відбувається дріфт (навіть за «правильних» процесів)
Поводження pull — це не моральна оцінка
Docker і Kubernetes не намагаються вас обдурити. Вони прагнуть бути ефективними. Якщо образ з тим самим ім’ям вже є локально, рантайм може не тягнути його з мережі. Якщо у вас кілька нод, у кожної свій локальний кеш. Якщо політика — «if-not-present», дріфт фактично є функцією.
Перебудови в CI означають, що ви відсилаєте рухому ціль
Класична антипатерн виглядає так:
- Розробники мерджать в main.
- CI будує
myapp:latestі пушить його. - CD деплойть
myapp:latest. - Гаряча правка тригерить іншу збірку, яка теж пушить
myapp:latest. - Деякі ноди підтягують, деякі — ні. Або вони підтягують у різний час.
Тепер у вас непланований канарі-розгорт — розподілений логікою кешів. Це не той канарі, який ви хотіли.
Базові образи змінюються, і часто вам цього хочеться
Патчі безпеки — реальність. Багато команд перебудовують образи, щоб отримати виправлені базові шари. Це добре. Але якщо ви перебудували й перезаписали той самий тег, ви перетворили гарну дію з гігієни безпеки в неоднозначність деплойту.
Втягуються регістри, проксі та «дружні» зеркала
Якщо у вас є кеш-регістр, pull-through proxy або дзеркало, у вас з’являється ще один шар, який може віддавати застарілі результати при неправильній конфігурації. Ваш ноутбук може звертатися до Docker Hub. Продакшн — до корпоративного дзеркала з власною логікою оновлення. Той самий тег. Різні байти. Приємного дебагу.
Факти та історія, що пояснюють, чому це повторюється
- Docker Hub популяризував «latest» як UX за замовчуванням. Ранні робочі процеси Docker базувалися на зручності, а не на строгому походженні.
- Зберігання за вмістом передує контейнерам. Модель Git (хеш ідентифікує вміст) старша і концептуально схожа на дайджести образів; теги — це як імена гілок.
- OCI стандартизувала формати образів. Те, що ви тягнете сьогодні, в основному відповідає відкритому специфікації, тому різні рантайми та регістри взаємодіють.
- Мультиархітектурні образи змінили значення «образ». Тег може вказувати на індекс, який вибирає різні платформні маніфести залежно від архітектури ноди.
- Kubernetes взяв ImagePullPolicy за замовчуванням на основі тегів. Якщо ви використовуєте
:latest, воно за замовчуванням — Always; інакше часто — IfNotPresent — тонко і часто неправильно розуміється. - Кешування шарів — оптимізація з наслідками. Воно зменшує трафік і прискорює деплои, але також ховає оновлення тегів, якщо ви не форсуєте pull.
- Безпека ланцюга поставок просунула фіксацію дайджестів у мейнстрім. Походження, підписи, SBOM — ці практики трактують дайджест як анкер.
- «Незмінна інфраструктура» мала зробити деплои нудними. Контейнери полегшили пакування; дайджести полегшили розуміння того, що саме ви запакували.
Одна ідея, варта запам’ятовування і приклеювання до пайплайна:
Вільне тлумачення думки Werner Vogels: розглядайте все як тимчасове і проєктуйте з урахуванням відмов; ви витратите менше часу на молитви і більше — на відновлення сервісу.
Три корпоративні історії з практики
Міні-історія №1: Інцидент через хибне припущення
Середня SaaS-компанія мала акуратну конфігурацію: Kubernetes у продакшені, GitOps для маніфестів, контейнерний реєстр і контролер CD. Вони пишалися дисципліною. Потім стався збій логіну, що зачепив лише третину користувачів. Не всіх. Не всі регіони. Достатньо, щоб зіпсувати вечерю on-call.
Першою підказкою було те, що сигнатура помилки відрізнялася по подах. Деякі падали з винятком про відсутній шлях до CA-бандлу. Інші — працювали. У маніфесті деплойту було myorg/auth-service:1.8. Усі вважали, що 1.8 означає одне й те саме. Це було хибне припущення.
Вони перебудували :1.8, щоб підхопити CVE базового образу тиждень раніше. Регіст виявлявся дозволяв перезапис тегів. Деякі ноди ще мали кешовані старі шари; інші підтягнули перебудований образ під час рутинного churn. Kubernetes був налаштований на IfNotPresent. В результаті — split-brain деплой: два різні образи під одним тегом, що працювали одночасно.
Виправлення було швидким після діагностики: зафіксувати байти дайджестом у маніфесті, примусити rollout, а потім зробити теги іммутабельними для релізів. Постмортем був прямолінійним: «Ми трактували тег як ідентичність». Наступного тижня вони провели аудит усіх продакшн маніфестів на предмет плаваючих тегів і замінили їх на дайджести, зберігши зручні теги у виході CI.
Міні-історія №2: Оптимізація, що відгукнулася бумерангом
Інша організація працювала з високопрохідним API і втомилася від повільного масштабування нод. Пулінг образів під час autoscaling спричиняв холодні старти. Тож вони ввели регістр-мірор у VPC і агресивне кешування на нодах. Це спрацювало: масштабування стало швидшим, витрати на egress зменшилися.
Потім вони розгорнули критичний баг-фікс, і він не «прийшов» скрізь. Деякі ноди довго поводилися за старим. Мірор дотримувався заголовків кешування і мав інтервал оновлення; ноди надавали перевагу локальному кешу, щоб уникнути pull-ів. Команда побудувала швидкість, збільшивши застарілість.
Проблема не в самому кешуванні. Проблема в тому, що кеш вирішував питання коректності. Деплой посилався на api-gateway:stable, який оновлював CI. Тег перемістився, але кешу байди не було діла. Контролер деплойту думав, що зміна пройшла. В певному сенсі вона пройшла, просто не ті байти, що потрібно.
Вони залишили мірор, бо швидке масштабування вартує того. Але змінили контракт: промоція переносить тег на дайджест, деплои використовують дайджест, і кеши тепер безпечні, бо ідентифікатор перестав рухатися. Додатково вони додали просте правило: «якщо продакшн маніфест містить тег без @sha256 — PR провалюється». Нудно. Ефективно.
Міні-історія №3: Нудна, але правильна практика, що врятувала день
Фінансово орієнтована компанія (така, яка проходить аудити заради задоволення) мала непоказний процес релізів. Кожна збірка продукувала образ, підписувала його, записувала дайджест і зберігала цей дайджест поруч із тикетом зміни. Середовища промотилися лише по дайджесту. Жодних винятків. Розробники скаржилися, що це повільно і «корпоративно».
Одної п’ятниці з’явилася вразливість рантайму з лякаючими заголовками. Безпека попросила негайно патчити. Команда перебудувала базові образи, перебудувала сервіси і запушила нові образи. Потім, під час метушні, інша зміна випадково просочилася в контекст збірки одного сервісу — непроглянутий конфіг-твік. У багатьох компаніях так і починається вікенд-аутедж.
Ось що їх врятувало: промоція вимагала явного вибору дайджесту. Неперевірена збірка дала дайджест, що не співпадав з погодженим тикетом змін. Пайплайн відмовився її промотити. Команда виправила вразливість, використовуючи правильні дайджести, і не пустила випадковий конфіг у прод.
Ніхто не отримав оплесків. Не було героїчного дебагу. Система просто відмовилася бути хитрою. Ось до чого треба прагнути.
Жарт сухого гумору #2: Якщо ваш процес деплойту потребує героя, ваш процес — це по суті реаліті-шоу з гіршим освітленням.
Практичні завдання: команди, виводи та рішення
Цей розділ — «Я маю термінал і проблему». Кожне завдання містить команду, реалістичний фрагмент виводу, що це означає, і яке рішення прийняти.
Завдання 1: Подивитися, який образ фактично використовує запущений контейнер (Docker)
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.ID}}'
NAMES IMAGE ID
auth-service registry.local/auth:1.8 7c1d2e9a0b53
billing-worker registry.local/billing:prod 3a9b6d1c4f21
Що це означає: Ви бачите теги, а не дайджести. Це натяк, а не доказ. Тег міг переміститися з моменту старту контейнера.
Рішення: Проскануйте контейнер, щоб знайти незмінний image ID / відображення на дайджест.
Завдання 2: Переглянути image ID контейнера та repo digests (Docker)
cr0x@server:~$ docker inspect auth-service --format '{{.Image}} {{json .RepoDigests}}'
sha256:0e3d2b4f1e1b6d0f1b6b8c9a3f2a1d0c9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4 ["registry.local/auth@sha256:8b4c6a3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5"]
Що це означає: Запущений контейнер посилається на конкретний дайджест. Саме цей дайджест слід зафіксувати та порівнювати між середовищами.
Рішення: Якщо маніфест деплойить по тегу, змініть його, щоб деплойти по цьому дайджесту (або по погодженому).
Завдання 3: Перевірити, чи тег зараз вказує на інший дайджест (віддалена правда)
cr0x@server:~$ docker pull registry.local/auth:1.8
1.8: Pulling from auth
Digest: sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e
Status: Image is up to date for registry.local/auth:1.8
Що це означає: Поточний дайджест регістру для :1.8 показано. Якщо він не збігається з дайджестом із Завдання 2 — тег перемістився.
Рішення: Розглядайте це як провал гігієни релізів. Припиніть деплойити за цим тегом; зафіксуйте дайджест і розслідуйте причину зміни тегу.
Завдання 4: Перелічити локальні образи і побачити repo digests (впіймати «той самий тег, різні дайджести»)
cr0x@server:~$ docker images --digests --format 'table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}}'
REPOSITORY TAG DIGEST IMAGE ID CREATED SINCE
registry.local/auth 1.8 sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e 0e3d2b4f1e1b 9 days ago
registry.local/auth 1.8 <none> 9a1b2c3d4e5f 15 days ago
Що це означає: У вас може бути кілька локальних образів, які колись відповідали одному тегу. Старіший може бути без тегу.
Рішення: Не покладайтесь на наявність тегу в кеші; використовуйте дайджести в деплойтах і очищуйте старі образи при обмеженому диску.
Завдання 5: Форсований чистий pull, щоб усунути неоднозначність кешу (Docker)
cr0x@server:~$ docker rmi registry.local/auth:1.8
Untagged: registry.local/auth:1.8
Deleted: sha256:0e3d2b4f1e1b6d0f1b6b8c9a3f2a1d0c9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4
cr0x@server:~$ docker pull registry.local/auth:1.8
1.8: Pulling from auth
Digest: sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e
Status: Downloaded newer image for registry.local/auth:1.8
Що це означає: Тепер локальний тег розв’язується в поточний вміст регістру.
Рішення: Якщо це змінює поведінку, ви щойно довели дріфт через кеш. Виправляйте процес, а не ноду.
Завдання 6: У Kubernetes подивитися, що Deployment стверджує vs що реально виконують pod-и
cr0x@server:~$ kubectl -n prod get deploy auth-service -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
registry.local/auth:1.8
cr0x@server:~$ kubectl -n prod get pods -l app=auth-service -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[0].imageID}{"\n"}{end}'
auth-service-6d9df7c6d9-7h5qv docker-pullable://registry.local/auth@sha256:8b4c6a3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5
auth-service-6d9df7c6d9-km2nw docker-pullable://registry.local/auth@sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e
Що це означає: Той самий Deployment-тег — різні pod-и з різними дайджестами. Це дріфт.
Рішення: Закріпіть Deployment за одним дайджестом і зробіть rollout. Потім виправте пайплайн, щоб не дозволяти перезапис тегів.
Завдання 7: Перевірити ImagePullPolicy (Kubernetes), щоб зрозуміти поведінку кешу
cr0x@server:~$ kubectl -n prod get deploy auth-service -o jsonpath='{.spec.template.spec.containers[0].imagePullPolicy}{"\n"}'
IfNotPresent
Що це означає: Ноди можуть не підтягувати навіть якщо тег змінився, залежно від стану кешу.
Рішення: Не «виправляйте» це, ставлячи Always всюди як основний контроль. Виправте ідентифікатор (дайджест). Використовуйте Always вибірково для деву, де ви дійсно відслідковуєте теги.
Завдання 8: Підтвердити ревізію rollout і зіставити її зі зміною образу
cr0x@server:~$ kubectl -n prod rollout history deploy/auth-service
deployment.apps/auth-service
REVISION CHANGE-CAUSE
12 image updated to registry.local/auth:1.8
13 configmap reload
Що це означає: Історія показує «image updated», але не дайджест, якщо ви його явно не записали.
Рішення: Почніть анотодовувати деплои дайджестом (або commit SHA + дайджест) під час деплойту.
Завдання 9: Перевірити дайджест тегу в регістрі через інспекцію маніфесту (без гадань)
cr0x@server:~$ docker manifest inspect registry.local/auth:1.8 | head -n 12
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e",
"platform": {
"architecture": "amd64",
Що це означає: Тег вказує на індекс (мультиарх). Ви бачите дайджести по платформах.
Рішення: Закріпіть дайджест індексу, якщо хочете єдине посилання для всіх архітектур, або закріпіть платформні дайджести, якщо потрібен детерміністичний контроль по архітектурах.
Завдання 10: Знайти, яка нода виконує який дайджест (цільова ремедіація)
cr0x@server:~$ kubectl -n prod get pods -l app=auth-service -o wide
NAME READY STATUS RESTARTS AGE IP NODE
auth-service-6d9df7c6d9-7h5qv 1/1 Running 0 2h 10.20.1.14 ip-10-0-4-21
auth-service-6d9df7c6d9-km2nw 1/1 Running 0 2h 10.20.2.33 ip-10-0-6-18
cr0x@server:~$ kubectl -n prod get pod auth-service-6d9df7c6d9-7h5qv -o jsonpath='{.status.containerStatuses[0].imageID}{"\n"}'
docker-pullable://registry.local/auth@sha256:8b4c6a3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5
Що це означає: Ви можете локалізувати дріфт до нод, а не лише до подів.
Рішення: Якщо потрібно негайне стримування, cordon/drain проблемну ноду після виправлення маніфесту; інакше проблема повториться.
Завдання 11: Перевірити, що containerd вважає присутнім (рантайм-рівень правди)
cr0x@server:~$ sudo ctr -n k8s.io images ls | grep registry.local/auth
registry.local/auth:1.8 application/vnd.oci.image.index.v1+json sha256:1111aaaabbbb2222cccc3333dddd4444eeee5555ffff6666777788889999aaaa 245.3 MiB linux/amd64,linux/arm64
registry.local/auth@sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e application/vnd.oci.image.manifest.v1+json sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e 84.1 MiB linux/amd64
Що це означає: Рантајм може зберігати як посилання на теги, так і на дайджести. Список показує, що закешовано і для яких платформ.
Рішення: Якщо поди несподівано не підтягують, підтвердіть, чи дайджест вже є і чи політика pull та поведінка рантайму відповідають очікуванням.
Завдання 12: Підтвердити, що rollout дійсно замінив поди (а не лише оновив spec)
cr0x@server:~$ kubectl -n prod rollout status deploy/auth-service
deployment "auth-service" successfully rolled out
cr0x@server:~$ kubectl -n prod get rs -l app=auth-service --sort-by=.metadata.creationTimestamp
NAME DESIRED CURRENT READY AGE
auth-service-6d9df7c6d9 6 6 6 2h
auth-service-5c7bbf99b8 0 0 0 7d
Що це означає: У вас одна активна ReplicaSet. Добре. Якщо кілька активні несподівано — можливо, часткові rollout-и або зависле завершення.
Рішення: Якщо дріфт триває, проблема, ймовірно, у самому посиланні на образ (тег рухається) або в кешуванні нод разом із змінними ідентифікаторами.
Завдання 13: Виявити, чи маніфест містить плаваючий тег (дешевий запобіжник)
cr0x@server:~$ grep -RIn 'image: .*:[^ @]\+$' k8s/prod | head
k8s/prod/auth/deploy.yaml:27:image: registry.local/auth:1.8
k8s/prod/api/deploy.yaml:19:image: registry.local/api:latest
Що це означає: Рядки показують образи, які містять лише тег. Ви за один крок від неоднозначності через перезапис тегу.
Рішення: Замініть на форму з дайджестом або ввімкніть іммутабельність тегів; краще — і те, й інше.
Завдання 14: Закріпити образ дайджестом у Kubernetes (фактичне виправлення)
cr0x@server:~$ kubectl -n prod set image deploy/auth-service auth-service=registry.local/auth@sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e
deployment.apps/auth-service image updated
cr0x@server:~$ kubectl -n prod get deploy auth-service -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
registry.local/auth@sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e
Що це означає: Ваш бажаний стан тепер незмінний.
Рішення: Закомітьте цю зміну назад у Git, якщо ви використовуєте GitOps; не дозволяйте kubectl стати єдиним джерелом правди.
Завдання 15: Записати дайджест як анотацію деплойту (щоб аудит і відкат були адекватними)
cr0x@server:~$ kubectl -n prod annotate deploy/auth-service deployed-image-digest=sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e --overwrite
deployment.apps/auth-service annotated
cr0x@server:~$ kubectl -n prod get deploy/auth-service -o jsonpath='{.metadata.annotations.deployed-image-digest}{"\n"}'
sha256:2f9e1d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e
Що це означає: Тепер ви можете відповісти «що саме ми деплойнули?» без довгих пошуків у логах.
Рішення: Нехай пайплайн робить це автоматично під час промоції.
Швидкий план діагностики
Якщо продакшн поводиться дивно і ви підозрюєте дріфт версій/образів, не починайте з Dockerfile або графа залежностей. Почніть з ідентичності.
Перше: підтвердьте, чи поди запускають один і той самий дайджест
- Перевірте imageID у подах по репліках.
- Якщо існують кілька дайджестів — у вас дріфт. Зупиніться і виправте посилання деплойту перш за все.
Друге: перевірте, що вказує Deployment (тег чи дайджест)
- Якщо тег: вважайте його потенційно рухомим. Тимчасово небезпечно.
- Якщо дайджест: дріфт не має відбуватися, хіба що ви маєте плутанину з мультиархітектурою, кількома контейнерами або кількома деплойментами.
Третє: порівняйте стан регістру і поведінку кешу нод
- Підтвердіть, який дайджест тег вказує зараз через інспекцію маніфесту або pull, що виводить дайджест.
- Перевірте ImagePullPolicy і кеш рантайму нод.
- Якщо використовуєте мірор/проксі, підтвердіть, що він не віддає застарілі відображення тегів.
Четверте: вирішіть крок стримування
- Найкраще: закріпіть дайджест і зробіть rollout.
- Якщо потрібно терміново уніформізувати: дискодуйте ноди і змусьте нові pull-и після фіксації дайджесту.
- Якщо мультиархітектура: підтвердіть, який платформний дайджест кожна нода тягне; закріпіть відповідний рівень (індекс vs маніфест).
П’яте: напишіть постійне виправлення
- Іммуттабельність тегів у регістрі для релізних тегів.
- Пайплайн промоції, що переміщує середовища по дайджестах.
- Політики в CI, що блокують плаваючі теги у продакшн маніфестах.
Поширені помилки: симптом → корінна причина → виправлення
Помилка 1: «Деякі поди поводяться інакше, але вони в одному Deployment»
Симптом: Різні повідомлення про помилки, різна поведінка, той самий YAML.
Корінна причина: Змінний тег + IfNotPresent + змішані кеші нод (або застарілість мірору).
Виправлення: Деплойте по дайджесту. Потім зробіть rollout, щоб замінити всі поди. Ввімкніть іммутабельність тегів і припиніть перезапис релізних тегів.
Помилка 2: «Ми оновили тег, але нічого не змінилося»
Симптом: CI каже, що пушнув :stable, CD каже, що деплойнув, але поведінка рантайму незмінна.
Корінна причина: Рантайм не підтягнув, бо образ вже був; контролер не побачив зміни spec, що тригерить rollout.
Виправлення: Закріпіть дайджест (примусить зміну spec). Якщо ви мусите використовувати теги, налаштуйте політику, що змушує нові pull-и та тригерить rollout — і прийміть експлуатаційні витрати.
Помилка 3: «Ми закріпили дайджести і все одно бачимо відмінності між нодами»
Симптом: Той самий дайджест у маніфесті, але поведінка різна на arm64 vs amd64 нодах.
Корінна причина: Ви закріпили індексний дайджест замість платформного маніфесту (або навпаки), і підлягаючі платформні образи відрізняються (glibc vs musl, нативні залежності тощо).
Виправлення: Вирішіть, чи ви хочете єдиний мультиарх індекс, чи явну фіксацію по архітектурах. Тестуйте обидві архітектури. Не припускайте паритет.
Помилка 4: «Безпека перебудувала базові образи, і тепер прод неконсистентний»
Симптом: Після перебудови деякі поди починають падати в TLS, DNS або через часові пояси.
Корінна причина: Перебудували той самий тег; оновлення базового образу змінило сертифікати, поведінку libc або версії пакетів.
Виправлення: Перебудовуйте під новим тегом (або унікальним build-тегом), закріплюйте дайджест у деплойті і робіть промоцію з тестами. Продовжуйте патчити базові образи, але припиніть перезапис тегів, які використовуються кластерами в роботі.
Помилка 5: «Відкат не відкатився»
Симптом: Ви відкотили маніфест з :prod на :previous, і все одно бачите нову поведінку.
Корінна причина: Обидва теги вказують на той самий дайджест (тег перемістився), або «попередній» тег був перезаписаний під час перебудов.
Виправлення: Відкат виконуйте по дайджесту. Ведіть незмінний список last-known-good дайджестів для кожного сервісу та середовища.
Помилка 6: «Ми встановили ImagePullPolicy=Always, тож ми в безпеці»
Симптом: Часті повільні деплои, ліміти запитів до регістру та інколи відмови під час проблем регістру.
Корінна причина: Ви замінили ідентичність на pull-поведінку. Тепер коректність залежить від доступності регістру.
Виправлення: Використовуйте дайджести. Залишайте Always для деву де відстеження тегів навмисне, і забезпечте стійкість регістру/мірорів, якщо ви на них спираєтеся.
Чек-листи / покроковий план
Покроково: впровадження правила Image Tag у реальній організації
- Визначте політику: «Продакшн та стейджинг маніфести мають посилатися на образи по дайджесту.» Запишіть її. Нехай її можна перевіряти під час рев’ю.
- Вирішіть схему іменування: Тримайте теги для людей (commit SHA, номер збірки, PR-номер). Дайджести — для машин.
- Зробіть промоцію явною: Промоція переміщує дайджест між середовищами. Не «перебудова і деплой».
- Увімкніть іммутабельність тегів, де можливо: Принаймні для релізних тегів (наприклад,
v1.2.3). Якщо регістр може відмовляти перезаписам — вмикайте це. - Додайте CI-перевірку: Блокуйте продакшн маніфести, що містять
image: repo:tagбез@sha256. - Записуйте деплойнуті дайджести: Додавайте анотації або метадані релізу в Git, щоб інцидент-реагування не залежало від «племінної пам’яті».
- Вирішіть стратегію для мультиархітектури: Ви закріплюєте індекс чи per-arch дайджести? Оформіть документ і протестуйте.
- Навчіть on-call: Вчіть «спочатку перевірити imageID» як стандартний крок для дивної поведінки.
- Ведіть список last-known-good дайджестів: По одному на сервіс для кожного середовища. Це робить відкат однорядковою операцією.
- Періодично аудитуйте: Шукайте маніфести з плаваючими тегами; ставте це як рутинну, нудну роботу, як прострочені TLS сертифікати.
Операційний чек-лист: перед тим як оголосити інцидент «виправленим»
- Всі репліки працюють з одним дайджестом (або очікуваним набором per-arch дайджестів).
- Потрібний стан посилається саме на цей дайджест, а не лише на тег.
- Тег для людиноподібної мітки в регістрі вказує на той самий дайджест, який ви деплойнули (опційно, але корисно для перевірки).
- Роллаут завершено, старі ReplicaSet-и масштабовані до нуля (якщо ви цього не планували).
- План відкату — «змінити дайджест назад», а не «сподівайся, що тег вчора означав те саме».
Чек-лист релізу: як не створювати дріфт
- Кожна збірка створює унікальний тег (commit SHA, build ID) і пушиться один раз.
- Промоція вибирає дайджест з вихідної збірки.
- Під час деплойту перебудов не відбувається; деплой споживає артефакти.
- Оновлення базових образів породжують нові теги/дайджести і ніколи не перезаписують релізні теги, які використовуються кластерами.
- Інцидент-реагування включає захоплення дайджесту з робочих подів у тикет.
FAQ
1) Чому деплой по :latest такий поганий?
Бо це не версія. Це рухомий покажчик. Ви не можете надійно відповісти «що запущено?» і не зможете надійно відкотитися.
2) Хіба семантичний тег типу 1.2.3 не є безпечним?
Тільки якщо ваш регістр і процес роблять його іммутабельним. Інакше це просто тег з кращими манерами. Технічну гарантію дає фіксація по дайджесту.
3) Чи ускладнює фіксація дайджестів роботу людей?
Трохи, доки ви не розділите «мітки для людей» і «ідентичність для машин». Тримайте теги для навігації й дашбордів, але деплойте дайджест. Ваш майбутній «я» тихо подякує.
4) Що з ImagePullPolicy у Kubernetes — чи варто ставити Always?
Не робіть це основним механізмом безпеки. Always збільшує трафік pull-ів і пов’язує коректність із доступністю регістру. Використовуйте дайджести для незмінності, а політику pull підбирайте під потреби продуктивності і свіжості.
5) Якщо я фіксую по дайджесту, чи можу я припинити турбуватися про відтворюваність збірок?
Ні. Фіксація дайджесту гарантує, що ви деплойте один і той самий артефакт повторно. Відтворювані збірки гарантують, що цей артефакт відповідає джерелу і входам, яких ви очікуєте. Це різні проблеми.
6) Як коректно робити відкат?
Тримайте last-known-good дайджест для сервісу. Відкочуйте, змінюючи образ деплойменту на той дайджест. Уникайте відкатів за тегами на кшталт :previous, якщо не забезпечили їхню іммутабельність.
7) Чому дві ноди тягнуть різні образи для того самого тегу?
Бо кеші локальні і політика pull може відрізнятися. Також мультиархітектурні теги можуть розв’язуватись по-різному залежно від архітектури ноди. Теги — ключі запиту, а не стабільні ідентифікатори.
8) У чому різниця між фіксацією індексного дайджесту та платформного?
Індексний дайджест ідентифікує список маніфестів (мультиарх). Платформний дайджест — конкретний маніфест для amd64, arm64 тощо. Фіксуйте той рівень, що відповідає вашій стратегії флоту.
9) Ми використовуємо регістр-мірор. Чи змінює це пораду?
Ні, радше підсилює її. Дзеркала покращують продуктивність pull-ів, але можуть додати застарілість. Дайджести роблять дзеркала безпечними, бо кеш орієнтований на незмінні ідентифікатори.
10) Це тільки проблема Kubernetes?
Ні. Будь-яка система, що деплойть контейнери, може страждати від дріфту тегів: Docker Compose, Nomad, ECS, plain Docker на VM. Підлягаюча проблема однакова: змінні посилання.
Висновок: наступні кроки, які ви можете зробити цього тижня
Якщо ви нічого більше не зробите, зробіть це: виберіть один продакшн сервіс і змініть його деплой з repo:tag на repo@sha256:digest. Запишіть дайджест у метадані деплойту. Підтвердіть, що кожна репліка працює з тим самим дайджестом. Ви щойно ліквідували цілу категорію неоднозначностей.
Потім зробіть це системно:
- Додайте CI-гіт, що відхиляє плаваючі теги в продакшн маніфестах.
- Припиніть перезапис релізних тегів. Якщо регістр підтримує іммутабельність, увімкніть її для простору релізів.
- Розділіть збірку і деплой: збірка продукує дайджести; деплой вибирає дайджести. Промоція — це зміна покажчика в Git, а не перебудова.
- Навчіть on-call перевіряти
imageIDспочатку. Це найшвидша правда у вас під рукою.
«Працює на моїй машині» не означає, що ваші колеги неуважні. Це означає, що ваша система допустила неоднозначність. Приберіть неоднозначність. Зробіть байти нудними.