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

IaC-র পরীক্ষা — plan review, policy as code আর নিরাপত্তা scan

Testing Infrastructure as Code — Plan Review, Policy as Code and Security Scanning

Taught in Bangla, this lesson builds a testing ladder for infrastructure code: fmt, validate and scanners, validation, precondition, postcondition and check blocks, terraform test with mock and real runs, plan review by people and by a policy script on plan JSON, OPA and Sentinel, and a CI gate combining plan comments, approval, saved plans, destroy protection and cost checks.

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

  • কোন ভুল কোন ধাপে ধরা পড়ে, আর কোন ধাপ কত সময় ও খরচ নেয়
  • validation, precondition, postcondition আর check-এর পার্থক্য, প্রতিটির আসল ব্যর্থতাসহ
  • terraform test দিয়ে mock আর আসল, দুই ধরনের পরীক্ষা লেখা
  • Plan JSON পড়ে নিজের policy script লেখা, আর OPA, Sentinel ও scanner কোথায় বসে তা বোঝা
  • CI-তে plan comment, approval, saved plan, destroy সুরক্ষা আর খরচের হিসাব মিলিয়ে একটি গেট বানানো

১ · “Plan সবুজ” মানেই নিরাপদ নয়

একটি কাল্পনিক অনলাইন বাজার অ্যাপের কথা ভাবুন। এক শুক্রবার দুপুরে একজন প্রকৌশলী একটি ছোট pull request খুললেন। বদল মাত্র একটি লাইন: database-এর একটি setting. Plan চলল, কোনো error নেই। সবার চোখ গেল শেষ লাইনে, 1 to add, 0 to change, 1 to destroy. কেউ খেয়াল করলেন না যে এই “১টি destroy” আসলে orders database.

Terraform-এর ভাষায় code ঠিক ছিল। ব্যাকরণ ঠিক, provider খুশি, plan তৈরি। ভুলটি ছিল অর্থে। IaC-র পরীক্ষা ঠিক এই ফাঁক ভরে: code শুধু “চলে কি না” নয়, “যা করবে তা আমরা চাই কি না”।

সদরঘাটে একটি লঞ্চ ছাড়ার আগে একটিমাত্র যাচাই হয় না। খাতায় যাত্রীর হিসাব মেলানো হয়, ওজন দেখা হয়, ফিটনেসের কাগজ দেখা হয়, আবহাওয়ার সংকেত দেখা হয়, শেষে একজন দায়িত্বশীল মানুষ ছাড়পত্র দেন। প্রতিটি যাচাই আলাদা ভুল ধরে। কাগজ ঠিক থাকলেও ঝড়ের সংকেত থাকতে পারে। IaC-র পরীক্ষাও এমন স্তরে স্তরে সাজানো।

পরীক্ষার মই

প্রতিটি ধাপ আগের ধাপের চেয়ে ধীর ও দামি, কিন্তু আরও বাস্তব ভুল ধরে। তাই সস্তা ধাপ আগে চালানো হয়, যাতে ভুল যত দ্রুত সম্ভব ধরা পড়ে। এই চিন্তাকে বলে shift left.

পরীক্ষার মই: গতি বনাম ভরসা

The IaC testing ladder, fastest at the bottom

পরীক্ষার মই: নিচে দ্রুত, ওপরে বেশি ভরসা ধাপ সাধারণ সময় Drift check Integration test Unit test (mock) Plan ও policy Static analysis validate fmt সূচি ধরে মিনিট সেকেন্ড সেকেন্ড-মিনিট সেকেন্ড সেকেন্ড মুহূর্ত দণ্ড লম্বা = ফলের ওপর ভরসা বেশি
ছবি ৫৫.১ — গুণগত ছবি, মাপা সংখ্যা নয়। Unit test নিচে আছে কারণ mock আসল সিস্টেম ছোঁয় না।
ধাপকী ধরেকী ধরে নাযা লাগে
fmtএলোমেলো লেখাঅর্থের কিছুই নয়কিছু না
validateভুল argument, ভুল typeআসল সিস্টেমের অবস্থাProvider schema
Static analysisঝুঁকিপূর্ণ setting, অচল মানচলার সময়ে ঠিক হওয়া মানScanner
Plan ও policyকী বদলাবে, নিয়ম ভাঙে কি নাProvider-এর চলার সময়ের ব্যর্থতাState, read access
Unit testআপনার module-এর যুক্তিআসল API-র আচরণMock
Integration testসত্যিই তৈরি হয় কি নাProduction-এর চাপআসল account, খরচ
Drift checkপরে হাতে করা বদলনতুন code-এর ভুলনিয়মিত সূচি

এই পর্বের সব Terraform আউটপুট একটি আসল run থেকে নেওয়া: Terraform ১.১৭, local আর kubernetes provider, একটি স্থানীয় single-node cluster. কোনো cloud account ছোঁয়া হয়নি। ID, hash আর সময় আপনার run-এ আলাদা হবে। AWS, Azure বা Google Cloud-এর code থাকলে সেটি চালানো হয়নি, আর সেখানে তা লেখা আছে।

২ · সবচেয়ে সস্তা তিন ধাপ: fmt, validate আর scanner

ধরুন AI একটি ছোট file resource লিখে দিল। লেখা এলোমেলো, আর দুটি argument দেখতে খুব বিশ্বাসযোগ্য। প্রথম ধাপ terraform fmt -check. এটি কিছু বদলায় না, শুধু জানায়। আসল run:

$ terraform fmt -check -diff
main.tf
--- old/main.tf
+++ new/main.tf
@@ -8,8 +8,8 @@
 }

 resource "local_file" "banner" {
-  filename="banner.txt"
-    content = "welcome"
+  filename  = "banner.txt"
+  content   = "welcome"
   file_mode = "0640"
-  owner = "deploy"
+  owner     = "deploy"
 }
$ echo $?
3

Exit code শূন্য নয়, তাই CI-তে এই ধাপ লাল হবে। সমাধান একটিই: terraform fmt চালিয়ে commit করা। এতে review-তে স্থান আর লাইন মেলানো নিয়ে কথা বলতে হয় না। মনে রাখুন, fmt-এর চোখে file_mode একদম ঠিক। সে শুধু লেখার ধরন দেখে।

পরের ধাপ terraform validate. এটি provider-এর schema-র সাথে মেলায়, কিন্তু কোনো cloud API ডাকে না। তাই CI-তে terraform init -backend=false-এর পর credential ছাড়াই চলে।

$ terraform validate

Error: Unsupported argument

  on main.tf line 13, in resource "local_file" "banner":
  13:   file_mode = "0640"

An argument named "file_mode" is not expected here.

Error: Unsupported argument

  on main.tf line 14, in resource "local_file" "banner":
  14:   owner = "deploy"

An argument named "owner" is not expected here.
$ echo $?
1

AI-এর একটি পরিচিত ভুল এখানেই ধরা পড়ল। local_file-এ file_mode বা owner নেই। আসল argument-টি file_permission. ভুল argument এই ধাপে খরচ ছাড়াই আটকে যায়। কিন্তু validate সবুজ হলেও এটুকুই জানা গেল: ব্যাকরণ ঠিক। Bucket সবার জন্য খোলা কি না, সে তা বলবে না।

Static analysis: code পড়ে ঝুঁকি খোঁজা

Static analysis মানে কিছু না চালিয়ে শুধু লেখা পড়ে সমস্যা খোঁজা। IaC-তে এর তিনটি পরিচিত নাম আছে।

tflint

tflint একটি linter. Provider plugin দিয়ে সে অচল instance type-এর মতো ভুলও ধরতে পারে, যা validate ধরে না।

ভুল আর অভ্যাস

Checkov

Checkov ঝুঁকিপূর্ণ setting খোঁজে। HCL পড়তে পারে, plan JSON-ও পড়তে পারে। Bridgecrew-এ শুরু, পরে Palo Alto Networks-এর হাতে আসে।

নিরাপত্তা নিয়ম

Trivy config

Trivy মূলত image scanner, কিন্তু trivy config IaC-ও দেখে। পুরোনো tfsec-এর কাজ Aqua Security এখানে নিয়ে এসেছে।

Image আর IaC একসাথে

এই তিনটি tool এই পর্ব লেখার সময় চালানো হয়নি। নিচের দুটি ব্লক আসল run নয়, শুধু ফরম্যাট দেখানোর নমুনা। Rule ID, লেখা আর সাজানো version-ভেদে আলাদা হতে পারে।

$ checkov -f plan.json --framework terraform_plan --compact
Passed checks: 7, Failed checks: 1, Skipped checks: 0

Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access."
    FAILED for resource: aws_s3_bucket.assets
$ trivy config --severity HIGH,CRITICAL .
main.tf (terraform)

Tests: 12 (SUCCESSES: 11, FAILURES: 1)
Failures: 1 (HIGH: 1, CRITICAL: 0)

AVD-AWS-0086 (HIGH): No public access block so not blocking public acls

দুটি নমুনাই একই ধরনের কথা বলছে, যা পর্ব ৩৪-এর misconfiguration-এর গল্প। Scanner-এর দুর্বলতাও জানা দরকার। কিছু মান apply-এর আগে অজানা থাকে, তাই HCL-এ ঠিক দেখালেও ফল ভুল হতে পারে। এজন্য plan JSON-এর ওপরেও চালানো ভালো। আবার scanner অনেক সময় ভুল সতর্কতা দেয়। প্রতিটি skip-এর কারণ লিখে রাখা তাই জরুরি।

৩ · HCL-এর ভেতরের চার পাহারাদার

Terraform-এর HCL ভাষার ভেতরেই নিয়ম লেখা যায়। নিয়মগুলো code-এর পাশে থাকে, তাই module যিনিই ব্যবহার করুন, নিয়ম সাথে যায়। চারটি উপায় আছে, আর পার্থক্য হলো কখন চলে আর ব্যর্থ হলে কী হয়।

যাচাইকী দেখেব্যর্থ হলেপ্রথম এসেছে
validationইনপুট variableError, run থামে০.১৩ (২০২০)
preconditionResource তৈরির আগের অনুমানError, ওই resource তৈরি হয় না১.২ (২০২২)
postconditionতৈরি বা পড়ার পরের ফলError, তবে বদল হয়ে যেতে পারে১.২ (২০২২)
checkসার্বিক অবস্থাশুধু warning, run চলে১.৫ (২০২৩)

একটি ছোট configuration-এ চারটিই বসিয়েছি। নিচের অংশগুলো পুরো ফাইল থেকে নেওয়া, আর ফাইলটি fmt -check ও validate পাস করেছে।

variable "env" {
  type    = string
  default = "dev"
}

variable "replicas" {
  type    = number
  default = 2

  validation {
    condition     = var.replicas >= 1 && var.replicas <= 6
    error_message = "replicas must be between 1 and 6."
  }
}

# Data source with a postcondition: the namespace must carry a team label.
data "kubernetes_namespace_v1" "target" {
  metadata {
    name = "abcl-tp"
  }

  lifecycle {
    postcondition {
      condition     = can(self.metadata[0].labels["team"])
      error_message = "Namespace needs a team label."
    }
  }
}

resource "local_file" "app_config" {
  filename = "${path.module}/app.conf"
  content  = "env=${var.env}\nreplicas=${var.replicas}\n"

  lifecycle {
    precondition {
      condition     = var.env != "prod" || var.replicas >= 2
      error_message = "prod needs at least 2 replicas."
    }
  }
}

# check block: a failed assert only warns, it never stops the run.
check "namespace_has_quota" {
  data "kubernetes_resources" "quotas" {
    api_version = "v1"
    kind        = "ResourceQuota"
    namespace   = "abcl-tp"
  }

  assert {
    condition     = length(data.kubernetes_resources.quotas.objects) > 0
    error_message = "Namespace abcl-tp has no ResourceQuota."
  }
}

এবার প্রতিটিকে ইচ্ছা করে ভাঙলাম। লম্বা আউটপুট থেকে অপ্রাসঙ্গিক লাইন বাদ দিয়ে … রেখেছি।

$ terraform plan -var replicas=9
…
Error: Invalid value for variable

  on main.tf line 24:
  24: variable "replicas" {
    ├────────────────
    │ var.replicas is 9

replicas must be between 1 and 6.

This was checked by the validation rule at main.tf:28,3-13.

$ terraform plan -var env=prod -var replicas=1
…
Error: Resource precondition failed

  on main.tf line 54, in resource "local_file" "app_config":
  54:       condition     = var.env != "prod" || var.replicas >= 2
    ├────────────────
    │ var.env is "prod"
    │ var.replicas is 1

prod needs at least 2 replicas.

$ terraform plan       # namespace-এ team label নেই
…
Error: Resource postcondition failed

  on main.tf line 42, in data "kubernetes_namespace_v1" "target":
  42:       condition     = can(self.metadata[0].labels["team"])
    ├────────────────
    │ self.metadata[0].labels is map of string with 1 element

Namespace needs a team label.

$ terraform apply -auto-approve -var env=prod -var replicas=2
…
Warning: Check block assertion failed

  on main.tf line 69, in check "namespace_has_quota":
  69:     condition     = length(data.kubernetes_resources.quotas.objects) > 0
    ├────────────────
    │ data.kubernetes_resources.quotas.objects is empty tuple

Namespace abcl-tp has no ResourceQuota.

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

প্রথম তিনটিতে exit code ১. শেষটিতে warning এলেও exit code ০, আর ফাইল তৈরি হয়ে গেছে। এটাই check-এর চরিত্র: “জানিয়ে রাখলাম, আটকালাম না।” তাই এটি চলমান সিস্টেমের স্বাস্থ্য দেখার জন্য ভালো, কিন্তু কঠোর নিয়মের জন্য নয়।

Postcondition-এর একটি ফাঁদ

Resource-এর কোনো মান apply-এর পরেই জানা গেলে postcondition চলে সেই পরে। ততক্ষণে জিনিসটি তৈরি হয়ে গেছে। একটি Service-এ ইচ্ছা করে ভুল IP-পরিসর চাইলাম:

$ terraform apply -auto-approve
…
kubernetes_service_v1.api: Creating...
kubernetes_service_v1.api: Creation complete after 0s [id=abcl-tp/pc-api]

Error: Resource postcondition failed

  on main.tf line 28, in resource "kubernetes_service_v1" "api":
  28:       condition     = startswith(self.spec[0].cluster_ip, "192.168.")
    ├────────────────
    │ self.spec[0].cluster_ip is "10.110.80.18"

Service IP is outside the expected range.
$ terraform state list
kubernetes_service_v1.api

Exit code ১, কিন্তু Service আছে, আর সেটি state-এও আছে। এরপর সাধারণ plan-ও একই error-এ থেমেছে। অর্থাৎ postcondition কোনো rollback নয়। সে শুধু পরের ধাপকে থামায়। ব্যর্থ হলে জিনিসটি ঠিক করা বা destroy করার দায় আপনার।

৪ · terraform test: HCL দিয়েই HCL-এর পরীক্ষা

terraform test আসে ১.৬ সংস্করণে (২০২৩), আর mock_provider আসে ১.৭-এ (২০২৪)। Test লেখা হয় .tftest.hcl ফাইলে। প্রতিটি run ব্লক একবার plan বা apply চালিয়ে assert মেলায়। Go শেখার দরকার পড়ে না। OpenTofu-তেও tofu test আছে।

আমাদের module-এর নিয়ম: prod-এ কমপক্ষে ২টি replica. Code-এ একটি ইচ্ছাকৃত বাগ রাখলাম, max-এর জায়গায় min:

locals {
  # Rule: prod always runs at least 2 copies.
  replicas = var.env == "prod" ? min(var.replicas, 2) : var.replicas
}
# Unit test: a fake kubernetes provider, so no cluster is needed.
mock_provider "kubernetes" {}

variables {
  name = "bazar-api"
}

run "dev_keeps_one_replica" {
  command = plan

  assert {
    condition     = output.replicas == 1
    error_message = "dev should keep 1 replica."
  }
}

run "prod_runs_at_least_two" {
  command = plan

  variables {
    env      = "prod"
    replicas = 1
  }

  assert {
    condition     = output.replicas >= 2
    error_message = "prod must run at least 2 replicas."
  }
}

run "too_many_replicas_rejected" {
  command = plan

  variables {
    replicas = 9
  }

  expect_failures = [var.replicas]
}
$ terraform test
tests\unit.tftest.hcl... in progress
  run "dev_keeps_one_replica"... pass
  run "prod_runs_at_least_two"... fail

Error: Test assertion failed

  on tests\unit.tftest.hcl line 26, in run "prod_runs_at_least_two":
  26:     condition     = output.replicas >= 2
    ├────────────────
    │ Diff:
    │ --- actual
    │ +++ expected
    │ - 1
    │ + 2


prod must run at least 2 replicas.
  run "too_many_replicas_rejected"... pass
tests\unit.tftest.hcl... tearing down
tests\unit.tftest.hcl... fail

Failure! 2 passed, 1 failed.
$ echo $?
1

কোনো cluster লাগেনি, কারণ mock_provider নকল উত্তর দেয়। এই আসল run Windows-এ হয়েছে, তাই path-এ \ দেখছেন। তৃতীয় run-টি pass করেছে কারণ expect_failures বলছে “এখানে ব্যর্থ হওয়াই সঠিক।” নিয়ম কেউ মুছে দিলে এই test লাল হবে। min বদলে max করার পরের run:

tests\unit.tftest.hcl... in progress
  run "dev_keeps_one_replica"... pass
  run "prod_runs_at_least_two"... pass
  run "too_many_replicas_rejected"... pass
tests\unit.tftest.hcl... tearing down
tests\unit.tftest.hcl... pass

Success! 3 passed, 0 failed.

Unit বনাম integration

command = plan আর mock মিলে unit test. এটি আপনার যুক্তি দেখে। কিন্তু আসল API কী বলবে, mock তা জানে না। Integration test-এ command = apply সত্যিই জিনিস বানায়, মেলায়, আর শেষে মুছে দেয়। নিচের test স্থানীয় cluster-এ একটি আসল Deployment চালিয়ে মুছে ফেলেছে:

# Integration test: real provider, real Deployment, destroyed afterwards.
provider "kubernetes" {
  config_context = "docker-desktop"
}

run "real_rollout_in_scratch_namespace" {
  command = apply

  variables {
    name     = "tp-itest"
    env      = "prod"
    replicas = 2
  }

  assert {
    condition     = kubernetes_deployment_v1.app.spec[0].replicas == "2"
    error_message = "The Deployment should ask for 2 replicas."
  }
}
$ terraform test -filter='tests\integration.tftest.hcl'
tests\integration.tftest.hcl... in progress
  run "real_rollout_in_scratch_namespace"... pass
tests\integration.tftest.hcl... tearing down
tests\integration.tftest.hcl... pass

Success! 1 passed, 0 failed.
$ kubectl get deploy -n abcl-tp
No resources found in abcl-tp namespace.

একটি নীরব ফাঁদ: -filter-এ path ভুল লিখলে এই run-এ আউটপুট এলো Success! 0 passed, 0 failed., সাথে একটি Unknown test file warning, আর exit code ০. অর্থাৎ CI সবুজ, অথচ কোনো পরীক্ষাই হয়নি। তাই pipeline-এ পাস করা test-এর সংখ্যাও দেখুন।

৫ · Plan পড়া: মানুষের চোখ আর যন্ত্রের চোখ

Plan review-এর প্রথম নিয়ম: শেষ লাইনের সংখ্যা নয়, চিহ্ন পড়ুন। + মানে তৈরি, ~ মানে জায়গায় বদল, - মানে মুছে ফেলা, আর -/+ মানে মুছে নতুন করে বানানো। এই শেষটিই সবচেয়ে ভয়ের, কারণ যে argument-এর জন্য এটি ঘটছে তার পাশে # forces replacement লেখা থাকলেও লম্বা plan-এ চোখ এড়িয়ে যায়।

কিন্তু ৮০০ লাইনের plan মানুষ মন দিয়ে পড়তে পারে না। তাই plan-কে যন্ত্রে পড়ার মতো করা হয়। দুটি কমান্ড:

$ terraform plan -out=bad.tfplan -var replicas=8 \
    -var 'labels={app="bazar"}' -var service_type=LoadBalancer
…
Saved the plan to: bad.tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "bad.tfplan"
$ terraform show -json bad.tfplan > bad.json

Saved plan ফাইলটি binary, পড়ার জন্য নয়। show -json তাকে plan JSON বানায়। এর আসল অংশ resource_changes: প্রতিটি resource, তার actions, আর আগের ও পরের মান। আসল run থেকে একটি এন্ট্রি, ছোট করে:

{
  "format_version": "1.2",
  "terraform_version": "1.17.1",
  "resource_changes": [
    {
      "address": "kubernetes_deployment_v1.api",
      "type": "kubernetes_deployment_v1",
      "change": {
        "actions": ["create"],
        "before": null,
        "after": {
          "metadata": [{ "labels": { "app": "bazar" }, "name": "bazar-api" }],
          "spec": [{ "replicas": "8" }]
        }
      }
    }
  ]
}

এবার একটি সত্যিকারের ছোট policy as code. শুধু Python-এর standard library, আর exit code দিয়ে pipeline-কে জানানো। ফাইলটি আসলে এই bad.json-এর ওপর চলেছে:

#!/usr/bin/env python3
"""Deny risky changes in a Terraform plan. Usage: policy.py plan.json"""
import json
import sys

MAX_REPLICAS = 6
REQUIRED_LABELS = {"app", "owner"}

plan = json.load(open(sys.argv[1], encoding="utf-8"))
denies = []
for rc in plan.get("resource_changes", []):
    actions, after = rc["change"]["actions"], rc["change"]["after"] or {}
    if actions in (["no-op"], ["read"], ["delete"]):
        continue
    if rc["type"] == "kubernetes_deployment_v1":
        labels = after["metadata"][0].get("labels") or {}
        missing = sorted(REQUIRED_LABELS - set(labels))
        if missing:
            denies.append(f"{rc['address']}: missing labels {missing}")
        replicas = int(after["spec"][0]["replicas"])
        if replicas > MAX_REPLICAS:
            denies.append(f"{rc['address']}: replicas {replicas} > {MAX_REPLICAS}")
    if rc["type"] == "kubernetes_service_v1":
        if after["spec"][0].get("type") == "LoadBalancer":
            denies.append(f"{rc['address']}: public LoadBalancer needs an exception")

for d in denies:
    print("DENY ", d)
print(f"{len(denies)} deny, checked {len(plan.get('resource_changes', []))} changes")
sys.exit(1 if denies else 0)
$ python policy.py bad.json
DENY  kubernetes_deployment_v1.api: missing labels ['owner']
DENY  kubernetes_deployment_v1.api: replicas 8 > 6
DENY  kubernetes_service_v1.api: public LoadBalancer needs an exception
3 deny, checked 3 changes
$ echo $?
1
$ python policy.py good.json
0 deny, checked 3 changes
$ echo $?
0
$ terraform apply good.tfplan
…
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

তিনটি নিয়মই একসাথে ধরা পড়েছে। একটি plan-এ সব ভুল জানানো ভালো, যাতে লেখককে বারবার push করতে না হয়। Script-টি ["delete"] বাদ দেয়, কারণ মুছে ফেলা জিনিসের after থাকে null. খেয়াল করুন, Kubernetes provider এখানে replicas-কে string হিসেবে রাখে, তাই int() দরকার ছিল। Policy লিখতে হলে JSON-এর আসল গঠন নিজের চোখে দেখতে হয়।

Plan ফাইল আর plan JSON গোপন তথ্য বহন করতে পারে। Terraform-এর sensitive চিহ্ন শুধু পর্দায় লুকায়; plan ফাইলে মান থেকে যায়। তাই এগুলোকে পর্ব ৩৩-এর secret-এর মতো দেখুন: অল্প সময় রাখুন, access সীমিত রাখুন।

ধাপে ধাপে: public bucket ধরা

একই কৌশল cloud-এর একটি পরিচিত ভুলে কেমন কাজ করে দেখুন। ধরা যাক একটি রিপোর্টের ছবি দ্রুত দেখাতে কেউ পর্ব ২১-এর bucket-এর public block খুলে দিয়েছেন। নিচের plan একটি নমুনা, AWS provider এখানে চালানো হয়নি।

নমুনা planমানুষের চোখে
  # aws_s3_bucket_public_access_block.assets will be updated in-place
  ~ resource "aws_s3_bucket_public_access_block" "assets" {
      ~ block_public_policy     = true -> false
      ~ restrict_public_buckets = true -> false
    }
Plan: 0 to add, 1 to change, 0 to destroy.
plan.jsonযন্ত্রের চোখে
"type": "aws_s3_bucket_public_access_block",
"change": {
  "actions": ["update"],
  "before": { "block_public_policy": true },
  "after":  { "block_public_policy": false }
}
নিয়মRule: NO_PUBLIC_BUCKET
if type == "aws_s3_bucket_public_access_block"
   and "delete" not in actions
   and after.block_public_policy == false:
       deny("public access is allowed")
Pipelineধাপ লাল
DENY  NO_PUBLIC_BUCKET: ...assets: public access is allowed
1 deny, checked 1 changes
exit code 1
বিকল্প পথনমুনা Git diff
# public block আগের মতোই থাকল
+ resource "aws_cloudfront_distribution" "assets" { ... }
Pipelineনতুন plan
Plan: 1 to add, 0 to change, 0 to destroy.
0 deny, checked 1 changes
exit code 0
  1. বদলটি দেখতে নিরীহ। Plan বলছে মাত্র ১টি change. কোনো destroy নেই, তাই দ্রুত চোখ বোলালে কিছুই ভয়ের লাগে না। অথচ দুটি true false হয়ে গেছে।
  2. JSON-এ একই তথ্য। terraform show -json-এর পরে এটি একটি পরিষ্কার মান: after.block_public_policy হলো false. যন্ত্র এখানে ক্লান্ত হয় না।
  3. নিয়ম মেলানো। Policy শুধু এই type দেখে, মুছে ফেলার ক্ষেত্র বাদ দেয়, তারপর মান পরীক্ষা করে। তিনটি শর্তই মিলল।
  4. Deny. Script exit code ১ দিয়ে থামল। PR-এর check লাল, merge বন্ধ। কারণটি লেখাতেই আছে, তাই লেখক জানেন কী বদলাতে হবে।
  5. ঠিক সমাধান। Bucket খোলার বদলে লেখক সামনে একটি CDN বসালেন, যেখানে শুধু CDN bucket পড়তে পারে। Public block আগের মতোই থাকল। (নমুনাটি সংক্ষিপ্ত, পুরো resource নয়।)
  6. নতুন plan পাস। 1 to add, 0 to change, policy ০ deny, exit code ০. PR আবার review-এর জন্য তৈরি। নিয়ম কাজ আটকায়নি, ভুল পথ আটকেছে।

এবার নিজে নিয়ম বদলে দেখুন। নিচে একটি ছোট policy evaluator, যা নমুনা resource change-এর ওপর চলে। এতে একটি exception-ও আছে, যার মেয়াদ শেষ হয়। today-কে ১২১ করলে কী হয়, বা bucket-এর মান true করলে কী হয়, দেখুন:

// A tiny policy engine over sample Terraform resource changes.
const changes = [
  { address: "aws_s3_bucket_public_access_block.assets", type: "public_access_block",
    actions: ["update"], after: { block_public_policy: false } },
  { address: "google_compute_firewall.ssh", type: "firewall",
    actions: ["create"], after: { source_ranges: ["0.0.0.0/0"], ports: ["22"] } },
  { address: "kubernetes_deployment_v1.api", type: "deployment",
    actions: ["update"], after: { replicas: 8, labels: { app: "bazar" } } },
  { address: "azurerm_storage_account.logs", type: "storage",
    actions: ["create"], after: { min_tls_version: "TLS1_2", tags: { owner: "ops" } } },
];
const rules = [
  { id: "NO_PUBLIC_BUCKET", type: "public_access_block",
    bad: (a) => a.block_public_policy === false ? "public access is allowed" : null },
  { id: "NO_OPEN_SSH", type: "firewall",
    bad: (a) => a.source_ranges.includes("0.0.0.0/0") && a.ports.includes("22")
      ? "port 22 is open to the whole internet" : null },
  { id: "MAX_REPLICAS", type: "deployment",
    bad: (a) => a.replicas > 6 ? "replicas " + a.replicas + " > 6" : null },
  { id: "OWNER_LABEL", type: "deployment",
    bad: (a) => a.labels.owner ? null : "label owner is missing" },
];
const waivers = [{ rule: "MAX_REPLICAS", address: "kubernetes_deployment_v1.api", until: 120 }]; // expires
const today = 100; // simulated day number, so the output never changes
let denies = 0, waived = 0;
for (const c of changes) for (const r of rules) {
  if (r.type !== c.type) continue;
  const why = r.bad(c.after);
  if (!why) continue;
  const w = waivers.find((x) => x.rule === r.id && x.address === c.address && x.until >= today);
  if (w) { waived++; console.log("WAIVED  " + r.id + ": " + why + " (until day " + w.until + ")"); }
  else { denies++; console.log("DENY    " + r.id + ": " + c.address + ": " + why); }
}
console.log("result: " + denies + " deny, " + waived + " waived, " + changes.length + " changes");
console.log(denies ? "exit code 1: stop the pipeline" : "exit code 0: continue");

আউটপুট:

DENY    NO_PUBLIC_BUCKET: aws_s3_bucket_public_access_block.assets: public access is allowed
DENY    NO_OPEN_SSH: google_compute_firewall.ssh: port 22 is open to the whole internet
WAIVED  MAX_REPLICAS: replicas 8 > 6 (until day 120)
DENY    OWNER_LABEL: kubernetes_deployment_v1.api: label owner is missing
result: 3 deny, 1 waived, 4 changes
exit code 1: stop the pipeline

Azure storage-এর এন্ট্রিটি নিয়ম মেলে না বলে নীরবে পাস করেছে। MAX_REPLICAS-এর exception-টি একটি নির্দিষ্ট resource আর একটি নির্দিষ্ট তারিখে বাঁধা। এটাই ভালো exception-এর লক্ষণ: নিয়ম বন্ধ হয় না, শুধু একটি জায়গায়, কিছু দিনের জন্য ছাড় পায়।

৬ · Policy engine: নিজের script কখন যথেষ্ট নয়

ত্রিশ লাইনের Python দিয়ে শুরু করা ভালো। কিন্তু নিয়ম ২০০টি হলে আপনার লাগবে নিয়মের test, version, exception, audit log আর আলাদা দলের মালিকানা। Policy engine এই কাঠামো দেয়।

Toolভাষাকোথায় চলেমনে রাখুন
OPARegoযেকোনো জায়গায়, JSON পেলেইসাধারণ policy engine; CNCF graduated (২০২১)
ConftestRegoCI, ফাইলের ওপরOPA-কে Terraform, YAML, Dockerfile-এ লাগানোর সহজ CLI
Sentinelনিজস্ব ভাষাHCP Terraform, Terraform EnterpriseHashiCorp-এর বাণিজ্যিক পণ্যের অংশ
KyvernoYAML-ধাঁচেরKubernetes admissionPlan নয়, cluster-এ আসা object দেখে
OPA GatekeeperRegoKubernetes admissionপর্ব ৪৯-এ দেখেছেন

আমাদের owner নিয়মটি Rego-তে এমন। এটি নমুনা, এই পর্বের জন্য চালানো হয়নি। Conftest সাধারণত main package-এর deny নিয়ম খোঁজে:

package main

deny contains msg if {
    rc := input.resource_changes[_]
    rc.type == "kubernetes_deployment_v1"
    rc.change.after != null
    not rc.change.after.metadata[0].labels.owner
    msg := sprintf("%s: owner label is missing", [rc.address])
}
$ conftest test bad.json --policy policy/
FAIL - bad.json - main - kubernetes_deployment_v1.api: owner label is missing

1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions

Rego নিয়ম ঘোষণা করে, ধাপ লেখে না। সব শর্ত সত্য হলেই msg তৈরি হয়। after != null লাইনটি ছাড়া মুছে ফেলা Deployment-ও ভুলভাবে deny হতো, কারণ Rego-তে না-থাকা (undefined) মানের ওপর not সত্য হয়। আমাদের Python script যে কারণে delete বাদ দিয়েছে, এখানেও সেই একই কথা।

Sentinel-এর একটি ধারণা সব জায়গায় কাজে লাগে: নিয়মের তিনটি কঠোরতা। advisory শুধু জানায়, soft-mandatory আটকায় তবে অনুমোদিত মানুষ ছাড় দিতে পারেন, আর hard-mandatory কেউ এড়াতে পারে না। নতুন নিয়ম advisory দিয়ে শুরু করলে দেখা যায় কতগুলো ভুল সতর্কতা আসছে। এরপর ধাপে ধাপে কঠোর করা নিরাপদ।

কোথায় কোন নিয়ম

একই নিয়ম একাধিক স্তরে থাকতে পারে। CI-র policy লেখককে দ্রুত জানায়, কিন্তু কেউ console-এ হাতে বদল করলে CI দেখে না। তাই নিচে থাকে guardrail: পর্ব ৩৫-এর organization policy আর Kubernetes-এর admission control. প্রথমটি দ্রুত উত্তর দেয়, দ্বিতীয়টি নিশ্চয়তা দেয়। একটি আরেকটির বিকল্প নয়।

৭ · CI-তে নিরাপদ plan: comment, approval, saved plan

এবার সব ধাপ এক জায়গায়। নিয়ম তিনটি: সব পরীক্ষা pull request-এ চলবে, plan মানুষের সামনে আসবে, আর apply হবে শুধু সেই plan যা দেখা হয়েছে। Pull request-এর সাধারণ প্রবাহ Software Engineering কোর্সে আছে, এখানে শুধু Terraform-এর অংশটুকু। সংখ্যাগুলো একটি কাল্পনিক PR-এর।

  1. PushPR #57
  2. Lintfmt, validate
  3. Testtest, scan
  4. Plan-out
  5. Policyplan JSON
  6. CommentPR-এ plan
  7. Approveমানুষ
  8. Applysaved plan
PR #57 · কাল্পনিক উদাহরণ
কমান্ড
ফল
Merge বা apply
  1. Push. Branch থেকে PR খোলা হলো। infra/ ফোল্ডারে বদল, তাই workflow শুরু হলো। এখনো কোনো অনুমতি নেই।
  2. Lint. Credential ছাড়াই fmt আর validate চলে গেল। ভুল থাকলে এখানেই সেকেন্ডের মধ্যে থামত।
  3. Test আর scan. Mock-ভিত্তিক terraform test আর scanner চলল। এখনো আসল সিস্টেম ছোঁয়া হয়নি।
  4. Plan. এই প্রথম state আর আসল API পড়া হলো। তাই শুধু read-অনুমতির একটি স্বল্পমেয়াদি OIDC credential যথেষ্ট। ফল tfplan ফাইলে জমা।
  5. Policy. Plan JSON-এর ওপর নিয়ম চলল। ৩টি change, ০ deny.
  6. Comment. Plan লেখাটি PR-এ এলো, যাতে রিভিউয়ারকে log খুঁজতে না হয়। মেশিন নিয়ম দেখেছে, মানুষ এবার উদ্দেশ্য দেখবেন।
  7. Approve. রিভিউয়ার চিহ্নগুলো পড়লেন: ১টি নতুন, ২টি বদল, কোনো মুছে ফেলা নেই। Production environment-এর গেটে approve করলেন।
  8. Apply. শুধু tfplan apply হলো। Terraform নতুন করে কিছু ভাবল না, plan-এ যা ছিল ঠিক তাই করল: 1 added, 2 changed, 0 destroyed.

GitHub Actions-এ এর একটি কাঠামো নিচে। অন্য CI-তেও ধারণা একই। এই YAML parse করে যাচাই করা হয়েছে, কিন্তু আসল repository-তে চালানো হয়নি। Action-এর version লেখার সময়ের, মিলিয়ে নিন। Cloud login-এর ধাপ এখানে বাদ দেওয়া। Pipeline-এর গতি আর cache নিয়ে কথা হবে পর্ব ৫৭-এ, আর OIDC-র বিস্তারিত পর্ব ৬১-তে।

name: terraform
on:
  pull_request:
    paths: ["infra/**"]
  push:
    branches: [main]
    paths: ["infra/**"]

permissions:
  contents: read
  id-token: write       # short-lived cloud login (OIDC), no stored keys
  pull-requests: write  # lets the job comment on the PR

jobs:
  plan:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: infra
    steps:
      - uses: actions/checkout@v7  # in production pin a full commit SHA
      - uses: hashicorp/setup-terraform@v4
        with:
          terraform_version: 1.17.1
          terraform_wrapper: false
      - run: terraform fmt -check -recursive
      - run: terraform init -input=false
      - run: terraform validate
      - run: terraform test
      - run: terraform plan -input=false -out=tfplan
      - run: terraform show -json tfplan > plan.json
      - run: python3 policy.py plan.json
      - name: Comment the plan on the PR
        if: github.event_name == 'pull_request'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          terraform show -no-color tfplan > plan.txt
          gh pr comment ${{ github.event.pull_request.number }} --body-file plan.txt
      - uses: actions/upload-artifact@v7
        with:
          name: tfplan
          path: infra/tfplan
          retention-days: 1

  apply:
    if: github.event_name == 'push'
    needs: plan
    runs-on: ubuntu-latest
    environment: production  # required reviewers approve here
    concurrency:
      group: terraform-prod
      cancel-in-progress: false  # never kill a running apply
    defaults:
      run:
        working-directory: infra
    steps:
      - uses: actions/checkout@v7
      - uses: hashicorp/setup-terraform@v4
        with:
          terraform_version: 1.17.1
          terraform_wrapper: false
      - uses: actions/download-artifact@v8
        with:
          name: tfplan
          path: infra
      - run: terraform init -input=false
      - run: terraform apply -input=false tfplan

concurrency দুটি apply-কে একসাথে চলতে দেয় না। environment: production-এ GitHub-এর required reviewer বসালে job সেখানে অপেক্ষা করে। Apply ধাপে কোনো নতুন plan নেই, আর -auto-approve-ও নেই, কারণ saved plan apply করতে প্রশ্ন লাগে না।

একটি সূক্ষ্ম ফাঁক আছে: PR-এ দেখা plan আর merge-এর পরের plan দুটি আলাদা run. তাই reviewer-কে production গেটেও plan দেখতে হয়। Atlantis-এর মতো tool অন্য পথ নেয়: PR-এর ভেতরেই comment দিয়ে apply করে, আর apply শেষে merge হয়।

Plan-ও code চালায়। terraform plan provider চালায়, আর external data source-এর মতো জিনিস যেকোনো প্রোগ্রাম চালাতে পারে। তাই অচেনা fork-এর PR-এ production credential দিয়ে কখনো plan চালাবেন না। PR-এর plan-এর জন্য read-only role দিন, আর apply-এর জন্য আলাদা role.

Saved plan কেন, আর সেটি কখন বাসি হয়

ভাবুন, আপনি ফ্রিজ দেখে বাজারের ফর্দ বানালেন। আপনার ভাই ইতোমধ্যে ডিম কিনে এনেছেন। পুরোনো ফর্দ ধরে গেলে ডিম দ্বিগুণ হবে। plan -out হলো সেই ফর্দ। Saved plan-এর ভেতরে লেখা থাকে কোন state দেখে এটি বানানো। State-এর serial নম্বর মিলছে না মানে কেউ এর মধ্যে বদল করেছে, আর plan-টি হয়ে গেছে stale. আসল error নিচে, serial-এর সংখ্যাগুলো বোঝার জন্য ধরা।

State আর দুটি ফাইলের হিসাব
কে কাজ করছেন
State serial
a.tfplan যে serial দেখেছে
Deployment-এর label
replicas
  1. শুরু। State-এর serial ৭. Deployment চলছে ২টি replica-য়, আর সব মিলে আছে।
  2. রিনার plan. রিনা replica ৩ করতে plan -out=a.tfplan চালালেন। ফাইলটি মনে রাখল, সে serial ৭ দেখে বানানো। Review-এর অপেক্ষায় রইল।
  3. সোহানের apply. এর মধ্যে সোহান একটি label যোগ করে apply করলেন। প্রতিটি লেখায় serial বাড়ে, তাই এখন ৮.
  4. রিনার apply ব্যর্থ। রিনা apply a.tfplan চালালেন। Terraform দেখল ফাইলের serial ৭, আর state-এর ৮. কিছু না ছুঁয়েই থেমে গেল। এটি বাগ নয়, এটিই সুরক্ষা।
  5. নতুন plan. রিনাকে আবার plan করতে হবে। নতুন ফাইল serial ৮ দেখবে, label সহ পুরো ছবি পাবে, আর আবার review হবে।
$ terraform plan -out=a.tfplan -var replicas=3
…
Plan: 0 to add, 1 to change, 0 to destroy.
Saved the plan to: a.tfplan
$ terraform apply -auto-approve -var 'labels={app="bazar",owner="team-payments",tier="web"}'
…
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
$ terraform apply a.tfplan

Error: Saved plan is stale

The given plan file can no longer be applied because the state was changed by
another operation after the plan was created.
$ echo $?
1

Plan-এর ঝুঁকি মাপা

সব plan-এ সমান মনোযোগ দেওয়া যায় না। তাই অনেক দল delete আর replace-এর সংখ্যা থেকে একটি risk score বানায়। নিচের ওজনগুলো আমাদের ধরে নেওয়া, কোনো মান (standard) নয়। আসল plan JSON-এ ["delete","create"] আর ["create","delete"] (create_before_destroy থাকলে) গুনলেই replace-এর সংখ্যা মেলে।

// Plan risk score from counts. The weights are our assumption, not a standard.
const weight = { create: 1, update: 2, replace: 8, delete: 10 };
const envFactor = { dev: 1, staging: 2, prod: 4 };
function score(p) {
  const base = p.create * weight.create + p.update * weight.update +
    p.replace * weight.replace + p.delete * weight.delete;
  const stateful = p.statefulTouched * 25;      // database or storage replaced/deleted
  return (base + stateful) * envFactor[p.env];
}
function band(s) {
  if (s < 20) return "low";
  if (s < 80) return "medium";
  return "high";
}
const plans = [
  { name: "new tag on 12 resources", env: "prod", create: 0, update: 12, replace: 0, delete: 0, statefulTouched: 0 },
  { name: "add 3 subnets",           env: "prod", create: 3, update: 1,  replace: 0, delete: 0, statefulTouched: 0 },
  { name: "replace 1 service",       env: "prod", create: 0, update: 1,  replace: 1, delete: 0, statefulTouched: 0 },
  { name: "replace 1 database",      env: "prod", create: 0, update: 0,  replace: 1, delete: 0, statefulTouched: 1 },
  { name: "same database in dev",    env: "dev",  create: 0, update: 0,  replace: 1, delete: 0, statefulTouched: 1 },
  { name: "delete 40 old objects",   env: "staging", create: 0, update: 0, replace: 0, delete: 40, statefulTouched: 0 },
];
console.log("score  band    plan");
for (const p of plans) {
  const s = score(p);
  console.log(String(s).padStart(5) + "  " + band(s).padEnd(6) + "  " + p.name + " (" + p.env + ")");
}

আউটপুট:

score  band    plan
   96  high    new tag on 12 resources (prod)
   20  medium  add 3 subnets (prod)
   40  medium  replace 1 service (prod)
  132  high    replace 1 database (prod)
   33  medium  same database in dev (dev)
  800  high    delete 40 old objects (staging)

হাতে মিলিয়ে দেখুন: database-এর base ৮, stateful ২৫, মোট ৩৩; prod-এ ×৪ = ১৩২. এবার প্রথম সারি দেখুন। ১২টি tag যোগ একটি নিরীহ কাজ, অথচ score ৯৬. শুধু সংখ্যা গুনলে এমন ভুল হবেই। তাই score সিদ্ধান্ত নেয় না, শুধু ঠিক করে কতটা মনোযোগ লাগবে। চাইলে update-এর ওজন ১ করে আবার চালান।

শেষ গেট: pass, review না block

আগের সব ফল এবার একটি জায়গায় আসে। গেটের যুক্তি ইচ্ছা করেই সরল: কঠিন ব্যর্থতা আটকে দেয়, সন্দেহজনক জিনিস মানুষের কাছে পাঠায়, বাকিটা এগিয়ে যায়।

// A pipeline gate: every input is a fact from an earlier stage.
function gate(r) {
  const block = [], review = [];
  if (!r.fmtOk || !r.validateOk) block.push("format or validate failed");
  if (r.testsFailed > 0) block.push(r.testsFailed + " test failed");
  if (r.policyDenies > 0) block.push(r.policyDenies + " policy deny");
  if (r.criticalFindings > 0) block.push(r.criticalFindings + " critical finding");
  if (r.stateSerial !== r.planSerial) block.push("saved plan is stale");
  if (r.deletesProtected > 0) block.push("deletes a protected resource");
  if (r.risk >= 80) review.push("risk " + r.risk);
  if (r.costDelta >= 100) review.push("cost +$" + r.costDelta + "/month");
  if (r.env === "prod") review.push("prod needs a human");
  if (block.length) return "BLOCK  " + block.join("; ");
  if (review.length) return "REVIEW " + review.join("; ");
  return "PASS   safe to apply the saved plan";
}
const ok = { fmtOk: true, validateOk: true, testsFailed: 0, policyDenies: 0, criticalFindings: 0,
  deletesProtected: 0, stateSerial: 42, planSerial: 42, risk: 8, costDelta: 0, env: "dev" };
const cases = [
  ["small dev change", {}],
  ["same change in prod", { env: "prod", risk: 32 }],
  ["costly scale-up", { env: "staging", risk: 24, costDelta: 360 }],
  ["public bucket", { env: "prod", policyDenies: 1 }],
  ["state moved after plan", { env: "prod", stateSerial: 43 }],
  ["database replace", { env: "prod", risk: 132, deletesProtected: 1 }],
  ["format and test fail", { fmtOk: false, testsFailed: 1 }],
];
for (const [name, patch] of cases) {
  console.log(name.padEnd(24) + gate({ ...ok, ...patch }));
}

আউটপুট:

small dev change        PASS   safe to apply the saved plan
same change in prod     REVIEW prod needs a human
costly scale-up         REVIEW cost +$360/month
public bucket           BLOCK  1 policy deny
state moved after plan  BLOCK  saved plan is stale
database replace        BLOCK  deletes a protected resource
format and test fail    BLOCK  format or validate failed; 1 test failed

Block-এর তালিকা আগে দেখা হয়, তাই ঝুঁকি কম হলেও একটি policy deny সব থামায়। Prod সবসময় অন্তত review-তে যায়। শেষ সারিতে দুটি কারণই একসাথে জানানো হয়েছে, যেন লেখক এক push-এই সব ঠিক করতে পারেন।

৮ · Destroy সুরক্ষা, drift আর খরচ

Destroy সুরক্ষার স্তর

Terraform-এর নিজের সুরক্ষা হলো prevent_destroy. একটি ConfigMap-এ এটি বসিয়ে পুরো configuration-এ terraform destroy চালালাম:

resource "kubernetes_config_map_v1" "settings" {
  metadata {
    name      = "bazar-settings"
    namespace = "abcl-tp"
  }
  data = { currency = "BDT" }

  lifecycle {
    prevent_destroy = true
  }
}
$ terraform destroy -auto-approve
…
Plan: 0 to add, 0 to change, 3 to destroy.

Error: Instance cannot be destroyed

  on main.tf line 29:
  29: resource "kubernetes_config_map_v1" "settings" {

Resource kubernetes_config_map_v1.settings has lifecycle.prevent_destroy set,
but the plan calls for this resource to be destroyed. To avoid this error and
continue with the plan, either disable lifecycle.prevent_destroy or reduce
the scope of the plan using the -target option.
$ echo $?
1

একটি resource সুরক্ষিত ছিল, তবু বাকি দুটিও মুছল না, কারণ পুরো plan-ই ব্যর্থ। এবার ফাঁকটি দেখুন। Resource-এর পুরো ব্লকটিই ফাইল থেকে মুছে plan চালালাম:

  # kubernetes_config_map_v1.settings will be destroyed
  # (because kubernetes_config_map_v1.settings is not in configuration)
…
Plan: 0 to add, 1 to change, 1 to destroy.

কোনো error নেই, exit code ০. নিয়মটি ব্লকের ভেতরে ছিল, ব্লকের সাথেই চলে গেছে। অর্থাৎ prevent_destroy শুধু দুর্ঘটনা আটকায়, উদ্দেশ্যমূলক মুছে ফেলা নয়। তাই এটি Terraform-এর বাইরের সুরক্ষার বিকল্প নয়।

গুরুত্বপূর্ণ database-এ তাই স্তর বসান। নিচের HCL একটি নমুনা, চালানো হয়নি। Provider-ভেদে নাম আলাদা: AWS RDS আর Google Cloud SQL-এ deletion_protection ধরনের setting আছে, Azure-এ resource lock আছে। ভরসার শেষ স্তর হলো পর্ব ২৪-এর backup, আর মাঝে মাঝে তা ফিরিয়ে এনে যাচাই করা।

resource "aws_db_instance" "orders" {
  # ...engine, size and network omitted...
  deletion_protection       = true  # the cloud API itself refuses delete
  skip_final_snapshot       = false
  final_snapshot_identifier = "orders-final"
  backup_retention_period   = 7

  lifecycle {
    prevent_destroy = true          # Terraform refuses at plan time
  }
}

Drift check: সূচি ধরে plan

Drift পর্ব ৫৩-এর বিষয়। এখানে দেখব এটিকে পরীক্ষার ধাপ কীভাবে বানাবেন। -detailed-exitcode দিলে plan-এর exit code তিন রকম: ০ মানে কোনো বদল নেই, ১ মানে error, ২ মানে বদল আছে। হাতে kubectl scale চালিয়ে replica ২ থেকে ৪ করার পরের আসল run:

$ kubectl scale deploy bazar-api --replicas=4
deployment.apps/bazar-api scaled
$ terraform plan -refresh-only -detailed-exitcode
…
Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:

  # kubernetes_deployment_v1.api has changed
  ~ resource "kubernetes_deployment_v1" "api" {
…
      ~ spec {
          ~ replicas                  = "2" -> "4"
…
$ echo $?
2

একটি নির্ধারিত job রাতে এটি চালাতে পারে। Exit code ২ এলে শুধু alert দেবে, নিজে থেকে apply করবে না, কারণ হাতের বদলটি হয়তো কোনো incident সামলাতে করা হয়েছিল। মাঝের exit code ২-কে CI অনেক সময় ব্যর্থতা ধরে, তাই script-এ এটি আলাদা করে দেখতে হয়। -refresh-only কিছু বদলায় না, শুধু আসল অবস্থা পড়ে জানায়।

খরচও একটি পরীক্ষা

Infracost plan পড়ে provider-এর দামের তালিকা মিলিয়ে PR-এ মাসিক খরচের পার্থক্য দেখায়। এটি এই পর্বে চালানো হয়নি, তাই শুধু ধারণাটি বলি। হিসাবটি হাতেও করা যায়। ধরে নিই একটি VM-এর ভাড়া ঘণ্টায় $০.১০, আর মাস ৩০ দিনের:

৩টি থেকে ৮টি VM → বাড়তি ৫টি
৫ × $০.১০ × ৭২০ ঘণ্টা = মাসে +$৩৬০

এই সংখ্যাই গেটে costDelta: 360 হিসেবে গিয়েছিল। দাম সবসময় provider-এর pricing পাতায় মিলিয়ে নিন। এ ধরনের tool ব্যবহার-নির্ভর খরচ (data transfer, request) ঠিকমতো জানতে পারে না, যদি না ব্যবহারের অনুমান আপনি দেন। পুরো হিসাব পর্ব ৭২ আর ৭৩-এ, আর budget alert থাকলে ভুল apply-এর খরচও দ্রুত চোখে পড়ে।

৯ · পরীক্ষার environment, preview stack আর দুর্ঘটনার শিক্ষা

Integration test-এর জন্য আলাদা জায়গা লাগে। আদর্শ হলো একটি আলাদা sandbox account বা project, যেখানে কড়া budget আর সীমিত অনুমতি আছে। Production-এর পাশে test চালালে test-এর ভুল সরাসরি production-এ লাগতে পারে।

Ephemeral preview stack মানে প্রতিটি PR-এর জন্য ক্ষণস্থায়ী একটি পুরো পরিবেশ। PR খুললে তৈরি, বন্ধ করলে মুছে যায়। বিয়েবাড়ির প্যান্ডেলের মতো: অনুষ্ঠানের জন্য বসে, শেষে খুলে নেওয়া হয়। কিন্তু খুলে নিতে ভুলে গেলে ভাড়া চলতেই থাকে।

  • নাম: প্রতিটি resource-এর নামে PR নম্বর রাখুন, যেমন bazar-pr-57, যাতে দুই PR ধাক্কা না খায়। Module-এর variable দিয়েই এটি হয়।
  • Tag: ttl=2d আর owner tag বসান। একটি নির্ধারিত job মেয়াদোত্তীর্ণ জিনিস খুঁজে মুছবে। Workflow মাঝপথে ব্যর্থ হলে এই job-ই শেষ ভরসা।
  • মাপ: ছোট instance আর ছোট database. শুধু test-এর ডেটা, production-এর আসল ডেটা কখনো নয়।
  • Clean-up: PR বন্ধ হলে terraform destroy. তারপর পরীক্ষা করুন যে সত্যিই কিছু বাকি নেই। prevent_destroy বা deletion_protection পরিষ্কারের পথ আটকাতে পারে, তাই test environment-এ এগুলো variable দিয়ে সামলান।
  • Provider-এর সীমা: কিছু জিনিস তৈরি হতে অনেক সময় নেয় বা quota-য় আটকায়। এগুলো preview-তে না বানিয়ে একটি ভাগ করা সংস্করণ ব্যবহার করুন।

বাস্তব দুর্ঘটনা থেকে যা নেওয়ার

নিচের ঘটনাগুলো কোম্পানিগুলোর প্রকাশিত বিবরণ বা সরকারি নথি থেকে, সংক্ষেপে। সেগুলো Terraform-এর ঘটনা নয়, কিন্তু শিক্ষা এখানেই খাটে।

ঘটনাপ্রকাশিত বিবরণ অনুযায়ীIaC-তে শিক্ষা
Amazon S3, ফেব্রুয়ারি ২০১৭একটি কমান্ডের ইনপুট ভুল হওয়ায় উদ্দেশ্যের চেয়ে অনেক বেশি server সরে যায়। AWS পরে tool-এ সীমা বসায়।মুছে ফেলার সংখ্যায় সীমা রাখুন, blast radius ছোট করুন।
Knight Capital, আগস্ট ২০১২SEC-এর নথি অনুযায়ী নতুন code আটটির মধ্যে একটি server-এ পৌঁছায়নি; প্রায় ৪৫ মিনিটে ক্ষতি কয়েকশো মিলিয়ন ডলার।হাতে deploy আর server-এর অমিল, অর্থাৎ drift, যাচাই ছাড়া ছাড়বেন না।
Facebook, অক্টোবর ২০২১Meta জানায়, একটি কমান্ড backbone সংযোগ নামিয়ে দেয়, আর যে audit tool থামানোর কথা, তার বাগে সেটি থামেনি।গেটও সফটওয়্যার, গেটেরও পরীক্ষা লাগে। Policy-র test লিখুন।
খোলা object storage, বহু ঘটনাবিভিন্ন সংবাদে ভুল সেটিংয়ের কারণে ডেটা ফাঁসের খবর এসেছে। AWS ২০১৮ সালে Block Public Access আনে, আর ২০২৩ সাল থেকে নতুন bucket-এ এটি ডিফল্ট চালু।নিরাপদ ডিফল্ট আর plan-এর policy একসাথে রাখুন।

আরও গভীরে: OPA-র মডেল হলো সিদ্ধান্তকে কার্যকর করা থেকে আলাদা করা: আপনার system প্রশ্ন পাঠায়, OPA উত্তর দেয়। একে অনেকে PDP (decision point) আর PEP (enforcement point) ভাগ বলেন, যা XACML-এর মতো পুরোনো access-control মান থেকে এসেছে। Plan JSON-এর after_unknown অংশ জানায় কোন মান apply-এর আগে অজানা; এই মানের ওপর নিয়ম বসালে policy হয় ভুল করে পাস করে, নয় ভুল করে আটকায়। Terraform-এর JSON আউটপুটের নিজস্ব format_version আছে, তাই policy tool আপগ্রেডের আগে আপনার নিয়মগুলোর test চালানো জরুরি।

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

  • check আর precondition-এর পার্থক্য? check ব্যর্থ হলে শুধু warning; precondition ব্যর্থ হলে error, resource তৈরি হয় না।
  • Plan JSON কেন? HCL-এ variable আর module গোঁজা থাকে; plan-এ চূড়ান্ত মান থাকে, তাই policy বেশি নির্ভুল।
  • Saved plan কেন apply করবেন? যা review হয়েছে ঠিক তাই চলবে; state বদলালে Terraform stale বলে থামবে।
  • prevent_destroy কি যথেষ্ট? না। ব্লক মুছলেই নিয়ম চলে যায়; provider-এর deletion protection আর backup লাগে।
  • Mock test-এর সীমা কী? এটি আপনার যুক্তি দেখে, আসল API-র আচরণ নয়; তাই অল্প কিছু integration test-ও লাগে।

১০ · AI-এর যুগে IaC-র পরীক্ষা

AI কয়েক সেকেন্ডে শত লাইন HCL লিখে দেয়, আর এই পর্বের সব ধাপ ঠিক সেই লেখা যাচাইয়ের জন্যই কাজে লাগে। আমরা শুরুতেই দেখেছি, দেখতে বিশ্বাসযোগ্য file_mode আসলে কোনো argument নয়। AI-এর লেখা policy-ও পরীক্ষা ছাড়া নেবেন না। AI প্রায়ই খোলা 0.0.0.0/0, * অনুমতি, public storage, code-এ বসানো secret আর latest tag দেয়। সে খরচের কথা ভাবে না, আর সতর্কতা ছাড়াই terraform destroy বা rm -rf বলে দিতে পারে।

AI-কে কোড লিখতে বলার সময় যাচাইয়ের দায়িত্বও ভাগ করে দিন। শেষ দুটি লাইনই আসল কাজ করে।

তুমি একজন সতর্ক platform engineer. নিচের plan JSON-এর একটি অংশ আর আমাদের নিয়ম দিচ্ছি: নিয়ম: প্রতিটি Deployment-এ owner label থাকবে; replicas ৬-এর বেশি নয়; Service-এর type LoadBalancer নয়। [এখানে plan JSON-এর অংশ দিন, secret বা আসল account id মুছে] ১. Python standard library দিয়ে একটি policy script লেখো যা resource_changes পড়ে আর deny থাকলে exit 1 দেয়। ২. "after" null হলে কী হবে, আর replicas string হলে কী হবে, দুটোই সামলাও। ৩. যে ক্ষেত্রে নিশ্চিত নও, সেগুলো আলাদা করে বলো। কোনো argument বা JSON key বানিয়ে লিখবে না। ৪. Script যাচাইয়ের জন্য একটি পাস করা আর একটি ব্যর্থ নমুনা দাও, আর প্রতিটির প্রত্যাশিত আউটপুট লেখো। ৫. কোনো destroy বা apply কমান্ড চালাতে বলবে না।

AI-এর লেখা HCL হাতে পেলে

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

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

প্র ০১Plan review মানুষের হাতে রাখলেই কি নিরাপদ? যন্ত্র সব পরীক্ষা করে দিলে মানুষের কাজটি আসলে কী?

“একজন মানুষ দেখেছেন” কথাটি শুনতে ভরসার, কিন্তু মনোযোগ সীমিত। ১৯৮৩ সালে Lisanne Bainbridge তাঁর “Ironies of Automation” প্রবন্ধে একটি পরিচিত সমস্যা দেখান: যন্ত্র বেশিরভাগ কাজ করলে মানুষের ভাগে পড়ে নজরদারি, অথচ দীর্ঘ সময় স্বাভাবিক কিছু দেখতে দেখতে মানুষ অস্বাভাবিকটি চিনতে কম পারে। Plan review-তে এটি রোজের ঘটনা। প্রতিদিন দশটি নিরীহ plan দেখলে এগারোতম plan-এর একটি -/+ চোখে পড়ে না।

আরেকটি ফাঁদ হলো দ্রুততার চাপ। বারবার অপেক্ষা করতে হলে approval একটি অভ্যাসের ক্লিক হয়ে যায়। তখন গেটটি নিরাপত্তা না দিয়ে শুধু ধীর করে।

তাই কাজ ভাগ করতে হয়। যা নিয়মে বাঁধা যায়, তা যন্ত্রের: label আছে কি না, port খোলা কি না, কতগুলো জিনিস মুছছে। মানুষের জন্য থাকে যা নিয়মে বাঁধা যায় না: “এই বদলটি কি PR-এর উদ্দেশ্যের সাথে মেলে?”, “এই সময়ে এটি করা ঠিক?”, “ব্যর্থ হলে ফেরার পথ কী?” একটি সহজ কৌশল হলো plan-এর সারাংশ ওপরে দেখানো: কতগুলো delete, replace, কোন stateful resource. নিরীহ plan স্বয়ংক্রিয়ভাবে পার হলে মানুষের মনোযোগ বাঁচে ঝুঁকির plan-এর জন্য।

তবু স্বয়ংক্রিয় পাসে সতর্ক থাকুন, কারণ risk score নিজেও ভুল করে। আমরা দেখেছি ১২টি tag-এর score বড় হয়ে গেল। উল্টো দিকে একটি সামান্য security group বদলের score ছোট হলেও পুরো সার্ভিসকে অচল করতে পারে। তাই score-এর ওজন নিয়মিত পুরোনো incident-এর সাথে মিলিয়ে ঠিক করা ভালো।

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

প্র ০২IaC-তে mock test আর আসল integration test-এর ভাগ কেমন হওয়া উচিত? Software-এর test pyramid কি এখানে হুবহু খাটে?

Mike Cohn তাঁর Succeeding with Agile (২০০৯) বইয়ে test pyramid জনপ্রিয় করেন: অনেক unit test, কিছু integration test, অল্প end-to-end test. সাধারণ software-এ এটি ভালো খাটে, কারণ সেখানে আপনার নিজের যুক্তি অনেক। Terraform code-এ যুক্তি প্রায়ই পাতলা। বেশিরভাগ লাইন “এই setting-টি এই মান” বলে। এমন code-এ mock test লিখলে অনেক সময় আপনি একই কথা দুবার লেখেন, আর test শুধু code-এর আয়না হয়ে যায়।

Mock কোথায় সত্যিই দামি? যেখানে module-এ শর্ত, loop, হিসাব বা নিয়ম আছে। যেমন আমাদের prod-এর “কমপক্ষে ২” নিয়ম। সেখানে এক সেকেন্ডের test আসল বাগ ধরেছে, আর কোনো খরচ হয়নি। expect_failures দিয়ে validation বেঁচে আছে কি না, সেটিও mock-এ দেখা যায়।

আসল ঝুঁকি থাকে অন্য জায়গায়: IAM অনুমতি কম কি না, দুটি resource-এর ক্রম ঠিক কি না, provider-এর আসল ডিফল্ট কী, region-এ জিনিসটি পাওয়া যায় কি না। Mock এগুলো জানে না, কারণ সে শুধু নকল মান ফেরত দেয়। তাই IaC-র আকৃতি pyramid-এর চেয়ে একটু মোটা মাঝখানের মতো: scanner আর policy-র চওড়া ভিত, তার ওপরে কিছু লক্ষ্য করা integration test.

ইতিহাসও এটি দেখায়। Gruntwork-এর Terratest (Go-তে লেখা) অনেক বছর ধরে আসল infrastructure বানিয়ে পরীক্ষা করার জনপ্রিয় পথ ছিল। ২০২৩ সালে terraform test আর ২০২৪ সালে mock আসায় HCL-এই সহজ test লেখা গেল, কিন্তু আসল পরীক্ষার দরকার ফুরায়নি। Integration test-এর দাম আছে: মিনিট লাগে, টাকা লাগে, আর মাঝপথে থেমে গেলে জিনিস পড়ে থাকে। তাই এগুলো প্রতিটি PR-এ না চালিয়ে module বদলালে বা রাতে চালানো সাধারণ রীতি।

মূল উপলব্ধি: Test-এর আকৃতি ঠিক করে আপনার ঝুঁকি কোথায়। যুক্তির ঝুঁকিতে mock, cloud-এর আচরণের ঝুঁকিতে আসল পরীক্ষা, আর দুটির ওপরে সস্তা scanner.

প্র ০৩CI-তে policy থাকলে cloud account-এর guardrail কি বাড়তি? একই নিয়ম একাধিক জায়গায় রাখা অপচয় নয় কি?

Security-তে একটি পুরোনো কথা আছে: যে নিয়ম একটি মাত্র দরজায় পাহারা দেয়, সেটি অন্য দরজা দিয়ে এড়ানো যায়। CI-র policy শুধু সেই পরিবর্তন দেখে যা CI দিয়ে আসে। কেউ console-এ হাতে bucket খুলে দিলে, অন্য একটি script চালালে, বা incident-এর সময় সরাসরি CLI ব্যবহার করলে CI কিছুই জানবে না।

S3-এর ইতিহাস ভালো উদাহরণ। খোলা bucket থেকে ডেটা ফাঁসের অনেক খবর আসার পর AWS ২০১৮ সালে Block Public Access যোগ করে, যা account স্তরে বসিয়ে ভুল bucket setting-কেও অকেজো করে দেওয়া যায়। পরে ২০২৩ সাল থেকে নতুন bucket-এ এটি ডিফল্টভাবে চালু হয়। অর্থাৎ শেষ পর্যন্ত সমস্যাটি সবচেয়ে নিচের স্তরে নিরাপদ ডিফল্ট দিয়ে কমানো হলো, মানুষকে আরও সতর্ক হতে বলে নয়।

তাহলে নিচের guardrail থাকলে CI-র policy কেন? কারণ অভিজ্ঞতার পার্থক্য। Organization policy যখন apply-এর মাঝখানে ব্যর্থ হয়, তখন আধা-তৈরি অবস্থা থাকে, আর error প্রায়ই অস্পষ্ট AccessDenied. CI-র policy একই কথা PR-এ, plan-এর আগেই, পরিষ্কার ভাষায় বলে। একটি লেখককে শেখায়, অন্যটি সিস্টেমকে রক্ষা করে।

এটিই defense in depth, যা পর্ব ৩৪-এ দেখেছেন। তবে সত্যিকারের অপচয়ও আছে: একই নিয়ম তিনটি ভাষায় তিনবার লেখা, যেগুলো ধীরে ধীরে আলাদা হয়ে যায়। সমাধান হলো একটি জায়গায় নিয়মের অর্থ লিখে রাখা, প্রতিটি নিয়মের একটি মালিক আর একটি ID রাখা, আর detection-এর জন্য চলমান সিস্টেমেও একটি পাহারা রাখা। Kubernetes-এ এই ভূমিকা admission control নেয়।

মূল উপলব্ধি: বাঁয়ের পরীক্ষা দ্রুত জানায়, ডানের পরীক্ষা নিশ্চয়তা দেয়। একই নিয়ম দুই জায়গায় থাকা অপচয় নয়, যদি দুটির কাজ আলাদা হয়।

প্র ০৪Policy খুব কড়া হলে দলগুলো পথ এড়াতে শুরু করে। ব্যতিক্রম আর কঠোরতার ভারসাম্য কীভাবে রাখবেন?

ধরুন একটি নতুন scanner প্রথম দিনেই ৪০০টি সতর্কতা দিল আর pipeline বন্ধ করে দিল। দুই সপ্তাহ পরে কী হবে সবাই জানেন: কেউ || true বসাবেন, কেউ পুরো নিয়ম skip করবেন, আর কেউ console-এ চলে যাবেন। নিয়ম খাতায় থাকবে, নিরাপত্তা থাকবে না। তাই policy একটি সামাজিক ব্যবস্থাও, শুধু প্রযুক্তি নয়।

একটি প্রমাণিত পথ হলো ধাপে ধাপে চালু করা। Sentinel-এর advisory, soft-mandatory, hard-mandatory স্তরগুলো এই চিন্তা থেকেই। Kyverno-তে এই একই চিন্তা audit আর enforce মোডে দেখা যায়, আর Gatekeeper-এ dry-run ও deny-তে। প্রথমে শুধু দেখুন কতগুলো সতর্কতা আসে আর কতগুলো ভুল। ভুলগুলো ঠিক করুন, পুরোনো জিনিসের তালিকা বানান, নতুন জিনিসে আগে কঠোর হোন, তারপর পুরোনোতে।

Exception-কে লুকিয়ে রাখবেন না, code-এ আনুন। একটি ভালো exception-এ থাকে নির্দিষ্ট resource, নির্দিষ্ট নিয়ম, কারণ, অনুমোদনকারী আর মেয়াদ। আমাদের simulator-এর until ক্ষেত্রটি ঠিক এই কাজ করে। মেয়াদ না থাকলে “সাময়িক” ছাড় চিরস্থায়ী হয়ে যায়। Exception-এর সংখ্যা একটি ভালো মাপকাঠিও: কোনো নিয়মে খুব বেশি exception মানে নিয়মটিই হয়তো ভুল।

আর গেটের নিজের বাগও ভুললে চলবে না। ২০২১ সালের Facebook outage-এর বিবরণে Meta লেখে, ভুল কমান্ড থামানোর জন্য একটি audit tool ছিল, কিন্তু তার বাগের কারণে সেটি কাজ করেনি। তাই policy-র জন্যও test চাই: একটি ভালো plan যা পাস করবে, একটি খারাপ plan যা ব্যর্থ হবে। OPA-তে opa test আর Conftest-এ conftest verify দিয়ে নিয়মের unit test লেখা যায়। ভাঙা গেট আর গেট না থাকা প্রায় একই, বরং আগেরটি বেশি বিপজ্জনক, কারণ সবাই ভরসা করে থাকে।

মূল উপলব্ধি: ভালো policy মানুষকে সহজ ও নিরাপদ পথ দেখায়। সে পথ কঠিন হলে মানুষ অন্য পথ খুঁজবে। পরিমাপ করুন, ধাপে ধাপে কঠোর করুন, আর ব্যতিক্রমের মেয়াদ রাখুন।

প্র ০৫Merge-এর আগে apply, নাকি merge-এর পরে apply? দুই পক্ষের যুক্তি কী, আর এটি GitOps-এর সাথে কীভাবে মেলে?

এটি IaC দলের একটি ক্লাসিক তর্ক। সাধারণ software-এ merge আগে, deploy পরে। Infrastructure-এ সমস্যা হলো, apply ব্যর্থ হতে পারে এমন কারণে যা plan দেখেনি: quota শেষ, নাম আগে থেকেই আছে, permission কম। তখন main বলে একটি কথা, আর আসল সিস্টেম আরেক অবস্থায়।

Apply-before-merge: Atlantis এই ধারা জনপ্রিয় করে। এটি Hootsuite-এ তৈরি হয়ে ২০১৭ সালে open source হয়। PR-এ comment করলে plan হয়, approval-এর পরে comment করলে apply হয়, সফল হলে merge. সুবিধা: main সবসময় যা সত্যিই চলেছে তাই। অসুবিধা: দুটি PR একই জিনিসে হাত দিলে দ্বন্দ্ব হয়। তাই Atlantis নিজে project ধরে lock রাখে। আর merge না হওয়া code production-এ চলে গেছে, এই অস্বস্তিও থাকে।

Apply-after-merge: এতে Git-এর ইতিহাস পরিষ্কার, আর branch protection সহজে কাজ করে। কিন্তু apply ব্যর্থ হলে main-ই ভাঙা, তখন একটি নতুন PR লাগে। PR-এর plan আর merge-এর পরের plan আলাদা হতে পারে, কারণ এর মধ্যে অন্য PR ঢুকেছে। আমাদের workflow-এ তাই merge-এর পরে নতুন plan হয়, আর production গেটে সেটিই দেখতে হয়।

দুই পথেই saved plan কাজে লাগে, আর stale plan-এর সুরক্ষা ঝুঁকি কমায়। ছোট দলে apply-after-merge আর একটি মাত্র apply queue সহজ। অনেক দল ও অনেক state থাকলে PR-ভিত্তিক automation বেশি কাজে দেয়।

পর্ব ৬০-এ দেখবেন GitOps আরেক ধাপ এগিয়ে যায়: একটি agent নিয়মিত Git-এর সাথে চলমান অবস্থা মেলায়, আর main-ই একমাত্র সত্য। Kubernetes-এ এই reconcile loop স্বাভাবিক, কারণ cluster নিজেই এই মডেলে চলে। Terraform-এ state আর plan-এর ধাপ থাকায় এটি হুবহু খাটে না, যদিও একই লক্ষ্যে কিছু controller-ভিত্তিক tool তৈরি হয়েছে।

মূল উপলব্ধি: দুটি পথই ঠিক, শুধু ঝুঁকি আলাদা জায়গায় থাকে। আসল প্রশ্ন: ব্যর্থ apply-এর পরে Git আর সিস্টেমের অমিল আপনি কোথায় সইতে রাজি?

অনুশীলন

প্রথম তিনটি কাগজে-কলমেই হয়। চতুর্থটিতে কোনো cloud account লাগে না।

  1. ঝুঁকির হিসাব (সংখ্যাগত): ওজন ধরুন create ১, update ২, replace ৮, delete ১০; stateful resource replace বা delete হলে প্রতিটিতে বাড়তি ২৫; পরিবেশের গুণিতক dev ১, staging ২, prod ৪. একটি prod plan-এ ২টি create, ৩টি update, ১টি replace আছে, আর কোনো stateful resource নেই। Score কত? Replace-টি database হলে কত হবে? ২০-এর নিচে low, ৮০-এর নিচে medium, ৮০ বা তার ওপরে high ধরলে দুই ক্ষেত্রে band কী?

    Base = ২×১ + ৩×২ + ১×৮ = ২ + ৬ + ৮ = ১৬. Prod-এ ১৬ × ৪ = ৬৪, অর্থাৎ medium.

    Database হলে (১৬ + ২৫) × ৪ = ৪১ × ৪ = ১৬৪, অর্থাৎ high. একটি মাত্র শব্দের পার্থক্যে score আড়াই গুণের বেশি হলো। এটি ইচ্ছাকৃত, কারণ stateful জিনিস ফেরানো কঠিন।

  2. ফল আগে বলুন: একটি নতুন খালি state-এ পরপর এই কমান্ডগুলো চলল: (১) terraform plan -out=x.tfplan, যা ১টি resource তৈরি করবে; (২) terraform apply x.tfplan; (৩) terraform apply x.tfplan আবার; (৪) terraform plan -detailed-exitcode. প্রতিটির শেষে resource-এর সংখ্যা, আর (৩) ও (৪)-এর ফল কী হবে?

    (১)-এর পরে resource ০, কারণ plan কিছু তৈরি করে না। (২)-এর পরে resource ১, আর state-এর serial বেড়েছে। (৩) ব্যর্থ হবে, কারণ x.tfplan পুরোনো state দেখে বানানো: Saved plan is stale, exit code ১, resource তবু ১. একটি saved plan মাত্র একবার চলে। (৪)-এ code আর আসল অবস্থা মিলে গেছে, তাই No changes, exit code ০.

  3. Pipeline-এর সমালোচনা: একটি দলের workflow: main-এ push হলে checkov -d . || true, তারপর terraform apply -auto-approve, প্রতিটি repository-তে একই admin key secret হিসেবে রাখা। State local ফাইলে, runner-এ। কোথায় কোথায় সমস্যা, আর আপনি কী বদলাবেন?
    • || true scanner-এর ব্যর্থতা গিলে ফেলে। পাহারাদার আছে, বাঁশি নেই। কারণসহ skip রাখুন।
    • plan -out নেই, PR review নেই, তাই কেউ জানে না কী হবে। PR-এ plan, policy আর approval বসান।
    • -auto-approve নতুন plan বানিয়েই চালায়, যা কেউ দেখেনি। শুধু saved plan apply করুন।
    • স্থায়ী admin key: একটি ফাঁসেই সব শেষ। OIDC-র স্বল্পমেয়াদি credential আর plan ও apply-এর আলাদা, সীমিত role নিন।
    • Runner-এ local state: runner মুছলে state হারায়, আর lock নেই। Lock-সহ remote backend ব্যবহার করুন (পর্ব ৫৩)।
    • Destroy সুরক্ষা, drift check আর test কিছুই নেই। মই-এর নিচ থেকে একে একে যোগ করুন।
  4. নিজে চালান (ঐচ্ছিক, খরচ নেই): Terraform ইনস্টল থাকলে একটি খালি ফোল্ডারে hashicorp/local provider দিয়ে একটি local_file বানান, যার content-এ var.env বসে। env-এ validation দিন, আর একটি .tftest.hcl-এ expect_failures দিয়ে ভুল মান পরীক্ষা করুন। শেষে কী মুছবেন?

    এই কাঠামোতে tests/env.tftest.hcl লিখুন:

    run "bad_env_rejected" {
      command = plan
    
      variables {
        env = "qa"
      }
    
      expect_failures = [var.env]
    }

    এরপর terraform init আর terraform test চালান। validation-এ contains(["dev", "prod"], var.env) থাকলে test পাস করবে। এই plan-ভিত্তিক test কিছু তৈরি করে না। আগে apply করে থাকলে ফোল্ডারে একটি ছোট ফাইল আছে; terraform destroy চালিয়ে, পরে ফোল্ডারটি মুছলেই পরিষ্কার। কোনো cloud account লাগে না, তাই খরচও নেই।

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

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

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

  1. terraform fmt -check আর validate
  2. terraform test আর scanner
  3. terraform plan -out=tfplan
  4. terraform show -json করে policy চালানো
  5. Plan-এ মানুষের approval
  6. terraform apply tfplan

Terraform-এর check ব্লকের assert ব্যর্থ হলে কী ঘটে?

  • Run সাথে সাথে error দিয়ে থামে
  • আগের apply স্বয়ংক্রিয়ভাবে rollback হয়
  • Warning দেখায়, কিন্তু run শেষ হয়
  • কিছুই দেখায় না

একটি resource-এ postcondition আছে, যা শুধু apply-এর পরে জানা যায় এমন একটি মান দেখে। Apply-এর পরে শর্তটি ব্যর্থ হলে resource-টির কী হয়?

  • Resource তৈরিই হয়নি
  • Resource থেকে যায়, কিন্তু run error দিয়ে থামে
  • Terraform resource-টি মুছে ফেলে
  • শুধু warning আসে, পরের সব কাজ চলে

Terraform plan-এর ওপর policy চালাতে terraform show -json tfplan কেন ব্যবহার করা হয়?

  • এতে plan দ্রুত তৈরি হয়
  • এটি সব secret মুছে ফেলে
  • JSON ফাইলটি পরে apply করার জন্য লাগে
  • Saved plan binary, আর JSON যন্ত্রে পড়া যায়

terraform plan -detailed-exitcode exit code ২ দিলে তার অর্থ কী?

  • Plan সফল, আর কিছু বদল বাকি আছে
  • Plan error দিয়ে ব্যর্থ হয়েছে
  • কোনো বদল নেই
  • Plan-এর পরিবর্তনগুলো apply হয়ে গেছে

একটি resource-এ prevent_destroy = true ছিল। কেউ পুরো resource ব্লকটি ফাইল থেকে মুছে PR খুললেন। Plan-এ কী ঘটবে?

  • Instance cannot be destroyed error আসবে
  • Plan-এ resource-টি destroy দেখাবে, কোনো error ছাড়াই
  • Resource থাকবে, শুধু state থেকে সরে যাবে
  • terraform validate ব্যর্থ হবে

দুজন প্রকৌশলীর একজন saved plan বানালেন, অন্যজন এর মধ্যে একই state-এ apply করলেন। প্রথমজন saved plan apply করলে কী ঘটবে?

  • Plan চলবে, দ্বিতীয়জনের বদল মুছে যাবে
  • Terraform নিজে থেকে নতুন plan বানিয়ে চালাবে
  • “Saved plan is stale” error দিয়ে থামবে
  • দুটি বদল স্বয়ংক্রিয়ভাবে মিশে যাবে

শব্দকোষ

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

মনে রাখুন

  1. পরীক্ষা মই-এর মতো: fmt, validate, scanner, plan ও policy, test, drift. সস্তা ধাপ আগে।
  2. validation ইনপুট, precondition আগের অনুমান, postcondition পরের ফল দেখে। check শুধু সতর্ক করে।
  3. Plan JSON policy-কে চূড়ান্ত মান দেখায়। এটি আর plan ফাইল গোপন তথ্যের মতো রাখুন।
  4. Apply করুন শুধু review করা saved plan. State বদলালে সেটি stale হয়।
  5. prevent_destroy ব্লক মুছলেই চলে যায়। Provider-এর সুরক্ষা আর যাচাই করা backup রাখুন।

পরের পর্বে: এতক্ষণ দেখেছেন infrastructure কীভাবে তৈরি ও পরীক্ষা হয়। কিন্তু তৈরি server-এর ভেতরের software আর setting কে সামলাবে? এবার Ansible, Packer আর immutable infrastructure.

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

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