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

تصميم الوحدات المتقدم

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

تصميم الوحدات المتقدم

وحدات Terraform هي الوحدة الأساسية لإعادة الاستخدام والتجريد في أي قاعدة كود IaC كبيرة. المبتدئ يكتب وحدة تغلّف بعض الموارد ويعتبرها منجزة. أما المهندس المتمرس في شركة كـ Stripe أو Airbnb فيصمم الوحدات كـ APIs — بواجهات مدروسة، وعقود ثابتة، وإشارات ميزات اختيارية، وحدود تركيب محددة. هذا الدرس يعلمك المستوى الثاني.

الوحدة كـ API منشور

كل وحدة تشاركها عبر الفرق يجب أن تُعامَل كمكتبة ذات إصدارات. متغيرات الإدخال هي توقيع الدالة؛ والمخرجات هي قيم الإرجاع؛ وREADME هو التوثيق. يعتمد المستدعون على الواجهة لا على التفاصيل الداخلية. هذا يعني أنك تستطيع إعادة هيكلة داخل الوحدة — استبدال aws_lb بـ aws_alb، تغيير نمط التسمية، إضافة التشفير — دون كسر كل مستهلك، طالما تحافظ على عقود المتغيرات والمخرجات الخارجية.

الانضباط حول هذه الواجهة يهم بشكل هائل على نطاق واسع. في Google، تخضع وحدات Terraform الداخلية المنشورة في السجل الداخلي لنفس قواعد الإصدار الدلالي كأي مكتبة أخرى: التغييرات الجذرية تستلزم رفع الإصدار الرئيسي، والمستدعون يُثبّتون النطاق، والترقية هجرة مقصودة وليست أثراً جانبياً عرضياً لتغيير أحد الزملاء.

التركيب: وحدات تستدعي وحدات

أقوى نمط معماري في Terraform المتقدم هو التركيب — تجميع وحدات صغيرة أحادية الغرض في وحدات أكبر تمثل شريحة قابلة للنشر من البنية التحتية. فكر في الأمر كـ LEGO: وحدة vpc، ووحدة rds، ووحدة ecs-service تبقى كل منها نحيلة ومركزة. وحدة app-stack عالية المستوى تجمعها في وحدة نشر متماسكة. والوحدة الجذرية (دليل بيئتك) تجمع حزمة أو أكثر.

Terraform module composition layers Root Module (environments/prod) app-stack module vpc subnets, routes, IGW rds instance, SG, subnet group ecs-service task def, service, ALB vpc_id db_endpoint Primitive Resources aws_vpc · aws_subnet · aws_db_instance · aws_ecs_service · aws_lb · aws_security_group
طبقات التركيب: تُغلَّف الموارد البدائية في وحدات مركزة، تُجمَّع في app-stack، تستهلكها الوحدة الجذرية.

القاعدة الأساسية: كل طبقة تعرف فقط الطبقة التي مباشرة تحتها، لا أعمق من ذلك. الوحدة الجذرية تُنشئ app-stack. الـ app-stack تُنشئ vpc وrds وecs-service. تلك الوحدات الطرفية تدير موارد AWS المباشرة. لا طبقة تمتد إلى شقيقة أو تتخطى مستوى. هذا يجعل نطاق التأثير ضيقاً والإعادة الهيكلية آمنة.

تصميم الواجهة: المتغيرات

واجهة متغيرات الوحدة هي المكان الذي تقع فيه معظم أخطاء التصميم. اتبع هذه القواعد لبناء واجهات تصمد مع الزمن.

استخدم objects للتكوين المتجمع، لا متغيراً مسطحاً لكل حقل. بدلاً من خمسة عشر متغيراً منفصلاً مثل var.enable_deletion_protection وvar.backup_retention_days وغيرها، جمّع الإعدادات المترابطة منطقياً في كائن منظوم. هذا يبقي الوحدة المستدعية نظيفة ويسمح لك بإضافة حقول اختيارية جديدة دون تغيير كل موقع استدعاء قائم.

صرّح بأنواع صريحة. متغير من نوع string مقابل object({ ... }) يوثق نفسه ويكتشف الأخطاء في وقت التخطيط بدلاً من تمرير قيم خاطئة بصمت. استخدم optional() داخل أنواع الكائنات (متاح منذ Terraform 1.3) لتحديد الحقول التي لها قيم افتراضية.

# modules/rds/variables.tf variable "db_config" { description = "Database configuration object" type = object({ instance_class = string engine_version = string allocated_storage_gb = number backup_retention_days = optional(number, 7) deletion_protection = optional(bool, true) multi_az = optional(bool, false) }) } variable "name" { description = "Logical name — used as a prefix for all resource names" type = string validation { condition = can(regex("^[a-z][a-z0-9-]{2,30}$", var.name)) error_message = "Name must be lowercase, 3-31 chars, starting with a letter." } } variable "subnet_ids" { description = "List of subnet IDs for the DB subnet group" type = list(string) } variable "tags" { description = "Map of tags merged onto all resources" type = map(string) default = {} }

الميزات الاختيارية عبر إشارات الميزات

الوحدات الحقيقية تحتاج إلى خدمة حالات استخدام متعددة دون أن تصبح وحدة مختلفة لكل حالة. النمط الاحترافي هو تقييد الموارد الفرعية الاختيارية خلف متغيرات منطقية أو كائنية. عندما تكون الإشارة في قيمتها الصفرية (false أو null)، يكون عدد الموارد صفراً — لا وجود لها. عند التمكين، تُنشأ. وسيطا count وfor_each في Terraform يجعلان هذا ممكناً.

# modules/rds/main.tf # Core resource — always created resource "aws_db_instance" "this" { identifier = var.name engine = "mysql" engine_version = var.db_config.engine_version instance_class = var.db_config.instance_class allocated_storage = var.db_config.allocated_storage_gb db_subnet_group_name = aws_db_subnet_group.this.name backup_retention_period = var.db_config.backup_retention_days deletion_protection = var.db_config.deletion_protection multi_az = var.db_config.multi_az tags = var.tags } # Optional read replica — only created when replica_count > 0 variable "replica_count" { type = number default = 0 } resource "aws_db_instance" "replica" { count = var.replica_count identifier = "${var.name}-replica-${count.index}" replicate_source_db = aws_db_instance.this.id instance_class = var.db_config.instance_class tags = var.tags } # Optional CloudWatch alarm — enabled via object flag (null = disabled) variable "alarm_config" { description = "Set to enable a CloudWatch CPU alarm. Null disables it." type = object({ threshold_percent = number sns_topic_arn = string }) default = null } resource "aws_cloudwatch_metric_alarm" "cpu" { count = var.alarm_config != null ? 1 : 0 alarm_name = "${var.name}-high-cpu" comparison_operator = "GreaterThanThreshold" evaluation_periods = 3 metric_name = "CPUUtilization" namespace = "AWS/RDS" period = 60 statistic = "Average" threshold = var.alarm_config[0].threshold_percent alarm_actions = [var.alarm_config[0].sns_topic_arn] dimensions = { DBInstanceIdentifier = aws_db_instance.this.identifier } }
فضّل null على false للكائنات الاختيارية. القيمة المنطقية enable_alarm = false لا تزال تجبر المستدعي على توفير جميع حقول تكوين التنبيه (ARN الموضوع، العتبة). أما alarm_config = null الافتراضية فتعني أن المستدعي يوفر صفراً من الحقول إلا إذا اختار الاشتراك. هذا يجعل استدعاء الوحدة أنظف بكثير في الحالة الشائعة.

المخرجات: العقد مع المستدعين

المخرجات ليست لاحقة بالفكر. إنها الواجهة التي من خلالها تستهلك الوحدات الأم والوحدات الجذرية نتائج وحدتك. صدّر كل ما قد يحتاجه المستدعي بشكل معقول: معرّفات الموارد، ARNs، أسماء DNS، معرّفات مجموعات الأمان. لا تكشف تفاصيل التنفيذ الداخلية (مثل locals الوسيطة أو الأسماء المحسوبة التي قد تتغير). ضع علامة على المخرجات الحساسة بـ sensitive = true لكي تُحجب القيم من مخرج التخطيط والسجلات.

استقرار المخرجات يُعدّ مصدر قلق لتغييرات جذرية. إذا أزلت أو أعدت تسمية مخرج يشير إليه المستدعون — حتى دون تغيير أي مورد — سيفشل تخطيطهم بخطأ مرجعي. عامل أسماء المخرجات بنفس الدقة التي تعامل بها سطح واجهة برمجية عامة. أهمل ثم أضف مسمى بديلاً قبل الحذف.

الأنماط المضادة التي يجب إزالتها

تظهر هذه الأنماط باستمرار في قواعد كود Terraform في الشركات التي نمت بسرعة دون حوكمة. تعلم كيف تتعرف عليها وتصلحها.

  • وحدة "الإله": وحدة واحدة تُوفر كل شيء — الشبكات، والحوسبة، وقاعدة البيانات، ونظام أسماء النطاقات، وإدارة الهوية والوصول — لبيئة كاملة. لديها أكثر من 200 متغير إدخال، تستغرق 45 دقيقة للتخطيط، ومن المستحيل تغييرها بأمان. الحل: التحليل إلى وحدات طرفية ذات مسؤولية واحدة تجمعها جذر نحيل.
  • القيم المُضمَّنة داخل الوحدات: وحدة تفترض us-east-1، أو معرف AMI محدد، أو معرف حساب محدد. تعمل مع صاحبها وتفشل مع كل فريق آخر. الحل: اجعل القيمة متغيراً؛ يوفر المستدعي السياق.
  • تمرير تكوينات الموفر الكاملة إلى الوحدات: وحدة تأخذ var.aws_access_key وتضبط موفرها الخاص. هذا يكسر تسمية الموفر بالاسم المستعار، ويجعل أنماط assume-role مستحيلة، ويكسر نموذج توريث الموفر القياسي في Terraform. الحل: يجب ألا تضبط الوحدات الموفرين أبداً — هذه مهمة الوحدة الجذرية.
  • اعتبار terraform.tfvars الواجهة الوحيدة: ملفات .tfvars مسطحة ضخمة بمئات المتغيرات السائبة دون تطبيق للنوع. الحل: متغيرات كائنية منظومة مع كتل validation.
لا تستخدم count على وحدة تُنشئ موارد متعددة مميزة إذا كان الترتيب مهماً. إذا استخدمت count = length(var.envs) على وحدة وأضفت لاحقاً بيئة جديدة في الموضع 0، سيخطط Terraform لتدمير وإعادة إنشاء كل شيء من الفهرس 0 فصاعداً. استخدم for_each مع خريطة أو مجموعة من السلاسل بدلاً من ذلك — الموارد مفهرسة بمفتاح الخريطة لا بالموضع، لذا إضافة مفتاح جديد تُنشئ فقط المورد الجديد.

تجميع الأمور معاً: مثال استدعاء

إليك كيف تُنشئ وحدة جذرية مثيلاً من وحدة rds المصممة أعلاه — نظيف، صريح، وسهل المراجعة في طلب السحب:

# environments/prod/main.tf module "app_db" { source = "git::https://github.com/acme/tf-modules.git//rds?ref=v2.4.0" name = "app-prod" subnet_ids = module.vpc.private_subnet_ids tags = local.common_tags db_config = { instance_class = "db.r7g.xlarge" engine_version = "8.0.36" allocated_storage_gb = 200 backup_retention_days = 14 deletion_protection = true multi_az = true } replica_count = 2 alarm_config = { threshold_percent = 80 sns_topic_arn = module.alerting.pagerduty_sns_arn } }

ثبّت على وسم Git محدد (لا على فرع) في مصادر وحدات الإنتاج. مرجع الفرع يعني أن تغيير زميل غير مراجَع يمكن أن يغير بصمت ما يوفره terraform apply التالي. الوسوم ثابتة؛ الفروع ليست كذلك.