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

ক্যাপস্টোন — code থেকে production: container, Kubernetes, Terraform, pipeline আর observability

Capstone — From Code to Production with Containers, Kubernetes, Terraform, a Pipeline and Observability

Taught in Bangla, this hands-on capstone lesson takes a fictional bookshop API from requirements and SLOs to a multi-stage Dockerfile, a pipeline with SHA-tagged images, and Terraform on a local Kubernetes cluster. It runs two bad releases, burn-rate alerts, a chaos check, an incident with postmortem, estimates cloud cost at assumed prices, and ends with cleanup and an honest readiness score, all with real output.

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

  • আগের ৭৫টি পর্বের টুকরো দক্ষতা জুড়ে একটি পূর্ণ delivery পথ বানানো
  • Requirement থেকে SLO, আর SLO থেকে error budget ও alert-এর সংখ্যা বের করা
  • একটি commit-কে পরীক্ষিত image বানিয়ে Terraform দিয়ে Kubernetes-এ ছাড়া
  • খারাপ release চেনা, rollback করা, incident নোট আর postmortem লেখা
  • একই নকশার মাসিক খরচ হিসাব করা, আর AWS, Azure ও Google Cloud-এ সাজানো
  • নিজের প্রজেক্ট কতটা তৈরি, সেটি একটি readiness তালিকায় সৎভাবে মাপা

এই পর্বের transcript সম্পর্কে: নিচের সব আউটপুট একটি আসল run থেকে নেওয়া: Docker Engine ২৯ (BuildKit), Terraform ১.১৭ (kubernetes provider ৩.৪), এক node-এর local cluster (Kubernetes ১.৩৪) আর Prometheus ৩.৫. লম্বা লাইন ছাঁটা হয়েছে, বাদ দেওয়া অংশে … আছে। SHA, digest, নাম, IP, সময় আর আকার আপনার run-এ আলাদা হবে। কোনো cloud account ব্যবহার হয়নি: GitHub Actions-এর YAML, cloud-এর খরচ আর service-এর মানচিত্র না-চালানো নকশা, আর প্রথম সপ্তাহের on-call কাল্পনিক; সেগুলোতে সে কথা লেখা আছে।

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

আমাদের কাল্পনিক গ্রাহক ঢাকার একটি বইয়ের দোকান, নাম বইঘর। তাদের চাই ছোট একটি API. নিচের অংশগুলোর নম্বর আর প্রজেক্টের ধাপের নম্বর একই, তাই “ধাপ ৫” মানে অংশ ৫.

নিজে করে দেখতে চাইলে: লাগবে Docker, একটি local Kubernetes cluster আর Terraform; cloud account লাগবে না। ল্যাপটপে অন্তত ৮ GB memory থাকলে সুবিধা হয়। ফাইল সাজানোর ধরন নিচে। আমাদের cluster আর Docker একই মেশিনের engine ভাগ করে, তাই Pod 127.0.0.1-এর registry থেকে image পেয়েছে। kind বা minikube-এ registry-র ঠিকানা আলাদা হতে পারে, তাদের docs দেখে নেবেন। Port-গুলো সব 127.0.0.1-এ বাঁধা, বাইরের কেউ পৌঁছাতে পারে না। পুরো কাজ এক বসায় শেষ করতে না পারলেও ক্ষতি নেই, তবে উঠে যাওয়ার আগে ধাপ ১০-এর cleanup চালাবেন।

boighor/
  app.py  test_app.py  Dockerfile  pipeline.sh
infra/
  main.tf
monitoring/
  prometheus.yaml  loadgen.yaml

১ · Requirement আর SLO — কী বানাব, কতটা ভালো হলে যথেষ্ট

প্রথম কাজটিতে কোনো code নেই। Docker খোলার আগে লিখে ফেলা দরকার কী বানাচ্ছি আর “ভালো চলছে” বলতে কী বুঝি। না হলে পরের সব সিদ্ধান্ত হবে অনুমান।

বিষয়সিদ্ধান্তকোন পর্বের দক্ষতা
কাজ/ দোকান ও বইয়ের তথ্য, /healthz স্বাস্থ্য, /metrics মাপপর্ব ৬৩
ভাষাPython-এর standard library, বাইরের কোনো package নয়Python কোর্স
অবস্থাStateless: database নেই, তাই backup-ও নেইপর্ব ২৪
ট্রাফিকধরে নিই গড়ে ২০ request/সেকেন্ডপর্ব ৬৮
Deployদিনে কয়েকবার, ব্যবহারকারী টের পাবেন নাপর্ব ৫৯
নিরাপত্তাRoot নয়, লেখার অনুমতিহীন filesystem, ন্যূনতম অনুমতিপর্ব ৪৯

SLO থেকে সংখ্যা

আমাদের SLI: GET /-এর যত request এলো, তার মধ্যে কতগুলো 5xx ছাড়া উত্তর পেল। SLO: ৩০ দিনে ৯৯.৯%. হিসাবটি পর্ব ৬৬-এর:

প্রশ্নহিসাবফল
মাসে মোট request20 × 86,400 × 30৫,১৮,৪০,০০০ (৫.১৮৪ কোটি)
Error budget51,840,000 × 0.001৫১,৮৪০টি ব্যর্থ request
পুরো বন্ধ থাকলে কত সময়43,200 মিনিট × 0.001৪৩.২ মিনিট
দ্রুত alert-এর সীমা14.4 × 0.001error ratio ১.৪৪%

১৪.৪ সংখ্যাটি জাদু নয়: এই burn rate-এ ১ ঘণ্টায় ৩০ দিনের budget-এর ২% খরচ হয় (14.4 × 1 ÷ 720 = 0.02). Google-এর SRE Workbook এই মানটি সূচনা হিসেবে দেখিয়েছে।

পুরো প্রজেক্টের মানচিত্র নিচে। একটি commit বাঁ দিকের ল্যাপটপ থেকে যাত্রা শুরু করে, আর alert-এর শিক্ষা আবার ল্যাপটপেই ফেরে।

বইঘরের পুরো পথ

From a commit to running Pods, and the feedback loop back

Commit থেকে Pod পর্যন্ত পথ আর feedback loop ল্যাপটপ · git commit Pipeline (ধাপ ৩) Registry Terraform (ধাপ ৪) Kubernetes (ধাপ ৫) Prometheus (ধাপ ৬) Alert → on-call (ধাপ ৮) app.py · Dockerfile · test test → build → smoke → push ভুল হলে এখানেই থামে image:<commit-SHA> image_tag বদলানো, plan, apply Deployment · ৩টি Pod · Service readiness · HPA · ConfigMap /metrics · burn-rate rule rollback · incident নোট postmortem · নতুন test শিক্ষা ফিরে আসে
ছবি ৭৬.১ — প্রতিটি বাক্স আগের কোনো মডিউল। এই পর্বের কাজ বাক্সগুলোর মাঝের তিরগুলো।

২ · App আর Dockerfile — multi-stage, non-root

App-টি ছোট, মাত্র একটি ফাইল। তিনটি অংশ চোখে পড়ার মতো, আর তিনটিই পরে আমাদের বাঁচাবে:

VERSION = os.environ.get("BUILD_SHA", "dev")      # which commit am I?
hits = {("/", "200"): 0, ("/", "500"): 0}         # series exist from the first scrape

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/healthz":
            return self.send(503 if draining else 200,
                             "draining\n" if draining else "ok\n", "text/plain")
        if self.path == "/metrics":
            return self.send(200, metrics(), "text/plain; version=0.0.4")
        ...

def on_term(signum, frame):
    global draining
    draining = True          # /healthz now says 503: stop sending me traffic
    threading.Thread(target=drain, args=(srv,), daemon=True).start()
  • BUILD_SHA: প্রতিটি উত্তরে version থাকে। তাই rollout-এর সময় বাইরে থেকেই দেখা যায় কে উত্তর দিচ্ছে।
  • শূন্য দিয়ে শুরু করা counter: ৫০০-এর series প্রথম থেকেই ০ হিসেবে থাকে। না হলে প্রথম error-এর আগে অনুপাতের হিসাবই খালি আসত।
  • SIGTERM পেয়ে তিন সেকেন্ড কাজ: এটিই graceful shutdown. পর্ব ৬৮-এর আসল run-এ এটি ছাড়া Pod মোছায় request ভেঙেছিল।

Dockerfile-এ দুটি stage. মূল কৌশল শেষের দিকের COPY --from=test: চূড়ান্ত image তার ফাইলটি test stage থেকে নেয়। তাই test না চললে BuildKit চূড়ান্ত image বানাতেই পারে না।

# Stage 1: run the tests. If they fail, nothing after this can be built.
FROM python:3.12-alpine AS test
WORKDIR /src
COPY app.py test_app.py ./
RUN python -m unittest -v

# Stage 2: the image we ship. It takes app.py FROM the test stage.
FROM python:3.12-alpine AS runtime
ARG BUILD_SHA=dev
ENV BUILD_SHA=$BUILD_SHA PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY --from=test /src/app.py ./app.py
USER 10001:10001
EXPOSE 8000
CMD ["python", "app.py"]

যাচাই না করলে “non-root” শুধু একটি দাবি। তাই image চালিয়ে দেখা হলো:

$ docker run --rm --hostname demo --entrypoint sh $IMAGE -c 'echo uid=$(id -u); ls -la /app'
uid=10001
total 12
…
-rwxr-xr-x    1 root     root          3424 Oct 31 10:04 app.py

User 10001, ফোল্ডারে শুধু app.py: test_app.py চূড়ান্ত image-এ নেই। এই multi-stage build-এ আকার এই run-এ ৭৪.১ MB, আর pull হওয়া অংশ ১৮ MB-র মতো, কারণ layer সংকুচিত থাকে। সংখ্যাগুলোর বিস্তারিত পর্ব ৩৮-এ।

৩ · Pipeline — test, build, SHA tag, push

হোটেলের রান্নাঘর ভাবুন: পরিবেশনের আগে একই খাবার একই কয়েকটি হাত ঘুরে আসে, কারও মেজাজের ওপর নির্ভর করে না। CI pipeline-ও তাই। আমরা আগে একটি স্থানীয় script লিখি, যা hosted CI-এর stage-গুলোই ল্যাপটপে চালায়। Registry হিসেবে চলছে registry:2 container, শুধু 127.0.0.1:18761-এ।

#!/usr/bin/env bash
# A local pipeline: the same stages a hosted CI would run on every push.
set -euo pipefail
REGISTRY=127.0.0.1:18761
IMAGE=abcl-cap-boighor

[ -z "$(git status --porcelain)" ] || { echo "STOP: uncommitted changes"; exit 1; }
SHA=$(git rev-parse --short=12 HEAD)
REF="$REGISTRY/$IMAGE:$SHA"

echo "== 1/4 test   commit $SHA"
docker build --quiet --target test --output type=cacheonly .
echo "unit tests passed"

echo "== 2/4 build  $REF"
docker build --quiet --build-arg BUILD_SHA="$SHA" -t "$REF" . >/dev/null
docker image ls "$REF" --format 'size {{.Size}}'

echo "== 3/4 smoke  run the image, then ask it for /healthz and /"
docker run -d --name abcl-cap-smoke --hostname demo \
  -p 127.0.0.1:18763:8000 "$REF" >/dev/null
trap 'docker rm -fv abcl-cap-smoke >/dev/null' EXIT
for _ in 1 2 3 4 5 6 7 8 9 10; do
  curl -fsS http://127.0.0.1:18763/healthz >/dev/null 2>&1 && break
  sleep 0.5
done
curl -fsS http://127.0.0.1:18763/healthz
curl -fsS http://127.0.0.1:18763/

echo "== 4/4 push   only a tested image reaches the registry"
docker push "$REF" | tail -n 1
echo "ready to deploy: image_tag=$SHA"
$ bash pipeline.sh
== 1/4 test   commit bc5cc517cd8c
unit tests passed
== 2/4 build  127.0.0.1:18761/abcl-cap-boighor:bc5cc517cd8c
size 74.1MB
== 3/4 smoke  run the image, then ask it for /healthz and /
ok
{"shop": "boighor", "version": "bc5cc517cd8c", "greeting": "Welcome to Boighor", "book": "Padma Nadir Majhi"}
== 4/4 push   only a tested image reaches the registry
bc5cc517cd8c: digest: sha256:94502e31e84e33ef892cf7c8eba368e535014527734a79749d6d8b2d47f0e260 size: 856
ready to deploy: image_tag=bc5cc517cd8c

Tag হলো commit SHA, তাই একটি image দেখেই বলা যায় কোন code থেকে এসেছে। শেষ লাইনটিই পরের ধাপের ইনপুট। latest ব্যবহার করলে এই সম্পর্ক হারিয়ে যেত (পর্ব ৪১)। আসল production-এ deploy-এর সময় image digest ধরলে আরও শক্ত নিশ্চয়তা পাওয়া যায়, কারণ tag সরানো যায়, digest যায় না।

Hosted CI-তে একই কাজ (চালানো হয়নি)

নিচের GitHub Actions workflow একটি নকশা, কোনো repository-তে চালানো হয়নি, শুধু YAML হিসেবে যাচাই করা। Action-এর version, অনুমতি আর registry-র নাম আপনার অবস্থা অনুযায়ী বদলাবে। Stage-গুলো স্থানীয় script-এরই।

name: boighor-ci
on:
  push:
    branches: [main]
permissions:
  contents: read
  packages: write
jobs:
  deliver:
    runs-on: ubuntu-24.04
    env:
      IMAGE: ghcr.io/example-org/boighor
    steps:
      - uses: actions/checkout@v4
      - name: Test
        run: docker build --target test --output type=cacheonly .
      - name: Build
        run: docker build --build-arg BUILD_SHA=${{ github.sha }} -t "$IMAGE:${{ github.sha }}" .
      - name: Smoke
        run: |
          docker run -d --name smoke -p 127.0.0.1:8000:8000 "$IMAGE:${{ github.sha }}"
          for i in 1 2 3 4 5 6 7 8 9 10; do curl -fsS http://127.0.0.1:8000/healthz && break; sleep 1; done
          curl -fsS http://127.0.0.1:8000/
      - name: Log in to the registry
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
      - name: Push
        run: docker push "$IMAGE:${{ github.sha }}"

Cloud-এ deploy করতে গেলে দীর্ঘস্থায়ী key না রেখে OIDC দিয়ে অল্প সময়ের পরিচয় নেওয়াই ভালো (পর্ব ৬১)। Action-গুলো tag-এর বদলে পূর্ণ commit SHA-তে বাঁধলে তৃতীয় পক্ষের বদলে যাওয়া code-এর ঝুঁকিও কমে।

৪ · Terraform — namespace থেকে HPA পর্যন্ত এক ফাইলে

kubectl apply দিয়েও কাজ চলত। কিন্তু আমরা চাই পুরো পরিবেশটি একটি ফাইল থেকে আবার বানানো যাক। তাই পর্ব ৫২-এর Terraform, cloud provider-এর বদলে hashicorp/kubernetes provider দিয়ে। পাঁচটি resource: namespace, ConfigMap, Deployment, Service আর HPA. সবচেয়ে গুরুত্বপূর্ণ দুটি নিচে, হুবহু, terraform validate আর fmt -check পার করা।

variable "image_tag" {
  type        = string
  description = "Commit SHA that the pipeline pushed. Never 'latest'."
}

locals {
  ns     = "abcl-cap"
  labels = { app = "boighor" }
  image  = "127.0.0.1:18761/abcl-cap-boighor:${var.image_tag}"
}
resource "kubernetes_deployment_v1" "shop" {
  metadata {
    name      = "boighor"
    namespace = kubernetes_namespace_v1.shop.metadata[0].name
    labels    = local.labels
  }
  spec {
    replicas                  = var.replicas
    min_ready_seconds         = 3
    progress_deadline_seconds = 45
    selector {
      match_labels = local.labels
    }
    strategy {
      type = "RollingUpdate"
      rolling_update {
        max_surge       = "1"
        max_unavailable = "0"
      }
    }
    template {
      metadata {
        labels = local.labels
      }
      spec {
        termination_grace_period_seconds = 10
        security_context {
          run_as_non_root = true
          seccomp_profile {
            type = "RuntimeDefault"
          }
        }
        container {
          name  = "boighor"
          image = local.image
          port {
            name           = "http"
            container_port = 8000
          }
          env_from {
            config_map_ref {
              name = kubernetes_config_map_v1.shop.metadata[0].name
            }
          }
          readiness_probe {
            http_get {
              path = "/healthz"
              port = "http"
            }
            period_seconds    = 2
            failure_threshold = 2
          }
          liveness_probe {
            http_get {
              path = "/healthz"
              port = "http"
            }
            period_seconds    = 10
            failure_threshold = 3
          }
          resources {
            requests = { cpu = "20m", memory = "32Mi" }
            limits   = { cpu = "200m", memory = "64Mi" }
          }
          security_context {
            allow_privilege_escalation = false
            read_only_root_filesystem  = true
            capabilities {
              drop = ["ALL"]
            }
          }
        }
      }
    }
  }
  lifecycle {
    ignore_changes = [spec[0].replicas] # the HPA owns this number
  }
}

max_unavailable = "0" মানে নতুন Pod ready না হলে পুরোনোটি নামবে না। progress_deadline_seconds = 45 ঠিক করে কতক্ষণ অগ্রগতি না দেখলে rollout “ব্যর্থ” ধরা হবে। ignore_changes দরকার, নইলে HPA Pod বাড়ালে পরের apply সেটি ফিরিয়ে দিত। এখানে একই /healthz দুই probe-এ, যা ছোট service-এ চলে। বড় service-এ readiness আর liveness আলাদা endpoint পায় (পর্ব ৪৩)।

$ terraform plan -var image_tag=bc5cc517cd8c -out=v1.plan
…
Plan: 5 to add, 0 to change, 0 to destroy.

$ terraform apply v1.plan
kubernetes_namespace_v1.shop: Creating...
kubernetes_namespace_v1.shop: Creation complete after 0s [id=abcl-cap]
…
kubernetes_config_map_v1.shop: Creation complete after 0s [id=abcl-cap/boighor-config]
kubernetes_service_v1.shop: Creation complete after 0s [id=abcl-cap/boighor]
kubernetes_deployment_v1.shop: Creation complete after 8s [id=abcl-cap/boighor]
…
kubernetes_horizontal_pod_autoscaler_v2.shop: Creation complete after 0s [id=abcl-cap/boighor]

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

$ kubectl -n abcl-cap get deploy,rs,pod,svc,hpa,cm    # অংশবিশেষ
NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/boighor   3/3     3            3           8s

NAME                          READY   STATUS    RESTARTS   AGE
pod/boighor-9dc6dbc5d-2rrpz   1/1     Running   0          8s
pod/boighor-9dc6dbc5d-fvqfz   1/1     Running   0          8s
pod/boighor-9dc6dbc5d-xl7r9   1/1     Running   0          8s

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/boighor   ClusterIP   10.105.172.42   <none>        8000/TCP   8s

NAME                                          REFERENCE            TARGETS              MINPODS   MAXPODS   …
horizontalpodautoscaler.autoscaling/boighor   Deployment/boighor   cpu: <unknown>/60%   3         5         …

আট সেকেন্ডে সব তৈরি। Deploy-এর আগে local image মুছে রাখা হয়েছিল, আর kubelet-এর event বলেছে Successfully pulled image: অর্থাৎ Pod সত্যিই আমাদের registry থেকে এসেছে। একটি জিনিস লক্ষ করুন: HPA-র target <unknown>. এই cluster-এ এখন metrics-server নেই, তাই HPA CPU মাপতে পারছে না, ৩টিতেই থেমে আছে। Resource তৈরি হওয়া মানেই কাজ করা নয়। এটি আমাদের readiness তালিকায় বাকি কাজ হিসেবে উঠবে।

এখানে state আছে local ফাইলে, শুধু শেখার জন্য। দলে কাজ করলে remote state আর lock লাগবে (পর্ব ৫৩), কারণ state-এ গোপন মানও থাকতে পারে, আর দুজন একসাথে apply করলে ক্ষতি হয়।

৫ · Rolling deploy, readiness আর প্রথম খারাপ release

দ্বিতীয় commit-এ home page-এ দাম যোগ হলো। এবার পুরো শিকলটি একসাথে দেখুন। এই সময় cluster-এ একটি ছোট Pod প্রতি সেকেন্ডে ২০টি request পাঠাচ্ছিল, যাতে “ব্যবহারকারী টের পেলেন কি না” মাপা যায়।

  1. ল্যাপটপgit commit
  2. Pipelinepipeline.sh
  3. Registrypush
  4. Terraformapply
  5. Clusterrollout
আসল run · commit b538f57 · ২০ request/সেকেন্ড
এখন যা ঘটছে
পুরোনো Pod (bc5cc51)
নতুন Pod (b538f57)
ব্যর্থ request
  1. Commit. show the price on the home page, SHA b538f571b047. কিছুই এখনো বদলায়নি, cluster-এ পুরোনো ৩টি Pod চলছে।
  2. Test. Docker-এর ভেতরে unit test চলল ও পাস করল। ব্যর্থ হলে শিকল এখানেই থামত।
  3. Build. Tag সরাসরি commit SHA, আকার এই run-এ ৭৪.১ MB. BuildKit test stage-এর cache নিল, তাই test আবার চলেনি।
  4. Smoke test. নতুন image একটি container-এ চালিয়ে /healthz আর / ডাকা হলো। উত্তরে এলো "price_bdt": 300.
  5. Push. Registry দিল digest sha256:ea6523be…. এখন image একটি স্থায়ী artifact, আবার build লাগবে না।
  6. Plan আর apply. Plan-এ শুধু একটি লাইন বদল: image, পুরোনো SHA থেকে নতুন SHA. Plan: 0 to add, 1 to change, 0 to destroy.
  7. প্রথম নতুন Pod. maxSurge=1 তাই একটি বাড়তি Pod এলো। সে 0/1: readiness পাস না করা পর্যন্ত Service তাকে ট্রাফিক দেবে না। পুরোনো ৩টি কাজ চালাচ্ছে।
  8. এক এক করে বদল। নতুনটি ready হওয়ার পরে একটি পুরোনো Pod থামছে। SIGTERM পেয়ে সে ৩ সেকেন্ড কাজ চালিয়ে বিদায় নেয়। তারপর পরের নতুন Pod.
  9. শেষ। Terraform বলল Modifications complete after 25s. এই সময়ের আশপাশের ৬০৪টি request-এ দুই version-ই উত্তর দিয়েছে, ব্যর্থ ০টি।
$ kubectl -n abcl-cap get pod -l app=boighor    # rollout-এর মাঝখানে
boighor-9dc6dbc5d-2rrpz   1/1   Terminating         0     74s
boighor-9dc6dbc5d-fvqfz   1/1   Running             0     74s
boighor-9dc6dbc5d-xl7r9   1/1   Running             0     74s
boighor-9fdb47867-gjddn   1/1   Running             0     5s
boighor-9fdb47867-xhhrh   0/1   ContainerCreating   0     0s

$ kubectl -n abcl-cap logs loadgen
…
t=  64s ok=604 http5xx=0 dropped=0 b538f57:418 bc5cc51:186

খারাপ release ১: pipeline সবুজ, cluster আটকে দিল

তৃতীয় commit-এ দোকানের ক্যাটালগ /tmp-তে cache করা হলো। Cache লেখা না গেলে /healthz বলে 503. Pipeline পুরো সবুজ, কারণ ল্যাপটপের container-এ /tmp লেখা যায়। কিন্তু cluster-এ আমরাই read-only root filesystem দিয়েছি। এটি ক্লাসিক “আমার মেশিনে তো চলে” বাগ।

$ terraform apply -auto-approve -var image_tag=1002160772e5
kubernetes_deployment_v1.shop: Modifying... [id=abcl-cap/boighor]
…
Error: Deployment exceeded its progress deadline

  with kubernetes_deployment_v1.shop,
  on main.tf line 49, in resource "kubernetes_deployment_v1" "shop":
…
$ kubectl -n abcl-cap get pod -l app=boighor
NAME                       READY   STATUS    RESTARTS      AGE
boighor-845c76dc89-hgpn6   0/1     Running   1 (21s ago)   55s
boighor-9fdb47867-7x9zp    1/1     Running   0             105s
boighor-9fdb47867-pgn9z    1/1     Running   0             116s
boighor-9fdb47867-vkmjp    1/1     Running   0             110s

$ kubectl -n abcl-cap logs boighor-845c76dc89-hgpn6
catalog cache failed: [Errno 30] Read-only file system: '/tmp/boighor-catalog.json'
boighor 1002160772e5 listening on :8000

$ kubectl -n abcl-cap logs loadgen --tail=2
t= 215s ok=100 http5xx=0 dropped=0 b538f57:100
t= 220s ok=100 http5xx=0 dropped=0 b538f57:100

৫৬ সেকেন্ড পরে Terraform exit 1 দিল। নতুন Pod ready হয়নি, তাই max_unavailable = 0 পুরোনো তিনটিকে সরায়নি। ট্রাফিকে একটিও ভুল নেই: ব্যবহারকারীরা কিছুই টের পাননি। RESTARTS 1-ও লক্ষ করুন: একই /healthz liveness-এও আছে, তাই kubelet অকারণে Pod restart করেছে। এই কারণেই বড় service-এ দুটি probe আলাদা হয়।

এবার rollback. আমরা শুধু আগের SHA দিয়ে আবার apply করলাম:

$ terraform apply -auto-approve -var image_tag=b538f571b047
kubernetes_deployment_v1.shop: Modifying... [id=abcl-cap/boighor]
kubernetes_deployment_v1.shop: Modifications complete after 1s [id=abcl-cap/boighor]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

১ সেকেন্ড, কারণ পুরোনো Pod-গুলো সরেইনি। তাড়াহুড়োয় অনেকে kubectl rollout undo চালান। সেটিও আমরা চালিয়ে দেখেছি: image বদলে গেল, কিন্তু Terraform তা জানে না। পরের plan-এ এই drift ধরা পড়ল, আর ~ image লাইন আবার নতুন SHA-তে ফিরিয়ে দিতে চাইল। তাই নিয়ম: জরুরি মুহূর্তে undo চলতে পারে, তবে পরে অবশ্যই variable বা Git-এ একই সিদ্ধান্ত লিখে ফেলতে হবে।

পথগতিSource of truthকখন
terraform apply আগের SHA-তেPlan আর apply লাগেঠিক থাকেস্বাভাবিক পথ
kubectl rollout undoএক লাইনেDrift তৈরি হয়জরুরি মুহূর্তে, পরে state মেলাতে হবে
GitOps revertController মেলানো পর্যন্তGit-এ স্পষ্ট ইতিহাসপর্ব ৬০

৬ · Prometheus আর burn-rate rule — চোখ বসানো

এতক্ষণ আমরা ট্রাফিকের log পড়ে ভালো-মন্দ বুঝেছি। আসল service-এ এটি করবে Prometheus. সে একই namespace-এ Deployment হিসেবে চলছে, নিজের ServiceAccount নিয়ে, আর RBAC Role-এ অনুমতি শুধু এই namespace-এর Pod পড়ার। Least privilege এখানেও।

Prometheus কোন Pod থেকে পড়বে, সেটি হাতে লেখা হয়নি। Pod বদলালে IP বদলায়, তাই সে Kubernetes API থেকেই তালিকা নেয়। প্রতি ৫ সেকেন্ডে সে প্রতিটি Pod-এর /metrics scrape করে। আসল সিস্টেমে ১৫ থেকে ৩০ সেকেন্ড বেশি প্রচলিত; এখানে ফল দ্রুত দেখার জন্য ছোট রাখা। লেবেল দিয়ে শুধু app=boighor Pod রাখা হলো, ফলে Prometheus নিজেকে বা loadgen-কে পড়ে না।

global:
  scrape_interval: 5s
  evaluation_interval: 5s
rule_files:
  - /etc/prometheus/slo.rules.yml
scrape_configs:
  - job_name: boighor
    kubernetes_sd_configs:
      - role: pod
        namespaces:
          names: [abcl-cap]
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        regex: boighor
        action: keep
      - source_labels: [__meta_kubernetes_pod_name]
        target_label: pod

এবার মূল rule. ধাপ ১-এর সংখ্যাগুলোই এখানে code হয়ে বসেছে:

groups:
  - name: boighor-slo
    rules:
      - record: boighor:error_ratio:rate5m
        expr: |
          sum(rate(boighor_http_requests_total{path="/",code=~"5.."}[5m]))
          / sum(rate(boighor_http_requests_total{path="/"}[5m]))
      - record: boighor:error_ratio:rate1h
        expr: |
          sum(rate(boighor_http_requests_total{path="/",code=~"5.."}[1h]))
          / sum(rate(boighor_http_requests_total{path="/"}[1h]))
      - alert: BoighorFastBurn
        expr: |
          boighor:error_ratio:rate5m > (14.4 * 0.001)
          and boighor:error_ratio:rate1h > (14.4 * 0.001)
        for: 1m
        labels:
          severity: page
        annotations:
          summary: "Boighor is burning its 30-day error budget 14.4x too fast"

দুটি recording rule ভারী হিসাব আগেই করে রাখে। Alert rule দুই জানালা একসাথে দেখে: এটিই multi-window alert. ১ ঘণ্টার জানালা নিশ্চিত করে সমস্যাটি ছোট ঝাঁকুনি নয়, ৫ মিনিটের জানালা নিশ্চিত করে সমস্যাটি এখনো চলছে। promtool check config বলল SUCCESS: 4 rules found (এখানে দেখানো নেই এমন একটি ১ মিনিটের recording rule সহ)।

$ kubectl -n abcl-cap exec $POD -- python -c "import urllib.request as u; print(u.urlopen('http://127.0.0.1:8000/metrics').read().decode())"
boighor_build_info{version="a12de41eaff0"} 1
boighor_http_requests_total{path="/",code="200"} 168
boighor_http_requests_total{path="/",code="500"} 0
boighor_request_seconds_bucket{le="0.005"} 167
…
boighor_request_seconds_count 168

$ python q.py 'sum(rate(boighor_http_requests_total{path="/"}[1m]))'
19.3984808456819
$ python q.py 'boighor:error_ratio:rate5m'
0
$ python q.py --alerts
no alerts

q.py আমাদের ছোট script, যা port-forward করা Prometheus-এর HTTP API ডাকে। ওপরের /metrics নমুনাটি পরের, সংশোধিত version থেকে নেওয়া; বাকি তিনটি chaos পরীক্ষার ঠিক আগে। প্রতি সেকেন্ডে প্রায় ১৯.৪ request মাপা গেল, পাঠানো হচ্ছিল ২০: Pod বদলের কাছাকাছি সময়ে এক মিনিটের জানালায় এমন সামান্য কম আসতে পারে। Error ratio ০, alert নেই। স্বাভাবিক অবস্থা (steady state) এখন সংখ্যায় লেখা। এখানে Alertmanager নেই, তাই alert শুধু Prometheus-এই দেখা যায়, কারও ফোনে যায় না। এটিও বাকি কাজের তালিকায় যাবে।

৭ · Chaos check — একটি Pod মুছে দেখা

“তিনটি replica আছে, তাই একটি মরলে কিছু হবে না” এখনো একটি বিশ্বাস। পর্ব ৬৮-এর মতো এটি পরীক্ষায় বদলাই। ভূমিকম্পের মহড়ায় যেমন আগে ঠিক করা থাকে কে কোথায় যাবেন, এখানেও আগেই লেখা হলো:

  • Hypothesis: একটি Pod মুছলে ৫ সেকেন্ডের প্রতিটি জানালায় ব্যর্থ বা হারানো request ০ থাকবে।
  • Blast radius: শুধু আমাদের namespace-এর একটি Pod.
  • থামার শর্ত: http5xx বা dropped ১%-এর ওপরে গেলে পরীক্ষা বন্ধ।
$ kubectl -n abcl-cap delete pod boighor-9fdb47867-7x9zp
pod "boighor-9fdb47867-7x9zp" deleted from abcl-cap namespace

$ kubectl -n abcl-cap logs loadgen --tail=3
t= 265s ok=100 http5xx=0 dropped=0 b538f57:100
t= 270s ok=100 http5xx=0 dropped=0 b538f57:100
t= 275s ok=100 http5xx=0 dropped=0 b538f57:100

$ kubectl -n abcl-cap delete pod boighor-9fdb47867-c42j6 --grace-period=0 --force
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. …
pod "boighor-9fdb47867-c42j6" force deleted from abcl-cap namespace

$ kubectl -n abcl-cap logs loadgen --tail=2
t= 290s ok=100 http5xx=0 dropped=0 b538f57:100
t= 295s ok=100 http5xx=0 dropped=0 b538f57:100

$ kubectl -n abcl-cap get pod -l app=boighor
NAME                      READY   STATUS    RESTARTS   AGE
boighor-9fdb47867-pgn9z   1/1     Running   0          3m10s
boighor-9fdb47867-q7bjm   1/1     Running   0          20s
boighor-9fdb47867-vkmjp   1/1     Running   0          3m4s

দুবারই hypothesis টিকল, ReplicaSet নিজে নতুন Pod আনল, alert বাজেনি। তবে সীমাটি স্পষ্ট থাকুক: এটি ২০ request/সেকেন্ডের হালকা ট্রাফিক, এক node, মাত্র দুটি পরীক্ষা। Node বা zone হারানো এখানে মাপা সম্ভব নয়, সেখানে PodDisruptionBudget আর একাধিক node লাগবে। একটি পাস মানে “এই পরীক্ষায় ভুল পাওয়া যায়নি”, “সিস্টেম অটুট” নয়।

৮ · Incident — যে release readiness পার হয়ে গেল

এবার সত্যিকারের পরীক্ষা। খারাপ commit-টি আগেই git revert করা হয়েছে। নতুন ফিচার: প্রতিটি বইয়ের গড় rating. নতুন বই “Aguner Parashmani”-র এখনো কোনো rating নেই। Code-এ আছে:

"rating": round(sum(book["ratings"]) / len(book["ratings"]), 1)

ফাঁকা তালিকায় শূন্য দিয়ে ভাগ। কিন্তু test দেখে শুধু প্রথম বই, smoke test-ও প্রথম বই পায়, আর /healthz এই code ছোঁয়ই না। Pipeline সবুজ, rollout সবুজ, ৩/৩ Pod ready. বইগুলো পালা করে আসে, তাই প্রতি চারটি request-এ একটি ভাঙে।

আসল run · SLO ৯৯.৯% · সীমা ১৪.৪ · f9134a4 · সময় ল্যাপটপের
সময়
Pod-এর অবস্থা
১ মিনিটের burn
৫ মিনিটের burn
১ ঘণ্টার burn
Alert
  1. Apply শুরু। নতুন SHA f9134a436006 Terraform-এ গেল। Burn তখনো ০. Pipeline-এর চারটি ধাপই পাস।
  2. Rollout “সফল”। ২৫ সেকেন্ডে Terraform-এর Apply complete. সব Pod ready, অথচ এক মিনিটের burn ২৮.১ ছাড়িয়েছে। Readiness সবুজ, ব্যবহারকারী লাল।
  3. এক জানালা পার। ৫ মিনিটের burn ১৫.৭, সীমা ১৪.৪-এর ওপরে। ১ ঘণ্টার burn ৯.৯, তাই alert এখনো চুপ। ছোট ঝাঁকুনিতে page না করাই এই নকশার উদ্দেশ্য।
  4. দুই জানালা পার। দুটিই সীমার ওপরে (২৮.১ আর ১৭.৫)। for: 1m আছে, তাই alert pending, এখনো কাউকে ডাকেনি। ১ ঘণ্টার burn এত দ্রুত উঠেছে, কারণ এই ল্যাবে ট্রাফিক চলছিল মাত্র কয়েক মিনিট, জানালায় পুরো এক ঘণ্টার ডেটা ছিল না। পুরো ডেটা থাকলে ২৫% error-এ সীমা পেরোতে প্রায় সাড়ে তিন মিনিট লাগত।
  5. Firing. Apply-র ১ মিনিট ৪৬ সেকেন্ড পরে alert বাজল। এক মিনিটের burn ২৫০: error ২৫% ÷ budget ০.১%. পাতার হিসাব আর মাপ মিলে গেছে।
  6. Rollback শুরু। এই ল্যাবে কোনো মানুষ ঘুমাচ্ছিলেন না, তাই on-call-এর সাড়া দেওয়ার সময় ধরে নিয়ে ৬০ সেকেন্ড অপেক্ষা করে Terraform-এ আগের SHA দেওয়া হলো।
  7. ক্ষতি থামল। ২৬ সেকেন্ডে পুরোনো version ফিরল। এবার rollback আগের মতো ১ সেকেন্ডে হয়নি, কারণ পুরোনো Pod আগেই নেমে গিয়েছিল, নতুন করে উঠতে হলো। মোট ব্যর্থ ৮১১টি request.
  8. Alert থামল। ক্ষতি থামার সাড়ে চার মিনিট পরে ৫ মিনিটের burn ১২.৪-এ নামতেই alert থামল। ১ ঘণ্টার burn তখনো ৪৭.৩. এই দেরি স্বাভাবিক: জানালা ধীরে খালি হয়।
$ kubectl -n abcl-cap logs loadgen    # rollout শেষে, প্রতি ৫ সেকেন্ড
t= 355s ok=75 http5xx=25 dropped=0 f9134a4:75
t= 360s ok=76 http5xx=24 dropped=0 f9134a4:76

$ for i in 1 2 3 4; do curl -s -o - -w ' [%{http_code}]\n' http://127.0.0.1:18764/ | tr -d '\n'; echo; done
{"shop": "boighor", …, "book": "Padma Nadir Majhi", "price_bdt": 300, "rating": 4.7} [200]
{"shop": "boighor", …, "book": "Lalsalu", "price_bdt": 220, "rating": 5.0} [200]
{"shop": "boighor", …, "book": "Hajar Bachhar Dhore", "price_bdt": 180, "rating": 4.0} [200]
{"error": "ZeroDivisionError"} [500]

$ docker logs abcl-cap-dbg
boighor f9134a436006 listening on :8000

একই image আলাদা container-এ চালিয়ে চারবার ডাকতেই ভুলটি ধরা পড়ল। আর docker logs দেখুন: কোনো error লেখা নেই! Request log বন্ধ রাখায় app কারণটি কোথাও জানায়নি। Alert বলেছে “ভাঙছে”, “কেন” বলেনি। এটি postmortem-এ সরাসরি যাবে।

Incident নোট (severity ২)

সময়ঘটনা
১৬:১৪:১৮f9134a4 deploy শুরু; প্রভাবও শুরু
১৬:১৫:০৪BoighorFastBurn pending
১৬:১৬:০৪Alert firing (পর্যবেক্ষণ ১০ সেকেন্ড পরপর)
১৬:১৭:০৪Rollback সিদ্ধান্ত: আগের SHA-তে Terraform apply
১৬:১৭:৩০Rollback শেষ; প্রভাব বন্ধ
১৬:২২:০৪Alert থামল

প্রভাব: প্রায় ৩ মিনিট ১২ সেকেন্ড; ৮১১টি request-এ 500, কোনো request হারায়নি (dropped ০)। Prometheus-এর increase() দিল ৮১৭.৭, কারণ সে সীমার বাইরে অনুমান করে বাড়ায়; গ্রাহকের দিকের গণনা ৮১১-ই আসল সংখ্যা। MTTD ১ মিনিট ৪৬ সেকেন্ড, প্রশমনের সময় ৩ মিনিট ১২ সেকেন্ড। মাসিক budget-এর ১.৫৬% খরচ।

সংক্ষিপ্ত postmortem

পর্ব ৬৭-এর নিয়মে এটি blameless: প্রশ্নটি “কে ভুল করল” নয়, “সিস্টেম কেন ভুলটিকে এতদূর আসতে দিল”।

  • ভালো যা হলো: burn-rate alert ঠিক সময়ে বাজল; একটি SHA বদলেই পথ ফিরল; image আবার বানাতে হলো না।
  • খারাপ যা হলো: rollout “সফল” বলা সত্ত্বেও ২৫% request ভাঙছিল; deploy নিজে থেকে থামেনি; log-এ কারণ ছিল না।
  • ভাগ্য: বইটি তালিকার প্রথমে নয়, চতুর্থে। প্রথমে থাকলে test-ই ধরত, আর এই পর্ব ছোট হতো।
Action itemধরনঅবস্থা
Test প্রতিটি বই ঘুরে দেখবেপ্রতিরোধহয়েছে
ফাঁকা rating-এ null ফেরানোসমাধানহয়েছে
প্রতিটি 5xx-এর কারণ log-এ লেখাধরাবাকি
Canary: নতুন version-এ অল্প ট্রাফিক, burn দেখে এগোনোপ্রতিরোধবাকি
Smoke test-এ --read-only আর একাধিক / requestপ্রতিরোধবাকি

প্রথম কাজের প্রমাণ দেখুন। সংশোধনের আগেই নতুন test commit করে pipeline চালানো হলো, যাতে নিশ্চিত হওয়া যায় test সত্যিই ভুলটি ধরে। ঠিক এই পর্যায়ে এসে ভুলটি আর registry-তে পৌঁছাতে পারল না:

$ bash pipeline.sh; echo pipeline_exit=$?
== 1/4 test   commit c60cc618efdf
…
test_home_works_for_every_book (test_app.HomeTest.test_home_works_for_every_book) ... ERROR
…
ZeroDivisionError: division by zero
…
FAILED (errors=1)
…
ERROR: failed to build: failed to solve: process "/bin/sh -c python -m unittest -v" did not complete …
pipeline_exit=1

দুটি ভিন্ন আসল run জোড়া: --quiet দিয়ে script-এ test-এর বিস্তারিত লুকানো থাকে, তাই test-এর লাইনগুলো একই commit-এ --progress=plain দিয়ে নেওয়া। সংশোধনের commit a12de41 এরপর চারটি ধাপ পার হলো, rollout-এ ২৫ সেকেন্ডে ০টি ব্যর্থ request. এটি fix forward নয়: আগে rollback করে ক্ষতি থামানো, তারপর ধীরে-সুস্থে সংশোধন।

Budget-এর হিসাব রাখা

Error budget policy অর্থবহ হয় যদি কেউ হিসাব রাখেন। নিচের tracker-এ আসল ঘটনাগুলো বসানো। events-এ নিজের ঘটনা যোগ করুন, বা SLO আর REQ_PER_SEC বদলে আবার চালান।

// Request-budget tracker for the Boighor SLO. Change the numbers and run again.
const SLO = 0.999, REQ_PER_SEC = 20, DAYS = 30;
const monthly = REQ_PER_SEC * 86400 * DAYS;
const budget = Math.round(monthly * (1 - SLO));
console.log(`requests in ${DAYS} days: ${monthly.toLocaleString("en-US")}`);
console.log(`failed requests allowed: ${budget.toLocaleString("en-US")}`);

// Events of the month: [name, failed requests]. The first three are from the real run.
const events = [
  ["v2 rolling update", 0],
  ["bad image stopped by readiness", 0],
  ["rating bug, 3 minutes", 811],
  ["(your event here)", 0],
];
let used = 0;
for (const [name, failed] of events) {
  used += failed;
  const left = 100 * (budget - used) / budget;
  console.log(name.padEnd(32), String(failed).padStart(6), `${left.toFixed(2)}% left`.padStart(13));
}

// How fast would a given error ratio finish the budget?
function burn(errorRatio) {
  const rate = errorRatio / (1 - SLO);
  const hours = DAYS * 24 / rate;
  return { rate, hours };
}
for (const e of [0.002, 0.0144, 0.25, 1]) {
  const { rate, hours } = burn(e);
  console.log(`error ${(e * 100).toFixed(2).padStart(6)}%  burn ${rate.toFixed(1).padStart(6)}x  budget gone in ${hours.toFixed(1).padStart(6)} h`);
}
// If nobody had rolled back: the same bug through an 8-hour night.
const night = REQ_PER_SEC * 8 * 3600 * 0.25;
console.log(`8 h at 25% errors: ${night} failed, ${(100 * night / budget).toFixed(0)}% of the budget`);

আউটপুট:

requests in 30 days: 51,840,000
failed requests allowed: 51,840
v2 rolling update                     0  100.00% left
bad image stopped by readiness        0  100.00% left
rating bug, 3 minutes               811   98.44% left
(your event here)                     0   98.44% left
error   0.20%  burn    2.0x  budget gone in  360.0 h
error   1.44%  burn   14.4x  budget gone in   50.0 h
error  25.00%  burn  250.0x  budget gone in    2.9 h
error 100.00%  burn 1000.0x  budget gone in    0.7 h
8 h at 25% errors: 144000 failed, 278% of the budget

৮১১ শুনতে বড়, কিন্তু budget-এর মাত্র ১.৫৬%. আসল শিক্ষা শেষ লাইনে: alert না থাকলে রাতের ৮ ঘণ্টায় ১,৪৪,০০০টি request ভাঙত, যা পুরো মাসের budget-এর ২৭৮%. Alert-এর দাম এখানেই। 0.7 h হলো পরিচিত ৪৩.২ মিনিট।

প্রথম সপ্তাহের on-call

একটি incident দিয়ে on-call বোঝা যায় না। নিচের সপ্তাহটি কাল্পনিক, তবে শুরুর সংখ্যা আসল। লক্ষ করুন, সব কাজ রাত জাগা নয়: বেশিরভাগই alert পরিষ্কার করা, runbook লেখা আর হস্তান্তর।

Budget ৫১,৮৪০ · শুরুতে ৮১১ খরচ · ঘটনাগুলো কাল্পনিক
দিন
ঘটনা
এই সপ্তাহের page
মোট খরচ (request)
Budget বাকি
  1. হস্তান্তর। আগের on-call-এর নোট পড়া, alert-এর পথ পরীক্ষা, rollback-এর কমান্ড নিজে একবার চালিয়ে দেখা। প্রথম দিনেই কাগজে-কলমে নয়, হাতে যাচাই।
  2. শান্ত deploy. a12de41-এর পরের ছোট release, ব্যর্থ ০. On-call মানে শুধু আগুন নেভানো নয়, এমন দিনই বেশি হওয়া উচিত।
  3. রাত ২টার page. কাল্পনিক ধীর error, ১,২০০টি ব্যর্থ request. Runbook দেখে rollback, সকালে তদন্ত। মোট ৮১১ + ১,২০০ = ২,০১১.
  4. Alert মেরামত। রাতের page-এ runbook-এর লিংক ছিল না, যোগ হলো। এটিই toil কমানোর কাজ। নতুন ক্ষতি ০.
  5. Pod OOM. ছাড়ের দিনে একটি Pod OOMKilled, ৪৩০টি ব্যর্থ request. মোট ২,৪৪১. Memory limit বাড়ানোর আগে মেট্রিক দেখে কারণ খোঁজা হলো।
  6. শান্ত দিন। কিছুই ঘটেনি। এই সময়েই গত দুই ঘটনার সংক্ষিপ্ত নোট লেখা।
  7. হস্তান্তর। ২টি page, budget ৯৫.২৯% বাকি। পরের জনকে লিখে দেওয়া: খোলা কাজ কী, কোন alert সন্দেহজনক। Policy-র কোনো সীমা স্পর্শ করেনি, তাই feature-এর কাজ চলবে।

৯ · Cloud-এ চালালে খরচ কত, আর কোন service কার জায়গায়

এই ধাপ চালানো হয়নি, শুধু হিসাব। সব দাম ধরে নেওয়া গোল সংখ্যা, কোনো provider-এর বর্তমান দাম নয়; আসল সিদ্ধান্তের আগে provider-এর pricing page দেখবেন। অঞ্চল, ডলারের হার আর ব্যবহারের ধরনে হিসাব অনেকটা বদলায় (পর্ব ৭২)।

// Monthly cost model of the Boighor design. ALL PRICES ARE ASSUMED ROUND NUMBERS.
// They are not any provider's real price: check the provider's pricing page.
const HOURS = 720;                       // 30-day month
const P = { controlPlane: 0.10, node: 0.04, loadBalancer: 0.025, nat: 0.05,
            egressGB: 0.10, natGB: 0.05, logGB: 0.50, registryGB: 0.10 };
const USE = { egressGB: 200, natGB: 20, logGB: 15, registryGB: 5, millionReq: 51.84 };

function design(name, nodes, nodePrice, hasControlPlane, hasNat) {
  const items = {
    "control plane": hasControlPlane ? P.controlPlane * HOURS : 0,
    "worker nodes": nodes * nodePrice * HOURS,
    "load balancer": P.loadBalancer * HOURS,
    "NAT gateway": hasNat ? P.nat * HOURS + P.natGB * USE.natGB : 0,
    "internet egress": P.egressGB * USE.egressGB,
    "logs and metrics": P.logGB * USE.logGB,
    "image registry": P.registryGB * USE.registryGB,
  };
  const total = Object.values(items).reduce((a, b) => a + b, 0);
  return { name, items, total };
}
const designs = [
  design("A managed K8s, 3 nodes", 3, P.node, true, true),
  design("B same, 1-yr commitment -30%", 3, P.node * 0.7, true, true),
  design("C two VMs, no K8s, no NAT", 2, P.node, false, false),
];
for (const d of designs) {
  console.log(d.name);
  for (const [k, v] of Object.entries(d.items))
    if (v > 0) console.log("  " + k.padEnd(18), ("$" + v.toFixed(2)).padStart(9));
  console.log("  " + "TOTAL / month".padEnd(18), ("$" + d.total.toFixed(2)).padStart(9),
    `  $${(d.total / USE.millionReq).toFixed(2)} per million requests`);
}

আউটপুট:

A managed K8s, 3 nodes
  control plane         $72.00
  worker nodes          $86.40
  load balancer         $18.00
  NAT gateway           $37.00
  internet egress       $20.00
  logs and metrics       $7.50
  image registry         $0.50
  TOTAL / month        $241.40   $4.66 per million requests
B same, 1-yr commitment -30%
  control plane         $72.00
  worker nodes          $60.48
  load balancer         $18.00
  NAT gateway           $37.00
  internet egress       $20.00
  logs and metrics       $7.50
  image registry         $0.50
  TOTAL / month        $215.48   $4.16 per million requests
C two VMs, no K8s, no NAT
  worker nodes          $57.60
  load balancer         $18.00
  internet egress       $20.00
  logs and metrics       $7.50
  image registry         $0.50
  TOTAL / month        $103.60   $2.00 per million requests

তিনটি পর্যবেক্ষণ। এক, “সার্ভার” (worker node) বিলের মাত্র ৩৬%: বাকিটা control plane, load balancer আর NAT gateway-এর মতো চলতে থাকা খরচ। দুই, ৩০% ছাড়ের commitment মোট বিল কমায় মাত্র ১১%, কারণ ছাড় শুধু node-এ। তিন, নকশা C অর্ধেকেরও কম দামে চলে। Unit economics-এর ভাষায় প্রতি দশ লাখ request-এ $৪.৬৬ বনাম $২.০০. Load balancer, NAT আর egress-এর দর পর্ব ৭২-এর ধরে নেওয়া দরই।

নকশা C সস্তা, কিন্তু সমান নয়: rolling update, self-healing আর HPA নিজেকে সামলাতে হবে। আর সত্যি বলতে, ২০ request/সেকেন্ডের ছোট দোকানের জন্য Kubernetes দরকারি নাও হতে পারে; একটি managed container সেবাই হয়তো যথেষ্ট (পর্ব ১৯)। এখানে Kubernetes বেছেছি শেখার জন্য, বইঘরের প্রয়োজনে নয়। আসল প্রজেক্টে এই সিদ্ধান্ত খোলাখুলি লিখে রাখা দরকার।

একই নকশা তিন provider-এ (চালানো হয়নি)

Service-এর নাম ও ক্ষমতা বদলায়, তাই নিচের সারণি শুরুর মানচিত্র, চূড়ান্ত নয়। লেখার সময়ের নাম দেওয়া হয়েছে; ব্যবহারের আগে provider-এর docs দেখবেন।

আমাদের local অংশAWSAzureGoogle Cloud
Local cluster (managed Kubernetes)EKSAKSGKE
K8s ছাড়া container চালানোECS on FargateAzure Container AppsCloud Run
registry:2ECRAzure Container RegistryArtifact Registry
Local Terraform stateS3 bucket, lock সহBlob StorageCloud Storage
ClusterIP-এর বদলে public পথApplication Load BalancerApplication GatewayCloud Load Balancing
PrometheusManaged Service for PrometheusAzure Monitor managed PrometheusManaged Service for Prometheus
LogCloudWatch LogsAzure Monitor LogsCloud Logging
গোপন মানSecrets ManagerKey VaultSecret Manager
Pipeline-এর পরিচয় (OIDC)IAM OIDC providerWorkload identity federationWorkload Identity Federation

Terraform-এর kubernetes provider-এর অংশ প্রায় একই থাকবে: মূল বদল হবে cluster-এর ঠিকানা আর image-এর registry. কঠিন অংশটি হলো network, IAM আর data transfer, যা local run শেখায় না। নকশার স্তম্ভগুলো ধরে মিলিয়ে নিতে পর্ব ৭৪ দেখুন।

১০ · Cleanup আর সৎ হিসাব — সব মুছে ফেলা, কতটা তৈরি মাপা

শেষ কাজটি সবচেয়ে অবহেলিত। Cloud-এ ভুলে যাওয়া resource মাস শেষে বিল হয়ে ফেরে। তাই প্রতিটি জিনিস নাম ধরে মোছা হলো, আর তারপর “মুছেছে” দাবি না করে যাচাই।

$ kubectl -n abcl-cap delete pod loadgen
$ kubectl -n abcl-cap delete -f prometheus.yaml
$ kubectl -n abcl-cap delete configmap loadgen-script

$ terraform destroy -auto-approve -var image_tag=a12de41eaff0
kubernetes_horizontal_pod_autoscaler_v2.shop: Destruction complete after 0s
kubernetes_service_v1.shop: Destruction complete after 0s
kubernetes_deployment_v1.shop: Destruction complete after 0s
kubernetes_config_map_v1.shop: Destruction complete after 0s
kubernetes_namespace_v1.shop: Destruction complete after 7s

Destroy complete! Resources: 5 destroyed.

$ kubectl get ns abcl-cap
Error from server (NotFound): namespaces "abcl-cap" not found

$ docker rm -fv abcl-cap-registry      # -v: এর নামহীন volume-ও যাবে
abcl-cap-registry
$ docker rmi prom/prometheus:v3.5.0 registry:2
…
$ docker ps -a --format '{{.Names}}' | grep -c abcl-cap
0

Terraform উল্টো ক্রমে মুছেছে: যে যার ওপর নির্ভরশীল, সে আগে। Namespace শেষে, ৭ সেকেন্ডে। Prometheus আর loadgen Terraform-এর বাইরে ছিল, তাই সেগুলো আগে হাতে মুছতে হলো। এটিও একটি শিক্ষা: যা IaC-তে নেই, তা মনে রাখতে হয়। Image-গুলোও একটি একটি করে সঠিক নামে মোছা হয়েছে। docker volume ls-এর তালিকা শুরু আর শেষে হুবহু এক ছিল। প্রতিটি কমান্ডে নাম নির্দিষ্ট, কোনো prune নেই, কারণ একই মেশিনে অন্য কাজও চলছিল।

নিজে cloud-এ এই প্রজেক্ট করলে: কিছু তৈরির আগে budget alert বসান, resource-এ tag দিন, কাজ শেষে terraform destroy করে পরদিন বিলের পাতা দেখুন। Load balancer, NAT gateway, disk আর log অনেক সময়ই আলাদা করে পড়ে থাকে। Cloud ব্যবহারে টাকা লাগতে পারে; এই পর্ব শেষ করতে কোনো account লাগে না।

কতটা তৈরি? সৎ score

প্রজেক্ট চলছে মানেই আসল গ্রাহকের জন্য তৈরি নয়। Production readiness তালিকা সেই ফাঁকটি দেখায়। প্রতিটি সারি আমাদের আসল অবস্থা। done-এর মান বদলে দেখুন score কীভাবে সরে। লক্ষ করুন, কিছু সারি launch blocker: ৯৯% score-ও সেগুলো ঢাকতে পারে না।

// Production readiness score for Boighor. Flip done to true as you fix each item.
// Each row: [area, check, weight, launch blocker?, done?]
const checks = [
  ["delivery", "image tagged by commit SHA",        3, true,  true],
  ["delivery", "tests can stop the build",          3, true,  true],
  ["delivery", "app resources in Terraform",        3, true,  true],
  ["delivery", "remote state with locking",         3, true,  false],
  ["runtime",  "non-root, read-only filesystem",    3, true,  true],
  ["runtime",  "readiness and graceful shutdown",   3, true,  true],
  ["runtime",  "HPA has a working metric source",   2, false, false],
  ["runtime",  "PodDisruptionBudget",               1, false, false],
  ["observe",  "SLO with a burn-rate rule",         3, true,  true],
  ["observe",  "alert reaches a real person",       3, true,  false],
  ["observe",  "app logs every 5xx with a cause",   2, false, false],
  ["safety",   "TLS at the public edge",            3, true,  false],
  ["safety",   "rollback rehearsed",                3, true,  true],
  ["safety",   "chaos test passed",                 2, false, true],
  ["money",    "budget alert and cost tags",        2, false, false],
];
const areas = {};
let got = 0, all = 0;
for (const [area, , weight, , done] of checks) {
  areas[area] ??= { got: 0, all: 0 };
  areas[area].all += weight; all += weight;
  if (done) { areas[area].got += weight; got += weight; }
}
for (const [name, a] of Object.entries(areas))
  console.log(name.padEnd(9), `${a.got}/${a.all}`.padStart(6), "#".repeat(a.got).padEnd(13, "."));
const score = (100 * got / all).toFixed(0);
const blockers = checks.filter(c => c[3] && !c[4]).map(c => c[1]);
console.log(`score ${got}/${all} = ${score}%`);
console.log(blockers.length ? `NOT READY, ${blockers.length} blockers:` : "READY to launch");
blockers.forEach(b => console.log("  - " + b));

আউটপুট:

delivery    9/12 #########....
runtime      6/9 ######.......
observe      3/8 ###..........
safety       5/8 #####........
money        0/2 .............
score 23/39 = 59%
NOT READY, 3 blockers:
  - remote state with locking
  - alert reaches a real person
  - TLS at the public edge

আমাদের চমৎকার demo ৫৯%, আর launch করা যাবে না। হতাশার কিছু নেই: এই সততাটাই পেশাদারিত্ব। ওজন আর “blocker” বাছাই আমাদের নিজের সিদ্ধান্ত, কোনো মান নয়; আপনার দলের তালিকা আলাদা হবে।

প্রজেক্টটি আরও বাড়ান

  • সবচেয়ে সহজ: ওপরের তিনটি blocker শেষ করুন: Alertmanager যোগ করুন, state-কে Kubernetes backend-এ নিন, public পথে TLS দিন। সাথে metrics-server বসিয়ে HPA সচল করুন।
  • মাঝারি: PostgreSQL যোগ করুন: তখন StatefulSet, backup আর RPO/RTO সত্যিকারের প্রশ্ন হবে। অথবা canary বানান, যেখানে burn দেখে deploy নিজে থেমে যায়।
  • নিরাপত্তা: pipeline-এ image scan আর SBOM, NetworkPolicy, signed image (পর্ব ৬১)।
  • Delivery: Terraform-এর জায়গায় Argo CD বা Flux দিয়ে GitOps, dev থেকে prod-এ artifact promotion.
  • Cloud: পুরো নকশাটি একটি provider-এ এক দিনের জন্য তুলুন, budget alert সহ, সন্ধ্যায় destroy করুন, আর বিল মিলিয়ে দেখুন অনুমান কতটা ঠিক ছিল।

পুরো কোর্স এক নজরে

প্রজেক্টের প্রতিটি সিদ্ধান্তের পেছনে একটি আগের মডিউল আছে। কোথাও আটকালে সেই পর্বে ফিরে যান।

মডিউলবইঘরে যেখানে কাজে লাগলফিরে পড়ুন
১ · ভিত্তিService model বাছাই, খরচের ভাষা, DevOps-এর মনোভাবপর্ব ০৭
২ · LinuxPipeline-এর নিরাপদ bash, signal, log পড়াপর্ব ১০
৩ · Computeনকশা A, B আর C-এর তুলনাপর্ব ১৯
৪ · StorageStateless সিদ্ধান্ত, পরের ধাপের databaseপর্ব ২৪
৫ · NetworkLoad balancer, NAT আর egress-এর খরচপর্ব ২৬
৬ · নিরাপত্তাLeast privilege, OIDC-র নকশা, গোপন মানের জায়গাপর্ব ৩১
৭ · DockerMulti-stage image, non-root, registryপর্ব ৩৮
৮ · KubernetesDeployment, probe, Service, HPA, RBACপর্ব ৪৪
৯ · IaCPlan, apply, drift, destroyপর্ব ৫৩
১০ · CI/CDPipeline-এর stage, একবার build, rollback-এর পথপর্ব ৫৭
১১ · SREMetric, burn rate, chaos, postmortemপর্ব ৬৭
১২ · Serverlessনকশা C-এর বিকল্প হিসেবে managed container আর functionপর্ব ৭১
১৩ · FinOps ও নকশাখরচের মডেল, unit economics, readiness তালিকাপর্ব ৭৩

আরও গভীরে: Google-এর Site Reliability Engineering (২০১৬) বইয়ে Production Readiness Review-র ধারণা আছে: কোনো service SRE দলের হাতে নেওয়ার আগে নিয়মিত যাচাই। Accelerate (২০১৮) দেখায় deploy-এর গতি আর স্থিতি পরস্পরের শত্রু নয়, তথ্যের ভিত্তিতে ভালো দল দুটিতেই ভালো করে। আমাদের দুই খারাপ release এই বক্তব্যের ছোট উদাহরণ: দ্রুত rollback থাকলে ঝুঁকি কমে।

পরীক্ষা ও ইন্টারভিউতে: “৯৯.৯% SLO-তে ২৫% error-এর burn কত?” উত্তর ২৫০. “Readiness সবুজ থাকলেও outage কেন?” Probe শুধু তার নিজের পথটুকু দেখে। “Rolling update-এ maxUnavailable=0 কেন?” নতুন Pod ready না হলে পুরোনো নামে না। “rollout undo-এর পরে Terraform কী দেখে?” Drift. “Image tag হিসেবে latest কেন নয়?” কোন code চলছে তা জানা যায় না, rollback-ও অনিশ্চিত হয়।

AI-এর যুগে পুরো প্রজেক্ট

AI এই পর্বের প্রায় সব ফাইল কয়েক সেকেন্ডে লিখে দিতে পারে। কিন্তু দুটি খারাপ release-ই সবুজ পথ দিয়ে এসেছিল, আর সেগুলো ধরা পড়েছে মাপ আর পরীক্ষায়, লেখার সৌন্দর্যে নয়। AI প্রায়ই * অনুমতি দেয়, 0.0.0.0/0 খোলে, storage public করে, secret code-এ লেখে, latest tag আর সরানো API version বসায়, ভুয়া flag বানায়, খরচ ভুলে যায়, আর সাবধান না করে terraform destroy বলে দেয়।

AI-কে “লিখে দাও” না বলে “ভাঙো” বলুন। নিচের prompt-এ আপনার নিজের ফাইল বসিয়ে দিন, তবে কখনো কোনো আসল secret বা key নয়।

তুমি একজন কড়া SRE reviewer. নিচে আমার Dockerfile, Terraform আর Prometheus rule আছে (কোনো secret নেই)। [ফাইলগুলো এখানে পেস্ট করুন] ১. Pipeline সবুজ থাকলেও production-এ ভাঙবে এমন ৫টি পরিস্থিতি বলো। ২. প্রতিটির জন্য আমি নিজে চালিয়ে যাচাই করতে পারি এমন একটি পরীক্ষা দাও। ৩. কোনো argument, flag বা resource নিয়ে নিশ্চিত না হলে "নিশ্চিত নই" লেখো, আর কোন official docs দেখব বলো। ৪. কোনো destructive কমান্ড দিলে আগে সতর্ক করো, আর dry-run বা plan-এর পথ দাও। ৫. খরচ বাড়াতে পারে এমন প্রতিটি পরিবর্তন আলাদা করে দেখাও।

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

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

প্র ০১Health check সবুজ, rollout সফল, অথচ ব্যবহারকারীর request ভাঙছে। তাহলে health check-এর আসল কাজ কী, আর কী তার কাজ নয়?

বইঘরের দুটি release এই প্রশ্নের দুই দিক দেখিয়েছে। প্রথমটিতে readiness আসল বাগটি ধরেছিল, কারণ বাগটি /healthz-এর পথেই ছিল। দ্বিতীয়টিতে বাগ ছিল /-এর একটি শাখায়, যা probe ছোঁয় না। Probe একটি প্রশ্নই করে: “এই Pod কি এখন ট্রাফিক নেওয়ার মতো অবস্থায় আছে?” সে “আপনার ব্যবসার নিয়ম ঠিক আছে কি?” জিজ্ঞেস করে না।

তাহলে কি probe-কে আরও বুদ্ধিমান করব? এখানে সাবধান। Readiness-এ database বা অন্য service পরীক্ষা ঢোকালে, সেই নির্ভরতা একবার ধীর হলে সব Pod একসাথে not-ready হয়ে Service খালি হতে পারে। ছোট সমস্যা তখন পূর্ণ outage হয়। আর liveness-এ এমন পরীক্ষা ঢোকালে kubelet সুস্থ Pod-ও মেরে ফেলবে, যা আমাদের run-এ RESTARTS 1 হয়ে অল্প পরিসরে দেখা গেছে।

Google-এর Site Reliability Engineering (২০১৬) বই এ কারণে symptom আর cause আলাদা করতে শেখায়: alert হোক ব্যবহারকারী যা ভোগেন তার ওপর, যেমন error ratio; কারণ খোঁজা হোক dashboard আর log-এ। Probe থাকে নিচের স্তরে, Pod-এর দেখভালের জন্য। SLO থাকে ওপরের স্তরে, ব্যবহারকারীর অভিজ্ঞতার জন্য। একটি অন্যটির বিকল্প নয়।

Progressive delivery এই দুই স্তর জোড়ে: canary-তে নতুন version অল্প ট্রাফিক পায়, আর পরের ধাপে যাওয়া নির্ভর করে SLI-এর ওপর। বইঘরে canary থাকলে ২৫% error তিনটি Pod-এ না ছড়িয়ে অল্প অংশেই ধরা পড়ত, মোট ক্ষতি অনেক কম হতো।

মনে রাখার সহজ উপায় ধরুন ক্লিনিকের একটি দৃশ্য। রোগীর তাপমাত্রা মাপা দরকারি, কিন্তু তাপমাত্রা স্বাভাবিক থাকলেই তিনি সুস্থ নন। Probe হলো সেই তাপমাত্রা। রোগী কেমন বোধ করছেন, সেটি জানতে তাঁকেই জিজ্ঞেস করতে হয়, আর সফটওয়্যারে সেই জিজ্ঞাসার নাম SLI.

মূল উপলব্ধি: Probe Pod-কে সামলায়, SLO ব্যবহারকারীকে রক্ষা করে। “সব সবুজ” শুনলেই জিজ্ঞেস করুন: কার চোখে সবুজ?

প্র ০২Rollback কি সব সময় নিরাপদ? কখন আগের version-এ ফেরা নতুন বিপদ ডেকে আনে?

বইঘরে rollback সহজ ছিল, কারণ app stateless, প্রতিটি image সংরক্ষিত, আর সিদ্ধান্তটি একটি SHA বদল। বাস্তবে তিনটি জিনিস এই সরলতা ভাঙে।

  • Data: নতুন version database-এর একটি column মুছে দিলে পুরোনো code আর চলবে না। তাই schema বদল হয় expand-contract কৌশলে: আগে নতুন কাঠামো যোগ, দুই version একসাথে চালানো, সবশেষে পুরোনোটি মোছা। মাঝের প্রতিটি অবস্থা ফেরতযোগ্য থাকে।
  • নির্ভরতা: আরেকটি service হয়তো ইতিমধ্যে নতুন API ধরে ফেলেছে। পিছিয়ে গেলে সে ভাঙবে।
  • ভুল রোগ নির্ণয়: ভুল জায়গায় ফিরলে অবস্থা আরও খারাপ হতে পারে।

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

তাই পরিণত দল rollback-কে একটি মহড়া করা কাজ হিসেবে রাখে, যেমন আমরা ধাপ ৫-এ করেছি। Feature flag আরেকটি পথ: code থাকে, শুধু আচরণ বন্ধ হয়। আর কখনো ফেরার পথ না থাকলে fix forward-ই একমাত্র উপায়, তবে সেটি জেনে রাখা দরকার আগে থেকেই, incident-এর মাঝখানে নয়।

একটি ব্যবহারিক অভ্যাস: প্রতিটি বড় release-এর সাথে ছোট একটি নোট রাখুন, ফেরার পথ কী, আর কোন মুহূর্তের পরে ফেরা যাবে না।

মূল উপলব্ধি: Rollback একটি বোতাম নয়, একটি নকশা। Release-এর আগেই জিজ্ঞেস করুন: এটি কি ফেরানো যায়, আর সেটি কি কখনো পরীক্ষা করেছি?

প্র ০৩একটি ছোট বইয়ের দোকানের জন্য Kubernetes কি বাড়াবাড়ি? কোন লক্ষণ দেখে আপনি অন্য পথ বেছে নেবেন?

সৎ উত্তর: সম্ভবত হ্যাঁ। অনুমিত দামের মডেলে Kubernetes-এর নকশা দুই VM-এর দ্বিগুণের বেশি, আর বিলে যা ধরা পড়ে না, তা হলো মানুষের সময়: upgrade, node, network plugin, security patch.

Kubernetes জন্মেছে Google-এর Borg-এর অভিজ্ঞতা থেকে (Borg paper ২০১৫; Kubernetes ২০১৪-তে উন্মুক্ত)। সেই পরিবেশে হাজারো service আর বিশাল দল ছিল। ছোট দলে প্রশ্ন হয় উল্টো: প্ল্যাটফর্মটি আমাদের সমস্যা মেটাচ্ছে, নাকি নতুন সমস্যা আনছে? Dan McKinley-র ২০১৫ সালের প্রবন্ধ Choose Boring Technology এই ভাবনাটি জনপ্রিয় করে: প্রতিটি দলের “নতুনত্বের সুযোগ” সীমিত, তাই সেটি ব্যবসার মূল জায়গায় খরচ করা ভালো।

তবে Kubernetes-এর পক্ষেও শক্ত যুক্তি আছে। দলে আগে থেকেই অভিজ্ঞতা থাকলে, অনেকগুলো service হলে, কিংবা managed সেবায় চালালে বাড়তি বোঝা অনেক কমে। বহনযোগ্যতাও একটি মূল্য, যদিও সেটির দাম আছে (পর্ব ৭৪)।

সিদ্ধান্তের লক্ষণ: একটি বা দুটি stateless service, অল্প ট্রাফিক, দলে Kubernetes জানা কেউ নেই হলে managed container সেবা বা PaaS ভালো। দশটির বেশি service, আলাদা দল, একই deploy পথ দরকার হলে Kubernetes-এর যুক্তি জোরালো। মনে রাখবেন, container image আর SLO দুই পথেই একই থাকে, তাই পরে সরে যাওয়া তুলনামূলক সহজ। এই প্রজেক্টের সবচেয়ে বহনযোগ্য অংশ হলো চিন্তা: tag, probe, burn rate, rollback.

আরেকটি হিসাব করে দেখুন। দলে দুজন মানুষ হলে cluster upgrade-এর একটি সপ্তাহ সরাসরি ফিচারের কাজ থেকে কাটা যায়। ছোট দলের কাছে এই সময়ই সবচেয়ে দামি সম্পদ। তাই প্রতি মাসে প্ল্যাটফর্মের পেছনে কত ঘণ্টা যাচ্ছে, সেটিও ডলারের বিলের পাশে খরচের মডেলে বসিয়ে দেখা ভালো।

মূল উপলব্ধি: টুল বাছাই হোক সমস্যার মাপে, resume-র মাপে নয়। কারণ লিখে রাখুন, যাতে পরিস্থিতি বদলালে সিদ্ধান্তও বদলানো যায়।

প্র ০৪Readiness checklist-এর মতো সাধারণ তালিকা কেন এত কাজের? বুদ্ধিমান প্রকৌশলীদের কি তালিকা লাগে?

তালিকার শক্তি জ্ঞান বাড়ানোয় নয়, ভুলে না যাওয়ায়। Atul Gawande-র The Checklist Manifesto (২০০৯) দেখায়: বিমান চালনা আর অস্ত্রোপচারে অভিজ্ঞ মানুষরাও চাপের মধ্যে সহজ ধাপ বাদ দেন, আর ছোট তালিকা সেই ভুল কমায়। জটিল ব্যবস্থায় ব্যর্থতার বড় অংশ অজ্ঞতা থেকে নয়, জানা জিনিস প্রয়োগ না করা থেকে আসে।

Cloud-এর বড় উদাহরণ GitLab-এর ৩১ জানুয়ারি ২০১৭-র ঘটনা। প্রতিষ্ঠানটির খোলা postmortem অনুযায়ী, চাপের মধ্যে কাজ করতে গিয়ে একজন প্রকৌশলী ভুল server-এ data মুছে ফেলেন। এরপর দেখা যায় কয়েকটি backup পদ্ধতির কোনোটিই ঠিকমতো কাজ করছিল না, কেউ নিয়মিত যাচাই করেনি। শেষে ভাগ্যক্রমে নেওয়া একটি snapshot থেকে ফেরা হয়, প্রায় ৬ ঘণ্টার data হারিয়ে। “Backup আছে” বিশ্বাস ছিল; “restore চালিয়ে দেখেছি” তালিকায় থাকলে ফাঁকটি আগেই ধরা পড়ত।

আমাদের তালিকাতেও একই মনোভাব: “HPA resource আছে” নয়, “HPA-র metric কাজ করছে”; “alert rule আছে” নয়, “alert মানুষের কাছে পৌঁছায়”। ভালো তালিকার গুণ তিনটি। এক, প্রতিটি সারি যাচাইযোগ্য, মত নয়। দুই, ছোট: লম্বা তালিকা কেউ পড়েন না। তিন, প্রতিটি incident তালিকায় সংশোধন আনে।

বিপদও আছে: তালিকা আনুষ্ঠানিকতা হয়ে গেলে মানুষ না দেখে টিক দেন। তাই যত সারি সম্ভব স্বয়ংক্রিয় করা ভালো: policy as code, pipeline-এর test, Terraform-এর plan যাচাই (পর্ব ৫৫)। যন্ত্র যা পারে যন্ত্র করুক, মানুষ রাখুন বিচারের জন্য।

নিজের প্রজেক্টে শুরু করুন দশ-বারোটি সারি দিয়ে, আর প্রতিটি incident-এর পরে দেখুন কোন সারিটি থাকলে ঘটনাটি আটকানো যেত।

মূল উপলব্ধি: তালিকা বুদ্ধির অপমান নয়, স্মৃতির সহায়ক। “আছে” নয়, “পরীক্ষা করে দেখেছি”, এই দুটি শব্দের মাঝেই বেশিরভাগ outage লুকিয়ে থাকে।

প্র ০৫Postmortem-এ “দোষী” না খুঁজলে কি দায়িত্ববোধ হারিয়ে যায় না? Blameless সংস্কৃতি বাস্তবে কীভাবে কাজ করে?

Blameless মানে দায়হীনতা নয়। এর মানে হলো মানুষকে শাস্তির ভয় ছাড়া সত্য বলার সুযোগ দেওয়া, কারণ সত্য না জানলে সিস্টেম ঠিক হয় না। John Allspaw ২০১২ সালে Etsy-র engineering blog-এ এই ধারণা ছড়িয়ে দেন। যুক্তিটি সহজ: কেউ শাস্তির ভয় পেলে পরের বার তথ্য লুকাবেন, আর পরের incident আরও অন্ধকারে ঘটবে।

দৃষ্টান্ত হিসেবে ফেব্রুয়ারি ২০১৭-র Amazon S3 outage ধরুন। AWS-এর প্রকাশিত বিবরণ অনুযায়ী একজন অনুমোদিত কর্মী একটি playbook-এর কমান্ড চালান, কিন্তু একটি ইনপুট ভুল হওয়ায় উদ্দেশ্যের চেয়ে বেশি server সরে যায়, আর তার প্রভাব বহু সেবায় ছড়ায়। প্রকাশিত প্রতিকার মানুষকে দোষ দেওয়ায় নয়, টুল বদলানোয়: ক্ষমতা সরানো হবে ধীরে, ন্যূনতম সীমার নিচে নামা আটকানো হবে। অর্থাৎ প্রশ্নটি ছিল, “একটি টাইপ-ভুল এত বড় হলো কেন?”

আমাদের ঘটনাতেও একই। “Rahim কেন শূন্য দিয়ে ভাগ করলেন” প্রশ্নে কিছু পাওয়া যায় না; যে কেউ করতে পারতেন। “Test কেন শুধু প্রথম বই দেখে, deploy কেন নিজে থামল না, log কেন চুপ” — এই প্রশ্নগুলোর উত্তর কাজে বদলানো যায়।

দায়িত্ব থাকে দুই জায়গায়: সত্যি বলার দায়, আর action item শেষ করার দায়। প্রতিটি কাজের মালিক আর তারিখ চাই। মালিকহীন postmortem হলো ড্রয়ারের কাগজ। বিপজ্জনক ব্যতিক্রমও আছে: ইচ্ছাকৃত ক্ষতি বা বারবার নিয়ম ভাঙা blameless-এর আওতায় পড়ে না, Sidney Dekker-এর “just culture” ধারণা এই রেখাটিই টানে।

লেখার ভঙ্গিও গুরুত্বপূর্ণ: নোটে নামের বদলে ভূমিকা লিখুন, আর “উচিত ছিল” না লিখে “তখন যা জানা ছিল” লিখুন।

মূল উপলব্ধি: “মানুষের ভুল” একটি সিদ্ধান্ত নয়, তদন্তের শুরু। প্রতিটি incident সিস্টেমের একটি বিনা-মূল্যের পরীক্ষা, তবে ফল পেতে সত্য লিখতে হয়।

অনুশীলন

প্রথম দুটি হিসাব, পরের দুটি নকশা আর পূর্বাভাস। Cloud account লাগবে না।

  1. হিসাব: একটি API-র SLO ৯৯.৯৫%, ৩০ দিনে, গড় ট্রাফিক ৫০ request/সেকেন্ড। (ক) মাসিক budget কত request? (খ) ৫% error চললে burn rate কত? (গ) কত ঘণ্টায় পুরো budget শেষ? (ঘ) এই অবস্থায় ১০ মিনিট চললে budget-এর কত অংশ যায়?

    (ক) মোট 50 × 2,592,000 = 129,600,000. Budget 0.0005 অংশ = ৬৪,৮০০টি।

    (খ) 0.05 ÷ 0.0005 = ১০০.

    (গ) 720 ÷ 100 = ৭.২ ঘণ্টা.

    (ঘ) 10 মিনিট = 600 সেকেন্ড। ব্যর্থ 50 × 600 × 0.05 = 1,500. অংশ 1,500 ÷ 64,800 = ২.৩১%. যাচাই: 10 মিনিট ÷ 43,200 মিনিট × 100 = 2.31%, মিলেছে।

  2. কমান্ড: pipeline.sh-এর smoke ধাপে কী বদলালে প্রথম খারাপ release (read-only filesystem-এ ব্যর্থ) ল্যাপটপেই ধরা পড়ত? লাইনটি লিখুন।

    docker run-এ --read-only যোগ করুন, যাতে cluster-এর মতোই root filesystem লেখা না যায়:

    docker run -d --read-only --name abcl-cap-smoke --hostname demo \
      -p 127.0.0.1:18763:8000 "$REF" >/dev/null

    আরও ভালো হয় --user 10001 --cap-drop ALL --security-opt no-new-privileges যোগ করলে। নীতিটি একটাই: পরীক্ষার পরিবেশ production-এর নিষেধগুলোও মানবে, নইলে পরীক্ষা মিথ্যা নিশ্চয়তা দেবে।

  3. নকশা ও সমালোচনা: এক বন্ধু বললেন: “আমাদের pipeline সব সময় myapp:latest push করে, Kubernetes-এ imagePullPolicy: Always, আর rollback করতে Pod মুছে দিই।” তিনটি সমস্যা ধরুন আর সংশোধন দিন।
    • latest: চলছে কোন code, বলা যায় না। সমাধান: commit SHA বা digest.
    • Pod template বদলায় না: Tag একই থাকলে Deployment “কোনো বদল নেই” ধরে, rollout-ই হয় না। Pod নতুন করে উঠলে যে image আসবে, সেটি সময়ের ওপর নির্ভর করে। একই Deployment-এ দুই version-ও চলতে পারে।
    • Rollback নেই: Pod মুছলে সেই একই latest আবার pull হয়, যা নতুন খারাপ image. ফেরার কিছুই নেই। সমাধান: আগের SHA-তে deploy, আর সেই পথের মহড়া।
  4. ফল অনুমান: একটি Deployment-এ replicas=3, maxSurge=1, maxUnavailable=0, পুরোনো ৩টি Pod ready. নতুন image-এর readiness সব সময় ব্যর্থ। progressDeadlineSeconds=45. আপনি নতুন image দিয়ে deploy করলেন, আর কিছু করলেন না। ৫ মিনিট পরে (ক) কতটি Pod চলছে, কতটি ready? (খ) ট্রাফিক কারা পায়? (গ) Deployment কি নিজে rollback করবে?

    (ক) মোট ৪টি: পুরোনো ৩টি ready, নতুন ১টি not-ready. বইঘরের আসল run-এ ঠিক এই ছবিই দেখা গেছে।

    (খ) শুধু পুরোনো ৩টি, কারণ not-ready Pod Service-এর endpoint-এ থাকে না।

    (গ) না। Deadline পার হলে Deployment-এর condition হয় Progressing=False, কারণ ProgressDeadlineExceeded, কিন্তু Kubernetes নিজে আগের version-এ ফেরে না। এই অবস্থা দেখে ব্যবস্থা নেওয়া আপনার, বা Argo Rollouts-এর মতো টুলের কাজ।

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

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

ক্রমএকটি নিরাপদ release-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Commit তৈরি, যার SHA হবে image-এর tag
  2. Unit test চলে, ব্যর্থ হলে build থামে
  3. Image বানিয়ে smoke test, তারপর registry-তে push
  4. terraform plan পড়ে apply
  5. Rolling update: নতুন Pod ready হলে পুরোনো নামে
  6. SLI আর burn rate দেখে release-কে ভালো বলা

৯৯.৯% SLO-র একটি service-এ error ratio ২৫%. Burn rate কত, আর ৩০ দিনের পুরো budget কত ঘণ্টায় শেষ?

  • ২৫, ২৮.৮ ঘণ্টা
  • ১৪.৪, ৫০ ঘণ্টা
  • ২৫০, ২.৮৮ ঘণ্টা
  • ১,০০০, ০.৭২ ঘণ্টা

নতুন version-এর সব Pod readiness পাস করেছে, Terraform-ও “Apply complete” বলেছে, অথচ ২৫% request-এ 500 আসছে। সবচেয়ে সম্ভাব্য কারণ কোনটি?

  • Service not-ready Pod-এ ট্রাফিক পাঠাচ্ছে
  • বাগটি এমন code-পথে, যা probe ছোঁয় না
  • Terraform state নষ্ট হয়ে গেছে
  • Pod-এর CPU limit খুব কম

maxSurge=1 আর maxUnavailable=0 সহ rolling update-এ নতুন image-এর readiness সব সময় ব্যর্থ হলে কী ঘটে?

  • পুরোনো সব Pod নেমে যায়, service বন্ধ হয়
  • Deployment নিজে থেকে আগের version-এ ফিরে যায়
  • নতুন Pod ট্রাফিক পেতে থাকে, ব্যবহারকারী ভুল দেখেন
  • Rollout আটকে থাকে, পুরোনো Pod ট্রাফিক সামলায়

Terraform দিয়ে চলা একটি Deployment-এ কেউ kubectl rollout undo চালালেন। পরের terraform plan-এ কী দেখা যাবে?

  • Image config-এর মানে ফেরানোর একটি বদল (drift)
  • No changes, কারণ Terraform kubectl-এর কাজ দেখে না
  • পুরো Deployment মুছে নতুন করে তৈরির plan
  • State ফাইল নষ্ট বলে error

HPA তৈরি হলো, কিন্তু TARGETS-এ দেখাচ্ছে cpu: <unknown>/60%. এর সবচেয়ে সম্ভাব্য অর্থ কী?

  • HPA ঠিকমতো কাজ করছে, CPU খুব কম
  • Metric পাওয়া যাচ্ছে না, যেমন metrics-server নেই
  • Terraform-এর কোডে syntax ভুল
  • Pod-এর সংখ্যা সর্বোচ্চে পৌঁছেছে

অনুমিত দামের একটি নকশায় মোট মাসিক বিল $২৪১.৪০, যার মধ্যে worker node $৮৬.৪০. Node-এ ৩০% ছাড়ের commitment নিলে মোট বিল প্রায় কত কমবে?

  • ৩০%
  • ২৫%
  • প্রায় ১১%
  • প্রায় ২%

শব্দকোষ

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

মনে রাখুন

  1. আগে SLO, তারপর code: ৯৯.৯% মানে এখানে মাসে ৫১,৮৪০টি ব্যর্থতার অনুমতি।
  2. Image-এর পরিচয় commit SHA; একবার build, আর deploy মানে শুধু tag বদল।
  3. Readiness ভাঙা Pod আটকায়, ভাঙা ব্যবসার নিয়ম ধরে burn-rate alert; দুটোই লাগে।
  4. Rollback-এর মহড়া দিন, আর প্রতিটি incident থেকে একটি নতুন test বা action item বের করুন।
  5. খরচ অনুমিত দামে হিসাব করুন, শেষে সব মুছে যাচাই করুন, আর readiness সৎভাবে মাপুন।

পরের পর্বে: শেখানোর পর্ব এখানেই শেষ। এবার পরীক্ষা ও ইন্টারভিউ প্রস্তুতি শুরু: পর্ব ৭৭-এ CIDR, availability, খরচ, autoscaling আর SLO-র numerical সমস্যা, পূর্ণ সমাধানসহ।

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

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