قفل حالة Terraform S3 بدون

٢٥ مايو ٢٠٢٦

Terraform S3 State Locking Without DynamoDB (2026)

يمكن لـ Terraform S3 backend قفل الحالة (state) من تلقاء نفسه من خلال وسيط use_lockfile، دون الحاجة إلى جدول DynamoDB. تتوفر هذه الميزة بشكل عام منذ إصدار Terraform 1.11، وهي تقوم بكتابة كائن .tflock بجوار ملف الحالة الخاص بك باستخدام عمليات الكتابة الشرطية (conditional writes) في S3. يوضح هذا البرنامج التعليمي كيفية إعداد ذلك من البداية إلى النهاية.

ملخص

ستقوم بإنشاء دلو (bucket) S3 مشفر وذو إصدارات (versioned)، وتوجيه Terraform S3 backend إليه باستخدام use_lockfile = true، والتأكد من أن التشغيل المتزامن الثاني يتم حظره بواسطة كائن قفل أصلي بدلاً من جدول DynamoDB. ستقوم أيضاً بترحيل مشروع موجود حالياً ومقفل بواسطة DynamoDB دون أي توقف عن العمل، وكتابة سياسة IAM بأقل الامتيازات. تستغرق العملية حوالي 20 دقيقة. تحتاج إلى حساب AWS وإصدار Terraform 1.11 أو أحدث.

لسنوات، كان قفل S3 backend يعني إنشاء جدول DynamoDB منفصل: خدمة AWS أخرى لتهيئتها، وواجهة IAM أخرى لإدارتها، وبند آخر للدفع، ومورد آخر للاحتفاظ به في كود Terraform الخاص بك. قدم إصدار Terraform 1.10 قفل S3 الأصلي كخيار تجريبي، وقام إصدار Terraform 1.11 بترقيته ليصبح متاحاً بشكل عام مع وضع علامة "مهجور" (deprecated) على وسائط DynamoDB.12 لذا فإن الإجابة المختصرة على سؤال "هل لا يزال Terraform بحاجة إلى DynamoDB لقفل الحالة؟" هي لا. دلو واحد الآن يقوم بالمهمتين.

ما ستتعلمه

  • إنشاء دلو S3 مشفر وذو إصدارات للاحتفاظ بحالة Terraform
  • تكوين S3 backend باستخدام وسيط use_lockfile للقفل الأصلي
  • تهيئة الـ backend والتأكد من وجود كائن القفل .tflock
  • إعادة إنتاج تعارض في قفل الحالة ومسح قفل قديم باستخدام force-unlock
  • ترحيل مشروع موجود مقفل بـ DynamoDB إلى قفل S3 دون توقف عن العمل
  • كتابة سياسة IAM بأقل الامتيازات لـ S3 backend وملف القفل

المتطلبات الأساسية

  • Terraform 1.11 أو أحدث. ميزة use_lockfile متاحة بشكل عام من الإصدار 1.11؛ تم شحنها تجريبياً في 1.10، لذا فإن 1.11+ هو الإصدار الذي تريده في بيئة الإنتاج. تم التحقق من هذا البرنامج التعليمي مقابل Terraform 1.15.2. تحقق من إصدارك باستخدام terraform version.
  • حساب AWS و AWS CLI v2، مهيأ ببيانات الاعتماد (aws configure، أو متغيرات البيئة، أو AWS IAM Identity Center). تحقق باستخدام aws sts get-caller-identity.
  • أذونات IAM لإنشاء دلو S3 ووضع الكائنات فيه.
  • إلمام أساسي بـ terraform init، و plan، و apply. إذا كانت الحالة عن بُعد (remote state) جديدة بالنسبة لك، فإن دليل أساسيات البنية التحتية ككود يغطي "لماذا" قبل أن يغطي هذا الدليل "كيف".

الخطوة 1: إنشاء دلو S3 للحالة

هناك مشكلة "البيضة والدجاجة" مع الحالة عن بُعد: يجب أن يكون الدلو موجوداً قبل أن يتمكن Terraform من تخزين الحالة فيه. الحل الأنظف هو إنشاء الدلو مرة واحدة باستخدام AWS CLI، ثم ترك Terraform يستخدمه من ذلك الحين فصاعداً.

اختر منطقة (region) واسماً فريداً عالمياً للدلو. أسماء الدلاء مشتركة عبر كل حسابات AWS على الأرض، لذا ألحق اسمك بمعرف حسابك:

export AWS_REGION="us-east-1"
export TF_STATE_BUCKET="tf-state-acme-$(aws sts get-caller-identity --query Account --output text)"

aws s3api create-bucket \
  --bucket "$TF_STATE_BUCKET" \
  --region "$AWS_REGION"

لأي منطقة أخرى غير us-east-1، يجب عليك أيضاً تمرير --create-bucket-configuration LocationConstraint="$AWS_REGION" — حيث أن us-east-1 هي المنطقة الوحيدة التي يكون فيها حذف هذا الخيار صالحاً.

بعد ذلك، قم بتشغيل إصدارات الدلو (bucket versioning). توصي وثائق S3 backend صراحةً بذلك حتى تتمكن من استعادة نسخة سابقة من الحالة بعد حذف غير مقصود أو تطبيق خاطئ:3

aws s3api put-bucket-versioning \
  --bucket "$TF_STATE_BUCKET" \
  --versioning-configuration Status=Enabled

قم بتمكين التشفير الافتراضي من جانب الخادم (server-side encryption) بحيث يتم تشفير كل من ملف الحالة وملف القفل عند التخزين:

aws s3api put-bucket-encryption \
  --bucket "$TF_STATE_BUCKET" \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'

أخيراً، احظر جميع عمليات الوصول العام. تحتوي ملفات الحالة بشكل روتيني على أسرار، لذا فإن الدلو الخاص أمر غير قابل للتفاوض:

aws s3api put-public-access-block \
  --bucket "$TF_STATE_BUCKET" \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

الدلو جاهز الآن للاحتفاظ بالحالة والأقفال.

الخطوة 2: تكوين S3 backend باستخدام use_lockfile

قم بإنشاء دليل عمل وملف main.tf. يخبر بلوك backend "s3" برنامج Terraform بمكان وجود الحالة؛ والوسيط الوحيد use_lockfile = true هو ميزة القفل الأصلي بالكامل.3

terraform {
  required_version = ">= 1.11"

  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.9"
    }
  }

  backend "s3" {
    bucket       = "tf-state-acme-REPLACE-WITH-YOURS"
    key          = "global/s3-locking-demo/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

resource "random_pet" "server" {
  length = 2
}

output "server_name" {
  value = random_pet.server.id
}

استبدل قيمة bucket بالاسم الذي أنشأته في الخطوة 1. ملاحظات قليلة حول بلوك الـ backend:

  • key هو مسار الكائن داخل الدلو. يتم اشتقاق ملف القفل منه: يكتب Terraform ‏global/s3-locking-demo/terraform.tfstate.tflock — نفس المسار مع لاحقة .tflock.3
  • encrypt = true يطلب من Terraform تشفير الحالة وكائنات القفل التي يرفعها، بالإضافة إلى التشفير الافتراضي على مستوى الدلو من الخطوة 1.
  • use_lockfile = true يفعل القفل الأصلي. لا يوجد وسيط dynamodb_table في أي مكان في هذا التكوين.
  • لا يمكن لبلوكات الـ Backend استخدام المتغيرات أو الإدراج (interpolation)، لذا فإن القيم حرفية. للقيم الخاصة بالبيئة، استخدم التكوين الجزئي ومرر -backend-config في وقت التهيئة (init).

مورد random_pet هو عرض توضيحي بسيط ومقصود: يقوم بإنشاء اسم ودود ويخزنه في الحالة دون تهيئة أو فوترة أي بنية تحتية حقيقية.4 إنه موجود حتى يكون لديك حالة لتقفلها.

الخطوة 3: تهيئة الـ backend والتأكد من كائن القفل

قم بتهيئة دليل العمل. يقوم Terraform بتكوين S3 backend وتنزيل موفر (provider) ‏random:

terraform init
Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Installing hashicorp/random v3.9.0...

Terraform has been successfully initialized!

الآن قم بالتطبيق. اكتب yes عند المطالبة:

terraform apply
Terraform will perform the following actions:

  # random_pet.server will be created
  + resource "random_pet" "server" {
      + id        = (known after apply)
      + length    = 2
      + separator = "-"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

random_pet.server: Creating...
random_pet.server: Creation complete after 0s [id=stirring-mongoose]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

server_name = "stirring-mongoose"

انظر إلى ما استقر في الدلو:

aws s3 ls "s3://$TF_STATE_BUCKET/global/s3-locking-demo/"
2026-05-25 09:14:02       1180 terraform.tfstate

سترى terraform.tfstate ولكن لن ترى ملف .tflock. هذا متوقع وصحيح: كائن القفل موجود فقط أثناء العملية. يقوم Terraform بإنشائه في بداية plan أو apply ويحذفه في اللحظة التي تنتهي فيها العملية. بين عمليات التشغيل، لا يوجد قفل لتراه. تحت الغطاء، يقوم Terraform بإنشاء هذا الكائن باستخدام كتابة شرطية في S3 — وهي عملية PutObject تحمل ترويسة If-None-Match، وهي ميزة جعلتها S3 متاحة بشكل عام في أغسطس 2024 — لذا تنجح الكتابة فقط إذا لم يكن كائن القفل موجوداً بالفعل.5

الخطوة 4: مشاهدة تعارض القفل أثناء العمل

لرؤية القفل وهو يحظر فعلياً تشغيلاً متزامناً، تحتاج إلى نافذتي تيرمينال (terminal) في نفس دليل المشروع.

التيرمينال 1 — ابدأ عملية apply تحتوي على عمل حقيقي، ثم توقف عند مطالبة الموافقة. يجبر علم -replace برنامج Terraform على التخطيط لاستبدال المورد التجريبي، بحيث يكون لعملية apply دائماً شيء لتأكيده. يحصل Terraform على قفل الحالة في بداية العملية ويحتفظ به حتى تكتمل العملية، بما في ذلك أثناء انتظاره لإجابتك على المطالبة:

terraform apply -replace="random_pet.server"
  # random_pet.server will be replaced, as requested
-/+ resource "random_pet" "server" {
      ~ id     = "stirring-mongoose" -> (known after apply)
        length = 2
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Enter a value:

اترك تلك المطالبة كما هي. القفل الآن ممسوك.

التيرمينال 2 — بينما ينتظر التيرمينال 1، حاول إجراء أي عملية حالة:

هما جوهر الأمر. فشلت عملية PutObject المشروطة في Terminal 2 لأن كائن .tflock موجود بالفعل — وهذا هو بالضبط نوع الحماية الذي تريده. عد إلى Terminal 1، وأجب بـ yes (أو no)، وسيتم تحرير القفل؛ أعد تشغيل Terminal 2 وسينجح الأمر.

إذا تعطلت عملية Terraform في منتصف التشغيل، فسيتم ترك كائن .tflock خلفها، لأن كائنات S3 لا تنتهي صلاحيتها تلقائيًا. هذا ما يسمى بالقفل الراكد (stale lock). تأكد من عدم وجود أي شخص آخر يقوم بتشغيل Terraform فعليًا، ثم قم بتحريره باستخدام الـ ID الوارد في رسالة الخطأ:

terraform force-unlock 4d1f0e87-3b6a-2c9d-a5e1-77c0b9f4e220

في أنابيب CI، حيث يكون التنافس القصير بين الوظائف المتتالية أمرًا طبيعيًا، يفضل استخدام terraform plan -lock-timeout=120s. سيقوم Terraform حينها بإعادة محاولة الحصول على القفل لمدة دقيقتين قبل الاستسلام، بدلاً من الفشل عند أول تعارض.

الخطوة 5: ترحيل مشروع موجود بعيدًا عن DynamoDB

إذا كنت تقوم بالفعل بتشغيل خلفية S3 مع dynamodb_table، فيمكنك الانتقال إلى القفل الأصلي (native locking) دون أي توقف، لأن وسيطات S3 و DynamoDB مسموح لها بالتعايش معًا. عندما يتم تعيين كليهما، يحصل Terraform على قفل من كلا النظامين في كل عملية تشغيل.3 هذا التداخل هو ما يجعل الترحيل آمنًا.

افترض أن خلفيتك الحالية تبدو هكذا:

  backend "s3" {
    bucket         = "tf-state-acme-REPLACE-WITH-YOURS"
    key            = "global/s3-locking-demo/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }

المرحلة 1 — تشغيل كليهما. أضف use_lockfile = true مع الاحتفاظ بـ dynamodb_table:

  backend "s3" {
    bucket         = "tf-state-acme-REPLACE-WITH-YOURS"
    key            = "global/s3-locking-demo/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    use_lockfile   = true
  }

أعد التهيئة حتى يلتقط Terraform تكوين الخلفية المتغير:

terraform init -reconfigure

قم بتشغيل دورة plan/apply عادية. سيتم الآن إنشاء وحذف كل من عنصر DynamoDB وكائن S3 .tflock في كل عملية تشغيل. اترك هذه المرحلة تعمل لبضعة أيام عبر فريقك بالكامل و CI.

المرحلة 2 — التخلي عن DynamoDB. بمجرد أن تثق في القفل الأصلي، قم بإزالة وسيطات DynamoDB:

  backend "s3" {
    bucket       = "tf-state-acme-REPLACE-WITH-YOURS"
    key          = "global/s3-locking-demo/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
terraform init -reconfigure

المرحلة 3 — التنظيف. مع ترحيل كل مشروع، احذف الجدول غير المستخدم الآن وأذونات IAM التي كانت تشير إليه:

aws dynamodb delete-table --table-name terraform-locks --region us-east-1

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

الخطوة 6: تأمين الخلفية بسياسة IAM ذات الحد الأدنى من الصلاحيات

تمتلك أوراق اعتماد المسؤول الخاصة بك بالفعل وصولاً كافيًا لتشغيل كل ما سبق. بالنسبة لمشغل CI أو دور أتمتة مخصص، حدد الأذونات بدقة. تحدد وثائق خلفية S3 بالضبط ما هو مطلوب، ويستحق ملف القفل الاهتمام لأن مجموعة أذوناته تختلف عن ملف الحالة:3

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListStatePrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::tf-state-acme-REPLACE-WITH-YOURS",
      "Condition": {
        "StringEquals": {
          "s3:prefix": "global/s3-locking-demo/terraform.tfstate"
        }
      }
    },
    {
      "Sid": "ReadWriteState",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::tf-state-acme-REPLACE-WITH-YOURS/global/s3-locking-demo/terraform.tfstate"
    },
    {
      "Sid": "ManageLockFile",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::tf-state-acme-REPLACE-WITH-YOURS/global/s3-locking-demo/terraform.tfstate.tflock"
    }
  ]
}

التفصيل الذي يربك الناس: يحتاج مورد .tflock إلى s3:DeleteObject لأن Terraform يحذف كائن القفل عند انتهاء العملية، لكن ملف الحالة لا يُحذف — Terraform لا يحذف الحالة أبدًا، لذا فإن منح s3:DeleteObject على terraform.tfstate سيكون بمثابة صلاحية زائدة.3 إذا وجهت kms_key_id إلى مفتاح KMS يديره العميل، فامنح الدور أيضًا kms:Encrypt و kms:Decrypt و kms:GenerateDataKey على ذلك المفتاح.

التحقق

تأكد من ثلاثة أشياء قبل اعتبار المهمة منتهية.

الحالة مشفرة أثناء السكون:

aws s3api head-object \
  --bucket "$TF_STATE_BUCKET" \
  --key "global/s3-locking-demo/terraform.tfstate" \
  --query 'ServerSideEncryption' --output text
AES256

القفل يمنع التزامن فعليًا: أعد تشغيل اختبار المحطتين (two-terminal test) من الخطوة 4 وتأكد من فشل Terminal 2 بـ Error acquiring the state lock.

لا يوجد قفل راكد متبقي: مع عدم تشغيل أي عملية Terraform، قم بسرد بادئة الحالة وتأكد من رؤية terraform.tfstate ولكن بدون terraform.tfstate.tflock.

aws s3 ls "s3://$TF_STATE_BUCKET/global/s3-locking-demo/"

وجود كائن .tflock بينما لا يوجد شيء قيد التشغيل يعني وجود قفل راكد — قم بمسحه باستخدام terraform force-unlock ومعرف القفل من الخطأ، كما هو موضح في الخطوة 4.

الأخطاء الشائعة

Error acquiring the state lock في عملية تشغيل عادية ومنفردة. هذا قفل راكد تركه عملية متعطلة. تأكد من عدم وجود زميل أو وظيفة CI في منتصف التشغيل، ثم استخدم terraform force-unlock <ID> باستخدام المعرف الموجود في الخطأ. كحل أخير، احذف كائن .tflock من الحاوية (bucket) مباشرة.

terraform init يبلغ عن تغيير تكوين الخلفية. متوقع في أي وقت تقوم فيه بتحرير كتلة backend "s3"، بما في ذلك عند إضافة use_lockfile. قم بتشغيل terraform init -reconfigure. استخدم terraform init -migrate-state فقط عندما تتغير الـ bucket أو الـ key نفسها.

AccessDenied عند كتابة ملف القفل. تفتقر سياسة IAM إلى s3:PutObject أو s3:DeleteObject على مورد *.tflock. أضف بيان ManageLockFile من الخطوة 6 — السياسة التي تغطي terraform.tfstate فقط ستقرأ وتكتب الحالة بشكل جيد ولكنها ستفشل في اللحظة التي يبدأ فيها القفل بالعمل.

IllegalLocationConstraintException من create-bucket. أنت تقوم بإنشاء الحاوية في منطقة أخرى غير us-east-1 بدون --create-bucket-configuration LocationConstraint="$AWS_REGION". أضف هذا العلم لكل منطقة باستثناء us-east-1.

الدروس القديمة لا تزال تخبرك بإنشاء جدول DynamoDB. إنها تسبق إصدار Terraform 1.10. بالنسبة للإصدار 1.11 والأحدث، فإن use_lockfile هو المسار المدعوم ووسيطة dynamodb_table مهجورة ومن المقرر إزالتها في إصدار فرعي مستقبلي.23

الخطوات التالية

لديك الآن خلفية S3 بحاوية واحدة تقوم بتخزين وقفل الحالة دون الاعتماد على DynamoDB. من هنا:

  • قم بتقسيم الحالة لكل بيئة باستخدام workspace_key_prefix ومساحة عمل Terraform لكل مرحلة.
  • انقل إعداد الخلفية (backend bootstrap) إلى الأتمتة بحيث تكون الحاوية نفسها قابلة لإعادة الإنتاج.
  • اربط terraform plan/apply بـ CI مع -lock-timeout بحيث تصطف الوظائف المتتالية بسلاسة. الأنماط الموجودة في نشر تطبيق إنتاجي على Fly.io و ترحيل Kubernetes ingress إلى Gateway API تتماشى جيدًا مع الحالة البعيدة المقفلة.

يعد قفل S3 الأصلي أحد تلك التغييرات النادرة التي تزيل جزءًا متحركًا بدلاً من إضافة جزء جديد. استمتع بهذا الفوز.

Footnotes

  1. إصدار Terraform v1.10.0 (2024-11-27) — قدم قفل الحالة الأصلي التجريبي لـ S3. https://GitHub.com/hashicorp/terraform/releases/tag/v1.10.0

  2. إصدار Terraform v1.11.0 (2025-02-27) — أصبح use_lockfile متاحًا بشكل عام؛ وتم إهمال القفل المستند إلى DynamoDB. https://GitHub.com/hashicorp/terraform/releases/tag/v1.11.0 2

  3. "Backend Type: s3" — وثائق HashiCorp Developer (قفل الحالة، use_lockfile، .tflock، أذونات IAM، الهجرة). https://developer.hashicorp.com/terraform/language/backend/s3 2 3 4 5 6 7

  4. hashicorp/random provider — Terraform Registry. https://registry.terraform.io/providers/hashicorp/random/latest

  5. "Amazon S3 يدعم الآن عمليات الكتابة المشروطة" — AWS What's New، 2024-08-20. https://aws.amazon.com/about-aws/whats-new/2024/08/amazon-s3-conditional-writes/


نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.