أساسيات Terraform

الحالة البعيدة والخلفيات

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

الحالة البعيدة والخلفيات

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

يغطي هذا الدرس الخلفيتَين اللتين ستصادفهما في كل مؤسسة إنتاجية تقريباً (S3 مع قفل DynamoDB، وخلفيات HTTP)، وكيف يعمل قفل الحالة وما يحدث عند فشله، وكيفية التعامل مع البيانات الحساسة التي تكتبها Terraform حتماً في الحالة.

لماذا الحالة البعيدة غير قابلة للتفاوض في الفرق

تنهار الحالة المحلية بثلاث طرق مختلفة تحتاج كل منها إلى حادثة مؤلمة لتتعلمها:

  • لا مشاركة: مهندس ثانٍ يستنسخ المستودع لا يملك ملف الحالة. أول terraform plan له يُظهر كل مورد على أنه "سيُنشأ" — بنية تحتية موجودة بالفعل في السحابة.
  • لا قفل: مهمتا CI تُشغّلان في وقت واحد تستطيعان قراءة نفس الحالة، وحساب خطة، ثم الكتابة مجدداً — مع أن الكتابة الثانية تُلغي الأولى بصمت. تُترك الموارد يتيمة دون سجل في الحالة.
  • لا متانة: فشل قرص الحاسوب المحمول أو مستودع .git تالف يعني ضياع الحالة. مقارنة ما تعتقد Terraform أنه موجود مقابل ما تملكه السحابة فعلاً هو تمرين جنائي يستغرق أياماً.
المعيار الصناعي: كل فريق يُشغّل Terraform في الإنتاج يستخدم حالة بعيدة. في شركات مثل Stripe وShopify وCloudflare يُوفَّر الخلفية الخاصة بالحالة قبل أي بنية تحتية أخرى — إنها متطلب مسبق لا فكرة لاحقة. الخلفية نفسها خارج إدارة Terraform عادةً (مشكلة الدجاجة والبيضة) ويُهيّئها فريق المنصة مرة واحدة بنص برمجي.

خلفية S3 + DynamoDB

هذا هو المعيار الفعلي للبنية التحتية المبنية على AWS. تُخزَّن الحالة ككائن JSON في حاوية S3 (مع تمكين الإصدار والتشفير من جهة الخادم). يُوفّر القفلَ جدولُ DynamoDB بسمة نصية واحدة باسم LockID. عند بدء عملية Terraform، تكتب عنصر قفل إلى DynamoDB؛ عند انتهائها (نجاحاً أو فشلاً)، تحذف العنصر. أي عملية متزامنة تحاول كتابة نفس عنصر القفل تحصل على فشل في التحقق الشرطي من DynamoDB وتخرج Terraform بخطأ بدلاً من المتابعة بدون قفل.

# 1. تهيئة بنية تحتية الخلفية (افعل هذا مرة واحدة، يدوياً أو عبر وحدة جذر منفصلة) aws s3api create-bucket \ --bucket acme-terraform-state-prod \ --region us-east-1 # تمكين الإصدار — ضروري لتاريخ الحالة والتراجع aws s3api put-bucket-versioning \ --bucket acme-terraform-state-prod \ --versioning-configuration Status=Enabled # تمكين التشفير من جهة الخادم (AES-256) aws s3api put-bucket-encryption \ --bucket acme-terraform-state-prod \ --server-side-encryption-configuration '{ "Rules": [{ "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } }] }' # منع كل الوصول العام — حاويات الحالة يجب ألا تكون عامة أبداً aws s3api put-public-access-block \ --bucket acme-terraform-state-prod \ --public-access-block-configuration \ "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" # إنشاء جدول قفل DynamoDB aws dynamodb create-table \ --table-name acme-terraform-locks \ --attribute-definitions AttributeName=LockID,AttributeType=S \ --key-schema AttributeName=LockID,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --region us-east-1

مع تهيئة الحاوية والجدول، اضبط الخلفية في وحدة Terraform الجذر. تقع إعدادات الخلفية في كتلة terraform {} ولا يمكنها الإشارة إلى متغيرات أو محليات — يجب أن تكون القيم سلاسل ثابتة. هذا مقصود: تحتاج Terraform لحل الخلفية قبل تقييم أي شيء آخر في الإعداد.

# backend.tf — إعداد خلفية الوحدة الجذر terraform { required_version = ">= 1.6" backend "s3" { bucket = "acme-terraform-state-prod" key = "services/api-gateway/terraform.tfstate" region = "us-east-1" encrypt = true # فرض SSE حتى لو كان افتراضي الحاوية غير مضبوط dynamodb_table = "acme-terraform-locks" # جدول القفل profile = "prod" # ملف تعريف AWS CLI (احذفه في CI؛ استخدم أدوار IAM) } } # بعد تعديل إعداد الخلفية، شغّل دائماً: # terraform init # ستكتشف Terraform الخلفية الجديدة وتعرض ترحيل الحالة المحلية الموجودة.
اتفاقية تسمية المفاتيح: معامل key هو مسار كائن S3 داخل الحاوية. نظام تسمية مسطح (prod.tfstate) يصبح غير قابل للصيانة على نطاق واسع. استخدم تسلسلاً يعكس شجرة خدماتك: <team>/<service>/<environment>/terraform.tfstate. تُفصل كثير من المؤسسات أيضاً طبقات الشبكة والحوسبة والبيانات في ملفات حالة مستقلة حتى لا يتمكن وحدة حوسبة معطوبة من إفساد حالة الشبكة. هذا هو مبدأ "عزل الحالة" وهو أحد أكثر القرارات الهيكلية تأثيراً ستتخذها في مشروع Terraform.

قفل الحالة: كيف يعمل وماذا تفعل عند انهياره

كل أمر Terraform يمكنه تعديل الحالة — apply وdestroy وstate mv وimport — يكتسب قفلاً قبل البدء. الأوامر التي تقرأ الحالة فقط — plan وoutput وshow — لا تكتسب قفلاً بشكل افتراضي. يحتوي سجل القفل المخزن في DynamoDB على نوع العملية، واسم الجهاز، وإصدار Terraform، والطابع الزمني.

# مثال: كيف يبدو عنصر قفل DynamoDB (مسترجَع بـ AWS CLI) aws dynamodb get-item \ --table-name acme-terraform-locks \ --key '{"LockID": {"S": "acme-terraform-state-prod/services/api-gateway/terraform.tfstate"}}' \ --region us-east-1 # المخرج (عندما يكون القفل محجوزاً): # { # "Item": { # "LockID": { "S": "acme-terraform-state-prod/services/api-gateway/terraform.tfstate" }, # "Info": { "S": "{\"ID\":\"a1b2c3d4...\",\"Operation\":\"OperationTypeApply\", # \"Who\":\"ci-runner@github-actions\",\"Version\":\"1.7.5\", # \"Created\":\"2025-03-15T14:22:10.411Z\",\"Path\":\"...\"}" } # } # } # --- استرداد القفل العالق --- # يمكن أن يصبح القفل عالقاً إذا انهارت مهمة CI في منتصف التطبيق # أو فقد الحاسوب المحمول اتصاله بالشبكة. # ترفض Terraform التشغيل حتى يُزال القفل. # الخطوة 1: حدّد معرّف القفل من رسالة الخطأ التي طبعتها Terraform، # أو من عنصر DynamoDB أعلاه. # الخطوة 2: ازل القفل بالقوة (يتطلب حكماً بشرياً — تأكد أن عملية القفل ميتة فعلاً) terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890 # الخطوة 3: تحقق من أن ملف الحالة غير تالف بعد تطبيق مقطوع terraform plan # يجب أن يُظهر فقط الانجراف المشروع، لا تغييرات وهمية
لا تُزل القفل بالقوة أبداً أثناء عملية حية. إذا شغّلت terraform force-unlock بينما apply آخر قيد التقدم فعلاً، فقد أزلت الضمانة الوحيدة للتزامن. ستقرأ العملية التالية حالة قديمة، وتحسب خطة غير صحيحة، وقد تحذف أو تُعيد إنشاء موارد كانت العملية الأولى تُعدّلها. تأكد دائماً أن عملية القفل ميتة (تحقق من حالة مهمة CI، اتصل بالمهندس) قبل إلغاء القفل. على الأقل، انتظر 10 دقائق بعد الطابع الزمني للقفل.

خلفيات HTTP (GitLab، Terraform Cloud، مخصصة)

خلفية HTTP هي واجهة عامة: تُنفّذ Terraform طلبات GET وPOST (تحديث) وDELETE (إلغاء قفل) ضد أي خادم HTTP يُطبّق البروتوكول. يمتلك GitLab CI/CD خلفية حالة HTTP مدمجة (واحدة لكل مشروع، لكل بيئة)، مما يجعله الاختيار الافتراضي للمؤسسات الموجودة بالفعل على GitLab. يستخدم Terraform Cloud وHCP Terraform بروتوكولاً متوافقاً مع HTTP تحت الغطاء.

# خلفية حالة Terraform المُدارة بـ GitLab # يوفر GitLab نقطة نهاية HTTP لكل مشروع؛ المصادقة تستخدم رمز وصول المشروع. terraform { backend "http" { address = "https://gitlab.example.com/api/v4/projects/42/terraform/state/production" lock_address = "https://gitlab.example.com/api/v4/projects/42/terraform/state/production/lock" unlock_address = "https://gitlab.example.com/api/v4/projects/42/terraform/state/production/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_min = 5 # تُمرَّر بيانات الاعتماد عبر متغيرات البيئة، لا ترميزها هنا: # TF_HTTP_USERNAME = "gitlab-ci-token" # TF_HTTP_PASSWORD = "$CI_JOB_TOKEN" # مُحقَن تلقائياً بواسطة GitLab CI } } # في .gitlab-ci.yml، تُمرّر خطوة init بيانات الاعتماد عبر متغيرات البيئة: # variables: # TF_HTTP_USERNAME: "gitlab-ci-token" # TF_HTTP_PASSWORD: $CI_JOB_TOKEN # # terraform init # يقرأ إعداد الخلفية ويُصادق # terraform plan -out plan.tfplan # terraform apply plan.tfplan
Remote State Architecture: S3 Backend with DynamoDB Locking Engineer terraform apply CI Runner terraform apply DynamoDB Lock Table LockID (atomic write) S3 Bucket terraform.tfstate Versioning: ON Encryption: AES256 AWS API EC2 / RDS / VPC / ... acquire lock blocked read/write state provision resources Lock released on success OR failure
خلفية الحالة البعيدة S3: يكتسب المهندس قفل DynamoDB قبل القراءة أو الكتابة؛ مهمة CI محظورة حتى يُحرَّر القفل. تُوفَّر الموارد عبر AWS API بينما تتتبعها الحالة في S3.

البيانات الحساسة في الحالة: الواقع الإنتاجي

حالة Terraform ليست جرداً بسيطاً. إنها تُخزّن كل خصائص كل مورد مُدار — بما فيها تلك التي يُعلّمها مزود السحابة كحساسة. مثيل RDS المُنشأ حديثاً يكتب كلمة مرور المدير بنص عادي في الحالة. مفتاح وصول IAM يكتب secret بنص عادي. مورد شهادة TLS يكتب المفتاح الخاص. هذا ليس خللاً في Terraform؛ إنه نتيجة حتمية لإدارة البنية التحتية المتوازنة: تحتاج Terraform معرفة القيمة الحالية لتقرر ما إذا كانت تحتاج للتغيير.

  • شفّر الحالة أثناء الراحة: مكّن دائماً SSE في S3 (AES256 أو AWS KMS بمفتاح يديره العميل). للبيئات المنظّمة بشكل صارم، استخدم KMS CMK حتى تستطيع تدقيق وتدوير مفتاح التشفير بشكل مستقل.
  • قيّد الوصول بـ IAM: الأدوار التي تُشغّل Terraform فقط يجب أن تملك s3:GetObject وs3:PutObject وdynamodb:PutItem على حاوية الحالة وجدول القفل. يجب ألا يمتلك المهندسون وصولاً مباشراً إلى حاويات S3 الإنتاجية — يجب أن يتفاعلوا عبر خطوط CI فقط.
  • لا تُودع الحالة في git أبداً: أضف *.tfstate و*.tfstate.backup إلى .gitignore في كل مشروع Terraform. استخدم git-secrets أو hook ما قبل الإيداع لمنع الإيداعات العرضية.
  • استخدم sensitive = true في المخرجات: عَلِّم أي مخرجات تحتوي على قيمة سرية كحساسة. ستحجب Terraform قيمتها في مخرجات CLI وملفات الخطة — لكنها ستبقى في الحالة. الحساسية في Terraform حرس تجربة مستخدم، لا حد أمان.
# تعليم المخرجات كحساسة لتُحجب في مخرجات CLI output "db_password" { value = aws_db_instance.main.password sensitive = true # تطبع Terraform "(sensitive value)" بدلاً من كلمة المرور الفعلية } output "api_key" { value = aws_iam_access_key.deployer.secret sensitive = true } # استرداد مخرج حساس صراحةً (ضروري لتمريره لأدوات أخرى) terraform output -raw db_password # يتجاوز الحجب؛ أنبب بحذر terraform output -json | jq -r '.db_password.value' # أفضل ممارسة: لا تُخزّن أسراراً طويلة الأمد في حالة Terraform أبداً. # استخدم aws_secretsmanager_secret أو aws_ssm_parameter (SecureString) كمخزن، # وأشر إليها من إعداد التطبيق بـ ARN/path — لا بالقيمة عبر الحالة. # # مثال: توليد كلمة مرور عشوائية وتخزينها في Secrets Manager resource "random_password" "db" { length = 32 special = true } resource "aws_secretsmanager_secret" "db_password" { name = "prod/api-db/password" } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = random_password.db.result } # الآن يُشير مثيل RDS إلى Secrets Manager، لا مباشرةً إلى حالة Terraform resource "aws_db_instance" "main" { password = random_password.db.result # لا يزال في الحالة، لكن قابل للتدوير عبر Secrets Manager }
تشفير الحالة بـ KMS (تصليب الإنتاج): استبدل encrypt = true (AES256) بـ KMS CMK لأحمال العمل الإنتاجية: أضف kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/mrk-..." إلى إعداد الخلفية. هذا يمنحك استخدام مفتاح مدقَّق بـ CloudTrail، وتدوير المفتاح، والقدرة على إلغاء الوصول إلى كل الحالة التاريخية بتعطيل المفتاح — تحكم لا تستطيع AES256 مع المفاتيح المُدارة بـ AWS توفيره.

الترحيل بين الخلفيات

عند تغيير إعداد الخلفية — مثلاً، الانتقال من محلي إلى S3، أو تغيير مفتاح S3 — تكتشف Terraform التغيير عند الـ terraform init التالي وتطلب منك ترحيل الحالة الموجودة. شغّل دائماً terraform plan فوراً بعد الترحيل لتأكيد أن الحالة المُرحَّلة تتطابق مع ما تملكه السحابة فعلاً. الترحيل الناجح يُظهر صفراً من التغييرات المخططة.

# بعد تعديل backend.tf للإشارة إلى حاوية S3 جديدة أو مفتاح مختلف: terraform init -migrate-state # ستطبع Terraform: # Initializing the backend... # Do you want to copy existing state to the new backend? (yes/no) # اكتب "yes" للترحيل. # تحقق فوراً من نظافة الترحيل: terraform plan # المخرج المتوقع: "No changes. Your infrastructure matches the configuration." # إذا رأيت تغييرات غير متوقعة، لم يُرحَّل الحالة بشكل نظيف. # توقف فوراً، استعد من الخلفية السابقة، وحقّق.

الخلاصة

خلفيات الحالة البعيدة هي الأساس التشغيلي لأي سير عمل Terraform يعتمد على الفريق. توفر مجموعة S3 + DynamoDB متانة تخزين الكائنات، وتاريخاً بإصدارات، وتشفيراً أثناء الراحة، وقفلاً ذرياً — تغطية كل أوجه فشل الحالة المحلية. توفر خلفيات HTTP (GitLab، Terraform Cloud) نفس الضمانات عبر بروتوكول موحّد. فهم قفل الحالة — كيف يُكتسب، وكيفية الاسترداد بأمان من الأقفال العالقة، ولماذا تُسبّب العمليات المتزامنة بدون قفل تلفاً في البيانات — هو المعرفة التي تفصل المهندسين الذين يستخدمون Terraform عن الذين يُشغّلونه بأمان على نطاق واسع. أخيراً، معاملة الحالة كأصل حساس (تشفيرها، تقييد الوصول، عدم إيداعها في git) ليست اختيارية: ملف حالتك هو تفريغ جزئي لكل سر تحتفظ به بنيتك التحتية.