এই পর্বে যা শিখবেন
- আগের ৭৫টি পর্বের টুকরো দক্ষতা জুড়ে একটি পূর্ণ 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: ৩০ দিনে ৯৯.৯%. হিসাবটি পর্ব ৬৬-এর:
| প্রশ্ন | হিসাব | ফল |
|---|---|---|
| মাসে মোট request | 20 × 86,400 × 30 | ৫,১৮,৪০,০০০ (৫.১৮৪ কোটি) |
| Error budget | 51,840,000 × 0.001 | ৫১,৮৪০টি ব্যর্থ request |
| পুরো বন্ধ থাকলে কত সময় | 43,200 মিনিট × 0.001 | ৪৩.২ মিনিট |
| দ্রুত alert-এর সীমা | 14.4 × 0.001 | error 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
২ · 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.pyUser 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=bc5cc517cd8cTag হলো 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 পাঠাচ্ছিল, যাতে “ব্যবহারকারী টের পেলেন কি না” মাপা যায়।
- ল্যাপটপ
git commit - Pipeline
pipeline.sh - Registry
push - Terraform
apply - Cluster
rollout
| এখন যা ঘটছে | |
| পুরোনো Pod (bc5cc51) | |
| নতুন Pod (b538f57) | |
| ব্যর্থ request |
- Commit.
show the price on the home page, SHAb538f571b047. কিছুই এখনো বদলায়নি, cluster-এ পুরোনো ৩টি Pod চলছে। - Test. Docker-এর ভেতরে unit test চলল ও পাস করল। ব্যর্থ হলে শিকল এখানেই থামত।
- Build. Tag সরাসরি commit SHA, আকার এই run-এ ৭৪.১ MB. BuildKit test stage-এর cache নিল, তাই test আবার চলেনি।
- Smoke test. নতুন image একটি container-এ চালিয়ে
/healthzআর/ডাকা হলো। উত্তরে এলো"price_bdt": 300. - Push. Registry দিল digest
sha256:ea6523be…. এখন image একটি স্থায়ী artifact, আবার build লাগবে না। - Plan আর apply. Plan-এ শুধু একটি লাইন বদল:
image, পুরোনো SHA থেকে নতুন SHA.Plan: 0 to add, 1 to change, 0 to destroy. - প্রথম নতুন Pod.
maxSurge=1তাই একটি বাড়তি Pod এলো। সে0/1: readiness পাস না করা পর্যন্ত Service তাকে ট্রাফিক দেবে না। পুরোনো ৩টি কাজ চালাচ্ছে। - এক এক করে বদল। নতুনটি ready হওয়ার পরে একটি পুরোনো Pod থামছে। SIGTERM পেয়ে সে ৩ সেকেন্ড কাজ চালিয়ে বিদায় নেয়। তারপর পরের নতুন Pod.
- শেষ। 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 revert | Controller মেলানো পর্যন্ত | 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 alertsq.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-এ একটি ভাঙে।
| সময় | |
| Pod-এর অবস্থা | |
| ১ মিনিটের burn | |
| ৫ মিনিটের burn | |
| ১ ঘণ্টার burn | |
| Alert |
- Apply শুরু। নতুন SHA
f9134a436006Terraform-এ গেল। Burn তখনো ০. Pipeline-এর চারটি ধাপই পাস। - Rollout “সফল”। ২৫ সেকেন্ডে Terraform-এর
Apply complete. সব Pod ready, অথচ এক মিনিটের burn ২৮.১ ছাড়িয়েছে। Readiness সবুজ, ব্যবহারকারী লাল। - এক জানালা পার। ৫ মিনিটের burn ১৫.৭, সীমা ১৪.৪-এর ওপরে। ১ ঘণ্টার burn ৯.৯, তাই alert এখনো চুপ। ছোট ঝাঁকুনিতে page না করাই এই নকশার উদ্দেশ্য।
- দুই জানালা পার। দুটিই সীমার ওপরে (২৮.১ আর ১৭.৫)।
for: 1mআছে, তাই alertpending, এখনো কাউকে ডাকেনি। ১ ঘণ্টার burn এত দ্রুত উঠেছে, কারণ এই ল্যাবে ট্রাফিক চলছিল মাত্র কয়েক মিনিট, জানালায় পুরো এক ঘণ্টার ডেটা ছিল না। পুরো ডেটা থাকলে ২৫% error-এ সীমা পেরোতে প্রায় সাড়ে তিন মিনিট লাগত। - Firing. Apply-র ১ মিনিট ৪৬ সেকেন্ড পরে alert বাজল। এক মিনিটের burn ২৫০: error ২৫% ÷ budget ০.১%. পাতার হিসাব আর মাপ মিলে গেছে।
- Rollback শুরু। এই ল্যাবে কোনো মানুষ ঘুমাচ্ছিলেন না, তাই on-call-এর সাড়া দেওয়ার সময় ধরে নিয়ে ৬০ সেকেন্ড অপেক্ষা করে Terraform-এ আগের SHA দেওয়া হলো।
- ক্ষতি থামল। ২৬ সেকেন্ডে পুরোনো version ফিরল। এবার rollback আগের মতো ১ সেকেন্ডে হয়নি, কারণ পুরোনো Pod আগেই নেমে গিয়েছিল, নতুন করে উঠতে হলো। মোট ব্যর্থ ৮১১টি request.
- 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 লেখা আর হস্তান্তর।
| দিন | |
| ঘটনা | |
| এই সপ্তাহের page | |
| মোট খরচ (request) | |
| Budget বাকি |
- হস্তান্তর। আগের on-call-এর নোট পড়া, alert-এর পথ পরীক্ষা, rollback-এর কমান্ড নিজে একবার চালিয়ে দেখা। প্রথম দিনেই কাগজে-কলমে নয়, হাতে যাচাই।
- শান্ত deploy.
a12de41-এর পরের ছোট release, ব্যর্থ ০. On-call মানে শুধু আগুন নেভানো নয়, এমন দিনই বেশি হওয়া উচিত। - রাত ২টার page. কাল্পনিক ধীর error, ১,২০০টি ব্যর্থ request. Runbook দেখে rollback, সকালে তদন্ত। মোট ৮১১ + ১,২০০ = ২,০১১.
- Alert মেরামত। রাতের page-এ runbook-এর লিংক ছিল না, যোগ হলো। এটিই toil কমানোর কাজ। নতুন ক্ষতি ০.
- Pod OOM. ছাড়ের দিনে একটি Pod
OOMKilled, ৪৩০টি ব্যর্থ request. মোট ২,৪৪১. Memory limit বাড়ানোর আগে মেট্রিক দেখে কারণ খোঁজা হলো। - শান্ত দিন। কিছুই ঘটেনি। এই সময়েই গত দুই ঘটনার সংক্ষিপ্ত নোট লেখা।
- হস্তান্তর। ২টি 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 অংশ | AWS | Azure | Google Cloud |
|---|---|---|---|
| Local cluster (managed Kubernetes) | EKS | AKS | GKE |
| K8s ছাড়া container চালানো | ECS on Fargate | Azure Container Apps | Cloud Run |
registry:2 | ECR | Azure Container Registry | Artifact Registry |
| Local Terraform state | S3 bucket, lock সহ | Blob Storage | Cloud Storage |
| ClusterIP-এর বদলে public পথ | Application Load Balancer | Application Gateway | Cloud Load Balancing |
| Prometheus | Managed Service for Prometheus | Azure Monitor managed Prometheus | Managed Service for Prometheus |
| Log | CloudWatch Logs | Azure Monitor Logs | Cloud Logging |
| গোপন মান | Secrets Manager | Key Vault | Secret Manager |
| Pipeline-এর পরিচয় (OIDC) | IAM OIDC provider | Workload identity federation | Workload 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
0Terraform উল্টো ক্রমে মুছেছে: যে যার ওপর নির্ভরশীল, সে আগে। 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-এর মনোভাব | পর্ব ০৭ |
| ২ · Linux | Pipeline-এর নিরাপদ bash, signal, log পড়া | পর্ব ১০ |
| ৩ · Compute | নকশা A, B আর C-এর তুলনা | পর্ব ১৯ |
| ৪ · Storage | Stateless সিদ্ধান্ত, পরের ধাপের database | পর্ব ২৪ |
| ৫ · Network | Load balancer, NAT আর egress-এর খরচ | পর্ব ২৬ |
| ৬ · নিরাপত্তা | Least privilege, OIDC-র নকশা, গোপন মানের জায়গা | পর্ব ৩১ |
| ৭ · Docker | Multi-stage image, non-root, registry | পর্ব ৩৮ |
| ৮ · Kubernetes | Deployment, probe, Service, HPA, RBAC | পর্ব ৪৪ |
| ৯ · IaC | Plan, apply, drift, destroy | পর্ব ৫৩ |
| ১০ · CI/CD | Pipeline-এর stage, একবার build, rollback-এর পথ | পর্ব ৫৭ |
| ১১ · SRE | Metric, 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 নয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১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 লাগবে না।
-
হিসাব: একটি 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%, মিলেছে।
-
কমান্ড:
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-এর নিষেধগুলোও মানবে, নইলে পরীক্ষা মিথ্যা নিশ্চয়তা দেবে। -
নকশা ও সমালোচনা: এক বন্ধু বললেন: “আমাদের pipeline সব সময়
myapp:latestpush করে, 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, আর সেই পথের মহড়া।
-
ফল অনুমান: একটি 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-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Commit তৈরি, যার SHA হবে image-এর tag
- Unit test চলে, ব্যর্থ হলে build থামে
- Image বানিয়ে smoke test, তারপর registry-তে push
terraform planপড়েapply- Rolling update: নতুন Pod ready হলে পুরোনো নামে
- 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 নিলে মোট বিল প্রায় কত কমবে?
- ৩০%
- ২৫%
- প্রায় ১১%
- প্রায় ২%
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- আগে SLO, তারপর code: ৯৯.৯% মানে এখানে মাসে ৫১,৮৪০টি ব্যর্থতার অনুমতি।
- Image-এর পরিচয় commit SHA; একবার build, আর deploy মানে শুধু tag বদল।
- Readiness ভাঙা Pod আটকায়, ভাঙা ব্যবসার নিয়ম ধরে burn-rate alert; দুটোই লাগে।
- Rollback-এর মহড়া দিন, আর প্রতিটি incident থেকে একটি নতুন test বা action item বের করুন।
- খরচ অনুমিত দামে হিসাব করুন, শেষে সব মুছে যাচাই করুন, আর readiness সৎভাবে মাপুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৭৭ · Numerical সমস্যার অনুশীলন — CIDR, availability, খরচ, autoscaling আর SLO, পূর্ণ সমাধানসহপরবর্তী পর্বপুরো কোর্সের হিসাব একসাথে ঝালাই: CIDR থেকে burn rate পর্যন্ত, পূর্ণ সমাধানসহ।
- ক্লাউড কম্পিউটিং ও DevOps · কোর্স হোম৮১টি পর্বপরের পর্ব ৭৭, Numerical সমস্যার অনুশীলন, আর পুরো রোডম্যাপ এখান থেকে।
- পর্ব ৬৬ · Alerting আর SLOআবার পড়ুনBurn rate-এর পুরো তত্ত্ব আর দুই জানালার alert.
- পর্ব ৫৯ · Deployment strategyপরের ধাপCanary, blue-green আর feature flag দিয়ে ঝুঁকি কমানো।
- System Designপাশের ট্র্যাকনির্ভরযোগ্য সিস্টেমের নকশা ও সিদ্ধান্ত।
- Continuous Delivery ও DevOpsপাশের ট্র্যাকGit আর দলের দিক থেকে delivery-র পথ।