أساسيات Terraform

الوحدات: بنية تحتية قابلة لإعادة الاستخدام

18 دقيقة الدرس 9 من 30

الوحدات: بنية تحتية قابلة لإعادة الاستخدام

في Google وAmazon، لا يقوم أيُّ فريق بإنشاء S3 bucket أو VPC بكتابة كتل موارد خام من الصفر. بل يستهلكون وحدة داخلية — حزمة مُعتمَدة ومُختبَرة تُجسّد كل خط الأمان القياسي ومتطلبات الوسوم. الوحدات (Modules) هي الآلية التي ترقى بـ Terraform من سكريبتات شخصية إلى منصات بنية تحتية على مستوى المنظمة. حين تبدأ بالشعور بأن نسخ كتل الموارد بين المجلدات أمر خاطئ، تكون الوحدات هي الجواب.

ما هي الوحدة فعلياً؟

وحدة Terraform هي ببساطة مجلد يحتوي على ملفات .tf. كل مشروع Terraform هو بالفعل وحدة — المجلد الذي تُشغّل منه terraform init يُسمى الوحدة الجذر (root module). حين تستدعي مجلداً آخر (أو حزمة بعيدة) من جذرك، فذلك يُعدّ وحدة فرعية (child module). لا يوجد تركيب خاص للإعلان عن الوحدة؛ الحدود هي مسار نظام الملفات.

الملفات الثلاثة التي ستجدها تقريباً دائماً في وحدة منظَّمة جيداً هي:

  • main.tf — تعريفات الموارد
  • variables.tf — إعلانات متغيرات المدخلات
  • outputs.tf — القيم التي تُعرّضها الوحدة لمن يستدعيها

اختيارياً: versions.tf (قيود المزود المطلوبة) وREADME.md (توثيق للإنسان، إلزامي في Terraform Registry).

Module composition: root calls child modules Root Module (main.tf) module "vpc" source = ./modules/vpc cidr = var.cidr env = var.env module "rds" source = ./modules/rds subnet_ids = module.vpc .private_subnets module "app" source = terraform-aws-modules /ecs/aws version = "~> 5.0" modules/vpc main / vars / outputs modules/rds main / vars / outputs Registry Module terraform-aws-modules/ecs output ref
الوحدة الجذر تُوصّل ثلاث وحدات فرعية — اثنتان محليتان وواحدة من سجل Terraform العام.

كتابة وحدة محلية

ابدأ بمثال بسيط لكنه واقعي: وحدة S3 bucket قابلة لإعادة الاستخدام تُطبّق التحكم في الإصدارات والتشفير من جانب الخادم وحجب الوصول العام — وهي الخط الأساسي الذي يجب أن يتمتع به كل bucket في الإنتاج.

# modules/s3-private/variables.tf variable "bucket_name" { description = "Globally unique bucket name" type = string } variable "tags" { description = "Tags applied to all resources" type = map(string) default = {} } # modules/s3-private/main.tf resource "aws_s3_bucket" "this" { bucket = var.bucket_name tags = var.tags } resource "aws_s3_bucket_versioning" "this" { bucket = aws_s3_bucket.this.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_server_side_encryption_configuration" "this" { bucket = aws_s3_bucket.this.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" } bucket_key_enabled = true } } resource "aws_s3_bucket_public_access_block" "this" { bucket = aws_s3_bucket.this.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } # modules/s3-private/outputs.tf output "bucket_id" { value = aws_s3_bucket.this.id } output "bucket_arn" { value = aws_s3_bucket.this.arn }

تُغلّف الوحدة أربعة موارد خلف واجهة من متغيرَين. لا يحتاج المستهلك أن يعرف شيئاً عن SSE أو حجب الوصول العام — تلك القرارات مُشفَّرة مرة واحدة داخل الوحدة.

استهلاك وحدة باستخدام كتلة module

في الوحدة الجذر، قم بربط الوحدة الفرعية باستخدام كتلة module. وسيط source يخبر Terraform بمكان العثور على الوحدة. الوسيطات المتبقية تُحوَّل إلى إعلانات variable في الوحدة الفرعية.

# root/main.tf module "artifacts_bucket" { source = "./modules/s3-private" bucket_name = "acme-ci-artifacts-${var.env}" tags = { env = var.env team = "platform" managed = "terraform" } } module "logs_bucket" { source = "./modules/s3-private" bucket_name = "acme-access-logs-${var.env}" tags = { env = var.env team = "security" managed = "terraform" } } # Reference a module output output "artifacts_arn" { value = module.artifacts_bucket.bucket_arn }

بعد إضافة كتلة module جديدة، يجب عليك تشغيل terraform init قبل terraform plan. يقوم init بتنزيل أو نسخ مصادر الوحدات إلى ذاكرة التخزين المؤقت .terraform/modules/. إغفال هذه الخطوة هو أكثر أخطاء "module not found" شيوعاً.

لا تُضف .terraform/ إلى Git أبداً. يحتوي هذا المجلد على مزودين محمّلين ونسخ من مصادر الوحدات — قد يصل حجمه إلى مئات الميغابايتات وهو قابل للاسترداد الكامل بتشغيل terraform init. أضف .terraform/ و*.tfstate إلى .gitignore. ملف state الوحيد الذي قد يُوجد في Git هو *.tfstate مستخدم للتطوير المحلي في بيئة sandbox شخصية — حتى هذا غير مشجَّع.

مصادر الوحدات والإصدارات

يقبل وسيط source خمسة أنواع من المصادر، والاختيار له تداعيات تشغيلية حقيقية:

  • مسار محلي (./modules/vpc) — أسرع دورة تطوير، لا شبكة ولا إصدارات. استخدمه للوحدات التي تعيش في المستودع ذاته مع التكوين الجذر (أسلوب monorepo). التغييرات يتم التقاطها فوراً عند init التالي.
  • Terraform Registry (hashicorp/consul/aws) — السجل العام على registry.terraform.io. يُصنَّف عبر version. وحدات المجتمع مثل terraform-aws-modules/vpc/aws هي نقطة البداية القياسية في الصناعة. ثبّت دائماً إصداراً.
  • GitHub / GitLab (git::https://github.com/org/repo.git//subdir?ref=v1.2.3) — مفيد للوحدات الخاصة قبل إعداد سجل خاص. ثبّت على وسم (tag) أو SHA لالتزام، لا على اسم فرع في الإنتاج.
  • سجل خاص (Terraform Cloud, Spacelift, Env0) — النمط المؤسسي. نفس تركيب source كالسجل العام لكنه مُصادَق. يُتيح إصدارات دلالية وخطوط اختبار آلية وضوابط وصول على إصدارات الوحدات.
  • S3 / GCS bucket (s3::https://s3.amazonaws.com/bucket/modules/vpc.zip) — آلية توزيع خاصة خفيفة دون الحاجة لسجل كامل.
# Pinning a public Registry module — ALWAYS use version constraints module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.8" # allows 5.8.x, 5.9.x — blocks 6.0.0 name = "prod-vpc" cidr = "10.0.0.0/16" azs = ["us-east-1a", "us-east-1b", "us-east-1c"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] enable_nat_gateway = true single_nat_gateway = false # one NAT per AZ for HA enable_dns_hostnames = true tags = local.common_tags } # Pinning a private GitHub module to a tag module "eks_cluster" { source = "git::https://github.com/acme-platform/terraform-modules.git//eks?ref=v3.1.0" cluster_name = "prod-eks" cluster_version = "1.30" vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets }
عوامل قيود الإصدار للوحدات: = 5.8.0 يُثبّت بالضبط (هش — يحجب تصحيحات الأمان). ~> 5.8 (القيد التشاؤمي) يسمح بترقيات patch وminor ضمن نفس الإصدار الرئيسي، وهو الافتراضي الموصى به للوحدات من جهات خارجية. >= 5.0, < 6.0 نطاق صريح مفيد حين تعلم أن تغييراً جوهرياً قادماً في 6.0. شغّل terraform get -update لسحب أحدث إصدار مسموح به إلى ذاكرة التخزين المؤقت للوحدة.

تركيب الوحدات وتسلسل المخرجات

تصبح الوحدات قوية حين تتشابك مخرجاتها. النمط هو: وحدة توفر مورداً، ووحدة ثانية تستهلك مخرجها كمتغير مدخل، ويبني Terraform رسم التبعيات تلقائياً.

في كتلة الكود أعلاه، يرجع module.vpc.vpc_id وmodule.vpc.private_subnets إلى مخرجات يُصدرها وحدة VPC. يرى Terraform هذا المرجع العابر للوحدات ويضمن أن VPC مُجهَّز بالكامل قبل أي محاولة لإنشاء EKS cluster. لا تحتاج أبداً إلى إدارة depends_on لمراجع المخرجات العابرة للوحدات — الرسم البياني ضمني.

ابقِ الوحدات رفيعة عند الواجهة. وحدة بـ40 متغير مدخل تحاول فعل الكثير — هذا إشارة إلى تقسيمها. في HashiCorp وفرق IaC الناضجة، تحل الوحدة عادةً مشكلة واحدة محددة النطاق بوضوح (S3 bucket آمن، EKS node group مُقوّى، RDS cluster مع مصادقة IAM). يمرر المستهلكون حفنة من المتغيرات، والوحدة تتخذ جميع قرارات الأمان والامتثال الموجَّهة داخلياً. التعقيد مُغلَّف، لا مُصدَّر.

اختبار وإصدار وحداتك الخاصة

حين تُدير وحدات تستهلكها فرق أخرى، تحتاج إلى نظام إصدار. النمط القياسي يُشبه كيفية إصدار مكتبة برمجية:

  1. احتفظ بالوحدات في مستودع مخصص (أو مجلد فرعي محدد جيداً في monorepo). لا تخلط كود الوحدات مع التكوين على مستوى الجذر.
  2. ضع وسماً لكل إصدار بـوسم semver (v1.0.0، v1.1.0). يُثبّت المستهلكون على وسم، لا على فرع.
  3. اكتب اختبارات آلية مع Terratest (مبني على Go) أو أمر Terraform المدمج terraform test (مبني على HCL، متاح منذ Terraform 1.6 / OpenTofu 1.7). تُجهّز الاختبارات بنية تحتية حقيقية في حساب AWS معزول، تتحقق من المخرجات، وتُدمّر كل شيء عند الانتهاء.
  4. شغّل مجموعة الاختبارات في CI على كل pull request قبل الدمج والوسم.

هذا الخط — PR → اختبارات CI لبنية تحتية حقيقية → دمج → وسم → المستهلكون يُحدّثون تثبيت الإصدار — هو المعيار في Gruntwork وHashiCorp وفرق هندسة المنصات الكبيرة التي تنشر كتالوجات وحدات داخلية لآلاف المهندسين.