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

Terragrunt وخطوط أنابيب DRY

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

Terragrunt وخطوط أنابيب DRY

Terraform محرك قوي لـ IaC، لكنه يعاني من ثغرة هيكلية: لا يوفر آلية أصيلة للحفاظ على إعدادات استدعاء الوحدات جافة من التكرار. بمجرد إدارة ثلاثة بيئات (dev, staging, prod) عبر منطقتين، تجد نفسك تنسخ نفس كتلة backend، ونفس إصدار الموفّر، ونفس مصدر الوحدة في عشرات الملفات. Terragrunt هو الغلاف التنسيقي الخفيف الذي يحل هذه المشكلة تحديداً: يحتفظ بالإعداد الجذري في مكان واحد، ويوصّل الحالة البعيدة تلقائياً، ويعبّر عن تبعيات المكدسات بصورة تصريحية، ويتيح تشغيل run-all apply لتقارب بيئة كاملة بترتيب طوبولوجي. على نطاق الشركات الكبرى، هذا هو الفرق بين مستودع بنية تحتية من خمسة ملفات وكارثة من 3,000 ملف متكرر.

ما هو Terragrunt فعلياً

Terragrunt ثنائي Go يلتف حول terraform. كل أمر Terragrunt يُنشئ دليلاً مؤقتاً، يكتب فيه إعدادات الواجهة الخلفية والموفّر، ثم يفوّض التنفيذ إلى terraform. فريقك لا يكتب Terraform بشكل مختلف — بل يتوقف فقط عن كتابة الشيفرة النمطية المتكررة. يقرأ Terragrunt ملفات terragrunt.hcl التي تستخدم HCL2 مع كتل خاصة بـ Terragrunt: remote_state، dependency، inputs، generate، وinclude.

Terragrunt ليس بديلاً عن Terraform. إنه منسّق. كل وحدة بنية تحتية لا تزال وحدة Terraform عادية. يضيف Terragrunt الطبقة فوقها: أين تقع الحالة، وبأي ترتيب تُنفَّذ الأشياء، وما المدخلات التي تتدفق بين المكدسات.

هيكل المستودع المعياري

يفصل مستودع Terragrunt القياسي بين الماذا (وحدات Terraform) والأين والكيف (إعدادات Terragrunt الحية). هيكل نموذجي لمنصة AWS بثلاث بيئات:

infra-live/ ├── terragrunt.hcl # الإعداد الجذري — قالب الواجهة الخلفية، إصدار الموفّر ├── dev/ │ ├── account.hcl # معرّف حساب dev والمنطقة │ ├── vpc/ │ │ └── terragrunt.hcl │ ├── eks/ │ │ └── terragrunt.hcl │ └── rds/ │ └── terragrunt.hcl ├── staging/ │ ├── account.hcl │ ├── vpc/terragrunt.hcl │ └── eks/terragrunt.hcl └── prod/ ├── account.hcl ├── vpc/terragrunt.hcl └── eks/terragrunt.hcl

يكمن السحر في ملف terragrunt.hcl الجذري. كل مكدس ورقي يُضمّنه عبر include، مما يعني كتابة إعداد الواجهة الخلفية والموفّرات المطلوبة مرة واحدة بالضبط.

ملف terragrunt.hcl الجذري — المصدر الوحيد للحقيقة

# infra-live/terragrunt.hcl locals { # الصعود في شجرة الدلائل للعثور على account.hcl على مستوى البيئة account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl")) env = local.account_vars.locals.env aws_region = local.account_vars.locals.aws_region account_id = local.account_vars.locals.account_id } # توليد كتلة موفّر AWS تلقائياً لكل مكدس فرعي generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOF provider "aws" { region = "${local.aws_region}" assume_role { role_arn = "arn:aws:iam::${local.account_id}:role/TerraformDeployRole" } } EOF } # قالب واجهة خلفية واحدة — البكت والمفتاح مبنيان من مسار الدليل remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { bucket = "myco-tfstate-${local.account_id}" key = "${path_relative_to_include()}/terraform.tfstate" region = local.aws_region encrypt = true dynamodb_table = "myco-tfstate-lock" } }

الدالة الأساسية هي path_relative_to_include(). للمكدس الموجود في prod/eks/terragrunt.hcl تُعيد prod/eks، فيصبح مفتاح S3 prod/eks/terraform.tfstate — فريد لكل مكدس، دون تسمية يدوية، ودون خطر التصادم.

استخدم بكت S3 واحداً لكل حساب AWS، وليس لكل بيئة. بكت واحد بمفاتيح مقسّمة بالمسار يسهل تدقيقه وتأمينه بسياسات البكت بكثير مقارنة بتكاثر البكتات لكل بيئة.

ملف terragrunt.hcl للمكدسات الورقية — استدعاء الوحدات دون شيفرة نمطية

# infra-live/prod/eks/terragrunt.hcl include "root" { path = find_in_parent_folders() # يصعد حتى يجد infra-live/terragrunt.hcl } # سحب مخرجات VPC من مكدس شقيق — يوصّله Terragrunt تلقائياً dependency "vpc" { config_path = "../vpc" # مخرجات وهمية لتشغيلات plan فقط في CI عندما لا يوجد VPC بعد mock_outputs = { vpc_id = "vpc-00000000" private_subnets = ["subnet-00000001", "subnet-00000002"] } mock_outputs_allowed_terraform_commands = ["validate", "plan"] } terraform { source = "git::https://github.com/myco/infra-modules.git//eks?ref=v3.7.0" } inputs = { cluster_name = "prod-eks" vpc_id = dependency.vpc.outputs.vpc_id subnet_ids = dependency.vpc.outputs.private_subnets cluster_version = "1.30" node_groups = { general = { instance_type = "m6i.2xlarge", desired_size = 6 } } }

لا يوجد backend.tf، ولا provider.tf، ولا versions.tf. يُولّدها Terragrunt الثلاثة عند التشغيل من الإعداد الجذري. يحتوي الملف الورقي فقط على ما هو فريد لهذا المكدس: مصدر الوحدة، وتثبيت الإصدار، والمدخلات.

ربط التبعيات وأمر run-all

كتلة dependency هي الميزة الأقوى في Terragrunt. تقرأ حالة مكدس آخر (config_path) وتعرض مخرجاته ككائن منظّم. هذا يحل محل مصادر بيانات terraform_remote_state المتناثرة ويجعل رسم بياني التبعية صريحاً وقابلاً للقراءة آلياً.

مع إعلان التبعيات، يمكنك تقارب بيئة كاملة بأمر واحد:

# تخطيط جميع المكدسات في prod بترتيب التبعية (متوازٍ حيث آمن) terragrunt run-all plan --terragrunt-working-dir infra-live/prod # تطبيق جميع المكدسات في prod — يحل Terragrunt الرسم البياني DAG تلقائياً terragrunt run-all apply --terragrunt-working-dir infra-live/prod # تطبيق مكدس eks وكل ما يعتمد عليه terragrunt run-all apply --terragrunt-working-dir infra-live/prod/eks # تدمير بترتيب عكسي للتبعية (إيقاف آمن) terragrunt run-all destroy --terragrunt-working-dir infra-live/prod \ --terragrunt-ignore-dependency-errors

ينشئ run-all رسماً بيانياً موجّهاً لا دوري (DAG) من جميع كتل dependency التي يجدها في شجرة الدليل المستهدف. المكدسات المستقلة تعمل بالتوازي؛ المعتمدة تنتظر. يقلص هذا عادةً وقت التطبيق على مستوى البيئة بنسبة 60–80% مقارنة بالتنفيذ المتسلسل.

Terragrunt run-all Dependency DAG vpc يُطبَّق أولاً eks يعتمد على vpc rds يعتمد على vpc elasticache يعتمد على vpc app-deployment يعتمد على eks + rds تعمل بالتوازي
يحل Terragrunt run-all رسم DAG للتبعيات — المكدسات المستقلة (eks, rds, elasticache) تُطبَّق بالتوازي بعد اكتمال vpc.

الإعداد الجاف مع account.hcl

يمتلك مستودع Terragrunt الإنتاجي عادةً مستويين أو ثلاثة من ملفات الإعداد المشتركة التي يقرأها Terragrunt عبر read_terragrunt_config():

  • account.hcl — على مستوى دليل البيئة. يحتوي على معرّف حساب AWS والمنطقة واسم البيئة لهذا الفرع.
  • region.hcl — على مستوى دليل المنطقة في بنيات متعددة المناطق.
  • ملف terragrunt.hcl الجذري — يقرأ كليهما، ويُولّد الموفّر والواجهة الخلفية لكل فرع تلقائياً.

هذا يعني إضافة بيئة رابعة (مثل perf) تتطلب فقط إنشاء دليل واحد، وملف account.hcl بثلاث قيم، ونسخ ملفات terragrunt.hcl الورقية. لا حاجة للمساس بأي كتل backend أو provider أو versions.

لا تستخدم run-all apply على بيئة الإنتاج دون مراجعة مسبقة لـ run-all plan في CI. التنفيذ بالرسم البياني DAG سريع بالضبط لأنه متوازٍ — تغيير خاطئ يمكن أن يكتمل عبر مكدسات متعددة قبل أن تتمكن من إيقافه. يجب دائماً أن تكون تطبيقات الإنتاج مشروطة بخطة معتمدة من إنسان محفوظة كأداة CI.

Terragrunt في خطوط أنابيب CI/CD

يستخدم نمط خط الأنابيب الموصى به run-all plan عند فتح طلب السحب وrun-all apply عند الدمج في main، مقيّداً بدليل البيئة المتغيرة:

# .github/workflows/terraform.yml (مبسّط) name: Terragrunt Plan / Apply on: pull_request: paths: ["infra-live/**"] push: branches: [main] paths: ["infra-live/**"] jobs: detect-env: runs-on: ubuntu-latest outputs: env_dir: ${{ steps.detect.outputs.env_dir }} steps: - uses: actions/checkout@v4 with: { fetch-depth: 2 } - id: detect run: | CHANGED=$(git diff --name-only HEAD~1 HEAD -- infra-live/ | head -1) ENV_DIR=$(echo "$CHANGED" | cut -d/ -f1-2) echo "env_dir=$ENV_DIR" >> "$GITHUB_OUTPUT" plan: needs: detect-env runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4 - uses: gruntwork-io/terragrunt-action@v2 with: tf_version: "1.9.5" tg_version: "0.67.0" tg_command: run-all plan tg_dir: ${{ needs.detect-env.outputs.env_dir }} apply: needs: detect-env runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: gruntwork-io/terragrunt-action@v2 with: tf_version: "1.9.5" tg_version: "0.67.0" tg_command: run-all apply tg_dir: ${{ needs.detect-env.outputs.env_dir }}
ثبّت كلاً من tf_version وtg_version في CI. إصدارات Terragrunt متكررة وتُدخل أحياناً تغييرات سلوكية. انجراف الإصدارات غير المنضبط بين المطورين وCI هو مصدر شهير لتباين "يعمل على جهازي". استخدم ملف .terraform-version وملف .terragrunt-version في جذر المستودع واقرأهما في خط أنابيبك.

أنماط الفشل الشائعة في بيئة الإنتاج

  • المخرجات الوهمية القديمة في plan. إذا لم تتطابق mock_outputs مع المخرجات الفعلية، تبدو الخطط سليمة لكن التطبيق يفشل. راجع المخرجات الوهمية في كل مرة تتغير فيها شكل مخرجات الوحدة الأصلية.
  • تنازع القفل في run-all apply. المكدسات المتوازية التي تشترك في جدول DynamoDB للقفل قد تصطدم بالتقنين عند التوازي الشديد. اضبط --terragrunt-parallelism 4 لتحديد التزامن في البيئات ذات المكدسات الكثيرة.
  • تصادم مسار الحالة بعد إعادة تسمية الدليل. إذا أعدت تسمية dev/rds/ إلى dev/aurora/، يُولّد Terragrunt مفتاح S3 جديداً. الحالة القديمة مهجورة؛ المسار الجديد يبدأ فارغاً ويحاول إنشاء كل شيء من جديد. نفّذ دائماً terraform state mv أو أعد تسمية مفتاح S3 فعلياً قبل دخول تغيير اسم الدليل.
  • الملفات المولّدة محفوظة في git. يكتب Terragrunt provider.tf وbackend.tf في أدلة العمل. أضف .terragrunt-cache/ وأي ملفات generated*.tf إلى .gitignore — حفظها يكسر نموذج DRY.

إتقان Terragrunt يحوّل مجموعة هشة من أدلة Terraform الخاصة بكل بيئة إلى منصة بنية تحتية متماسكة وقابلة للتدقيق وسريعة. الاستثمار في الإعداد الجذري يؤتي ثماره منذ اليوم الأول بمجرد انضمام المهندس الثاني إلى الفريق.