أساسيات Terraform

الوسائط الوصفية: count وfor_each وlifecycle

22 دقيقة الدرس 8 من 30

الوسائط الوصفية: count وfor_each وlifecycle

كل كتلة مورد في Terraform تدير، افتراضيًا، كائن بنية تحتية واحدًا بالضبط. حين تحتاج إلى عشر قواعد لمجموعة الأمان، أو خمسة دلاء S3، أو أسطول من مستخدمي IAM، فإن تكرار كتل الموارد ليس خيارًا — إذ يدمر القابلية للقراءة ويجعل الإعداد عرضة للانجراف. الوسائط الوصفية هي وسائط خاصة تتعرف عليها Terraform نفسها (لا المزود) وتغير كيفية تصرف المورد: عدد النسخ الموجودة، ومجموعة القيم التي تحرك كل نسخة، والقواعد التي تحكم الإنشاء والحذف والاستبدال. في Google وStripe وCloudflare، الاستخدام الصحيح لـcount وfor_each وlifecycle شرط مسبق لكتابة أي وحدة إنتاجية.

count: التكرار العددي البسيط

يأخذ count عددًا صحيحًا غير سالب ويأمر Terraform بإنشاء ذلك العدد من النسخ المتطابقة (أو شبه المتطابقة) من المورد. كل نسخة تُعنوَن بـresource_type.resource_name[index]، مع توفر count.index داخل الكتلة للتمييز بين النسخ.

# إنشاء ثلاث نسخ EC2 متطابقة في طبقة الويب resource "aws_instance" "web" { count = var.web_instance_count # مثلًا: 3 ami = data.aws_ami.ubuntu.id instance_type = "t3.medium" subnet_id = var.subnet_ids[count.index % length(var.subnet_ids)] tags = { Name = "web-${count.index + 1}" Role = "web" } } # الإشارة إلى نسخة محددة أو كل النسخ: # aws_instance.web[0].id -- النسخة الأولى # aws_instance.web[*].id -- splat: قائمة بجميع المعرفات # aws_instance.web[count.index] -- داخل الكتلة نفسها output "web_instance_ids" { value = aws_instance.web[*].id }
خطأ إنتاجي — count وثبات الفهرس: عند استخدام count، تعرّف Terraform كل نسخة بفهرسها الرقمي. إذا أزلت عنصرًا من منتصف قائمة (مثلًا: كان لديك 5 نسخ وأزلت الفهرس 2)، فإن Terraform يُعيد فهرسة كل ما فوق العنصر المحذوف. يؤدي هذا إلى تدمير وإعادة إنشاء النسخ 2 و3 و4 — حتى لو أردت حذف نسخة واحدة فقط. بالنسبة للموارد التي تهم هويتها (EC2 وRDS ومستخدمو IAM)، استخدم for_each بمجموعة أو خريطة حتى يكون لكل نسخة مفتاح نصي ثابت.

for_each: التكرار المحرَّك بالمفاتيح

يقبل for_each إما set(string) أو map(any) ويُنشئ نسخة واحدة من المورد لكل عنصر. كل نسخة تُعنوَن بـresource_type.resource_name["key"]. لأن النسخ مُفهرَسة بسلاسل نصية لا بأرقام، فإن إضافة أو إزالة عنصر واحد لا تؤثر إلا على تلك النسخة بالذات — جميع النسخ الأخرى تبقى دون تغيير. هذا هو الخيار الآمن للإنتاج لجميع أنماط الموارد المتعددة غير التافهة.

# النمط 1: for_each مع مجموعة سلاسل (حين تشترك جميع النسخ في الإعداد) variable "availability_zones" { type = set(string) default = ["us-east-1a", "us-east-1b", "us-east-1c"] } resource "aws_subnet" "private" { for_each = var.availability_zones vpc_id = aws_vpc.main.id availability_zone = each.key cidr_block = cidrsubnet(var.vpc_cidr, 4, index(tolist(var.availability_zones), each.key)) tags = { Name = "private-${each.key}" } } # النمط 2: for_each مع خريطة (حين تختلف إعدادات النسخ) variable "iam_users" { type = map(object({ path = string groups = list(string) })) default = { "svc-deployer" = { path = "/service/", groups = ["deployers"] } "svc-reader" = { path = "/service/", groups = ["readers"] } "ops-admin" = { path = "/ops/", groups = ["admins", "deployers"] } } } resource "aws_iam_user" "this" { for_each = var.iam_users name = each.key path = each.value.path tags = { ManagedBy = "terraform" } } resource "aws_iam_user_group_membership" "this" { for_each = var.iam_users user = aws_iam_user.this[each.key].name groups = each.value.groups } # الإشارة إلى نسخة محددة: # aws_iam_user.this["svc-deployer"].arn # aws_iam_user.this["ops-admin"].unique_id
تحويل قائمة إلى مجموعة لـ for_each: إذا وصل متغير على شكل list(string)، حوّله قبل تمريره لـfor_each: for_each = toset(var.my_list). للخرائط المشتقة من كائنات معقدة، استخدم تعبير for في محلي: local.user_map = { for u in var.users : u.name => u } ثم for_each = local.user_map. لا تمرر قائمة مباشرةً — ستظهر رسالة خطأ من Terraform.
count vs for_each index stability count (index) vs for_each (key) — حذف العنصر الأوسط count — غير مستقر (مفاتيح رقمية) قبل (حذف [1]) web[0] alice web[1] bob web[2] carol بعد web[0] alice ✓ محفوظة web[1] carol ✗ مُعاد إنشاؤها حذف bob يدمر carol ويعيد إنشاءها لأنها انتقلت من [2] إلى [1] خطير للموارد ذات الحالة (RDS وElasticSearch ومستخدمو IAM) for_each — مستقر (مفاتيح نصية) قبل (حذف "bob") web["alice"] web["bob"] web["carol"] بعد web["alice"] ✓ محفوظة web["bob"] ✗ محذوفة web["carol"] ✓ محفوظة bob وحدها محذوفة. alice وcarol لم تتأثرا إطلاقًا. آمن لجميع أنواع الموارد
حذف عنصر من المنتصف بـ count يسبب استبدالات متتالية؛ أما for_each فيستهدف المفتاح المحذوف فقط.

lifecycle: التحكم في الإنشاء والحذف والاستبدال

تجلس كتلة lifecycle داخل أي مورد وتتجاوز السلوك الافتراضي لـTerraform لأحداث دورة حياة ذلك المورد. لها أربعة وسائط: create_before_destroy وprevent_destroy وignore_changes وreplace_triggered_by. الحصول عليها صحيحًا هو الفارق بين نشر بدون توقف وحادثة في الثالثة صباحًا.

create_before_destroy

افتراضيًا، حين يجب على Terraform استبدال مورد (تغيير يفرض موردًا جديدًا — مثل تغيير AMI أو صورة قالب إطلاق)، فإنه يدمر المورد القديم أولًا ثم ينشئ الجديد. هذا يعني فجوة مؤقتة في الطاقة الاستيعابية. لأساطيل موازنة التحميل وشهادات TLS وأدوار IAM مع مرفقات السياسات، هذه الفجوة غير مقبولة. create_before_destroy = true يعكس الترتيب: ينشئ Terraform البديل ثم يدمر الأصلي بعد تأكيد البديل.

# تدوير AMI بدون توقف لقالب إطلاق Auto Scaling resource "aws_launch_template" "web" { name_prefix = "web-" image_id = var.ami_id instance_type = "m6i.large" lifecycle { create_before_destroy = true # حين يتغير image_id، ينشئ Terraform إصدار قالب إطلاق جديد # قبل تدمير القديم، بحيث يكون للـ ASG قالب صالح دائمًا. } } # استبدال شهادة ACM — يجب أن تتواجد الشهادة الجديدة قبل إزالة القديمة resource "aws_acm_certificate" "main" { domain_name = var.domain_name validation_method = "DNS" lifecycle { create_before_destroy = true } } # دور IAM — أنشئ الدور الجديد والسياسات قبل حذف القديم resource "aws_iam_role" "worker" { name_prefix = "worker-" assume_role_policy = data.aws_iam_policy_document.assume.json lifecycle { create_before_destroy = true } }
لماذا name_prefix لا name: حين يكون create_before_destroy = true، يجب أن يتواجد المورد الجديد مع القديم في آنٍ واحد. تشترط AWS أسماءً فريدة لمعظم الموارد. استخدم name_prefix بدلًا من name حتى تستطيع Terraform توليد لاحقة فريدة للمورد الجديد. مع name الثابت، تفشل خطوة الإنشاء لأن الاسم لا يزال محجوزًا من قِبل المورد القديم.

prevent_destroy

يجعل prevent_destroy = true Terraform يُخطئ — ويوقف التخطيط — إذا كان المخطط سيدمر ذلك المورد. هذا حارس أخير للموارد التي يجب ألا تُحذف عن طريق الخطأ: مجموعات RDS في الإنتاج، ودلاء S3 ذات بيانات امتثالية، ومفاتيح KMS، ونطاقات Elasticsearch. إنه ضمان على مستوى الكود لا على مستوى الصلاحيات — يمكن لمشغّل متعمد إزالة الكتلة وإعادة التشغيل. ادمجه مع سياسات موارد AWS ووحدات SCPs للدفاع المتعمق.

resource "aws_db_cluster" "main" { cluster_identifier = "prod-aurora-cluster" engine = "aurora-postgresql" engine_version = "15.3" master_username = var.db_user master_password = var.db_password deletion_protection = true # حارس على مستوى AWS (منفصل عن lifecycle) lifecycle { prevent_destroy = true # حارس Terraform: يُخطئ المخطط إذا خُطط للتدمير } } resource "aws_s3_bucket" "audit_logs" { bucket = "company-audit-logs-prod" lifecycle { prevent_destroy = true } }

ignore_changes

يأمر ignore_changes Terraform بالتوقف عن تتبع الانجراف على سمات محددة. حالة الاستخدام المعيارية هي حين يغيّر نظام خارجي (موسّع تلقائي، مشغّل بشري، أداة إدارة إعداد) سمةً ما بشكل مشروع بعد أن أنشأ Terraform المورد. بدون ignore_changes، سيرصد Terraform الانجراف في كل مخطط ويعكسه — مما يُفسد تغييرات النظام الخارجي. السمات الشائعة: desired_capacity في ASG، وami حين تُدار الصور بعملية خارجية، وtags حين تحقن سياسة علامات علامات تخصيص التكلفة خارج Terraform.

resource "aws_autoscaling_group" "web" { name = "web-asg" min_size = 2 max_size = 20 desired_capacity = 4 # القيمة الأولية؛ يديرها الموسّع بعد الإنشاء launch_template { id = aws_launch_template.web.id version = "$Latest" } lifecycle { # سياسات التوسع والإجراءات المجدولة تغير desired_capacity. # تجاهلها حتى لا يعكس Terraform قرارات الموسّع. ignore_changes = [desired_capacity] # تجاهل علامات تخصيص التكلفة المحقونة خارجيًا أيضًا # ignore_changes = [tags["CostCenter"], tags["BusinessUnit"]] } } # replace_triggered_by: فرض الاستبدال حين يتغير مورد مرتبط # (مثلًا: دوران نسخ EC2 حين يتغير قالب الإطلاق) resource "aws_autoscaling_group" "web_v2" { name = "web-v2-asg" min_size = 2 max_size = 20 desired_capacity = 4 launch_template { id = aws_launch_template.web.id version = "$Latest" } lifecycle { replace_triggered_by = [aws_launch_template.web] # حين يُستبدل aws_launch_template.web، يُستبدل ASG أيضًا. # ادمجه مع create_before_destroy لتدويرات بدون توقف. create_before_destroy = true } }
ممارسة احترافية — دمج وسائط lifecycle: لتدوير أسطول إنتاجي، ادمج الثلاثة: create_before_destroy = true (بدون توقف)، وreplace_triggered_by = [aws_launch_template.web] (تتالي تلقائي)، وignore_changes = [desired_capacity] (احترام الموسّع). هذا الثلاثي هو النمط المعتمد في فرق هندسة المنصة في Airbnb وLyft وGitHub. محاولة إدارة تدوير الأسطول بدون هذه الوسائط تقود إلى دورات taint يدوية ونوافذ توقف مجدولة.

الموارد الشرطية بـ count

نمط شائع هو count = var.condition ? 1 : 0 لإنشاء مورد بشكل شرطي. هذه الطريقة الوحيدة الاصطلاحية في Terraform للتعبير عن "ربما أنشئ هذا المورد". حين يكون count صفرًا، لا تدير Terraform أي نسخ والمورد غائب فعليًا. عند الإشارة لمورد شرطي كهذا من مورد آخر، استخدم one(resource_type.name[*].attribute) لاستخراج القيمة بأمان (يُعيد null حين count صفر بدلًا من الإخطاء).

# إنشاء bastion host شرطيًا فقط في بيئات غير الإنتاج resource "aws_instance" "bastion" { count = var.environment != "production" ? 1 : 0 ami = data.aws_ami.ubuntu.id instance_type = "t3.nano" subnet_id = var.public_subnet_id tags = { Name = "bastion-${var.environment}", Role = "bastion" } } # إنشاء CloudWatch alarm شرطيًا فقط حين يُوفَّر ARN للـ SNS resource "aws_cloudwatch_metric_alarm" "cpu_high" { count = var.alarm_sns_arn != "" ? 1 : 0 alarm_name = "cpu-high-${var.environment}" comparison_operator = "GreaterThanThreshold" evaluation_periods = 3 metric_name = "CPUUtilization" namespace = "AWS/EC2" period = 60 statistic = "Average" threshold = 80 alarm_actions = [var.alarm_sns_arn] } # مرجع آمن باستخدام one(): output "bastion_ip" { value = one(aws_instance.bastion[*].public_ip) # يُعيد null إذا لم يكن bastion موجودًا — بدون خطأ }
for_each مع قيم مجهولة: تشترط Terraform أن تكون المفاتيح المستخدمة في خريطة أو مجموعة for_each معروفة وقت التخطيط. إذا جاء المفتاح من سمة مورد لم يُنشأ بعد (مثل معرف مُسنَّد ديناميكيًا)، فإن Terraform تُخطئ بـ "The set of keys cannot be determined until apply." الحل هو استخدام قيم معروفة كمفاتيح — أسماء وسبائك ومعرفات ثابتة — لا معرفات محسوبة. إذا كان لا بد من استخدام قيمة محسوبة، ارجع لـcount مع length() متقبلًا مقايضة ثبات الفهرس.