يتوسع نظام أنواع Terraform وتعليماته الوصفية بأناقة من عرض توضيحي بمورديْن إلى قاعدة كود هندسة منصة تدير آلاف الموارد. يمثل بنيتان محوريتان هذا التوسع: الكتل الديناميكية، التي تتيح توليد إعداد متداخل متكرر من مجموعة بدلًا من النسخ واللصق، والأنواع المركبة (object وmap وlist من الكائنات وغيرها)، التي تتيح للمستدعين التعبير عن مدخلات غنية ومنظمة في متغير واحد بدلًا من عشرات السلاسل المسطحة. ادمج هذه الأدوات مع دوال try وcan في Terraform للوصول الآمن إلى السمات، وستتمكن من كتابة وحدات مرنة حقًا دون أن تكون هشة.
لماذا توجد الكتل الديناميكية
تحتوي كثير من موارد AWS على كتل متداخلة تتكرر: قواعد ingress في مجموعة أمان، كتل lifecycle_rule في حاوية S3، كتل header في توزيع CloudFront. بدون الكتل الديناميكية يجب كتابة كل منها يدويًا، ما يعني أن العدد مشفّر في القالب. حين يحتاج المستدعي إلى قاعدة واحدة أكثر أو أقل، يجب تفريع الوحدة. تحل الكتل الديناميكية هذا بالتكرار على مجموعة وإصدار كتلة متداخلة واحدة لكل عنصر — المبدأ نفسه كحلقة for، لكن مُعبَّرًا عنه بشكل تصريحي داخل كتلة المورد.
تشريح الكتلة الديناميكية: التسمية على dynamic يجب أن تطابق اسم الكتلة المتداخلة التي تريد توليدها (ingress أو lifecycle_rule إلخ). داخل content، استخدم <التسمية>.value.<السمة> للوصول إلى العنصر الحالي. استخدم <التسمية>.key للمفتاح (أو الفهرس في القائمة). وسيطة iterator تُعيد تسمية متغير الحلقة حين يتعارض الاسم الافتراضي مع سمة موجودة.
قيود الأنواع المركبة
الأنواع البدائية في Terraform (string وnumber وbool) ليست معبّرة بما يكفي لواجهات الوحدات الحقيقية. تستخدم وحدات الإنتاج الأنواع المركبة لفرض البنية:
object({...}) — مجموعة ثابتة من السمات المسماة، لكل منها نوعها. استخدمه للإعداد المنظم حيث كل سمة ذات معنى ومسماة.
map(T) — جدول بحث بمفاتيح سلسلة نصية لقيم من النوع T. استخدمه لخرائط العلامات، والإعدادات لكل خدمة بمفتاح اسم الخدمة، أو الإعدادات الخاصة بالبيئة.
list(object({...})) — مجموعة مرتبة من العناصر المنظمة. النوع الأمثل للمدخلات التي تقود الكتل الديناميكية.
optional(T, default) — (متاح من Terraform 1.3) يضع علامة على سمة الكائن كاختيارية مع قيمة افتراضية، مما يجعل إعداد المستدعي أقل إطنابًا بكثير.
# متغير وحدة واقعي باستخدام الأنواع المركبة:
variable "services" {
description = "خريطة الخدمات الصغيرة للنشر. المفتاح = اسم الخدمة."
type = map(object({
image = string
cpu = number
memory = number
port = number
desired_count = number
health_path = optional(string, "/health")
env_vars = optional(map(string), {})
secrets = optional(list(string), [])
}))
}
# terraform.tfvars الخاص بالمستدعي:
# services = {
# api = {
# image = "012345678901.dkr.ecr.us-east-1.amazonaws.com/api:v2.1.0"
# cpu = 512
# memory = 1024
# port = 8080
# desired_count = 3
# health_path = "/api/health"
# env_vars = { LOG_LEVEL = "info", REGION = "us-east-1" }
# secrets = ["arn:aws:secretsmanager:us-east-1:...:db-password"]
# }
# worker = {
# image = "012345678901.dkr.ecr.us-east-1.amazonaws.com/worker:v1.4.0"
# cpu = 256
# memory = 512
# port = 9090
# desired_count = 2
# }
# }
# استهلاك الخريطة باستخدام for_each:
resource "aws_ecs_service" "services" {
for_each = var.services
name = each.key
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.services[each.key].arn
desired_count = each.value.desired_count
}
resource "aws_ecs_task_definition" "services" {
for_each = var.services
family = each.key
cpu = each.value.cpu
memory = each.value.memory
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
container_definitions = jsonencode([{
name = each.key
image = each.value.image
portMappings = [{ containerPort = each.value.port }]
environment = [for k, v in each.value.env_vars : { name = k, value = v }]
secrets = [for arn in each.value.secrets : { name = basename(arn), valueFrom = arn }]
}])
}
ممارسة احترافية — استخدم map(object) بدلًا من list(object) للموارد ذات المفاتيح الفريدة: عند تشغيل for_each على مورد، تمنح الخريطة كل نسخة مفتاحًا ثابتًا وذا معنى (اسم الخدمة، اسم القاعدة). القائمة تمنح فهارس موضعية — أعِد ترتيب القائمة وسيريد Terraform هدم كل شيء وإعادة إنشائه. في Stripe وGitHub، تُفضّل وحدات المنصة الداخلية عالميًا map(object) كنوع أساسي للمتغيرات التي تقود الموارد تحديدًا لتجنب مفاجآت مخطط الهدم عند إعادة الترتيب.
الكتل الديناميكية داخل كتل ديناميكية
الكتل الديناميكية المتداخلة ضرورية أحيانًا — على سبيل المثال، قواعد دورة حياة S3 تحتوي على كتلة transition متداخلة. التداخل يعمل، لكن الأفضل عمليًا أن يكون على مستوى واحد فقط؛ التداخل الأعمق يجعل القالب أصعب قراءةً من المشكلة التي يحلها.
خطأ إنتاجي — الكتل الديناميكية والمجموعة الفارغة: إذا استقبل for_each قائمة أو خريطة فارغة، لا تُصدر كتل — وهذا صحيح. لكن إذا كانت الكتلة المتداخلة مطلوبة من قِبل المزود (بعض الموارد تتطلب قاعدة دخول واحدة على الأقل، وإلا رفض AWS الطلب)، يجب التحقق مسبقًا من أن المجموعة غير فارغة. وحدة تنتج مخططًا Terraform صالحًا لكنها تتسبب في خطأ AWS API وقت التطبيق تمثل أصعب أنواع الأخطاء في تشخيص CI. أضف كتلة validation على المتغير تؤكد length(var.ingress_rules) > 0 حين يتطلب المورد ذلك.
الوصول الآمن للسمات مع try وcan
عند العمل مع أنواع مركبة من مصادر بيانات خارجية، أو حالة بعيدة، أو سمات كائن اختيارية، قد يفشل الوصول إلى السمة وقت التخطيط إذا كان المفتاح مفقودًا أو القيمة null. توفر Terraform دالتين للوصول الآمن:
try(expr1, expr2, ...) — تقيّم التعبيرات من اليسار لليمين وتعيد أول واحد لا ينتج خطأ. فكّر فيها كـtry/catch لتعبيرات HCL.
can(expr) — تقيّم تعبيرًا وتعيد true إذا نجح دون خطأ، وfalse خلاف ذلك. مصممة للاستخدام داخل كتل validation.
# القراءة من حالة بعيدة حيث قد يوجد مفتاح أو لا يوجد:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "company-tfstate"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}
locals {
# الرجوع بأمان إلى معرف VPC افتراضي إذا كانت الحالة البعيدة
# لا تملك المخرج المتوقع (مثلاً أثناء الإعداد الأوّلي):
vpc_id = try(
data.terraform_remote_state.network.outputs.vpc_id,
var.fallback_vpc_id
)
# قراءة آمنة لمفتاح اختياري متداخل من متغير map.
# قد لا يمتلك var.config دائمًا المفتاح الفرعي "monitoring":
monitoring_enabled = try(var.config.monitoring.enabled, false)
retention_days = try(var.config.monitoring.retention_days, 7)
}
# -------------------------------------------------------
# can() في كتل validation — النمط الاصطلاحي:
variable "kms_key_arn" {
type = string
description = "ARN مفتاح KMS للتشفير. يجب أن يكون ARN AWS صالحًا."
validation {
condition = can(regex("^arn:aws:kms:", var.kms_key_arn))
error_message = "يجب أن يبدأ kms_key_arn بـ arn:aws:kms:."
}
}
variable "subnet_cidr" {
type = string
description = "كتلة CIDR للشبكة الفرعية."
validation {
# can() يلف cidrhost() — إذا كان CIDR مشوهًا، يرمي cidrhost() خطأ؛
# can() يلتقطه ويعيد false، مما يُطلق error_message.
condition = can(cidrhost(var.subnet_cidr, 0))
error_message = "يجب أن يكون subnet_cidr بصياغة CIDR صالحة."
}
}
# -------------------------------------------------------
# try() للبحث الآمن في الخريطة — تجنب lookup() مع افتراضي:
locals {
# فضّل try() على lookup() للوصول المتداخل المركب:
db_port = try(var.services["database"].port, 5432)
# قراءة سمة قد لا توجد في إصدارات مزود قديمة:
arn = try(aws_lb.main.arn, aws_alb.main.arn)
}
الكتل الديناميكية تُكرّر على مجموعة لإصدار كتل إعداد متداخلة؛ try() وcan() تحمي من أخطاء الوصول إلى سمات اختيارية أو خارجية.
قيود الأنواع: object مقابل map مقابل any
اختيار قيد النوع الصحيح هو قرار تصميم وحدة له عواقب حقيقية على قابلية الاستخدام والأمان:
استخدم object({...}) حين مجموعة السمات ثابتة ومسماة — تعرف بالضبط ما هي المفاتيح ولكل منها نوع محدد. يمنح المستدعين سمات مسماة مع فحص النوع والإكمال التلقائي.
استخدم map(T) حين المفاتيح يحددها المستدعي وعشوائية (خريطة علامات، خريطة علامات ميزات بمفتاح الاسم، إعداد لكل منطقة بمفتاح كودها).
استخدم list(object({...})) حين يهم الترتيب أو يتوقع المورد تسلسلًا محددًا (مثلاً قواعد WAF تُقيَّم بالترتيب).
تجنب type = any في واجهات الوحدات العامة. يجتاز فحص عقد المخطط لكنه يؤجل جميع أخطاء النوع إلى وقت التطبيق، وهو أكثر تكلفةً في التشخيص من وقت التخطيط.
# الجمع بين المكونات — وحدة قواعد مستمعي ALB جاهزة للإنتاج:
variable "listener_rules" {
description = "قائمة مرتبة من قواعد مستمع ALB. العناصر الأولى لها أولوية أقل."
type = list(object({
priority = number
conditions = list(object({
field = string # "path-pattern" أو "host-header"
values = list(string)
}))
target_group_arn = string
}))
}
resource "aws_lb_listener_rule" "rules" {
count = length(var.listener_rules)
listener_arn = var.listener_arn
priority = var.listener_rules[count.index].priority
dynamic "condition" {
for_each = var.listener_rules[count.index].conditions
content {
dynamic "path_pattern" {
for_each = condition.value.field == "path-pattern" ? [condition.value] : []
content {
values = path_pattern.value.values
}
}
dynamic "host_header" {
for_each = condition.value.field == "host-header" ? [condition.value] : []
content {
values = host_header.value.values
}
}
}
}
action {
type = "forward"
target_group_arn = var.listener_rules[count.index].target_group_arn
}
}
رؤية جوهرية — حيلة القائمة ذات العنصر الواحد: حين يجب أن تظهر كتلة مرةً واحدة بالضبط أو لا تظهر (كتلة اختيارية واحدة)، شغّلها باستخدام for_each = condition ? [1] : []. القائمة الفارغة لا تُصدر كتلة؛ القائمة ذات العنصر الواحد تُصدر كتلة واحدة بالضبط. هذا أسلوب Terraform الاصطلاحي وأنظف بكثير من تكرار تعريفات الموارد. هذا النمط مرئي في مثال dynamic "expiration" أعلاه وفي عمليًا كل وحدة ناضجة في سجل Terraform.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية