Terraform المتقدم وأنماط البنية ككود

الكتل الديناميكية والأنواع المركبة

18 دقيقة الدرس 4 من 28

الكتل الديناميكية والأنواع المركبة

يتوسع نظام أنواع Terraform وتعليماته الوصفية بأناقة من عرض توضيحي بمورديْن إلى قاعدة كود هندسة منصة تدير آلاف الموارد. يمثل بنيتان محوريتان هذا التوسع: الكتل الديناميكية، التي تتيح توليد إعداد متداخل متكرر من مجموعة بدلًا من النسخ واللصق، والأنواع المركبة (object وmap وlist من الكائنات وغيرها)، التي تتيح للمستدعين التعبير عن مدخلات غنية ومنظمة في متغير واحد بدلًا من عشرات السلاسل المسطحة. ادمج هذه الأدوات مع دوال try وcan في Terraform للوصول الآمن إلى السمات، وستتمكن من كتابة وحدات مرنة حقًا دون أن تكون هشة.

لماذا توجد الكتل الديناميكية

تحتوي كثير من موارد AWS على كتل متداخلة تتكرر: قواعد ingress في مجموعة أمان، كتل lifecycle_rule في حاوية S3، كتل header في توزيع CloudFront. بدون الكتل الديناميكية يجب كتابة كل منها يدويًا، ما يعني أن العدد مشفّر في القالب. حين يحتاج المستدعي إلى قاعدة واحدة أكثر أو أقل، يجب تفريع الوحدة. تحل الكتل الديناميكية هذا بالتكرار على مجموعة وإصدار كتلة متداخلة واحدة لكل عنصر — المبدأ نفسه كحلقة for، لكن مُعبَّرًا عنه بشكل تصريحي داخل كتلة المورد.

# بدون كتل ديناميكية — هش ومنسوخ: resource "aws_security_group" "web" { name = "web-sg" vpc_id = var.vpc_id ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # إضافة HTTPS لـIPv6 تتطلب تعديل الوحدة نفسها — ممارسة سيئة. } # ------------------------------------------------------- # مع الكتل الديناميكية — مدفوعة بمتغير: variable "ingress_rules" { description = "قائمة كائنات قواعد الدخول لمجموعة أمان الويب." type = list(object({ from_port = number to_port = number protocol = string cidr_blocks = list(string) description = optional(string, "") })) default = [ { from_port = 80, to_port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }, { from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }, ] } resource "aws_security_group" "web" { name = "web-sg" vpc_id = var.vpc_id dynamic "ingress" { for_each = var.ingress_rules content { from_port = ingress.value.from_port to_port = ingress.value.to_port protocol = ingress.value.protocol cidr_blocks = ingress.value.cidr_blocks description = ingress.value.description } } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
تشريح الكتلة الديناميكية: التسمية على 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 متداخلة. التداخل يعمل، لكن الأفضل عمليًا أن يكون على مستوى واحد فقط؛ التداخل الأعمق يجعل القالب أصعب قراءةً من المشكلة التي يحلها.

variable "lifecycle_rules" { type = list(object({ id = string enabled = bool prefix = optional(string, "") expiration = optional(number, null) transitions = optional(list(object({ days = number storage_class = string })), []) })) default = [] } resource "aws_s3_bucket_lifecycle_configuration" "main" { bucket = aws_s3_bucket.main.id dynamic "rule" { for_each = var.lifecycle_rules content { id = rule.value.id status = rule.value.enabled ? "Enabled" : "Disabled" filter { prefix = rule.value.prefix } dynamic "transition" { for_each = rule.value.transitions content { days = transition.value.days storage_class = transition.value.storage_class } } dynamic "expiration" { for_each = rule.value.expiration != null ? [rule.value.expiration] : [] content { days = expiration.value } } } } } # مثال استدعاء — صفر تكرار لثلاث قواعد متدرجة: # lifecycle_rules = [ # { # id = "move-to-ia" # enabled = true # prefix = "logs/" # transitions = [ # { days = 30, storage_class = "STANDARD_IA" }, # { days = 90, storage_class = "GLACIER" }, # ] # expiration = 365 # }, # ]
خطأ إنتاجي — الكتل الديناميكية والمجموعة الفارغة: إذا استقبل 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) }
Dynamic Block Evaluation Flow Dynamic Block Evaluation: Collection → Nested Blocks → Resource var.ingress_rules list(object({...})) { port=80, cidr=["0.0.0.0/0"] } { port=443, cidr=["0.0.0.0/0"] } dynamic "ingress" for_each = var.ingress_rules content { from_port = ingress.value.port } الكتل المتداخلة المُصدَرة ingress { from_port=80 protocol=tcp } ingress { from_port=443 protocol=tcp } try() — تقييم آمن للتعبيرات try(expr1, expr2, fallback) يعيد أول تعبير لا يُنتج خطأ expr1 يفشل (مفتاح مفقود) → جرّب التعبير التالي expr2 ينجح → أعِد قيمته الاستخدام: قراءة حالة بعيدة، مفاتيح اختيارية متداخلة can() — فحص أمان منطقي can(expr) → true | false يعيد true إذا نجح التعبير بلا خطأ can(cidrhost(var.cidr, 0)) → هل صيغة CIDR صالحة؟ can(regex("^arn:", v)) → هل صيغة ARN صالحة؟ الاستخدام: كتل validation، محليات شرطية
الكتل الديناميكية تُكرّر على مجموعة لإصدار كتل إعداد متداخلة؛ 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.