عندما يكون لديك سيرفر واحد فأنت تدخل إليه عبر SSH وتثبت الحزم وتعدل ملفات الإعداد وتفتح المنافذ بيدك، وتكتب ما فعلته في ملف ملاحظات إذا تذكرت، وبعد سنة تحتاج إلى سيرفر ثان مطابق للأول فتفتح الملاحظات وتكتشف أنها لا تذكر نصف ما تغير، وأن أحد الزملاء عدل شيئاً في ليلة عطل ولم يخبر أحداً، فيصبح السيرفر الجديد شبيهاً بالقديم وليس مطابقاً له، والفرق الصغير بينهما هو الذي يظهر في يوم الإطلاق. والحل الذي تستخدمه الفرق اليوم لهذه المشكلة هو البنية كشيفرة Infrastructure as Code، واختصاراً IaC، أي أن تكتب السيرفرات والشبكات وسجلات DNS في ملفات نصية داخل Git، ثم تترك أداة تنشئها وتعدلها لتطابق هذه الملفات.
وهذا المقال هو أول مقال في مرحلة البنية كشيفرة 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 بأنها التعامل مع تعريف البنية كما تتعامل مع كود البرنامج، فتطبق عليه الممارسات نفسها: الملفات في Git، والمراجعة قبل الدمج، والاختبار، والتنفيذ الآلي.
والمشكلة التي جاءت لتحلها لها اسم معروف، وهو السيرفر الفريد Snowflake Server، وقد وصفه فاولر في مقال قصير سنة 2012 بأنه السيرفر الذي ضبط باليد وعدل مرة بعد مرة حتى أصبح نسخة فريدة مثل ندفة الثلج، وقال إن هذه الندفة الفريدة جيدة لمنتجع التزلج وسيئة لمركز البيانات، والسبب أن السيرفر الفريد لا تستطيع أن تعيد بناءه إذا تعطل، ولا تعرف بالضبط ما الذي عليه، فتخاف أن تحدثه أو تلمسه. والصورة التالية تبين الفرق بين الطريقتين:

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

والملف التالي مثال تصريحي حقيقي بلغة HCL التي يستخدمها OpenTofu وTerraform، وهو ينشئ ملف إعداد على الجهاز نفسه عبر المزود hashicorp/local، واخترنا هذا المزود بالذات لأنه لا يحتاج إلى حساب عند أي مزود سحابي، فالفكرة هنا هي الشكل وليس السيرفر:
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 الرسمية العملية المتكررة بأمان Idempotent بأنها العملية التي تكون نتيجة تنفيذها مرة واحدة هي نفسها نتيجة تنفيذها مرات كثيرة متتالية دون أي تغيير بينها، وهذه الخاصية هي التي تجعلك تشغل الأداة في كل مرة دون خوف، سواء كان السيرفر جديداً أو قديماً أو نصف مجهز بعد خطأ في منتصف التشغيل السابق. والمصطلح مأخوذ من الرياضيات، وقد قابلته في طرق HTTP عندما تحدثنا عن الطرق المتكررة Idempotent مثل PUT وDELETE التي لا تتغير نتيجتها إذا أعيد إرسالها.
ولكي ترى الفرق بنفسك نفذنا تجربة صغيرة داخل Container مؤقت من ال Image الرسمية python:3.12-slim المبنية على Debian 13، دون شبكة، فكتبنا أولاً سكربتاً بسيطاً ينفذ الأوامر بالترتيب كما يفعل أغلبنا في أول مرة:
#!/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 نفسه، ثم عرضنا ملف الإعداد:
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'والمخرج سوف يكون كما يلي:
--- 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 حتى يتوقف السكربت عند أول خطأ، وهذا يمنع التقرير الكاذب، ولكنه يجعل التشغيل الثاني يتوقف عند السطر الأول، فلا يمكنك تشغيل السكربت إلا على سيرفر جديد تماماً. والحل الصحيح هو أن يسأل كل سطر عن الحالة الحالية قبل أن يغيرها، كما في السكربت التالي:
#!/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 جديد، وبين التشغيل الثاني والثالث غيرنا ملف الإعداد بيدنا كما يفعل زميل مستعجل:
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'والمخرج سوف يكون كما يلي:
--- 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 فعليك أن تكتب الفحص بنفسك لكل سطر، وأن تتذكر الحالات التي لم تخطر ببالك.
والصورة التالية تلخص التجربة:

الحالة المطلوبة 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 إن الأداة تستخدمه لتربط الموارد الحقيقية بما في الإعداد، وتتابع البيانات الوصفية Metadata، وتحسن الأداء في البنى الكبيرة، وإنه يحفظ بشكل افتراضي في ملف محلي اسمه terraform.tfstate. والصورة التالية تبين العلاقة بين الثلاثة:

وفي ملف الحالة ثلاث نقاط عليك أن تعرفها من اليوم الأول، حتى لو لم تعمل إلا على جهازك:
- الأسرار داخل ملف الحالة: تقول صفحة البيانات الحساسة في الحالة إن الحالة قد تحتوي على بيانات حساسة مثل كلمات المرور الأولى لقواعد البيانات، وإن الحالة المحلية تحفظ في ملفات JSON بنص واضح، وتؤكد وثائق Terraform الشيء نفسه وتنصح بحفظ الحالة في مكان بعيد مشفر مع صلاحيات وصول وسجل تدقيق. لذلك لا تقم أبداً بإضافة
terraform.tfstateإلى Git، والسبب أن من يقرأ المستودع يقرأ الأسرار، وتاريخ Git لا ينسى كما شرحنا في اكتشاف الأسرار المسربة. ويوفر OpenTofu منذ الإصدار 1.7 تشفيراً لملف الحالة نفسه، وقد شرحناه في مقال Terraform أم OpenTofu. - القفل Locking: إذا شغل شخصان الأداة في اللحظة نفسها على الحالة نفسها فقد يكتب كل منهما فوق الآخر، لذلك تدعم الأدوات قفل الحالة State Locking، فمن يبدأ أولاً يقفل الملف ومن يأتي بعده ينتظر أو يتوقف.
- الحالة البعيدة Remote State: عندما يعمل فريق على البنية نفسها لا يصلح أن يكون الملف على جهاز أحدهم، فيحفظ في مكان مشترك يسمى الخلفية Backend، مثل Bucket في S3 أو قاعدة PostgreSQL، وأين تحفظه وكيف تقفله وتحميه هو موضوع مقال كامل في المرحلة نفسها.
وقد يتساءل البعض: إذا فقدت ملف الحالة، هل تحذف السيرفرات؟ والإجابة لا، فالسيرفرات تبقى تعمل عند المزود، ولكن الأداة لم تعد تعرف أنها تملكها، فإذا شغلتها بعد ذلك سوف تحاول إنشاء كل شيء من جديد، وعليك أن تعيد ربط الموارد بالحالة يدوياً عبر الاستيراد Import، وهذا عمل ممل في البنى الكبيرة، لذلك احفظ نسخاً احتياطية من الحالة كما تحفظها من قاعدة البيانات.
الخطة Plan والتطبيق Apply
الأداة التصريحية لا تنفذ الفرق مباشرة، وإنما تعرضه عليك أولاً، وهذه هي الخطة Plan، وبحسب وثائق الأمر tofu plan فهي تقرأ الحالة الحالية للموارد الموجودة لتتأكد أن ملف الحالة محدث، ثم تقارن الإعداد بالحالة السابقة وتسجل الفروق، ثم تقترح مجموعة من التغييرات إذا طبقت جعلت الموارد مطابقة للإعداد، دون أن تنفذ أياً منها. وبعد أن تراجع الخطة تنفذها بالأمر apply. وتصف دورة العمل الأساسية في OpenTofu هذا في ثلاث خطوات: اكتب Write، ثم اعرض الخطة Plan، ثم طبق Apply، وتذكر أن مخرج الخطة في الفريق يراجع مع طلب الدمج Pull Request قبل أن يدمج. والصورة التالية تبين الدورة كاملة:

ولم نشغل OpenTofu في هذا المقال، فأول تشغيل حقيقي له سوف يكون في مقال «أول بنية مع OpenTofu» من المرحلة نفسها، لذلك أخذنا الرسائل التالية من كود OpenTofu 1.13.1 نفسه، وهي الملف الذي يكتب مخرج الخطة، حتى تعرفها عندما تراها:
| ما تراه في مخرج الخطة | معناه |
|---|---|
+ 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 إذا وجدت تغييرات، فيعرف الخط هل يحتاج إلى مراجعة دون أن يقرأ النص.
الانحراف Drift: عندما يتغير السيرفر خارج الكود
الانحراف Drift هو الفرق بين الحالة الفعلية وما في الكود عندما يحدث بسبب تغيير خارج الأداة، مثل زميل فتح منفذاً من لوحة تحكم المزود ليحل عطلاً في الثانية فجراً ونسي أن يغلقه، أو تحديث تلقائي غير إعداداً، أو سكربت قديم ما زال يعمل. والانحراف خطير لأن الكود لم يعد يصف الواقع، فإذا أنشأت سيرفراً جديداً من الكود فلن يكون فيه هذا التغيير، وإذا كان التغيير ثغرة فلن يراها من يراجع الكود. والصورة التالية تبين كيف يكشفه Plan:

فعندما تشغل الخطة وتجد الأداة أن مورداً تغير خارجها، تطبع قبل الخطة الرسالة Note: Objects have changed outside of OpenTofu مع التغييرات، وهذه أيضاً من الملف نفسه في كود OpenTofu، ثم تقترح إرجاع المورد إلى ما في الكود. وأمامك هنا طريقان صحيحان فقط: إما أن التغيير خطأ فتطبق الخطة وتعيد الواقع إلى الكود، وإما أن التغيير صحيح ومطلوب فتضيفه إلى الكود وتراجعه في طلب دمج ثم تطبقه. ويوجد أيضاً وضع خاص هو tofu plan -refresh-only الذي تقول الوثائق إنه ينشئ خطة هدفها الوحيد تحديث ملف الحالة ليطابق التغييرات التي حدثت خارج الأداة دون أن يغير أي مورد، وهو مفيد عندما تريد أن ترى الانحراف وحده أو تسجله.
ولاحظ أن الأداة لا تكشف الانحراف إلا عندما تشغلها، فإذا كنت تشغلها مرة كل شهر فالانحراف يعيش شهراً كاملاً، لذلك تشغل كثير من الفرق خطة مجدولة كل ليلة بالخيار -detailed-exitcode، فإذا خرجت بالرمز 2 دون أن يدمج أحد شيئاً فهذا يعني أن شيئاً تغير خارج الكود، ويصل تنبيه إلى الفريق.
البنية غير القابلة للتعديل Immutable Infrastructure
حتى الآن كنا نتحدث عن سيرفر واحد يعيش سنوات ونصلحه في مكانه كلما احتاج، وهذا يسمى البنية القابلة للتعديل Mutable Infrastructure، ومشكلتها أن كل تحديث وكل إصلاح طارئ يضيف طبقة جديدة، ومع الوقت يعود السيرفر فريداً كما كان، حتى لو كانت كل التغييرات بأداة. والفكرة المقابلة وصفها كيف موريس Kief Morris في مقال السيرفر غير القابل للتعديل Immutable Server سنة 2013: السيرفر بعد نشره لا يعدل أبداً، وإنما يستبدل بنسخة جديدة محدثة، والتغييرات كلها تحدث في ال Image الأساسية التي يبنى منها، فتختبر ثم ينشأ منها سيرفر جديد ويحذف القديم. والصورة التالية تبين الفرق:

وأنت تستخدم هذه الفكرة كل يوم دون أن تسميها إذا كنت تعمل بـ Docker، فأنت لا تدخل إلى ال Container لتحدث البرنامج داخله، وإنما تبني Image جديدة وتشغل منها Container جديداً وتحذف القديم، كما في دليل تثبيت Docker. والفكرة نفسها على مستوى السيرفر كله تحتاج إلى أداة تبني Images جاهزة للسيرفرات مثل Packer، وهي من المقالات القادمة في المرحلة. ولكن لاحظ أن هذه الطريقة لا تناسب كل شيء، فقاعدة البيانات لا تستبدلها كل أسبوع، وإنما تفصل بياناتها على قرص أو Volume مستقل يبقى عندما يستبدل السيرفر، لذلك تجد أغلب الفرق تستبدل سيرفرات التطبيق وتعدل سيرفرات البيانات بحذر.
التجهيز Provisioning وإدارة الإعدادات Configuration Management
سوف تقرأ كثيراً سؤال «OpenTofu أم Ansible؟»، والسؤال نفسه فيه خلط، لأن الأداتين تعملان في طبقتين مختلفتين:
- التجهيز Provisioning: إنشاء الأشياء نفسها عند المزود، أي السيرفر والشبكة الخاصة وجدار الحماية والقرص الإضافي وسجل DNS وال Bucket، وهذا عمل OpenTofu وTerraform.
- الإعداد الأول عند الإقلاع First Boot: أشياء تحدث مرة واحدة عند أول تشغيل للسيرفر، مثل إنشاء المستخدم ووضع مفتاح SSH، وهذا عمل cloud-init الذي تدعمه أغلب المزودات.
- إدارة الإعدادات Configuration Management: ما يوجد داخل السيرفر بعد أن ينشأ، أي الحزم والمستخدمين وملفات الإعداد والخدمات، وهذا عمل Ansible وأمثاله.
- التطبيق: ما يعمل فوق كل ذلك، وعندنا في الغالب Docker Compose أو Kubernetes.
وهذا التقسيم ليس رأينا وحدنا، فوثائق OpenTofu نفسها تقول في مقارنتها مع أدوات إدارة الإعدادات إن هذه الأدوات تثبت البرامج وتديرها على جهاز موجود من قبل، وإن OpenTofu ليس أداة لإدارة الإعدادات، وإنه يستطيع أن يجهز cloud-init ليشغل أداة الإعدادات عند أول إقلاع. والصورة التالية تبين الطبقات الأربع:

ولكي ترى كيف يبدو العمل نفسه من تجربة Bash بأداة إدارة إعدادات، هذا Playbook صغير في Ansible يصف المستخدم والمجلد والسطر نفسها، باستخدام الوحدات Modules الرسمية user وfile وlineinfile:
- 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 بالخيار
--check، الذي يعرض التغييرات المحتملة دون أن ينفذها.
والخلاصة العملية أن كثيراً من الفرق تستخدم الاثنين معاً: OpenTofu ينشئ السيرفر وسجل DNS ويمرر إلى cloud-init ما يلزم، ثم يأتي Ansible فيضبط ما بداخله، وسوف نخصص لهذا السؤال مقالاً كاملاً في المرحلة.
ومن هنا إلى GitOps
إذا جمعت كل ما سبق، أي حالة مطلوبة مكتوبة بشكل تصريحي في Git، ومراجعة لكل تغيير في طلب دمج، وأداة تطابق الواقع مع الملفات وتكشف الانحراف، فأنت قريب جداً من الطريقة التي تسمى GitOps، والفرق أن GitOps تضيف أن تكون المطابقة مستمرة وآلية، فبرنامج يراقب المستودع والبيئة طوال الوقت ويعيد البيئة إلى ما في المستودع، بدلاً من أن يشغل أحد الأداة بيده. وقد شرحنا مبادئها الأربعة وكيف تطبقها على بيئات dev وstaging والإنتاج Production في GitOps ببساطة، فلا نعيدها هنا.
ماذا بعد في هذه المرحلة؟
هذا المقال هو الأساس، والمقالات القادمة في مرحلة البنية كشيفرة تطبق كل مصطلح فيه على أداة حقيقية، وأولها Terraform أم OpenTofu؟ قصة تغيير الترخيص الذي يشرح لماذا يوجد اليوم مشروعان بالكود نفسه تقريباً وكيف تختار بينهما، ثم إنشاء أول سيرفر وسجلات 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 هي الخطوة التي تجعل المطابقة مستمرة وآلية.
دليلTerraform أم OpenTofu؟ قصة تغيير الترخيص وما يعنيه لكلماذا غير Terraform رخصته في 2023 فولد منه OpenTofu، وأين يختلف المشروعان اليوم، وكيف تختار بينهما.
دليلمن الفرع إلى الإنتاج: البيئات وGitOps للفرقكيف يختبر فريقك كل ميزة وحدها ويعرضها على العميل دون أن تصل ميزة غير جاهزة إلى الإنتاج.
دليلSecrets Scanning: كيف تكتشف الأسرار المسربة في الكودكيف يخرج المفتاح من جهازك إلى تاريخ Git وخط CI، وكيف تكتشفه قبل غيرك، وماذا تفعل في الساعة الأولى بعد تسربه.
دليلإدارة النسخ وGit للمبتدئين: لماذا نحتاجها وكيف تعمللماذا تضيع الملفات بأسماء final_v2 ويمسح الزملاء عمل بعضهم، وكيف يحل Git ذلك على مشروع حقيقي من أول commit.
دليلمصطلحات الاستضافة الذاتية Self-hosting للمبتدئينمرجع مبسط لأهم المصطلحات التي سوف تقابلها في أدلة الاستضافة الذاتية، بأسمائها الإنجليزية كما تظهر في لوحات التحكم، مع شرح قصير لكل منها.سجل التحديثات
- أكتوبر 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 دون تشغيله.