সরাসরি পর্বে যান
পর্ব ৫৮ · ৮১-এর মধ্যে · মডিউল ১০

Artifact আর promotion — একবার build, সব জায়গায় deploy

Artifacts and Promotion — Build Once, Deploy Everywhere

Taught in Bangla, this lesson shows two builds of one source producing different digests, why rebuilding per environment voids testing, reproducible builds, keeping config outside the artifact, promotion by tagging the same digest through gates, SemVer, commit SHA and build numbers, deploying by digest, retention, rollback that depends on old digests surviving, and tracing a Pod back to its commit.

এই পর্বে যা শিখবেন

  • Environment-প্রতি rebuild কেন পরীক্ষার ফলকে অর্থহীন করে, আর digest কেন বদলায়
  • Reproducible build কী, আর সেটি promotion-এর বিকল্প নয় কেন
  • Config আর artifact আলাদা রেখে একই digest-কে dev আর prod-এ চালানো
  • Tag বসিয়ে promotion, গেট আর অনুমোদন দিয়ে environment-এর ধাপ সাজানো
  • SemVer, commit SHA আর build number-এর আলাদা কাজ, আর digest দিয়ে deploy
  • Artifact কোথায় রাখবেন, কত দিন রাখবেন, আর digest দিয়ে rollback কীভাবে করবেন
  • Label, annotation আর build record দিয়ে Pod থেকে commit পর্যন্ত খুঁজে বের করা

১ · একই source, দুই build, দুই digest

একটি ওষুধ কারখানার কথা ভাবুন। ল্যাব একটি ব্যাচ পরীক্ষা করে পাস দিল। এবার বাজারে পাঠানোর দিন কেউ বলল, “ফর্মুলা তো একই, নতুন করে আরেক ব্যাচ বানিয়ে পাঠাই।” ফর্মুলা এক হতে পারে, কিন্তু ল্যাব তো এই নতুন ব্যাচ পরীক্ষা করেনি। কাঁচামালের চালান আলাদা, মেশিনের দিন আলাদা। কাগজের সিলটি আগের ব্যাচের, ওষুধটি অন্য।

Software-এ এই ভুল খুব সাধারণ। Pipeline-এ build-dev, build-staging আর build-prod নামে তিনটি আলাদা job থাকে। সবাই ভাবেন, commit তো একই। সত্যিই কি একই জিনিস তৈরি হয়? আমরা মেপে দেখেছি।

আউটপুট সম্পর্কে: এই পর্বের প্রতিটি transcript একটি আসল run-এর: Docker Engine ২৯ আর BuildKit, এই কম্পিউটারের 127.0.0.1:18581-এ চালানো registry:2 (version 2.8.3), আর একটি local single-node cluster (Kubernetes ১.৩৪), namespace abcl-ap. আপনার run-এ id, digest, size, নাম, সময় আর IP আলাদা হবে। কোনো দূরের registry-তে কিছু push করা হয়নি, আর কাজ শেষে সব মুছে ফেলা হয়েছে। App-টি একটি কাল্পনিক অনলাইন বাজার। Browser-এ Docker চলে না, তাই transcript পড়ার জন্য, আর simulator নিজে বদলে চালানোর জন্য।

Dockerfile-টি ছোট। শেষের RUN ধাপে build-এর সময় একটি ফাইলে লেখা হয়, যেমন অনেক প্রজেক্ট version পাতায় “কবে বানানো” দেখায়:

FROM alpine:3.20
WORKDIR /app
COPY VERSION start.sh ./
RUN chmod +x start.sh && date -u > /app/built-at.txt
CMD ["/app/start.sh"]
$ docker build --no-cache --progress=plain -t abcl-ap-web:build-a .
…
#9 exporting manifest list sha256:4b329749109e6c399411d87f083928c5ac7352bd194d46ac580831482157de49 0.0s done
$ docker build --no-cache --progress=plain -t abcl-ap-web:build-b .
…
#9 exporting manifest list sha256:134548e5d7b79b385e62b2684ccd29f3bfc0437b57f039c4ee860310a0919d6e 0.0s done
$ docker run --rm abcl-ap-web:build-a cat /app/built-at.txt
Fri Oct 30 20:33:26 UTC 2026
$ docker run --rm abcl-ap-web:build-b cat /app/built-at.txt
Fri Oct 30 20:33:29 UTC 2026

একই ফোল্ডার, একই Dockerfile, তিন সেকেন্ডের ব্যবধান। Digest 4b32… আর 1345…, মিল নেই। --no-cache এখানে ইচ্ছা করে দেওয়া, কারণ CI-এর নতুন runner-এ সাধারণত cache থাকে না। আর digest যে বিষয়বস্তুর hash, সেটি আপনি পর্ব ৪১-এ দেখেছেন। তাই digest আলাদা মানে বাইট আলাদা।

এবার date বাদ দিয়ে দেখি। হয়তো দোষ শুধু ওই একটি লাইনের? Dockerfile থেকে date সরিয়ে RUN chmod +x start.sh রেখে আবার দুবার --no-cache build করা হলো। তারপর দুটিকে পাশাপাশি রাখা হলো:

যা মেপেছিBuild ১Build ২ফল
Alpine-এর base layer08bc4e53…08bc4e53…এক
WORKDIR-এর layerda8b6377…da8b6377…এক
COPY-এর layer651a2652…c48a53cf…আলাদা
RUN chmod-এর layer5f70bf18…5f70bf18…এক
Config-এর Created…20:34:10…20:34:13আলাদা
Image manifest9f75ad47…78970e9f…আলাদা

কোডের কোনো বদল নেই, তবু image আলাদা। Layer-এর tar-এ ফাইলের সময় থাকে, আর image config-এ তৈরির সময়। দুটিই hash-এর অংশ। এটি নির্দোষ পার্থক্য। কিন্তু একই দরজা দিয়ে অদৃশ্য বদলও ঢোকে। আসল ঝুঁকির তালিকা হলো:

  • Base image সরে যাওয়া: alpine:3.20 একটি tag, পরের সপ্তাহে সেটি অন্য digest-কে দেখাতে পারে।
  • Dependency সরে যাওয়া: lock file ছাড়া pip install বা npm install নতুন version আনতে পারে।
  • Toolchain বদল: runner-এর compiler বা OS image হালনাগাদ হলে একই কোড অন্যভাবে তৈরি হয়।
  • Environment-ভেদে flag: --build-arg ENV=prod দিলে prod-এর কোডপথ কখনো পরীক্ষাই হয়নি।
মূল ধারণা

পরীক্ষার ফল source-এর নয়, নির্দিষ্ট বাইটের। staging-এ পাস করেছে digest A. prod-এ যদি digest B যায়, তাহলে “staging-এ পাস” কথাটি prod-এর জিনিস সম্পর্কে কিছুই বলে না। এই নীতির নাম build once, deploy many. Jez Humble আর David Farley-র Continuous Delivery (২০১০) বইয়ে এটি deployment pipeline-এর গোড়ার নিয়মগুলোর একটি হিসেবে আছে।

Reproducible build: সংক্ষেপে

Reproducible build মানে একই source, একই tool আর একই নির্দেশ থেকে প্রতিবার হুবহু একই বাইট আসা। Linux distribution-গুলোর একটি উদ্যোগ (Reproducible Builds, যার শুরু Debian-কে ঘিরে) এটিকে ছড়িয়ে দিয়েছে। মূল কৌশল হলো সময়কে স্থির করা। SOURCE_DATE_EPOCH নামের একটি প্রচলিত variable-এ একটি নির্দিষ্ট সময় দেওয়া হয়। আমাদের date-ছাড়া Dockerfile-এ এটি চালিয়ে দেখলাম:

$ docker buildx build --no-cache --progress=plain --provenance=false \
    --build-arg SOURCE_DATE_EPOCH=1700000000 \
    --output type=image,name=abcl-ap-web:r1,rewrite-timestamp=true,unpack=false \
    -f Dockerfile.nodate .
…
#9 exporting manifest sha256:6d61df3afd6d9407833dbe58a9e27d5fa7f0a2a210ffdf32a3d25ac8b5566f7a 0.0s done
#9 exporting config sha256:b43639a30d58e3a515e364037fd865024f3248cb2e593b1692ee3951bac9a38e 0.0s done
$ docker buildx build … name=abcl-ap-web:r2 … -f Dockerfile.nodate .
…
#9 exporting manifest sha256:6d61df3afd6d9407833dbe58a9e27d5fa7f0a2a210ffdf32a3d25ac8b5566f7a 0.0s done
#9 exporting config sha256:b43639a30d58e3a515e364037fd865024f3248cb2e593b1692ee3951bac9a38e
$ docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' | grep abcl-ap-web:r
abcl-ap-web:r1 6d61df3afd6d
abcl-ap-web:r2 6d61df3afd6d

দুবার --no-cache, তবু manifest আর config হুবহু এক। তিনটি জিনিস লেগেছে। SOURCE_DATE_EPOCH সময় ঠিক করে। rewrite-timestamp=true layer-এর ফাইলের সময় বদলে দেয় (এই Docker-এ unpack=false ছাড়া এটি error দিয়েছিল: conflicts with "unpack"). আর --provenance=false build-এর নথিটি বাদ দেয়।

শেষেরটি একটি চমকপ্রদ পর্যবেক্ষণ। এই Dockerfile-এর দুটি সাধারণ build, পুরোটা cache থেকে এলে, image manifest 78970e9f… দুবারই এক ছিল, কিন্তু BuildKit-এর ডিফল্ট attestation manifest (c49a9e2c… বনাম ae9b089d…) আলাদা হওয়ায় ওপরের index digest-ও আলাদা হয় (88a4c250… বনাম 4c8e7e68…). তাই দুটি digest তুলনার আগে জেনে নিন আপনি কোন স্তরের digest দেখছেন।

Reproducible build, promotion-এর বিকল্প নয়। এটি একটি যাচাইয়ের হাতিয়ার: দুজন আলাদাভাবে build করে দেখতে পারেন, CI যা বানিয়েছে তা source-এর সাথে মেলে কি না। বাস্তবে এটি পুরোপুরি পাওয়া কঠিন, কারণ base image আর সব dependency-ও digest বা lock file দিয়ে বেঁধে রাখতে হয়। তাই দুটোই করুন, কিন্তু deploy করুন পরীক্ষিত digest.

২ · Artifact: একটি জিনিস, অনেক environment

Artifact হলো build-এর ফল, যা deploy করা যায় এবং আর বদলায় না। Container image একটি artifact. একটি .jar, .whl, .deb, zip করা function package, বা build করা static website-এর ফোল্ডারও তাই। Artifact হাতে থাকলে আর source বা compiler লাগে না।

নিচের ছবিটি এই পর্বের মূল কথা। Commit থেকে একবার build হয়। Registry-তে একটিই digest থাকে। তিন environment সেই একই digest চালায়। আলাদা হয় শুধু নিচের config.

একবার build, registry, তিন environment

Build once, store once, deploy the same digest everywhere

Build once, registry, dev, staging, prod Commit c3561ea একবার build CI · build 41 · version 1.4.0 Registry: একটিই digest sha256:826745340a24… tag: dev · staging · prod · 1.4.0 dev 8267453… staging 8267453… prod 8267453… Config: dev db.dev · debug Config: staging db.stg · info Config: prod db.prod · warn Artifact এক, config আলাদা
ছবি ৫৮.১ — Digest ওপর থেকে নিচে এক থাকে। Environment-এর পার্থক্য আসে শুধু নিচ থেকে।

বিয়েবাড়ির ক্যাটারিং ভাবুন। একই হাঁড়ির বিরিয়ানি তিনটি হলে যায়। হল আলাদা, টেবিল আলাদা, অতিথির সংখ্যা আলাদা, কিন্তু রান্না আলাদা নয়। প্রতিটি হলের জন্য নতুন করে রাঁধলে স্বাদ মেলানোর দায় কেউ নিতে পারবে না। Artifact হলো হাঁড়ির বিরিয়ানি, আর config হলো হলের ব্যবস্থা।

৩ · Config থাকে বাইরে: একই digest, dev আর prod

একই artifact সব জায়গায় চলতে হলে তার ভেতরে কোনো environment-এর ঠিকানা থাকা চলবে না। The Twelve-Factor App (Heroku-র প্রকৌশলীদের লেখা, ২০১১-র দিকে প্রকাশিত) এখানে দুটি নীতি দেয়। তৃতীয় নীতি বলে, config থাকবে কোডের বাইরে, environment-এ। পঞ্চম নীতি বলে, build, release আর run তিনটি কঠোরভাবে আলাদা ধাপ। Build artifact বানায়। Release সেই artifact-এর সাথে একটি environment-এর config জোড়ে। Run সেটি চালায়।

একটি সহজ পরীক্ষা আছে: এই মুহূর্তে কি repository-টি public করে দেওয়া যেত, কোনো গোপন তথ্য বা ঠিকানা ফাঁস না করে? না গেলে config কোডে মিশে আছে।

আমাদের start.sh চালু হয়ে একটি লাইন ছাপে: VERSION ফাইল থেকে version, আর environment variable থেকে বাকিটা। Image-এর ভেতরে কোনো database-এর নাম নেই। এবার deploy-এর সময়ের config Kubernetes-এ দিই, পর্ব ৪৭-এর ConfigMap দিয়ে। dev-এর ফাইলটিও হুবহু এমন, শুধু নাম আর মান আলাদা:

apiVersion: v1
kind: ConfigMap
metadata:
  name: web-config-prod
  namespace: abcl-ap
data:
  APP_ENV: prod
  DB_HOST: db.prod.internal
  LOG_LEVEL: warn
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-prod
  namespace: abcl-ap
  labels:
    app.kubernetes.io/name: bazar-web
    app.kubernetes.io/version: 1.4.0
    environment: prod
  annotations:
    example.com/commit: c3561ea51d7924cd989f7861fc6e5271c6ce9eb4
    example.com/build: "41"
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: bazar-web
      environment: prod
  template:
    metadata:
      labels:
        app.kubernetes.io/name: bazar-web
        environment: prod
    spec:
      terminationGracePeriodSeconds: 1
      containers:
        - name: web
          image: 127.0.0.1:18581/bazar/web@sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8
          imagePullPolicy: IfNotPresent
          envFrom:
            - configMapRef:
                name: web-config-prod
          resources:
            requests: {cpu: 10m, memory: 16Mi}
            limits: {cpu: 50m, memory: 32Mi}
$ kubectl -n abcl-ap apply -f web-dev.yaml -f web-prod.yaml
configmap/web-config-dev created
deployment.apps/web-dev created
configmap/web-config-prod created
deployment.apps/web-prod created
$ kubectl -n abcl-ap logs deploy/web-dev
version=1.4.0 env=dev db=db.dev.internal log=debug
$ kubectl -n abcl-ap logs deploy/web-prod
version=1.4.0 env=prod db=db.prod.internal log=warn
$ kubectl -n abcl-ap get pods -l app.kubernetes.io/name=bazar-web \
    -o custom-columns='POD:.metadata.name,ENV:.metadata.labels.environment,IMAGE_ID:.status.containerStatuses[0].imageID'
POD                         ENV    IMAGE_ID
web-dev-67d5b96c4f-x4m5d    dev    docker-pullable://127.0.0.1:18581/bazar/web@sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8
web-prod-5fcb855b47-7fs5f   prod   docker-pullable://127.0.0.1:18581/bazar/web@sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8

দুটি log লাইনে version=1.4.0 এক, কিন্তু env, db আর log আলাদা। অথচ শেষের টেবিলে দুটি Pod-এর digest হুবহু এক। Image একবারও বদলায়নি, বদলেছে শুধু ConfigMap. status.containerStatuses[].imageID হলো node-এর জানানো আসল চলমান image, তাই এটি spec-এর লেখার চেয়ে বেশি নির্ভরযোগ্য প্রমাণ।

একক-node cluster-এর পার্থক্য: এখানে runtime cri-dockerd, তাই docker-pullable:// উপসর্গ এসেছে, আর node একই Docker daemon দিয়ে 127.0.0.1-এর registry পেয়েছে। Production-এ cluster TLS-সহ একটি নেটওয়ার্ক registry থেকে নামায়, আর containerd-তে imageID-র উপসর্গ ভিন্ন হয়।

গোপন মান ConfigMap-এ রাখা যায় না। Password আর token যাবে পর্ব ৩৩-এর secret manager বা Kubernetes Secret-এ। নিচের সিমুলেটরে একটি বাজার app-এর সেটিংসের তালিকা আছে। প্রতিটি সারিতে where বলে মানটি কোথায় রাখা। চালান, তারপর সারিগুলো ঠিক করে দেখুন কখন fails: 0 হয়।

const settings = [
  { key: 'APP_NAME',    where: 'image',  dev: 'bazar', staging: 'bazar', prod: 'bazar' },
  { key: 'WORKER_MODE', where: 'image',  dev: 'async', staging: 'async', prod: 'async' },
  { key: 'DB_HOST',     where: 'image',  dev: 'db.dev', staging: 'db.stg', prod: 'db.prod' },
  { key: 'LOG_LEVEL',   where: 'config', dev: 'debug', staging: 'info', prod: 'warn' },
  { key: 'PAGE_SIZE',   where: 'config', dev: '20', staging: '20', prod: '20' },
  { key: 'DB_PASSWORD', where: 'config', dev: 'x1', staging: 'x2', prod: 'x3' },
  { key: 'API_TOKEN',   where: 'image',  dev: 'tk', staging: 'tk', prod: 'tk' },
];
const isSecret = k => /PASSWORD|TOKEN|SECRET|PRIVATE_KEY/.test(k);
function check(s) {
  const differs = new Set([s.dev, s.staging, s.prod]).size > 1;
  if (isSecret(s.key)) {
    return s.where === 'secret' ? ['ok', 'secret store']
      : ['FAIL', `secret in ${s.where}: move to secret store`];
  }
  if (s.where === 'image' && differs) {
    return ['FAIL', 'differs per env: inject at deploy time'];
  }
  if (s.where === 'image') return ['ok', 'same everywhere, may stay in image'];
  if (!differs) return ['note', 'same everywhere, config is optional'];
  return ['ok', 'differs per env, injected'];
}
let fails = 0;
for (const s of settings) {
  const [state, why] = check(s);
  if (state === 'FAIL') fails++;
  console.log(state.padEnd(5), s.key.padEnd(12), s.where.padEnd(7), why);
}
const baked = settings.filter(s => s.where === 'image'
  && new Set([s.dev, s.staging, s.prod]).size > 1).map(s => s.key);
console.log(`
fails: ${fails}`);
console.log(`images needed for 3 environments: ${baked.length ? 3 : 1}`);
if (baked.length) console.log(`forced by: ${baked.join(', ')}`);

আসল আউটপুটের শেষ অংশ:

FAIL  DB_HOST      image   differs per env: inject at deploy time
ok    LOG_LEVEL    config  differs per env, injected
note  PAGE_SIZE    config  same everywhere, config is optional
FAIL  DB_PASSWORD  config  secret in config: move to secret store
FAIL  API_TOKEN    image   secret in image: move to secret store

fails: 3
images needed for 3 environments: 3
forced by: DB_HOST

DB_HOST image-এ আছে আর environment-ভেদে আলাদা, তাই সে একাই তিনটি image বানাতে বাধ্য করছে। এটিই rebuild per environment-এর শিকড়। API_TOKEN-এর মান তিন জায়গায় এক হলেও এটি গোপন, আর image-এর প্রতিটি layer নামিয়ে পড়া যায়। APP_NAME সব জায়গায় এক বলে image-এ থাকলে ক্ষতি নেই। সব জায়গায় একই মান থাকা সেটিং (PAGE_SIZE) config-এ রাখলেও ভুল নয়, শুধু একটি সিদ্ধান্ত। DB_HOST-কে config করে আবার চালালে শেষ লাইন হবে images needed for 3 environments: 1.

৪ · Promotion: নতুন build নয়, নতুন নাম

Environment-কে ভাবুন সিঁড়ির ধাপ হিসেবে। প্রতিটি ধাপ পার হলে artifact-এর ওপর বিশ্বাস একটু বাড়ে। প্রতিটি ধাপের কাজ আলাদা:

ধাপকাজডেটাঢোকার গেট (উদাহরণ)
devনতুন build দ্রুত চালিয়ে দেখাবানানো ডেটাUnit test, image scan
stagingProduction-এর মতো জায়গায় পুরো ব্যবস্থা মেলানোনকল বা মুখোশ-পরানো ডেটাdev-এ smoke test পাস
prodআসল ব্যবহারকারীআসল ডেটাIntegration test, অনুমোদন, freeze নেই

এগুলো একটি উদাহরণ, নিয়ম নয়। ছোট দল dev আর prod নিয়েও চলে। অনেক দল প্রতি pull request-এ একটি preview environment বানায়। Environment যত বেশি, দেরি আর খরচ তত বেশি। আর staging যত prod-এর কাছাকাছি (dev/prod parity), তার পাসের দাম তত বেশি। এই ধাপগুলোর pipeline-এ বসানোর নকশা পর্ব ৫৭-এ (CI/CD pipeline-এর নকশা) আছে।

Promotion কী করে? Artifact-এর বাইট ছোঁয় না, শুধু নতুন একটি tag বসায়। এই কাজের নাম re-tagging. আসল run-এ প্রথমে commit-এর SHA-সহ tag দিয়ে push করা হলো:

$ docker run -d --name abcl-ap-registry -p 127.0.0.1:18581:5000 \
    -e REGISTRY_STORAGE_DELETE_ENABLED=true registry:2
$ git log --oneline -1
c3561ea feat: order search for bazar app
$ docker build --label org.opencontainers.image.revision=c3561ea51d7924cd989f7861fc6e5271c6ce9eb4 \
    --label org.opencontainers.image.version=1.4.0 \
    --label org.opencontainers.image.source=https://git.example.com/bazar/web \
    --label ci.build.number=41 -t 127.0.0.1:18581/bazar/web:sha-c3561ea .
…
$ docker push 127.0.0.1:18581/bazar/web:sha-c3561ea
…
d6319746b12c: Pushed
b4a900aa5554: Pushed
sha-c3561ea: digest: sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8 size: 856

$ docker tag 127.0.0.1:18581/bazar/web:dev 127.0.0.1:18581/bazar/web:staging
$ docker push 127.0.0.1:18581/bazar/web:staging
The push refers to repository [127.0.0.1:18581/bazar/web]
4776bccc6296: Layer already exists
d6319746b12c: Layer already exists
25f1d6b1951a: Layer already exists
b4a900aa5554: Layer already exists
04d53b51086a: Already exists
staging: digest: sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8 size: 856

দ্বিতীয় push-এ একটি বাইটও নতুন করে যায়নি: সব সারিতে already exists. শেষ লাইনের digest আগের মতোই। docker tag শুধু local-এ একটি নতুন নাম লেখে, আর docker push registry-কে জানায় এই নামটিও ওই manifest-কে দেখায়। dev আর 1.4.0 tag একইভাবে বসানো হয়েছিল।

Image না নামিয়েও promotion হয়। Registry-র HTTP API-তে manifest পড়ে সেটিই নতুন tag-এ লিখে দিলেই হলো। prod tag আমরা এভাবেই বসিয়েছি। $ACC হলো OCI manifest-এর Accept header, আর $REG হলো http://127.0.0.1:18581:

$ curl -s -H "$ACC" -o manifest.json $REG/v2/bazar/web/manifests/staging
$ curl -s -o /dev/null -w 'PUT status %{http_code}\n' -X PUT \
    -H "Content-Type: application/vnd.oci.image.index.v1+json" \
    --data-binary @manifest.json $REG/v2/bazar/web/manifests/prod
PUT status 201
$ curl -s $REG/v2/bazar/web/tags/list
{"name":"bazar/web","tags":["dev","staging","1.4.0","sha-c3561ea","prod"]}
$ for T in sha-c3561ea 1.4.0 dev staging prod; do …; done     # HEAD, Docker-Content-Digest
sha-c3561ea      sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8
1.4.0            sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8
dev              sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8
staging          sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8
prod             sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8

পাঁচটি নাম, একটিই digest. একই বাইট লিখলে একই hash আসে, তাই এই পথে digest বদলানোর কোনো সুযোগ নেই। বাস্তবে এই কাজ হাতে curl-এ কেউ করেন না। crane tag, skopeo copy, docker buildx imagetools create বা cloud registry-র নিজস্ব কমান্ড একই কাজ করে। dev আর prod-এর registry আলাদা হলে দ্বিতীয় registry-তে copy করতে হয়। তখন manifest হুবহু রাখলে digest এক থাকে, কিন্তু কোনো tool format বদলালে digest বদলে যেতে পারে, তাই copy-র পরে মিলিয়ে নিন।

  1. Build 41sha-c3561eaএকবারই
  2. dev:devunit, scan
  3. staging:stagingsmoke test
  4. অনুমোদনgateমানুষের সই
  5. prod:prodআসল ব্যবহারকারী
Registry-র অবস্থা
sha-c3561ea
dev
staging
prod
নতুন বাইট push
  1. Build একবার। Commit c3561ea থেকে CI build 41 বানাল। Push-এ পাঁচটি blob গেল। Tag sha-c3561ea বলছে এটি কোন commit-এর। এর পরে আর কোথাও docker build নেই।
  2. dev. একই digest-এ dev tag বসল। নতুন বাইট শূন্য। ধরা যাক unit test আর scan পাস।
  3. staging. staging tag বসল, আসল push-এ সব সারি already exists. Digest এখনো 8267453…. এবার staging-এর config দিয়ে smoke test চলে।
  4. অনুমোদন। Registry-তে কিছু বদলায় না। গেট দেখছে পরীক্ষা হয়েছে এই digest-এর, আর একজন মানুষ সই দিলেন। (আমাদের run-এ এই অনুমোদন কাল্পনিক, বাকি সব আসল।)
  5. prod. Manifest PUT-এ prod tag বসল, status 201. চারটি নাম, একটি digest. prod-এ যা যাচ্ছে, ঠিক সেই বাইটই dev আর staging পার হয়েছে। এটিই promotion-এর পুরো দাবি।

গেট আর অনুমোদন

Gate হলো একটি শর্ত, যা পূরণ না হলে artifact পরের ধাপে যায় না। বেশিরভাগ গেট স্বয়ংক্রিয় হওয়া উচিত: test পাস, scan-এ জানা গুরুতর সমস্যা নেই, smoke test সবুজ, change freeze চলছে না। মানুষের অনুমোদন রাখুন সেখানে, যেখানে আইন, ঝুঁকি বা separation of duties সত্যিই চায়। যিনি build চালালেন, তিনিই নিজের কাজ অনুমোদন করলে গেটের অর্থ থাকে না।

নিচের সিমুলেটরে একটি artifact-এর অবস্থা built থেকে prod-এর দিকে যায়। তিনটি নিয়ম: ধাপ লাফানো যাবে না, অন্য digest ঢুকবে না, আর প্রতিটি গেট পাস করতে হবে।

const STAGES = ['built', 'dev', 'staging', 'prod'];
const artifact = { digest: 'sha256:8267', stage: 'built' };
const facts = { unitTests: true, imageScan: true, smokeDev: true,
  integration: false, loadTest: true,
  approvedBy: 'rumana', builtBy: 'ci-bot', freeze: false };
const gates = {
  dev: [['unit tests', f => f.unitTests], ['image scan', f => f.imageScan]],
  staging: [['smoke test on dev', f => f.smokeDev]],
  prod: [['integration tests', f => f.integration], ['load test', f => f.loadTest],
    ['human approval', f => !!f.approvedBy && f.approvedBy !== f.builtBy],
    ['no change freeze', f => !f.freeze]],
};
function promote(a, to, digest) {
  const need = STAGES.indexOf(a.stage) + 1;
  if (STAGES.indexOf(to) !== need)
    return `REJECT ${to}: must go to ${STAGES[need]} first`;
  if (digest !== a.digest)
    return `REJECT ${to}: digest ${digest} was not tested here`;
  const failed = gates[to].filter(([, ok]) => !ok(facts)).map(([n]) => n);
  if (failed.length) return `BLOCKED ${to}: ${failed.join(', ')}`;
  a.stage = to;
  return `OK ${to}: gates passed (${gates[to].length})`;
}
const attempts = [
  ['prod', 'sha256:8267'],    // skip two stages
  ['dev', 'sha256:8267'],
  ['staging', 'sha256:5b7b'], // a rebuilt image
  ['staging', 'sha256:8267'],
  ['prod', 'sha256:8267'],
];
for (const [to, d] of attempts) {
  console.log(promote(artifact, to, d).padEnd(52), '| stage =', artifact.stage);
}
facts.integration = true;     // the failed test is fixed and re-run
console.log(promote(artifact, 'prod', 'sha256:8267').padEnd(52), '| stage =', artifact.stage);

আসল আউটপুট:

REJECT prod: must go to dev first                    | stage = built
OK dev: gates passed (2)                             | stage = dev
REJECT staging: digest sha256:5b7b was not tested here | stage = dev
OK staging: gates passed (1)                         | stage = staging
BLOCKED prod: integration tests                      | stage = staging
OK prod: gates passed (4)                            | stage = prod

REJECT আর BLOCKED-এ অবস্থা বদলায় না, সেটিই state machine-এর মূল গুণ। তৃতীয় সারিতে rebuild করা digest ঢুকতে পারেনি। পঞ্চম সারিতে integration test ফেল করায় আটকে গেছে। নতুন build নয়, পরীক্ষার বাধা সরিয়ে একই digest শেষ সারিতে prod-এ গেল। approvedBy-তে 'ci-bot' লিখে চালিয়ে দেখুন: নিজের build নিজে অনুমোদন করা আটকে যায়।

এই গেট pipeline-এ কেমন দেখায়? নিচের নমুনাটি GitHub Actions ধরনের। লক্ষ করুন, এতে docker build নেই। environment: production-এ অনুমোদনের নিয়ম repository-র সেটিংসে বসে, আর সেই সুবিধা plan-ভেদে আলাদা হতে পারে। GitLab-এর protected environment, Azure Pipelines-এর approval আর AWS CodePipeline-এর manual approval একই ভাবনার অন্য রূপ। লেখাটি চালানো হয়নি, শুধু YAML হিসেবে যাচাই করা, আর registry login ও crane install-এর ধাপ বাদ দেওয়া।

name: release
on: workflow_dispatch
jobs:
  promote-prod:
    runs-on: ubuntu-latest
    environment: production      # approval rule lives here
    env:
      IMAGE: registry.example.com/bazar/web
      DIGEST: ${{ vars.TESTED_DIGEST }}   # sha256:... from staging
    steps:
      - name: Refuse a digest that staging did not test
        run: |
          got=$(crane digest "$IMAGE:staging")
          test "$got" = "$DIGEST"
      - name: Add the prod tag, no build here
        run: crane tag "$IMAGE@$DIGEST" prod

৫ · Version-এর তিন নাম, আর digest দিয়ে deploy

একটি build-এর কয়েকটি নাম থাকে, আর প্রত্যেকে আলাদা প্রশ্নের উত্তর দেয়:

নামআমাদের উদাহরণযে প্রশ্নের উত্তরদুর্বলতা
SemVer1.4.0ব্যবহারকারীর জন্য কতটা বদলেছে?মানুষের সিদ্ধান্ত, tag সরানো যায়
Commit SHAc3561eaঠিক কোন কোড?ক্রম বোঝা যায় না
Build number41কোনটি আগে, কোনটি পরে?একই pipeline-এর বাইরে অর্থ নেই
Digestsha256:8267453…ঠিক কোন বাইট?মানুষ পড়তে পারে না

SemVer ২.০.০ (Tom Preston-Werner-এর লেখা) MAJOR.MINOR.PATCH-এর নিয়ম দেয়। এর পুরো আলোচনা আছে Software Engineering কোর্সে। Library-র জন্য SemVer জরুরি, কারণ ব্যবহারকারী version দেখে সিদ্ধান্ত নেন। ভেতরের web service-এ অনেক দল শুধু SHA আর তারিখ রাখে, আর সেটিও ঠিক। একটি SemVer-এর + অংশে build metadata রাখা যায়, যেমন 1.4.0+41.c3561ea, তবে OCI tag-এ + চলে না, তাই tag-এ 1.4.0-41-c3561ea ধরনের লেখা হয়।

Tag মানুষের জন্য, digest যন্ত্রের জন্য। তাই deploy by digest করুন। আমাদের YAML-এ ছিল image: …/bazar/web@sha256:8267…. কেউ prod tag সরালেও এই Deployment-এর Pod আগের বাইটই নামাবে। পড়ার সুবিধার জন্য web:1.4.0@sha256:… লেখা যায়, সেক্ষেত্রে digest-ই নির্ধারক। Kustomize-এর images অংশ আর Helm-এর values দিয়ে digest বসানো হয় (পর্ব ৫০), আর পর্ব ৬০-এর GitOps-এ সেই digest Git-এ commit হয়।

prod বা staging-এর মতো চলমান tag registry-তে থাকা ভালো, কারণ মানুষ এক নজরে অবস্থা দেখতে পারে। কিন্তু Deployment-এর image:-এ এগুলো লিখবেন না। একই YAML দুই দিনে দুটি আলাদা জিনিস চালালে rollback আর তদন্ত দুটোই অনুমান হয়ে যায়। অন্যদিকে 1.4.0 বা sha-… tag-কে immutable রাখুন।

৬ · Artifact কোথায় থাকে, আর কত দিন

Artifact-এর গুদাম তিন রকম। একে একসাথে artifact store বলা হয়। কাজের ধরন দেখে বেছে নিন:

ধরনকী রাখেউদাহরণখেয়াল রাখুন
Container registryImage; OCI আকারে Helm chart-ওAmazon ECR, Azure Container Registry, Google Artifact Registry, GHCR, HarborDigest নিজে থেকেই আসে, tag immutable করা যায়
Package repositorynpm, PyPI, Maven, NuGet, deb, rpmJFrog Artifactory, Sonatype Nexus, GitHub Packages, AWS CodeArtifact, Azure Artifactsএকই version আবার ছাপানো বন্ধ রাখুন
Object storageZip, static site, function package, firmwareAmazon S3, Azure Blob Storage, Google Cloud StorageDigest নিজে লিখতে হয়, versioning চালু রাখুন

তিন জায়গায় একই নিয়ম: artifact একবার ছাপা হলে আর বদলাবে না, আর ভেতরে যাওয়ার অধিকার CI-এর হাতে, মানুষের নয়।

Retention: কী রাখবেন, কী মুছবেন

প্রতিটি commit-এ build হলে registry দ্রুত ভরে। খরচ হিসাব করি। ধরে নিই দিনে ২০টি build, প্রতিটিতে গড়ে ৮০ MB নতুন layer (বাকিটা ভাগ করা), আর মাসে GB-প্রতি $০.১০ (কল্পিত দর, provider-এর দর দেখে নিন):

৯০ দিন রাখলে: ২০ × ৮০ MB × ৯০ = ১৪৪,০০০ MB = ১৪৪ GB → মাসে $১৪.৪০
৩০ দিন রাখলে: ৪৮ GB → মাসে $৪.৮০

টাকার অঙ্ক ছোট, ঝুঁকিটি বড়। Retention-এর আসল প্রশ্ন খরচ নয়, কোন digest-এ কখনো ফিরতে হতে পারে। মোছার নিয়ম ভুল হলে rollback-এর দিন ধরা পড়ে। আমরা ইচ্ছা করে সেটি ঘটিয়ে দেখলাম। পুরোনো build 40 (1.3.9) registry থেকে digest ধরে মুছে local tag-ও সরানো হলো, তারপর dev-কে সেই digest-এ ফেরানোর চেষ্টা:

$ curl -s -o /dev/null -w 'DELETE status %{http_code}\n' -X DELETE $REG/v2/bazar/web/manifests/sha256:031f9b49…
DELETE status 202
$ kubectl -n abcl-ap set image deploy/web-dev web=127.0.0.1:18581/bazar/web@sha256:031f9b49…
deployment.apps/web-dev image updated
$ kubectl -n abcl-ap rollout status deploy/web-dev --timeout=20s
Waiting for deployment "web-dev" rollout to finish: 1 old replicas are pending termination...
error: timed out waiting for the condition
$ kubectl -n abcl-ap get pods -l environment=dev
NAME                       READY   STATUS             RESTARTS   …
web-dev-67d5b96c4f-s2zpd   1/1     Running            0          …
web-dev-7c8d78f8f4-lnmmz   0/1     ImagePullBackOff   0          …
# Pod event, trimmed:
Failed   Failed to pull image "…@sha256:031f9b49…": … failed to resolve reference … not found
$ kubectl -n abcl-ap rollout undo deploy/web-dev
deployment.apps/web-dev rolled back
$ kubectl -n abcl-ap logs deploy/web-dev
version=1.4.0 env=dev db=db.dev.internal log=debug

Registry 202 বলল, manifest শেষ। নতুন Pod ImagePullBackOff-এ আটকে গেল। পুরোনো Pod Running রইল, কারণ পর্ব ৪৪-এর rolling update নতুন Pod Ready না হলে পুরোনোটি সরায় না। এখানে সেবা বাঁচল, কিন্তু আসল জরুরি অবস্থায় যেখানে নতুন version ভাঙা, সেখানে আপনার ফেরার পথই বন্ধ। সেই দিনটিই সবচেয়ে খারাপ দিন। (registry:2-তে delete চালু করতে আলাদা setting লাগে, আর ডিস্ক খালি করতে পরে garbage collection চালাতে হয়।)

একটি নিরাপদ নিয়মের ধরন, যা প্রতিষ্ঠান নিজের প্রয়োজনে বদলাবে:

৭ · Rollback: পুরোনো digest আবার deploy করা

Promotion বুঝলে rollback সহজ। Rollback মানে পুরোনো source থেকে আবার build নয়। যে digest আগে prod-এ ভালো চলেছে, সেটিকে আবার deploy করা। পরীক্ষিত বাইট ফিরে আসে, কোনো নতুন অনিশ্চয়তা ঢোকে না। আমরা দ্বিতীয় commit b1ec8fd থেকে build 42 (1.4.1, digest e79a448…) বানিয়ে prod-এ পাঠিয়ে, আবার ফিরিয়ে আনলাম।

web-prod-এর অবস্থা
Spec-এর digest
চলমান ReplicaSet
Revision
Pod-এর log
commit annotation
  1. শুরু। prod চলছে build 41-এ। ReplicaSet 5fcb855b47, revision ১। Log বলছে version=1.4.0.
  2. নতুন release. kubectl set image দিয়ে digest e79a448… বসল। নতুন ReplicaSet 55b74d97f5 তৈরি হলো, revision ২। Log: version=1.4.1.
  3. সমস্যা ধরা পড়ল। ধরুন delivery charge ভুল আসছে (এই অংশটি কাল্পনিক)। কোথায় ফিরবেন? Release record-এ শেষ ভালো digest লেখা আছে: 8267453…. কোনো build চলছে না, কারও ল্যাপটপে কিছু খোঁজা হচ্ছে না।
  4. পুরোনো digest আবার। set image-এ পুরোনো digest দেওয়া হলো। Pod template আগের মতো হয়ে গেল, তাই controller নতুন কিছু বানাল না, পুরোনো ReplicaSet 5fcb855b47-কেই বাড়াল। Revision হলো ৩।
  5. যাচাই। Log আবার version=1.4.0, আর Pod-এর imageID 8267453…. Rollback শেষ বলার আগে spec নয়, চলমান imageID দেখুন। Annotation-ও ফেরাতে হয়েছে, নইলে নথি মিথ্যা বলত।
$ kubectl -n abcl-ap set image deploy/web-prod \
    web=127.0.0.1:18581/bazar/web@sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8
deployment.apps/web-prod image updated
$ kubectl -n abcl-ap annotate deploy/web-prod \
    kubernetes.io/change-cause="rollback to build 41, commit c3561ea" \
    example.com/commit=c3561ea51d7924cd989f7861fc6e5271c6ce9eb4 example.com/build=41 --overwrite
deployment.apps/web-prod annotated
$ kubectl -n abcl-ap rollout status deploy/web-prod --timeout=60s
…
deployment "web-prod" successfully rolled out
$ kubectl -n abcl-ap logs deploy/web-prod
version=1.4.0 env=prod db=db.prod.internal log=warn
$ kubectl -n abcl-ap rollout history deploy/web-prod
deployment.apps/web-prod
REVISION  CHANGE-CAUSE
2         build 42, commit b1ec8fd, 1.4.1
3         rollback to build 41, commit c3561ea

$ kubectl -n abcl-ap get rs -l environment=prod -o custom-columns=…
NAME                  DESIRED   READY    REVISION   IMAGE
web-prod-55b74d97f5   0         <none>   2          127.0.0.1:18581/bazar/web@sha256:e79a448b9c5f…
web-prod-5fcb855b47   1         1        3          127.0.0.1:18581/bazar/web@sha256:826745340a24…

History-তে revision ১ নেই, কারণ একই ReplicaSet revision ৩ হয়ে গেছে। Change-cause annotation-এ আমরা নিজেরা কারণ লিখেছি, Kubernetes নিজে এটি বানায় না। kubectl rollout undo একই কাজ করে, কিন্তু সে cluster-এর রাখা ReplicaSet-এর ওপর নির্ভর করে (revisionHistoryLimit, ডিফল্ট ১০)। Digest ধরে ফেরা registry-র ওপর নির্ভর করে, তাই নতুন cluster-এও কাজ করে। GitOps-এ (পর্ব ৬০) rollback হয় Git-এর digest-লেখা commit ফিরিয়ে।

Artifact ফেরানো মানে সবকিছু ফেরানো নয়। Database migration, বাইরের API-র বদল আর ইতিমধ্যে লেখা ডেটা ফেরে না। পুরোনো কোড নতুন schema-য় চলবে কি না, সেটি আগে থেকে ভাবতে হয়। ফেরা সম্ভব না হলে পথ হলো roll forward: দ্রুত একটি ঠিক করা নতুন build পাইপলাইন দিয়ে পাঠানো। ধাপে ধাপে ঝুঁকি কমানোর কৌশলগুলো পর্ব ৫৯-এ (Deployment strategy) আছে।

৮ · Traceability: Pod থেকে commit পর্যন্ত

রাত দুটায় alert এলো। প্রথম প্রশ্ন: production-এ এখন ঠিক কোন কোড চলছে? Traceability মানে এই প্রশ্নের উত্তর মিনিটে দিতে পারা। শিকলটি চার কড়ার। প্রতিটি কড়া আমরা build-এর সময়ই বসিয়েছি:

  1. Image-এর ভেতরে: OCI-র মানক label org.opencontainers.image.revision, .version আর .source.
  2. Kubernetes object-এ: label app.kubernetes.io/version (দিয়ে খোঁজা যায়), আর annotation-এ commit আর build.
  3. চলমান অবস্থায়: Pod-এর imageID, যা বানানো লেখা নয়, runtime-এর জানানো।
  4. Pipeline-এর নথিতে: কোন digest, কোন commit, কে অনুমোদন করল, কখন।

এবার শিকল ধরে পেছনে হাঁটি, আসল run-এ:

$ kubectl -n abcl-ap get pod web-prod-5fcb855b47-7fs5f \
    -o jsonpath='{.status.containerStatuses[0].imageID}{"\n"}'
docker-pullable://127.0.0.1:18581/bazar/web@sha256:826745340a24facfd74e979ee5f574d7146d167327f13b25915ecdabc5b4b7a8
$ kubectl -n abcl-ap get deploy web-prod \
    -o jsonpath='{.metadata.annotations.example\.com/commit}{" build "}{.metadata.annotations.example\.com/build}{"\n"}'
c3561ea51d7924cd989f7861fc6e5271c6ce9eb4 build 41
$ docker image inspect 127.0.0.1:18581/bazar/web:sha-c3561ea --format '{{json .Config.Labels}}'
{"ci.build.number":"41","org.opencontainers.image.revision":"c3561ea51d7924cd989f7861fc6e5271c6ce9eb4",
 "org.opencontainers.image.source":"https://git.example.com/bazar/web","org.opencontainers.image.version":"1.4.0"}
$ git log -1 --format='%h %ad %s' --date=short c3561ea
c3561ea 2026-10-29 feat: order search for bazar app

Digest থেকে label, label থেকে commit, commit থেকে বিবরণ। চারটি কমান্ড। Annotation আর image-এর label দুটি আলাদা উৎস, তাই মিলিয়ে দেখলে হাতে সরানো annotation ধরা পড়ে। (docker image inspect-এ পাঠযোগ্য করতে একটি লাইন ভাঙা হয়েছে।)

একটি নতুন release-এ কী কী গেল? Release notes এই শিকল থেকেই বেরোয়। দুই commit-এর মাঝের তালিকাই তার কাঁচামাল:

$ git log --oneline c3561ea..b1ec8fd
b1ec8fd fix: round delivery charge

ভালো release note শুধু commit-এর তালিকা নয়। তাতে থাকে version, digest, commit, build, অনুমোদনকারী, ঝুঁকি আর ফেরার digest. লেখার ধরন আছে Commit আর release পর্বে। নিচের সিমুলেটরে build record দিয়ে চলমান Pod-গুলোর পরিচয় বের করা হয়। Pod-এর সারিগুলো শিক্ষার জন্য বানানো, digest-গুলো আমাদের run-এর।

// From a running Pod's digest back to a commit. Add rows and re-run.
const builds = [   // the build record the pipeline writes after each push
  { digest: '8267453', build: 41, ver: '1.4.0', commit: 'c3561ea',
    subject: 'feat: order search for bazar app',
    tested: ['dev', 'staging', 'prod'] },
  { digest: 'e79a448', build: 42, ver: '1.4.1', commit: 'b1ec8fd',
    subject: 'fix: round delivery charge', tested: ['dev', 'staging'] },
];
const running = [  // SAMPLE of status imageID values, trimmed
  { pod: 'web-dev-67d5b', env: 'dev',  imageID: '8267453' },
  { pod: 'web-prod-5fcb', env: 'prod', imageID: '8267453' },
  { pod: 'web-prod-9ab1', env: 'prod', imageID: 'e79a448' },
  { pod: 'web-prod-77c3', env: 'prod', imageID: '5b7b193' },
];
const index = new Map(builds.map(b => [b.digest, b]));
function trace(p) {
  const b = index.get(p.imageID);
  if (!b) return `UNKNOWN  no build record: stop and investigate`;
  const line = `build ${b.build} v${b.ver} ${b.commit} "${b.subject}"`;
  return b.tested.includes(p.env) ? `OK       ${line}`
                                  : `SKIPPED  ${line}, not cleared for ${p.env}`;
}
for (const p of running) {
  console.log(`${p.pod} ${p.env.padEnd(4)} ${p.imageID}  ${trace(p)}`);
}
const prod = running.filter(p => p.env === 'prod');
const kinds = new Set(prod.map(p => p.imageID));
console.log(`\nprod pods: ${prod.length}, different digests: ${kinds.size}`);
if (kinds.size > 1) console.log('prod is mixed: a rollout is running, or something drifted');

আসল আউটপুট:

web-dev-67d5b dev  8267453  OK       build 41 v1.4.0 c3561ea "feat: order search for bazar app"
web-prod-5fcb prod 8267453  OK       build 41 v1.4.0 c3561ea "feat: order search for bazar app"
web-prod-9ab1 prod e79a448  SKIPPED  build 42 v1.4.1 b1ec8fd "fix: round delivery charge", not cleared for prod
web-prod-77c3 prod 5b7b193  UNKNOWN  no build record: stop and investigate

prod pods: 3, different digests: 3
prod is mixed: a rollout is running, or something drifted

SKIPPED মানে build চেনা, কিন্তু prod-এর ছাড়পত্র নেই: কেউ গেট পাশ কাটিয়েছেন। UNKNOWN আরও গুরুতর: এই digest আমাদের কোনো build-এর নয়। হতে পারে হাতে বানানো image, হতে পারে আরও খারাপ কিছু। ঠিক এই পরীক্ষাই admission policy দিয়ে cluster-এর দরজায় বসানো যায় (পর্ব ৪৯ আর পর্ব ৬১)।

5b7b193… digest-টি কোত্থেকে এলো? এটি আমাদের সত্যিকারের একটি rebuild: একই commit c3561ea, একই label, --no-cache. Build দুটি আর digest দুটি আসল। prod-এর গল্পটি কাল্পনিক, কিন্তু বহু দলের চেনা।

  1. staging পাস8267453…পরীক্ষিত
  2. prod jobdocker buildআবার build
  3. prod5b7b193…অপরীক্ষিত
Registryপরীক্ষিত build
staging  sha256:826745340a24…
label    revision=c3561ea…  version=1.4.0  build=41
built-at Fri Oct 30 20:33:29 UTC 2026
CI logএকই commit, নতুন runner
$ docker build --no-cache --label …revision=c3561ea… -t …:rebuilt .
#9 exporting manifest list sha256:5b7b19347a31… done
দুটি imageনাম এক, বাইট আলাদা
           tested            rebuilt
digest     826745340a24…     5b7b19347a31…
revision   c3561ea…          c3561ea…
version    1.4.0             1.4.0
built-at   20:33:29          20:38:10
Dashboardযা চোখে পড়ে
prod     version 1.4.0   commit c3561ea   সব ঠিক দেখাচ্ছে
staging  version 1.4.0   commit c3561ea   সব ঠিক দেখাচ্ছে
তদন্তDigest ধরে তুলনা
staging imageID  sha256:826745340a24…
prod    imageID  sha256:5b7b19347a31…
মিল নেই: prod-এর জিনিস কখনো পরীক্ষা হয়নি
Pipeline-এর নতুন গেটসমাধান
promote prod:
  docker build         নিষিদ্ধ
  digest == tested     বাধ্যতামূলক
  deploy               image@sha256:826745340a24…
  1. সব ঠিকঠাক। Build 41 staging-এ পাস করেছে। পরীক্ষিত digest 826745340a24….
  2. ভুল সিদ্ধান্ত। prod job-এ পুরোনো অভ্যাসে docker build রয়ে গেছে। নতুন runner, কোনো cache নেই। একই commit থেকে তৈরি হলো 5b7b19347a31….
  3. Label মিথ্যা বলে না, কিন্তু সব বলেও না। দুটির revision আর version হুবহু এক। আসল run-এ পার্থক্য ছিল built-at-এর সময়। কিন্তু fresh runner-এ base image বা dependency সরলে পার্থক্য আসল কোডেও থাকতে পারত।
  4. কেউ টের পায় না। Version আর commit দেখে সব এক লাগে। তাই dashboard সবুজ। শুধু version দেখে পরিচয় নিলে এই ভুল চোখেই পড়ে না।
  5. ঘটনার দিন। prod-এ একটি সমস্যা, staging-এ সেটি তৈরি করা যাচ্ছে না। অনেক সময় গেল। শেষে কেউ digest মেলালেন। উত্তর পরিষ্কার: prod-এর বাইট কোনো পরীক্ষার মুখ দেখেনি।
  6. স্থায়ী সমাধান। prod-এর job থেকে build সরানো, আর গেটে “digest পরীক্ষিত digest-এর সমান” শর্ত বসানো। মানুষের সতর্কতার ওপর নয়, pipeline-এর গঠনের ওপর ভরসা।

আরেক ধাপ: provenance

Label যে কেউ লিখতে পারেন। আমাদের rebuild-এ label হুবহু এক ছিল, লক্ষ করেছেন তো? “এই image সত্যিই ওই commit থেকে, ওই pipeline-এ তৈরি”, এই দাবির প্রমাণ হলো স্বাক্ষরিত provenance. BuildKit ডিফল্টে যে attestation manifest বানিয়েছে, সেটি তার একটি রূপ (পর্ব ৪১)। পাকা ব্যবস্থায় CI স্বাক্ষর করে, আর deploy-এর আগে তা যাচাই হয়। SLSA-র স্তরসহ পুরো আলোচনা পর্ব ৬১-এ (DevSecOps) পাবেন।

আরও গভীরে: Build-কে একটি বিশুদ্ধ function ভাবলে সব পরিষ্কার হয়: artifact = f(source, dependencies, toolchain, time). Reproducible build এই তালিকা থেকে time সরায় আর বাকিগুলো বেঁধে ফেলে। Nix আর Bazel-এর মতো tool এই ভাবনার ওপর দাঁড়িয়ে। Promotion উল্টো পথে এগোয়: f-কে একবারই চালায়, তারপর ফলটিকেই বহন করে। OCI distribution spec-এ manifest-এর digest তার কাঁচা বাইটের ওপর হিসাব হয়, তাই manifest হুবহু লিখলে digest বদলাতে পারে না।

পরীক্ষা ও ইন্টারভিউতে:

  • Build once, deploy many কেন? পরীক্ষার ফল নির্দিষ্ট বাইটের, rebuild করলে বাইট বদলাতে পারে।
  • Artifact আর config-এর পার্থক্য? Artifact সব environment-এ এক, config environment-ভেদে আলাদা আর deploy-এর সময় আসে।
  • Promotion-এ কী বদলায়? Tag বা নথি, digest নয়।
  • Tag না digest দিয়ে deploy? Digest, কারণ tag সরতে পারে।
  • Rollback-এর শর্ত? পুরোনো digest registry-তে আছে, আর schema বা ডেটা পুরোনো কোডের সাথে মানানসই।

৯ · AI-এর যুগে artifact আর promotion

AI খুব দ্রুত pipeline লিখে দেয়, আর সেগুলো দেখতে ঠিকঠাক লাগে। সমস্যা হলো, সে সবচেয়ে বেশি যে নমুনা দেখেছে সেটিই দেয়। আর সেই নমুনায় প্রায়ই environment-প্রতি build আর latest tag থাকে।

AI-এর ভুলকেন বিপজ্জনককী করবেন
প্রতি environment-এ docker build --build-arg ENV=…পরীক্ষিত বাইট prod-এ যায় নাএক build, deploy-এর সময় config
image: app:latest বা :prodকী চলছে জানা যায় নাYAML-এ digest
Dockerfile-এ ENV DB_PASSWORD=…Secret image-এর layer-এ থেকে যায়Secret manager, আর prompt-এ কখনো secret নয়
বানানো কমান্ড: docker retag, kubectl rollout undo --to-digestএগুলো নেই; আসল flag --to-revision--help আর অফিসিয়াল docs
“জায়গা খালি করতে” কড়া retention ruleচলমান বা rollback-এর digest মুছে যায়আগে তালিকা দেখা, in-use digest রক্ষা
Registry-র token বা অনুমতিতে *CI ভাঙলে সব image ঝুঁকিতেLeast privilege, স্বল্পমেয়াদি token (পর্ব ৬১)
সমস্যায় kubectl delete পরামর্শকারণের প্রমাণ মুছে যায়, সেবাও পড়তে পারেআগে --dry-run=server, তারপর rollout-এর পথ

AI-এর লেখা pipeline পর্যালোচনায় নিচের কাঠামোটি ব্যবহার করুন। শেষ অনুরোধটি খেয়াল করুন: তাকে দাবি নয়, প্রমাণের পথ দিতে বলা হচ্ছে।

নিচে আমার CI/CD pipeline-এর YAML. Secret সরিয়ে দিয়েছি। শুধু artifact আর promotion দেখো। [এখানে YAML পেস্ট করুন] ১. কোথায় কোথায় image build হচ্ছে? একবারের বেশি হলে লাইন নম্বরসহ দেখাও। ২. prod-এ যাওয়া image পরীক্ষিত digest-এর সাথে মেলে কি না, pipeline কোথায় যাচাই করছে? ৩. কোনো deployment-এ tag ব্যবহার হলে digest-এর পরিবর্তন দেখাও। ৪. Config বা secret কোথাও image-এ ঢুকছে কি না বলো। ৫. Rollback করতে হলে কোন digest, কোন কমান্ড, আর সেটি নিরাপদ না হলে কেন, বলো। প্রতিটি কমান্ড আর flag-এর জন্য অফিসিয়াল docs-এর নাম দাও। নিশ্চিত না হলে "নিশ্চিত নই" লেখো। কিছু মোছার কমান্ড দিও না।

AI-এর লেখা pipeline হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১“একবার build” নীতি কি সব জায়গায় মানা যায়? Frontend আর mobile app-এ কী হয়?

Backend container-এ নীতিটি সহজে মানা যায়, কারণ process চালু হওয়ার সময় environment variable পড়ে। Browser-এ চলা JavaScript-এর গল্প আলাদা। Webpack বা Vite ধরনের bundler সাধারণত VITE_API_URL-এর মতো মান build-এর সময়ই কোডে বসিয়ে দেয়। Browser-এর কোনো environment variable নেই। তাই সহজ পথ হয় dev, staging আর prod-এর জন্য তিনটি bundle বানানো, যা ঠিক আমাদের নিষিদ্ধ কাজটি।

এর বিকল্প আছে। App চালু হয়ে /config.json নামিয়ে নিতে পারে, আর সেই ফাইল প্রতিটি environment-এ আলাদা থাকে। অথবা server index.html-এ একটি ছোট window.__CONFIG__ বসিয়ে দিতে পারে, আর ConfigMap থেকে সেই ফাইল mount হতে পারে। আরেকটি পথ হলো একই origin-এর relative path, যাতে API-র ঠিকানাই লাগে না। এতে bundle একটিই থাকে, তবে প্রথম লোডে একটি বাড়তি request যোগ হয়।

Mobile app-এ সমস্যা আরও বাস্তব। Store-এ যাওয়া binary সই করা, আর release আর debug build-এ optimization, signing key ও permission আলাদা হতে পারে। অনেক দল তাই একটি release-candidate binary তৈরি করে সেটিকেই internal track থেকে beta আর production track-এ ওঠায়। Android আর iOS-এর store-গুলোর track বা TestFlight ধারণা এই promotion-ই। সার্ভারের ঠিকানা আসে দূরের config থেকে।

কিছু ক্ষেত্রে সত্যিই একাধিক artifact লাগে, যেমন CPU architecture-ভেদে। তখনও নীতিটি মরে না: একই build ধাপ থেকে amd64 আর arm64 একসাথে তৈরি হয়, একটি index-এ বাঁধা হয়, আর সেই index-এর digest-ই promote হয়।

মূল উপলব্ধি: নীতিটি “একটি ফাইল” নিয়ে নয়, “যা পরীক্ষা হয়েছে, সেটিই পাঠানো” নিয়ে। যেখানে একাধিক রূপ লাগে, সেখানে সবগুলো এক ধাপে তৈরি করে একসাথে পরীক্ষা ও promote করুন।

প্র ০২Build যদি পুরোপুরি reproducible হয়, তাহলে prod-এর জন্য আবার build করলে দোষ কী?

যুক্তিটি আকর্ষণীয়: বাইট যদি হুবহু আসে, তাহলে rebuild আর promotion একই। তত্ত্বে ঠিক, বাস্তবে তিনটি সমস্যা আছে। প্রথমত, “হুবহু আসবে” একটি দাবি, যা build শেষ হওয়ার আগে জানা যায় না। digest মিলিয়ে দেখলে সেটি promotion-এই পরিণত হয়, শুধু বাড়তি সময় আর CPU খরচ করে। দ্বিতীয়ত, reproducibility ভঙ্গুর। আমাদের ছোট উদাহরণেই তিনটি setting লেগেছে, আর একটি apt-get update বা অনির্দিষ্ট base image সব ভেঙে দিতে পারে। তৃতীয়ত, ঘটনার সময় rollback-এ আবার build চালানো মানে ধীর হওয়া, আর একটি নতুন ব্যর্থতার সুযোগ যোগ করা।

তাহলে reproducible build-এর আসল কাজ কী? যাচাই। ২০২০ সালে প্রকাশ পাওয়া SolarWinds ঘটনায় প্রকাশিত প্রতিবেদন অনুযায়ী আক্রমণকারী build ব্যবস্থার ভেতরে ঢুকে source না বদলে তৈরি হওয়া software-এ ক্ষতিকর কোড বসিয়েছিল। স্বাক্ষরও বৈধ ছিল, কারণ স্বাক্ষরের যন্ত্র তো কোম্পানিরই। এমন ক্ষেত্রে source পড়ে কিছু ধরা পড়ে না। কিন্তু একটি আলাদা, পরিষ্কার ব্যবস্থায় একই source build করে digest মেলালে অমিল ধরা পড়ার সম্ভাবনা থাকে।

Debian-এর মতো distribution এই কারণেই বড় আকারে package পুনর্নির্মাণ করে মিলিয়ে দেখে। ২০২৪ সালের xz ঘটনায় ক্ষতিকর অংশ source tarball আর build ধাপের ফাঁকে লুকানো ছিল, যা দেখায় “source” বলতে ঠিক কী বোঝায়, সেটিও প্রশ্নের বিষয়।

সুতরাং দুটি কৌশল প্রতিদ্বন্দ্বী নয়। Promotion বলে “পরীক্ষিত জিনিসটিই গেল।” Reproducible build বলে “জিনিসটি সত্যিই ওই source থেকে এসেছে।” প্রথমটি deployment-এর সততা, দ্বিতীয়টি build-এর সততা। আর provenance সেই দাবির স্বাক্ষরিত নথি, যা আপনি পর্ব ৬১-এ দেখবেন।

মূল উপলব্ধি: Reproducibility দিয়ে সন্দেহ যাচাই করুন, promotion দিয়ে জিনিস পাঠান। একটি অন্যটির জায়গা নেয় না।

প্র ০৩Staging-এ পাস করলেই কি prod নিরাপদ? কতগুলো environment আসলে দরকার?

Staging কখনো prod-এর হুবহু নকল হয় না। আসল ব্যবহারকারীর ধরন, ডেটার আকার, ট্রাফিকের চূড়া, cache-এর অবস্থা, বাইরের সেবার আচরণ, সবই আলাদা। ঈদের আগের অনলাইন বিক্রির ভিড় staging-এ আসে না। আসল ডেটা staging-এ কপি করাও ব্যক্তিগত তথ্যের ঝুঁকি, তাই মুখোশ পরিয়ে নিতে হয়। ফলে staging একটি ফিল্টার, প্রমাণ নয়।

আরেকটি ফাঁদ হলো সবার জন্য একটিই staging রাখা। একাধিক দল সেখানে পাঠালে লাইন লাগে, আর একজনের ভাঙা অন্যের পরীক্ষা নষ্ট করে। অনেক দল তাই স্বল্পজীবী preview environment-এ যায়, যা pull request-এর সাথে জন্মায় আর মরে। Infrastructure as Code এটিকে সম্ভব করেছে। খরচ তবে বাড়ে, আর সব নির্ভরতা বানানো কঠিন।

আধুনিক ভাবনা হলো কিছু পরীক্ষা prod-এই করা, কিন্তু ঝুঁকি সীমিত রেখে। Canary-তে নতুন digest অল্প ট্রাফিক পায়। Feature flag কোড ছেড়ে দেয় কিন্তু সুবিধা বন্ধ রাখে। Metric খারাপ হলে পুরোনো digest-এ ফেরা যায়। এগুলো পর্ব ৫৯-এ আসবে। লক্ষ করুন, এই সব কৌশল নির্ভর করে একটি জিনিসের ওপর: পুরোনো আর নতুন digest দুটি আলাদা, পরিচিত, আর ফেরত আনা যায়।

কতগুলো environment? কোনো জাদুর সংখ্যা নেই। প্রতিটি environment সম্পর্কে জিজ্ঞেস করুন: এটি কোন ধরনের ভুল ধরে, যা আগের ধাপ ধরে না? উত্তর না থাকলে সেটি শুধু অপেক্ষা আর খরচ বাড়ায়। Accelerate (২০১৮) বইয়ের গবেষণাও দ্রুত, ছোট আর ঘন ঘন পরিবর্তনের পক্ষে কথা বলে, লম্বা হাতবদলের পক্ষে নয়।

মূল উপলব্ধি: Environment বিশ্বাস বাড়ায় ধাপে ধাপে, কিন্তু শেষ পরীক্ষা production-এই হয়। তাই লক্ষ্য শুধু আগেই ভুল ধরা নয়, ভুল হলে দ্রুত আর ছোট পরিসরে ফিরতে পারা।

প্র ০৪মানুষের অনুমোদনের গেট কি সত্যিই নিরাপত্তা বাড়ায়?

অনুভূতিতে তো বাড়ায়: একজন সিনিয়র দেখলেন, সই দিলেন। কিন্তু দুটি প্রশ্ন আছে। তিনি কি সত্যিই ঝুঁকি বোঝার মতো তথ্য পেলেন? আর পঞ্চাশটি পরিবর্তনের তালিকায় একজন ব্যস্ত মানুষ কতটা গভীরে যেতে পারেন?

Accelerate (Forsgren, Humble, Kim; ২০১৮) বইয়ের গবেষণা জানায়, বাইরের change approval board-এর অনুমোদন দ্রুততার সাথে নেতিবাচকভাবে জড়িত, অথচ change failure rate কমানোর সাথে তার সম্পর্ক পাওয়া যায়নি। এটি survey-ভিত্তিক সম্পর্কের ফল, সব প্রতিষ্ঠানের জন্য আইন নয়। তবু বার্তাটি স্পষ্ট: দূরের কমিটির সই নিরাপত্তার নিশ্চয়তা নয়। Peer review, স্বয়ংক্রিয় পরীক্ষা আর ছোট পরিবর্তন সেই কাজ ভালো করে।

দেরিরও একটি নিরাপত্তা-মূল্য আছে। অনুমোদন সপ্তাহে একবার বসলে পরিবর্তন জমে বড় ব্যাচ হয়, আর বড় ব্যাচ ভাঙলে কারণ খোঁজা কঠিন। উল্টো দিকে ব্যাংক, স্বাস্থ্যসেবা বা তালিকাভুক্ত প্রতিষ্ঠানে নিরীক্ষার কারণে separation of duties লাগতে পারে। সেখানে সমাধান গেট তোলা নয়, গেটকে হালকা আর প্রমাণ-নির্ভর করা। Pipeline নিজেই প্রমাণ জমায়: কে কোড লিখল, কে review করল, কোন digest, কোন test. অনুমোদনকারী সেই প্রমাণ দেখেন, প্রতিটি ফাইল নয়।

আরেকটি ব্যবস্থা হলো ঝুঁকিভিত্তিক গেট। Config-এর ছোট বদল স্বয়ংক্রিয়, database schema বা payment-এর বদল মানুষের হাতে। আর জরুরি অবস্থার জন্য একটি লিপিবদ্ধ পথ রাখুন, যাতে পরে পর্যালোচনা হয়। না থাকলে মানুষ গেট পাশ কাটানোর গোপন পথ বানায়।

মূল উপলব্ধি: গেটের মূল্য সইয়ে নয়, সে কোন ভুল আটকায় তাতে। যা যন্ত্র যাচাই করতে পারে যন্ত্রকে দিন, আর মানুষকে দিন সেই সিদ্ধান্ত, যার জন্য বিচারবুদ্ধি লাগে।

প্র ০৫“Production-এ এখন কী চলছে?” এই সহজ প্রশ্নটির উত্তর না জানলে কী হতে পারে?

সবচেয়ে বিখ্যাত উদাহরণগুলোর একটি Knight Capital, ২০১২ সাল। মার্কিন নিয়ন্ত্রক SEC-এর প্রকাশিত নথি অনুযায়ী, নতুন trading কোড আটটি server-এ হাতে পাঠানোর সময় একটি server বাদ পড়ে। সেই server-এ বহু বছরের পুরোনো একটি অব্যবহৃত অংশ রয়ে গিয়েছিল। নতুন কোড একটি পুরোনো flag-এর অর্থ নতুনভাবে ব্যবহার করায় পুরোনো অংশটি জেগে ওঠে। প্রায় ৪৫ মিনিটে লক্ষ লক্ষ ভুল order বাজারে যায়, আর ক্ষতি ৪৬ কোটি মার্কিন ডলার ছাড়িয়ে যায়।

এখান থেকে কী শেখার আছে? ঘটনার মূলে ছিল সব server-এ একই জিনিস চলছে কি না, তার স্বয়ংক্রিয় যাচাই না থাকা। এটি artifact আর traceability-র সমস্যা। একই digest যদি সব জায়গায় ঠিকভাবে পৌঁছাত, আর একটি পরীক্ষা বলত “৮ server-এর মধ্যে ৭টিতে এই digest, ১টিতে অন্য”, তাহলে অমিলটি আগেই চোখে পড়ার সুযোগ ছিল। আমাদের trace_lookup.js-এর শেষ লাইনটি ঠিক এই পরীক্ষা।

সব ব্যর্থতা এত নাটকীয় নয়। দৈনন্দিন খরচটি নীরব। একটি log4j ধরনের জরুরি নিরাপত্তা-খবর এলে প্রশ্ন ওঠে: কোন কোন সেবায় এই library আছে? Digest থেকে SBOM-এ যাওয়ার পথ থাকলে উত্তর কয়েক মিনিটে আসে, না থাকলে দিন লাগে। Audit-এর সময়ও একই প্রশ্ন: গত মার্চে কোন version চলছিল, কে পাঠিয়েছিল?

Immutable infrastructure এখানে সাহায্য করে (পর্ব ৫৬)। Server-এ কেউ হাতে বদল করে না, পুরো জিনিস বদলায়। তাই চলমান অবস্থা নথির সাথে মেলে। আর চলমান Pod-এর imageID নথি নয়, প্রত্যক্ষ পর্যবেক্ষণ। Pipeline যা লিখেছে আর cluster যা বলছে, দুটি মিলিয়ে দেখাই drift ধরার উপায়।

মূল উপলব্ধি: যা মাপা যায় না, তা নিয়ন্ত্রণ করা যায় না। Production-এর অবস্থা অনুমানের বিষয় নয়, একটি কমান্ডে পাওয়া তথ্য হওয়া উচিত।

অনুশীলন

Hands-on অংশগুলো ঐচ্ছিক। শুধু নিজের কম্পিউটারে local registry দিয়ে করুন। Cloud registry ব্যবহার করলে খরচ হতে পারে, আর কাজ শেষে নিজের বানানো container, image আর volume মুছে ফেলুন।

  1. Retention-এর হিসাব: একটি দল দিনে ১২টি build করে, প্রতিটিতে গড়ে ১৫০ MB নতুন layer. সব build ৬০ দিন রাখা হয়। ধরে নিন GB-প্রতি মাসে $০.১০ (কল্পিত দর)। (ক) স্থিতিশীল অবস্থায় কত GB থাকবে? (খ) মাসিক খরচ কত? (গ) শুধু dev build ১৪ দিনে মুছলে, আর দিনে ২টি prod-যোগ্য build ৬০ দিন রাখলে, কত GB থাকবে?

    (ক) ১২ × ১৫০ MB × ৬০ = ১,০৮,০০০ MB = ১০৮ GB. (খ) ১০৮ × $০.১০ = $১০.৮০. (গ) prod-যোগ্য: ২ × ১৫০ × ৬০ = ১৮,০০০ MB = ১৮ GB. dev: ১০টি × ১৫০ × ১৪ = ২১,০০০ MB = ২১ GB. মোট ৩৯ GB, খরচ $৩.৯০. খরচ কমল, কিন্তু আসল সিদ্ধান্ত হলো prod-যোগ্য digest-এর সময়সীমা rollback আর audit-এর প্রয়োজন মেটায় কি না।

  2. পথ খুঁজুন: একটি namespace shop-এ Deployment api চলছে। Pod-এর চলমান digest থেকে commit পর্যন্ত যেতে কোন তিনটি কমান্ড চালাবেন? নিজের কম্পিউটারে cluster থাকলে কমান্ডগুলো নিজের কোনো Pod-এ চালিয়ে দেখতে পারেন।

    ১. kubectl -n shop get pods -l app=api -o custom-columns='POD:.metadata.name,ID:.status.containerStatuses[0].imageID', চলমান digest পেতে। ২. docker pull repo@sha256:…, তারপর docker image inspect repo@sha256:… --format '{{json .Config.Labels}}', org.opencontainers.image.revision পেতে। ৩. git log -1 <sha>. Annotation-এর commit-ও মিলিয়ে নিন। প্রথম কমান্ডের একাধিক digest এলে হয় rollout চলছে, নয় কিছু একটা অমিল।

  3. সমালোচনা করুন: একটি pipeline-এ আছে: build-staging (--build-arg API_URL=staging…), deploy-staging, build-prod (--build-arg API_URL=prod…, tag latest), deploy-prod (image: app:latest). অন্তত চারটি সমস্যা বলুন আর নতুন নকশা দিন।

    সমস্যা: (১) দুবার build, তাই prod-এর বাইট অপরীক্ষিত। (২) API_URL image-এ ঢুকেছে, config আর artifact মিশে গেছে। (৩) latest tag কোন build বোঝায় না, আর Kubernetes-এ এই tag rollback-কে অনিশ্চিত করে। (৪) কোনো গেট নেই, staging ফেল করলেও prod job চলতে পারে। (৫) নথি নেই, commit-এ ফেরার পথ নেই।

    নতুন নকশা: build একবার, tag sha-<commit>, OCI label-সহ। Job-এর output digest. deploy-staging সেই digest আর staging-এর ConfigMap দিয়ে। Smoke test আর গেট। promote-এ crane tag দিয়ে prod tag, অনুমোদনসহ। deploy-prod একই digest আর prod-এর ConfigMap দিয়ে। শেষে release record লেখা।

  4. ফল অনুমান করুন: একটি registry-তে নিচের ঘটনাগুলো ক্রমে ঘটল। Build A-র digest aaa, build B-র bbb. (১) A push হলো tag dev-এ। (২) dev থেকে staging tag বসল। (৩) Deployment X তৈরি হলো image: web@sha256:aaa দিয়ে, Deployment Y তৈরি হলো image: web:staging দিয়ে। (৪) B push হলো tag dev-এ। (৫) dev থেকে staging tag বসল। (৬) X আর Y-এর একটি করে Pod মুছে ফেলা হলো, node-এ image আগে থেকেই আছে, imagePullPolicy: IfNotPresent. তিনটি প্রশ্ন: dev, staging কাকে দেখায়? X-এর নতুন Pod কী চালায়? Y-এর?

    dev আর staging দুটিই bbb-কে দেখায়। X-এর নতুন Pod চালায় aaa, কারণ digest সরে না। Y-এর উত্তরটি চালাকির: node-এ web:staging নামে পুরোনো aaa আগে থেকেই আছে, IfNotPresent আর pull করে না, তাই নতুন Pod সম্ভবত aaa-ই চালাবে, যদিও registry-র tag bbb বলছে। অন্য node-এ, যেখানে image নেই, সেখানে Pod bbb চালাবে। একই YAML, দুই node-এ দুই রকম। এজন্যই YAML-এ চলমান tag লেখা বিপজ্জনক।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি পরিবর্তন commit থেকে production পর্যন্ত যাওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✍️ Commit merge হলো
  2. 🏗️ একবার build, label-সহ, SHA tag দিয়ে registry-তে push
  3. 🧪 সেই digest dev-এ deploy, test আর scan
  4. 🔬 একই digest-এ staging tag, staging-এর config দিয়ে deploy
  5. ✅ গেট আর অনুমোদন
  6. 🚀 একই digest prod-এ, prod-এর config দিয়ে

একই commit থেকে docker build --no-cache দুবার চালিয়ে দুটি আলাদা image digest পাওয়া গেল। এর সবচেয়ে সম্ভাব্য অর্থ কী?

  • Docker-এ কোনো bug আছে
  • দুটি image-এর বাইট আলাদা, যেমন timestamp-এর কারণে, যদিও source এক
  • Commit-এর source নিশ্চয়ই বদলে গেছে
  • Tag আলাদা ছিল, তাই digest আলাদা

একটি image-কে staging থেকে prod-এ promote করতে সঠিক কাজ কোনটি?

  • prod-এর settings দিয়ে আবার build করা
  • একই commit নতুন runner-এ build করে prod tag দেওয়া
  • একই digest-এ prod tag বসানো, যাতে বাইট না বদলায়
  • staging tag মুছে ফেলে পরের build-এর অপেক্ষা করা

Twelve-Factor App-এর দৃষ্টিতে নিচের কোনটি config, যা artifact-এর বাইরে থাকা উচিত?

  • প্রতিটি environment-এর database-এর ঠিকানা
  • মূল্য হিসাবের function
  • App-এর ব্যবহৃত library-র কোড
  • App-এর URL route-এর তালিকা

Deployment-এ image: web:prod না লিখে image: web@sha256:… লেখার প্রধান সুবিধা কী?

  • Image-এর আকার ছোট হয়
  • Registry থেকে আর কখনো pull করতে হয় না
  • Image-এর নিরাপত্তা ত্রুটি আপনাআপনি ধরা পড়ে
  • Tag সরলেও Pod ঠিক একই বাইট চালায়

নতুন release-এ সমস্যা। শেষ ভালো digest registry থেকে retention rule মুছে দিয়েছে, আর node-এ সেটি নেই। সেই digest-এ kubectl set image দিলে সাধারণত কী হবে?

  • Kubernetes পুরোনো source থেকে আবার build করবে
  • নতুন Pod ImagePullBackOff-এ আটকাবে, আগের Pod rolling update-এ থেকে যাবে
  • সব Pod সাথে সাথে মুছে সেবা পড়ে যাবে
  • সবচেয়ে কাছের version-টি আপনাআপনি চলবে

চলমান Pod-এ আসলে কোন image চলছে, তার সবচেয়ে নির্ভরযোগ্য প্রমাণ কোনটি?

  • Deployment-এর annotation
  • Deployment-এর image: লাইনের tag
  • Pod-এর status.containerStatuses[].imageID
  • Git-এর সর্বশেষ commit

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. পরীক্ষার ফল নির্দিষ্ট digest-এর। Rebuild মানে নতুন, অপরীক্ষিত বাইট।
  2. Artifact এক, config আলাদা: environment-ভেদে যা বদলায়, তা deploy-এর সময় আসে; secret আসে secret manager থেকে।
  3. Promotion মানে একই digest-এ নতুন tag আর গেট পার হওয়া, নতুন build নয়।
  4. YAML-এ digest লিখুন; rollback মানে পুরোনো digest আবার deploy, তাই retention-এ সেটি রক্ষা করুন।
  5. Traceability: imageID → label → commit. Label-এর সততার প্রমাণ স্বাক্ষরিত provenance.

পরের পর্বে: Artifact তৈরি, পরীক্ষিত আর প্রস্তুত। কিন্তু নতুন digest-কে চলমান ব্যবহারকারীদের সামনে কীভাবে আনবেন? পর্ব ৫৯-এ দেখব rolling, blue-green, canary আর feature flag.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান