> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# مصطلحات البنية كشيفرة IaC التي تحتاجها قبل أي أداة
- URL: https://arabroot.io/articles/البنية-كشيفرة-iac-للمبتدئين/
- Published: 2026-10-06T08:00:00.000Z
- Updated: 2026-10-09T09:43:42.000Z
- Description: ما معنى أن تكتب بنيتك في ملفات داخل Git، ولماذا يفشل السكربت العادي عند تشغيله مرة ثانية، وما الذي تحفظه الأداة في ملف الحالة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, DevOps وCI/CD, الاستضافة الذاتية

عندما يكون لديك سيرفر واحد فأنت تدخل إليه عبر SSH وتثبت الحزم وتعدل ملفات الإعداد وتفتح المنافذ بيدك، وتكتب ما فعلته في ملف ملاحظات إذا تذكرت، وبعد سنة تحتاج إلى سيرفر ثان مطابق للأول فتفتح الملاحظات وتكتشف أنها لا تذكر نصف ما تغير، وأن أحد الزملاء عدل شيئاً في ليلة عطل ولم يخبر أحداً، فيصبح السيرفر الجديد شبيهاً بالقديم وليس مطابقاً له، والفرق الصغير بينهما هو الذي يظهر في يوم الإطلاق. والحل الذي تستخدمه الفرق اليوم لهذه المشكلة هو **البنية كشيفرة Infrastructure as Code**، واختصاراً IaC، أي أن تكتب السيرفرات والشبكات وسجلات DNS في ملفات نصية داخل Git، ثم تترك أداة تنشئها وتعدلها لتطابق هذه الملفات.

وهذا المقال هو أول مقال في [مرحلة البنية كشيفرة IaC والأتمتة](https://arabroot.io/start/#stage-iac) من خارطة الطريق، لذلك لن نقوم فيه بإنشاء سيرفر حقيقي، وإنما نشرح المصطلحات التي سوف تقابلها في كل أداة وفي كل مقال بعده، حتى إذا قرأت كلمة مثل State أو Drift أو Idempotent عرفت ماذا تعني ولماذا تهمك، وسوف نناقش في هذا المقال ما يلي:

- ما هي البنية كشيفرة، وما هو السيرفر الفريد Snowflake Server الذي جاءت لتمنعه.
- الفرق بين الطريقة الأمرية Imperative والطريقة التصريحية Declarative.
- التكرار الآمن Idempotency، مع تجربة صغيرة تبين لماذا يفشل السكربت العادي عند تشغيله مرة ثانية.
- الحالة المطلوبة Desired State وملف الحالة State File، ولماذا يحتوي ملف الحالة على أسرار.
- الخطة Plan والتطبيق Apply، والانحراف Drift وكيف يكشفه Plan.
- البنية غير القابلة للتعديل Immutable Infrastructure.
- الفرق بين التجهيز Provisioning وإدارة الإعدادات Configuration Management، أي متى تستخدم OpenTofu ومتى تستخدم Ansible، ثم علاقة كل ذلك بـ GitOps.

## ما هي البنية كشيفرة Infrastructure as Code؟

البنية كشيفرة هي أن تصف البنية التحتية Infrastructure التي يعمل عليها نظامك، أي السيرفرات والشبكات وجدران الحماية Firewalls وسجلات DNS ومساحات تخزين الملفات Buckets، في ملفات نصية تحفظها في نظام إدارة النسخ Version Control مثل Git، ثم تستخدم أداة تقرأ هذه الملفات وتنفذها على المزود، بدلاً من أن تضغط الأزرار في لوحة التحكم أو تكتب الأوامر بيدك على كل سيرفر. ويعرفها مارتن فاولر Martin Fowler في [مقاله عن Infrastructure as Code](https://martinfowler.com/bliki/InfrastructureAsCode.html?ref=arabroot.io) بأنها التعامل مع تعريف البنية كما تتعامل مع كود البرنامج، فتطبق عليه الممارسات نفسها: الملفات في Git، والمراجعة قبل الدمج، والاختبار، والتنفيذ الآلي.

والمشكلة التي جاءت لتحلها لها اسم معروف، وهو **السيرفر الفريد Snowflake Server**، وقد وصفه فاولر في [مقال قصير سنة 2012](https://martinfowler.com/bliki/SnowflakeServer.html?ref=arabroot.io) بأنه السيرفر الذي ضبط باليد وعدل مرة بعد مرة حتى أصبح نسخة فريدة مثل ندفة الثلج، وقال إن هذه الندفة الفريدة جيدة لمنتجع التزلج وسيئة لمركز البيانات، والسبب أن السيرفر الفريد لا تستطيع أن تعيد بناءه إذا تعطل، ولا تعرف بالضبط ما الذي عليه، فتخاف أن تحدثه أو تلمسه. والصورة التالية تبين الفرق بين الطريقتين:

![على اليسار مدير يضبط ثلاثة سيرفرات بيده عبر SSH فتختلف إصداراتها وإعداداتها: nginx 1.24 مع جدار حماية مفعل، وnginx 1.26 دون جدار حماية، وnginx 1.22 على المنفذ 8080، وعلى اليمين ملف main.tf في Git تطبقه الأداة فتخرج ثلاثة سيرفرات متطابقة](https://arabroot.io/content/images/2026/10/iac-terms-01-snowflake.webp)

السيرفرات التي تضبط باليد تختلف مع الوقت، والسيرفرات التي تبنى من ملف واحد تبقى متطابقة

وقد يتساءل البعض: لماذا لا أكتب سكربت Bash بالأوامر نفسها وأحفظه في Git، أليست هذه بنية كشيفرة أيضاً؟ والإجابة أنها خطوة أولى صحيحة، فهي أفضل بكثير من الملاحظات، ولكن السكربت العادي له مشكلتان سوف تراهما في الأقسام التالية: فهو يصف الخطوات وليس النتيجة، ولا يعرف ماذا يفعل إذا كانت النتيجة موجودة من قبل، وهاتان المشكلتان هما سبب وجود المصطلحين التاليين.

## الطريقة الأمرية Imperative والطريقة التصريحية Declarative

في **الطريقة الأمرية Imperative** أنت تكتب الخطوات بالترتيب: ثبت هذه الحزمة، ثم أنشئ هذا المستخدم، ثم أنشئ هذا المجلد، ثم أضف هذا السطر إلى ملف الإعداد، والأداة تنفذ ما كتبته كما هو دون أن تسأل عن الحالة الحالية، وهذا ما يفعله أي سكربت Bash. أما في **الطريقة التصريحية Declarative** فأنت تكتب النتيجة التي تريدها: يجب أن تكون حزمة nginx مثبتة، ويجب أن يوجد مستخدم اسمه deploy، ويجب أن يحتوي الملف على هذا السطر، والأداة هي التي تقرأ الوضع الحالي وتقارنه بما كتبته وتقرر الخطوات اللازمة للوصول إليه.

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

![على اليسار سكربت setup.sh بأربعة أوامر مرقمة بالترتيب تنفذ على السيرفر كما هي، وعلى اليمين ملف يصف النتيجة المطلوبة: الحزمة مثبتة والمستخدم موجود والمجلد موجود والسطر في الملف، وأداة تقرأ الوضع الحالي ثم تقارنه بالملف ثم تغير الفرق فقط](https://arabroot.io/content/images/2026/10/iac-terms-02-imperative-declarative.webp)

الطريقة الأمرية تكتب الخطوات، والطريقة التصريحية تكتب النتيجة وتترك الخطوات للأداة

والملف التالي مثال تصريحي حقيقي بلغة HCL التي يستخدمها OpenTofu وTerraform، وهو ينشئ ملف إعداد على الجهاز نفسه عبر المزود [hashicorp/local](https://search.opentofu.org/provider/hashicorp/local/latest/docs/resources/file?ref=arabroot.io)، واخترنا هذا المزود بالذات لأنه لا يحتاج إلى حساب عند أي مزود سحابي، فالفكرة هنا هي الشكل وليس السيرفر:

```hcl
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.9"
    }
  }
}

resource "local_file" "app_conf" {
  filename        = "${path.module}/app.conf"
  content         = "max_connections=100\n"
  file_permission = "0644"
}
```

في الإعداد أعلاه لاحظ التالي:

- لا يوجد في الملف أي أمر مثل «أنشئ» أو «عدل» أو «احذف»، وإنما يوجد وصف لمورد Resource واحد: ملف اسمه `app.conf` بهذا المحتوى وهذه الصلاحيات.
- الكتلة `required_providers` تحدد المزود Provider، وهو الإضافة التي تعرف كيف تتعامل مع نوع معين من الأشياء، وهنا الملفات المحلية، وفي مقالات قادمة سوف يكون المزود هو مزود السيرفرات أو مزود DNS.
- إذا شغلت الأداة والملف غير موجود فسوف تنشئه، وإذا كان موجوداً بالمحتوى نفسه فلن تفعل شيئاً، وإذا غيرت السطر في الكود إلى `max_connections=200` فسوف تعرف أن عليها أن تستبدل الملف، وأنت لم تكتب أياً من هذه الحالات الثلاث.

وتذكر أن أغلب الأدوات تجمع الطريقتين بنسب مختلفة، فـ Ansible مثلاً تكتب فيه المهام Tasks بالترتيب مثل السكربت، ولكن كل مهمة منها تصف حالة وليست أمراً، كما سيأتي في قسم إدارة الإعدادات.

## التكرار الآمن Idempotency: التشغيل الثاني لا يغير شيئاً

تعرف [مصطلحات Ansible الرسمية](https://docs.ansible.com/ansible/latest/reference%5Fappendices/glossary.html?ref=arabroot.io) العملية المتكررة بأمان Idempotent بأنها العملية التي تكون نتيجة تنفيذها مرة واحدة هي نفسها نتيجة تنفيذها مرات كثيرة متتالية دون أي تغيير بينها، وهذه الخاصية هي التي تجعلك تشغل الأداة في كل مرة دون خوف، سواء كان السيرفر جديداً أو قديماً أو نصف مجهز بعد خطأ في منتصف التشغيل السابق. والمصطلح مأخوذ من الرياضيات، وقد قابلته في [طرق HTTP](https://arabroot.io/articles/http-%D9%88-https-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#methods) عندما تحدثنا عن الطرق المتكررة Idempotent مثل PUT وDELETE التي لا تتغير نتيجتها إذا أعيد إرسالها.

ولكي ترى الفرق بنفسك نفذنا تجربة صغيرة داخل Container مؤقت من ال Image الرسمية `python:3.12-slim` المبنية على Debian 13، دون شبكة، فكتبنا أولاً سكربتاً بسيطاً ينفذ الأوامر بالترتيب كما يفعل أغلبنا في أول مرة:

```bash
#!/bin/sh
# setup-naive.sh: runs the commands in order, without any check
useradd --create-home deploy
mkdir /srv/app
echo "max_connections=100" >> /etc/app.conf
echo "done"
```

ثم شغلناه مرتين متتاليتين على ال Container نفسه، ثم عرضنا ملف الإعداد:

```bash
docker run --rm --name lab-iac-terms-naive --network none -v "$PWD:/lab:ro" python:3.12-slim sh -c '
  echo "--- run 1"; sh /lab/setup-naive.sh; echo "exit code: $?"
  echo "--- run 2"; sh /lab/setup-naive.sh; echo "exit code: $?"
  echo "--- cat /etc/app.conf"; cat /etc/app.conf'
```

والمخرج سوف يكون كما يلي:

```text
--- run 1
done
exit code: 0
--- run 2
useradd: user 'deploy' already exists
mkdir: cannot create directory ‘/srv/app’: File exists
done
exit code: 0
--- cat /etc/app.conf
max_connections=100
max_connections=100
```

في المخرج أعلاه لاحظ التالي:

- التشغيل الأول نجح، وهذا هو الفخ، فأغلب السكربتات تختبر على سيرفر جديد مرة واحدة فقط ثم توضع في المستودع.
- في التشغيل الثاني فشل `useradd` لأن المستخدم موجود، وفشل `mkdir` لأن المجلد موجود، ومع ذلك أكمل السكربت وطبع `done` وكان رمز الخروج Exit Code صفراً، أي أن أي أداة تراقب السكربت سوف تظن أن كل شيء نجح.
- والأسوأ هو السطر الأخير، فالسطر `max_connections=100` أضيف مرتين، وبعد عشرة تشغيلات سوف يكون في الملف عشرة أسطر، وبعض البرامج تأخذ أول قيمة وبعضها تأخذ آخر قيمة وبعضها يرفض الملف.

والحل الأول الذي يخطر بالبال هو إضافة `set -e` حتى يتوقف السكربت عند أول خطأ، وهذا يمنع التقرير الكاذب، ولكنه يجعل التشغيل الثاني يتوقف عند السطر الأول، فلا يمكنك تشغيل السكربت إلا على سيرفر جديد تماماً. والحل الصحيح هو أن يسأل كل سطر عن الحالة الحالية قبل أن يغيرها، كما في السكربت التالي:

```bash
#!/bin/sh
# setup-idempotent.sh: checks the current state first and changes only what differs
if id deploy >/dev/null 2>&1; then echo "ok: user deploy"; else useradd --create-home deploy; echo "changed: user deploy"; fi
if [ -d /srv/app ]; then echo "ok: /srv/app"; else mkdir -p /srv/app; echo "changed: /srv/app"; fi
if grep -qxF "max_connections=100" /etc/app.conf 2>/dev/null; then echo "ok: /etc/app.conf"; else echo "max_connections=100" > /etc/app.conf; echo "changed: /etc/app.conf"; fi
```

وشغلناه ثلاث مرات على Container جديد، وبين التشغيل الثاني والثالث غيرنا ملف الإعداد بيدنا كما يفعل زميل مستعجل:

```bash
docker run --rm --name lab-iac-terms-idem --network none -v "$PWD:/lab:ro" python:3.12-slim sh -c '
  echo "--- run 1"; sh /lab/setup-idempotent.sh
  echo "--- run 2"; sh /lab/setup-idempotent.sh
  echo "--- someone edits the file by hand"; echo "max_connections=500" > /etc/app.conf
  echo "--- run 3"; sh /lab/setup-idempotent.sh
  echo "--- cat /etc/app.conf"; cat /etc/app.conf'
```

والمخرج سوف يكون كما يلي:

```text
--- run 1
changed: user deploy
changed: /srv/app
changed: /etc/app.conf
--- run 2
ok: user deploy
ok: /srv/app
ok: /etc/app.conf
--- someone edits the file by hand
--- run 3
ok: user deploy
ok: /srv/app
changed: /etc/app.conf
--- cat /etc/app.conf
max_connections=100
```

في المخرج أعلاه لاحظ التالي:

- التشغيل الثاني لم يغير شيئاً، وكل سطر قال `ok`، وهذا هو التكرار الآمن.
- التشغيل الثالث وجد أن الملف تغير خارج السكربت فأعاده إلى الحالة المكتوبة وقال `changed` لهذا البند وحده، وهذا هو بالضبط ما سوف نسميه بعد قليل اكتشاف الانحراف Drift وتصحيحه.
- كلمتا `ok` و`changed` ليستا من اختيارنا بالصدفة، فهما الحالتان اللتان يطبعهما Ansible لكل مهمة، والفرق أن Ansible وOpenTofu يكتبان هذه الفحوص بدلاً منك لكل نوع من الموارد، أما في Bash فعليك أن تكتب الفحص بنفسك لكل سطر، وأن تتذكر الحالات التي لم تخطر ببالك.

والصورة التالية تلخص التجربة:

![جدول من ثلاثة تشغيلات: السكربت البسيط ينجح في التشغيل الأول ثم يعطي في الثاني والثالث أخطاء أن المستخدم موجود والمجلد موجود ويضيف السطر مرة أخرى، والسكربت المتكرر بأمان يغير ثلاثة بنود في التشغيل الأول ثم لا يغير شيئاً في الثاني والثالث](https://arabroot.io/content/images/2026/10/iac-terms-03-idempotency.webp)

السكربت الذي يتحقق قبل أن يغير يعطي النتيجة نفسها مهما شغلته

## الحالة المطلوبة Desired State والحالة الفعلية Actual State

بعد التجربة السابقة أصبح عندك شيئان مختلفان: **الحالة المطلوبة Desired State** وهي ما كتبته في الملفات، و**الحالة الفعلية Actual State** وهي ما يوجد الآن على السيرفر أو عند المزود، وعمل أي أداة IaC تصريحية هو أن تقرأ الاثنين وتقارن بينهما ثم تغير الفرق فقط حتى تصبح الفعلية مطابقة للمطلوبة، وهذه العملية تسمى المطابقة Reconciliation. لذلك تذكر قاعدة واحدة في كل ما يأتي: الملفات في Git هي المرجع، وما يختلف عنها على السيرفر خطأ يجب أن يصحح، إما بإرجاع السيرفر إلى الملف، أو بتعديل الملف ومراجعته ثم تطبيقه.

## ملف الحالة State File: ذاكرة الأداة

في تجربة Bash كان الفحص سهلاً، فالسكربت يسأل السيرفر مباشرة: هل يوجد مستخدم اسمه deploy؟ ولكن عند مزود سحابي الأمر مختلف، فإذا كتبت في الكود سيرفراً اسمه `web` وأنشأته الأداة، فالمزود أعطاه رقماً مثل `srv-48213`، وفي المرة القادمة يجب أن تعرف الأداة أن `web` في الكود هو `srv-48213` عند المزود، وإلا أنشأت سيرفراً ثانياً. ولهذا يحتفظ OpenTofu وTerraform بملف يسمى **ملف الحالة State File**، وتقول [وثائق OpenTofu](https://opentofu.org/docs/language/state/?ref=arabroot.io) إن الأداة تستخدمه لتربط الموارد الحقيقية بما في الإعداد، وتتابع البيانات الوصفية Metadata، وتحسن الأداء في البنى الكبيرة، وإنه يحفظ بشكل افتراضي في ملف محلي اسمه `terraform.tfstate`. والصورة التالية تبين العلاقة بين الثلاثة:

![ثلاثة أشياء: الكود main.tf وهو الحالة المطلوبة، والواقع عند المزود وهو الحالة الفعلية، وفي الوسط ملف الحالة terraform.tfstate الذي يحفظ آخر ما عرفته الأداة ويربط المورد web في الكود بالسيرفر srv-48213، وبجانبه قفل وعلامة تحذير لأن فيه كلمة مرور بنص واضح](https://arabroot.io/content/images/2026/10/iac-terms-04-state.webp)

الكود يقول ماذا تريد، والواقع هو ما يوجد فعلاً، وملف الحالة هو الخريطة بينهما

وفي ملف الحالة ثلاث نقاط عليك أن تعرفها من اليوم الأول، حتى لو لم تعمل إلا على جهازك:

- **الأسرار داخل ملف الحالة:** تقول [صفحة البيانات الحساسة في الحالة](https://opentofu.org/docs/language/state/sensitive-data/?ref=arabroot.io) إن الحالة قد تحتوي على بيانات حساسة مثل كلمات المرور الأولى لقواعد البيانات، وإن الحالة المحلية تحفظ في ملفات JSON بنص واضح، وتؤكد [وثائق Terraform](https://developer.hashicorp.com/terraform/language/state/sensitive-data?ref=arabroot.io) الشيء نفسه وتنصح بحفظ الحالة في مكان بعيد مشفر مع صلاحيات وصول وسجل تدقيق. لذلك لا تقم أبداً بإضافة `terraform.tfstate` إلى Git، والسبب أن من يقرأ المستودع يقرأ الأسرار، وتاريخ Git لا ينسى كما شرحنا في [اكتشاف الأسرار المسربة](https://arabroot.io/articles/%D8%A7%D9%83%D8%AA%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%B3%D8%B1%D8%A7%D8%B1-%D8%A7%D9%84%D9%85%D8%B3%D8%B1%D8%A8%D8%A9-%D9%85%D8%B9-gitleaks-%D9%88-trufflehog/#leak-history). ويوفر OpenTofu منذ الإصدار 1.7 تشفيراً لملف الحالة نفسه، وقد شرحناه في [مقال Terraform أم OpenTofu](https://arabroot.io/articles/terraform-%D8%A7%D9%85-opentofu-%D9%82%D8%B5%D8%A9-%D8%AA%D8%BA%D9%8A%D9%8A%D8%B1-%D8%A7%D9%84%D8%AA%D8%B1%D8%AE%D9%8A%D8%B5/#state-encryption).
- **القفل Locking:** إذا شغل شخصان الأداة في اللحظة نفسها على الحالة نفسها فقد يكتب كل منهما فوق الآخر، لذلك تدعم الأدوات [قفل الحالة State Locking](https://opentofu.org/docs/language/state/locking/?ref=arabroot.io)، فمن يبدأ أولاً يقفل الملف ومن يأتي بعده ينتظر أو يتوقف.
- **الحالة البعيدة Remote State:** عندما يعمل فريق على البنية نفسها لا يصلح أن يكون الملف على جهاز أحدهم، فيحفظ في مكان مشترك يسمى الخلفية Backend، مثل Bucket في S3 أو قاعدة PostgreSQL، وأين تحفظه وكيف تقفله وتحميه هو موضوع مقال كامل في [المرحلة نفسها](https://arabroot.io/start/#stage-iac).

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

## الخطة Plan والتطبيق Apply

الأداة التصريحية لا تنفذ الفرق مباشرة، وإنما تعرضه عليك أولاً، وهذه هي **الخطة Plan**، وبحسب [وثائق الأمر tofu plan](https://opentofu.org/docs/cli/commands/plan/?ref=arabroot.io) فهي تقرأ الحالة الحالية للموارد الموجودة لتتأكد أن ملف الحالة محدث، ثم تقارن الإعداد بالحالة السابقة وتسجل الفروق، ثم تقترح مجموعة من التغييرات إذا طبقت جعلت الموارد مطابقة للإعداد، دون أن تنفذ أياً منها. وبعد أن تراجع الخطة تنفذها بالأمر [apply](https://opentofu.org/docs/cli/commands/apply/?ref=arabroot.io). وتصف [دورة العمل الأساسية](https://opentofu.org/docs/intro/core-workflow/?ref=arabroot.io) في OpenTofu هذا في ثلاث خطوات: اكتب Write، ثم اعرض الخطة Plan، ثم طبق Apply، وتذكر أن مخرج الخطة في الفريق يراجع مع طلب الدمج Pull Request قبل أن يدمج. والصورة التالية تبين الدورة كاملة:

![خمس خطوات مرقمة من اليسار إلى اليمين: كتابة main.tf على اللابتوب، ثم init لتنزيل المزودات، ثم plan يعرض واحداً للإضافة وواحداً للتعديل وصفراً للحذف، ثم مراجعة شخص في طلب الدمج، ثم apply على السحابة، وتحتها الأوامر tofu init ثم tofu plan -out=plan.bin ثم المراجعة ثم tofu apply plan.bin](https://arabroot.io/content/images/2026/10/iac-terms-05-plan-apply.webp)

الخطة هي فرصتك الأخيرة لترى ما سوف يحذف أو يستبدل قبل أن يحدث

ولم نشغل OpenTofu في هذا المقال، فأول تشغيل حقيقي له سوف يكون في مقال «أول بنية مع OpenTofu» من المرحلة نفسها، لذلك أخذنا الرسائل التالية من [كود OpenTofu 1.13.1 نفسه](https://github.com/opentofu/opentofu/blob/v1.13.1/internal/command/jsonformat/plan.go?ref=arabroot.io)، وهي الملف الذي يكتب مخرج الخطة، حتى تعرفها عندما تراها:

| ما تراه في مخرج الخطة                                           | معناه                                                                                          |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| \+ create و\# … will be created                                 | مورد جديد سوف ينشأ                                                                             |
| \~ update in-place و\# … will be updated in-place               | مورد موجود سوف يعدل في مكانه دون أن يحذف                                                       |
| \-/+ destroy and then create replacement و\# … must be replaced | المورد سوف يحذف ثم ينشأ من جديد، وهذا أخطر سطر في الخطة، فالسيرفر الذي يستبدل يفقد ما على قرصه |
| +/- create replacement and then destroy                         | استبدال ينشئ الجديد أولاً ثم يحذف القديم                                                       |
| \# … will be destroyed                                          | مورد سوف يحذف                                                                                  |
| Plan: 1 to add, 1 to change, 0 to destroy.                      | السطر الأخير الذي يلخص العدد، والأرقام هنا مثال                                                |
| No changes. Your infrastructure matches the configuration.      | الحالة الفعلية مطابقة للمطلوبة، وهذا ما تريد أن تراه في التشغيل الثاني                         |

ولاحظ ميزتين في الأمر نفسه تفيدانك عندما تشغله من خط CI/CD: الخيار `-out=FILENAME` يحفظ الخطة في ملف تمرره بعد ذلك إلى `tofu apply`، فينفذ ما راجعته بالضبط وليس خطة جديدة قد تختلف، والخيار `-detailed-exitcode` يجعل رمز الخروج 0 إذا لم توجد تغييرات و1 عند الخطأ و2 إذا وجدت تغييرات، فيعرف الخط هل يحتاج إلى مراجعة دون أن يقرأ النص.

📌

في 28 فبراير 2017 توقفت خدمة Amazon S3 في منطقة US-EAST-1 أكثر من أربع ساعات، وبحسب [تقرير AWS الرسمي](https://aws.amazon.com/message/41926/?ref=arabroot.io) بدأ الأمر عندما نفذ مهندس مخول أمراً من دليل تشغيل معتمد Playbook لإزالة عدد صغير من السيرفرات، ولكن أحد مدخلات الأمر كتب بشكل خاطئ فأزيلت مجموعة أكبر بكثير، منها سيرفرات النظامين اللذين يعرفان مكان كل ملف ويخصصان مساحة الملفات الجديدة. وكان من الإصلاحات التي ذكرها التقرير أن الأداة أصبحت تزيل السعة ببطء، وأضيفت لها حماية تمنع أي إزالة تنزل بالنظام تحت الحد الأدنى المطلوب. والدرس هنا هو فكرة الخطة نفسها: أي أداة تغير البنية يجب أن تريك ما سوف تفعله قبل أن تفعله، وأن ترفض التغيير الذي يتجاوز حداً آمناً، لأن الخطأ في إدخال واحد سوف يحدث يوماً ما مهما كان المهندس خبيراً.

## الانحراف Drift: عندما يتغير السيرفر خارج الكود

**الانحراف Drift** هو الفرق بين الحالة الفعلية وما في الكود عندما يحدث بسبب تغيير خارج الأداة، مثل زميل فتح منفذاً من لوحة تحكم المزود ليحل عطلاً في الثانية فجراً ونسي أن يغلقه، أو تحديث تلقائي غير إعداداً، أو سكربت قديم ما زال يعمل. والانحراف خطير لأن الكود لم يعد يصف الواقع، فإذا أنشأت سيرفراً جديداً من الكود فلن يكون فيه هذا التغيير، وإذا كان التغيير ثغرة فلن يراها من يراجع الكود. والصورة التالية تبين كيف يكشفه Plan:

![ملف main.tf يقول إن جدار الحماية يفتح المنفذين 22 و443، وشخص يفتح المنفذ 8080 من لوحة التحكم في الثانية فجراً فيتغير السيرفر خارج الكود، ثم يعرض tofu plan الرسالة Objects have changed outside of OpenTofu مع المنافذ الثلاثة، وتحتها جوابان صحيحان: إما tofu apply ليغلق 8080، وإما إضافة 8080 إلى الكود ومراجعته ثم تطبيقه، والجواب الخاطئ هو تجاهله](https://arabroot.io/content/images/2026/10/iac-terms-06-drift.webp)

الخطة تكشف الانحراف، وأنت تختار بين إرجاع الواقع إلى الكود أو تعديل الكود ليطابق الواقع

فعندما تشغل الخطة وتجد الأداة أن مورداً تغير خارجها، تطبع قبل الخطة الرسالة `Note: Objects have changed outside of OpenTofu` مع التغييرات، وهذه أيضاً من الملف نفسه في كود OpenTofu، ثم تقترح إرجاع المورد إلى ما في الكود. وأمامك هنا طريقان صحيحان فقط: إما أن التغيير خطأ فتطبق الخطة وتعيد الواقع إلى الكود، وإما أن التغيير صحيح ومطلوب فتضيفه إلى الكود وتراجعه في طلب دمج ثم تطبقه. ويوجد أيضاً وضع خاص هو `tofu plan -refresh-only` الذي تقول الوثائق إنه ينشئ خطة هدفها الوحيد تحديث ملف الحالة ليطابق التغييرات التي حدثت خارج الأداة دون أن يغير أي مورد، وهو مفيد عندما تريد أن ترى الانحراف وحده أو تسجله.

ولاحظ أن الأداة لا تكشف الانحراف إلا عندما تشغلها، فإذا كنت تشغلها مرة كل شهر فالانحراف يعيش شهراً كاملاً، لذلك تشغل كثير من الفرق خطة مجدولة كل ليلة بالخيار `-detailed-exitcode`، فإذا خرجت بالرمز 2 دون أن يدمج أحد شيئاً فهذا يعني أن شيئاً تغير خارج الكود، ويصل تنبيه إلى الفريق.

## البنية غير القابلة للتعديل Immutable Infrastructure

حتى الآن كنا نتحدث عن سيرفر واحد يعيش سنوات ونصلحه في مكانه كلما احتاج، وهذا يسمى البنية القابلة للتعديل Mutable Infrastructure، ومشكلتها أن كل تحديث وكل إصلاح طارئ يضيف طبقة جديدة، ومع الوقت يعود السيرفر فريداً كما كان، حتى لو كانت كل التغييرات بأداة. والفكرة المقابلة وصفها كيف موريس Kief Morris في [مقال السيرفر غير القابل للتعديل Immutable Server](https://martinfowler.com/bliki/ImmutableServer.html?ref=arabroot.io) سنة 2013: السيرفر بعد نشره لا يعدل أبداً، وإنما يستبدل بنسخة جديدة محدثة، والتغييرات كلها تحدث في ال Image الأساسية التي يبنى منها، فتختبر ثم ينشأ منها سيرفر جديد ويحذف القديم. والصورة التالية تبين الفرق:

![في الأعلى سيرفر واحد يتغير في مكانه من v1 إلى تحديث ثم إصلاح طارئ ثم تعديل يدوي حتى لا يعرف أحد ما عليه، وفي الأسفل Image v1 ينشأ منها server v1، ثم Image v2 ينشأ منها server v2، ويحذف server v1 بعد أن يعمل الجديد](https://arabroot.io/content/images/2026/10/iac-terms-07-immutable.webp)

في البنية غير القابلة للتعديل لا يوجد سيرفر عمره سنوات فيه تعديلات لا يعرفها أحد

وأنت تستخدم هذه الفكرة كل يوم دون أن تسميها إذا كنت تعمل بـ Docker، فأنت لا تدخل إلى ال Container لتحدث البرنامج داخله، وإنما تبني Image جديدة وتشغل منها Container جديداً وتحذف القديم، كما في [دليل تثبيت Docker](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/). والفكرة نفسها على مستوى السيرفر كله تحتاج إلى أداة تبني Images جاهزة للسيرفرات مثل Packer، وهي من المقالات القادمة في المرحلة. ولكن لاحظ أن هذه الطريقة لا تناسب كل شيء، فقاعدة البيانات لا تستبدلها كل أسبوع، وإنما تفصل بياناتها على قرص أو Volume مستقل يبقى عندما يستبدل السيرفر، لذلك تجد أغلب الفرق تستبدل سيرفرات التطبيق وتعدل سيرفرات البيانات بحذر.

## التجهيز Provisioning وإدارة الإعدادات Configuration Management

سوف تقرأ كثيراً سؤال «OpenTofu أم Ansible؟»، والسؤال نفسه فيه خلط، لأن الأداتين تعملان في طبقتين مختلفتين:

- التجهيز Provisioning: إنشاء الأشياء نفسها عند المزود، أي السيرفر والشبكة الخاصة وجدار الحماية والقرص الإضافي وسجل DNS وال Bucket، وهذا عمل OpenTofu وTerraform.
- الإعداد الأول عند الإقلاع First Boot: أشياء تحدث مرة واحدة عند أول تشغيل للسيرفر، مثل إنشاء المستخدم ووضع مفتاح SSH، وهذا عمل [cloud-init](https://cloudinit.readthedocs.io/en/latest/?ref=arabroot.io) الذي تدعمه أغلب المزودات.
- إدارة الإعدادات Configuration Management: ما يوجد داخل السيرفر بعد أن ينشأ، أي الحزم والمستخدمين وملفات الإعداد والخدمات، وهذا عمل Ansible وأمثاله.
- التطبيق: ما يعمل فوق كل ذلك، وعندنا في الغالب Docker Compose أو Kubernetes.

وهذا التقسيم ليس رأينا وحدنا، فوثائق OpenTofu نفسها تقول في [مقارنتها مع أدوات إدارة الإعدادات](https://opentofu.org/docs/intro/vs/chef-puppet/?ref=arabroot.io) إن هذه الأدوات تثبت البرامج وتديرها على جهاز موجود من قبل، وإن OpenTofu ليس أداة لإدارة الإعدادات، وإنه يستطيع أن يجهز cloud-init ليشغل أداة الإعدادات عند أول إقلاع. والصورة التالية تبين الطبقات الأربع:

![أربع طبقات مرقمة: التجهيز Provisioning بأداة OpenTofu أو Terraform للسيرفرات والشبكات وDNS، ثم الإقلاع الأول بأداة cloud-init للمستخدم ومفتاح SSH مرة واحدة، ثم إدارة الإعدادات بأداة Ansible للحزم والمستخدمين والملفات والخدمات، ثم التطبيق بأداة Docker Compose أو Kubernetes](https://arabroot.io/content/images/2026/10/iac-terms-08-layers.webp)

أداة تنشئ السيرفر، ثم أداة تضبط ما بداخله، ثم التطبيق فوقهما

ولكي ترى كيف يبدو العمل نفسه من تجربة Bash بأداة إدارة إعدادات، هذا Playbook صغير في Ansible يصف المستخدم والمجلد والسطر نفسها، باستخدام الوحدات Modules الرسمية [user](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/user%5Fmodule.html?ref=arabroot.io) وfile وlineinfile:

```yaml
- name: Prepare the app server
  hosts: web
  become: true
  tasks:
    - name: User deploy exists
      ansible.builtin.user:
        name: deploy
        create_home: true

    - name: Directory /srv/app exists
      ansible.builtin.file:
        path: /srv/app
        state: directory

    - name: max_connections line is present
      ansible.builtin.lineinfile:
        path: /etc/app.conf
        line: max_connections=100
        create: true
```

في الإعداد أعلاه لاحظ التالي:

- المهام بالترتيب مثل السكربت، ولكن كل مهمة تصف حالة: المستخدم موجود، والمجلد موجود، والسطر موجود، ولا يوجد أي `if`، فالوحدة نفسها تفحص قبل أن تغير، وتقول `ok` أو `changed` كما فعل سكربتنا.
- لا يحتاج Ansible إلى ملف حالة مثل OpenTofu، لأنه يسأل السيرفر نفسه في كل مرة، وهذا مناسب لما داخل السيرفر، أما عند المزود فلا بد من خريطة تربط الأسماء بالأرقام كما رأينا.
- يوجد في Ansible ما يشبه الخطة، وهو [وضع الفحص Check Mode](https://docs.ansible.com/ansible/latest/playbook%5Fguide/playbooks%5Fcheckmode.html?ref=arabroot.io) بالخيار `--check`، الذي يعرض التغييرات المحتملة دون أن ينفذها.

والخلاصة العملية أن كثيراً من الفرق تستخدم الاثنين معاً: OpenTofu ينشئ السيرفر وسجل DNS ويمرر إلى cloud-init ما يلزم، ثم يأتي Ansible فيضبط ما بداخله، وسوف نخصص لهذا السؤال مقالاً كاملاً في المرحلة.

## ومن هنا إلى GitOps

إذا جمعت كل ما سبق، أي حالة مطلوبة مكتوبة بشكل تصريحي في Git، ومراجعة لكل تغيير في طلب دمج، وأداة تطابق الواقع مع الملفات وتكشف الانحراف، فأنت قريب جداً من الطريقة التي تسمى GitOps، والفرق أن GitOps تضيف أن تكون المطابقة مستمرة وآلية، فبرنامج يراقب المستودع والبيئة طوال الوقت ويعيد البيئة إلى ما في المستودع، بدلاً من أن يشغل أحد الأداة بيده. وقد شرحنا مبادئها الأربعة وكيف تطبقها على بيئات dev وstaging والإنتاج Production في [GitOps ببساطة](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%8A%D8%A6%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D8%A7%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA-%D9%88-gitops-%D9%84%D9%84%D9%81%D8%B1%D9%82/#gitops)، فلا نعيدها هنا.

## ماذا بعد في هذه المرحلة؟

هذا المقال هو الأساس، والمقالات القادمة في [مرحلة البنية كشيفرة](https://arabroot.io/start/#stage-iac) تطبق كل مصطلح فيه على أداة حقيقية، وأولها [Terraform أم OpenTofu؟ قصة تغيير الترخيص](https://arabroot.io/articles/terraform-%D8%A7%D9%85-opentofu-%D9%82%D8%B5%D8%A9-%D8%AA%D8%BA%D9%8A%D9%8A%D8%B1-%D8%A7%D9%84%D8%AA%D8%B1%D8%AE%D9%8A%D8%B5/) الذي يشرح لماذا يوجد اليوم مشروعان بالكود نفسه تقريباً وكيف تختار بينهما، ثم إنشاء أول سيرفر وسجلات DNS من ملف، ثم حفظ ملف الحالة وقفله وحمايته، ثم تشغيل الخطة في كل طلب دمج، ثم cloud-init وAnsible وPacker وإدارة الأسرار.

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

- **إضافة ملف الحالة إلى Git:** فيه أسرار بنص واضح، وتاريخ Git يحفظها حتى لو حذفت الملف بعد ذلك، فأضف `*.tfstate` و`*.tfstate.*` إلى `.gitignore` من أول يوم.
- **التعديل من لوحة التحكم «مرة واحدة فقط»:** كل تعديل خارج الكود انحراف، والانحراف الذي لا يصحح يعود في أسوأ وقت، فإذا اضطررت إليه في عطل فأضفه إلى الكود في اليوم نفسه.
- **تطبيق خطة لم يقرأها أحد:** سطر `must be replaced` على سيرفر قاعدة بيانات يعني حذفه، فاقرأ الخطة كلها وابحث عن الحذف والاستبدال قبل أي شيء آخر.
- **سكربتات Bash تختبر مرة واحدة:** إذا كتبت سكربتاً للإعداد فشغله مرتين على السيرفر نفسه قبل أن تعتمد عليه، فإذا لم يكن التشغيل الثاني بلا تغييرات فهو ليس جاهزاً.
- **استخدام OpenTofu لكل شيء:** تثبيت الحزم وتعديل الملفات داخل السيرفر عبر OpenTofu ممكن بطرق ملتوية، ولكنه عمل أداة إدارة الإعدادات، فاستخدم كل أداة في طبقتها.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- البنية كشيفرة تعني أن تكتب السيرفرات والشبكات وسجلات DNS في ملفات داخل Git، فتراجعها وتعيد تطبيقها، بدلاً من سيرفرات فريدة تضبط باليد.
- الطريقة الأمرية تكتب الخطوات، والطريقة التصريحية تكتب النتيجة، والأداة التصريحية تقرأ الوضع الحالي وتغير الفرق فقط.
- التكرار الآمن Idempotency يعني أن التشغيل الثاني لا يغير شيئاً، والسكربت العادي لا يملك هذه الخاصية إلا إذا كتبت الفحص بنفسك لكل سطر.
- ملف الحالة هو الخريطة بين الكود والموارد الحقيقية، وفيه أسرار بنص واضح، فلا تضعه في Git، واقفله واحفظ منه نسخاً احتياطية.
- الخطة Plan تريك ما سوف ينشأ ويعدل ويحذف قبل أن يحدث، وتكشف الانحراف Drift الذي حدث خارج الكود، والتطبيق Apply ينفذ الخطة التي راجعتها.
- OpenTofu وTerraform للتجهيز، وcloud-init للإقلاع الأول، وAnsible لما داخل السيرفر، وGitOps هي الخطوة التي تجعل المطابقة مستمرة وآلية.

[![](https://arabroot.io/content/images/2026/10/terraform-opentofu-cover-cover.webp)دليلTerraform أم OpenTofu؟ قصة تغيير الترخيص وما يعنيه لكلماذا غير Terraform رخصته في 2023 فولد منه OpenTofu، وأين يختلف المشروعان اليوم، وكيف تختار بينهما.](https://arabroot.io/articles/terraform-%D8%A7%D9%85-opentofu-%D9%82%D8%B5%D8%A9-%D8%AA%D8%BA%D9%8A%D9%8A%D8%B1-%D8%A7%D9%84%D8%AA%D8%B1%D8%AE%D9%8A%D8%B5/)[![](https://arabroot.io/content/images/2026/10/environments-gitops-cover-cover.webp)دليلمن الفرع إلى الإنتاج: البيئات وGitOps للفرقكيف يختبر فريقك كل ميزة وحدها ويعرضها على العميل دون أن تصل ميزة غير جاهزة إلى الإنتاج.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%8A%D8%A6%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D8%A7%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA-%D9%88-gitops-%D9%84%D9%84%D9%81%D8%B1%D9%82/)[![](https://arabroot.io/content/images/2026/10/secrets-scanning-cover-cover.webp)دليلSecrets Scanning: كيف تكتشف الأسرار المسربة في الكودكيف يخرج المفتاح من جهازك إلى تاريخ Git وخط CI، وكيف تكتشفه قبل غيرك، وماذا تفعل في الساعة الأولى بعد تسربه.](https://arabroot.io/articles/%D8%A7%D9%83%D8%AA%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%B3%D8%B1%D8%A7%D8%B1-%D8%A7%D9%84%D9%85%D8%B3%D8%B1%D8%A8%D8%A9-%D9%85%D8%B9-gitleaks-%D9%88-trufflehog/)[![](https://arabroot.io/content/images/2026/10/git-basics-cover-cover.webp)دليلإدارة النسخ وGit للمبتدئين: لماذا نحتاجها وكيف تعمللماذا تضيع الملفات بأسماء final\_v2 ويمسح الزملاء عمل بعضهم، وكيف يحل Git ذلك على مشروع حقيقي من أول commit.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/self-hosting-terms-cover-cover-1.webp)دليلمصطلحات الاستضافة الذاتية Self-hosting للمبتدئينمرجع مبسط لأهم المصطلحات التي سوف تقابلها في أدلة الاستضافة الذاتية، بأسمائها الإنجليزية كما تظهر في لوحات التحكم، مع شرح قصير لكل منها.](https://arabroot.io/articles/%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B6%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D8%AA%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال اعتماداً على وثائق OpenTofu وTerraform وAnsible ومقالات Martin Fowler وKief Morris وتقرير AWS عن حادثة S3، وتنفيذ تجربة التكرار الآمن بسكربتات Bash داخل Container من ال Image `python:3.12-slim` (Debian 13.7) على Docker Engine 29.8.0، أما رسائل الخطة فمأخوذة من كود OpenTofu 1.13.1 دون تشغيله.