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

Terraform module — variable, output, reuse আর environment

Terraform Modules — Variables, Outputs, Reuse and Environments

Taught in Bangla, this lesson writes a real module and calls it for dev and prod, covers root and child modules, inputs, validation, outputs and for_each, local, Git and registry sources, version pinning and SemVer, directories versus workspaces for environments, DRY versus clarity, composition, and handling breaking changes with moved blocks, with real output and simulators.

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

  • একটি আসল module লিখে dev আর prod-এর জন্য দুইবার ডাকা, for_each-সহ
  • Input-এর default, override আর validation কীভাবে মিলে চূড়ান্ত মান ঠিক করে
  • Local, Git আর registry উৎস, version pinning, SemVer আর প্রকাশের নিয়ম
  • ডিরেক্টরি বনাম workspace: কোনটি কখন, আর ভুল environment-এ apply কীভাবে ঘটে
  • DRY বনাম স্পষ্টতা, module কখন বেশি বড়, composition আর breaking change সামলানো

এই পর্বের আউটপুট কোথা থেকে: “আসল run” লেখা সব আউটপুট Terraform ১.১৭, hashicorp/kubernetes provider 3.4.0 আর একটি স্থানীয় single-node cluster-এ (Kubernetes ১.৩৪) চালিয়ে নেওয়া। Id, IP, hash আর সময় আপনার run-এ আলাদা হবে। Run হয়েছে Windows-এ, তাই আপেক্ষিক path-এ \ দেখবেন। ছোট করতে কিছু লাইন বাদ দিয়ে … বসানো হয়েছে। Cloud provider-এর কোনো plan এখানে চালানো হয়নি; cloud-এর নমুনাগুলো আলাদা করে চিহ্নিত।

১ · একই কোড দুইবার — dev আর prod-এর পুনরাবৃত্তি

আগের তিন পর্বে আপনি Infrastructure as Code, Terraform-এর plan ও apply, আর state দেখেছেন। এবার একটি বাস্তব সমস্যা ধরি। ধরুন একটি কাল্পনিক অনলাইন বাজার অ্যাপ আছে, নাম shop. এর একটি Deployment আর একটি Service লাগে। HCL-এ দুটি resource মিলে প্রায় ৫০ লাইন।

কিছুদিন পরে পরীক্ষার জন্য dev environment দরকার হলো। সবচেয়ে সহজ কাজ: ৫০ লাইন কপি করে নাম বদলে দেওয়া। তারপর staging. এবার কপি তিনটি। এখন কেউ prod-এ memory limit ঠিক করলেন, কিন্তু dev-এ করলেন না। তিন মাস পরে দেখা গেল তিনটি কপি তিন রকম, আর কেউ জানেন না কোন পার্থক্যটি ইচ্ছাকৃত আর কোনটি ভুল।

এর দাম তিনভাবে দিতে হয়। এক, একটি নিরাপত্তা-সংশোধন তিন জায়গায় করতে হয়। দুই, dev-এ পরীক্ষা করা জিনিস prod-এর সাথে আর মেলে না, তাই পরীক্ষার ফল বিশ্বাস করা যায় না। তিন, code review-তে ৫০ লাইনের মধ্যে আসল দুই লাইনের পার্থক্য খুঁজে পাওয়া কঠিন।

বিয়েবাড়ির ক্যাটারিংয়ের কথা ভাবুন। রান্নাঘরের রেসিপি খাতায় কাচ্চির রেসিপি একবারই লেখা। ৫০ জনের অনুষ্ঠানে আর ৫০০ জনের অনুষ্ঠানে নতুন রেসিপি লেখা হয় না; একই রেসিপিতে শুধু “কতজন” আর “ঝাল কম না বেশি” বদলায়। Module হলো সেই রেসিপি, input হলো অতিথির সংখ্যা, আর environment হলো কোন অনুষ্ঠান। রেসিপি ঠিক করলে সব অনুষ্ঠানে একবারে ঠিক হয়।

২ · Module কী — root, child আর চারটি ফাইল

Terraform-এর সংজ্ঞাটি খুব সরল: একই ফোল্ডারে থাকা .tf ফাইলগুলোর সমষ্টিই একটি module. তাই আপনি আগের পর্বগুলোতে না জেনেই module লিখেছেন।

Root module

যে ফোল্ডারে আপনি terraform plan চালান। State, backend আর provider-এর সেটিং এখানেই থাকে। প্রতিটি Terraform run-এ ঠিক একটি root module থাকে।

যিনি ডাকেন

Child module

অন্য কোনো module যাকে module block দিয়ে ডাকে। Child module নিজে একা চলে না, আর সাধারণত নিজের provider সেটিং রাখে না; ডাকার পক্ষ থেকে provider পায়।

যাকে ডাকা হয়

Child module-কে একটি function হিসেবে ভাবুন। Input variable তার parameter, output তার return value, আর ভেতরের resource তার local কাজ। বাইরে থেকে ভেতরের resource সরাসরি পড়া যায় না; যা বের করতে চান, output হিসেবে ঘোষণা করতে হয়। এই সীমানাই পরে কোড বদলানোর স্বাধীনতা দেয়।

ফাইলের নাম Terraform-এর কাছে কোনো অর্থ রাখে না; সে ফোল্ডারের সব .tf একসাথে পড়ে। তবু সবাই একই সাজ মানেন, যাতে অচেনা module খুলেই পথ পাওয়া যায়।

ফাইলভেতরে যা থাকেআমাদের module-এ
main.tfResource আর localsDeployment, Service
variables.tfসব input, type, default, validation৭টি variable
outputs.tfবাইরে দেওয়ার মানname, URL, IP
versions.tfTerraform আর provider-এর version সীমা~> 3.0

দুই root module, এক child module

Two root modules call one child module and get separate state

Root module থেকে child module, তারপর দুটি environment envs/dev (root) replicas: default 1 memory: default 32Mi envs/prod (root) replicas = 3 memory = 64Mi modules/web-app (child) main · variables · outputs · versions একই কোড, একবারই লেখা shop-dev ১টি pod, Service নিজের state ফাইল shop-prod ৩টি pod, Service নিজের state ফাইল
ছবি ৫৪.১ — কোড এক জায়গায়, input আলাদা, state আলাদা। Root module শুধু ঠিক করে “কোন মান দিয়ে ডাকব”।

৩ · একটি আসল module, দুইবার ডাকা

এবার সত্যিকারের একটি ছোট module লিখি: একটি web app, যা একটি namespace-এর ভেতরে Deployment আর Service বানায়। নিচের সব HCL terraform fmt -check আর terraform validate পাস করেছে।

locals {
  full_name = "${var.name}-${var.environment}"
  labels = {
    app = local.full_name
    env = var.environment
  }
}

resource "kubernetes_deployment_v1" "app" {
  metadata {
    name      = local.full_name
    namespace = var.namespace
    labels    = local.labels
  }

  spec {
    replicas = var.replicas

    selector {
      match_labels = local.labels
    }

    template {
      metadata {
        labels = local.labels
      }

      spec {
        container {
          name  = "web"
          image = var.image

          resources {
            requests = { cpu = "10m", memory = "16Mi" }
            limits   = { memory = var.memory_limit }
          }
        }
      }
    }
  }
}

resource "kubernetes_service_v1" "app" {
  metadata {
    name      = local.full_name
    namespace = var.namespace
    labels    = local.labels
  }

  spec {
    type     = var.service_type
    selector = local.labels

    port {
      port        = 80
      target_port = 80
    }
  }
}

খেয়াল করুন, এখানে কোনো মান সরাসরি লেখা নেই; সব var. থেকে আসে। Module-এর versions.tf-এ শুধু required_providers আছে, provider block নেই। Provider-এর সেটিং root-এ থাকে, যাতে একই module যেকোনো cluster বা account-এ চলতে পারে।

output "service_name" {
  value       = kubernetes_service_v1.app.metadata[0].name
  description = "Service name."
}

output "service_url" {
  value       = "http://${kubernetes_service_v1.app.metadata[0].name}.${var.namespace}.svc.cluster.local"
  description = "In-cluster URL."
}

output "cluster_ip" {
  value       = kubernetes_service_v1.app.spec[0].cluster_ip
  description = "Virtual IP of the Service, known only after apply."
}

এবার root module থেকে দুইবার ডাকি। একই source, শুধু input আলাদা। Dev-এ replicas আর memory_limit দেওয়াই হয়নি, তাই module-এর default চলবে।

module "web_dev" {
  source = "../modules/web-app"

  name        = "shop"
  environment = "dev"
  namespace   = "abcl-tm"
  image       = "nginx:1.27-alpine"
}

module "web_prod" {
  source = "../modules/web-app"

  name         = "shop"
  environment  = "prod"
  namespace    = "abcl-tm"
  image        = "nginx:1.27-alpine"
  replicas     = 3
  memory_limit = "64Mi"
}

এখন নিচের স্টেপার ধরে এগোন। এক module থেকে কীভাবে দুটি আলাদা plan তৈরি হলো, সেটি মানগুলো থেকেই দেখতে পাবেন।

1module "web_dev" {2  source = "../modules/web-app"3  environment = "dev"4}56module "web_prod" {7  source = "../modules/web-app"8  environment  = "prod"9  replicas     = 310  memory_limit = "64Mi"11}
এই ধাপের call-এর মান
call
replicas
memory_limit
full_name
মোট add
  1. Init দুটি call খুঁজে পেল। terraform init দুটি module block পড়ল। দুটিই একই ফোল্ডারে গেছে। Local source-এর ফাইল কপি হয় না, শুধু path মনে রাখা হয়।
  2. Dev-এর input মিলল। Caller দিয়েছেন environment = "dev". replicas আর memory_limit দেননি, তাই default বসল: 1 আর 32Mi.
  3. Locals হিসাব হলো। "${var.name}-${var.environment}" থেকে full_name হলো shop-dev. সব label আর object-এর নাম এখান থেকেই আসে।
  4. Dev-এর ২টি resource plan-এ এলো। ঠিকানার আগে module.web_dev. বসেছে। একই resource block, কিন্তু call আলাদা বলে ঠিকানা আলাদা। মোট add এখন ২।
  5. Prod-এর input মিলল। এবার caller নিজে 3 আর 64Mi দিয়েছেন, তাই default চাপা পড়ল। মানের পাশে “caller” লেখাটি লক্ষ করুন।
  6. Prod-এর ২টি resource যোগ হলো। full_name হলো shop-prod. Terraform-এর সারাংশ: Plan: 4 to add, 0 to change, 0 to destroy. কোড একবার, resource চারটি।

Apply করার পরে cluster-এ আসলে কী হলো, দেখি:

$ terraform apply tfplan
module.web_prod.kubernetes_service_v1.app: Creation complete after 0s [id=abcl-tm/shop-prod]
module.web_dev.kubernetes_service_v1.app: Creation complete after 0s [id=abcl-tm/shop-dev]
module.web_dev.kubernetes_deployment_v1.app: Creation complete after 4s [id=abcl-tm/shop-dev]
module.web_prod.kubernetes_deployment_v1.app: Creation complete after 4s [id=abcl-tm/shop-prod]

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

Outputs:

dev_url = "http://shop-dev.abcl-tm.svc.cluster.local"
prod_url = "http://shop-prod.abcl-tm.svc.cluster.local"

$ kubectl -n abcl-tm get deploy
NAME        READY   UP-TO-DATE   AVAILABLE   AGE
shop-dev    1/1     1            1           4s
shop-prod   3/3     3            3           4s

এখন prod-এর replicas 3 থেকে 4 করে plan চালালে শুধু prod-এর একটি লাইন বদলায়; dev অস্পর্শিত থাকে (আসল run):

  # module.web_prod.kubernetes_deployment_v1.app will be updated in-place
  ~ resource "kubernetes_deployment_v1" "app" {
        id               = "abcl-tm/shop-prod"
      ~ spec {
          ~ replicas                  = "3" -> "4"
        …
Plan: 0 to add, 1 to change, 0 to destroy.

নিজে চালাতে চাইলে (ঐচ্ছিক): এই পরীক্ষার জন্য কোনো cloud account লাগে না; নিজের কম্পিউটারের একটি স্থানীয় Kubernetes cluster-ই যথেষ্ট। আলাদা একটি পরীক্ষার namespace বানিয়ে নিন, আর provider-এ ভুল করে অফিসের cluster-এর context না থাকে সেটি মিলিয়ে দেখুন। কাজ শেষে root ফোল্ডারে terraform destroy চালান, তার plan পড়ুন, তারপর namespace মুছুন। একই কোড cloud-এ চালালে সত্যিকারের খরচ হতে পারে, আর destroy ভুলে গেলে বিল চলতেই থাকে; তাই cloud-এ পরীক্ষার আগে budget alert রাখুন।

৪ · Input, validation, output আর for_each

Module-এর ভালো-মন্দ অনেকটাই তার input-এর নকশায়। প্রতিটি variable-এ তিনটি জিনিস ভাবুন: type (string, number, list, map, object), default (থাকলে input ঐচ্ছিক, না থাকলে বাধ্যতামূলক) আর validation (কোন মান গ্রহণযোগ্য)। নিরাপদ default দিন; আর যা সত্যিই ডাকার পক্ষকে ভাবতে হবে, যেমন image, তার default রাখবেন না।

variable "environment" {
  type        = string
  description = "One of dev, staging, prod."

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment must be dev, staging or prod."
  }
}

variable "image" {
  type        = string
  description = "Container image with a fixed tag."

  validation {
    condition     = !endswith(var.image, ":latest") && strcontains(var.image, ":")
    error_message = "image must carry a fixed tag, not latest."
  }
}

variable "replicas" {
  type        = number
  default     = 1
  description = "Number of pods."

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

ইচ্ছা করে তিনটি ভুল মান দিয়ে ডাকলে কী হয়, দেখুন। এই call-টি ভুল বলেই validate পাস করে না:

$ cat main.tf
module "web" {
  source = "../modules/web-app"

  name        = "shop"
  environment = "production"
  namespace   = "abcl-tm"
  image       = "nginx:latest"
  replicas    = 20
}

$ terraform plan

Error: Invalid value for variable

  on main.tf line 5, in module "web":
   5:   environment = "production"
    ├────────────────
    │ var.environment is "production"

environment must be dev, staging or prod.

This was checked by the validation rule at
..\modules\web-app\variables.tf:10,3-13.

Error: Invalid value for variable

  on main.tf line 7, in module "web":
   7:   image       = "nginx:latest"
    ├────────────────
    │ var.image is "nginx:latest"

image must carry a fixed tag, not latest.
…
Error: Invalid value for variable

  on main.tf line 8, in module "web":
   8:   replicas    = 20
    ├────────────────
    │ var.replicas is 20

replicas must be between 1 and 6.
…

Terraform একবারে তিনটি ভুলই জানাল, আর cluster-এ একটিও অনুরোধ যায়নি। আর বাধ্যতামূলক input বাদ দিলে আসে The argument "image" is required, but no definition was found. একই রকম নির্দিষ্ট বার্তা।

এই পুরো নিয়মটি নিচের ছোট simulator-এ আছে। Code-এ calls-এর মান বদলে চালিয়ে দেখুন; যেমন replicas: 7 দিন, বা image মুছে দিন।

// Module input resolver: required, default, override, type, validation.
const schema = {
  name: { type: "string" },
  environment: { type: "string", check: v => ["dev", "staging", "prod"].includes(v),
    msg: "environment must be dev, staging or prod" },
  image: { type: "string", check: v => v.includes(":") && !v.endsWith(":latest"),
    msg: "image must carry a fixed tag, not latest" },
  replicas: { type: "number", default: 1, check: v => v >= 1 && v <= 6,
    msg: "replicas must be between 1 and 6" },
  memory_limit: { type: "string", default: "32Mi" },
};
function resolve(call) {
  const out = [], errors = [];
  for (const key in call) if (!(key in schema)) errors.push(`unsupported argument "${key}"`);
  for (const [key, s] of Object.entries(schema)) {
    const given = key in call;
    if (!given && !("default" in s)) { errors.push(`missing required "${key}"`); continue; }
    const value = given ? call[key] : s.default;
    if (typeof value !== s.type) { errors.push(`${key}: expected ${s.type}`); continue; }
    if (s.check && !s.check(value)) errors.push(s.msg);
    out.push(`${key}=${value} (${given ? "caller" : "default"})`);
  }
  return { out, errors };
}
const base = { name: "shop", image: "nginx:1.27-alpine" };
const calls = {
  dev: { ...base, environment: "dev" },
  prod: { ...base, environment: "prod", replicas: 3, memory_limit: "64Mi" },
  typo: { ...base, environment: "prod", replica: 3 },
  bad: { name: "shop", environment: "production", image: "nginx:latest", replicas: 20 },
};
for (const [label, call] of Object.entries(calls)) {
  const { out, errors } = resolve(call);
  console.log(`--- ${label}: ${errors.length ? "REJECTED" : "ok"}\n  ` + (errors.length ? errors : out).join("\n  "));
}

Dev-এ দেখবেন replicas=1 (default) আর memory_limit=32Mi (default); prod-এ replicas=3 (caller). typo call-এ replica বানান ভুল, তাই unsupported argument "replica" আসে; এটি একটি বড় সুবিধা, কারণ চুপচাপ default-এ চলে যাওয়ার বদলে ভুলটি ধরা পড়ে। bad call একসাথে তিনটি error দেয়, আসল Terraform-এর মতোই। Simulator সরল করা হয়েছে: আসল Terraform অজানা argument ধরে কনফিগারেশন পড়ার ধাপেই, validation-এর আগে।

অনেক সম্পর্কিত input: object আর optional

Input বাড়তে থাকলে একই বিষয়ের variable গুলো একটি object-এ রাখা যায়। Terraform ১.৩ (২০২২) থেকে optional() দিয়ে object-এর ভেতরের কোনো field-কে ঐচ্ছিক করা আর তার default দেওয়া যায়। ফলে ডাকার পক্ষকে সব field লিখতে হয় না, কিন্তু type পরীক্ষা ঠিকই থাকে। আমাদের পরীক্ষায় { min = 1 } দিয়ে ডাকলে Terraform মান বানাল min = 1, max = 6.

variable "scaling" {
  type = object({
    min = number
    max = optional(number, 6)
  })
  default = { min = 1 }
}

তবে সব কিছু একটি বিশাল settings object-এ ভরবেন না। তখন module-এর interface পড়ে আর বোঝা যায় না, আর ভুলের বার্তাও অস্পষ্ট হয়। সাধারণ নিয়ম: যে মানগুলো একসাথে বদলায়, সেগুলো এক object-এ; যা স্বাধীন, তা আলাদা variable. আর প্রতিটি variable-এ description লিখুন, কারণ পরে সেটিই ডকুমেন্টেশন হয়।

Output: module-এর দরজা

Output-এ শুধু তা-ই দিন, যা অন্যদের সত্যিই দরকার; যা দেবেন, সেটি পরে সরালে ব্যবহারকারীর কোড ভাঙবে। গোপন মান থাকলে sensitive = true দিন, তবে মনে রাখবেন তাতে শুধু স্ক্রিনে দেখানো বন্ধ হয়; মানটি state-এ থেকেই যায় (পর্ব ৫৩ দেখুন)।

একই module অনেকবার: for_each

দুটি module block হাতে লিখলে কাজ চলে, কিন্তু environment বাড়লে আবার পুনরাবৃত্তি আসে। Terraform ০.১৩ (২০২০) থেকে module block-এ for_each আর count ব্যবহার করা যায়। একটি map থেকে যত key, তত instance:

locals {
  environments = {
    dev  = { replicas = 1, memory_limit = "32Mi" }
    prod = { replicas = 3, memory_limit = "64Mi" }
  }
}

module "web" {
  source   = "../modules/web-app"
  for_each = local.environments

  name         = "shop"
  environment  = each.key
  namespace    = "abcl-tm"
  image        = "nginx:1.27-alpine"
  replicas     = each.value.replicas
  memory_limit = each.value.memory_limit
}

output "urls" {
  value = { for env, app in module.web : env => app.service_url }
}
$ terraform state list
module.web["dev"].kubernetes_deployment_v1.app
module.web["dev"].kubernetes_service_v1.app
module.web["prod"].kubernetes_deployment_v1.app
module.web["prod"].kubernetes_service_v1.app

# map-এ staging = { replicas = 2, memory_limit = "32Mi" } যোগ করার পরে
$ terraform plan
  # module.web["staging"].kubernetes_deployment_v1.app will be created
  # module.web["staging"].kubernetes_service_v1.app will be created
Plan: 2 to add, 0 to change, 0 to destroy.

count দিলে ঠিকানা হতো module.web[0], module.web[1]. তালিকার মাঝখান থেকে একটি সরালে পরের সবগুলোর নম্বর বদলাত, আর Terraform সেগুলো বদলাতে চাইত। for_each-এ key-ই ঠিকানা, তাই একটি সরালে বাকিগুলো স্থির থাকে। দুটি সীমা মনে রাখুন: key plan-এর সময়েই জানা থাকতে হবে, আর যে module-এর ভেতরে নিজস্ব provider block আছে, তাতে for_each চলে না।

৫ · Module-এর উৎস, version আর প্রকাশ

source বলে দেয় module-টি কোথা থেকে আসবে। তিনটি উৎসই সবচেয়ে বেশি ব্যবহৃত।

# 1. Local path: same repository, no version of its own
module "web_local" {
  source = "../modules/web-app"
  # ...
}

# 2. Git: an exact tag (or a commit id) after ?ref=
module "web_git" {
  source = "git::https://example.com/platform/tf-modules.git//web-app?ref=v2.0.1"
  # ...
}

# 3. Registry: namespace/name/provider, with a version constraint
module "web_registry" {
  source  = "example-org/web-app/kubernetes"
  version = "~> 1.4"
  # ...
}

Git ঠিকানার //web-app মানে repository-র ভেতরের সাব-ফোল্ডার। Registry-র নামটি কাল্পনিক। এই পর্বের Git পরীক্ষায় একটি স্থানীয় repository থেকে tag ধরে module আনা হয়েছে, আর path কেটে … করা হয়েছে:

$ terraform init
Initializing modules...
Downloading git::file:///…?ref=v1.0.0 for web...
- web in .terraform\modules\web\web-app
…
$ ls .terraform/modules
modules.json  web

লক্ষ করুন, Git module কপি হয়ে .terraform/modules/-এ আসে; local module-এ কপি হয় না। আর version শুধু registry-র জন্য। Local path-এ লিখলে Terraform সরাসরি আটকায় (আসল run):

Error: Invalid registry module source address
…
Terraform assumed that you intended a module registry source address because
you also set the argument "version", which applies only to registry modules.

Version pinning: কেন আর কীভাবে

Pin না করলে আজকের plan আর আগামী মাসের plan আলাদা কোডে চলতে পারে, যদিও আপনি কিছু বদলাননি। Semantic Versioning (MAJOR.MINOR.PATCH) এখানে একটি প্রতিশ্রুতি:

বদলঅর্থModule-এ উদাহরণ
PATCH: 2.0.0 → 2.0.1ভুল সংশোধন, ব্যবহারকারীকে কিছু করতে হয় নাভুল label ঠিক করা, moved যোগ
MINOR: 2.0.1 → 2.1.0নতুন সুবিধা, পুরোনো call আগের মতোই চলেDefault-সহ নতুন variable
MAJOR: 2.1.0 → 3.0.0Breaking changeVariable-এর নাম বদল, output সরানো
Constraintযা গ্রহণ করেকখন
= 1.4.2শুধু 1.4.2Prod-এ সবচেয়ে নিরাপদ
~> 1.4.21.4.2 থেকে 1.5.0-এর নিচেশুধু patch নিতে চাইলে
~> 1.41.4.0 থেকে 2.0.0-এর নিচেPatch আর minor, dev-এ

একটি প্রচলিত ভুল ধারণা: .terraform.lock.hcl শুধু provider-এর version আর hash রাখে, module-এর নয়। Registry module-এ ~> 1.4 লিখলে terraform init -upgrade নতুন version আনতে পারে, আর নতুন checkout-এ সাধারণ init-ও constraint-এর ভেতরের নতুনতম version নেয়। Lock ফাইল এর কোনোটিই ঠেকাবে না। Git-এ constraint নেই, শুধু নির্দিষ্ট ref; সেখানে tag সরানো সম্ভব বলে কড়া নিরাপত্তায় অনেকে পুরো commit id পিন করেন।

এক repository, না আলাদা?

দুই পথেরই দাম আছে। সব module আর সব environment এক repository-তে রাখলে local path ব্যবহার করা যায়, আর একটি pull request-এ module ও call একসাথে বদলায়। কিন্তু তখন module-এর একটি বদল সব environment-এ একসাথে পৌঁছায়, কারণ local module-এর নিজের কোনো version নেই। আলাদা repository-তে module রাখলে tag দিয়ে dev আর prod আলাদা version-এ থাকতে পারে, তবে একটি বদলে দুটি জায়গায় কাজ করতে হয়। ছোট দল সাধারণত local path দিয়ে শুরু করে; একাধিক দল একই module ব্যবহার শুরু করলে version-সহ উৎসে নেওয়ার যুক্তি শক্ত হয়।

Module প্রকাশ করা

দলের ভেতরে সাধারণত Git tag-ই যথেষ্ট: পরিবর্তন merge করুন, CHANGELOG লিখুন, v2.1.0 tag দিন। সর্বজনীন Terraform Registry-তে দিতে হলে লেখার সময়ে নিয়ম হলো GitHub-এর public repository, নাম terraform-<PROVIDER>-<NAME> আর SemVer tag; নিয়ম বদলাতে পারে, তাই আগে অফিসিয়াল ডকুমেন্টেশন দেখুন। HCP Terraform-এর মতো সেবায় private registry-ও আছে। একটি tag প্রকাশের পরে সেটি আর সরাবেন না; ভুল হলে নতুন patch দিন।

৬ · Environment সাজানো — ডিরেক্টরি বনাম workspace

Module কোড ভাগ করে। কিন্তু dev আর prod-এর state কীভাবে আলাদা হবে? দুটি প্রধান পথ আছে।

পথ ১: প্রতি environment-এ একটি ডিরেক্টরি

infra/
├── modules/
│   └── web-app/        # child module
└── envs/
    ├── dev/            # root module, own state
    │   ├── main.tf
    │   └── versions.tf
    └── prod/           # root module, own state
        ├── main.tf
        └── versions.tf
$ find . -name '*.tfstate'
./dev/terraform.tfstate
./prod/terraform.tfstate

$ diff -u dev/main.tf prod/main.tf
…
-  name        = "shop"
-  environment = "dev"
-  namespace   = "abcl-tm"
-  image       = "nginx:1.27-alpine"
+  name         = "shop"
+  environment  = "prod"
+  namespace    = "abcl-tm"
+  image        = "nginx:1.27-alpine"
+  replicas     = 3
+  memory_limit = "64Mi"
 }

প্রতিটি environment-এ কী আলাদা, তা সরাসরি একটি ফাইলে পড়া যায়। আসল প্রকল্পে প্রতিটি ফোল্ডারে আলাদা backend আর আলাদা credential থাকে। এখানে সুবিধার জন্য state স্থানীয় রাখা হয়েছে।

পথ ২: Workspace

CLI workspace মানে একই কোড আর একই backend-এ একাধিক নামের state. আলাদা ফোল্ডার নেই; কোন environment চলছে তা একটি নির্বাচিত নামের ওপর নির্ভর করে, আর কোডে সেটি terraform.workspace. মান আলাদা করা হয় tfvars ফাইল দিয়ে।

variable "replicas" {
  type = number
}

variable "memory_limit" {
  type = string
}

module "web" {
  source = "../modules/web-app"

  name         = "shop"
  environment  = terraform.workspace
  namespace    = "abcl-tm"
  image        = "nginx:1.27-alpine"
  replicas     = var.replicas
  memory_limit = var.memory_limit
}
$ terraform workspace show
default
$ terraform workspace new dev
Created and switched to workspace "dev"!
…
$ terraform apply -var-file=dev.tfvars
…
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
$ terraform workspace new prod
Created and switched to workspace "prod"!
…
$ terraform apply -var-file=prod.tfvars
…
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
$ terraform workspace list
  default
  dev
* prod

$ find . -name '*.tfstate'
./terraform.tfstate.d/dev/terraform.tfstate
./terraform.tfstate.d/prod/terraform.tfstate

একটি মজার ঘটনা: workspace না বদলে default-এ validate চালাতেই module-এর validation ধরে ফেলে: var.environment is "default". আগে লেখা নিয়মটি এখানে একটি ভুল আটকাল।

Workspace-এর আসল ঝুঁকি হলো, কোন environment-এ আছেন তা ফাইলে নয়, একটি লুকানো অবস্থায় থাকে। নিচের স্টেপারে সত্যি একই ভুল করা হয়েছে: prod workspace-এ dev-এর tfvars.

1$ terraform workspace new dev2$ terraform apply -var-file=dev.tfvars3$ terraform workspace new prod4$ terraform apply -var-file=prod.tfvars5$ terraform workspace show6$ terraform plan -var-file=dev.tfvars7$ terraform apply -var-file=dev.tfvars8$ terraform apply -var-file=prod.tfvars
অবস্থা
workspace
tfvars
shop-dev
shop-prod
prod memory
  1. Dev তৈরি। নতুন workspace, খালি state. Apply-এ shop-dev এলো, ১টি pod.
  2. Prod তৈরি। নতুন workspace, নিজের খালি state. shop-prod এলো, ৩টি pod, 64Mi. এখন নির্বাচিত workspace prod, আর সেটি পরের দিন পর্যন্ত এভাবেই থাকবে।
  3. পরের দিন। একজন dev-এ কাজ করতে বসলেন, কিন্তু workspace বদলাননি। terraform workspace show চালালে prod দেখা যেত, কিন্তু তাড়ায় অনেকেই এটি চালান না। Prompt-এ workspace দেখানো থাকলে ঝুঁকি কমে।
  4. Plan সতর্ক করেছিল। Dev-এর tfvars নিয়ে plan চলল prod-এর state-এর ওপর। আউটপুটে replicas 3 থেকে 1, memory 64Mi থেকে 32Mi. সংখ্যা ছোট বলে এটি নিরীহ দেখায়: 1 to change, 0 to destroy. এখানে id লাইনে abcl-tm/shop-prod লেখা ছিল, কিন্তু সেটি পড়তে হয়।
  5. Apply হয়ে গেল। Prod ৩টি pod থেকে ১টিতে নামল। Terraform কোনো ভুল করেনি; সে যা বলা হয়েছে তা-ই করেছে। ঈদের আগের ভিড়ের রাতে এটি হলে সরাসরি outage.
  6. ফিরিয়ে আনা। Prod-এর tfvars দিয়ে আবার apply করলে ৩টি pod ফিরে এলো। এখানে state বা data নষ্ট হয়নি, তাই ফেরানো সহজ ছিল। Database-এর মতো জিনিসে এমন ভুল সহজে ফেরানো যায় না।

Workspace রাখলে একটি পাহারা বসানো যায়। Terraform ১.৯ থেকে variable-এর validation অন্য মানও পড়তে পারে, তাই tfvars নিজেই বলে দেবে সে কোন environment-এর:

variable "env_of_tfvars" {
  type        = string
  description = "Which environment this tfvars file belongs to."

  validation {
    condition     = var.env_of_tfvars == terraform.workspace
    error_message = "tfvars file does not match the selected workspace."
  }
}
$ terraform workspace show
prod
$ terraform plan -var-file=dev.tfvars

Error: Invalid value for variable

  on dev.tfvars line 3:
   3: env_of_tfvars = "dev"
    ├────────────────
    │ terraform.workspace is "prod"
    │ var.env_of_tfvars is "dev"

tfvars file does not match the selected workspace.

This was checked by the validation rule at guard.tf:5,3-13.

কেন অনেক দল আলাদা ডিরেক্টরি পছন্দ করে

বিষয়আলাদা ডিরেক্টরিCLI workspace
কোন environmentফোল্ডারের নামেই দেখা যায়লুকানো একটি অবস্থা
Backend আর credentialআলাদা রাখা সহজএকই backend, সাধারণত একই credential
Blast radiusএকটি ফোল্ডারের ভুল সেখানেই থাকেএক ভুল কমান্ডে prod-এ যেতে পারে
প্রকৃত পার্থক্যdiff বা review-তে দেখা যায়Tfvars আর শর্ত মিলিয়ে বুঝতে হয়
Module versionDev আগে, prod পরে পিন করা যায়সব workspace-এ একই কোড
পুনরাবৃত্তিএকই ধাঁচের কিছু লেখা (boilerplate) বারবার আসেকম
ভালো মানায়Dev, staging, prodএকই রকম অস্থায়ী কপি, যেমন প্রতিটি feature-এর test stack

HashiCorp-এর নিজের ডকুমেন্টেশনও (লেখার সময়ে) বলে, আলাদা credential বা access control লাগে এমন deployment-এর জন্য CLI workspace উপযুক্ত নয়। একটি সতর্কতা: HCP Terraform-এর “workspace” শব্দটি একই নামের হলেও আলাদা জিনিস; সেখানে প্রতিটি workspace-এর নিজের variable, access আর state থাকে।

একই খাতায় দুই দোকানের হিসাব রাখলে কোন পাতায় আছেন তা সবসময় মনে রাখতে হয়। দুই দোকানের জন্য দুটি খাতা, দুই আলমারিতে, দুই চাবিতে রাখলে ভুল দোকানের হিসাবে লেখা প্রায় অসম্ভব। ডিরেক্টরি পদ্ধতি হলো দুই খাতা; workspace হলো এক খাতার দুই ভাগ।

Environment-এর পার্থক্য পরীক্ষা

আলাদা ডিরেক্টরির ঝুঁকি হলো ধীরে ধীরে অমিল জমে যাওয়া। নিয়মটি হওয়া উচিত: আকার আলাদা হতে পারে, গঠন আর নিরাপত্তা নয়। নিচের simulator দুই environment-এর মান মিলিয়ে দেখে কোন পার্থক্য পরিকল্পিত, কোনটি সন্দেহজনক। prod-এর public_db ঠিক করে আবার চালান।

// Environment diff: which dev/prod differences are planned, which are a surprise?
const dev = { module_version: "2.0.1", image: "shop:1.8.2", replicas: 1,
  memory_limit: "32Mi", db_size: "small", multi_az: false,
  public_db: false, log_days: 7 };
const prod = { module_version: "1.9.0", image: "shop:1.8.2", replicas: 3,
  memory_limit: "64Mi", db_size: "large", multi_az: true,
  public_db: true, log_days: 90 };
// Sizing may differ on purpose. Everything else must match or follow a rule.
const sizing = new Set(["replicas", "memory_limit", "db_size", "multi_az", "log_days"]);
const rules = { public_db: v => v === false };
let same = 0, planned = 0, surprises = [];
for (const key of Object.keys(dev)) {
  const a = dev[key], b = prod[key];
  const broken = rules[key] && !(rules[key](a) && rules[key](b));
  if (a === b && !broken) { same++; continue; }
  let kind = "planned";
  if (broken) kind = "RULE BROKEN";
  else if (!sizing.has(key)) kind = "SURPRISE";
  if (kind === "planned") planned++; else surprises.push(key);
  console.log(`${key.padEnd(15)} ${String(a).padEnd(8)} ${String(b).padEnd(8)} ${kind}`);
}
console.log(`\nsame=${same} planned=${planned} to_review=${surprises.length}`);
console.log("review before apply: " + surprises.join(", "));

ফল: same=1 planned=5 to_review=2. replicas বা db_size-এর পার্থক্য ইচ্ছাকৃত। কিন্তু module_version 2.0.1 1.9.0 SURPRISE দেখাচ্ছে prod পুরোনো version-এ পড়ে আছে; এটি সাময়িকভাবে ঠিক হতে পারে, তবে কেউ একজনকে জানতে হবে। আর public_db false true RULE BROKEN সরাসরি নিরাপত্তার ভুল। এমন পরীক্ষা বাস্তবে policy as code দিয়ে করা হয়, যা পরের পর্বে আসবে।

ডিরেক্টরি পদ্ধতিতে একটি বদল সাধারণত এভাবে এগোয়: আগে dev-এর ফাইলে নতুন module version, plan আর apply; কয়েক দিন দেখে staging; শেষে prod. প্রতিটি ধাপ আলাদা review. একে promotion বলে, আর এর পূর্ণ রূপ পর্ব ৫৮-তে আসবে। এখানে মূল কথা হলো, “কোন environment কোন version-এ আছে” তথ্যটি Git-এই লেখা থাকে।

Terragrunt: ধারণা

আলাদা ডিরেক্টরিতে backend আর provider-এর একই লেখা বারবার আসে। Terragrunt (Gruntwork-এর তৈরি, Terraform-এর ওপরে বসা একটি পাতলা হাতিয়ার) সেই boilerplate এক জায়গায় আনে, আর অনেক ফোল্ডারে একসাথে plan চালাতে সাহায্য করে। নিচের নমুনাটি শুধু ধারণার জন্য, চালানো হয়নি; ফাইলের নাম আর কমান্ড version-ভেদে বদলেছে, তাই অফিসিয়াল ডকুমেন্টেশন দেখুন।

include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "../../modules/web-app"
}

inputs = {
  environment = "prod"
  replicas    = 3
}

ছোট দলের জন্য সাধারণত এটি বাড়তি একটি হাতিয়ার; আগে সাধারণ ডিরেক্টরি পদ্ধতিতে কাজ চালান, boilerplate সত্যিই ব্যথা দিলে তখন ভাবুন।

৭ · নকশা — DRY, পাতলা wrapper আর composition

DRY (Don't Repeat Yourself) ভালো নিয়ম, কিন্তু infrastructure-এ অন্ধভাবে মানলে উল্টো ক্ষতি হয়। HCL-এ কোড পড়েন অনেক মানুষ, আর ভুলের দাম বেশি। তাই লক্ষ্য হলো কম লাইন নয়, পরিষ্কার পরিবর্তন।

  • এক resource-এর ওপর পাতলা wrapper সাধারণত লাভহীন। যে module শুধু একটি resource-এর সব argument variable করে দেয়, সে কিছুই লুকায় না; শুধু একটি ধাপ বাড়ায়। Module-এর মূল্য তখনই, যখন সে একটি মতামত আনে: “আমাদের সব bucket এনক্রিপ্টেড, public নয়।”
  • দুইবারের আগে নয়, তিনবারে ভাবুন। দুটি কপি মাঝে মাঝে আলাদা হয়ে যাওয়াই ঠিক। ভুল abstraction-এর চেয়ে সামান্য পুনরাবৃত্তি সস্তা।
  • Boolean-এর ঝাঁক একটি সতর্ক সংকেত। create_db, create_cache, enable_queue মানে একটি module কয়েকটি কাজ করছে।

Module কখন বেশি বড়

লক্ষণকেন সমস্যাকরণীয়
Variable-এর সংখ্যা অনেক, বেশিরভাগ শুধু ভেতরে পাঠানো হয় (pass-through)আসলে কিছুই লুকানো হচ্ছে নাভাগ করা বা সরাসরি resource
Network আর app একই module-এদুটির বদলের গতি আলাদা, plan বড়আলাদা module, আলাদা state
Plan খুব ধীরএক state-এ অনেক resourceState ভাগ করা (পর্ব ৫৩)
নামে “and” বা “common”একাধিক দায়িত্বদায়িত্ব ধরে নাম ও ভাগ

Composition: ছোট module জোড়া

Composition মানে root module-এ ছোট ছোট module সাজানো, যেখানে একটির output আরেকটির input. এটি অনেক স্তরের গভীর nesting-এর চেয়ে সাধারণত ভালো, কারণ পুরো ছবি root-এ এক নজরে দেখা যায়। আগে local cluster-এ একটি আসল উদাহরণ: api module-এর Service IP আরেকটি module-এর ConfigMap-এ যায়।

module "api" {
  source = "../modules/web-app"

  name        = "api"
  environment = "dev"
  namespace   = "abcl-tm"
  image       = "nginx:1.27-alpine"
}

module "frontend_config" {
  source = "../modules/app-config"

  name        = "frontend"
  namespace   = "abcl-tm"
  api_address = module.api.cluster_ip
}
$ terraform apply -auto-approve
module.api.kubernetes_service_v1.app: Creating...
module.api.kubernetes_deployment_v1.app: Creating...
module.api.kubernetes_service_v1.app: Creation complete after 0s [id=abcl-tm/api-dev]
module.frontend_config.kubernetes_config_map_v1.this: Creating...
module.frontend_config.kubernetes_config_map_v1.this: Creation complete after 0s [id=abcl-tm/frontend-config]
module.api.kubernetes_deployment_v1.app: Creation complete after 1s [id=abcl-tm/api-dev]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

$ kubectl -n abcl-tm get configmap frontend-config -o jsonpath='{.data.API_ADDRESS}'
10.107.14.17
$ kubectl -n abcl-tm get svc api-dev
NAME      TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
api-dev   ClusterIP   10.107.14.17   <none>        80/TCP    2s

$ terraform graph | grep 'frontend_config.*api'
  "module.frontend_config.kubernetes_config_map_v1.this" -> "module.api.kubernetes_service_v1.app";

ConfigMap শুধু Service-এর পরে শুরু হয়েছে, Deployment-এর জন্য অপেক্ষা করেনি। Module-এর পুরোটা নয়, যে resource থেকে output এসেছে, নির্ভরতা শুধু তার ওপর। এই ক্রম কেউ হাতে লেখেননি; module.api.cluster_ip রেফারেন্স থেকেই graph তৈরি হয়েছে। নিচের simulator একই যুক্তিতে network, data আর compute সাজায়:

// Modules wired by outputs: find the apply order, spot a cycle.
function plan(mods) {
  const done = {}, order = [];
  const ready = m => m.needs.every(n => n.split(".")[0] in done);
  while (order.length < Object.keys(mods).length) {
    const next = Object.keys(mods).find(k => !(k in done) && ready(mods[k]));
    if (!next) {
      const stuck = Object.keys(mods).filter(k => !(k in done));
      return `CYCLE: cannot order ${stuck.join(", ")}`;
    }
    const inputs = mods[next].needs.map(n => {
      const [m, o] = n.split(".");
      return `${o}=${done[m][o]}`;
    });
    done[next] = mods[next].make();
    order.push(`${order.length + 1}. ${next}` +
      (inputs.length ? `  <- ${inputs.join(", ")}` : "  (no module inputs)"));
  }
  return order.join("\n");
}
const stack = {
  compute: { needs: ["network.subnet_id", "data.db_host"], make: () => ({ url: "http://app.example" }) },
  data:    { needs: ["network.subnet_id"], make: () => ({ db_host: "db-7.internal" }) },
  network: { needs: [], make: () => ({ subnet_id: "subnet-0a1" }) },
};
console.log("stack:\n" + plan(stack));
const loop = {
  ...stack,
  network: { needs: ["compute.url"], make: () => ({ subnet_id: "subnet-0a1" }) },
};
console.log("\nafter network starts reading compute.url:\n" + plan(loop));

Code-এ compute সবার আগে লেখা, তবু ক্রম হলো 1. network, 2. data, 3. compute. ফাইলের ক্রম নয়, নির্ভরতাই ক্রম ঠিক করে। network যখন compute.url পড়তে শুরু করে, আউটপুট আসে CYCLE: cannot order compute, data, network. আসল Terraform-ও এমন ঘটনায় Cycle error দেয়। সমাধান হলো মাঝের জিনিসটি আলাদা করে নিচের স্তরে নেওয়া, যাতে নির্ভরতা এক দিকেই চলে।

এবার cloud-এর আকারে একই ছবি। নিচের নমুনা চালানো হয়নি; AWS provider এখানে ডাউনলোড করা হয়নি, শুধু terraform fmt পরীক্ষা হয়েছে। ফোল্ডারের ভেতরের module গুলোও ধরে নেওয়া।

module "network" {
  source     = "./modules/network"
  name       = "shop-prod"
  cidr_block = "10.20.0.0/16"
}

module "data" {
  source     = "./modules/data"
  name       = "shop-prod"
  subnet_ids = module.network.private_subnet_ids
  db_size    = "large"
}

module "compute" {
  source      = "./modules/compute"
  name        = "shop-prod"
  subnet_ids  = module.network.private_subnet_ids
  db_endpoint = module.data.endpoint
  min_size    = 3
}

# modules/network/main.tf (excerpt)
resource "aws_vpc" "this" {
  cidr_block = var.cidr_block
}
ModuleAWSAzureGoogle Cloud
networkaws_vpcazurerm_virtual_networkgoogle_compute_network
dataaws_db_instanceazurerm_postgresql_flexible_servergoogle_sql_database_instance
computeaws_autoscaling_groupazurerm_linux_virtual_machine_scale_setgoogle_compute_instance_group_manager

Module provider-এর পার্থক্য লুকায় না; aws_vpc-র module Azure-এ চলবে না। একই interface রাখা যায়, কিন্তু ভেতরের কোড আলাদা লিখতে হয়। বাস্তবে network, data আর compute প্রায়ই আলাদা state-এও রাখা হয়, কারণ network বদলায় কম আর ভুলের blast radius বেশি। তখন এক state থেকে আরেকটিতে মান আনার পথ পর্ব ৫৩-তে দেখেছেন।

৮ · Breaking change, moved, ডকুমেন্টেশন

Module একবার অন্যের হাতে গেলে সে একটি API. আর Terraform-এ resource-এর পরিচয় হলো তার ঠিকানা। তাই ভেতরের একটি resource-এর নাম বদলানোও breaking change, যদিও cluster-এর কিছুই বদলায়নি। নিচের পরীক্ষায় একটি module-এর তিনটি সত্যিকারের Git tag আছে। v2.0.0-এ দুটি কাজ করা হয়েছে: replicas হয়েছে replica_count, আর resource-এর নাম app থেকে this.

  1. v1.0.0replicasresource: app
  2. v2.0.0replica_countresource: this
  3. v2.0.1movedঠিকানা বদলের নোটিশ
v1.0.0Prod চলছে
source = "git::…//web-app?ref=v1.0.0"
replicas = 2

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
module.web.kubernetes_deployment_v1.app
module.web.kubernetes_service_v1.app
ref বদলানো হলোকেউ init চালাননি
$ terraform plan

Error: Module source has changed

The source address was changed since this module was installed.
Run "terraform init" to install all modules required by this
configuration.
terraform initInput ভেঙেছে
Downloading git::file:///…?ref=v2.0.0 for web...

Error: Unsupported argument

  on main.tf line 8, in module "web":
   8:   replicas = 2

An argument named "replicas" is not expected here.
Caller ঠিক করার পরে planএবার ভয়ংকর অংশ
# module.web.kubernetes_deployment_v1.app will be destroyed
# (because kubernetes_deployment_v1.app is not in configuration)
# module.web.kubernetes_deployment_v1.this will be created
# module.web.kubernetes_service_v1.app will be destroyed
# (because kubernetes_service_v1.app is not in configuration)
# module.web.kubernetes_service_v1.this will be created
Plan: 2 to add, 0 to change, 2 to destroy.
v2.0.1: moved যোগএকই বদল, কোনো ক্ষতি নেই
# module.web.kubernetes_deployment_v1.app has moved to
#   module.web.kubernetes_deployment_v1.this
# module.web.kubernetes_service_v1.app has moved to
#   module.web.kubernetes_service_v1.this
Plan: 0 to add, 0 to change, 0 to destroy.

$ terraform state list
module.web.kubernetes_deployment_v1.this
module.web.kubernetes_service_v1.this
  1. v1.0.0-এ prod চলছে। State-এ ঠিকানা দুটি …app. কোনো সমস্যা নেই।
  2. ref বদলানো হলো। Terraform বলল source বদলেছে, আগে terraform init দরকার। অর্থাৎ ref বদলালে upgrade নীরবে ঘটে না; এটি একটি নিরাপত্তা-জাল।
  3. Input ভেঙে গেল। init নতুন কোড নামাল, আর সাথে সাথে জানাল replicas আর গ্রহণযোগ্য নয়। এটি ভালো ধরনের ভাঙা: জোরে শব্দ করে, কিছু নষ্ট না করে। MAJOR version বাড়ার কারণ এটাই।
  4. Plan-এ লুকানো ভাঙা। Call ঠিক করার পরে কোনো error নেই, কিন্তু plan বলছে 2 to destroy. জিনিস একই, নাম একই, শুধু ঠিকানা আলাদা। এই পরীক্ষায় apply চালিয়ে দেখা গেল, পুরোনো ও নতুন object-এর মধ্যে কোনো নির্ভরতা নেই বলে Terraform destroy আর create একসাথে শুরু করল। নতুন Service-এর IP 10.101.133.55 থেকে বদলে হলো 10.106.15.191, আর পুরোনো ঠিকানার destroy Still destroying… দেখিয়ে আটকে রইল (সম্ভবত একই নামের object থেকে যাওয়ায়)। আমরা ৯০ সেকেন্ড পরে run থামিয়ে দিই। অন্য provider-এ বরং “already exists” ধরনের error আসতে পারে।
  5. সমাধান moved. v2.0.1-এ moved block যোগ হলো। Terraform শুধু state-এর ঠিকানা বদলাল; cluster-এ একটি অনুরোধও পরিবর্তনের জন্য গেল না। Plan: 0 to add, 0 to change, 0 to destroy. পুরোনো ঠিকানা থেকে নতুন ঠিকানায় চিঠি পাঠানোর ডাকঘরের নোটিশের মতো।
moved {
  from = kubernetes_deployment_v1.app
  to   = kubernetes_deployment_v1.this
}

moved {
  from = kubernetes_service_v1.app
  to   = kubernetes_service_v1.this
}

moved Terraform ১.১ (২০২১) থেকে আছে। এটি module-এর ভেতরে লেখা যায়, তাই লেখকই ব্যবহারকারীদের জন্য migration পাঠাতে পারেন; আগে প্রতিটি ব্যবহারকারীকে হাতে terraform state mv চালাতে হতো। এটি module call-এও চলে। আগের অংশের দুটি আলাদা block থেকে for_each-এ যাওয়ার সময় আসল run-এর ফল:

# moved ছাড়া: module.web_dev → module.web["dev"]
Plan: 4 to add, 0 to change, 4 to destroy.

# নিচের দুই block যোগ করার পরে
#   moved { from = module.web_dev   to = module.web["dev"] }
#   moved { from = module.web_prod  to = module.web["prod"] }
  # module.web_dev.kubernetes_deployment_v1.app has moved to module.web["dev"].kubernetes_deployment_v1.app
  …
Plan: 0 to add, 0 to change, 0 to destroy.

Plan-এ একই নামের জিনিসে destroy আর create একসাথে দেখলে থামুন। প্রায়ই এটি একটি ঠিকানা বদল। moved রাখুন অন্তত যতদিন পুরোনো version-এ কেউ আছেন; মুছে ফেললে পিছিয়ে থাকা ব্যবহারকারীরা আবার destroy দেখবেন। আর ঠিকানা ছাড়াও কিছু বদল আছে, যেমন Deployment-এর selector বা কোনো immutable argument, যা moved দিয়ে এড়ানো যায় না।

Input-এর নাম ভাঙা ছাড়া বদলানো

moved শুধু resource-এর ঠিকানা বদলায়; variable-এর নাম বদলের জন্য এমন কিছু নেই। সেখানে পরিচিত কৌশল হলো আগে যোগ, পরে বাদ: কিছুদিন দুটি নামই চলবে। একটি MINOR release-এ নতুন replica_count আসে, পুরোনো replicas-ও থাকে, আর পরের MAJOR-এ পুরোনোটি সরে। Terraform ১.১৫ থেকে variable-এ deprecated argument আছে, যা ব্যবহারকারীকে সতর্ক করে (নিচের run ১.১৭-এ)। এটি নতুন সুবিধা, তাই আপনার version-এ বা OpenTofu-তে আছে কি না release note দেখুন।

variable "replicas" {
  type        = number
  default     = null
  description = "Deprecated, use replica_count."
  deprecated  = "replicas is deprecated, use replica_count."
}

variable "replica_count" {
  type        = number
  default     = null
  description = "Number of pods."
}

locals {
  replicas = coalesce(var.replica_count, var.replicas, 1)
}
$ terraform validate
Warning: Deprecated variable got a value

  on main.tf line 3, in module "web":
   3:   replicas = 3

replicas is deprecated, use replica_count.
Success! The configuration is valid, but there were some validation warnings
as shown above.

এতে ব্যবহারকারীরা নিজের সময়ে সরে যেতে পারেন, আর কারও plan হঠাৎ ভাঙে না। v2.0.0-এর মতো একসাথে নাম বদলানোর চেয়ে এটি একটু বেশি কাজ, কিন্তু অনেকে নির্ভর করলে সেই কাজের দাম দেওয়াই ভালো।

নিরাপদ upgrade-এর অভ্যাস

  1. CHANGELOG পড়ুন; MAJOR বদল হলে migration নোট খুঁজুন।
  2. Dev-এর ডিরেক্টরিতে ref বা version বদলান, terraform init চালান।
  3. terraform plan পড়ুন; destroy থাকলে কারণ না জেনে এগোবেন না।
  4. Dev-এ apply, পরীক্ষা; তারপর staging; শেষে prod. প্রতিটি ধাপ আলাদা pull request.

ডকুমেন্টেশন আর example

একটি module-এর সবচেয়ে ব্যবহৃত অংশ তার README.md: কী করে, কী করে না, একটি ন্যূনতম call, আর upgrade নোট। প্রতিটি variable আর output-এ description লিখুন; terraform-docs নামের একটি জনপ্রিয় community টুল সেখান থেকে টেবিল বানাতে পারে। একটি examples/ ফোল্ডারে চালানো যায় এমন উদাহরণ রাখুন: examples/basic, examples/complete. এগুলো একসাথে ডকুমেন্টেশন আর পরীক্ষা; CI-তে চালালে পুরোনো হয়ে পড়ে না। স্বয়ংক্রিয় পরীক্ষা (terraform test) আর policy পর্ব ৫৫-এ পাবেন।

Terraform প্রথম আসে ২০১৪ সালে, আর module তার শুরুর দিক থেকেই আছে। Module-এ for_each ও count এলো ০.১৩-তে (২০২০), moved এলো ১.১-এ (২০২১)। আগস্ট ২০২৩-এ HashiCorp Terraform-কে BUSL লাইসেন্সে নেয়; তার ফর্ক OpenTofu এখন Linux Foundation-এর অধীনে। Module-এর ধারণা দুই টুলেই একই রকম, তবে নতুন সুবিধায় পার্থক্য থাকতে পারে।

আরও গভীরে: Module আসলে সংকলনের সময়ের একটি namespace; plan-এর আগে Terraform সব module মিলিয়ে একটি বড় resource graph বানায়, আর module-এর input ও output সেই graph-এ আলাদা node হিসেবে থাকে। তাই নির্ভরতা পুরো module-এ নয়, প্রতিটি output ধরে তৈরি হয়, যা আসল run-এ দেখেছেন। Module call-এ depends_on দিলে এই সূক্ষ্মতা হারায়: পুরো module আরেকটির পরে চলে, আর অনেক মান “known after apply” হয়ে plan অস্পষ্ট হতে পারে। তাই সম্ভব হলে output রেফারেন্সই ব্যবহার করুন।

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

  • Root আর child module-এর পার্থক্য? যেখানে কমান্ড চলে সেটি root; module block দিয়ে ডাকা হলে child.
  • count না for_each? স্থির পরিচয় দরকার হলে for_each; তালিকার index বদলে যাওয়ার ঝুঁকি এড়ায়।
  • Lock ফাইল কি module পিন করে? না, শুধু provider. Module পিন করতে version বা ref.
  • Workspace কি environment আলাদা করার সেরা উপায়? Credential আলাদা লাগলে সাধারণত না; ডিরেক্টরি বা আলাদা account-এ আলাদা backend.
  • moved কী করে? Infrastructure না ছুঁয়ে state-এর ঠিকানা বদলায়।

৯ · AI-এর যুগে module লেখা ও যাচাই

AI খুব দ্রুত একটি module লিখতে পারে। কিন্তু module-এর ভুল একবার নয়, প্রতিটি call-এ গুণ হয়ে ছড়ায়। তাই এখানে যাচাই আরও জরুরি।

AI-এর ভুলফলকীভাবে ধরবেন
নিরাপদ নয় এমন default: 0.0.0.0/0, public storage, * অনুমতিপ্রতিটি call-এ ত্রুটি ছড়ায়variables.tf-এর সব default পড়ুন
image = "nginx:latest" বা version ছাড়া sourceপ্রতি run-এ ভিন্ন ফলValidation, নির্দিষ্ট tag আর ref
Child module-এর ভেতরে provider blockfor_each চলে না, reuse ভাঙেProvider শুধু root-এ রাখুন
অস্তিত্বহীন argument বা module-এর নামPlan-এ error, বা ভুল উৎস থেকে কোডterraform validate, registry-তে মিলিয়ে দেখা
“Refactor” করে resource-এর নাম বদল, moved ছাড়াProd-এ destroy আর createPlan-এর destroy গোনা
Secret-এর default মান, বা outputs-এ secretGit আর state-এ ফাঁসDefault বাদ, sensitive, secret manager
সমস্যা সমাধানে terraform destroy বা state rm পরামর্শ, সতর্কতা ছাড়াData হারানোDestructive কমান্ড নিজে বুঝে চালান
খরচ না ভেবে বড় default sizeসব environment-এ বাড়তি বিলছোট default, budget alert

নিচের prompt-এ সীমা আগেই বলে দেওয়া হয়েছে, আর ফল হিসেবে ঝুঁকির তালিকাও চাওয়া হয়েছে:

আমার একটি Terraform module আছে (নিচে পেস্ট করছি), আর আমি resource-এর নাম "app" থেকে "this" করতে চাই। সীমা: prod-এ কিছু destroy হবে না, কোনো variable-এর নাম বদলাবে না, provider version একই থাকবে। ১. দরকারি moved block গুলো লেখো। ২. কোন প্রকার SemVer বদল (patch, minor, major) হওয়া উচিত, কারণসহ বলো। ৩. আমি কোন কমান্ড চালিয়ে যাচাই করব, আর plan-এর আউটপুটে ঠিক কী দেখলে নিরাপদ, সেটি বলো। ৪. অস্তিত্ব নিশ্চিত নয় এমন কোনো argument ব্যবহার করলে আলাদা করে জানাও। apply বা destroy চালাতে বলবে না। [এখানে module-এর কোড পেস্ট করুন, কোনো secret ছাড়া]

AI-এর লেখা module হাতে পেলে — চেকলিস্ট

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

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

প্র ০১DRY তো ভালো নিয়ম। তাহলে infrastructure কোডে পুনরাবৃত্তি কখনো ঠিক হতে পারে কেন?

২০১৬ সালে Sandi Metz তাঁর “The Wrong Abstraction” লেখায় একটি বিখ্যাত কথা বলেন: ভুল abstraction-এর চেয়ে পুনরাবৃত্তি অনেক সস্তা। ঘটনাটি এভাবে ঘটে। দুটি জিনিস দেখতে একই, তাই একজন module বানালেন। কিছুদিন পরে একটি জায়গায় সামান্য ভিন্ন আচরণ দরকার হলো। কেউ একটি boolean যোগ করলেন। তারপর আরেকটি। এক বছর পরে module-এর ভেতরে শর্তের জঙ্গল, আর কেউ সাহস করে সেটি ভাঙেন না, কারণ অনেকে নির্ভর করছেন।

Infrastructure-এ এই ঝুঁকি আরও বড়, তিনটি কারণে:

  • পরিবর্তনের দাম বেশি। App-এ ভুল abstraction সরালে একটি deploy; Terraform module সরালে state-এর ঠিকানা বদলায়, যা production-এ ঝুঁকি।
  • স্পষ্টতা নিজেই নিরাপত্তা। Prod-এর ফাইলে সরাসরি replicas = 3 পড়া যায়। তিন স্তরের module আর merge-এর ভেতর দিয়ে মান খুঁজতে গেলে review দুর্বল হয়।
  • পার্থক্য অনেক সময় ইচ্ছাকৃত। Dev আর prod-এর backup নীতি একই হওয়ার কথা নয়। জোর করে এক করলে সেই সত্যটি লুকিয়ে পড়ে।

ব্যবহারিক নিয়ম হলো জিজ্ঞেস করা: এই দুটি জিনিস কি একই কারণে বদলাবে? যদি হ্যাঁ, একটি সত্যিকারের ধারণা, module বানান। যদি শুধু দেখতে একই, আলাদা রাখুন। তৃতীয়বার একই জিনিস লেখার সময়ে সাধারণত ছবিটি পরিষ্কার হয়। ভুল module বানিয়ে ফেললে সাহস করে ভেতরের কোড আবার কপিতে ফিরিয়ে আনাও একটি বৈধ সংশোধন; Metz-ও ঠিক এই পরামর্শই দেন।

মূল উপলব্ধি: DRY-এর লক্ষ্য লাইন কমানো নয়, একটি সিদ্ধান্তকে এক জায়গায় রাখা। সিদ্ধান্ত যদি সত্যিই আলাদা হয়, কপিটাই সৎ নকশা।

প্র ০২Workspace এত সুবিধাজনক হলেও অনেক দল production-এ কেন আলাদা ডিরেক্টরি বা আলাদা account বেছে নেয়?

মূল প্রশ্নটি সুবিধার নয়, সীমানার। একটি নিরাপত্তা-সীমানা তখনই কাজের, যখন ভুল করলেও সেটি পার হওয়া কঠিন। আমাদের আসল run-এ একটি মাত্র ভুল -var-file prod-কে ৩টি pod থেকে ১টিতে নামিয়েছে, কোনো error ছাড়াই। আলাদা ডিরেক্টরিতে একই ভুল করতে হলে ভুল ফোল্ডারে ঢুকতে হবে, আর আলাদা credential থাকলে অনুমতিতেই আটকে যাবে।

বিষয়টি তিন স্তরে ভাবা যায়:

  • Blast radius: একই backend-এ সব state থাকলে backend-এর অনুমতি সব environment-এ চলে। Dev-এর একজন ইঞ্জিনিয়ার বা CI job, যার prod-এ হাত দেওয়ার কথা নয়, সেও state পড়তে পারে; আর state-এ secret থাকতে পারে।
  • Least privilege: Least privilege মানলে dev-এর pipeline-এর prod-এর credential থাকা উচিত নয়। Workspace এটি নিজে আলাদা করে না। এজন্যই cloud-এ dev আর prod-কে আলাদা account বা subscription-এ রাখা একটি প্রচলিত অভ্যাস (landing zone, পর্ব ৩৫)।
  • Promotion: ডিরেক্টরিতে dev-এ module v2 আর prod-এ v1 রাখা স্বাভাবিক; ধীরে ধীরে এগোনো যায়। এক কোড ভাগ করলে কোড বদলানো মানেই সব workspace-এ বদল, যদি না শর্ত দিয়ে জটিল করেন।

তাই বলে workspace খারাপ নয়। একই ধরনের অস্থায়ী কপি, যেমন প্রতিটি pull request-এর জন্য একটি test stack, এতে চমৎকার মানায়। ঝুঁকি একই, ক্ষতি ছোট। অন্যদিকে HCP Terraform-এর workspace আলাদা ধারণা; সেখানে access আলাদা করা যায়, তাই নাম দেখে সিদ্ধান্ত নেবেন না।

মূল উপলব্ধি: নিয়মের ওপর ভরসা না করে কাঠামো দিয়ে ভুলকে কঠিন করুন। “সাবধানে কাজ করব” কোনো নিরাপত্তা-নকশা নয়।

প্র ০৩Terraform module, Helm chart আর Kustomize overlay — তিনটিই reuse-এর কথা বলে। আসল পার্থক্য কোথায়?

তিনটিই “একবার লিখে বহুবার ব্যবহার” সমস্যার সমাধান, কিন্তু কাজের স্তর আর পদ্ধতি আলাদা (পর্ব ৫০ দেখুন)।

  • Terraform module: Provider থাকলে যেকোনো জিনিস সামলায়: network, database, DNS, Kubernetes. মানের type আছে, validation আছে, plan আছে, আর state থাকে। দুর্বলতা হলো state-এর বোঝা, আর cluster-এর ভেতরের দ্রুত পরিবর্তনে এটি ধীর।
  • Helm chart: Text template, Kubernetes-এর জন্য তৈরি। Release-এর ইতিহাস আর rollback আছে। কিন্তু template YAML-কে text হিসেবে জোড়ে, তাই ভুল অনেক সময় দেরিতে ধরা পড়ে।
  • Kustomize overlay: Template নেই; একটি বৈধ YAML-এর ওপর patch বসায়। পড়া সহজ, কিন্তু প্রচুর শর্ত প্রকাশে দুর্বল।

নকশার দিক থেকে মূল পার্থক্য হলো: parameter বনাম patch. Module আর chart বলে “যে মান আমি খুলে দিয়েছি, শুধু সেগুলো বদলাতে পারবেন।” এতে লেখকের নিয়ন্ত্রণ বেশি, তবে নতুন প্রয়োজনে নতুন variable লাগে; তাই ধীরে ধীরে সব chart “সবকিছু values-এ” রূপ নেয়। Overlay বলে “যা খুশি বদলান।” এতে ব্যবহারকারীর স্বাধীনতা বেশি, তবে ভেতরের কাঠামো বদলালে patch ভাঙে।

বাস্তবে প্রায়ই মিলিয়ে ব্যবহার হয়: Terraform দিয়ে cluster আর managed database, GitOps-সহ Helm বা Kustomize দিয়ে app (পর্ব ৬০)। আমাদের module-এ Deployment Terraform দিয়ে বানানো হয়েছে শেখার সুবিধার জন্য; অনেক দল app deployment পৃথক টুলে রাখে, কারণ app-এর release-এর গতি infrastructure-এর চেয়ে অনেক বেশি।

মূল উপলব্ধি: টুল বাছুন পরিবর্তনের গতি আর মালিকানা দেখে। ধীরে বদলানো, অনেকের ভাগ করা জিনিস এক টুলে; দ্রুত বদলানো app আরেক টুলে।

প্র ০৪Resource-এর ভেতরের নাম বদলানো কেন breaking change, অথচ cloud-এ তো কিছুই বদলায়নি?

কারণ Terraform-এর কাছে পরিচয়ের দুটি স্তর আছে। আসল জিনিসের পরিচয় হলো provider-এর id, যেমন abcl-tm/shop-prod. কিন্তু “কোডের কোন block-টি সেই জিনিসের মালিক”, তা state-এ ঠিকানা দিয়ে বাঁধা: module.web.kubernetes_deployment_v1.app. নাম বদলালে Terraform দেখে, state-এ একটি ঠিকানা আছে যার কোড নেই, আর কোডে একটি block আছে যার state নেই। তার যুক্তিতে একটি মুছতে হবে, একটি বানাতে হবে। সে দুটিকে একই জিনিস বলে অনুমান করে না, আর এই না-করাটাই ঠিক; অনুমান ভুল হলে ক্ষতি আরও বড়।

আমাদের আসল run-এ এর ফল দেখা গেছে। প্রথমত plan-এ 2 to destroy. দ্বিতীয়ত apply-এ destroy আর create একসাথে শুরু হলো, Service নতুন IP পেল, আর পুরোনো ঠিকানার destroy শেষই হলো না। একটি stateful database হলে এটি data হারানোর ঘটনা হতে পারত।

Terraform ১.১-এর আগে সমাধান ছিল হাতে terraform state mv: প্রতিটি ব্যবহারকারী, প্রতিটি environment, একটি করে কমান্ড, কোনো review ছাড়া। ২০২১-এ moved এই কাজটিকে কোডে আনল, তাই এখন এটি version control-এ থাকে, plan-এ দেখা যায়, আর module-এর সাথে পৌঁছে যায়। পর্ব ৫৩-এর import ও removed block একই ধারার: state-এর কাজকে হাতের কমান্ড থেকে কোডে আনা।

এখানে Hyrum's Law মনে পড়ে: যথেষ্ট ব্যবহারকারী থাকলে আপনার সিস্টেমের সব দৃশ্যমান আচরণের ওপর কেউ না কেউ নির্ভর করবেন। Terraform module-এ resource-এর ঠিকানাও সেই আচরণের অংশ, যদিও আপনি কখনো সেটি “প্রকাশ” করেননি।

মূল উপলব্ধি: Module-এর public API শুধু variable আর output নয়; state-এর ঠিকানাও তার অংশ। ভেতরের refactor-ও তাই migration-সহ পাঠাতে হয়।

প্র ০৫Public registry-র একটি module সরাসরি ব্যবহার করা কতটা নিরাপদ? নিজে লিখব, না নেব?

Module হলো আপনার credential নিয়ে চলা অন্যের কোড। Plan-এর সময়েই provider আর data source চলে, আর module-এর ভেতরের কিছু ব্যবস্থা (যেমন local-exec provisioner) apply-এর সময়ে আপনার মেশিন বা CI-তে কমান্ড চালাতে পারে। তাই এটি একটি supply chain প্রশ্ন, ঠিক container image-এর মতো (পর্ব ৪১)।

নেওয়ার পক্ষে যুক্তি শক্ত। বহুল ব্যবহৃত একটি network module-এ হাজারো ব্যবহারকারীর ধরা ভুলের সংশোধন আছে, যা আপনি একা পেতেন না। বিপক্ষে: সব provider-সুবিধা খুলে রাখতে গিয়ে এগুলো বিশাল হয়, শত শত variable নিয়ে; আপনার দরকার হয়তো তার দশ ভাগের এক ভাগ। আর একটি major upgrade আপনার সময়সূচি মেনে আসে না।

একটি ব্যবহারিক মধ্যপথ:

  • উৎস যাচাই: কে রক্ষণাবেক্ষণ করেন, কত সক্রিয়, লাইসেন্স কী। Registry-র কোনো badge বা partner চিহ্ন সহায়ক, নিরাপত্তার নিশ্চয়তা নয়।
  • কড়া pin: নির্দিষ্ট version; সংবেদনশীল ক্ষেত্রে Git commit id, কারণ tag সরানো সম্ভব।
  • কোড পড়া: অন্তত provisioner, local-exec, বাইরের URL আর IAM অনুমতি খুঁজে দেখা। Plan খুঁটিয়ে দেখা।
  • নিজের wrapper বা mirror: বড় প্রতিষ্ঠান অনেক সময় বাইরের module নিজের registry-তে আনে, আর একটি পাতলা module-এ নিজেদের নীতি (tag, encryption) বসায়। এখানে পাতলা wrapper-এর একটি সত্যিকারের মূল্য আছে, কারণ সে একটি মতামত যোগ করছে।

সাধারণ নিয়ম: যা আপনার ব্যবসার বিশেষত্ব নয় এবং ভালো রক্ষণাবেক্ষণে আছে, সেটি নিন। যা ছোট আর আপনার নীতি-নির্ভর, সেটি নিজে লিখুন; একটি ৮০ লাইনের module দীর্ঘদিন বোঝা সহজ।

মূল উপলব্ধি: নেওয়া মানে নির্ভরতা কেনা। দাম দিতে হয় যাচাই, pin আর upgrade-এর সময়ে; সেই হিসাব করেই সিদ্ধান্ত নিন।

অনুশীলন

প্রথমে নিজে উত্তর লিখুন, তারপর মিলিয়ে নিন। এগুলোর জন্য কোনো cloud account লাগবে না।

  1. হিসাব ও constraint: একটি module-এ ৪টি resource আছে, আর for_each-এর map-এ ৩টি key. (ক) খালি state-এ plan-এ কত add? (খ) একটি key সরালে কী plan? (গ) প্রকাশিত version হলো 1.3.9, 1.4.2, 1.5.0, 2.0.0. ~> 1.4 আর ~> 1.4.0 কোনগুলো নেবে, আর সবচেয়ে নতুন কোনটি বেছে নেবে?

    (ক) ৩ × ৪ = ১২, অর্থাৎ Plan: 12 to add, 0 to change, 0 to destroy.

    (খ) শুধু সেই key-এর ৪টি resource যাবে: 0 to add, 0 to change, 4 to destroy. বাকি দুই key-এর ঠিকানা বদলায় না।

    (গ) ~> 1.4 মানে 1.4 বা তার বেশি, 2.0-এর নিচে: 1.4.2 আর 1.5.0; বাছবে 1.5.0. ~> 1.4.0 মানে 1.4.0 বা তার বেশি, 1.5.0-এর নিচে: শুধু 1.4.2.

  2. ফল আন্দাজ করুন: Root-এ module "web" দিয়ে ২টি resource তৈরি আছে। আপনি block-এর নাম module "frontend" করলেন। (ক) moved ছাড়া plan-এর শেষ লাইন কী? (খ) moved { from = module.web to = module.frontend } দিলে কী? (গ) (খ)-এর apply-এর পরে terraform state list-এর লাইন কেমন হবে?

    (ক) Plan: 2 to add, 0 to change, 2 to destroy. Terraform ঠিকানা আলাদা দেখে।

    (খ) Plan: 0 to add, 0 to change, 0 to destroy. সাথে দুটি “has moved to” লাইন।

    (গ) module.frontend.… দিয়ে শুরু দুটি লাইন; module.web আর নেই।

  3. নকশা সমালোচনা: একটি দল module "platform" বানিয়েছে: ৬৫টি variable, ভেতরে VPC, database, Kubernetes cluster আর সব app; create_db, create_cluster-এর মতো ১২টি boolean. একই call তিনটি workspace-এ চলে, আর source হলো ?ref=main. অন্তত পাঁচটি সমস্যা ও পরিবর্তনের পরামর্শ দিন।
    • বিশাল module: Network, data, compute আলাদা করুন, আর root-এ composition করুন।
    • Boolean-এর ঝাঁক: এটি কয়েকটি module একসাথে থাকার চিহ্ন। “না চাইলে” module-টি না ডাকাই পথ।
    • এক state, বড় blast radius: App-এর ছোট বদলেও VPC plan-এ আসে। বদলের গতি ধরে state ভাগ করুন।
    • ?ref=main: Pin নেই, প্রতি init-এ কোড বদলাতে পারে। SemVer tag দিন, dev আগে, prod পরে।
    • Workspace-এ prod: Credential আর backend এক। ডিরেক্টরি আর আলাদা account-এ নিন।

    ভাগ করার সময় moved বা import পরিকল্পনা করুন, নইলে prod-এ destroy আসবে।

  4. Workspace-এর অবস্থা বলুন: একটি নতুন ফোল্ডারে পরপর চলল: terraform workspace new dev, terraform workspace new prod, terraform workspace select dev, terraform apply (২টি resource). এরপর terraform workspace select prod আর terraform state list. (ক) শেষ কমান্ড কী দেখাবে? (খ) terraform workspace list-এ তারকা কোথায়? (গ) Local backend-এ কোন state ফাইলে resource আছে?

    (ক) কিছুই না। Prod-এর state খালি, কারণ apply হয়েছে dev-এ। একই কারণে এখানে apply করলে আবার ২টি resource তৈরির plan আসবে; কোডে নাম workspace-নির্ভর না হলে একই নামের সংঘর্ষ হতে পারে।

    (খ) * prod; সাথে default আর dev তালিকায় থাকবে।

    (গ) terraform.tfstate.d/dev/terraform.tfstate.

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

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

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

  1. CHANGELOG আর migration নোট পড়া
  2. Dev-এর call-এ ref বা version বদলানো
  3. terraform init চালিয়ে নতুন module আনা
  4. terraform plan পড়ে প্রতিটি destroy-এর কারণ বোঝা
  5. Dev-এ apply করে পরীক্ষা, তারপর prod-এ একই ধাপ

Terraform-এ root module বলতে কী বোঝায়?

  • Public registry থেকে নামানো module
  • যে ফোল্ডারে terraform plan চালানো হয়
  • module block দিয়ে ডাকা ফোল্ডার
  • যে module-এর administrator অনুমতি আছে

একটি দল registry module-এ version = "~> 1.4" লিখেছে আর .terraform.lock.hcl Git-এ রেখেছে। terraform init -upgrade চালালে কী হতে পারে?

  • Lock ফাইলের কারণে module একই version-এ থাকবে
  • Module 2.0.0-তে উঠে যেতে পারে
  • Module 2.0-এর নিচের নতুন 1.x version-এ উঠতে পারে
  • Lock ফাইল থাকলে -upgrade error দেবে

Module-এর ভেতরের resource-এর নাম app থেকে this করা হলো, moved ছাড়া। আগে তৈরি একটি resource-এর plan-এ কী দেখা যাবে?

  • পুরোনো ঠিকানায় destroy, নতুন ঠিকানায় create
  • কোনো পরিবর্তন নেই, Terraform নিজে বুঝে নেবে
  • একই resource-এ in-place update
  • Plan error দিয়ে থেমে যাবে

Module-এর একটি call-এ for_each ব্যবহারের মূল সুবিধা count-এর তুলনায় কী?

  • Plan অনেক দ্রুত চলে
  • প্রতিটি instance আলাদা state ফাইলে থাকে
  • প্রতিটি instance আলাদা credential পায়
  • স্থির key থাকায় একটি সরালে বাকিদের ঠিকানা বদলায় না

SemVer মানলে একটি Terraform module-এর কোন পরিবর্তনে MAJOR version বাড়ানো উচিত?

  • Default-সহ নতুন একটি variable যোগ
  • একটি বাধ্যতামূলক variable-এর নাম বদল
  • README-তে বানান সংশোধন
  • ভুল description সংশোধন

একটি প্রতিষ্ঠানে dev আর prod-এর credential আলাদা, আর dev-এর CI কখনো prod-এ হাত দিতে পারবে না। Terraform-এ কোন সাজ সবচেয়ে মানানসই?

  • একই ফোল্ডারে দুটি CLI workspace
  • এক root module-এ দুটি module call, এক state-এ
  • প্রতি environment-এ আলাদা root ডিরেক্টরি ও backend, child module ভাগ করে
  • সব কোড কপি করে একই backend-এ দুটি ফাইল

শব্দকোষ

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

মনে রাখুন

  1. Module হলো .tf ফাইলের ফোল্ডার: variable input, output ফেরত মান, resource ভেতরের কাজ।
  2. Validation ভুল মান plan-এ আটকায়; for_each স্থির key দিয়ে একই module বহুবার ডাকে।
  3. Version pin করুন: lock ফাইল module রাখে না। SemVer-এ ভাঙা বদল মানে MAJOR.
  4. ডিরেক্টরি আলাদা state আর credential-এ সীমানা দেয়; workspace অস্থায়ী কপিতে ভালো, কিন্তু ভুল environment-এর ঝুঁকি আনে।
  5. ঠিকানাই পরিচয়: নাম বদলালে moved দিন, আর plan-এর প্রতিটি destroy-এর কারণ জানুন।

পরের পর্বে: Module আছে, plan আছে। কিন্তু plan কে পড়বে, আর public database যে আটকাবে, সে কে? পর্ব ৫৫-এ দেখব plan review, policy as code আর নিরাপত্তা scan.

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

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