أساسيات Terraform

المتغيرات والمخرجات والمحلية

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

المتغيرات والمخرجات والمحلية

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

متغيرات المدخلات

متغير المدخل هو معامل مكتوب بنوع بيانات، مع تحقق اختياري، تقبله الوحدة أو الإعداد الجذري من الخارج. أعلِن عنها في variables.tf (اتفاقية لا إلزام). لكل متغير type، وdefault اختياري، وdescription للتوثيق التلقائي، وعلامة sensitive اختيارية. حذف default يجعل المتغير إلزاميًا — سيرفض Terraform التخطيط دون تزويده بقيمة.

# variables.tf variable "environment" { description = "بيئة النشر: dev أو staging أو production." type = string validation { condition = contains(["dev", "staging", "production"], var.environment) error_message = "يجب أن تكون البيئة إحدى القيم: dev أو staging أو production." } } variable "instance_type" { description = "نوع نسخة EC2 لطبقة الويب." type = string default = "t3.micro" } variable "replica_count" { description = "عدد نسخ طبقة الويب (1-10)." type = number default = 2 validation { condition = var.replica_count >= 1 && var.replica_count <= 10 error_message = "يجب أن يكون replica_count بين 1 و10 شاملًا." } } variable "allowed_cidr_blocks" { description = "قائمة كتل CIDR المسموح لها بالدخول على المنفذ 443." type = list(string) default = ["10.0.0.0/8"] } variable "tags" { description = "خريطة العلامات المُطبَّقة على كل مورد في هذه الوحدة." type = map(string) default = {} } variable "db_password" { description = "كلمة مرور RDS الرئيسية — أمدّ بها عبر TF_VAR أو خلفية الأسرار." type = string sensitive = true # القيمة مُخفاة في مخرج plan وعرض الحالة }
نظام الأنواع: يدعم Terraform أنواع string وnumber وbool وlist(T) وset(T) وmap(T) وobject({...}) وtuple([...]). أعلِن دائمًا عن أنواع صريحة. عند استخدام type = any تخسر كل التحقق والإكمال التلقائي — لا يُقبل إلا في وحدات غلاف رفيعة تمرّر بيانات مبهمة عمدًا.

تزويد القيم: ملفات tfvars

تتدفق القيم إلى المتغيرات عبر آليات متعددة بأولوية متصاعدة (الأحدث يتجاوز السابق):

  1. القيم الافتراضية في إعلان المتغير
  2. ملف terraform.tfvars (يُحمَّل تلقائيًا من الدليل العامل)
  3. ملفات *.auto.tfvars (تُحمَّل تلقائيًا أبجديًا)
  4. أعلام -var-file=<مسار> الممررة لـterraform plan أو apply
  5. أعلام -var="مفتاح=قيمة"
  6. متغيرات بيئة مسبوقة بـTF_VAR_ مثل TF_VAR_db_password

النمط المعتمد في كبرى شركات التقنية هو ملف .tfvars واحد لكل بيئة، مخزون في المستودع ومختار عبر خط أنابيب CI:

# envs/production.tfvars environment = "production" instance_type = "m6i.2xlarge" replica_count = 6 allowed_cidr_blocks = ["10.0.0.0/8", "192.168.0.0/16"] tags = { team = "platform" cost-center = "infra-prod" managed-by = "terraform" } # db_password غير موجودة هنا — يحقنها نظام CI: # export TF_VAR_db_password="$(vault kv get -field=password secret/rds/prod)" # --- # envs/dev.tfvars environment = "dev" instance_type = "t3.small" replica_count = 1 allowed_cidr_blocks = ["10.10.0.0/16"] tags = { team = "platform" cost-center = "infra-dev" managed-by = "terraform" } # استدعاء CI: # terraform plan -var-file=envs/production.tfvars
خطأ إنتاجي — الأسرار في tfvars: لا تضع كلمات مرور أو مفاتيح API أو رموز في أي ملف .tfvars حتى لو كان المستودع خاصًا. تخزّن ملفات الحالة هذه القيم بنص صريح، وتبقى ملفات .tfvars في تاريخ git. النمط الصحيح: المتغيرات الحساسة لا تملك قيمة افتراضية، وخط أنابيب CI يحقنها عبر متغيرات بيئة TF_VAR_ مسحوبة من مدير أسرار (Vault أو AWS Secrets Manager أو GitHub Actions secrets). ادمج هذا مع تشفير الحالة البعيدة.

القيم المحلية

القيمة المحلية (مُعلَنة في كتلة locals) هي تعبير يُحسب مرة واحدة ويُرجَع إليه عبر الوحدة بالبادئة local.. توجد المحليات لسبب واحد: عدم التكرار (DRY). إذا حسبت "${var.environment}-${var.project_name}" في اثني عشر حقل اسم مورد، فخطأ مطبعي واحد يكسر اتفاقية التسمية بصمت. احسبها مرة واحدة كمحلي وارجع إليها في كل مكان.

# locals.tf locals { # بادئة الاسم القياسية المستخدمة في كل اسم مورد name_prefix = "${var.environment}-${var.project_name}" # علامات مدمجة: الافتراضيات على مستوى الوحدة + التجاوزات المقدَّمة من المستدعي common_tags = merge( { Environment = var.environment ManagedBy = "terraform" Module = "web-tier" }, var.tags ) # تقسيم CIDR — يُحسب مرة ويُرجع إليه في SGs وجداول التوجيه vpc_cidr = "10.${var.environment == "production" ? "0" : "1"}.0.0/16" # علامة منطقية: الإنتاج يحصل على متعدد-AZ، الباقي على نطاق واحد is_production = var.environment == "production" az_count = local.is_production ? 3 : 1 } # مثال على الاستخدام: resource "aws_instance" "web" { count = var.replica_count ami = data.aws_ami.amazon_linux.id instance_type = var.instance_type tags = merge(local.common_tags, { Name = "${local.name_prefix}-web-${count.index + 1}" Role = "web" }) }
ممارسة احترافية — المحليات كطبقة توثيق: استخدم المحليات لإعطاء أسماء للتعبيرات المعقدة حتى لو أُشير إليها مرة واحدة فقط. local.is_production تقرأ كالإنجليزية وتوثّق القصد بنفسها؛ أما var.environment == "production" المبعثرة في عشرين تعبيرًا شرطيًا فأصعب مراجعةً أثناء مراجعة أمنية. في أمازون وكلاودفلير، تستخدم فرق المنصة المحليات كسطح API لطبقة الإعداد: المستدعون يضبطون المتغيرات، تحسب الوحدة المحليات، والموارد ترجع إلى المحليات فقط.

قيم المخرجات

قيمة المخرج تنشر بيانات من وحدة إلى مستدعيها، أو من وحدة جذرية إلى المشغّل وأدوات الأتمتة. تخدم المخرجات ثلاثة أغراض ملموسة: تتيح للوحدات الأم الرجوع إلى موارد الوحدات الفرعية (وحدة VPC تُخرج معرف VPC الذي تحتاجه وحدة الحوسبة)، وتغذّي خطوات خط أنابيب CI (يقرأ خط الأنابيب اسم DNS لموازن التحميل لتشغيل اختبارات الدخان)، وتعرض معلومات مفيدة بعد terraform apply.

# outputs.tf output "web_instance_ids" { description = "قائمة معرفات نسخ EC2 في طبقة الويب." value = aws_instance.web[*].id } output "load_balancer_dns" { description = "اسم DNS العام لموازن التحميل." value = aws_lb.web.dns_name } output "vpc_id" { description = "معرف VPC الذي أنشأته هذه الوحدة." value = aws_vpc.main.id } output "db_endpoint" { description = "نقطة نهاية نسخة RDS (مضيف:منفذ)." value = "${aws_db_instance.main.address}:${aws_db_instance.main.port}" sensitive = true # مُخفاة في وحدة التحكم؛ لا تزال قابلة للقراءة من المستدعين والحالة }

في وحدة أم أو إعداد جذري، أرجع إلى مخرج وحدة فرعية عبر module.<الاسم>.<اسم_المخرج>:

# في الوحدة الجذرية أو الوحدة الأم: module "network" { source = "./modules/network" environment = var.environment cidr_block = "10.0.0.0/16" } module "compute" { source = "./modules/compute" vpc_id = module.network.vpc_id # <-- مرجع مخرج الوحدة subnet_ids = module.network.private_subnet_ids } # قراءة مخرج بعد التطبيق: # terraform output load_balancer_dns # terraform output -json # كل المخرجات بصيغة JSON (مفيد في CI) # terraform output -raw load_balancer_dns # سلسلة خام بدون تنسيق

تحقق المتغيرات: اكتشاف الأخطاء قبل التخطيط

تعمل كتلة validation في Terraform قبل أي استدعاء API، فترفض المدخلات الخاطئة فورًا. هذا مكافئ التحقق من المدخلات في كود التطبيق على مستوى البنية التحتية. في مستودعات المؤسسات، وحدة بلا تحقق على متغيراتها الحرجة هي إخفاق في بوابة الجودة. قواعد إرشادية: تحقق من أسماء البيئات (سلسلة غير مقيدة قد تُنشئ موردًا باسم "prod " بمسافة لاحقة — حادثة حقيقية)، تحقق من صياغة CIDR، من أن أرقام المنافذ ضمن النطاق، وأن مفاتيح الكائنات المطلوبة غير فارغة.

variable "cidr_block" { type = string description = "كتلة CIDR للـ VPC — يجب أن تكون /16 إلى /24 صالحة." validation { condition = can(cidrhost(var.cidr_block, 0)) error_message = "يجب أن يكون cidr_block بصياغة CIDR صالحة مثل 10.0.0.0/16." } } variable "port" { type = number description = "منفذ التطبيق (1024-65535)." validation { condition = var.port >= 1024 && var.port <= 65535 error_message = "يجب أن يكون port بين 1024 و65535." } } variable "db_engine" { type = object({ engine = string engine_version = string }) validation { condition = contains(["postgres", "mysql", "aurora-postgresql"], var.db_engine.engine) error_message = "يجب أن يكون db_engine.engine إحدى القيم: postgres أو mysql أو aurora-postgresql." } }
Terraform Variables, Locals, and Outputs Data Flow تدفق البيانات: tfvars → Variables → Locals → Resources → Outputs مصادر القيم production.tfvars TF_VAR_* env vars أعلام -var القيم الافتراضية مُتحقَّق منها ✓ variables.tf var.environment var.instance_type var.replica_count var.db_password var.tags locals.tf local.name_prefix local.common_tags local.is_production local.az_count local.vpc_cidr الموارد والمخرجات aws_instance.web aws_lb.web aws_vpc.main output: vpc_id output: load_balancer_dns التحقق يعمل قبل Plan. القيم الحساسة مُخفاة في المخرج. المحليات تُحسب مرة وتُرجع إليها N مرة.
تدفق البيانات من مصادر القيم عبر المتغيرات والمحليات إلى الموارد، مع مخرجات تُعرّض البيانات الجوهرية للمستدعين وخطوط أنابيب CI.

الجمع بين المكونات: واجهة وحدة حقيقية

يُعرّف الجمع بين المتغيرات والتحقق والمحليات والمخرجات الواجهة البرمجية العامة للوحدة. الوحدة المصممة جيدًا لها واجهة ضيقة ومكتوبة بالأنواع: المستدعون لا يستطيعون تمرير بيانات فاسدة، التفاصيل الداخلية مخفية، والمخرجات تُعرض بالضبط ما يحتاجه المستهلكون. هذا هو النمط خلف كل وحدة Terraform في سجل Terraform وفي مستودعات الوحدات الداخلية في Netflix وShopify وDatadog.

# في CI، اقرأ اسم DNS لموازن التحميل بعد التطبيق وشغّل اختبار الدخان: LB_DNS=$(terraform output -raw load_balancer_dns) curl -sf "https://${LB_DNS}/health" || { echo "فشل اختبار الدخان"; exit 1; } # في وحدة أم، مرّر المخرج إلى وحدة أخرى: module "dns" { source = "./modules/route53" alb_dns_name = module.compute.load_balancer_dns hosted_zone_id = var.hosted_zone_id record_name = "${var.environment}.example.com" } # فحص مخرج حساس دون طباعة على الطرفية: terraform output -json | jq -r '.db_endpoint.value' # (هذا لا يزال يطبع القيمة بنص صريح — مرّرها لأداة الأسرار)
قاعدة تصميم الوحدة: يجب أن تكون المخرجات معرّفات مستقرة (IDs وARNs وأسماء DNS) — ليست سلاسل مشتقة يمكن للمستدعين حسابها بأنفسهم. إذا احتاج مستدعٍ إلى arn:aws:s3:::${module.storage.bucket_name}، أعرض ARN مباشرةً كمخرج لا اسم الدلو فقط. هذا يفصل المستدعين عن معرفة كيفية بناء ARN ويتيح للوحدة تغيير تسميتها الداخلية دون كسر المستدعين.