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

Terraform-এর মৌলিক — provider, resource, plan আর apply

Terraform basics — HCL, providers, resources, variables, plan, apply and lifecycle

Taught in Bangla, this lesson explains what Terraform actually does, HCL, providers, init and the lock file, resources, addresses and attributes, references and the dependency graph, variables, tfvars, locals and outputs, the fmt, validate, plan, apply and destroy workflow, reading plan symbols, count versus for_each in a real run, and lifecycle rules, with simulators.

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

  • HCL পড়া, version constraint লেখা আর lock file-এর কাজ বোঝা
  • Resource address, argument, attribute, data source আর dependency graph চেনা
  • Variable, tfvars, local আর output ব্যবহার, আর কোন মান জেতে তা বলতে পারা
  • fmt, validate, plan, apply, destroy নিরাপদ ক্রমে চালানো, আর saved plan-এর সুবিধা
  • Plan-এর প্রতিটি চিহ্ন পড়া, আর কোন বদল replace ডেকে আনে তা ধরা
  • for_each কেন count-এর চেয়ে নিরাপদ, আর lifecycle-এর তিনটি নিয়ম কখন লাগে

১ · Terraform আসলে কী করে

আগের পর্বে আপনি IaC-এর ধারণা পেয়েছেন: server বা network হাতে না বানিয়ে ফাইলে লেখা। সে ধারণাকে কাজে নামানোর সবচেয়ে প্রচলিত হাতিয়ারগুলোর একটি Terraform. এটি declarative ধরনের: আপনি বলেন শেষে কী থাকবে, পথ বের করে সে নিজে।

একজন ঠিকাদারের কথা ভাবুন। আপনি বাড়ির নকশা দিলেন। তাঁর কাছে আছে একটি হিসাবের খাতা, যাতে লেখা আগে কী কী বানানো হয়েছে। তিনি নকশা আর খাতা মিলিয়ে কাজের তালিকা বানান: এই দেয়াল নতুন, ওই জানালা বদল। আপনি তালিকা দেখে সই করলে মিস্ত্রিরা কাজে নামেন। রাজমিস্ত্রি আর বিদ্যুৎ মিস্ত্রি আলাদা লোক, কিন্তু ঠিকাদার একজনই। Terraform-এ নকশা হলো .tf ফাইল, খাতা হলো state, তালিকা হলো plan, আর মিস্ত্রি হলো provider.

Config থেকে আসল resource পর্যন্ত

Config and state go into Terraform Core; providers call the APIs; results return to state

Config, state, Core, provider, API আর resource-এর সম্পর্ক .tf ফাইল আপনি যা চান desired state শেষবার যা জেনেছে terraform.tfstate Terraform Core মিলিয়ে দেখে, graph আর plan বানায় kubernetes provider আলাদা plugin প্রোগ্রাম local provider আলাদা plugin প্রোগ্রাম Kubernetes API ফাইল সিস্টেম ConfigMap Deployment report.txt কমলা দাগ: আসল id আর attribute ফিরে state-এ জমা হয়
ছবি ৫২.১ — Core কোনো cloud চেনে না। প্রতিটি API-র ভাষা জানে তার provider.

HashiCorp ২০১৪ সালে Terraform প্রকাশ করে; প্রতিষ্ঠাতাদের একজন Mitchell Hashimoto. শুরুতে লাইসেন্স ছিল MPL 2.0. আগস্ট ২০২৩-এ তা বদলে BUSL হয়, আর তার জবাবে Linux Foundation-এর অধীনে আসে fork OpenTofu. এই পর্বের ধারণা দুটোতেই প্রায় একই, যদিও নতুন feature-এ পার্থক্য তৈরি হতে পারে।

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

কোন কাজে Terraform, কোন কাজে নয়

Terraform-এর শক্তি হলো জিনিসের জন্ম থেকে মৃত্যু পর্যন্ত হিসাব রাখা। তবে সব কাজের জন্য এটি সেরা নয়। সাধারণ ভাগটি এমন, যদিও দলভেদে আলাদা হয়:

কাজপ্রচলিত হাতিয়ারকারণ
VPC, VM, database, DNS, IAM বানানোTerraform, OpenTofu, CloudFormation, BicepCloud resource-এর পুরো জীবন হিসাবে থাকে
Cluster-এর ভেতরে app deploykubectl, Helm, GitOps toolRollout আর ঘনঘন release সেখানেই সহজ
Server-এর ভেতরে package আর configAnsible, machine imageপর্ব ৫৬-এর বিষয়
একবারের ছোট পরীক্ষাConsole বা CLIপরে মুছে ফেললে কোড লেখার দরকার নেই

এই পর্বে আমরা Kubernetes provider ব্যবহার করছি একটি বিনা খরচের পরীক্ষাগার হিসেবে, কারণ এতে আসল API, আসল state আর আসল replace দেখা যায়। এর মানে এই নয় যে production-এ সব app Terraform দিয়েই deploy করতে হবে।

২ · HCL, provider, init আর lock file

Terraform-এর ফাইল লেখা হয় HCL ভাষায়। এর গঠন খুব ছোট। একটি block-এর থাকে ধরন, কিছু label আর { }-এর ভেতরে argument. যেমন resource "local_file" "report" { … }-এ ধরন resource, label দুটি resource type আর আপনার দেওয়া নাম। ফোল্ডারের সব .tf ফাইল মিলে একটি config হয়, তাই ফাইলের নাম শুধু গুছিয়ে রাখার জন্য।

Argument-এর ডান দিকে থাকে একটি expression, অর্থাৎ এমন কিছু যার মান বের করা যায়। পুরো পর্বে এই কয়েক ধরনই বারবার আসবে:

লেখাঅর্থ
"bazar", 3, trueসরাসরি মান: string, number, bool
["dhaka", "sylhet"]List: ক্রম আছে, গোনা হয় ০ থেকে
{ app = "bazar" }Map: নাম আর মানের জোড়া
"${var.app_name}-web"String-এর ভেতরে মান বসানো (interpolation)
var.x, local.x, data.type.name.attrঅন্য জায়গার মান পড়া (reference)
length(var.branches)Built-in function; নিজের function লেখা যায় না
var.enabled ? 1 : 0শর্ত: সত্য হলে ১, নইলে ০
# commentমন্তব্য, Terraform এড়িয়ে যায়

আমরা একটি কাল্পনিক অনলাইন বাজার অ্যাপ bazar-এর ছোট config বানাব। প্রথম ফাইলে terraform block জানায় কোন Terraform আর কোন provider লাগবে।

terraform {
  required_version = ">= 1.9.0"

  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 3.4"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.9"
    }
  }
}

provider "kubernetes" {
  config_path    = "~/.kube/config"
  config_context = "docker-desktop"
}

Provider হলো আলাদা একটি প্রোগ্রাম, যে একটি নির্দিষ্ট API-র ভাষা জানে। provider block তাকে বলে কোথায় যুক্ত হতে হবে। Credential এখানে লিখবেন না; environment বা role থেকে নেওয়াই নিরাপদ, যা পর্ব ৩৩-এর বিষয়।

Version constraint

Version constraint বলে দেয় কোন version চলবে। সবচেয়ে কাজের চিহ্নটি ~>: শুধু ডান দিকের শেষ অংশটি বাড়তে পারবে।

লেখাযা চলবেকখন
= 3.4.0শুধু 3.4.0খুব কড়া নিয়ন্ত্রণ দরকার হলে
>= 3.43.4 বা নতুন, 4.0-ওModule-এ নিচের সীমা বলতে
~> 3.4>= 3.4 এবং < 4.0Minor আপডেট চলবে
~> 3.4.0>= 3.4.0 এবং < 3.5.0শুধু patch চলবে
>= 3.4, < 3.63.4.x আর 3.5.xপরিষ্কার নিচ ও ওপরের সীমা

সীমা ভাঙলে Terraform কাজই শুরু করে না। আমরা ইচ্ছে করে required_version = ">= 2.0.0" লিখে চালালাম।

$ terraform init
Initializing the backend...


Error: Unsupported Terraform Core version

  on v.tf line 2, in terraform:
   2:   required_version = ">= 2.0.0"

This configuration does not support Terraform version 1.17.1. To proceed,
either choose another supported Terraform version or update this version
constraint. …

terraform init আর lock file

terraform init ফোল্ডারটি কাজের জন্য তৈরি করে: constraint মেনে provider নামায়, আর যে version বেছে নিল তা লিখে রাখে।

$ terraform init
Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/kubernetes versions matching "~> 3.4"...
- Finding hashicorp/local versions matching "~> 2.9"...
- Installing hashicorp/kubernetes v3.4.0...
- Installed hashicorp/kubernetes v3.4.0 (signed by HashiCorp)
- Installing hashicorp/local v2.9.1...
- Installed hashicorp/local v2.9.1 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!
…

$ cat .terraform.lock.hcl
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/kubernetes" {
  version     = "3.4.0"
  constraints = "~> 3.4"
  hashes = [
    "h1:iIaKddGgcDoeG/TxxYcIOOnXNm4vkrLoLow3STxfYIs=",
    "zh:02a3156701f8eb7c43acaaf4ac136ddfe55cd86a703a5b2b54c679bf4fe25c02",
    …

Constraint হলো “কোন সীমার মধ্যে চলবে”, আর lock file হলো “ঠিক কোনটি বেছে নেওয়া হলো”। এটি অন্য ecosystem-এর lockfile-এর মতোই। hashes নামানো ফাইলটি যাচাই করে। তাই .terraform.lock.hcl Git-এ রাখুন, কিন্তু .terraform/ ফোল্ডার নয়, কারণ তাতে শুধু নামানো plugin থাকে।

Lock করা version আর নতুন constraint না মিললে কী হয়? আমরা constraint বদলে ~> 2.8.0 করলাম, যদিও lock file-এ আছে 2.9.1.

$ terraform init
…
Error: Failed to query available provider packages

Could not retrieve the list of available versions for provider
hashicorp/local: locked provider registry.terraform.io/hashicorp/local 2.9.1
does not match configured version constraint ~> 2.8.0; must use terraform
init -upgrade to allow selection of new versions

ঠিক কাজটিই হলো: version নিজে থেকে বদলায় না। বদলাতে চাইলে terraform init -upgrade চালিয়ে lock file-এর diff review করতে হয়।

ফাইলGit-এ?কারণ
*.tfহ্যাঁএটিই আপনার কোড
.terraform.lock.hclহ্যাঁসবার একই provider থাকে
.terraform/নানামানো plugin, আবার নামানো যায়
*.tfstate, *.tfstate.backupনাগোপন মান থাকতে পারে
Saved plan (tfplan)নাগোপন মান থাকতে পারে
*.tfvarsগোপন মান থাকলে নাPassword Git-এ গেলে বদলাতে হয়

৩ · Resource, address, argument আর attribute

Resource হলো Terraform-এর পরিচালনায় থাকা একটি জিনিস: একটি VM, একটি bucket বা একটি ConfigMap. নিচের ফাইলে একটি data source, কিছু locals, দুটি Kubernetes resource আর একটি ফাইল আছে।

data "kubernetes_namespace_v1" "team" {
  metadata {
    name = "abcl-tb"
  }
}

locals {
  namespace = data.kubernetes_namespace_v1.team.metadata[0].name
  labels = {
    app        = var.app_name
    managed-by = "terraform"
  }
}

resource "kubernetes_config_map_v1" "settings" {
  metadata {
    name      = "${var.app_name}-settings"
    namespace = local.namespace
    labels    = local.labels
  }

  data = {
    LOG_LEVEL = var.log_level
  }
}

resource "kubernetes_deployment_v1" "web" {
  metadata {
    name      = "${var.app_name}-web"
    namespace = local.namespace
    labels    = local.labels
  }

  spec {
    replicas = var.replicas

    selector {
      match_labels = local.labels
    }

    template {
      metadata {
        labels = local.labels
      }

      spec {
        container {
          name  = "web"
          image = "nginx:1.27-alpine"

          env_from {
            config_map_ref {
              name = kubernetes_config_map_v1.settings.metadata[0].name
            }
          }

          resources {
            requests = { cpu = "10m", memory = "16Mi" }
            limits   = { cpu = "100m", memory = "64Mi" }
          }
        }
      }
    }
  }
}

resource "local_file" "report" {
  filename = "${path.module}/report.txt"
  content  = "settings uid: ${kubernetes_config_map_v1.settings.metadata[0].uid}\n"
}

প্রতিটি resource-এর একটি address থাকে: type.name, যেমন kubernetes_config_map_v1.settings. এটি config-এর ভেতরের নাম। Cluster-এ জিনিসটির নাম bazar-settings. দুটি আলাদা জিনিস, আর এই পার্থক্য পরে কাজে লাগবে।

বিষয়ArgumentAttribute
কে ঠিক করেআপনি লেখেনProvider বা API জানায়
উদাহরণmetadata.name, datametadata[0].uid, id
কখন জানা যায়লেখার সময়প্রায়ই apply-এর পরে
Plan-এ দেখায়মানটি(known after apply)

সব argument আবার attribute হিসেবেও পড়া যায়। আর metadata[0]-এর [0] কেন? কারণ nested block ভেতরে একটি list হিসেবে থাকে, যদিও তাতে একটিই block. known after apply মানে API এখনো মানটি বানায়নি। বানানোর পর state-এ যা জমা হয়, তা দেখুন।

$ terraform state list
data.kubernetes_namespace_v1.team
kubernetes_config_map_v1.settings
kubernetes_deployment_v1.web
local_file.report

$ terraform state show kubernetes_config_map_v1.settings
# kubernetes_config_map_v1.settings:
resource "kubernetes_config_map_v1" "settings" {
    data      = {
        "LOG_LEVEL" = "info"
    }
    id        = "abcl-tb/bazar-settings"
    immutable = false

    metadata {
        generate_name    = null
        generation       = 0
        labels           = {
            "app"        = "bazar"
            "managed-by" = "terraform"
        }
        name             = "bazar-settings"
        namespace        = "abcl-tb"
        resource_version = "18539"
        uid              = "6523526f-59f7-4f35-a771-ddcbcedd0d6c"
    }
}

আপনি uid লেখেননি, Kubernetes বানিয়েছে। State সেটি মনে রেখেছে, তাই অন্য resource ব্যবহার করতে পারে। State-এর পুরো গল্প পর্ব ৫৩-এ। আপাতত একটি কথা: এই ফাইলে গোপন মান সাধারণ লেখায় থাকতে পারে, তাই *.tfstate Git-এ রাখবেন না।

Data source: শুধু পড়া

data block কিছু বানায় না, অন্যের বানানো জিনিসের তথ্য পড়ে। ওপরে namespace abcl-tb আগেই kubectl দিয়ে তৈরি ছিল, তাই Terraform তার মালিক নয়। Plan-এ দেখা যায় data.kubernetes_namespace_v1.team: Read complete after 0s [id=abcl-tb], আর destroy চালালেও namespace-টি থাকে। Cloud-এ একই কৌশলে সর্বশেষ machine image বা অন্য দলের VPC-র id খোঁজা হয়। সুবিধা হলো মান হাতে লিখতে হয় না, আর ঝুঁকি হলো “সর্বশেষ” কিছু পড়লে মান নিজে থেকে বদলাতে পারে, তাই পরের plan-এ অপ্রত্যাশিত বদল আসতে পারে।

৪ · Reference, dependency graph আর cloud-এর একই নকশা

আপনি কোথাও ক্রম লেখেননি। তবু Terraform জানে ConfigMap আগে, Deployment পরে। কারণ Deployment-এ লেখা আছে kubernetes_config_map_v1.settings.metadata[0].name. এই reference-ই একটি implicit dependency. সব মিলিয়ে তৈরি হয় dependency graph, যা নিজেই দেখা যায়।

$ terraform graph
digraph G {
  rankdir = "RL";
  node [shape = rect, fontname = "sans-serif"];
  "data.kubernetes_namespace_v1.team" [label="data.kubernetes_namespace_v1.team"];
  "kubernetes_config_map_v1.settings" [label="kubernetes_config_map_v1.settings"];
  "kubernetes_deployment_v1.web" [label="kubernetes_deployment_v1.web"];
  "local_file.report" [label="local_file.report"];
  "kubernetes_config_map_v1.settings" -> "data.kubernetes_namespace_v1.team";
  "kubernetes_deployment_v1.web" -> "kubernetes_config_map_v1.settings";
  "local_file.report" -> "kubernetes_config_map_v1.settings";
}

এটি DOT ভাষায় লেখা। A -> B মানে A নির্ভর করে B-এর ওপর, অর্থাৎ B আগে। Terraform graph-টির topological sort করে কাজ সাজায়, আর যাদের মধ্যে সম্পর্ক নেই তাদের একসাথে চালায়; ডিফল্ট সীমা ১০টি (-parallelism). Destroy হয় উল্টো ক্রমে। terraform graph -type=plan দিলে variable, local আর provider-সহ পুরো graph মেলে।

কে কার আগে

Arrows show creation order, the reverse of DOT's depends-on arrows

Dependency graph: team, settings, web, report, release_note data: team settings (ConfigMap) web (Deployment) report (file) release_note (file) ভাঙা দাগ: depends_on
ছবি ৫২.২ — একই graph, তৈরির ক্রমে। Report আর web-এর মধ্যে কোনো সম্পর্ক নেই।

depends_on: সম্পর্ক যখন কোডে দেখা যায় না

কখনো একটি জিনিস আরেকটির ওপর নির্ভর করে, অথচ কোনো attribute নেয় না। যেমন rollout শেষে একটি নোট লেখা। তখন depends_on লাগে।

resource "local_file" "release_note" {
  filename = "${path.module}/release.txt"
  content  = "web rollout finished\n"

  depends_on = [kubernetes_deployment_v1.web]
}
$ terraform apply -auto-approve
…
kubernetes_config_map_v1.settings: Creating...
kubernetes_config_map_v1.settings: Creation complete after 0s [id=abcl-tb/bazar-config-v2]
local_file.report: Creating...
local_file.report: Creation complete after 0s [id=879646a5fbcd5f724b1e6eda0721cf68c2edd43c]
kubernetes_deployment_v1.web: Creating...
kubernetes_deployment_v1.web: Creation complete after 2s [id=abcl-tb/bazar-web]
local_file.release_note: Creating...
local_file.release_note: Creation complete after 0s [id=0f62f7c476fe14dc3358807cec29ff55214f0e5a]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Report ফাইল আগেই হয়ে গেল, কারণ সে Deployment-এর ওপর নির্ভর করে না। কিন্তু release_note Deployment শেষ না হওয়া পর্যন্ত অপেক্ষা করল। terraform graph-এও নতুন লাইন এলো: "local_file.release_note" -> "kubernetes_deployment_v1.web";. এই run-এ ConfigMap-এর নাম bazar-config-v2, কারণ এটি ৭ নম্বর অংশের নাম-বদলের পরে চালানো। নিয়ম: reference দিয়ে সম্ভব হলে depends_on লিখবেন না। এটি কম স্পষ্ট, আর বেশি ব্যবহারে plan-এ অকারণে (known after apply) বাড়তে পারে।

Cloud-এও একই নকশা

নিচের নমুনায় একটি VPC, একটি subnet আর একটি VM. এটি চালানো হয়নি, কারণ এই পর্বে কোনো cloud account ব্যবহার হয়নি। শুধু গঠনটি দেখুন: VPC-র id attribute subnet নেয়, subnet-এর id নেয় VM. কাজেই ক্রম আবার নিজে থেকেই তৈরি।

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "ap-south-1"
}

variable "image_id" {
  type    = string
  default = "ami-0123456789abcdef0" # fake id, use a real image
}

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

resource "aws_subnet" "app" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.20.1.0/24"
}

resource "aws_instance" "web" {
  ami           = var.image_id
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.app.id
}
ধারণাAWSAzureGoogle Cloud
Providerawsazurermgoogle
Networkaws_vpcazurerm_virtual_networkgoogle_compute_network
Subnetaws_subnetazurerm_subnetgoogle_compute_subnetwork
VMaws_instanceazurerm_linux_virtual_machinegoogle_compute_instance

Terraform multi-cloud মানে “এক কোড সব cloud-এ” নয়। ভাষা আর workflow এক, কিন্তু resource type ও argument প্রতিটি provider-এর আলাদা। আর cloud-এ এই ফাইল apply করলে আসল টাকা খরচ হতে পারে। নিজে চালালে provider-এর দাম দেখে নিন, budget alert রাখুন, আর শেষে terraform destroy করুন।

৫ · Variable, tfvars, local আর output

একই config dev আর production-এ চালাতে মান বাইরে রাখতে হয়। Input variable হলো config-এর দরজা।

variable "app_name" {
  type        = string
  description = "Short name of the app"
  default     = "bazar"
}

variable "replicas" {
  type    = number
  default = 1

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

variable "log_level" {
  type    = string
  default = "info"
}
output "deployment_name" {
  description = "Name of the web Deployment"
  value       = kubernetes_deployment_v1.web.metadata[0].name
}

output "settings_uid" {
  value = kubernetes_config_map_v1.settings.metadata[0].uid
}
  • Type: string, number, bool, আর গঠনযুক্ত list(string), set(string), map(number), object({ … }).
  • Default: না থাকলে variable-টি বাধ্যতামূলক।
  • Local: locals block-এর নাম দেওয়া হিসাব, যা ফাইলের ভেতরেই থাকে। বাইরে থেকে বদলানো যায় না। ওপরের local.labels একবার লিখে তিন জায়গায় ব্যবহার হয়েছে।
  • Output: apply শেষে দেখানো মান। পরে module বা অন্য tool এগুলো পড়ে, যা পর্ব ৫৪-এ আসবে।

মান না দিলে, ভুল ধরনের মান দিলে বা validation ভাঙলে কী আসে, তিনটিই আসল run থেকে দেখুন। প্রথমটি default-হীন একটি পরীক্ষামূলক variable tier দিয়ে।

$ terraform plan -input=false
Error: No value for required variable

  on main.tf line 21:
  21: variable "tier" {

The root module input variable "tier" is not set, and has no default value.
Use a -var or -var-file command line argument to provide a value for this
variable.

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

  on main.tf line 6:
   6: variable "replicas" {

Unsuitable value for var.replicas set using -var="replicas=...": a number is
required.

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

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

replicas must be between 1 and 6.

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

sensitive কতটুকু লুকায়

Variable বা output-এ sensitive = true দিলে Terraform মানটি পর্দায় দেখায় না। কিন্তু এটি encryption নয়। নিচে একটি নকল token দিয়ে পরীক্ষা করা হলো।

variable "api_token" {
  type      = string
  sensitive = true
  default   = "fake-token-123"
}

output "token_for_tool" {
  value     = var.api_token
  sensitive = true
}
$ terraform output
token_for_tool = <sensitive>

$ grep -n 'fake-token' terraform.tfstate
8:      "value": "fake-token-123",

পর্দায় <sensitive>, অথচ state ফাইলে হুবহু মান। আসল গোপন মান তাই কখনো default-এ নয়, আর state-এর জায়গাটিও সুরক্ষিত হতে হয়। পরের পর্বে এই বিষয়ে আবার আসছে।

কোন মান জেতে

একই variable-এর মান অনেক জায়গা থেকে আসতে পারে। Terraform একটি নির্দিষ্ট ক্রমে পড়ে, আর পরে যেটি আসে সেটিই জেতে। tfvars ফাইল হলো শুধু name = value লেখা ফাইল।

ক্রম (নিচের জন জেতে)উৎসআসল run-এ region_name
১defaultfrom-default
২TF_VAR_region_name environmentfrom-env
৩terraform.tfvarsfrom-tfvars
৪terraform.tfvars.jsonfrom-tfvars-json
৫*.auto.tfvars, নামের বর্ণক্রমেfrom-b-auto (a-কে হারিয়ে)
৬-var আর -var-file, যে ক্রমে লেখাশেষে লেখা জন জেতে

নিচের ছোট simulator একই নিয়ম মানে। এর ৭টি ফল আসল Terraform-এ মিলিয়ে দেখা হয়েছে। CASES বদলে নিজে পরীক্ষা করুন।

// Which value does one variable get? Later in this order wins.
// rank: default 0, env 1, terraform.tfvars 2, .tfvars.json 3,
//       *.auto.tfvars 4 (sorted by file name), command line 5.
const RANK = { default: 0, env: 1, tfvars: 2, tfvarsJson: 3, auto: 4, cli: 5 };

function resolve(sources) {
  const sorted = sources
    .map((s, i) => ({ ...s, i }))
    .sort((a, b) => RANK[a.kind] - RANK[b.kind]
      || (a.kind === "auto" ? a.file.localeCompare(b.file) : a.i - b.i));
  return sorted[sorted.length - 1];
}
const d = { kind: "default", value: "from-default" };
const env = { kind: "env", value: "from-env" };
const tf = { kind: "tfvars", value: "from-tfvars" };
const js = { kind: "tfvarsJson", value: "from-tfvars-json" };
const a = { kind: "auto", file: "a.auto.tfvars", value: "from-a-auto" };
const b = { kind: "auto", file: "b.auto.tfvars", value: "from-b-auto" };
const cli = { kind: "cli", value: "from-cli" };
const file = { kind: "cli", value: "from-prod-file" };

const CASES = [
  ["default only", [d]],
  ["env added", [d, env]],
  ["env + tfvars", [d, env, tf]],
  ["env + tfvars + json", [d, env, tf, js]],
  ["two auto files", [d, env, tf, js, b, a]],
  ["-var-file then -var", [d, env, b, file, cli]],
  ["-var then -var-file", [d, env, b, cli, file]],
];
for (const [name, sources] of CASES) {
  console.log(name.padEnd(24) + resolve(sources).value);
}

ফল: default only → from-default থেকে শুরু করে two auto files → from-b-auto পর্যন্ত প্রতিটি ধাপে মান বদলায়। শেষ দুই লাইনে -var-file then -var দেয় from-cli, আর -var then -var-file দেয় from-prod-file. কারণ command line-এ জয় নির্ভর করে লেখার ক্রমের ওপর। সবচেয়ে বেশি ভুল হয় এখানেই: CI-র environment variable কাজ করছে না মনে হয়, অথচ পাশের একটি *.auto.tfvars তাকে চুপচাপ হারিয়ে দিয়েছে।

৬ · মূল workflow: fmt, validate, plan, apply, destroy

কাজের ক্রম সব জায়গায় একই। প্রথমে দুটি বিনা ঝুঁকির কমান্ড: terraform fmt লেখা গুছিয়ে দেয়, আর terraform validate API না ডেকেই ফাইলের ভুল ধরে।

$ terraform fmt -diff
main.tf
--- old/main.tf
+++ new/main.tf
@@ -1,4 +1,4 @@
 resource "local_file" "note" {
-filename="${path.module}/note.txt"
-    content = "hello"
+  filename = "${path.module}/note.txt"
+  content  = "hello"
 }

$ terraform validate             # filename-এর বানান ভুল করে লেখা হয়েছে
Error: Missing required argument
…
Error: Unsupported argument

  on main.tf line 2, in resource "local_file" "note":
   2:   filenam = "${path.module}/note.txt"

An argument named "filenam" is not expected here. Did you mean "filename"?

CI-তে terraform fmt -check ভুল বিন্যাসে exit code 3 দেয়, তাই pipeline থেমে যায়। এবার পুরো পথটি ধাপে ধাপে। “পরে” চেপে প্রতিটি ধাপে ফাইল আর আউটপুট লক্ষ করুন।

Folder আর cluster-এর অবস্থা
.terraform/
.terraform.lock.hcl
tfplan
state-এ resource
cluster-এ bazar-web
শুরুশুধু আপনার ফাইল
versions.tf  variables.tf  main.tf  outputs.tf
terraform initplugin নামল
- Installed hashicorp/kubernetes v3.4.0 (signed by HashiCorp)
- Installed hashicorp/local v2.9.1 (signed by HashiCorp)
Terraform has been successfully initialized!
terraform validateAPI ডাকা হয়নি
Success! The configuration is valid.
terraform planকিছু বদলায়নি
data.kubernetes_namespace_v1.team: Read complete after 0s [id=abcl-tb]
…
  # kubernetes_config_map_v1.settings will be created
  # kubernetes_deployment_v1.web will be created
  # local_file.report will be created
…
Plan: 3 to add, 0 to change, 0 to destroy.
Changes to Outputs:
  + deployment_name = "bazar-web"
  + settings_uid    = (known after apply)
terraform plan -out=tfplanএকই plan, ফাইলে জমা
Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"
terraform apply tfplanআসল API call
…
kubernetes_config_map_v1.settings: Creation complete after 0s [id=abcl-tb/bazar-settings]
local_file.report: Creation complete after 0s [id=c21bf8d4c14a…]
kubernetes_deployment_v1.web: Creation complete after 3s [id=abcl-tb/bazar-web]

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

Outputs:
deployment_name = "bazar-web"
settings_uid = "6523526f-59f7-4f35-a771-ddcbcedd0d6c"
terraform planআবার চালালে
No changes. Your infrastructure matches the configuration.
  1. শুরু। ফোল্ডারে শুধু চারটি .tf ফাইল। State নেই, plugin নেই, cluster-এ bazar-web নেই।
  2. init. Provider নেমে .terraform/-এ বসল, আর lock file তৈরি হলো। আসল resource এখনো একটিও হয়নি।
  3. validate. Syntax, argument-এর নাম আর type মিলল। Cluster-এ কোনো ডাক যায়নি।
  4. plan. এখানে plan আসল API পড়ে: data source পড়ল, config আর খালি state মিলিয়ে বলল ৩টি নতুন জিনিস লাগবে। কিছুই বানায়নি। এটি একরকম dry run.
  5. plan -out. একই plan ফাইলে জমা হলো। Review করে ঠিক এই ফাইলটিই apply করা হবে।
  6. apply. Apply-এ graph-এর ক্রমে কাজ হলো: ConfigMap আগে, তারপর বাকি দুটি একসাথে। Deployment ৩ সেকেন্ড নিল, কারণ provider rollout শেষের অপেক্ষা করে। State-এ এখন ৩টি resource.
  7. আবার plan. No changes. এটিই idempotent আচরণ: কাঙ্ক্ষিত অবস্থায় পৌঁছে গেলে আর কিছু করার নেই।

Plan-এর ভেতরে তিন দিকের তুলনা

দ্বিতীয়বার plan চালালে আউটপুটের শুরুতে এমন লাইন আসে: kubernetes_config_map_v1.settings: Refreshing state... [id=abcl-tb/bazar-settings]. অর্থাৎ Terraform শুধু ফাইল আর state মেলায় না, আসল API-কেও জিজ্ঞেস করে জিনিসটি এখন কেমন আছে। তিনটি জিনিস মেলে:

  1. Config: আপনি কী চান।
  2. State: Terraform শেষবার কী জেনেছিল।
  3. আসল অবস্থা: API এখন কী বলছে।

কেউ হাতে কিছু বদলালে এই তুলনাতেই drift ধরা পড়ে, যা পর্ব ৫৩-এ দেখবেন। CI-তে আরেকটি সুবিধা -detailed-exitcode. আসল run-এ নতুন config-এ terraform plan -detailed-exitcode exit code ২ দিল, আর apply-এর পরে দিল ০. নিয়মটি: ০ মানে কোনো বদল নেই, ১ মানে error, ২ মানে বদল আছে। এভাবে pipeline বুঝতে পারে review লাগবে কি না।

Saved plan কেন

খালি terraform apply নতুন করে plan বানায়, তাই আপনি যা দেখেছিলেন আর যা চলল, দুটি আলাদা হতে পারে। Saved plan এই ফাঁক বন্ধ করে। আর মাঝখানে state বদলালে Terraform পুরোনো plan চালাতে রাজি হয় না। নিচে একটি plan জমা রেখে মাঝখানে আরেকটি apply চালানো হলো।

$ terraform plan -out=up.tfplan
…
Plan: 0 to add, 1 to change, 0 to destroy.

$ terraform apply -auto-approve -var replicas=1
…
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

$ terraform apply up.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.

terraform apply tfplan আর নিশ্চিতকরণ চায় না। Plan ফাইলে গোপন মানও থাকতে পারে, তাই এটিকে state-এর মতোই সাবধানে রাখুন, Git-এ নয়। CI-তে plan আর apply একই পরিবেশে একই provider version-এ চালাতে হয়।

Plan কোনো গ্যারান্টি নয়

Plan একটি পূর্বাভাস। Apply-এর সময় API অন্য কিছু বলতেই পারে। নিচে Terraform দুটি ConfigMap বানাতে চায়, কিন্তু দ্বিতীয় নামের একটি ConfigMap আমরা আগেই kubectl দিয়ে বানিয়ে রেখেছিলাম।

$ terraform apply -auto-approve
…
Plan: 2 to add, 0 to change, 0 to destroy.
kubernetes_config_map_v1.first: Creating...
kubernetes_config_map_v1.first: Creation complete after 0s [id=abcl-tb/demo-first]
kubernetes_config_map_v1.second: Creating...

Error: configmaps "demo-second" already exists

  with kubernetes_config_map_v1.second,
  on main.tf line 8, in resource "kubernetes_config_map_v1" "second":
   8: resource "kubernetes_config_map_v1" "second" {

$ terraform state list
kubernetes_config_map_v1.first

Plan 2 to add বলেছিল, কারণ state-এ কিছু ছিল না। Terraform জানে শুধু নিজের state-এর জিনিস। আর দেখুন: first তৈরি হয়ে state-এই রয়ে গেল। Apply কোনো transaction নয়, rollback হয় না। কারণটি ঠিক করে আবার apply করলে সে বাকিটুকু শেষ করে।

শেষ ধাপ terraform destroy, যা state-এর সব resource মুছে ফেলে। Test environment গোছাতে এটি দারুণ, production-এ ভয়ংকর। আগে terraform plan -destroy চালিয়ে তালিকা দেখে নিন।

৭ · Plan পড়া: চিহ্ন, update আর replace

প্রতিটি resource-এর বাঁ পাশে একটি চিহ্ন থাকে। এই চিহ্ন পড়াই Terraform ব্যবহারের সবচেয়ে গুরুত্বপূর্ণ দক্ষতা।

চিহ্নTerraform-এর ভাষায়অর্থঝুঁকি
+createনতুন বানাবেকম, তবে খরচ বাড়তে পারে
~update in-placeএকই জিনিস রেখে বদলাবেমাঝারি, restart হতে পারে
-destroyমুছে ফেলবেবেশি
-/+destroy and then create replacementআগে মুছবে, পরে বানাবেবেশি, মাঝে কিছু থাকে না
+/-create replacement and then destroyআগে বানাবে, পরে মুছবেকম, তবে নাম আলাদা হতে হয়
<=readData source পড়বে apply-এর সময়নেই

শেষ লাইনের হিসাবে একটি ফাঁদ আছে: replace গোনা হয় ১টি add আর ১টি destroy হিসেবে। তাই Plan: 2 to add, 1 to change, 2 to destroy.-এর অর্থ হতে পারে, দুটি জিনিস মুছে নতুন হবে। নিচের reader-এ নিজের plan-এর শিরোনাম লাইন বসিয়ে দেখুন। নমুনাটি ছোট করা, আর WATCH-এ আপনার মূল্যবান resource-এর নাম দিন।

// Reads the action symbol of each resource line in a plan.
// Paste header lines from a plan; the sample is shortened.
const planText = `
  + resource "kubernetes_config_map_v1" "ledger" {
  ~ resource "kubernetes_deployment_v1" "web" {
-/+ resource "kubernetes_config_map_v1" "settings" {
+/- resource "kubernetes_service_v1" "api" {
  - resource "local_file" "release_note" {
 <= data "kubernetes_namespace_v1" "team" {
`;
const MEAN = {
  "+":   ["create", 1, 0, 0],
  "~":   ["update in-place", 0, 1, 0],
  "-":   ["destroy", 0, 0, 1],
  "-/+": ["destroy, then create", 1, 0, 1],
  "+/-": ["create, then destroy", 1, 0, 1],
  "<=":  ["read only", 0, 0, 0],
};
const WATCH = ["ledger", "database", "volume"]; // never lose these

let add = 0, change = 0, destroy = 0;
for (const line of planText.split("\n")) {
  const m = line.match(/^\s*(\S+)\s+(?:resource|data)\s+"(\S+)"\s+"(\S+)"/);
  if (!m || !MEAN[m[1]]) continue;
  const [text, a, c, d] = MEAN[m[1]];
  add += a; change += c; destroy += d;
  const risky = d > 0 && WATCH.some(w => m[3].includes(w));
  console.log(m[1].padEnd(4) + (m[2] + "." + m[3]).padEnd(42) + text +
    (risky ? "  << STOP, stateful" : ""));
}
console.log("Plan: " + add + " to add, " + change + " to change, " + destroy + " to destroy.");

ফল শেষ হয় Plan: 3 to add, 1 to change, 3 to destroy. লাইনে: + ১টি, দুই replace মিলে ২টি add; - ১টি, দুই replace মিলে ২টি destroy. <= কোনো সংখ্যায় যোগ হয় না। ledger ধরনের নাম মুছতে গেলে এখানে << STOP, stateful ছাপা হবে; -/+ লাইনে settings-এর বদলে নামটি বসিয়ে দেখুন। বাস্তবে সাধারণ <= খুব কম দেখা যায়, কারণ বেশিরভাগ data source plan-এর সময়ই পড়া হয়ে যায়।

Plan পড়ার রুটিন

লম্বা plan ওপর থেকে পড়লে চোখ ক্লান্ত হয়, আর আসল লাইনটিই চোখ এড়ায়। এই ক্রম মানুন:

  1. শেষ লাইন আগে: Plan: … লাইনের সংখ্যা কি আপনার edit-এর সাথে মানায়? এক লাইন বদলে 12 to destroy এলে সেখানেই থামুন।
  2. # … will be destroyed আর must be replaced: এই শিরোনামগুলো আগে খুঁজুন। কোনোটি ডেটা রাখে কি না দেখুন।
  3. # forces replacement: কোন argument দায়ী, তা বুঝুন। অনেক সময় সামান্য বদলে এটি এড়ানো যায়।
  4. ~ লাইনের ->: বাঁ দিকে পুরোনো, ডান দিকে নতুন। আপনি যা চাননি এমন কিছু আছে কি?
  5. Outputs: কোনো গুরুত্বপূর্ণ ঠিকানা বা id বদলাচ্ছে কি না, যা অন্য দল ব্যবহার করে।

ছোট edit, দুই রকম ফল

কোন argument জায়গায় বদলানো যায়, আর কোনটি যায় না, তা ঠিক করে API আর provider. Kubernetes-এ একটি object-এর metadata.name বদলানো যায় না, তাই provider সেটিকে “নতুন করে বানাতে হবে” বলে চিহ্নিত করে। Plan-এ এটি দেখা যায় # forces replacement মন্তব্যে। দুটি ছোট edit পাশাপাশি দেখুন।

kubernetes_config_map_v1.settings
Cluster-এ নাম
uid
ConfigMap-এ LOG_LEVEL
চলমান Pod-এর env
  1. শুরুর অবস্থা। ConfigMap-এর uid 6523526f…, আর Pod-এর env-এ info. Plan বলছে কিছু করার নেই।
  2. Edit ১: log_level হলো debug. শুধু data বদলাচ্ছে, যা জায়গায় বদলানো যায়। তাই চিহ্ন ~, আর ১টি change. এখনো কিছু বদলায়নি।
  3. Apply-এর ফল। uid একই, অর্থাৎ একই object. কিন্তু Pod-এর env এখনো info! Environment variable শুধু Pod শুরুর সময় পড়া হয়, আর Deployment-এর template বদলায়নি। Terraform সত্যি বলেছে, তবু app-এ বদল আসেনি।
  4. Edit ২: ConfigMap-এর নাম বদল। name বদলানো যায় না, তাই -/+. ঢেউ ছড়াল: নতুন uid-এর জন্য report ফাইলও replace, আর Deployment update. মোট 2 to add, 1 to change, 2 to destroy.
  5. Apply-এর ফল। নতুন uid 7c1e61f0…: এটি একেবারে নতুন object. Deployment-এর template বদলানোয় Pod নতুন হলো, আর এবার env debug. তবে ক্রম খেয়াল করুন: পুরোনো ConfigMap আগে মুছল, Deployment বদলাল সবার শেষে।
  6. create_before_destroy-সহ আরেকবার। নাম হলো bazar-config-v2, আর চিহ্ন +/-. নতুনটি আগে তৈরি, Deployment তাতে সরে গেল, পুরোনোটি (deposed object) মুছল সবার শেষে। মাঝখানে Pod-এর জন্য ConfigMap-হীন কোনো সময় থাকে না।

আসল plan-এর ওপরের অংশটি হুবহু এমন। ধাপ ৫-এর Pod-এর মানটি আলাদা একটি পুনরাবৃত্ত run-এ kubectl exec দিয়ে মিলিয়ে নেওয়া।

$ terraform plan
…
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # kubernetes_config_map_v1.settings must be replaced
-/+ resource "kubernetes_config_map_v1" "settings" {
      - binary_data = {} -> null
      ~ id          = "abcl-tb/bazar-settings" -> (known after apply)
      - immutable   = false -> null
        # (1 unchanged attribute hidden)

      ~ metadata {
          - annotations      = {} -> null
          ~ generation       = 0 -> (known after apply)
          ~ name             = "bazar-settings" -> "bazar-config" # forces replacement
          ~ resource_version = "18680" -> (known after apply)
          ~ uid              = "6523526f-59f7-4f35-a771-ddcbcedd0d6c" -> (known after apply)
            # (3 unchanged attributes hidden)
        }
    }
…
Plan: 2 to add, 1 to change, 2 to destroy.

ConfigMap-এর ফাঁদ: ধাপ ৩-এর সমস্যা Terraform-এর ভুল নয়। Kubernetes-এর ধরনই এমন, যা পর্ব ৪৭-এ আছে। সমাধান: config-এর hash Pod template-এর annotation-এ রাখা, অথবা ধাপ ৬-এর মতো নতুন নামে ConfigMap বানানো। Cloud-এ একই ধরনের ফাঁদ: database-এর subnet বদলালে বা VM-এর image বদলালে অনেক সময় replace আসে, যা provider-ভেদে আলাদা। তাই forces replacement লেখাটি খুঁজে পড়ার অভ্যাস করুন।

৮ · একাধিক কপি: count বনাম for_each

তিনটি শাখার জন্য তিনটি ConfigMap লাগলে তিনবার লিখবেন না। count দেয় সংখ্যা দিয়ে কপি, আর for_each দেয় নাম দিয়ে কপি। একই list দিয়ে দুটিই বানাই।

variable "branches" {
  type    = list(string)
  default = ["dhaka", "khulna", "sylhet"]
}

resource "kubernetes_config_map_v1" "by_count" {
  count = length(var.branches)

  metadata {
    name      = "c-${var.branches[count.index]}"
    namespace = "abcl-tb"
  }

  data = {
    BRANCH = var.branches[count.index]
  }
}

resource "kubernetes_config_map_v1" "by_each" {
  for_each = toset(var.branches)

  metadata {
    name      = "e-${each.key}"
    namespace = "abcl-tb"
  }

  data = {
    BRANCH = each.value
  }
}

ক্লাসের রোল নম্বর ভাবুন। রোল ২-এর ছাত্র চলে গেলে যদি সবার রোল এক ঘর করে এগিয়ে আসে, তাহলে খাতায় রোল ৩-এর সব নম্বর এখন ভুল লোকের নামে। count ঠিক এই রোল নম্বর। for_each-এ প্রত্যেকের নিজের নাম, তাই একজন চলে গেলে বাকিদের খাতা একই থাকে।

এবার list-এর মাঝখান থেকে khulna বাদ দিলাম। ছোট্ট এক শব্দের edit, কিন্তু ফল দেখুন।

Address → cluster-এর ConfigMap
by_count[0]
by_count[1]
by_count[2]
by_each["dhaka"]
by_each["khulna"]
by_each["sylhet"]
  1. শুরু। ছয়টি ConfigMap, প্রতি শাখায় দুটি করে। count-এর address-এ সংখ্যা, for_each-এর address-এ নাম।
  2. Edit. List থেকে khulna বাদ। এখন sylhet-এর অবস্থান ২ থেকে সরে ১ হলো। আমাদের কাছে এটি একটি মোছা, কিন্তু Terraform দেখে শুধু address.
  3. count-এর হিসাব। [0] মিলল। [1]-এর নাম ছিল c-khulna, এখন চাই c-sylhet, আর নাম বদলানো যায় না, তাই replace. [2] সীমার বাইরে, তাই destroy. Sylhet-এর কিছুই বদলায়নি, তবু তার ConfigMap ভাঙা পড়বে।
  4. for_each-এর হিসাব। Key khulna নেই, তাই শুধু সেটি মুছবে। dhaka আর sylhet ছোঁয়াই হবে না।
  5. Apply. এই plan-এ দুই ধরনের ফল একসাথে: count থেকে ১ add ও ২ destroy, আর for_each থেকে ১ destroy. মোট 1 to add, 3 to destroy.
  6. শেষ অবস্থা। দুই দিকের ফল একই দেখাচ্ছে: dhaka আর sylhet. তবে count-এর c-sylhet একটি নতুন object. ConfigMap-এর বদলে এটি database হলে ডেটা হারাত।

Map দিয়ে for_each: প্রতিটি কপির নিজের মান

for_each-এর সবচেয়ে কাজের রূপ হলো map. তখন each.key হলো পরিচয়, আর each.value হলো সেই কপির নিজের মান। toset() লেগেছিল কারণ for_each সরাসরি list নেয় না, শুধু map বা set নেয়।

variable "branch_replicas" {
  type = map(number)
  default = {
    dhaka  = 3
    khulna = 1
  }
}

resource "local_file" "branch" {
  for_each = var.branch_replicas

  filename = "${path.module}/${each.key}.txt"
  content  = "replicas=${each.value}\n"
}

output "branch_files" {
  value = [for f in local_file.branch : f.filename]
}
$ terraform apply -auto-approve
…
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

branch_files = [
  "./dhaka.txt",
  "./khulna.txt",
]

$ terraform state list
local_file.branch["dhaka"]
local_file.branch["khulna"]

Output-এর [for f in … : f.filename] হলো for expression: একটি সংগ্রহ থেকে নতুন list বানায়। এটি for_each নয়, resource-ও বানায় না।

নিচের simulator একই হিসাব করে। after বদলে দেখুন: শেষের item মুছলে count-এও ক্ষতি নেই, কিন্তু প্রথম item মুছলে সবচেয়ে বেশি।

// count vs for_each: which instances does a list edit touch?
// Edit the two lists and run again.
const before = ["dhaka", "khulna", "sylhet"];
const after  = ["dhaka", "sylhet"];

function countPlan(a, b) {
  const lines = [];
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
    const addr = "by_count[" + i + "]";
    if (i >= b.length) lines.push(["-", addr, a[i] + " (index out of range)"]);
    else if (i >= a.length) lines.push(["+", addr, b[i]]);
    else if (a[i] !== b[i]) lines.push(["-/+", addr, a[i] + " -> " + b[i]]);
  }
  return lines;
}
function eachPlan(a, b) {
  const lines = [];
  for (const k of a) if (!b.includes(k)) lines.push(["-", 'by_each["' + k + '"]', k]);
  for (const k of b) if (!a.includes(k)) lines.push(["+", 'by_each["' + k + '"]', k]);
  return lines;
}
function show(title, lines) {
  let add = 0, del = 0;
  console.log(title);
  for (const [sym, addr, note] of lines) {
    console.log("  " + sym.padEnd(4) + addr.padEnd(22) + note);
    if (sym !== "-") add++;
    if (sym !== "+") del++;
  }
  console.log("  Plan: " + add + " to add, 0 to change, " + del + " to destroy.\n");
}
show("count (identity = position)", countPlan(before, after));
show("for_each (identity = key)", eachPlan(before, after));

ফল আসল run-এর সাথে মেলে: count-এ Plan: 1 to add, 0 to change, 2 to destroy., for_each-এ Plan: 0 to add, 0 to change, 1 to destroy.. সহজ নিয়ম: কপিগুলো হুবহু একরকম হলে, বা শুধু “আছে অথবা নেই” বোঝাতে (count = var.enabled ? 1 : 0), count ঠিক আছে। প্রতিটির নিজের পরিচয় থাকলে for_each নিন। আগের config-এ count থাকলে moved block (Terraform ১.১ থেকে) দিয়ে address বদলানো যায়, resource না ভেঙেই।

৯ · lifecycle: Terraform-এর আচরণে সীমা টানা

lifecycle block একটি resource-এর জন্য বিশেষ নিয়ম দেয়। তিনটি argument সবচেয়ে কাজের।

create_before_destroy

আগের stepper-এর ৬ নম্বর ধাপ দেখুন। create_before_destroy দিলে replace-এর ক্রম উল্টে যায়। আসল run-এ শেষ লাইনটি এমন: kubernetes_config_map_v1.settings (deposed object 2c9a5c5f): Destroying... [id=abcl-tb/bazar-config]. Deposed object মানে নতুনটি এসে যাওয়ায় পদ হারানো পুরোনো জিনিস। শর্ত একটিই: নতুন আর পুরোনো একসাথে থাকতে হবে। তাই দুটির নাম একই হলে সংঘর্ষ হয়, যেমন Kubernetes-এ already exists. Load balancer-এর পেছনের VM বা certificate বদলানোর মতো কাজে এটি downtime কমায়।

resource "kubernetes_config_map_v1" "ledger" {
  metadata {
    name      = "${var.app_name}-ledger"
    namespace = local.namespace
  }

  data = {
    OWNER = "finance"
  }

  lifecycle {
    prevent_destroy = true
  }
}

prevent_destroy

prevent_destroy হলো জরুরি brake. Database বা গুরুত্বপূর্ণ bucket-এ এটি দিন। আসল run-এ terraform destroy চালালাম।

$ terraform destroy -auto-approve
…
Plan: 0 to add, 0 to change, 5 to destroy.
…
Error: Instance cannot be destroyed

  on main.tf line 80:
  80: resource "kubernetes_config_map_v1" "ledger" {

Resource kubernetes_config_map_v1.ledger 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.

$ terraform plan                 # ledger block পুরোটাই ফাইল থেকে মুছে
…
  # kubernetes_config_map_v1.ledger will be destroyed
  # (because kubernetes_config_map_v1.ledger is not in configuration)
…
Plan: 0 to add, 0 to change, 1 to destroy.

$ terraform destroy -auto-approve
…
local_file.report: Destruction complete after 0s
local_file.release_note: Destruction complete after 0s
kubernetes_config_map_v1.ledger: Destruction complete after 0s
kubernetes_deployment_v1.web: Destruction complete after 0s
kubernetes_config_map_v1.settings: Destruction complete after 0s
Destroy complete! Resources: 5 destroyed.

এক resource-এর বাধায় পুরো destroy থেমে গেল, কিছুই মোছেনি। নাম বদলে replace ডাকলেও একই error এসেছে। কিন্তু খেয়াল করুন শেষ অংশ: block মুছে দিলে পাহারাও মুছে যায়, কারণ নিয়মটি থাকে config-এ। Cluster বা cloud console থেকে মোছাও এটি আটকায় না। আসল সুরক্ষার জন্য provider-এর deletion protection আর backup-ও লাগে। আর destroy-এর ক্রম দেখুন: graph-এর উল্টো, settings সবার শেষে।

ignore_changes

কিছু মান Terraform-এর বাইরে বদলায়। যেমন autoscaler Deployment-এর replica বাড়ায়। আসল run-এ kubectl scale --replicas=2 চালানোর পর plan বলল ~ replicas = "2" -> "1", অর্থাৎ পরের apply autoscaler-এর কাজ উল্টে দেবে। Deployment-এ lifecycle { ignore_changes = [spec[0].replicas] } যোগ করলে একই অবস্থায় plan বলল:

$ terraform plan
…
No changes. Your infrastructure matches the configuration.

$ kubectl get deploy bazar-web
NAME        READY   UP-TO-DATE   AVAILABLE   AGE
bazar-web   1/2     2            1           45s

ignore_changes মানে ওই attribute-এর drift ইচ্ছে করে দেখবে না। ফলে config-এ মানটি বদলালেও সেটি আর প্রয়োগ হবে না, আর ভুল বদলও আড়ালে থাকবে। তাই এটি শুধু নির্দিষ্ট attribute-এ, কারণ লিখে রেখে ব্যবহার করুন। ignore_changes = all প্রায় সব সময়ই সমস্যা লুকানোর চিহ্ন।

আরও গভীরে: Core আর provider আলাদা প্রক্রিয়া, যারা gRPC-র মাধ্যমে কথা বলে। Plan-এর সময় Core provider-কে “এই পুরোনো আর এই নতুন মান থেকে কী হবে” জিজ্ঞেস করে, আর provider-ই জানায় কোন attribute replace দাবি করে। তাই একই ভাষার নিচে আচরণ provider-ভেদে আলাদা। প্রতিটি resource-এর নিয়ম পড়তে হয় Terraform Registry-তে। Lock file-এর h1: আর zh: hash দুটি আলাদা পদ্ধতির; নানা OS-এ দলের কাজ হলে terraform providers lock -platform=… দিয়ে সব platform-এর hash যোগ করা হয়।

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

  • terraform init কী করে? Provider ও module নামায়, backend ঠিক করে, lock file লেখে।
  • Argument আর attribute-এর পার্থক্য? Argument আপনি দেন, attribute provider ফেরত দেয়।
  • ~> 3.4-এর অর্থ? 3.4 বা তার ওপরে, তবে 4.0-এর নিচে।
  • for_each কেন count-এর চেয়ে নিরাপদ? Instance-এর পরিচয় অবস্থান নয়, স্থায়ী key, তাই মাঝের item মুছলে বাকিরা নড়ে না।
  • -/+ আর +/--এর পার্থক্য? প্রথমটি আগে মোছে, দ্বিতীয়টি (create_before_destroy) আগে বানায়।

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

HCL লিখতে AI খুব দ্রুত। কিন্তু AI আপনার state দেখে না, আর আপনার provider version-ও জানে না। HCL দেখতে ঠিক হলেও ফল হতে পারে একটি database মুছে যাওয়া। তাই AI-এর কোডের যাচাই হয় plan দিয়ে, আত্মবিশ্বাসী উত্তর দিয়ে নয়।

AI-এর সাধারণ ভুলযেভাবে ধরবেন
নেই এমন argument বানায়, বা পুরোনো resource নাম দেয়terraform validate, Registry-র docs
required_providers-এ version দেয় নাConstraint আর lock file আছে কি না দেখা
Security group-এ 0.0.0.0/0, IAM-এ * অনুমতি, public bucketPlan-এর diff পড়া, policy check (পর্ব ৫৫)
Password সরাসরি default বা .tfvars-এ লেখেSecret manager, সতর্ক ভাবে state রাখা
পরিচয়-সহ জিনিসে count ব্যবহার করেfor_each-এ বদলে নেওয়া
সমস্যা ঢাকতে ignore_changes = all বা -target বলেমূল কারণ খোঁজা
terraform destroy বা -auto-approve কোনো সতর্কতা ছাড়াই বলেনিজে plan পড়া, production-এ কখনো অন্ধভাবে নয়
খরচের কথা ভাবে নাInstance size দেখা, budget alert রাখা

Plan-এর আউটপুট দিয়ে AI-কে review করাতে পারেন। আগে id, account আর গোপন মান সরিয়ে নিন। State ফাইল কখনো পেস্ট করবেন না।

আমি Terraform 1.17 আর নিচের provider version ব্যবহার করছি। এই plan আউটপুট review করো: [এখানে terraform plan-এর আউটপুট, গোপন মান সরিয়ে] ১. প্রতিটি -/+ আর - লাইনের জন্য বলো কোন argument এটি ঘটাচ্ছে, আর এতে ডেটা হারানো বা downtime-এর ঝুঁকি আছে কি না। ২. নিজে কোনো argument-এর নাম বানিয়ো না। নিশ্চিত না হলে বলো "docs দেখুন"। ৩. শুধু যে লাইন বদলাতে হবে সেটি দেখাও, আর কেন বদলাচ্ছ বলো। ৪. destroy বা -auto-approve প্রস্তাব করবে না।

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

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

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

প্র ০১Script দিয়ে সরাসরি কাজ না করে plan আর apply আলাদা করা হলো কেন?

ধরুন একটি bash script cloud CLI ডেকে server বানায়। Script চালানো আর ফল পাওয়া একই মুহূর্তের কাজ। ভুল বুঝতে পারলে ততক্ষণে কিছু ঘটে গেছে। আর দ্বিতীয়বার চালালে সে আবার নতুন server বানাবে, কারণ সে আগের কথা মনে রাখে না।

Plan এই দুই সমস্যাকে আলাদা করে। প্রথমত, এটি “কী ঘটবে” প্রশ্নটিকে “ঘটানো” থেকে আলাদা করে। ফলে একজন মানুষ, অথবা একটি policy tool, কাজ শুরুর আগেই তালিকাটি দেখতে পারে। Pull request-এ plan-এর আউটপুট জুড়ে দেওয়ার রীতি এখান থেকেই। দ্বিতীয়ত, config আর state মিলিয়ে দেখার ফলে একই কাজ বারবার চালানো নিরাপদ হয়, যা idempotent আচরণ।

ফেব্রুয়ারি ২০১৭-এ Amazon S3-এর বড় outage-এর প্রকাশিত ব্যাখ্যায় বলা হয়েছিল, একটি নির্ধারিত কমান্ডে ভুল ইনপুটের কারণে ইচ্ছার চেয়ে বেশি server সরে যায়। পরে টুলটিতে গতি ও সীমার সুরক্ষা যোগ করা হয়। ঘটনাটি Terraform-এর ছিল না, তবে শিক্ষা একই: শক্তিশালী কমান্ডের সামনে একটি পূর্বদর্শন আর একটি সীমা থাকা দরকার।

তবে plan সর্বশক্তিমান নয়। আপনি এই পর্বেই দেখেছেন plan 2 to add বলার পরও apply ব্যর্থ হয়েছে, কারণ Terraform-এর জানা জগৎ শুধু state আর API-র উত্তর। অন্য কেউ মাঝখানে কিছু বদলালে plan পুরোনো হয়ে যায়। তাই plan-কে নিশ্চিত ভবিষ্যদ্বাণী না ভেবে একটি সুশিক্ষিত অনুমান ভাবাই ঠিক। এজন্যই saved plan আর stale check, আর এজন্যই পর্ব ৫৩-এর locking আসে। একই ভাবনা অন্য জায়গাতেও আছে: AWS CloudFormation-এর change set, আর kubectl diff কমান্ডে।

মূল উপলব্ধি: Plan-এর আসল মূল্য স্বয়ংক্রিয়তায় নয়, মানুষের সিদ্ধান্তের একটি বিন্দু তৈরি করায়। ঝুঁকিপূর্ণ কাজকে “দেখা” আর “করা” দুই ভাগে ভাঙলে ভুল ধরার একটি সুযোগ তৈরি হয়।

প্র ০২Terraform-এর লাইসেন্স বদল আর OpenTofu: একজন সাধারণ ব্যবহারকারীর জন্য এর মানে কী?

আগস্ট ২০২৩-এ HashiCorp তাদের প্রধান পণ্যগুলোর লাইসেন্স MPL 2.0 থেকে Business Source License (BUSL 1.1)-এ নেয়। BUSL ওপেন সোর্স সংজ্ঞায় পড়ে না: কোড পড়া ও ব্যবহার করা যায়, কিন্তু HashiCorp-এর সাথে প্রতিযোগিতা করে এমন পণ্যে ব্যবহারে সীমা আছে। নির্দিষ্ট সময় পরে কোড আবার উদার লাইসেন্সে যায় বলে শর্ত আছে।

কারণ হিসেবে বলা হয়েছিল, অন্য কোম্পানি তাদের কোডের ওপর ভিত্তি করে প্রতিযোগী সেবা বিক্রি করছে। Terraform-এর ওপর ব্যবসা গড়া কয়েকটি কোম্পানি ও community-র একটি অংশ এতে আপত্তি জানায়, এবং শেষ MPL সংস্করণ থেকে একটি fork শুরু করে, যা Linux Foundation-এর অধীনে OpenTofu নামে আসে। পরে IBM HashiCorp-কে কিনে নেয়; চুক্তির ঘোষণা ২০২৪ সালে, সম্পন্ন হয় ২০২৫ সালে।

একজন সাধারণ ব্যবহারকারী যিনি নিজের infrastructure চালাতে Terraform ব্যবহার করেন, তাঁর জন্য দৈনন্দিন কাজে সাধারণত বাধা নেই। প্রশ্নটি বেশি জরুরি তাঁদের জন্য, যাঁরা Terraform-কে ঘিরে পণ্য বিক্রি করেন। নিশ্চিত হতে হলে নিজের প্রতিষ্ঠানের আইনি পরামর্শ নেওয়াই ঠিক; এটি আইনি মতামত নয়।

দুটি tool-এর ভাষা ও workflow-এর বড় অংশ একই থাকলেও সময়ের সাথে পার্থক্য বাড়ে। যেমন OpenTofu নিজে থেকে state encryption যোগ করেছে। Provider-গুলো সাধারণত দুটিতেই চলে, তবে নতুন feature মিলিয়ে দেখতে হয়। কোনো দল চাইলে পরীক্ষার environment-এ আগে দেখে নিতে পারে যে একই config-এর plan দুটিতে একই আসে কি না।

মূল উপলব্ধি: Infrastructure tool বেছে নেওয়া মানে তার লাইসেন্স, মালিকানা আর ভবিষ্যৎ বেছে নেওয়া। Vendor lock-in শুধু cloud-এ নয়, tool-এও হয়। তাই ধারণাগুলো শিখুন, কারণ ধারণা tool-এর চেয়ে বেশি দিন টেকে।

প্র ০৩Terraform ফাইলের ক্রম না দেখে graph বানায় কেন? এতে লাভ আর দাম কী?

Script ওপর থেকে নিচে চলে, তাই ক্রমের দায়িত্ব লেখকের। একশো resource হলে কে কার আগে, তা মাথায় রাখা কঠিন। আর একটি লাইন ভুল জায়গায় বসলেই সব ভাঙে। Terraform প্রতিটি reference থেকে একটি directed acyclic graph বানায়, ফাইলের ক্রমের কোনো মূল্যই থাকে না। আপনি resource-গুলো যে কোনো ফাইলে, যে কোনো ক্রমে লিখতে পারেন।

এর তিনটি বড় লাভ। প্রথমত, সমান্তরাল কাজ: যাদের মধ্যে সম্পর্ক নেই তারা একসাথে চলে। এই পর্বের apply-তে report আর Deployment কারও অপেক্ষা করেনি। শত শত resource-এ সময়ের পার্থক্য বিশাল, যদিও খুব বেশি সমান্তরাল কাজ চাইলে API-র rate limit-এ আটকে যেতে পারে, আর তখন -parallelism কমাতে হয়। দ্বিতীয়ত, উল্টো destroy: একই graph উল্টে নিলেই মোছার সঠিক ক্রম, আলাদা করে লিখতে হয় না। তৃতীয়ত, পরিবর্তনের ঢেউ বোঝা: ConfigMap-এর নতুন uid report ফাইলকেও replace করাল, কারণ graph জানে কে কার মান ব্যবহার করে।

দামও আছে। Graph-এ বৃত্ত থাকলে অর্থাৎ A চায় B-কে, B চায় A-কে, Terraform Cycle error দেয়। Security group-এর পারস্পরিক নিয়মে এমন প্রায়ই হয়, যার সমাধান নিয়মগুলোকে আলাদা resource-এ নেওয়া। আরেকটি দাম: graph শুধু কোডে যা আছে তা জানে। একটি IAM policy বসার পরও তা ছড়াতে কয়েক সেকেন্ড লাগতে পারে, আর সেই সময়টি graph-এ নেই। এমন লুকানো নির্ভরতার জন্যই depends_on. আর graph দেখে বোঝা যায় না কাজটি নিরাপদ কি না, শুধু বোঝা যায় কোনটির পরে কোনটি হবে।

একই ধারণা build tool-এ (Make, Bazel) আর DSA-র topological sort-এ পাবেন।

মূল উপলব্ধি: ক্রম যখন তথ্য থেকে বের করা যায়, তখন তা হাতে না লেখাই ভালো। মানুষের কাজ সম্পর্ক ঘোষণা করা, ক্রম বের করা যন্ত্রের কাজ।

প্র ০৪count-এর index shift সমস্যাটি কি শুধু Terraform-এর? এর পেছনের সাধারণ নীতিটি কী?

না। এটি একটি সাধারণ প্রকৌশল-নীতির উদাহরণ: কোনো জিনিসের পরিচয় তার অবস্থান দিয়ে দেবেন না। count-এ একটি instance-এর পরিচয় তার index. Index বদলালে Terraform-এর চোখে সে অন্য জিনিস।

একই ভুল অন্য জায়গাতেও দেখা যায়। Database-এ সারির নম্বরকে primary key বানালে একটি সারি মুছলে বাকিদের foreign key ভেঙে যায়, তাই স্থায়ী id ব্যবহার হয়। React-এ list-এর key হিসেবে array index দিলে item মোছার পর input-এর মান ভুল সারিতে চলে যায়। আবার hash(key) mod N দিয়ে cache ভাগ করলে একটি server কমলে প্রায় সব key জায়গা বদলায়, আর consistent hashing ঠিক এই সরে যাওয়া কমায়।

Terraform-এ হিসাবটি সরল। nটি item-এর list-এ অবস্থান k (০ থেকে গোনা) মুছলে n-k-1টি replace হয়, আর শেষ index-টি destroy. পাঁচটির মধ্যে দ্বিতীয়টি (k=1) মুছলে ৩টি replace আর ১টি destroy. for_each-এ সব ক্ষেত্রেই ১টি destroy. আর যদি প্রতিটি replace একটি database হয়, তাহলে ক্ষতির মাত্রা blast radius অনেক বড়।

for_each-এরও নিয়ম আছে। তার key plan-এর সময়েই জানা থাকতে হয়। তাই apply-এর পরে জানা যায় এমন মান, যেমন একটি নতুন resource-এর id, key হিসেবে দিলে error আসে। আর key বদলালে আবার replace হয়, তাই key হওয়া উচিত স্থায়ী নাম, এমন কিছু নয় যা মাসে মাসে বদলায়। পুরোনো config-কে count থেকে for_each-এ নিতে হলে moved block দিয়ে পুরোনো address-কে নতুন address-এর সাথে জুড়ে দিতে হয়; নইলে Terraform ভাববে একটি মুছে আরেকটি বানাতে হবে। Plan দেখে নিশ্চিত হবেন যে সেখানে কোনো destroy নেই।

মূল উপলব্ধি: পরিচয় স্থায়ী হওয়া চাই, অবস্থান নয়। নতুন কোনো সিস্টেম দেখলে জিজ্ঞেস করুন: এখানে একটি জিনিসকে কী দিয়ে চেনা হয়, আর সেটি কখন বদলাতে পারে?

প্র ০৫Apply মাঝপথে ব্যর্থ হলে Terraform rollback করে না কেন? Database-এর transaction-এর মতো হলো না কেন?

Database-এ transaction সম্ভব, কারণ সব ডেটা একটি সিস্টেমের হাতে, আর সে একসাথে commit বা abort করতে পারে। Terraform-এর অবস্থা আলাদা। সে ডাকে নানা API: Kubernetes, ফাইল সিস্টেম, একাধিক cloud. এদের মধ্যে কোনো সাধারণ commit নেই। প্রতিটি call নিজেই একটি সম্পন্ন ঘটনা।

Rollback করতে গেলে সমস্যা আরও বাড়ে। প্রথমত, অনেক কাজ উল্টানো যায় না: একটি মোছা disk ফেরত আসে না। দ্বিতীয়ত, উল্টানোর কাজটিও ব্যর্থ হতে পারে, তখন অবস্থা আরও অজানা। তৃতীয়ত, সময়: database replace করতে কয়েক মিনিট লাগে, আর স্বয়ংক্রিয় rollback আরেকটি অপরিকল্পিত পরিবর্তন।

তাই Terraform অন্য পথ নিয়েছে: এগিয়ে যাওয়া, আর যা হলো তা ঠিকঠাক লিখে রাখা। এই পর্বের ব্যর্থ apply-তে first state-এ রয়ে গেল। বাধাটি সরালে পরের apply শুধু বাকিটুকু করবে। এটি reconcile ধারণা: কাঙ্ক্ষিত অবস্থা ঘোষিত, আর tool বারবার চেষ্টা করে সেখানে পৌঁছায়। Kubernetes controller-এর সাথে মিল আছে, যদিও Terraform নিজে থেকে লুপে চলে না, কেউ চালালে তবেই চলে।

পরিচিত উল্টো উদাহরণ AWS CloudFormation: ডিফল্টে সে ব্যর্থ stack update-এ পুরোনো অবস্থায় ফিরে যাওয়ার চেষ্টা করে। দুটি পদ্ধতিরই দাম আছে, আর rollback নিজেও আটকে যেতে পারে।

বাস্তব নিয়ম: বড় বদলকে ছোট ছোট apply-তে ভাগ করুন, যাতে ব্যর্থতার পরিধি ছোট থাকে। আর গুরুত্বপূর্ণ কাজের আগে backup আছে কি না দেখে নিন, কারণ Terraform নিজে আপনার ডেটা ফেরত দেবে না। Apply থেমে গেলে আতঙ্কে state হাতে না ঘেঁটে আগে terraform plan চালান।

মূল উপলব্ধি: Distributed system-এ “সব অথবা কিছুই না” প্রায় অসম্ভব। তাই ভালো tool ব্যর্থতা লুকায় না, বরং ব্যর্থতার পর আবার চেষ্টা করা নিরাপদ রাখে।

অনুশীলন

নিজে আগে উত্তর ভাবুন। হাতে-কলমের কাজটি শুধু নিজের কম্পিউটারে চলে, কোনো cloud account লাগে না।

  1. হিসাব করুন: Registry-তে একটি provider-এর version আছে 3.3.9, 3.4.0, 3.4.7, 3.9.2, 4.0.0. (ক) ~> 3.4, (খ) ~> 3.4.0, (গ) >= 3.4, < 3.6-এ কোনগুলো চলবে, আর lock file না থাকলে কোনটি নামবে? (ঘ) পাঁচ item-এর ["a","b","c","d","e"] list থেকে "b" মুছলে count আর for_each-এর plan-এর শেষ লাইন কী হবে, যদি নাম বদলালে replace হয়?

    (ক) 3.4.0, 3.4.7, 3.9.2; নামবে 3.9.2. (খ) 3.4.0, 3.4.7; নামবে 3.4.7. (গ) 3.4.0, 3.4.7; নামবে 3.4.7.

    (ঘ) count: [1] b→c, [2] c→d, [3] d→e, এই ৩টি replace, আর [4] destroy. তাই Plan: 3 to add, 0 to change, 4 to destroy. for_each: Plan: 0 to add, 0 to change, 1 to destroy. এটি index_shift.js-এ list দুটি বসিয়ে মিলিয়ে নিতে পারেন।

  2. ফল আগে বলুন: Variable tier-এর default = "a". TF_VAR_tier=b সেট করা। terraform.tfvars-এ "c", z.auto.tfvars-এ "d", m.auto.tfvars-এ "e", আর qa.tfvars-এ "f". (ক) terraform plan (খ) terraform plan -var tier=g -var-file=qa.tfvars (গ) terraform plan -var-file=qa.tfvars -var tier=g-এ tier-এর মান কত? এরপর (ঘ): একটি local_file-এর content বদলে plan চালালে চিহ্ন কী হবে, আর এই resource-এ ~ কি আশা করা যায়?

    (ক) "d": দুই auto ফাইলের মধ্যে নামের ক্রমে z পরে আসে। qa.tfvars নিজে থেকে পড়া হয় না, কারণ নামে .auto নেই। (খ) "f": command line-এ শেষে লেখা জন জেতে। (গ) "g".

    (ঘ) -/+, পাশে # forces replacement. local_file-এ ~ আশা করবেন না, কারণ এই resource-এর content বদল replace হিসেবেই আসে, যা এই পর্বের report ফাইলের plan-এ দেখা গেছে।

  3. Review করুন: একজন সহকর্মীর PR-এ আছে: required_providers-এ version নেই; .terraform.lock.hcl .gitignore-এ; তিনটি database count = length(var.dbs) দিয়ে বানানো; variable "db_password" { default = "Secret123" }; আর CI চালায় terraform apply -auto-approve, কোনো plan review ছাড়া। কী কী সমস্যা, আর কীভাবে ঠিক করবেন?
    • Version নেই, lock নেই: দুই জনের দুই provider version আসতে পারে। ~> constraint দিন, lock file Git-এ রাখুন।
    • count-এ database: মাঝের একটি বাদ দিলে পরেরগুলো replace, অর্থাৎ ডেটা হারানোর ঝুঁকি। for_each নিন, আর prevent_destroy ও provider-এর deletion protection যোগ করুন।
    • Hardcoded password: Git-এ গোপন মান গেল, আর সেটি বদলাতে হবে। Secret manager থেকে আনুন, sensitive = true দিন, আর মনে রাখুন state-এ মান তবুও থাকতে পারে।
    • -auto-approve, review নেই: PR-এ plan -out দেখান, অনুমোদনের পর সেই ফাইল apply করুন।
  4. হাতে-কলমে (ঐচ্ছিক, খরচ নেই): শুধু hashicorp/local provider দিয়ে একটি ফোল্ডারে local_file বানান। init, plan, apply চালান। content বদলে আবার plan পড়ুন। শেষে terraform destroy করুন। Lock file আর state ফাইলে কী আছে দেখুন।

    এই পর্বের আসল run-এ যা হয়েছে, আপনারও একই হওয়ার কথা: প্রথম plan-এ Plan: 1 to add, 0 to change, 0 to destroy.. content বদলালে -/+, আর # forces replacement. Destroy-এর পরে ফাইলটি থাকে না, আর state-এ কোনো resource থাকে না। শুরুতেই filenam লিখে terraform validate চালিয়ে দেখলে “Did you mean” বার্তাও পাবেন। এতে কোনো cloud account বা টাকা লাগে না; শেষে ফোল্ডারটি মুছে দিলেই পরিষ্কার।

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

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

ক্রমনতুন Terraform ফোল্ডারে নিরাপদ কাজের ক্রম সাজান (▲▼ চেপে সরান)

  1. terraform init: provider নামানো, lock file তৈরি
  2. terraform fmt আর terraform validate: ফাইল যাচাই
  3. terraform plan -out=tfplan: পরিবর্তনের তালিকা জমা
  4. মানুষের review: forces replacement আর destroy খোঁজা
  5. terraform apply tfplan: ঠিক সেই plan চালানো
  6. আবার terraform plan: No changes. মিলিয়ে দেখা

Provider-এর constraint version = "~> 3.4". নিচের কোন version চলবে?

  • 3.3.9
  • 3.9.2
  • 4.0.0
  • 4.1.0

Terraform plan-এ একটি resource-এর পাশে -/+ আর একটি argument-এর পাশে # forces replacement লেখা। এর অর্থ কী?

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

Terraform-এ count = length(var.names) আর names = ["a", "b", "c"] দিয়ে তিনটি resource তৈরি। প্রতিটির নাম names[count.index], আর নাম বদলালে replace হয়। List থেকে "b" মুছলে plan-এর শেষ লাইন কোনটি?

  • Plan: 0 to add, 0 to change, 1 to destroy.
  • Plan: 1 to add, 0 to change, 2 to destroy.
  • Plan: 0 to add, 1 to change, 1 to destroy.
  • Plan: 3 to add, 0 to change, 3 to destroy.

একটি Terraform variable-এর default আছে, TF_VAR_ environment variable আছে, terraform.tfvars-এ মান আছে, আর command line-এ -var দেওয়া। কোন মান ব্যবহার হবে?

  • default
  • TF_VAR_ environment variable
  • terraform.tfvars
  • -var-এর মান

terraform apply tfplan চালাতে গিয়ে এলো Error: Saved plan is stale. সবচেয়ে সম্ভাব্য কারণ কী?

  • Plan বানানোর পরে অন্য কোনো কাজে state বদলে গেছে
  • HCL ফাইলে syntax ভুল আছে
  • Provider নামানো হয়নি
  • Plan ফাইলের সময়সীমা এক ঘণ্টা পার হয়েছে

একটি Terraform resource-এ prevent_destroy = true আছে। কেউ পুরো resource block-টি ফাইল থেকে মুছে দিয়ে plan চালাল। কী হবে?

  • Error আসবে: Instance cannot be destroyed
  • কিছুই হবে না, resource-টি আগের মতো থাকবে
  • Plan সেটিকে destroy হিসেবে দেখাবে
  • Terraform block-টি আবার ফাইলে লিখে দেবে

শব্দকোষ

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

মনে রাখুন

  1. Config + state → plan → provider → API. Core হিসাব করে, provider কাজ করে, state মনে রাখে।
  2. Constraint বলে সীমা, lock file বলে ঠিক কোনটি। Lock file Git-এ, state আর plan ফাইল নয়।
  3. Reference থেকে graph আসে; depends_on শুধু লুকানো সম্পর্কের জন্য।
  4. চিহ্ন পড়ুন: +, ~, -, -/+, +/-. forces replacement দেখলেই থামুন।
  5. পরিচয় থাকলে for_each, আর prevent_destroy block মুছলে পাহারাও শেষ। Apply-এর পরে app নিজে দেখুন।

পরের পর্বে: এই পর্বে state ছিল আপনার কম্পিউটারের একটি ফাইল। দলে কাজ করলে সেটি কোথায় থাকবে, দুজন একসাথে apply করলে কী হবে, আর আগে থেকে থাকা জিনিস কীভাবে Terraform-এর হাতে আনবেন? এসব নিয়ে পর্ব ৫৩.

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

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