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

Terraform state — backend, lock, import আর drift

Terraform State — Backends, Locking, Import and Drift

Taught in Bangla, this lesson explains why Terraform needs state and why the file must be guarded like a secret, moving from local to remote backends, locking concurrent applies, state commands, moved, import and removed blocks, detecting drift with refresh-only plans, clearing stuck locks, recovering lost state, and splitting state to shrink blast radius and share data between stacks.

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

  • State কেন দরকার, ভেতরে কী থাকে, আর কেন একে গোপন নথির মতো রাখতে হয়
  • Local থেকে remote backend-এ যাওয়া, আর lock কীভাবে দুই apply-কে আলাদা রাখে
  • state list, show, mv, rm আর moved, import, removed ব্লক ব্যবহার করা
  • Drift ধরা, আটকে থাকা lock সরানো, আর state হারালে উদ্ধার করা
  • Blast radius দেখে state ভাগ করা, আর stack-এর মধ্যে তথ্য আদান-প্রদানের পথ বাছা

এই পর্বের আউটপুট কোথা থেকে? নিচের সব transcript একটি আসল run থেকে নেওয়া: Terraform ১.১৭, hashicorp/kubernetes provider ৩.৪ আর একটি local single-node cluster (Kubernetes ১.৩৪). কোনো cloud account ব্যবহার হয়নি। আপনার id, hash, uid আর সময় আলাদা হবে। লম্বা অংশ … দিয়ে ছাঁটা। Cloud provider-এর plan এখানে চালানো হয়নি, তাই S3, Azure আর GCS-এর অংশ কেবল নমুনা। Password সব নকল।

১ · State কেন লাগে — ভূমি অফিসের খতিয়ান

পর্ব ৫২-তে আপনি Terraform-এর plan আর apply দেখেছেন। সেখানে একটি প্রশ্ন বাকি ছিল। দ্বিতীয়বার apply চালালে Terraform নতুন একটি জিনিস বানায় না কেন? সে কীভাবে জানে জিনিসটি আগেই আছে?

উত্তর হলো state. এটি একটি JSON নথি, যেখানে Terraform প্রতিটি কাজের পরে লিখে রাখে কী বানিয়েছে। আপনার HCL ফাইলে লেখা থাকে “আমি কী চাই”, যাকে বলে desired state. বাস্তবে আছে cloud-এর জিনিসগুলো। State এই দুইয়ের মাঝখানের সেতু।

ভূমি অফিসের খতিয়ানের কথা ভাবুন। জমি মাঠেই আছে, খতিয়ান থেকে জমি তৈরি হয় না। কিন্তু খতিয়ান বলে দেয় কোন দাগ নম্বর কোন জমি, মালিক কে। খতিয়ান পুড়ে গেলে জমি উধাও হয় না, তবে কোনটি কার, তা নিয়ে শুরু হয় গোলমাল। State ঠিক এই খতিয়ান। kubernetes_secret_v1.db হলো দাগ নম্বর, আর abcl-ts/boighor-db হলো মাঠের আসল জমি।

State তিনটি কাজ করে:

মিলিয়ে রাখা

Config-এর resource address আর বাস্তবের object id জোড়া বাঁধে। এক নাম, এক জিনিস। অনেক provider-এ এই জোড়া অন্য কোথাও লেখা থাকে না।

Mapping

মনে রাখা

কোন জিনিস কার ওপর নির্ভরশীল, কোন provider বানিয়েছে, সব জমা থাকে। Config থেকে resource মুছে ফেললে তার তথ্য আর config-এ নেই, তবু Terraform জানে কোন ক্রমে ধ্বংস করবে।

Metadata

দ্রুত করা

হাজারটি resource থাকলে সবকিছু খুঁজে বের করা ধীর, আর provider-এর API সীমায়ও লাগে। State জানে কোন id পড়তে হবে, তাই সরাসরি সেগুলোই পড়ে।

Performance
Config = যা চাই · State = যা বানিয়েছি বলে মনে আছে · Cloud = যা আসলে আছে
Plan এই তিনটি মিলিয়ে দেখে

২ · State-এর ভেতরে কী থাকে

একটি কাল্পনিক অনলাইন বইয়ের দোকান “Boighor”-এর জন্য দুটি resource বানানো হলো: একটি ConfigMap আর একটি Secret. কাজটি হলো একটি local Kubernetes cluster-এ। এটুকুই config:

resource "kubernetes_config_map_v1" "app" {
  metadata {
    name      = "boighor-config"
    namespace = "abcl-ts"
  }
  data = {
    LOG_LEVEL = "info"
  }
}

resource "kubernetes_secret_v1" "db" {
  metadata {
    name      = "boighor-db"
    namespace = "abcl-ts"
  }
  data = {
    password = "fake-pass-1234"
  }
}

apply-এর পরে ফোল্ডারে terraform.tfstate তৈরি হলো। ফাইলটি ছেঁটে দেখি:

{
  "version": 4,
  "terraform_version": "1.17.1",
  "serial": 3,
  "lineage": "66e65cd8-cf8f-dc59-1527-03a976d8c15f",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "kubernetes_secret_v1",
      "name": "db",
      "provider": "provider[\"registry.terraform.io/hashicorp/kubernetes\"]",
      "instances": [
        {
          "attributes": {
            "data": {
              "password": "fake-pass-1234"
            },
            "id": "abcl-ts/boighor-db",
            …
          },
          "sensitive_attributes": [ … ],
          …
        }
      ]
    },
    …
  ]
}

serial প্রতিবার state বদলালে বাড়ে। lineage এই state-এর জন্মের id, যা বদলায় না। দুটি মিলে পুরোনো কপি বা অন্য state-এর কপি চিনে ফেলা যায়। আর "password": "fake-pass-1234" লাইনটি দেখুন: কোনো encryption নেই।

sensitive মানে ফাইলে লুকানো নয়। ওপরের ফাইলে password খোলা। অথচ terraform state show এটি লুকিয়ে দেখায়, নিচে দেখুন। লুকানো হয় শুধু পর্দায়। তাই state ফাইল Git-এ রাখবেন না, chat-এ পাঠাবেন না, আর গোপন Secret ফাইলের মতোই সামলাবেন।

$ terraform state show kubernetes_secret_v1.db
# kubernetes_secret_v1.db:
resource "kubernetes_secret_v1" "db" {
    binary_data_wo                 = (write-only attribute)
    data                           = (sensitive value)
    data_wo                        = (write-only attribute)
    id                             = "abcl-ts/boighor-db"
    immutable                      = false
    type                           = "Opaque"
    …
}

গোপন মান state-এর বাইরে রাখা

সবচেয়ে ভালো পথ হলো গোপন মানটি Terraform-এর হাতেই না দেওয়া। Secret manager-এ রাখুন (পর্ব ৩৩), আর app সেখান থেকে পড়ুক। যদি Terraform-কে মান পৌঁছে দিতেই হয়, নতুন version-এ আছে write-only argument. এই argument মান provider-এ পাঠায়, কিন্তু জমা করে না। এটি Terraform ১.১১ (২০২৫) থেকে আছে, আর provider-কেও এটি সমর্থন করতে হয়।

# the config uses data_wo + data_wo_revision instead of data
$ grep -c 'fake-pass-5678' terraform.tfstate
0
$ grep -n '"data_wo\|"data"' terraform.tfstate
20:            "data": null,
21:            "data_wo": null,
22:            "data_wo_revision": 1,
51:                "value": "data"
$ kubectl get secret boighor-db2 -n abcl-ts \
    -o jsonpath='{.data.password}' | base64 -d
fake-pass-5678

Cluster-এ password পৌঁছেছে, state-এ 0 বার। মান বদলাতে হলে data_wo_revision বাড়াতে হয়, কারণ Terraform পুরোনো মান জানে না, তাই নিজে থেকে বদল ধরতে পারে না।

আরও গভীরে: State-এর ফরম্যাট সরকারি API নয়, তাই হাতে সম্পাদনা না করে সবসময় terraform state কমান্ড ব্যবহার করুন। Script-এ পড়ার দরকার হলে terraform show -json-এর ফরম্যাটটি স্থিতিশীল বলে নথিভুক্ত। S3-তে লক ফাইলের গল্পটিও সুন্দর: ২০২০ সালে S3 strong consistency পেল, ২০২৪ সালে conditional write, আর তার পরে Terraform ১.১০-১.১১ DynamoDB ছাড়াই lock করতে শিখল। অর্থাৎ storage-এর একটি গ্যারান্টি বদলালে উপরের টুলের নকশাও সহজ হয়ে যায়।

৩ · Backend — state কোথায় থাকবে

Backend ঠিক করে state কোথায় জমা হবে আর lock কীভাবে হবে। কিছু না বললে ব্যবহার হয় local backend: আপনার ফোল্ডারের terraform.tfstate. একা শেখার জন্য এটি যথেষ্ট। দলে এটি তিন দিক থেকে ভাঙে। ফাইলটি এক জনের ল্যাপটপে, ল্যাপটপ নষ্ট হলে state-ও শেষ, আর দুজন একসাথে কাজ করলে কিছুই থামায় না।

প্রশ্নLocalRemote
কোথায় থাকেএক জনের ডিস্কেObject storage, cluster বা managed service-এ
দলের সবাই পায়?না, হাতে কপি করতে হয়হ্যাঁ, অনুমতি থাকলে
Lockএক মেশিনের ভেতরেই কাজ করেসবার জন্য এক lock (backend সমর্থন করলে)
Version ইতিহাসশুধু একটি .backupStorage-এর versioning থাকলে সব
Encryption আর auditডিস্কের ওপর নির্ভরStorage-এর সুবিধা ব্যবহার করা যায়
CI থেকে চালানোকার্যত অসম্ভবস্বাভাবিক পথ

Remote state দেখতে cloud account লাগে না। Terraform-এর kubernetes backend state রাখে একটি Secret-এ আর lock রাখে একটি Lease object-এ। শেখার জন্য চমৎকার, যদিও বড় state-এ Secret-এর প্রায় ১ MiB সীমায় আটকাবে। আগের local state-টিই সরিয়ে নিই:

terraform {
  backend "kubernetes" {
    secret_suffix  = "boighor"
    namespace      = "abcl-ts"
    config_path    = "~/.kube/config"
    config_context = "docker-desktop"
  }
}
$ terraform plan

Error: Backend initialization required, please run "terraform init"

Reason: Initial configuration of the requested backend "kubernetes"
…
$ terraform init -migrate-state -force-copy
Initializing the backend...

Successfully configured the backend "kubernetes"! Terraform will automatically
use this backend unless the backend configuration changes.
…
Terraform has been successfully initialized!

$ kubectl get secret,lease -n abcl-ts
NAME                             TYPE     DATA   AGE
secret/boighor-db                Opaque   1      119s
secret/tfstate-default-boighor   Opaque   1      0s

NAME                                                     HOLDER   AGE
lease.coordination.k8s.io/lock-tfstate-default-boighor            0s

Backend বদলালে Terraform আগে init চায়। -migrate-state state নতুন জায়গায় কপি করে। Secret-এর নাম tfstate-default-boighor: এখানে default হলো workspace, আর boighor হলো আমাদের suffix. Lease-এর HOLDER এখন ফাঁকা, মানে lock কেউ ধরে নেই। এরপরে পুরোনো local ফাইলগুলো নিরাপদে মুছে ফেলতে হবে, কারণ terraform.tfstate.backup-এ তখনও password আছে।

Remote হলেই কি গোপন? যাচাই করি। State gzip করা, base64-এর ভেতরে, কিন্তু encrypted নয়:

$ kubectl get secret tfstate-default-boighor -n abcl-ts \
    -o jsonpath='{.metadata.annotations.encoding}'
gzip
$ kubectl get secret tfstate-default-boighor -n abcl-ts \
    -o jsonpath='{.data.tfstate}' | base64 -d | gzip -d \
    | grep -c 'fake-pass-1234'
1

ফল 1: যিনি এই Secret পড়তে পারেন, তিনি সব password-ও পড়তে পারেন। তাই backend-এর পড়ার অনুমতিই আসল প্রশ্ন। পর্ব ৪৯-এর RBAC বা cloud IAM দিয়ে সেটি ছোট রাখতে হবে।

Cloud-এর backend — তিনটি নমুনা

নিচের তিনটি ব্লক চালানো হয়নি, কারণ cloud account ব্যবহার করা হয়নি। নামগুলো নকল। শুধু গঠন দেখুন, আর argument মেলাতে backend-এর সরকারি নথি পড়ুন। Bucket আর storage account আগে থেকেই বানিয়ে রাখতে হয়। এতে খরচ হতে পারে।

terraform {
  backend "s3" {
    bucket       = "example-tfstate-bucket"
    key          = "boighor/prod/terraform.tfstate"
    region       = "ap-southeast-1"
    encrypt      = true
    use_lockfile = true
  }
}

use_lockfile S3-তেই একটি .tflock object বানিয়ে lock করে। পুরোনো পথ ছিল DynamoDB টেবিল (dynamodb_table), যা Terraform ১.১১ থেকে deprecated. পুরোনো প্রজেক্টে এটি অনেক দেখবেন। Version দেখে সিদ্ধান্ত নিন।

terraform {
  backend "azurerm" {
    resource_group_name  = "example-tfstate-rg"
    storage_account_name = "exampletfstate"
    container_name       = "tfstate"
    key                  = "boighor/prod.terraform.tfstate"
  }
}

State থাকে Blob Storage-এ, আর lock হয় blob lease দিয়ে। Authentication-এর কয়েকটি পথ আছে, নথি দেখে বাছুন।

terraform {
  backend "gcs" {
    bucket = "example-tfstate-bucket"
    prefix = "boighor/prod"
  }
}

State থাকে Cloud Storage bucket-এ, আর lock হয় সেখানেই একটি lock object দিয়ে।

যে backend-ই হোক, চারটি নিয়ম এক:

Storage-এর encryption ফাইলটিকে ডিস্কে রক্ষা করে। যাঁর পড়ার অনুমতি আছে, তিনি খোলা লেখাই পান। Terraform নিজে client-side encryption দেয় না বলে জানি। OpenTofu ২০২৪ সালে state encryption যোগ করেছে। HashiCorp-এর HCP Terraform-এর মতো managed service-ও state রাখে, তবে সেগুলোর শর্ত আলাদা। Terraform ২০২৩ সালের আগস্টে BUSL লাইসেন্সে যায়, আর OpenTofu সেই fork, যা Linux Foundation-এর অধীনে চলে।

৪ · Lock — দুজন একসাথে apply করলে

মেসের বাজারের হিসাবের খাতা একটিই। রাফি খাতা নিয়ে নিজের ঘরে গেল। মিতু ফোনে পুরোনো পাতার ছবি দেখে নিজের খরচ লিখে নতুন পাতা বানাল। রাফি ফিরে এসে নিজের পাতাটি বসিয়ে দিল। মিতুর খরচ খাতা থেকে হাওয়া, অথচ টাকা তো খরচ হয়েছে। সমাধান সোজা: খাতা যার হাতে, শুধু সে লিখবে। বাকিরা অপেক্ষা করবে।

এটি একটি race condition. দুজনেই পুরোনো state পড়েন, দুজনেই জিনিস বানান, শেষে যিনি লেখেন তাঁর state টিকে থাকে। অন্য জনের বানানো জিনিসটি বাস্তবে চলে, বিল বাড়ায়, কিন্তু কোনো state-এ নেই। একে বলে orphaned resource. State lock এই পথ বন্ধ করে।

দুই engineer, এক backend, এক lock

একসাথে একজনই লিখতে পারেন

দুই engineer, remote backend আর lock রাফিterraform apply মিতুterraform apply lock পেলেন আটকে গেলেন Remote backend Stateserial, id-র তালিকা Lockholder: রাফির id রাফির apply বদলায় বাস্তবের infrastructurenetwork, database, queue…
ছবি ৫৩.১ — Lock state-এর পাশে থাকে। যিনি lock পান, শুধু তিনিই state আর বাস্তব বদলান।

এবার আসল পরীক্ষা। একই backend-এ দুটি আলাদা ফোল্ডার, যেন দুটি ল্যাপটপ। রাফি একটি Job যোগ করেছেন, যা ২০ সেকেন্ড চলে আর apply অপেক্ষা করে। মিতু LOG_LEVEL বদলে warn করেছেন। (এই run-এ ConfigMap resource-টির নাম web_config. নামটি কীভাবে বদলাল, অংশ ৫-এ দেখবেন।) Who লাইনে এই মেশিনের নাম ছিল, তাই ছাঁটা।

Backend-এর অবস্থা
Lease HOLDER
রাফি
মিতু
  1. কেউ কাজ করছেন না। Lease object আছে, কিন্তু HOLDER ফাঁকা। Lock মানে এখানে আলাদা কোনো সার্ভার নয়, শুধু একটি Kubernetes object.
  2. রাফি lock পেলেন। Terraform Lease-এর holderIdentity-তে একটি নতুন id বসাল, আর app.terraform.io/lock-info annotation-এ লিখল কে, কখন, কোন কাজ। Lock আগে, তারপর state পড়া।
  3. মিতু আটকে গেলেন। terraform apply সঙ্গে সঙ্গে exit code 1 দিয়ে থামল। এটি কোনো ত্রুটি নয়, এটিই সুরক্ষা। State বা cluster-এ কিছুই বদলায়নি। Lock Info বলছে কে আটকে রেখেছেন।
  4. অপেক্ষার পথ। ডিফল্টে Terraform একবার চেষ্টা করেই হাল ছাড়ে। -lock-timeout=60s দিলে সে ৬০ সেকেন্ড পর্যন্ত চেষ্টা চালিয়ে যায়। CI-তে এই flag কাজের।
  5. রাফি শেষ করলেন। Job ২৫ সেকেন্ডে শেষ, state লেখা হলো, lock ছাড়া হলো। HOLDER আবার ফাঁকা। মিতুর চলমান plan এবার lock পেল। পুরো অপেক্ষাসহ সেটি এই run-এ ৩১ সেকেন্ড নিল।
  6. আসল শিক্ষা। Lock কাজ করেছে, তবু plan বলছে 1 to destroy! মিতুর code-এ রাফির Job নেই, অথচ state-এ এখন আছে। Lock state-কে বাঁচায়, পুরোনো code থেকে বাঁচায় না। মিতুকে আগে রাফির commit টেনে আনতে হবে। Plan পড়া তাই বাধ্যতামূলক।

নিজে বদলে দেখুন: lock ছাড়া আর lock-সহ

নিচের simulator একটি সরল মডেল: প্রত্যেকে একবার state পড়েন, শেষে একবার লেখেন। সত্যিকারের clock নেই, সেকেন্ডগুলো কল্পিত। start বা lockTimeout বদলে আবার চালান।

const JOBS = [
  { who: "Rafi", start: 0, read: 2, write: 20, adds: "queue" },
  { who: "Mitu", start: 6, read: 8, write: 14, adds: "cache" },
];
function run(useLock, lockTimeout) {
  let state = { serial: 7, items: ["db"] };
  const real = ["db"], log = [];
  let holder = null;
  const live = JOBS.map((j) => ({ ...j, shift: 0, snap: null, done: false }));
  for (let t = 0; t <= 40; t++) {
    for (const j of live) {
      if (j.done || t < j.start) continue;
      if (useLock && holder !== j.who) {
        if (holder === null) { holder = j.who; j.shift = t - j.start; log.push(t + "s " + j.who + " takes the lock"); }
        else if (t - j.start >= lockTimeout) { j.done = true; log.push(t + "s " + j.who + " Error acquiring the state lock"); continue; }
        else continue;
      }
      const at = t - j.shift;
      if (at === j.read) { j.snap = { ...state, items: [...state.items] }; log.push(t + "s " + j.who + " reads serial " + j.snap.serial); }
      if (at === j.write) {
        real.push(j.adds);
        state = { serial: j.snap.serial + 1, items: [...j.snap.items, j.adds] };
        log.push(t + "s " + j.who + " creates " + j.adds + ", writes serial " + state.serial);
        j.done = true;
        if (holder === j.who) { holder = null; log.push(t + "s " + j.who + " frees the lock"); }
      }
    }
  }
  console.log(log.join("\n"));
  const orphans = real.filter((x) => !state.items.includes(x));
  console.log("state:", state.items.join(","), "| real:", real.join(","), "| orphans:", orphans.join(",") || "none");
}
console.log("--- no lock ---");            run(false, 0);
console.log("--- lock, no wait ---");      run(true, 0);
console.log("--- lock-timeout=60s ---");   run(true, 60);

Lock ছাড়া শেষ দুই লাইন: 20s Rafi creates queue, writes serial 8 আর state: db,queue | real: db,cache,queue | orphans: cache. দুজনেই serial 7 পড়েছেন, তাই দুজনের লেখাই serial 8, আর মিতুর cache বাস্তবে থেকেও state-এ নেই। lock, no wait-এ মিতু ৬ সেকেন্ডে error পান, কোনো orphan নেই। lock-timeout=60s-এ দুটি কাজই হয়: state: db,queue,cache, serial 9.

-lock=false ব্যবহার করবেন না। এই flag সুরক্ষাটিই খুলে ফেলে। Error-এর লেখাতেও তাই বলা আছে: “this is not recommended”. আর যে backend lock সমর্থন করে না, দলের কাজে সেটি বাছবেন না। Saved plan-এও একটি সুরক্ষা আছে: state বদলে গেলে পুরোনো plan ফাইল চলে না, আসল run-এ এসেছিল Error: Saved plan is stale. একে বলে stale plan.

৫ · State-এর কমান্ড আর moved — নাম বদলালে ধ্বংস নয়

State সম্পাদনা করতে কখনো JSON খুলবেন না। এই চারটি কমান্ডই যথেষ্ট:

কমান্ডযা করেবাস্তবে কিছু বদলায়?
terraform state listState-এ থাকা সব address দেখায়না
terraform state show ADDRএকটি resource-এর জমা মান দেখায়না
terraform state mv A BState-এ নাম বদলায়, config নয়না, তবে config না মিললে পরের plan বিপদ
terraform state rm ADDRState থেকে ভুলে যায়না, জিনিসটি বাস্তবে থাকে
$ terraform state list
kubernetes_config_map_v1.web_config
kubernetes_secret_v1.db

$ terraform state rm kubernetes_config_map_v1.web_config
Removed kubernetes_config_map_v1.web_config
Successfully removed 1 resource instance(s).

$ kubectl get cm boighor-config -n abcl-ts
NAME             DATA   AGE
boighor-config   1      23s

$ terraform apply -auto-approve
…
Plan: 1 to add, 0 to change, 0 to destroy.
kubernetes_config_map_v1.web_config: Creating...

Error: configmaps "boighor-config" already exists

state rm জিনিসটি মুছল না, শুধু খতিয়ান থেকে নাম কাটল। ConfigMap এখনো আছে। তাই Terraform নতুন একটি বানাতে গিয়ে ধাক্কা খেল। state rm-এর সাথে -dry-run দিলে শুধু Would remove … লেখে, কিছু বদলায় না। অভ্যাস করে নিন।

নাম বদলানোর ফাঁদ

Code পরিষ্কার করতে গিয়ে আপনি app নামটি বদলে web_config করলেন। আপনার কাছে এটি শুধু নাম। Terraform-এর কাছে পুরোনো ঠিকানা গেল, নতুন ঠিকানা এলো। সে ধরে নেয় একটি মুছতে হবে, একটি বানাতে হবে। ConfigMap-এ তেমন ক্ষতি নেই। ভাবুন resource-টি একটি database!

দোকানের সাইনবোর্ড বদলাতে কেউ দোকান ভেঙে আবার বানায় না। moved ব্লক হলো সিটি কর্পোরেশনে দেওয়া একটি নোটিশ: “দোকান একই, শুধু নাম বদলেছে।”

তিন জায়গার অবস্থা
Config-এর নাম
State-এর নাম
বাস্তবের uid
  1. শুরু। Config আর state-এ একই নাম app. Plan বলছে কোনো বদল নেই।
  2. শুধু নাম বদলালেন। Plan-এর দাবি: 1 to add, 1 to destroy. Terraform ঠিকই করছে, কারণ তার কাছে নামই পরিচয়। এই plan apply করলে জিনিসটি মুছে নতুন করে তৈরি হতো।
  3. moved যোগ করলেন। এবার plan বলছে has moved to, আর সংখ্যা তিনটিই শূন্য। পার্থক্য শুধু একটি ব্লক।
  4. Apply. শুধু state-এর নাম বদলাল। Resource-এ হাত পড়েনি, uid একই। এটি state mv-এর চেয়ে ভালো, কারণ পরিবর্তনটি code-এ থাকে, review হয়, আর সব environment-এ নিজে থেকে কার্যকর হয়।
  5. ব্লকটি কতদিন? অন্য environment-এর state বা shared module-এর ব্যবহারকারীরা হয়তো এখনো পুরোনো নামে আছেন। Terraform-এর নথিও তাই ব্লকটি রেখে দিতে বলে, বিশেষত module-এ।
moved {
  from = kubernetes_config_map_v1.app
  to   = kubernetes_config_map_v1.web_config
}

state mv হাতে চালিয়ে config না বদলালে কী হয়? আসল run-এ web_config-কে app2 নামে সরানোর পরে plan বলল: Plan: 1 to add, 0 to change, 1 to destroy. State আর config আবার আলাদা হয়ে গেছে। moved Terraform ১.১ (২০২১) থেকে আছে। এটি এক state-এর ভেতরেই কাজ করে, দুই state-এর মধ্যে নয়। এছাড়া resource-কে module-এ ঢোকানোর সময়ও এটি কাজে লাগে, যা পর্ব ৫৪-তে দেখবেন।

নিজে বদলে দেখুন: rename-এর plan

Plan মানে ঠিকানা মেলানো। নিচে একটি ছোট মডেল আছে, যেখানে একটি database-এর নাম বদলানো হয়েছে। config বা moved তালিকা বদলে চালান।

// Plan = compare configuration addresses with state addresses.
// Change `config` or `moved`, then run again.
const state = {
  "managed_db.main": { id: "db-0a1b", holdsData: true },
  "local_cfg.app": { id: "cfg-77", holdsData: false },
};
const config = ["managed_db.primary", "local_cfg.app"];   // main was renamed
const SCENARIOS = {
  "no moved block": [],
  "with moved block": [{ from: "managed_db.main", to: "managed_db.primary" }],
};
function plan(moved) {
  const st = structuredClone(state), lines = [];
  for (const m of moved) {
    if (st[m.from] && !st[m.to]) { st[m.to] = st[m.from]; delete st[m.from];
      lines.push("  ~ moved   " + m.from + " -> " + m.to + " (id " + st[m.to].id + " kept)"); }
    else if (st[m.from]) lines.push("  ! error   " + m.to + " already exists in state");
  }
  let add = 0, destroy = 0, lost = 0;
  for (const addr of config) if (!st[addr]) { add++; lines.push("  + create  " + addr + " (new id)"); }
  for (const addr of Object.keys(st)) if (!config.includes(addr)) {
    destroy++; if (st[addr].holdsData) lost++;
    lines.push("  - destroy " + addr + " (id " + st[addr].id + ")" + (st[addr].holdsData ? " DATA LOST" : ""));
  }
  lines.push("  Plan: " + add + " to add, " + destroy + " to destroy, " + lost + " data store(s) lost");
  return lines.join("\n");
}
for (const [name, moved] of Object.entries(SCENARIOS)) console.log(name + ":\n" + plan(moved));

ব্লক ছাড়া: - destroy managed_db.main (id db-0a1b) DATA LOST আর Plan: 1 to add, 1 to destroy, 1 data store(s) lost. ব্লকসহ: ~ moved managed_db.main -> managed_db.primary (id db-0a1b kept) আর সব শূন্য। Id একই থাকাই আসল কথা, কারণ data থাকে id-র পেছনের জিনিসটিতে, নামে নয়।

৬ · import আর removed — পুরোনো জিনিস আনা, দায়িত্ব ছাড়া

বাস্তবে অনেক কিছুই Terraform দিয়ে বানানো হয়নি। কেউ console-এ ক্লিক করে database বানিয়েছেন, কেউ kubectl চালিয়েছেন। Terraform এগুলো দেখতেই পায় না, কারণ সে শুধু নিজের state-এর জিনিস পড়ে।

দাদার আমলের বাড়ি আছে, কিন্তু খতিয়ানে ওঠেনি। বাড়ি ভাঙার দরকার নেই, শুধু মাপজোক করে নথিতে তুলতে হবে। Import ঠিক সেই কাজ।

দুটি পথ আছে। পুরোনো পথ terraform import ADDR ID কমান্ড: সঙ্গে সঙ্গে state বদলায়, কোনো plan নেই, code-এ কোনো চিহ্ন থাকে না। নতুন পথ import ব্লক (Terraform ১.৫, ২০২৩): plan-এ আগে দেখা যায়, review করা যায়, আর config-ও বানিয়ে দিতে পারে। Boighor-এর একজন কর্মী kubectl দিয়ে feature flag-এর একটি ConfigMap বানিয়েছিলেন। সেটিই আনি:

তিন জায়গার অবস্থা
Cluster
Config
State
  1. হাতে বানানো জিনিস। ConfigMap cluster-এ আছে, কিন্তু config বা state-এ নেই।
  2. Terraform অন্ধ। Plan বলছে কোনো বদল নেই। এই “No changes” মানে “আমি যা জানি তা ঠিক আছে”, এর বেশি নয়। হাতে বানানো জিনিস drift হিসেবে ধরা পড়ে না।
  3. Import ব্লক লিখলেন। to হলো নতুন ঠিকানা, id হলো provider-এর পরিচয়। Id-র ফরম্যাট resource-ভেদে আলাদা, এখানে namespace/name. Provider-এর নথিতে এটি পাবেন।
  4. Config তৈরি। -generate-config-out বাস্তব থেকে HCL লিখে দিল। Terraform নিজেই বলছে এটি experimental. তাই এই ফাইল পড়ে পরিষ্কার করে নেওয়া আপনার কাজ। এখনো কিছুই বদলায়নি।
  5. Apply. লক্ষ করুন সংখ্যাটি: 1 imported, 0 added. Cluster-এ কিছু তৈরি হয়নি, শুধু state-এ নতুন লাইন এসেছে।
  6. যাচাই। Plan-এ No changes এলে import সম্পূর্ণ। এখন import ব্লকটি মুছে ফেলা যায়।

তৈরি হওয়া ফাইলটি এমন। binary_data = {} বা generate_name = null-এর মতো বাড়তি লাইন পরিষ্কার করতে হবে, আর আসল মানগুলো মিলিয়ে দেখতে হবে:

# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "abcl-ts/boighor-flags"
resource "kubernetes_config_map_v1" "flags" {
  binary_data = {}
  data = {
    MAX_CART     = "20"
    NEW_CHECKOUT = "off"
  }
  immutable = false
  metadata {
    annotations   = {}
    generate_name = null
    labels        = {}
    name          = "boighor-flags"
    namespace     = "abcl-ts"
  }
}

Import-এর পরে plan সবসময় শূন্য হয় না। আসল run-এ একটি Secret import করতেই Plan: 1 to import, 0 to add, 1 to change, 0 to destroy এলো। কারণ wait_for_service_account_token-এর মতো কিছু argument Kubernetes-এ থাকে না, শুধু provider-এর নিজের সেটিং। এমন বদল নিরীহ। কিন্তু plan-এ destroy বা replace দেখলে থামুন, আর config ঠিক করে আবার plan চালান।

removed — Terraform-এর হাত থেকে ছেড়ে দেওয়া

কখনো উল্টোটি লাগে: জিনিসটি থাকবে, শুধু Terraform আর দেখবে না। removed ব্লক (Terraform ১.৭, ২০২৪) এই কাজ review-যোগ্যভাবে করে, state rm-এর মতো হঠাৎ নয়।

removed {
  from = kubernetes_config_map_v1.flags

  lifecycle {
    destroy = false
  }
}
$ terraform plan
…
Plan: 0 to add, 0 to change, 0 to destroy.

Warning: Some objects will no longer be managed by Terraform

If you apply this plan, Terraform will discard its tracking information for
the following objects, but it will not delete them:
 - kubernetes_config_map_v1.flags
…
$ terraform apply -auto-approve
…
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
$ kubectl get cm boighor-flags -n abcl-ts
NAME            DATA   AGE
boighor-flags   2      49s

Plan-এ শূন্য, তবু সতর্কবার্তা আছে। State থেকে নাম গেল, ConfigMap রইল। সাবধান: আমরা lifecycle ছাড়া একই ব্লক চালিয়ে দেখেছি, তখন plan এলো Plan: 0 to add, 0 to change, 1 to destroy. অর্থাৎ destroy = false না লিখলে জিনিসটি মুছে যাবে।

৭ · Drift — বাইরের হাতের ছাপ ধরা

পর্ব ৪২-এর Kubernetes controller সারাক্ষণ বাস্তব মিলিয়ে দেখে। Terraform তা করে না। সে কাজ করে শুধু আপনি চালালে। তাই মাঝখানে কেউ হাতে কিছু বদলালে drift জমতে থাকে (পর্ব ৫১)। রাত ২টায় incident-এ কেউ LOG_LEVEL debug করে দিলেন, আর ফেরাতে ভুলে গেলেন:

$ kubectl patch configmap boighor-config -n abcl-ts --type merge \
    -p '{"data":{"LOG_LEVEL":"debug"}}'
configmap/boighor-config patched

$ terraform plan -refresh-only
…
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_config_map_v1.app has changed
  ~ resource "kubernetes_config_map_v1" "app" {
      ~ data        = {
          ~ "LOG_LEVEL" = "info" -> "debug"
        }
        id          = "abcl-ts/boighor-config"
        …
    }
…
This is a refresh-only plan, so Terraform will not take any actions to undo
these. If you were expecting these changes then you can apply this plan to
record the updated values in the Terraform state without changing any remote
objects.

-refresh-only শুধু “বাস্তব বনাম state” দেখায়, কিছু করতে চায় না। আসল আউটপুটে কিছু শব্দও ছিল, যেমন + annotations = {}. Provider-এর এমন নিরীহ পার্থক্য ছেঁটে দিয়েছি। বাস্তবেও এগুলো ছেঁকে পড়তে হয়।

এবার সিদ্ধান্ত আপনার। দুটি পথ, আর কোনটি নেবেন তা নির্ভর করে বদলটি ঠিক ছিল কি না তার ওপর:

পরিস্থিতিযা করবেনফল
হাতের বদল ভুলterraform applyবাস্তব আবার config-এর মতো: "debug" -> "info"
হাতের বদল ঠিকCode বদলান, তারপর planConfig-ই নতুন সত্য, plan শূন্য
শুধু state মেলাতে চানterraform apply -refresh-onlyState বদলায়, কিন্তু code না বদলালে পরের plan বাস্তবকে আবার ফেরাতে চাইবে
$ terraform plan -detailed-exitcode
…
  ~ resource "kubernetes_config_map_v1" "app" {
      ~ data        = {
          ~ "LOG_LEVEL" = "debug" -> "info"
        }
…
Plan: 0 to add, 1 to change, 0 to destroy.
$ echo $?
2

$ terraform apply -refresh-only -auto-approve
…
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
$ terraform plan
…
Plan: 0 to add, 1 to change, 0 to destroy.

-detailed-exitcode script-এর জন্য: ০ মানে বদল নেই, ১ মানে error, ২ মানে বদল আছে। রাতে একটি scheduled pipeline-এ এটি চালালে drift ধরার সহজ alert পাওয়া যায়। আর দ্বিতীয় অংশটি দেখুন: apply -refresh-only state-এ debug তুলে নিলেও plan থামেনি, কারণ code এখনো info চায়। Drift যদি resource মুছে ফেলার হয়, plan দেখায় will be created. আমাদের run-এ ঠিক সেটাই হয়েছিল: Plan: 1 to add, 0 to change, 0 to destroy.

৮ · দুর্ঘটনা — আটকে থাকা lock আর হারানো state

Apply মাঝপথে মারা গেলে

CI runner মরে গেল, ল্যাপটপের ব্যাটারি শেষ। তখন lock ছাড়ার কেউ থাকে না। আমরা ইচ্ছা করে একটি apply-এর প্রক্রিয়া ৭ সেকেন্ডে মেরে ফেললাম, যখন একটি ৩০ সেকেন্ডের Job তৈরি হচ্ছিল:

$ kubectl get lease -n abcl-ts
NAME                           HOLDER                                 AGE
lock-tfstate-default-boighor   fd708136-ffdd-8202-d9da-6ff710b0bb0f   87s
$ kubectl get job boighor-reindex -n abcl-ts
NAME              STATUS    COMPLETIONS   DURATION   AGE
boighor-reindex   Running   0/1           7s         7s

$ terraform plan
Error: Error acquiring the state lock
…
$ terraform force-unlock -force 11111111-2222-3333-4444-555555555555
Failed to unlock state: lock id "11111111-2222-3333-4444-555555555555" does not match existing lock
…
$ terraform force-unlock -force fd708136-ffdd-8202-d9da-6ff710b0bb0f
Terraform state has been successfully unlocked!
…
$ terraform plan
  # kubernetes_job_v1.reindex will be created
Plan: 1 to add, 0 to change, 0 to destroy.
$ terraform apply -auto-approve
…
Error: Failed to create Job! API error: jobs.batch "boighor-reindex" already exists

$ terraform import kubernetes_job_v1.reindex abcl-ts/boighor-reindex
…
Import successful!

Holder রয়ে গেছে, কারণ ছাড়ার মতো কেউ ছিল না। এই backend-এ lock নিজে থেকে মেয়াদ শেষ হয়নি। force-unlock ঠিক ID না পেলে কিছু করে না, এটি একটি ভালো সুরক্ষা। কিন্তু lock সরানোই শেষ নয়। মরে যাওয়া apply Job পাঠিয়ে দিয়েছিল, state-এ লেখার সময় পায়নি। ফলে Job বাস্তবে চলছে, state-এ নেই। তাই import করতে হলো। এই লম্বা গল্পটিই force-unlock-এর আসল ঝুঁকি।

force-unlock-এর আগে তিনটি প্রশ্ন: Lock Info-তে কার নাম? তিনি কি সত্যিই কাজ করছেন না, মানে তাঁকে বা CI-কে জিজ্ঞেস করেছেন? ID কি আপনার নিজের চোখে দেখা? চলমান apply-এর lock সরালে দুজন একসাথে লিখবেন, আর আপনি নিজেই সেই race বানাবেন, যা lock থামাতে এসেছিল। শেষে plan দিয়ে state আর বাস্তব মিলিয়ে নিন।

State হারিয়ে গেলে

ধরুন state নেই, জিনিসগুলো আছে। আমরা একই code একটি খালি state নিয়ে চালালাম:

$ terraform plan
  # kubernetes_config_map_v1.web_config will be created
  # kubernetes_secret_v1.db will be created
Plan: 2 to add, 0 to change, 0 to destroy.

$ terraform apply -auto-approve
…
Error: configmaps "boighor-config" already exists
Error: secrets "boighor-db" already exists

# recover.tf has one import block for each resource
$ terraform plan
…
Plan: 2 to import, 0 to add, 1 to change, 0 to destroy.
$ terraform apply -auto-approve
…
Apply complete! Resources: 2 imported, 0 added, 1 changed, 0 destroyed.
$ terraform plan
No changes. Your infrastructure matches the configuration.

Kubernetes-এ নাম একই হলে error আসে, তাই বেঁচে গেলাম। অনেক cloud resource-এ নাম অনন্য হওয়া বাধ্যতামূলক নয়। সেখানে এই apply নীরবে দ্বিতীয় একটি সেট বানিয়ে ফেলতে পারে, আর বিল দ্বিগুণ হতে পারে। তাই state হারানো সন্দেহ হলে apply নয়, আগে plan পড়ুন।

উদ্ধারের ক্রম, সহজ থেকে কঠিন:

  1. Storage-এর version থেকে ফেরানো: Versioning চালু থাকলে আগের কপি বের করুন, এটিই সবচেয়ে সহজ পথ।
  2. Backup থেকে: terraform.tfstate.backup, অথবা আগে নেওয়া terraform state pull.
  3. Import করে নতুন state গড়া: বাস্তবের তালিকা বানিয়ে প্রতিটির জন্য import ব্লক। Tag থাকলে তালিকা বানানো অনেক সহজ হয় (পর্ব ৩৫)।

terraform state push দিয়ে কপি ফেরানো যায়, তবে Terraform ভুল কপি আটকে দেয়। Serial আর lineage এখানেই কাজে লাগে:

$ terraform state push older.json
Failed to write state: cannot import state with serial 17 over newer state with serial 18
$ terraform state push other.json
Failed to write state: cannot import state with lineage "00000000-1111-2222-3333-444444444444" over unrelated state with lineage "66e65cd8-cf8f-dc59-1527-03a976d8c15f"

পুরোনো serial বা অন্য lineage-এর কপি বসানো গেল না। ইচ্ছা করে বসাতে -force flag আছে, কিন্তু সেটি এই সুরক্ষা বন্ধ করে। আগে কপিটি মিলিয়ে দেখুন। Backup ফাইলেও গোপন মান থাকে, তাই সেটিও নিরাপদে রাখতে হবে।

৯ · State ভাগ করা — blast radius আর stack-এর মধ্যে তথ্য

পুরো কোম্পানির সব infrastructure এক state-এ রাখলে শুরুতে আরাম। পরে তিনটি সমস্যা আসে। প্রতিটি plan হাজার জিনিস পড়ে, তাই ধীর। একজনের apply-তে সবাই আটকে থাকেন। আর একটি ভুল plan-এর নাগালে network, database, সবকিছু চলে আসে। এই নাগালকেই বলে blast radius.

লঞ্চের তলা কয়েকটি জলরোধী কামরায় ভাগ করা থাকে। এক কামরায় পানি ঢুকলে পুরো লঞ্চ ডোবে না। State ভাগ করাও তাই। ভাগ করার তিনটি প্রশ্ন: কত ঘন ঘন বদলায়, কোন দল মালিক, আর ভুল হলে ক্ষতি কত? Network মাসে একবার বদলায়, ক্ষতি বিশাল। App দিনে দশবার বদলায়, ক্ষতি ছোট। এদের এক কামরায় রাখা ঠিক নয়।

নিচের simulator-এর সব সংখ্যা ধরে নেওয়া, মাপা নয়। এক হাজার resource-এর একটি সিস্টেম এক state-এ আর চার ভাগে কেমন দাঁড়ায়, দেখুন। stacks বদলে apps-কে আরও ভাগ করে চালান।

// Assumed numbers, not measurements. Change the stacks and run again.
const REFRESH_SEC = 0.5;     // one read call for one resource
const PARALLEL = 10;         // terraform's default -parallelism
const APPLY_MIN = 6;         // minutes the lock is held per apply
const stacks = [
  { name: "network", resources: 60, changesPerDay: 0.2 },
  { name: "database", resources: 40, changesPerDay: 0.5 },
  { name: "platform", resources: 200, changesPerDay: 2 },
  { name: "apps", resources: 700, changesPerDay: 12 },
];
const refreshSec = (n) => Math.ceil(n / PARALLEL) * REFRESH_SEC;
function report(name, n, changes, share) {
  const busyMin = changes * APPLY_MIN;
  console.log(name.padEnd(10), String(n).padStart(5), (refreshSec(n).toFixed(1) + "s").padStart(8),
    String(busyMin.toFixed(0)).padStart(6) + "m", (share * 100).toFixed(1).padStart(6) + "%");
}
const total = stacks.reduce((a, s) => a + s.resources, 0);
const allChanges = stacks.reduce((a, s) => a + s.changesPerDay, 0);
console.log("stack      items  refresh  locked  blast");
console.log("-- one state --");
report("all", total, allChanges, 1);
console.log("-- split by blast radius --");
for (const s of stacks) report(s.name, s.resources, s.changesPerDay, s.resources / total);
const weighted = stacks.reduce((a, s) => a + s.changesPerDay * s.resources, 0) / allChanges;
console.log("avg items at risk per apply:", total, "->", Math.round(weighted));
console.log("avg refresh per apply:", refreshSec(total).toFixed(1) + "s ->",
  (stacks.reduce((a, s) => a + s.changesPerDay * refreshSec(s.resources), 0) / allChanges).toFixed(1) + "s");

এক state-এ প্রতিটি apply-এর আগে refresh 50.0s, আর দিনে state আটকে থাকে 88m. ভাগ করলে গড় নেমে আসে: avg refresh per apply: 50.0s -> 30.0s আর avg items at risk per apply: 1000 -> 601. গড় তবু বড়, কারণ বেশিরভাগ বদল হয় apps-এ, যার ৭০০ resource এখনো এক সাথে। তবে আসল জয় হলো শেষ কলামে: database-এর একটি ভুল এখন 4.0%-এ সীমিত, আর network-এর দিকে কোনো app-এর apply হাত দিতে পারে না। হিসাবটি এমন: refresh = ⌈৭০০ ÷ ১০⌉ × ০.৫ = ৩৫ সেকেন্ড।

ভাগ করার দাম আছে। এক stack-এর মান অন্য stack-এ পৌঁছাতে হয়, ক্রম মেনে apply করতে হয়, আর “সব একসাথে” বদল আর এক plan-এ দেখা যায় না। তথ্য পৌঁছানোর তিনটি পথ আছে:

data "terraform_remote_state" "app" {
  backend = "kubernetes"
  config = {
    secret_suffix  = "boighor"
    namespace      = "abcl-ts"
    config_path    = "~/.kube/config"
    config_context = "docker-desktop"
  }
}

data "kubernetes_config_map_v1" "app" {
  metadata {
    name      = "boighor-config"
    namespace = "abcl-ts"
  }
}

output "from_remote_state" {
  value = data.terraform_remote_state.app.outputs.config_map_name
}

output "from_data_source" {
  value = data.kubernetes_config_map_v1.app.data["LOG_LEVEL"]
}
$ terraform apply -auto-approve
…
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

from_data_source = "info"
from_remote_state = "boighor-config"
পথসুবিধাঅসুবিধা
terraform_remote_stateসহজ। প্রকাশক stack output-এ যা দেয়, তা-ই পাওয়া যায়পড়তে পুরো state-এ access লাগে, গোপন মানসহ। দুই stack শক্ত করে জুড়ে যায়
Data sourceসরাসরি বাস্তব থেকে পড়ে। State-এর অনুমতি লাগে নাজিনিসটি খুঁজে পাওয়ার মতো নাম বা tag চাই। প্রকাশক stack জানে না কে নির্ভর করছে
Parameter store বা service registryদুই পাশ আলাদা থাকে। অনুমতি মান ধরে দেওয়া যায়একটি বাড়তি জিনিস চালাতে হয়

Terraform-এর নথিও সতর্ক করে যে terraform_remote_state শুধু output দেখালেও ব্যবহারকারীর পুরো state snapshot-এ access লাগে। তাই ভিন্ন দলের মধ্যে data source বা parameter store নিরাপদ। এক দলের ভেতরের ছোট stack-এ remote state সহজ পথ। Environment অনুযায়ী ভাগ করা আর module পর্ব ৫৪-এর বিষয়।

চলমান state ভাগ করতে moved কাজ করে না, কারণ সেটি এক state-এর ভেতরে। পথটি হলো নতুন stack-এ import, পুরোনোটিতে removed সঙ্গে destroy = false. দুটিই এই পর্বে আসল run-এ দেখেছেন। নিচের ক্রম মিলানোর ধাঁধায় পুরো পথটি পাবেন।

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

  • State কেন লাগে? Config-কে বাস্তবের id-র সাথে মেলানো, metadata রাখা আর দ্রুত plan করার জন্য।
  • Error acquiring the state lock পেলে কী করবেন? Lock Info পড়ে দেখবেন কে ধরে আছেন, অপেক্ষা করবেন। নিশ্চিত হলে তবেই force-unlock.
  • state rm আর terraform destroy-এর পার্থক্য? প্রথমটি শুধু state থেকে ভুলে যায়, দ্বিতীয়টি বাস্তবের জিনিস মুছে ফেলে।
  • নাম বদলালে destroy এড়াবেন কীভাবে? moved ব্লক দিয়ে, plan-এ শূন্য দেখে।
  • Secret কি state-এ থাকে? থাকতে পারে, খোলা লেখায়। তাই backend-এ encryption আর ছোট অনুমতি, সম্ভব হলে write-only argument বা secret manager.

১০ · AI-এর যুগে state

AI খুব দ্রুত HCL লেখে, কিন্তু আপনার state সে দেখতে পায় না। আর state-এর ভুল একটি কমান্ডেই ব্যয়বহুল হতে পারে। সাধারণ ভুলগুলো এমন:

AI যা দিতে পারেবিপদযা করবেন
Resource-এর নাম বদলে দিল, moved নেইDatabase মুছে নতুন তৈরিPlan-এ destroy খুঁজুন
Lock error দেখে সরাসরি force-unlock বা -lock=falseচলমান apply-এর সাথে সংঘর্ষআগে holder-কে খুঁজুন
State ঠিক করতে state rm বা terraform destroyOrphan অথবা সত্যিকারের ক্ষতি-dry-run আর plan, আগে backup
Backend bucket public বা versioning ছাড়াSecret ফাঁস, state ফেরানো যায় নানিজে policy পড়ুন
.gitignore-এ state নেই, Secret সরাসরি HCL-এPassword Git ইতিহাসেSecret manager, write-only argument
নেই এমন backend argument বা ভুল import id ফরম্যাটসময় নষ্ট, ভুল object importসরকারি নথি মিলিয়ে দেখুন

State-এর সমস্যায় AI-কে পুরো state কখনো দেবেন না, কারণ তাতে গোপন মান থাকতে পারে। শুধু address-এর তালিকা আর plan-এর ছাঁটা অংশ দিন, আর ধ্বংসাত্মক কমান্ড আগে চিনে নিতে বলুন:

আমি Terraform-এ একটি resource-এর নাম বদলাতে চাই। নিচে আমার terraform state list আর plan-এর সারাংশ আছে, কোনো secret নেই। [এখানে address-এর তালিকা ও plan-এর শেষ লাইন] ১. moved ব্লক লিখে দাও, আর বলো plan-এ আমি ঠিক কী দেখলে নিরাপদ। ২. কোনো কমান্ড state বা বাস্তবের জিনিস বদলালে আলাদা করে চিহ্নিত করো। ৩. destroy, state rm, force-unlock বা -lock=false প্রস্তাব কোরো না, যদি না কারণ ও ঝুঁকি স্পষ্ট করে বলো। ৪. যে argument নিয়ে নিশ্চিত নও, তা স্পষ্ট বলো, আর সরকারি নথির কোন পাতায় দেখব বলো।

AI-এর দেওয়া state-সংক্রান্ত কাজ হাতে পেলে

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

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

প্র ০১Cloud-এর API তো বলেই দিতে পারে কী কী আছে। তাহলে Terraform-এর আলাদা state কেন লাগে?

প্রশ্নটি যুক্তিসঙ্গত, আর Terraform-এর আগে-পরে অনেক টুল এই পথেই হেঁটেছে। ২০১২ সালে আসা Ansible সাধারণত কোনো state ফাইল রাখে না। প্রতিবার গিয়ে দেখে, যা নেই তা বানায়। এতে হালকা থাকা যায়, কিন্তু একটি বড় ফাঁক থাকে: আপনি playbook থেকে একটি কাজ মুছে দিলে Ansible জানে না সেই জিনিসটি মুছতে হবে। কারণ “আগে কী বানিয়েছিলাম”, তার স্মৃতি নেই।

Terraform-এর সমস্যা আরও কঠিন। ধরুন সে tag দিয়ে নিজের জিনিস চিনবে। কিন্তু সব resource-এ tag হয় না, আর tag হাতে মুছে ফেলা যায়। কিছু API-তে দুটি জিনিসের নাম একই হতে পারে। Terraform একসাথে শত শত provider-এর সাথে কাজ করে, তাই সব জায়গায় চলে এমন একটি সাধারণ মিলানোর নিয়ম সে বেছে নিয়েছে: নিজের একটি মানচিত্র রাখা। Terraform-এর নথিতেও state-এর উদ্দেশ্যে mapping, metadata আর performance লেখা আছে।

অন্য টুল স্মৃতিটি অন্যত্র রাখে। AWS CloudFormation stack-এর হিসাব provider নিজেই রাখে, তাই ব্যবহারকারী ফাইল সামলান না, কিন্তু এক cloud-এ আটকে থাকেন। Kubernetes-এর স্মৃতি থাকে etcd-তে, আর controller সবসময় বাস্তব মিলিয়ে চলে। অর্থাৎ state এড়ানো যায় না, শুধু কোথায় থাকবে আর কে দেখবে, তা বাছা যায়।

মূল উপলব্ধি: Declarative টুলে “আগে কী ছিল” জানা ছাড়া মুছে ফেলার সিদ্ধান্ত নেওয়া যায় না। State হলো মুছে ফেলার ক্ষমতার দাম, আর সেই দাম হলো একটি গোপন, গুরুত্বপূর্ণ ফাইল সামলানো।

প্র ০২Lock থাকলেই কি দুজনের কাজ পুরোপুরি নিরাপদ? কোন সমস্যা lock ধরতে পারে না?

না। এই পর্বের আসল run-এই তার প্রমাণ আছে। মিতু lock-এর জন্য অপেক্ষা করলেন, lock পেলেন, আর plan এলো 1 to destroy. Lock শুধু state-এর লেখা ক্রমে সাজায়। Git-এর code পুরোনো কি না, তা সে জানে না। সমাধান lock-এর বাইরে: একটি মূল branch থেকে একটি pipeline apply চালাবে, ল্যাপটপ থেকে নয়।

দ্বিতীয় ফাঁক হলো plan আর apply-এর মাঝের সময়। Plan চালিয়ে আপনি review-এর জন্য এক ঘণ্টা অপেক্ষা করলেন, এর মধ্যে অন্য কেউ apply করলেন। Terraform এখানে serial দেখে saved plan আটকে দেয়, আসল run-এ এসেছিল Error: Saved plan is stale. তাই -out দিয়ে plan বাঁচিয়ে সেই ফাইলই apply করা ভালো অভ্যাস।

তৃতীয় ফাঁক দূরের সমস্যা। Distributed system-এ lock-এর একটি পুরোনো ঝামেলা হলো, holder মরে গেছে মনে করে lock সরালেন, অথচ সে আসলে ধীর ছিল, আর ফিরে এসে লিখে ফেলল। Martin Kleppmann ২০১৬ সালের একটি পরিচিত লেখায় দেখিয়েছিলেন, এজন্য শুধু lock নয়, fencing token-এর মতো একটি বাড়তে থাকা সংখ্যা লাগে, যা storage যাচাই করবে। Terraform-এর serial এই ধারণার কাছাকাছি, তবে সাধারণ write-এ সব backend এমন কড়া যাচাই করে না বলেই ধরে নেওয়া নিরাপদ। System Design কোর্সের coordination পর্বে এই আলোচনা আছে।

চতুর্থ ফাঁক: lock শুধু Terraform-এর মধ্যে। কেউ console থেকে একই জিনিস বদলালে lock জানবেই না। সেটিই drift.

মূল উপলব্ধি: Lock একটি সরু সুরক্ষা: এক state-এ এক লেখক। পুরো নিরাপত্তা আসে একটি পথ থেকে: code review, এক pipeline, plan পড়া, আর হাতে বদল বন্ধ রাখা।

প্র ০৩Terraform কি drift দেখলেই নিজে থেকে ঠিক করে দেবে, Kubernetes controller-এর মতো? এটি কি ভালো হতো?

Terraform নিজে একটি কমান্ড-চালিত টুল। চালালে কাজ করে, থেমে যায়। Kubernetes controller উল্টো: সারাক্ষণ চলে, আর বদল দেখলেই ফিরিয়ে আনে। দ্বিতীয় পথটি শুনতে সুন্দর। তাহলে সবাই কেন Terraform-কে cron-এ apply -auto-approve দিয়ে চালান না?

কারণ দুটি পরিস্থিতির ঝুঁকি আলাদা। Pod মুছে আবার বানালে সাধারণত ক্ষতি নেই। কিন্তু Terraform-এর plan-এ থাকতে পারে database replace বা subnet মুছে ফেলা। রাত ৩টায় কেউ না পড়েই এটি চললে একটি ছোট drift বড় outage হয়ে যেতে পারে। আবার incident-এর সময় একজন engineer জরুরি হাতে একটি security group খুলেছেন, ঠিক তখনই cron সেটি বন্ধ করে দিলে আগুনে ঘি পড়ে।

তাই অনেক দল মাঝের পথ নেন। Scheduled pipeline-এ plan -refresh-only বা plan -detailed-exitcode চলে, exit code 2 হলে alert যায়, মানুষ দেখে সিদ্ধান্ত নেন। ছোট ও নিরাপদ resource-এ কিছু দল auto-apply করেন। HCP Terraform-এর মতো কিছু managed service-এ drift detection সুবিধা আছে বলে জানি। GitOps-এর টুল (পর্ব ৬০) Kubernetes-এর জগতে সত্যিই একটানা মিলিয়ে চলে, কারণ সেখানে নিচের স্তরটি নিজেই reconcile করতে তৈরি।

হাতের বদল পুরো বন্ধ করাও একটি উত্তর। Cloud-এ মানুষের লেখার অনুমতি কমিয়ে শুধু pipeline-কে দিলে drift-এর উৎসই ছোট হয়। তবে জরুরি প্রবেশের একটি পথ রাখতে হয়, যার audit log থাকে।

মূল উপলব্ধি: স্বয়ংক্রিয় সংশোধন তখনই ভালো, যখন সংশোধনের ক্ষতি ছোট। Drift ধরা সবসময় করুন, কিন্তু সারানোর সিদ্ধান্ত resource-এর ঝুঁকি দেখে নিন।

প্র ০৪Terraform state-এ secret খোলা থাকে কেন? এত বছরেও এটি পুরো সমাধান হলো না কেন?

কারণটি নকশার গভীরে। Terraform-কে জানতে হয় বাস্তব config-এর সাথে মিলছে কি না। Password বদলেছে কি না বুঝতে হলে পুরোনো মান রাখতে হয়। তাই state-এ সব attribute জমা হয়, আর provider কোনো মান ফেরত দিলে সেটিও। এছাড়া database তৈরির সময় cloud যদি নিজে একটি password বানায়, সেটি ফেরত আসে শুধু একবার, আর পরে কাজে লাগে state থেকেই।

এই বিষয়ে Terraform-এর GitHub-এ ২০১৪ সাল থেকে খোলা একটি বিখ্যাত আলোচনা ছিল (issue #516), যেখানে বছরের পর বছর মানুষ encryption চেয়েছেন। পাল্টা যুক্তি ছিল: ফাইল encrypt করলেও Terraform-কে চালাতে key লাগবে, আর যাঁর কাছে apply করার অনুমতি আছে তিনি কার্যত সবই পাবেন। সেজন্য HashiCorp জোর দিয়েছিল backend-এর encryption আর অনুমতির ওপর।

পরে দুটি পথ এলো। প্রথমত, Terraform ১.১০-১.১১ যুগে ephemeral value আর write-only argument: মান একেবারেই জমা না করা, যা আপনি data_wo-তে দেখলেন। এর দাম হলো Terraform আর বদল ধরতে পারে না, তাই data_wo_revision হাতে বাড়াতে হয়। দ্বিতীয়ত, OpenTofu ২০২৪ সালে client-side state encryption আনল, যেখানে backend-এর মালিকও লেখা পড়তে পারেন না।

বাস্তবে সবচেয়ে ভালো নকশা হলো Terraform-এর হাতে secret না দেওয়া। Terraform বানাবে secret-এর খালি পাত্র আর অনুমতি, মান বসাবে অন্য প্রক্রিয়া বা rotation (পর্ব ৩৩)। Workload-এর জন্য temporary credential ব্যবহার করলে জমা রাখার মতো secret-ই কমে যায়।

মূল উপলব্ধি: যে টুল তুলনা করতে চায়, তাকে মান মনে রাখতে হয়। তাই লক্ষ্য হলো state-এ যত কম secret ঢোকে, আর ঢুকলে কে পড়তে পারে তা ছোট রাখা।

প্র ০৫State কত ছোট করে ভাগ করবেন? প্রতিটি microservice-এর জন্য আলাদা state কি সবচেয়ে ভালো?

Blast radius-এর গল্পটি পুরোনো। Charity Majors ২০১৬ সালের একটি পরিচিত blog-এ লিখেছিলেন, staging-এর একটি কাজ করতে গিয়ে একই state-এ থাকা production-এর ক্ষতি কীভাবে সামনে আসে, আর শিরোনামেই পরামর্শ ছিল প্রতি environment-এ আলাদা tfstate. Yevgeniy Brikman-এর Terraform: Up & Running বইও environment আর স্তর ধরে ভাগ করার পক্ষে। এই পরামর্শ এখন প্রায় সাধারণ জ্ঞান।

কিন্তু উল্টো দিকের ঝুঁকিও বাস্তব। পঞ্চাশটি ছোট state মানে পঞ্চাশবার apply, ক্রম মনে রাখা, আর output-এর জটিল জাল। Network-এর একটি output বদলালে কোন কোন stack আবার চালাতে হবে, তা কেউ এক plan-এ দেখতে পান না। ফলে ছোট বদল অনেক state জুড়ে অর্ধেক হয়ে পড়ে থাকতে পারে। Terragrunt-এর মতো টুল এই সমন্বয়ের জন্যই জনপ্রিয় হয়েছে। Terraform নিজেও stack-ধরনের সমন্বয়ের দিকে এগোচ্ছে, তবে সেটির অবস্থা version ও পণ্য দেখে যাচাই করা ভালো।

ভালো সীমারেখা আসে কয়েকটি প্রশ্ন থেকে। কোন জিনিসগুলো একসাথে বদলায়? সেগুলো এক state-এ থাক। কোন জিনিসের বদলের হার খুব আলাদা? আলাদা করুন। কোন জিনিস ভুল হলে ক্ষতি ভয়াবহ, যেমন database? আলাদা করে কড়া অনুমতি দিন। মালিক দল আলাদা হলে state-ও আলাদা, যাতে lock আর অনুমতি দলের সাথে মেলে। আর environment সবসময় আলাদা।

Microservice-প্রতি একটি stack প্রায়ই যুক্তিসঙ্গত, যদি দল সেই service-এর মালিক হয় আর shared জিনিস অন্য stack-এ থাকে। কিন্তু দুজনের একটি ছোট দলে পঞ্চাশটি stack সাধারণত বাড়াবাড়ি। Plan যদি কয়েক মিনিট নেয় বা lock-এর জন্য লাইন পড়ে, সেটিই ভাগ করার সংকেত।

মূল উপলব্ধি: State-এর সীমা হলো দল আর ঝুঁকির সীমা, ফাইলের আকারের নয়। ভাগ করলে ঝুঁকি কমে, সমন্বয়ের কাজ বাড়ে। দুটোর হিসাব একসাথে করতে হয়।

অনুশীলন

হাতে-কলমে করতে চাইলে নিজের একটি throwaway ফোল্ডারে local_file resource দিয়েই বেশিরভাগ পরীক্ষা করা যায়। Cloud account লাগবে না। কাজ শেষে ফোল্ডারটি মুছে ফেলুন। Cloud-এ চালালে খরচ হতে পারে, আর terraform destroy দিয়ে পরিষ্কার করতে ভুলবেন না।

  1. হিসাব: একটি state-এ ১২০০ resource. ধরে নিন -parallelism ১০, আর একটি resource পড়তে ০.৫ সেকেন্ড। (ক) Refresh কত সময় নেয়? (খ) এখন দুই ভাগ করা হলো: A-তে ২০০, দিনে ৩টি apply; B-তে ১০০০, দিনে ১টি apply. প্রতি apply-তে গড় refresh সময় আর গড় “ঝুঁকিতে থাকা resource” কত?

    (ক) ১২০০ ÷ ১০ = ১২০ দফা, ১২০ × ০.৫ = ৬০ সেকেন্ড।

    (খ) A: ২০ দফা × ০.৫ = ১০ সেকেন্ড। B: ১০০ × ০.৫ = ৫০ সেকেন্ড। গড় refresh = (৩ × ১০ + ১ × ৫০) ÷ ৪ = ৮০ ÷ ৪ = ২০ সেকেন্ড। গড় ঝুঁকি = (৩ × ২০০ + ১ × ১০০০) ÷ ৪ = ১৬০০ ÷ ৪ = ৪০০ resource, আগে ছিল ১২০০। ভাগ তখনই বেশি কাজের, যখন ঘন ঘন বদলানো জিনিস ছোট ভাগে পড়ে।

  2. ফল অনুমান করুন: lock_race.js-এ শেষ লাইনে run(true, 60) বদলে run(true, 10) করলেন। বাকি সব আগের মতো। শেষ অংশে মিতুর লাইনটি কী হবে, আর শেষ state: লাইন কী হবে?

    মিতু ৬ সেকেন্ডে শুরু করেন, ১০ সেকেন্ড অপেক্ষা করেন, কিন্তু রাফি ২০ সেকেন্ডের আগে ছাড়েন না। তাই ১৬ সেকেন্ডে হার। আসল আউটপুট:

    16s Mitu Error acquiring the state lock
    20s Rafi creates queue, writes serial 8
    20s Rafi frees the lock
    state: db,queue | real: db,queue | orphans: none

    lockTimeout-কে ১৪ বা তার বেশি করলে মিতু lock পেতেন, কারণ তখন অপেক্ষা ২০ সেকেন্ড পর্যন্ত চলে।

  3. পরের অবস্থা অনুমান করুন: State-এ a আর b দুটি resource আছে, config-এও দুটি। আপনি চালালেন terraform state rm b, তারপর terraform plan, তারপর terraform apply -auto-approve. Resource b একটি ConfigMap, যার নাম cluster-এ এখনো আছে। প্রতি ধাপে কী হবে? সারাবেন কীভাবে?

    state rm-এর পরে ConfigMap থাকে, state-এ শুধু a. Plan দেখায় 1 to add, কারণ config-এ b আছে, state-এ নেই। Apply ব্যর্থ হয় already exists error দিয়ে। State-এ b ফেরে না। সমাধান: import ব্লক বা terraform import, তারপর plan-এ No changes. এই পর্বের অংশ ৫ আর ৮-এ ঠিক এই আউটপুট আছে।

  4. নকশা সমালোচনা: একটি দলের প্রস্তাব: “terraform.tfstate Git-এ রাখব, যাতে সবাই পায়। CI-তে -lock=false দেব, কারণ lock error বিরক্ত করে। সব environment এক state-এ রাখব, তাহলে এক plan-এ সব দেখা যাবে। বাইরে থেকে হাতে কিছু বদলালে CI রাতে apply -auto-approve করে ঠিক করে দেবে।” চারটি অংশের সমস্যা আর বিকল্প বলুন।
    • Git-এ state: Secret ইতিহাসে চিরকাল থাকে, আর Git-এ lock নেই। দুই branch-এ দুই state merge করা বিপজ্জনক। বিকল্প: versioning, encryption আর ছোট অনুমতিসহ remote backend.
    • -lock=false: Race সরাসরি খুলে দেয়। Error এলে -lock-timeout দিন, আর এক pipeline-এ ক্রম ঠিক করুন।
    • এক state: Dev-এর একটি ভুল prod ছুঁতে পারে। Environment আলাদা state-এ রাখুন, অনুমতিও আলাদা।
    • রাতে auto-apply: Incident-এর জরুরি বদল মুছে যেতে পারে, destroy-ও কেউ পড়বে না। বিকল্প: plan -detailed-exitcode দিয়ে alert, মানুষের সিদ্ধান্ত।

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

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

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

  1. দুই stack-এ plan চালিয়ে দেখা, কোথাও বাকি বদল নেই
  2. terraform state pull দিয়ে দুই state-এর backup নেওয়া
  3. নতুন stack-এ config আর import ব্লক যোগ করে plan পড়া
  4. নতুন stack-এ apply, ফল 1 imported
  5. পুরোনো stack-এ removed ব্লক, destroy = false, তারপর apply
  6. দুই stack-এ plan চালিয়ে No changes নিশ্চিত করা

Terraform state-এর প্রধান কাজ কোনটি?

  • HCL config-এর কপি রাখা, যাতে .tf ফাইল না লাগে
  • Config-এর resource-কে বাস্তবের object-এর সাথে মিলিয়ে রাখা
  • Cloud-এর খরচের হিসাব রাখা
  • Provider plugin জমা রাখা

একটি Secret resource-এর data attribute terraform state show-এ (sensitive value) দেখাচ্ছে। কোন কথাটি ঠিক?

  • মানটি state ফাইলে encrypted আছে
  • মানটি state ফাইলে একেবারেই নেই
  • মানটি পর্দায় লুকানো, কিন্তু state ফাইলে সাধারণ লেখা হিসেবে থাকতে পারে
  • Remote backend-এ রাখলে মানটি নিজে থেকে মুছে যায়

CI-তে Error acquiring the state lock এলো। সবচেয়ে ভালো প্রথম কাজ কোনটি?

  • Lock Info পড়ে দেখা কে কবে lock নিয়েছেন, আর সেই কাজ চলছে কি না
  • সঙ্গে সঙ্গে terraform force-unlock চালানো
  • -lock=false দিয়ে আবার চালানো
  • অন্য backend-এ state সরিয়ে নেওয়া

HCL-এ resource "x" "main" বদলে resource "x" "primary" করলেন, আর কিছু করলেন না। Plan সাধারণত কী দেখাবে?

  • No changes, কারণ জিনিসটি একই
  • 1 to change, নাম in-place বদলাবে
  • Plan error দিয়ে থেমে যাবে
  • 1 to add, 1 to destroy

terraform plan -refresh-only কী করে?

  • বাস্তবকে config-এর মতো করে ফিরিয়ে আনে
  • বাস্তব আর state-এর অমিল দেখায়, কোনো কিছু বদলানোর প্রস্তাব ছাড়াই
  • Cloud-এর সব অজানা resource খুঁজে বের করে
  • আটকে থাকা lock সরিয়ে দেয়

removed ব্লকে lifecycle { destroy = false } দিয়ে apply করলে কী হয়?

  • Resource state থেকে সরে যায়, বাস্তবের জিনিসটি থাকে
  • বাস্তবের জিনিসটি মুছে যায়, state-এ নাম থাকে
  • বাস্তব আর state, দুই জায়গা থেকেই মুছে যায়
  • কিছুই হয় না, শুধু একটি warning দেখায়

শব্দকোষ

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

মনে রাখুন

  1. State হলো config আর বাস্তবের মাঝের খতিয়ান। হাতে সম্পাদনা নয়, Git-এ নয়, গোপন নথির মতো রাখুন।
  2. দলে remote backend, lock, versioning, encryption আর ছোট অনুমতি একসাথে লাগে।
  3. Error acquiring the state lock সুরক্ষা। force-unlock শেষ উপায়, আর lock পুরোনো code থেকে বাঁচায় না।
  4. moved নাম বদলায়, import আনে, removed ছেড়ে দেয়। তিনটিই plan-এ পড়ে তবে apply করুন।
  5. plan -refresh-only drift দেখায়। State ভাগ করুন blast radius, মালিক দল আর বদলের হার দেখে।

পরের পর্বে: এতক্ষণ একটি একটি resource নিয়ে কাজ করলেন। এবার সেগুলো একটি reusable module-এ বাঁধবেন, variable আর output দিয়ে dev আর prod সাজাবেন, আর moved-কে সত্যিকারের refactor-এ কাজে লাগাবেন।

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

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