Foundationপ্রথম নীতি থেকে
LEVEL 12লেসন ১৯/২২কঠিন৫০ মিনিট

Infrastructure as Code ও Terraform — Provisioning-এ Reconciliation Loop

Infrastructure as Code and Terraform: The Reconciliation Loop Applied to Provisioning

cloud-networking-vpc লেসন হাতে-কলমে VPC/subnet/security-group বানিয়েছে — কনসোলে ক্লিক করে, বা এক-এক করে CLI কমান্ড চালিয়ে। আজ প্রশ্ন: এই একই resource-গুলো যদি ডজন-ডজন environment জুড়ে, বারবার, নির্ভুলভাবে বানাতে হয়? Terraform আর তার HCL ভাষা উত্তর — একটা declarative বর্ণনা লিখুন, tool নিজে বের করুক কী করতে হবে। এই লেসনের কেন্দ্রীয় দাবি: terraform plan/apply cycle ঠিক সেই একই declarative-reconciliation প্যাটার্ন যা kubernetes-architecture লেসনে controller-manager-এ দেখা গেছে — শুধু domain বদলেছে, container scheduling থেকে cloud provisioning-এ।

এই লেসন শেষে আপনি পারবেন

  • Imperative আর declarative provisioning-এর মধ্যে নির্ভুল পার্থক্য — অর্ডারড কমান্ডের sequence বনাম desired end-state-এর ঘোষণা — concrete উদাহরণসহ ব্যাখ্যা করতে পারবেন
  • HCL কীভাবে resource-দের একটা dependency graph বর্ণনা করে (একটা resource block অন্য resource-এর attribute reference করলেই implicit edge তৈরি হয়), আর Terraform কীভাবে সেই graph topologically sort করে creation/destruction order নিজে থেকে বের করে তা ব্যাখ্যা করতে পারবেন
  • terraform plan আর terraform apply-র নির্ভুল সেমান্টিক্স — desired state বনাম Terraform-এর recorded state-এর diff (dry-run, কোনো side effect নেই) বনাম সেই diff কার্যকর করা — বর্ণনা করতে পারবেন
  • state file ঠিক কেন প্রয়োজন (cloud provider-কে নির্ভরযোগ্যভাবে/দ্রুত 'কী আছে' জিজ্ঞেস করে diff বের করা যায় না) আর manual পরিবর্তনে drift কীভাবে জন্মায় তা ব্যাখ্যা করে, terraform refresh-এর ভূমিকা আর সীমাবদ্ধতা যুক্তি দিয়ে দেখাতে পারবেন
  • kubernetes-architecture লেসনের reconciliation loop প্যাটার্নের সাথে Terraform-এর plan/apply cycle-এর সরাসরি সমান্তরাল টেনে ব্যাখ্যা করতে পারবেন, আর remote state locking-এর মতো practical operational concern চিহ্নিত করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

virtualization-cloud/cloud-networking-vpc লেসন একটা VPC বানিয়েছে, তার ভেতরে subnet বসিয়েছে, security group লিখে ট্রাফিক নিয়ন্ত্রণ করেছে — কনসোলে ক্লিক করে, বা এক-এক করে CLI কমান্ড চালিয়ে। এই পদ্ধতি একটা demo-র জন্য যথেষ্ট। কিন্তু বাস্তব প্রতিষ্ঠানে একই ধরনের network প্রতিটা environment-এ লাগে — dev, staging, prod, হয়তো প্রতিটা customer-এর জন্য আলাদা — আর প্রশ্নগুলো তখন বদলে যায়:

এই ৩০টা resource ঠিক একইভাবে আবার বানাব কীভাবে, ভুল না করে? ছয় মাস পর কেউ যদি জিজ্ঞেস করে “এই security group কেন এভাবে সেট করা”, উত্তর কোথায় লেখা আছে? কেউ যদি ভুল করে কনসোল থেকে একটা rule মুছে ফেলে, সেটা টের পাব কীভাবে?

এই সমস্যার উত্তর Infrastructure as Code (IaC) — অবকাঠামো (VPC, subnet, security group, VM, …) হাতে-ক্লিক করে না বানিয়ে, কোডে বর্ণনা করে বানানো, যাতে সেই কোড version-control-এ থাকে, review হয়, আর বারবার নির্ভুলভাবে চালানো যায়। আজকের লেসনের হাতিয়ার Terraform — HashiCorp-এর তৈরি একটা multi-cloud IaC tool, যার ভাষার নাম HCL (HashiCorp Configuration Language)।

আজকের কাজ তিনটা স্তরে: প্রথমত, imperative বনাম declarative provisioning-এর নির্ভুল পার্থক্য বোঝা। দ্বিতীয়ত, HCL-এর resource graph আর Terraform-এর plan/apply cycle-এর ভেতরের যুক্তি — বিশেষভাবে state file, যেটা এই মডেলের সবচেয়ে গুরুত্বপূর্ণ, আর কখনো কখনো আশ্চর্যজনক অংশ। তৃতীয়ত, এই পুরো cycle-টাকে কেন kubernetes-architecture-এর reconciliation loop-এর সাথে একই বাক্যে রাখা যায়, তার সরাসরি যুক্তি।

মূল ধারণা

Imperative বনাম declarative — একটা concrete পার্থক্য

Imperative provisioning মানে একগুচ্ছ অর্ডারড কমান্ড — “এই VPC বানাও, তারপর তার ভেতরে এই subnet বানাও, তারপর এই security group বানাও…”। এখানে সঠিকতা দুইটা জিনিসের ওপর নির্ভর করে: (১) কমান্ডগুলো ঠিক এই ক্রমেই চালানো, কারণ subnet বানাতে VPC-র id লাগবে; (২) শেষবার script চালানোর পর থেকে কেউ ম্যানুয়ালি কিছু বদলায়নি — যদি বদলে থাকে, script আবার চালালে duplicate resource বানাবে বা crash করবে।

$ aws ec2 create-vpc --cidr-block 10.0.0.0/16
   -> vpc-0abc123 তৈরি হলো                              ধাপ ১
$ aws ec2 create-subnet --vpc-id vpc-0abc123 --cidr-block 10.0.1.0/24
   -> আগের ধাপের vpc-id হাতে/স্ক্রিপ্টে বসাতে হলো          ধাপ ২
$ aws ec2 create-security-group --vpc-id vpc-0abc123 --group-name web-sg
   -> একই vpc-id আবার লাগলো                              ধাপ ৩
$ aws ec2 run-instances --subnet-id subnet-0def456 --security-group-ids sg-0ghi789
   -> দুই আগের ধাপের id-ই লাগলো                          ধাপ ৪

Declarative provisioning সম্পূর্ণ ভিন্ন প্রশ্নের উত্তর দেয় — “কমান্ড কী চালাব” না, বরং “শেষ পর্যন্ত কী থাকা উচিত”:

এই VPC (10.0.0.0/16) এই subnet (10.0.1.0/24)-সহ, এই security group (443
খোলা)-সহ, একটা instance-সহ — এভাবে থাকা উচিত।

Tool (এখানে Terraform) নিজে বের করে কোন action দরকার — নতুন হলে create, বদলে গেলে update, বাদ পড়লে delete। আর সবচেয়ে গুরুত্বপূর্ণ বৈশিষ্ট্য: কিছু না বদলে থাকলে, আবার চালানো একটা নিরাপদ no-op — এই বৈশিষ্ট্যের নাম idempotency। Imperative script দ্বিতীয়বার চালালে সাধারণত ভাঙে (duplicate VPC, “already exists” error); declarative apply দ্বিতীয়বার চালালে চুপচাপ কিছুই করে না, কারণ desired state আগে থেকেই পৌঁছানো।

HCL — একটা resource graph-এর ভাষা

HCL-এ প্রতিটা resource block একটা cloud resource বর্ণনা করে। যখন একটা block অন্য একটা resource-এর attribute reference করে (যেমন একটা subnet-এর vpc_id field-এ aws_vpc.main.id লেখা), তখন সেটা শুধু একটা value-lookup না — এটা একটা dependency edge।

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_security_group" "web" {
  vpc_id = aws_vpc.main.id          # <- edge: web depends on main
  ingress {
    from_port = 443
    to_port   = 443
    protocol  = "tcp"
  }
}

resource "aws_subnet" "a" {
  vpc_id     = aws_vpc.main.id      # <- edge: a depends on main
  cidr_block = "10.0.1.0/24"
}

resource "aws_instance" "web" {
  subnet_id               = aws_subnet.a.id            # <- edge: web depends on a
  vpc_security_group_ids  = [aws_security_group.web.id] # <- edge: web depends on web-sg
  instance_type            = "t3.micro"
}

লক্ষ্য করুন — এই ফাইলে aws_security_group.web block-টা aws_subnet.a-এর আগে লেখা, অথচ দুটোই শুধু aws_vpc.main-এর ওপর নির্ভরশীল, একে অপরের ওপর না। টেক্সট-এ resource block-এর ক্রম কোনো গুরুত্ব বহন করে না — Terraform পুরো ফাইল পড়ে reference-গুলো থেকে একটা dependency graph বানায়, তারপর সেই গ্রাফ topologically sort করে creation order বের করে। কোনো imperative sequence কাউকে হাতে লিখতে হয় না — যা লেখা আছে তা শুধু সম্পর্ক (relationship), ক্রম না।

ভেতরে কী ঘটছে

terraform plan আর terraform apply — নির্ভুল সেমান্টিক্স

দুইটা কমান্ডের ভূমিকা আলাদা, আর এই আলাদা-থাকাটা ইচ্ছাকৃত:

terraform plan desired state (.tf ফাইল) আর Terraform-এর recorded state-এর মধ্যে একটা diff কম্পিউট করে দেখায় — কোনো cloud resource স্পর্শ না করে। এটা একটা pure computation, dry-run। terraform apply ঠিক সেই diff-টাই কার্যকর করে — provider API কল করে, resource create/update/delete করে।

এই বিভাজনের সুবিধা সরাসরি: plan চালিয়ে দেখা যায় ঠিক কী বদলাতে চলেছে, কোনো ঝুঁকি ছাড়াই — একটা human review-checkpoint, apply-এর আগে।

State file — কেন প্রয়োজন, নির্ভুলভাবে

এখানে একটা প্রশ্ন স্বাভাবিকভাবেই আসে: plan diff বের করতে recorded state-এর দরকার কেন? Terraform কেন সরাসরি cloud provider-কে জিজ্ঞেস করে না “এই মুহূর্তে কী আছে”?

দুইটা concrete কারণ:

১. Cloud provider API নির্ভরযোগ্যভাবে “সবকিছু কী আছে” বলতে পারে না, দ্রুতও না। একটা বড় infrastructure-এ শত শত resource — প্রতিটার বর্তমান পূর্ণ বিবরণ প্রতিটা plan-এ query করা ধীর, rate-limit-এর মুখে পড়ে, আর অনেক provider সরাসরি “এই resource-টা কি Terraform-এর তৈরি, নাকি অন্য কারো” এই প্রশ্নের উত্তর দিতেই পারে না — resource-এর নিজের attribute-এ এমন কোনো tag থাকে না যে বলে দেয় “আমি .tf ফাইলের কোন block-এর সাথে যুক্ত”।

২. Terraform-এর নিজের bookkeeping দরকার — শুধু cloud-এর attribute না। State file .tf ফাইলের প্রতিটা resource address (যেমন aws_subnet.a)-কে cloud provider-এর প্রকৃত resource ID (যেমন subnet-0def456)-এর সাথে map করে রাখে, sensitive attribute (যেমন generated password) ধরে রাখে যা provider re-query-তে নাও ফেরত আসতে পারে, আর resource-দের মধ্যে dependency metadata রাখে যা destroy-এর সঠিক (reverse) order বের করতে লাগে। এই তথ্য cloud provider-এর কাছে নেই — এটা শুধুমাত্র Terraform-এর নিজের।

তাই Terraform State — সাধারণত একটা terraform.tfstate JSON ফাইল — হলো Terraform-এর নিজের রেকর্ড “আমি বিশ্বাস করি বাস্তবে এই resource-গুলো এই attribute নিয়ে চলছে”। plan এই recorded belief-এর সাথে .tf-এর desired description তুলনা করে — বাস্তব cloud-এর সাথে সরাসরি না।

Drift — recorded state যখন বাস্তবতা থেকে সরে যায়

এখানেই সবচেয়ে গুরুত্বপূর্ণ, প্রায়ই আশ্চর্যজনক সমস্যাটা জন্মায়। ধরুন কেউ Terraform-এর বাইরে গিয়ে — সরাসরি cloud console থেকে — একটা resource বদলে দিলো বা মুছে দিলো। State file তখনই স্টেল (stale) হয়ে যায়: এটা এখনও পুরনো belief বহন করছে, বাস্তবতা বদলে গেছে।

terraform refresh (বা আধুনিক Terraform-এ -refresh-only apply) এই সমস্যার একটা আংশিক সমাধান — এটা বাস্তব cloud provider-কে query করে recorded state-কে বাস্তবতার সাথে মিলিয়ে নেয় (drift detection)। কিন্তু এটা .tf ফাইল স্পর্শ করে না — শুধু Terraform-এর bookkeeping ঠিক করে। এর মানে refresh-এর পরের plan আবার সঠিক diff দেখাবে — কিন্তু সেই diff-টা প্রায়ই বলবে “যা manually বদলানো হয়েছিল সেটা আবার desired অবস্থায় ফিরিয়ে আনো”, কারণ .tf ফাইলই ঘোষিত সত্য উৎস, console-এর manual পরিবর্তন না। refresh শুধু দৃশ্যমানতা দেয়, সিদ্ধান্ত না — সিদ্ধান্তটা এখনো মানুষকেই নিতে হয়: .tf ফাইল বদলাবেন (manual change-টা মেনে নিয়ে), না কি apply চালিয়ে manual change-টা revert করবেন।

ব্যবহারিক সতর্কতা — remote state আর locking

State file একটা সাধারণ local ফাইল হলে একটা বাস্তব ঝুঁকি তৈরি হয়: দলের দুইজন যদি একই সময়ে apply চালায়, দুইটা প্রসেস একই state file পড়ে-লিখতে গিয়ে corrupt করে ফেলতে পারে, বা একজনের হিসাব আরেকজনের হিসাবকে overwrite করে ফেলতে পারে। এই কারণেই বাস্তব দলগুলো state remote backend-এ রাখে (যেমন S3 + DynamoDB lock-table, বা Terraform Cloud) — যেখানে একটা lock নিশ্চিত করে একবারে শুধু একটা apply চলতে পারে, আর state-এর একটাই কেন্দ্রীয়, শেয়ার্ড কপি থাকে পুরো দলের জন্য।

উদাহরণ

terraform apply চালানোর সম্পূর্ণ পথ

terraform apply চালানোর পর ঠিক কী ঘটে — এই একটা trace-এই আজকের প্রতিটা ধাপ এক জায়গায়:

terraform apply -- HCL থেকে cloud resource পর্যন্ত
  1. HCL parse.tf ফাইলের প্রতিটা resource block পড়ে internal representation তৈরি হয়
  2. resource graph buildattribute reference (যেমন aws_vpc.main.id) থেকে dependency edge বসানো হয় — কোনো manual ordering ছাড়াই
  3. topological sortgraph থেকে creation order বের করা হয়; independent resource-দের আপেক্ষিক ক্রম অনির্ধারিত
  4. recorded state পড়াlocal বা remote backend থেকে state file লোড হয় — Terraform-এর বোঝা "বর্তমান অবস্থা"
  5. diff (plan)desired বনাম recorded — CREATE/UPDATE/DELETE/no-op তালিকা; কোনো side effect নেই
  6. plan প্রদর্শনব্যবহারকারীকে review-এর সুযোগ — apply-এর আগে থামার শেষ সুযোগ
  7. apply executetopologically-sorted ক্রমে provider API call — actual create/update/delete
  8. state file আপডেটনতুন resource id/attribute recorded state-এ লেখা হয় — পরের plan-এর নতুন ভিত্তি

লক্ষ্য করুন — “recorded state পড়া” ধাপটাই সেই জায়গা যেখানে drift-এর সমস্যা লুকিয়ে থাকে (আগের section-এ আলোচিত)। যদি সেই state আর বাস্তবতা না মেলে, বাকি পুরো pipeline (diff, plan, apply) একটা ভুল ভিত্তির ওপর দাঁড়িয়ে চলে।

ছবিতে

Kubernetes বনাম Terraform — একই প্যাটার্ন, দুই domain

┌──────────── Kubernetes (kubernetes-architecture লেসন) ───────────────┐
│  desired  = etcd-এ ব্যবহারকারীর ঘোষণা (যেমন "৩টা replica চাই")        │
│  observed = cluster-এ আসলে কী চলছে (kubelet-এর status রিপোর্ট থেকে)   │
│  loop     = controller-manager — continuous, automatic, অনন্তকাল      │
│  action   = gap অনুযায়ী create/delete Pod                            │
└────────────────────────────────────────────────────────────────────┘

┌──────────── Terraform (আজকের লেসন) ───────────────────────────────────┐
│  desired  = .tf ফাইলে ঘোষিত resource-দের বর্ণনা                       │
│  observed = state file-এ recorded অবস্থা (বাস্তব cloud না, bookkeeping)│
│  loop     = manual বা CI-triggered `terraform plan && apply` — এক pass │
│  action   = diff অনুযায়ী create/update/delete cloud resource          │
└────────────────────────────────────────────────────────────────────┘
সমান্তরালদুটো সম্পূর্ণ ভিন্ন সিস্টেম, একই declarative-reconciliation কাঠামো — শুধু 'observed state' কোথায় রেকর্ড হয় আর loop কত ঘন ঘন চলে তা ভিন্ন।

সবচেয়ে গুরুত্বপূর্ণ পার্থক্য একটাই বাক্যে: Kubernetes-এর loop continuous ও automatic — controller-manager নিজে থেকে বারবার চালায়, কোনো মানুষ trigger না করলেও। Terraform-এর loop manual বা CI-triggered, এক-বারের pass — কেউ apply না চালালে কিছুই reconcile হয় না। কিন্তু ভেতরের যুক্তি — desired বনাম observed তুলনা করা, ঠিক ততটুকু gap বন্ধ করা, কিছু না বদলালে idempotent no-op — হুবহু একই প্যাটার্নের পুনরাবৃত্তি, শুধু ভিন্ন domain-এ প্রয়োগ করা।

নিজে চালিয়ে দেখুন

EXPERIMENT

Terraform plan/apply core — resource graph, topological sort, diff — বাস্তবে চালিয়ে

যেকোনো OS (Python 3, কোনো dependency ছাড়াই)· ১৫ মিনিট
# tf_plan_apply.py -- core of Terraform's plan/apply algorithm: a
# declarative resource graph (HCL, conceptually), a diff() against
# RECORDED state, and a topological sort for dependency-correct ordering.
DESIRED = {
    "vpc-main":     {"type": "aws_vpc",            "depends_on": [],
                      "attrs": {"cidr_block": "10.0.0.0/16"}},
    "subnet-a":     {"type": "aws_subnet",         "depends_on": ["vpc-main"],
                      "attrs": {"cidr_block": "10.0.1.0/24"}},
    "sg-web":       {"type": "aws_security_group", "depends_on": ["vpc-main"],
                      "attrs": {"ingress_port": 443}},
    "instance-web": {"type": "aws_instance",       "depends_on": ["subnet-a", "sg-web"],
                      "attrs": {"instance_type": "t3.micro"}},
}


def topo_order(graph):
    """Kahn's algorithm over depends_on edges. A subnet resource block
    that REFERENCES a VPC resource's id (e.g. vpc_id = aws_vpc.vpc-main.id
    in real HCL) creates exactly this kind of edge -- nobody writes the
    creation order by hand, Terraform derives it from the reference."""
    indeg = {name: 0 for name in graph}
    children = {name: [] for name in graph}
    for name, r in graph.items():
        for dep in r["depends_on"]:
            children[dep].append(name)
            indeg[name] += 1
    ready = sorted([n for n, d in indeg.items() if d == 0])
    order = []
    while ready:
        n = ready.pop(0)
        order.append(n)
        for child in sorted(children[n]):
            indeg[child] -= 1
            if indeg[child] == 0:
                ready.append(child)
        ready.sort()
    return order


def diff(desired, recorded):
    """`terraform plan`'s core: compare desired config against Terraform's
    RECORDED state -- NOT the live cloud provider -- and compute exactly
    which resources need CREATE / UPDATE / DELETE / no-op. Pure
    computation, no side effects: this is why plan is safe to run anytime."""
    order = topo_order(desired)
    plan = []
    for name in order:
        d = desired[name]
        if name not in recorded:
            plan.append(("CREATE", name, d["type"], d["attrs"]))
        elif recorded[name]["attrs"] != d["attrs"]:
            plan.append(("UPDATE", name, d["type"], (recorded[name]["attrs"], d["attrs"])))
        else:
            plan.append(("no-op", name, d["type"], None))
    for name in recorded:
        if name not in desired:
            plan.append(("DELETE", name, recorded[name]["type"], None))
    return plan


def print_plan(plan, title):
    print(f"--- {title} ---")
    for action, name, rtype, detail in plan:
        if action == "CREATE":
            print(f"  + CREATE  {name} ({rtype})  attrs={detail}")
        elif action == "UPDATE":
            before, after = detail
            print(f"  ~ UPDATE  {name} ({rtype})  {before} -> {after}")
        elif action == "DELETE":
            print(f"  - DELETE  {name} ({rtype})")
        else:
            print(f"  . no-op   {name} ({rtype})")
    print(f"  plan: {sum(1 for p in plan if p[0]=='CREATE')} to create, "
          f"{sum(1 for p in plan if p[0]=='UPDATE')} to update, "
          f"{sum(1 for p in plan if p[0]=='DELETE')} to delete, "
          f"{sum(1 for p in plan if p[0]=='no-op')} unchanged")
    print()


def apply_plan(plan, recorded):
    """`terraform apply`: execute the plan's actions, in the SAME
    topologically-sorted order plan computed, and update recorded state
    to match. Plan only computes the diff; apply performs it."""
    for action, name, rtype, detail in plan:
        if action == "CREATE":
            recorded[name] = {"type": rtype, "attrs": DESIRED[name]["attrs"]}
        elif action == "UPDATE":
            recorded[name]["attrs"] = DESIRED[name]["attrs"]
        elif action == "DELETE":
            del recorded[name]
    return recorded


print("=== resource graph -> topological creation order ===")
print(" ", " -> ".join(topo_order(DESIRED)))
print()

recorded_state = {}  # brand-new: Terraform has never seen this infra before
plan1 = diff(DESIRED, recorded_state)
print_plan(plan1, "terraform plan  (recorded state is empty)")

recorded_state = apply_plan(plan1, recorded_state)
print("terraform apply: 4 resources created, recorded state updated.\n")

plan2 = diff(DESIRED, recorded_state)
print_plan(plan2, "terraform plan  (run again immediately -- idempotency check)")

প্রকৃত চালিয়ে পাওয়া output (deterministic — হুবহু এটাই আসবে):

=== resource graph -> topological creation order ===
  vpc-main -> sg-web -> subnet-a -> instance-web

--- terraform plan  (recorded state is empty) ---
  + CREATE  vpc-main (aws_vpc)  attrs={'cidr_block': '10.0.0.0/16'}
  + CREATE  sg-web (aws_security_group)  attrs={'ingress_port': 443}
  + CREATE  subnet-a (aws_subnet)  attrs={'cidr_block': '10.0.1.0/24'}
  + CREATE  instance-web (aws_instance)  attrs={'instance_type': 't3.micro'}
  plan: 4 to create, 0 to update, 0 to delete, 0 unchanged

terraform apply: 4 resources created, recorded state updated.

--- terraform plan  (run again immediately -- idempotency check) ---
  . no-op   vpc-main (aws_vpc)
  . no-op   sg-web (aws_security_group)
  . no-op   subnet-a (aws_subnet)
  . no-op   instance-web (aws_instance)
  plan: 0 to create, 0 to update, 0 to delete, 4 unchanged

তিনটা জিনিস লক্ষ্য করুন। প্রথমত, topological order — vpc-main সবার আগে (কারো ওপর নির্ভর করে না), instance-web সবার শেষে (দুটোর ওপর নির্ভরশীল); sg-web আর subnet-a-এর মধ্যে কোনো নির্ভরতা নেই বলে তাদের আপেক্ষিক ক্রম অনির্ধারিত — এখানে alphabetical tie-break-এ sg-web আগে এসেছে, কিন্তু subnet-a আগে এলেও equally বৈধ হতো। দ্বিতীয়ত, প্রথম plan-এ সবকিছু CREATE, কারণ recorded state খালি। তৃতীয়ত — সবচেয়ে গুরুত্বপূর্ণ — apply-এর ঠিক পরেই আবার plan চালালে সবকিছু no-op: এটাই idempotency, কথায় না বলে বাস্তবে দেখানো।

এটা কী প্রমাণ করে

declarative provisioning-এর কেন্দ্রীয় algorithm — dependency graph থেকে topological order বের করা, তারপর desired বনাম recorded state-এর diff কম্পিউট করা — শুধু কয়েকটা dict আর একটা function দিয়েই বাস্তবায়নযোগ্য, আর দ্বিতীয়বার চালালে সত্যিই idempotent no-op হয়।

নিজে বানান

BUILD IT

TerraformDriftSim — drift injection আর refresh-এর পর correction plan

Python · ●●●○○
  1. Experiment-এর diff()/topo_order()/apply_plan() পুনর্ব্যবহার করে প্রথম apply চালান — recorded_state আর real_infra দুইটা আলাদা dict বানান (একটা Terraform-এর বোঝা, একটা "বাস্তব cloud")
  2. real_infra-তে সরাসরি drift inject করুন — একটা resource-এর attribute বদলান আর আরেকটা মুছে দিন, recorded_state স্পর্শ না করে
  3. refresh() ছাড়াই diff(DESIRED, recorded_state) চালান — দেখুন এটা ভুলভাবে no-op দেখায়, কারণ recorded_state এখনো পুরনো belief বহন করছে
  4. refresh(recorded, real) লিখুন — real_infra পড়ে recorded_state-কে বাস্তবতার সাথে মিলিয়ে নিন
  5. refresh-এর পরে আবার diff() চালান — দেখুন এবার সঠিক (কিন্তু আশ্চর্যজনক) drift-correction plan বের হয়

লক্ষ্য: state file কেন “শুধু একটা cache” না, আর drift কীভাবে একটা নীরব সমস্যা — কোনো error message ছাড়াই ভুল plan দেখানো — তা কোডে দেখা, তারপর refresh কীভাবে দৃশ্যমানতা ফিরিয়ে দেয় (কিন্তু সিদ্ধান্ত না) তা দেখা।

# tf_drift.py -- extends tf_plan_apply.py with a REAL_INFRA dict standing
# in for "what the cloud provider actually has" (separate from Terraform's
# RECORDED state), a drift injection, and refresh() -- drift detection.
from tf_plan_apply import DESIRED, topo_order, diff, print_plan, apply_plan

# start from the already-applied state at the end of the experiment
recorded_state = apply_plan(diff(DESIRED, {}), {})
real_infra = {name: {"type": r["type"], "attrs": dict(r["attrs"])}
              for name, r in recorded_state.items()}  # cloud mirrors state, for now

print("*** drift injected directly against the cloud provider, bypassing terraform ***")
print("    - sg-web: ingress_port manually changed 443 -> 22 (a 'quick fix' via the console)")
real_infra["sg-web"]["attrs"]["ingress_port"] = 22
print("    - subnet-a: deleted manually from the console")
del real_infra["subnet-a"]
print()

print("=== plan WITHOUT refresh -- recorded_state is stale, doesn't know about drift ===")
plan_stale = diff(DESIRED, recorded_state)
print_plan(plan_stale, "terraform plan  (using stale recorded state)")
print("  ^ the danger: recorded state still says subnet-a exists and sg-web has")
print("    port 443, so plan wrongly reports nothing to do -- it never asked the")
print("    cloud provider, only compared against its own bookkeeping.")
print()


def refresh(recorded, real):
    """terraform refresh: read the REAL infrastructure and reconcile
    recorded state to match reality. It does not touch the .tf files --
    only Terraform's own bookkeeping, so the next plan/diff is accurate."""
    for name in list(recorded):
        if name not in real:
            del recorded[name]
        else:
            recorded[name]["attrs"] = dict(real[name]["attrs"])
    return recorded


recorded_state = refresh(recorded_state, real_infra)
print("=== terraform refresh -- recorded state reconciled against real infra ===")
print(f"  recorded state now has: {sorted(recorded_state.keys())}")
print(f"  sg-web recorded ingress_port: {recorded_state['sg-web']['attrs']['ingress_port']}")
print()

plan_drift = diff(DESIRED, recorded_state)
print_plan(plan_drift, "terraform plan  (after refresh -- the drift-correction plan)")
print("  ^ correct, but possibly surprising: terraform will RECREATE subnet-a")
print("    and REVERT sg-web's port back to 443 -- undoing the manual fix,")
print("    because the .tf files, not the console, are the declared source")
print("    of truth. A human who forgets this can be caught off guard.")

প্রকৃত চালিয়ে পাওয়া output (deterministic — হুবহু এটাই আসবে; উপরের experiment-এর output-এর পরে এই অংশটা যোগ হয়):

*** drift injected directly against the cloud provider, bypassing terraform ***
    - sg-web: ingress_port manually changed 443 -> 22 (a 'quick fix' via the console)
    - subnet-a: deleted manually from the console

=== plan WITHOUT refresh -- recorded_state is stale, doesn't know about drift ===
--- terraform plan  (using stale recorded state) ---
  . no-op   vpc-main (aws_vpc)
  . no-op   sg-web (aws_security_group)
  . no-op   subnet-a (aws_subnet)
  . no-op   instance-web (aws_instance)
  plan: 0 to create, 0 to update, 0 to delete, 4 unchanged

  ^ the danger: recorded state still says subnet-a exists and sg-web has
    port 443, so plan wrongly reports nothing to do -- it never asked the
    cloud provider, only compared against its own bookkeeping.

=== terraform refresh -- recorded state reconciled against real infra ===
  recorded state now has: ['instance-web', 'sg-web', 'vpc-main']
  sg-web recorded ingress_port: 22

--- terraform plan  (after refresh -- the drift-correction plan) ---
  . no-op   vpc-main (aws_vpc)
  ~ UPDATE  sg-web (aws_security_group)  {'ingress_port': 22} -> {'ingress_port': 443}
  + CREATE  subnet-a (aws_subnet)  attrs={'cidr_block': '10.0.1.0/24'}
  . no-op   instance-web (aws_instance)
  plan: 1 to create, 1 to update, 0 to delete, 2 unchanged

  ^ correct, but possibly surprising: terraform will RECREATE subnet-a
    and REVERT sg-web's port back to 443 -- undoing the manual fix,
    because the .tf files, not the console, are the declared source
    of truth. A human who forgets this can be caught off guard.

তিনটা মুহূর্ত মন দিয়ে পড়ুন: (১) refresh-এর আগে plan নীরবে ভুল — কোনো error, কোনো warning নেই, শুধু ভুল “কিছু বদলায়নি” সিদ্ধান্ত; (২) refresh নিজে কোনো cloud resource বদলায় না — শুধু recorded state-কে বাস্তবতার আয়না বানায়; (৩) refresh-এর পরের plan টেকনিক্যালি “সঠিক” (recorded state এখন বাস্তবতার সাথে মেলে, আর diff সেই বাস্তবতা বনাম .tf-এর সঠিক তুলনা) — কিন্তু ফলাফল একজন মানুষের কাছে আশ্চর্যজনক লাগতে পারে, কারণ এটা তার manual “quick fix”-টাকেই revert করার প্রস্তাব দিচ্ছে।

নিজে বাড়ান:

  1. একটা orphan_adopt() ফাংশন লিখুন যা drift-এর পর মানুষ যদি “না, manual change-টাই রাখতে চাই” সিদ্ধান্ত নেয়, তাহলে .tf-এর desired attrs-কেই বাস্তবতার সাথে মেলাতে বদলে দেয় (বাস্তব Terraform-এ এটা .tf ফাইল হাতে edit করার সমতুল্য) — তারপর দেখান পরের diff() no-op দেয়।
  2. instance-web-এর depends_on-এ subnet-a আছে — drift scenario-তে subnet-a মুছে গেলে instance-web-এর ওপর কী প্রভাব পড়া উচিত তা নিয়ে চিন্তা করুন (বাস্তব Terraform-এ নির্ভরশীল resource-ও প্রায়ই recreate হয়) আর diff()-এ সেই cascading logic যোগ করুন।
  3. apply_plan()-এ একটা simulated failure যোগ করুন (তৃতীয় resource তৈরির সময় exception) — partial apply-এর পর recorded_state-এর অবস্থা কী হওয়া উচিত তা ডিজাইন করুন।

বাস্তব সিস্টেমে

বাস্তব জগতে Infrastructure as Code

Multi-cloud আর provider ecosystem। Terraform-এর একই HCL ভাষা, একই plan/apply মডেল AWS, GCP, Azure, এমনকি Kubernetes বা SaaS tool (যেমন Datadog, GitHub)-এর জন্যও কাজ করে — প্রতিটার জন্য একটা আলাদা provider plugin, কিন্তু core reconciliation engine অভিন্ন। এই uniformity-ই Terraform-কে “de facto” multi-cloud IaC tool বানিয়েছে।

CI/CD-তে plan/apply। বাস্তব দলগুলো সাধারণত terraform plan-এর output pull-request-এ automatically পোস্ট করে (review-এর জন্য), আর apply শুধু merge-এর পর, একটা CI pipeline থেকে, remote state lock নিয়ে চালায় — কোনো ইঞ্জিনিয়ার সরাসরি নিজের laptop থেকে production apply করে না। এটাই আগের section-এর “remote state + locking” পরামর্শের বাস্তব রূপ।

Terraform বনাম Ansible — declarative বনাম procedural-ইশ। Ansible playbook-ও idempotent হতে পারে, কিন্তু এটা মূলত task-এর ordered list চালায় (procedural ধাঁচ কাছাকাছি) আর সাধারণত নিজের কোনো persistent state file রাখে না — প্রতিটা রান-এ live system query করে idempotency নিশ্চিত করে। Terraform বিপরীতভাবে একটা explicit resource graph আর explicit state file-ভিত্তিক — এই পার্থক্যটাই আজকের লেসনের declarative/imperative বিভাজনের একটা বাস্তব উদাহরণ (Kief Morris-এর IaC methodology-তে এই দুই ধরনকেই আলাদা করে আলোচনা করা হয়েছে)।

শেষ সারিটা লক্ষণীয় — Kubernetes-এর push-ভিত্তিক watch API observed-state-কে প্রায় real-time রাখে, তাই drift সেখানে কম সমস্যা তৈরি করে। Terraform-এর state file manual apply-র মাঝের ফাঁকে বাস্তবতা থেকে অনেক দূরে সরে যেতে পারে — এটাই আজকের drift আলোচনার মূল কারণ, আর কেন IaC দলগুলো নিয়মিত refresh/drift-detection চালানোর অভ্যাস গড়ে তোলে।

যে ভুলগুলো সবাই করে

“terraform apply সবসময় নিরাপদ, কারণ এটা তো idempotent — বারবার চালালেও কোনো ক্ষতি নেই।”

Idempotency শুধু তখনই ধরে থাকে যখন recorded state বাস্তবতাকে সঠিকভাবে প্রতিফলিত করে। Drift ঘটে গেলে (কেউ manually কিছু বদলেছে) apply আর “নিরাপদ no-op” থাকে না — এটা এমন কিছু create/update/delete করতে পারে যা কেউ প্রত্যাশা করেনি, যেমন আজকের build section-এর drift-correction plan যা manually-fix-করা security group port আবার revert করে দিয়েছিল।

“Terraform state file আসলে শুধু একটা cache — cloud provider থেকে যেকোনো সময় re-derive করা যায়, তাই হারালে সমস্যা নেই।”

State file শুধু cache না — এটা Terraform-এর একমাত্র bookkeeping যেখানে .tf resource address-কে বাস্তব cloud resource ID-র সাথে map করা থাকে, কিছু sensitive/generated attribute সংরক্ষিত থাকে যা provider re-query সবসময় ফেরত দেয় না, আর dependency metadata থাকে destroy-order-এর জন্য। State হারালে Terraform সেই resource-গুলোকে “আর নেই” ধরে নেয় আর duplicate তৈরির চেষ্টা করতে পারে — এই কারণেই remote state (locking-সহ) এত গুরুত্বপূর্ণ।

“terraform plan কখনো ভুল দেখাতে পারে না, কারণ এটা তো শুধু একটা dry-run, কোনো side effect নেই।”

Plan-এর computation নিজে নিশ্চিতভাবে সঠিক (dict/state-এর ওপর deterministic diff), কিন্তু এর input-টাই ভুল হতে পারে — recorded state যদি stale হয় (drift), plan সেই স্টেল ভিত্তির ওপর একটা সম্পূর্ণ ভুল (বা misleading) সিদ্ধান্তে পৌঁছাবে, যেমন আজকের build section-এ refresh-ছাড়া plan যেটা ভুলভাবে “কোনো action দরকার নেই” দেখিয়েছিল।

“HCL মূলত একটা imperative script-এর মতোই, শুধু syntax আলাদা — Terraform উপর থেকে নিচে resource block পড়ে চালায়।”

না — resource block-দের টেক্সট-এ থাকা ক্রম কার্যকর হওয়ার ক্রম নির্ধারণ করে না। Terraform পুরো ফাইল পড়ে attribute-reference থেকে একটা dependency graph বানায়, তারপর সেই graph topologically sort করে creation order বের করে — এই লেসনের HCL উদাহরণেই aws_security_group.web (নির্ভরতা কম) aws_subnet.a-এর আগে লেখা থাকা সত্ত্বেও প্রকৃত creation order নির্ধারিত হয়েছে শুধু graph দিয়ে, লেখার ক্রম দিয়ে না।

বুঝেছেন কি না দেখুন

1Imperative আর declarative provisioning-এর সংজ্ঞা এক বাক্য করে দিন — আর কোনটাতে দ্বিতীয়বার script/apply চালানো নিরাপদ no-op হয়ে যায়?স্মরণ
Imperative: একটা ordered command sequence, সঠিকতা exact ক্রম আর “কেউ ম্যানুয়ালি কিছু বদলায়নি” এই ধারণার ওপর নির্ভরশীল। Declarative: desired end-state-এর একটা বর্ণনা, tool নিজে action বের করে। Declarative-এই দ্বিতীয়বার apply নিরাপদ no-op — এটাই idempotency; imperative script দ্বিতীয়বার চালালে সাধারণত duplicate resource বা error দেয়।
2Terraform কেন প্রতিটা plan-এ সরাসরি cloud provider-কে “এই মুহূর্তে কী আছে” জিজ্ঞেস না করে, বরং নিজের state file-এর ওপর নির্ভর করে? দুইটা concrete কারণ দিন।যুক্তি
প্রথমত, বড় infrastructure-এ প্রতিটা resource-এর পূর্ণ বিবরণ প্রতি plan-এ query করা ধীর ও rate-limit-প্রবণ, আর provider সবসময় বলতে পারে না কোন resource কোন .tf block-এর সাথে যুক্ত। দ্বিতীয়ত, state file-এ এমন bookkeeping থাকে যা cloud provider-এর কাছেই নেই — resource-address-থেকে-ID mapping, কিছু sensitive/generated attribute, আর destroy-order metadata।
3Drift ঠিক কীভাবে জন্ম নেয়, আর terraform refresh এই সমস্যার কতটুকু সমাধান করে — কতটুকু করে না?যুক্তি
কেউ Terraform-এর বাইরে গিয়ে (console/CLI দিয়ে সরাসরি) একটা resource বদলালে বা মুছলে, recorded state সেই পরিবর্তন সম্পর্কে অজ্ঞ থেকে যায় — এটাই drift। refresh বাস্তব cloud query করে recorded state-কে বাস্তবতার সাথে মিলিয়ে নেয় (দৃশ্যমানতা ফেরায়), কিন্তু .tf ফাইল স্পর্শ করে না — তাই এটা সিদ্ধান্ত নেয় না যে manual change রাখা হবে না ফিরিয়ে দেওয়া হবে; refresh-এর পরের plan প্রায়ই manual change-টা revert করার প্রস্তাব দেয়, কারণ .tf-ই ঘোষিত সত্য উৎস।
4Experiment-এর topological-order output ছিল vpc-main -> sg-web -> subnet-a -> instance-web। sg-web-কে subnet-a-এর আগে কেন রাখা হলো — এটা কি কোনো নির্ভরতার প্রমাণ?প্রয়োগ
না — sg-web আর subnet-a দুটোই শুধু vpc-main-এর ওপর নির্ভরশীল, একে অপরের ওপর না। তাদের মধ্যে কোনো dependency edge নেই, তাই topological sort-এ তাদের আপেক্ষিক ক্রম অনির্ধারিত — কোডের tie-break নিয়ম (alphabetical) এখানে sg-web-কে আগে রেখেছে, কিন্তু subnet-a আগে এলেও সমানভাবে বৈধ topological order হতো।
5Build section-এর drift scenario-তে refresh-এর পরের plan sg-web-এর port ২২ থেকে আবার ৪৪৩-এ ফিরিয়ে আনার প্রস্তাব দিয়েছে — manual “fix”-টাকে “adopt” করার বদলে। কেন, আর একজন ইঞ্জিনিয়ার এটা কীভাবে এড়াবেন যদি সত্যিই ২২ রাখতে চান?প্রয়োগ
কারণ diff সবসময় .tf ফাইলকে ঘোষিত সত্য উৎস ধরে — refresh শুধু recorded state-কে বাস্তবতার আয়না বানায়, .tf-কে না। ইঞ্জিনিয়ার যদি সত্যিই port ২২ রাখতে চান, তাকে .tf ফাইলেই ingress_port = 22 লিখে সেই ইচ্ছাটাকে declarative source-এ আনতে হবে — তাহলে diff দেখবে desired আর (refresh-করা) recorded state ইতিমধ্যে মিলে গেছে, no-op।
6আপনার দলে পাঁচজন ইঞ্জিনিয়ার, সবাই নিজের laptop থেকে একই AWS account-এর বিরুদ্ধে terraform apply চালাতে পারেন, state file একটা shared local ফাইল (git repo-তে committed)। এই সেটআপে কোন দুইটা concrete ঝুঁকি আছে, আর একটা ভালো operational fix ডিজাইন করুন।ডিজাইন
ঝুঁকি ১: দুইজন প্রায় একই সময়ে apply চালালে state file corrupt হতে পারে বা একজনের লেখা আরেকজনের overwrite করে ফেলতে পারে — কোনো locking নেই। ঝুঁকি ২: state file-এ resource ID আর প্রায়ই sensitive attribute (password, key) থাকে — git history-তে committed মানে সেগুলো স্থায়ীভাবে leak হয়ে থাকে, এমনকি পরে মুছলেও। Fix: state remote backend-এ সরিয়ে নেওয়া (S3 + DynamoDB lock-table, বা Terraform Cloud) যা locking নিশ্চিত করে (একবারে একটাই apply চলতে পারবে) আর state-কে git history থেকে বাইরে রাখে, access-control-সহ encrypted storage-এ।

এরপর কী

আজ আমরা দেখেছি কীভাবে infrastructure provision হয় declaratively — plan/apply cycle, resource graph, state file, আর drift-এর বাস্তব ঝুঁকি। এই পুরো লেসন একটা প্রশ্নের উত্তর দিয়েছে: infrastructure ঠিক কী থাকা উচিত তা কীভাবে ঘোষণা করব, আর সেই ঘোষণা কীভাবে বাস্তবে পরিণত হয়।

কিন্তু একবার সেই infrastructure চালু হয়ে গেলে — dozens container, বহু মেশিন জুড়ে — একদম ভিন্ন একটা প্রশ্ন সামনে আসে: সিস্টেমের ভেতরে আসলে কী ঘটছে, সেটা জানব কীভাবে? পরের লেসন, virtualization-cloud/observability-metrics-logs-traces, ঠিক এই প্রশ্নের উত্তর দেবে — observability-র তিনটা স্তম্ভ (metrics, logs, traces)।

আরও পড়ুন

  • How Terraform Works — HashiCorp Terraform Intro — HashiCorp — Terraform Documentation · Declarative provisioning, plan/apply workflow, আর provider model-এর প্রামাণ্য পরিচিতি — এই লেসনের কাঠামোগত ভিত্তি
  • Terraform State — Purpose — HashiCorp — Terraform Documentation · State file কেন প্রয়োজন — mapping, metadata, performance — এর প্রামাণ্য ব্যাখ্যা; hood অংশের সরাসরি উৎস
  • Remote State — HashiCorp — Terraform Documentation · Remote backend আর state locking-এর প্রামাণ্য বর্ণনা — concurrent apply-জনিত corruption ঠেকানোর ব্যবহারিক ভিত্তি
  • Infrastructure as Code: Dynamic Systems for the Cloud Age (2nd ed.) — Kief Morris — O'Reilly Media, 2020 · IaC methodology-র প্রামাণ্য পাঠ্যপুস্তক — idempotency, repeatability, আর disposable infrastructure নীতির উৎস, realworld অংশের ভিত্তি