تصل فاتورة السحابة في بداية الشهر أعلى من الشهر السابق بثلاثين بالمئة، فيرسلها المدير المالي إلى مدير التقنية ويسأل: لماذا؟ ويرسلها مدير التقنية إلى قادة الفرق، فيقول كل فريق إنه لم يغير شيئاً كبيراً، وبعد أسبوع من الرسائل يكتشف أحدهم أن بيئة اختبار بقيت تعمل منذ ثلاثة أسابيع، وأن نصف السطور في الفاتورة لا يعرف أحد لمن هي. والمشكلة هنا ليست في السعر، فقد شرحنا الأسعار ونماذج الدفع في السحابة ليست دائماً الأرخص، والبنود التي تختبئ في الفاتورة في التكاليف الخفية في السحابة، وإنما المشكلة أن التكلفة لا صاحب لها، وأن المعلومة تصل بعد أن يصرف المال.
وهذا ما تحاول FinOps حله: أن يرى كل فريق تكلفة ما يشغله في وقتها، وأن يكون مسؤولاً عنها كما هو مسؤول عن أداء خدمته وأعطالها. وهذا المقال هو الأول في سلسلة FinOps على عرب رووت.
المواضيع في السلسلة:
- FinOps: كيف تجعل فريقك مسؤولاً عن تكلفة السحابة (الموضوع الحالي)
- أدوات FinOps: التكلفة قبل الدمج وبعد النشر
- تكلفة الذكاء الاصطناعي بالتوكنات: كيف تقيسها وتخفضها (قريباً)
وسوف نناقش في هذا المقال ما يلي:
- ما هي FinOps كما يعرفها إطار FinOps Foundation، بمراحلها ومبادئها وأدوارها.
- الوسوم Tags والتوزيع Allocation، والفرق بين عرض التكلفة Showback وتحميلها Chargeback.
- اقتصاديات الوحدة Unit Economics: تكلفة الطلب الواحد أو العميل الواحد.
- الميزانيات Budgets وتنبيهات الشذوذ Anomaly Alerts.
- الالتزامات Commitments والحجم الصحيح Rightsizing وأنماط الهدر Waste.
- مواصفة FOCUS لملفات الفواتير، ولماذا يوجد فيها أكثر من عمود للتكلفة.
- مختبر كامل: فاتورة شهر بصيغة قريبة من FOCUS نحللها مع DuckDB، فنوزعها على الفرق ونجد ما لا صاحب له ونحسب تكلفة الطلب ونكتشف شذوذاً.
- من يفعل ماذا في فريق صغير ليس فيه شخص متفرغ لهذا العمل.
ما هي FinOps؟
يعرف إطار FinOps Foundation هذا المجال بأنه إطار تشغيلي وممارسة ثقافية تزيد القيمة التي تحصل عليها المؤسسة من التقنية، وتبني المسؤولية المالية بالتعاون بين فرق الهندسة والمالية والأعمال. والمؤسسة نفسها مشروع تابع لمؤسسة Linux Foundation، وهي التي تكتب الإطار وتحدثه، ونشرت تحديثات 2026 للإطار في مارس 2026 (اطلعنا على الإطار في 13 أكتوبر 2026).
لاحظ أن التعريف لا يقول «خفض التكلفة»، وإنما يقول «القيمة»، وهذا فرق مقصود. فالفريق الذي يضاعف عدد سيرفراته لأن عدد العملاء تضاعف لا يعتبر مشكلة، والفريق الذي تتضاعف تكلفته وعدد عملائه ثابت هو المشكلة. ولكي ترى الفرق تحتاج أن تعرف من يصرف، وعلى ماذا، ومقابل ماذا.
وقد يتساءل البعض: لماذا نحتاج إطاراً كاملاً، ولماذا لا يكفي أن يراجع قسم المالية الفاتورة؟ والإجابة أن هذه هي الطريقة البسيطة الأولى، ومشكلتها في التوقيت، فالفاتورة الشهرية تصل بعد أن يصرف المال، والقسم المالي يرى «Amazon EC2 بمبلغ كذا» ولا يعرف أي سيرفر يخدم أي ميزة. والطريقة البسيطة الثانية أن يعين فريق مركزي يملك صلاحية إيقاف الموارد وفرض الحدود، ومشكلتها أنها تبطئ المهندسين وتجعلهم يرون التكلفة مسألة شخص آخر، فيطلبون الموارد بكثرة لأنهم لا يرون فاتورتها. والحل الذي يقترحه الإطار هو العكس: أن تصل البيانات في وقتها إلى من يقرر، أي المهندس الذي يختار حجم السيرفر، وأن ينظم فريق صغير العمل دون أن يملك القرار وحده.
المراحل الثلاث: Inform وOptimize وOperate
يقسم الإطار العمل إلى ثلاث مراحل تدور فيها المؤسسة بسرعة وبشكل متكرر، وليس مرة واحدة في السنة، والصورة التالية تبين الدورة والأدوار التي تشارك فيها:

- الإبلاغ Inform: أن ترى التكلفة وتوزعها على أصحابها، وتعرف ماذا تصرف ولماذا، ومنها الوسوم والتقارير والتوقعات وتكلفة الوحدة.
- التحسين Optimize: أن تجد فرص التوفير وتكتبها، مثل سيرفر أكبر من حاجته أو استخدام ثابت يستحق التزاماً بخصم.
- التشغيل Operate: أن تمكن الأشخاص المعنيين من تنفيذ التغيير، وأن تحول ما تعلمته إلى ميزانيات وتنبيهات وسياسات يومية.
والترتيب مهم، فلا تقم بشراء التزام لثلاث سنوات قبل أن تعرف من يستخدم هذه السيرفرات وهل يحتاجها أصلاً، والسبب أن التحسين دون رؤية يعني أنك تحسن ما لا تفهمه.
المبادئ الستة
يضع الإطار ستة مبادئ، ونذكرها هنا مع ما تعنيه لفريق تقني في العمل اليومي:
- الفرق تحتاج أن تتعاون: المالية والهندسة والمنتج يتكلمون عن الرقم نفسه، وليس كل فريق بجدوله الخاص.
- قيمة العمل تقود القرارات التقنية: لذلك تقيس تكلفة الوحدة وليس إجمالي الفاتورة فقط.
- كل فرد يتحمل مسؤولية استخدامه للتقنية: المهندس الذي يطلب السيرفر يرى تكلفته، كما يرى أخطاءه في السجلات.
- بيانات FinOps يجب أن تكون متاحة وفي وقتها ودقيقة: التقرير الذي يصل بعد شهر لا يغير قراراً.
- FinOps يجب أن تمكن مركزياً: فريق صغير يشتري الالتزامات ويتفاوض على الخصومات ويبني التقارير، حتى لا يكرر كل فريق العمل نفسه.
- استفد من نموذج التكلفة المتغير في السحابة: أطفئ ما لا تحتاجه، وكبر وصغر حسب الحمل، وهذه هي الميزة التي دفعت ثمنها عندما اخترت السحابة.
من يشارك؟ الأدوار Personas والمجالات Domains
يقسم الإطار الأدوار Personas إلى مجموعتين:
- ستة أدوار أساسية: ممارس FinOps، والهندسة Engineering، والمالية Finance، والقيادة Leadership، والمشتريات Procurement، والمنتج Product.
- خمسة أدوار مساندة: إدارة أصول التقنية ITAM، والإدارة المالية لتقنية المعلومات ITFM، وإدارة خدمات تقنية المعلومات ITSM، والأمن Security، والاستدامة Sustainability.
ولاحظ أن هذه أدوار وليست وظائف، ففي شركة من عشرين شخصاً قد يحمل مدير التقنية ثلاثة منها، وسوف نعود لهذا في آخر المقال.
ويقسم الإطار العمل إلى أربعة مجالات Domains فيها اثنتان وعشرون قدرة Capability:
- فهم الاستخدام والتكلفة Understand Usage & Cost: ومنها استيراد البيانات والتوزيع والتقارير وإدارة الشذوذ.
- قياس قيمة العمل Quantify Business Value: ومنها التوقع والميزانيات واقتصاديات الوحدة.
- تحسين الاستخدام والتكلفة Optimize Usage & Cost: ومنها تحسين الاستخدام وتحسين السعر Rate Optimization.
- إدارة الممارسة Manage the FinOps Practice: ومنها الحوكمة والتحميل على الفرق Chargeback والأتمتة.
ويقترح الإطار أن تبدأ صغيراً بأسلوب «Crawl, Walk, Run»، فلا تحاول تطبيق اثنتين وعشرين قدرة في الشهر الأول، وإنما ابدأ بالتوزيع والتقارير، ثم أضف ما تحتاجه.
ويستخدم الإطار مفهوم النطاقات FinOps Scopes، والنطاق جزء محدد من الصرف على فئات التقنية Technology Categories، مثل السحابة العامة وخدمات SaaS ومراكز البيانات والذكاء الاصطناعي، يرتبط بمنتج أو مركز تكلفة أو بيئة، وهذا يعني أن FinOps لم تعد للسحابة العامة وحدها. ولهذا سوف نخصص مقالاً كاملاً في هذه السلسلة لتكلفة الذكاء الاصطناعي بالتوكنات.
الوسوم Tags والتوزيع Allocation
التوزيع Allocation هو أن تنسب كل سطر في الفاتورة إلى صاحبه: فريق أو منتج أو بيئة أو عميل. وهو الأساس لكل ما بعده، فلا يمكنك أن تحاسب فريقاً على تكلفة لا تعرف أنها له. وهناك ثلاث طبقات تستخدم معاً:
- الحسابات Accounts أو الاشتراكات Subscriptions أو المشاريع Projects: أبسط طريقة وأدقها، فإذا كان للإنتاج حساب وللاختبار حساب آخر فلن تختلط تكلفتهما أبداً، حتى لو نسي أحدهم الوسوم.
- الوسوم Tags أو التسميات Labels: مفتاح وقيمة على كل مورد، مثل
team=checkoutوenv=prod، وهي التي تفصل الفرق داخل الحساب الواحد. - قواعد التوزيع: لما لا يمكن وسمه أصلاً، مثل نقل البيانات الخارج Egress وخطط الدعم والضرائب، فتوزعه بنسبة تتفق عليها مع الفرق.
والخطأ الأكثر تكراراً أن تبدأ بعشرين مفتاحاً للوسوم، فلا يلتزم بها أحد. ابدأ بثلاثة أو أربعة فقط، مثل team وenv وapp، واجعلها إلزامية عند إنشاء المورد وليس بعده، والسبب أن الوسم الذي يضاف بعد شهر لا يصلح الشهر الماضي. وفي AWS مثلاً يجب أن تفعل مفتاح الوسم في صفحة Cost allocation tags حتى يظهر في تقارير الفاتورة، وقد يحتاج ظهوره هناك إلى 24 ساعة ثم 24 ساعة أخرى للتفعيل، وحتى مع خاصية التعبئة بأثر رجعي Backfill التي تصل إلى اثني عشر شهراً، فإن المورد الذي لم يكن يحمل الوسم في ذلك الوقت يبقى في السجل بلا قيمة.
وأفضل مكان لفرض الوسوم هو الكود الذي ينشئ الموارد، فإذا كنت تستخدم البنية ككود كما شرحنا في البنية كشيفرة IaC للمبتدئين فضع الوسوم الافتراضية في إعداد المزود Provider، وافحص في خط CI/CD أن كل مورد جديد يحملها، فهذا أرخص بكثير من البحث عن أصحاب الموارد بعد شهر.
عرض التكلفة Showback وتحميلها Chargeback
بعد التوزيع تأتي خطوة إيصال الرقم إلى الفريق، وهناك طريقتان: عرض التكلفة Showback، أي تقرير دوري يقول لكل فريق كم كلف هذا الشهر دون أن يخصم من ميزانيته، وتحميل التكلفة Chargeback، أي أن المبلغ يخصم فعلاً من ميزانية الفريق أو الإدارة في الحسابات الداخلية للشركة. والصورة التالية تبين الطريق من سطر الفاتورة إلى الفريق:

وابدأ دائماً بعرض التكلفة Showback، والسبب أن التحميل Chargeback يحتاج أرقاماً دقيقة يقبلها الجميع، فإذا خصمت من ميزانية فريق مبلغاً ثم تبين أن ربعه لفريق آخر فقدت ثقة الفرق في الأرقام كلها. وعندما تصل نسبة ما لا صاحب له إلى أقل من خمسة بالمئة تقريباً، وتتفق الفرق على طريقة تقسيم المشترك، يمكنك الانتقال إلى التحميل إذا كانت الشركة تحتاجه.
والتكلفة المشتركة لها ثلاث طرق تقسيم شائعة: بالتساوي بين الفرق، أو بنسبة ثابتة تتفقون عليها، أو بنسبة التكلفة المباشرة لكل فريق، وهي الطريقة التي سوف نستخدمها في المختبر. ولا توجد طريقة صحيحة وأخرى خاطئة، وإنما المهم أن تكتبها وتتفق عليها قبل أن يرى أحد الأرقام، وإلا تحول كل تقرير إلى نقاش عن الطريقة بدلاً من الأرقام.
اقتصاديات الوحدة Unit Economics
إجمالي الفاتورة رقم لا يقول الكثير، فهل 4000 دولار في الشهر كثيرة؟ يعتمد ذلك على ما تحصل عليه مقابلها. واقتصاديات الوحدة Unit Economics أن تقسم التكلفة على مقياس من عملك يعبر عن القيمة، مثل تكلفة 1000 طلب، أو تكلفة العميل النشط في الشهر، أو تكلفة مليون طلب API، أو تكلفة الجيجابايت المخزن لكل عميل.
ولنأخذ مثالاً: متجر فاتورته ارتفعت من 4000 إلى 5000 دولار، وطلباته ارتفعت من 100 ألف إلى 140 ألف في الشهر، فتكلفة 1000 طلب انخفضت من 40 دولاراً إلى 35.7 دولار تقريباً، وهذا نمو صحي رغم أن الفاتورة زادت. أما إذا بقيت الطلبات 100 ألف فتكلفة 1000 طلب أصبحت 50 دولاراً، وهنا يجب أن تسأل عن السبب. ولهذا تحتاج بيانات من مصدرين: الفاتورة، ومقاييس المنتج التي شرحنا جمعها في مقاييس المنتج وتحليلات الاستخدام.
واختر مقياساً واحداً أو اثنين يفهمهما المدير المالي ومدير المنتج دون شرح، ولا تقم باختيار مقياس تقني مثل «تكلفة كل Pod»، والسبب أن الهدف ربط التكلفة بما يدفع العميل مقابله. وفي شركات SaaS متعددة العملاء تفيد تكلفة كل عميل Tenant كثيراً في التسعير، لأنها تكشف العميل الذي يكلفك أكثر مما يدفع، وقد شرحنا تصميم هذه الأنظمة في تعدد المستأجرين Multi-tenancy.
الميزانيات Budgets وتنبيهات الشذوذ Anomaly Alerts
الميزانية Budget هي حد شهري أو ربعي لحساب أو فريق أو وسم، مع تنبيه عند الوصول إلى نسبة منه أو عندما يتوقع المزود تجاوزه قبل نهاية الشهر. وتنبيه التوقع Forecast أهم من تنبيه التجاوز، والسبب أنه يصلك في منتصف الشهر بينما تنبيه التجاوز يصلك بعد أن يصرف المال. وفي AWS مثلاً مراقبة الميزانيات وتنبيهاتها مجانية، وأول ميزانيتين فيهما إجراءات تلقائية Actions مجانيتان كذلك، وبعدها 0.10 دولار يومياً لكل ميزانية بإجراءات (حسب صفحة الأسعار في 13 أكتوبر 2026).
أما تنبيه الشذوذ Anomaly فلا يحتاج حداً تكتبه بنفسك، وإنما يتعلم النمط المعتاد لكل خدمة أو حساب أو وسم وينبهك عندما يخرج الصرف عنه، مثل خدمة AWS Cost Anomaly Detection التي تقول في توثيقها إن التنبيهات قد تبدأ بالوصول خلال 24 ساعة من إنشاء المراقب. ولاحظ أن الشذوذ يكتشف بعد حدوثه، وأن بيانات الفاتورة نفسها تتأخر ساعات، فلا تعتمد عليه وحده لما يمكن منعه بالكود، مثل حدود الحجم في خط النشر.
والقاعدة الأهم في التنبيهات أن يصل كل تنبيه إلى الفريق الذي يستطيع التصرف، مع رابط لما تغير، وليس إلى بريد عام يقرؤه الجميع ولا يتصرف فيه أحد. وهذا هو نفس مبدأ المناوبة والملكية الذي شرحناه في من يتحمل المسؤولية عندما يتعطل الإنتاج؟، فارتفاع التكلفة المفاجئ حادثة صغيرة لها صاحب.
الالتزامات Commitments: الحجوزات وخطط التوفير
شرحنا في التكاليف الخفية في السحابة كيف تعمل الحجوزات Reserved Instances وخطط التوفير Savings Plans، وأنك تلتزم بمبلغ في الساعة لسنة أو ثلاث سنوات مقابل خصم، وتدفعه حتى لو لم تستخدمه. وما نضيفه هنا هو كيف تدير هذه الالتزامات كعمل مستمر، وليس كقرار شراء مرة واحدة:
- نسبة التغطية Coverage: كم من استخدامك المؤهل تغطيه الالتزامات. التغطية المنخفضة تعني أنك تدفع سعر الطلب On-Demand لاستخدام ثابت كان يمكن أن يكون أرخص.
- نسبة الاستغلال Utilization: كم من الالتزام الذي اشتريته تستخدمه فعلاً. الاستغلال الأقل من مئة بالمئة يعني أنك تدفع مقابل لا شيء، وهو أخطر من التغطية المنخفضة لأنه خسارة مؤكدة.
- الترتيب: احذف ما لا تحتاجه، ثم صغر ما هو أكبر من حاجته، ثم التزم بما بقي. والسبب أن الالتزام قبل الحجم الصحيح يعني أنك تلتزم بالهدر لسنة كاملة.
- الشراء على دفعات: بدلاً من التزام كبير واحد، اشتر التزامات أصغر كل شهر أو كل ربع، فتنتهي في أوقات مختلفة ويمكنك تقليلها تدريجياً إذا تغير استخدامك.
- من يقرر: المهندسون يعرفون الاستخدام الثابت في السنة القادمة، والمالية تعرف الميزانية والتدفق النقدي، والمشتريات تعرف العقود، وهذا مثال واضح على أن القرار لا يملكه دور واحد.
الحجم الصحيح Rightsizing
الحجم الصحيح Rightsizing أن يكون السيرفر أو قاعدة البيانات أو ال Pod بالحجم الذي يحتاجه الحمل الفعلي مع هامش معقول، وليس بالحجم الذي اختاره أحدهم يوم الإطلاق «احتياطاً». والطريقة البسيطة أن تنظر إلى متوسط استخدام المعالج CPU فتجده عشرة بالمئة فتصغر السيرفر إلى الربع، وهذه الطريقة تفشل لسببين: المتوسط يخفي الذروة، فالسيرفر الذي متوسطه عشرة بالمئة قد يصل إلى تسعين بالمئة ساعة في اليوم، والمعالج ليس المورد الوحيد، فالذاكرة RAM هي التي تحدد الحجم في أغلب تطبيقات الويب وقواعد البيانات.
لذلك انظر إلى النسبة المئوية 95 أو 99 على مدى أسبوعين على الأقل، وانظر إلى الذاكرة والقرص والشبكة مع المعالج. ولاحظ أن بعض المزودين لا يجمعون استخدام الذاكرة دون وكيل Agent داخل السيرفر، فخدمة AWS Compute Optimizer مثلاً تحتاج وكيل CloudWatch حتى ترى الذاكرة وتبني توصياتها عليها. وفي Kubernetes يكون الحجم الصحيح على مستوى الطلبات Requests والحدود Limits لكل Container، وهذا ما تقيسه أدوات مثل OpenCost التي نشرحها في المقال التالي.
أنماط الهدر الشائعة Waste Patterns
أغلب الهدر في الفواتير لا يأتي من قرار كبير خاطئ، وإنما من أشياء صغيرة نسيها أحدهم، ومنها:
- بيئات الاختبار التي تعمل طوال الأسبوع: الأسبوع 168 ساعة، وساعات العمل 60 ساعة تقريباً إذا كانت 12 ساعة في خمسة أيام، فإطفاء بيئة staging خارج هذه الساعات يوفر نحو 64 بالمئة من ساعات تشغيلها.
- السيرفرات التي انتهى عملها: اختبار حمل، أو تجربة لميزة ألغيت، أو سيرفر قديم بعد ترحيل.
- الأقراص غير المرتبطة Unattached Volumes: عندما تحذف السيرفر قد يبقى القرص بحسب الإعداد، ويستمر في الفاتورة شهوراً.
- النسخ Snapshots القديمة: نسخ يومية بلا سياسة حذف تتراكم حتى تصبح أكبر من البيانات نفسها.
- قواعد البيانات المضاعفة: نسخة كاملة بحجم الإنتاج لكل بيئة، بينما يكفي الاختبار حجم أصغر.
- السجلات بمستوى Debug في الإنتاج: تضاعف تكلفة جمع السجلات وتخزينها، وقد شرحنا ماذا تسجل في السجلات Logging للمطورين.
- موزعات الحمل وعناوين IP التي لا يمر بها شيء: تبقى بعد حذف الخدمة وتدفع رسومها بالساعة.
ولاحظ أن أغلب هذه الأنماط تظهر في الفاتورة كسطور بلا وسوم، لأن من ينشئ شيئاً على عجل هو نفسه من ينسى وسمه، وهذا ما سوف نراه في المختبر.
مواصفة FOCUS: لغة واحدة لملفات الفواتير
كل مزود سحابة يصدر ملف الفاتورة التفصيلي بأعمدته وأسمائه الخاصة، فإذا كانت عندك AWS وAzure معاً كتبت تقريرين مختلفين، ثم جدولاً ثالثاً يدمجهما. ومواصفة FOCUS، واختصارها FinOps Open Cost and Usage Specification، تحدد أعمدة ومعاني موحدة لبيانات التكلفة والاستخدام، فيكتب كل مزود ملفه بالأسماء نفسها. وآخر إصدار منها هو 1.4، الذي صادقت عليه لجنة FOCUS في 4 يونيو 2026، وأضاف بيانات الفواتير Invoice Detail وفترات الفوترة لمطابقة الاستخدام مع الفاتورة الرسمية.
ولاحظ أن المزودين لا يدعمون الإصدار نفسه في الوقت نفسه، فالصفحة الرئيسية لمشروع FOCUS تذكر في 13 أكتوبر 2026 أن AWS وMicrosoft Azure وGoogle Cloud تصدر بيانات بالإصدار 1.2، وأن Oracle تصدرها بالإصدار 1.0. ومن الأعمدة التي سوف تستخدمها كثيراً:
| العمود | معناه |
|---|---|
BilledCost | المبلغ كما يظهر في الفاتورة لهذا السطر |
EffectiveCost | التكلفة بعد توزيع الالتزامات المدفوعة مقدماً على الاستخدام الذي استفاد منها |
ListCost | التكلفة بسعر القائمة العام قبل أي خصم |
ChargeCategory | نوع السطر: استخدام Usage، أو شراء Purchase، أو ضريبة Tax، أو رصيد Credit، أو تعديل Adjustment |
العمودان ServiceName وServiceCategory | الخدمة وفئتها، مثل Compute وStorage وNetworking |
SubAccountId | الحساب أو الاشتراك أو المشروع الذي يتبعه السطر |
ResourceId | معرف المورد، وقد يكون فارغاً لسطور لا ترتبط بمورد |
Tags | الوسوم كقيمة JSON |
CommitmentDiscountId | الالتزام الذي غطى هذا السطر إن وجد |
وأهم ما يجب أن تفهمه هو الفرق بين BilledCost وEffectiveCost، والصورة التالية تبينه بمثال من المختبر: خطة توفير تغطي أربعة سيرفرات لفريق checkout:

لذلك وزع التكلفة على الفرق باستخدام EffectiveCost دائماً، واستخدم BilledCost فقط عندما تطابق الأرقام مع الفاتورة التي تدفعها المالية.
مختبر: تحليل فاتورة شهر كامل مع DuckDB
الآن سوف نطبق كل ما سبق على فاتورة شهر كامل. ولأننا لن ننشئ حساباً سحابياً لهذا المقال، ولأن فواتير الشركات الحقيقية لا تنشر، فقد كتبنا سكربتاً يولد فاتورة مصطنعة بالكامل لمتجر إلكتروني خلال سبتمبر 2026، بأعمدة مأخوذة من مواصفة FOCUS 1.4 (جزء منها فقط). وأسعار السيرفرات في الملف هي أسعار AWS العامة لمنطقة فرانكفورت eu-central-1 كما ظهرت في قائمة الأسعار العامة يوم 13 أكتوبر 2026، وبقية الأرقام من اختيارنا للتمرين، فلا تستخدم هذا الملف مرجعاً للأسعار.
واخترنا DuckDB لأنه قاعدة بيانات تحليلية تعمل داخل العملية نفسها دون سيرفر، وتقرأ ملفات CSV وParquet مباشرة بلغة SQL، وهذا يناسب ملفات الفواتير التي تصل عادة بهذه الصيغ. ونشغل كل شيء داخل Container من Python 3.13.16 مع DuckDB 1.5.6.
الفاتورة المصطنعة
السكربت التالي يولد ملفين: الفاتورة billing_2026-09.csv، ومقاييس المتجر store_metrics_2026-09.csv (عدد الطلبات والعملاء النشطين لكل يوم):
"""Generates a SYNTHETIC billing export in a FOCUS-like shape (a subset of FOCUS 1.4 columns).
Nothing here comes from a real account. Compute hourly rates are AWS eu-central-1 on-demand
Linux prices (public price list, 13 Oct 2026); every other number is invented for the exercise.
"""
import csv, json, random
from datetime import date, timedelta
random.seed(42)
START = date(2026, 9, 1)
DAYS = 30
PROD, STAGING = "111111111111", "222222222222"
# (resource id, service, category, account, hourly price, tags or None)
INSTANCES = [
*[(f"i-chk-{n}", "m7g.large", PROD, 0.0978, {"team": "checkout", "env": "prod", "app": "store"}) for n in range(1, 5)],
*[(f"i-cat-{n}", "m7g.xlarge", PROD, 0.1955, {"team": "catalog", "env": "prod", "app": "store"}) for n in range(1, 4)],
*[(f"i-srch-{n}", "r7g.xlarge", PROD, 0.2584, {"team": "search", "env": "prod", "app": "store"}) for n in range(1, 3)],
*[(f"i-plat-{n}", "t4g.medium", PROD, 0.0384, {"team": "platform", "env": "prod"}) for n in range(1, 4)],
("i-old-worker-1", "m7g.large", PROD, 0.0978, None), # nobody remembers who started these
("i-old-worker-2", "m7g.large", PROD, 0.0978, None),
*[(f"i-stg-{n}", "t4g.medium", STAGING, 0.0384, {"team": "checkout", "env": "staging", "app": "store"}) for n in range(1, 3)],
]
SP_ID = "sp-compute-1y" # a 1-year Compute Savings Plan that covers the four checkout servers
SP_RATE = 0.0745 # committed rate per covered m7g.large hour
SP_HOURLY = 4 * SP_RATE
rows = []
def row(day, service, category, account, rid, name, billed, effective, listc, qty, unit, tags,
charge="Usage", commit_id="", commit_status=""):
rows.append({
"BillingPeriodStart": "2026-09-01", "ChargePeriodStart": day.isoformat(),
"ChargePeriodEnd": (day + timedelta(days=1)).isoformat(),
"ServiceProviderName": "AWS", "SubAccountId": account, "RegionId": "eu-central-1",
"ServiceName": service, "ServiceCategory": category, "ChargeCategory": charge,
"ResourceId": rid, "ResourceName": name,
"ConsumedQuantity": round(qty, 3), "ConsumedUnit": unit,
"ListCost": round(listc, 4), "BilledCost": round(billed, 4), "EffectiveCost": round(effective, 4),
"CommitmentDiscountId": commit_id, "CommitmentDiscountStatus": commit_status,
"Tags": json.dumps(tags or {}, separators=(",", ":")),
})
for i in range(DAYS):
d = START + timedelta(days=i)
for rid, itype, acct, price, tags in INSTANCES:
cost = 24 * price
if rid.startswith("i-chk-"): # covered by the Savings Plan: billed 0, effective = committed rate
row(d, "Amazon EC2", "Compute", acct, rid, itype, 0.0, 24 * SP_RATE, cost, 24, "Hours", tags,
commit_id=SP_ID, commit_status="Used")
else:
row(d, "Amazon EC2", "Compute", acct, rid, itype, cost, cost, cost, 24, "Hours", tags)
# the Savings Plan fee itself: a Purchase line, billed every day, effective 0 (it is spread over the usage above)
row(d, "AWS Savings Plans", "Compute", PROD, SP_ID, "Compute Savings Plan", 24 * SP_HOURLY, 0.0, 0.0, 24, "Hours",
None, charge="Purchase", commit_id=SP_ID)
# Managed databases
for rid, team, daily in (("db-checkout", "checkout", 11.52), ("db-catalog", "catalog", 7.68)):
row(d, "Amazon RDS", "Databases", PROD, rid, rid, daily, daily, daily, 24, "Hours",
{"team": team, "env": "prod", "app": "store"})
# Object storage for product photos
gb = 3800 + 12 * i
row(d, "Amazon S3", "Storage", PROD, "s3-store-media", "store-media", gb * 0.0245 / 30, gb * 0.0245 / 30,
gb * 0.0245 / 30, gb, "GB-Month", {"team": "catalog", "env": "prod", "app": "store"})
# Two volumes left behind after an old migration, attached to nothing
for rid in ("vol-0a11", "vol-0b22"):
row(d, "Amazon EBS", "Storage", PROD, rid, rid, 500 * 0.0952 / 30, 500 * 0.0952 / 30, 500 * 0.0952 / 30,
500, "GB-Month", None)
# Shared network lines that carry no team tag
weekend = d.weekday() >= 5
egress_gb = random.uniform(380, 430) * (0.8 if weekend else 1.0)
row(d, "AWS Data Transfer", "Networking", PROD, "", "internet egress", egress_gb * 0.09, egress_gb * 0.09,
egress_gb * 0.09, egress_gb, "GB", None)
nat_gb = random.uniform(140, 170)
row(d, "Amazon VPC", "Networking", PROD, "nat-0prod", "nat-gateway", 1.08 + nat_gb * 0.052,
1.08 + nat_gb * 0.052, 1.08 + nat_gb * 0.052, nat_gb, "GB", None)
row(d, "Elastic Load Balancing", "Networking", PROD, "alb-store", "alb-store", 0.648 + 1.9, 0.648 + 1.9,
0.648 + 1.9, 24, "Hours", {"team": "platform", "env": "prod", "app": "store"})
logs_gb = random.uniform(9, 11)
row(d, "Amazon CloudWatch", "Management and Governance", PROD, "logs-store", "log ingestion",
logs_gb * 0.63, logs_gb * 0.63, logs_gb * 0.63, logs_gb, "GB", {"team": "platform", "env": "prod"})
# From 21 Sep: a load test in staging leaves eight large servers running until the end of the month
if d >= date(2026, 9, 21):
for n in range(1, 9):
tags = {"team": "search", "env": "staging"} if n <= 6 else None # two were started by hand, untagged
cost = 24 * 0.3910
row(d, "Amazon EC2", "Compute", STAGING, f"i-loadtest-{n}", "m7g.2xlarge", cost, cost, cost, 24,
"Hours", tags)
with open("billing_2026-09.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader(); w.writerows(rows)
# Business metrics from the application side (also synthetic): orders and active customers per day
with open("store_metrics_2026-09.csv", "w", newline="") as f:
w = csv.writer(f); w.writerow(["day", "orders", "active_customers"])
for i in range(DAYS):
d = START + timedelta(days=i)
weekend = d.weekday() >= 5
orders = int(random.gauss(4200, 150) * (1.25 if weekend else 1.0))
w.writerow([d.isoformat(), orders, int(orders * random.uniform(0.62, 0.66))])
print(len(rows), "billing rows written")في الإعداد أعلاه لاحظ التالي:
- في الفاتورة حسابان: الإنتاج
111111111111والاختبار222222222222، وهذا هو التوزيع بالحسابات الذي ذكرناه. - سيرفرات فريق checkout تغطيها خطة توفير، فسطرها في
BilledCostصفر، ويظهر رسم الخطة في سطر مستقل نوعهPurchase. - سيرفران قديمان وقرصان غير مرتبطين بلا وسوم، وهي من أنماط الهدر التي ذكرناها.
- نقل البيانات الخارج و NAT Gateway بلا وسوم، لأنها مشتركة بين الفرق.
- من يوم 21 سبتمبر تبقى ثمانية سيرفرات كبيرة تعمل في حساب الاختبار بعد اختبار حمل، ستة منها بوسوم واثنان أنشئا يدوياً بلا وسوم.
وملف الاستعلامات analyze.sql فيه كل استعلام باسم، والسكربت run.py يشغلها بالترتيب:
import re, sys, duckdb
con = duckdb.connect()
parts = re.split(r"^-- name: (\w+)\n", open("analyze.sql").read(), flags=re.M)[1:]
only = sys.argv[1:]
for name, sql in zip(parts[::2], parts[1::2]):
if name == "load":
con.execute(sql); continue
if only and name not in only:
continue
print(f"== {name}")
con.sql(sql).show(max_width=200)والآن نشغل Container ونثبت فيه DuckDB، ثم نولد البيانات:
docker run -d --name lab-fin-duckdb -v "$PWD:/lab" -w /lab python:3.13.16-slim sleep infinity
docker exec lab-fin-duckdb pip install -q --root-user-action=ignore duckdb==1.5.6
docker exec lab-fin-duckdb python gen_billing.py
docker exec lab-fin-duckdb sh -c "head -3 billing_2026-09.csv; wc -l *.csv"والمخرج سوف يكون كما يلي:
860 billing rows written
BillingPeriodStart,ChargePeriodStart,ChargePeriodEnd,ServiceProviderName,SubAccountId,RegionId,ServiceName,ServiceCategory,ChargeCategory,ResourceId,ResourceName,ConsumedQuantity,ConsumedUnit,ListCost,BilledCost,EffectiveCost,CommitmentDiscountId,CommitmentDiscountStatus,Tags
2026-09-01,2026-09-01,2026-09-02,AWS,111111111111,eu-central-1,Amazon EC2,Compute,Usage,i-chk-1,m7g.large,24,Hours,2.3472,0.0,1.788,sp-compute-1y,Used,"{""team"":""checkout"",""env"":""prod"",""app"":""store""}"
2026-09-01,2026-09-01,2026-09-02,AWS,111111111111,eu-central-1,Amazon EC2,Compute,Usage,i-chk-2,m7g.large,24,Hours,2.3472,0.0,1.788,sp-compute-1y,Used,"{""team"":""checkout"",""env"":""prod"",""app"":""store""}"
861 billing_2026-09.csv
31 store_metrics_2026-09.csv
892 totalلاحظ في السطرين الأولين أن ListCost هو 2.3472 دولار لليوم (24 ساعة بسعر 0.0978)، وأن BilledCost صفر، وأن EffectiveCost هو 1.788 دولار، أي السعر الملتزم به في خطة التوفير.
العمودان BilledCost وEffectiveCost
أول استعلام في الملف يحمل البيانات ويستخرج الوسمين team وenv من عمود JSON، والثاني يقارن العمودين حسب نوع السطر:
-- name: load
CREATE TABLE bill AS
SELECT *, json_extract_string(Tags, '$.team') AS team, json_extract_string(Tags, '$.env') AS env
FROM read_csv('billing_2026-09.csv', header = true);
CREATE TABLE metrics AS SELECT * FROM read_csv('store_metrics_2026-09.csv', header = true);
-- name: billed_vs_effective
SELECT ChargeCategory,
round(sum(BilledCost), 2) AS billed,
round(sum(EffectiveCost), 2) AS effective
FROM bill GROUP BY ALL ORDER BY ChargeCategory;والمخرج سوف يكون كما يلي:
┌────────────────┬─────────┬───────────┐
│ ChargeCategory │ billed │ effective │
│ varchar │ double │ double │
├────────────────┼─────────┼───────────┤
│ Purchase │ 214.56 │ 0.0 │
│ Usage │ 4164.72 │ 4379.28 │
└────────────────┴─────────┴───────────┘المجموع في العمودين واحد، وهو 4379.28 دولار، ولكن BilledCost يضع 214.56 دولار في سطر الشراء، بينما EffectiveCost يضعها صفراً ويوزعها على الاستخدام. والآن نرى أثر ذلك على الفرق.
التوزيع على الفرق
-- name: by_team
SELECT coalesce(team, '(untagged)') AS team,
round(sum(BilledCost), 2) AS billed,
round(sum(EffectiveCost), 2) AS effective,
round(100 * sum(EffectiveCost) / sum(sum(EffectiveCost)) OVER (), 1) AS pct
FROM bill GROUP BY ALL ORDER BY effective DESC;والمخرج سوف يكون كما يلي:
┌────────────┬─────────┬───────────┬────────┐
│ team │ billed │ effective │ pct │
│ varchar │ double │ double │ double │
├────────────┼─────────┼───────────┼────────┤
│ (untagged) │ 1944.58 │ 1730.02 │ 39.5 │
│ search │ 935.14 │ 935.14 │ 21.4 │
│ catalog │ 750.04 │ 750.04 │ 17.1 │
│ checkout │ 400.9 │ 615.46 │ 14.1 │
│ platform │ 348.63 │ 348.63 │ 8.0 │
└────────────┴─────────┴───────────┴────────┘في المخرج أعلاه لاحظ التالي:
- فريق checkout يبدو بعمود
billedأنه يكلف 400.90 دولار، وتكلفته الحقيقية 615.46 دولار، والفرق هو رسم خطة التوفير الذي ذهب إلى ما لا صاحب له. ولو وزعت بالعمود الخطأ لظن الفريق أنه الأرخص. - 39.5 بالمئة من التكلفة بلا وسم فريق، وهذه النسبة وحدها تكفي لتعرف أن التوزيع غير صالح لتحميل التكلفة Chargeback بعد.
ما لا صاحب له
-- name: untagged
SELECT ServiceName, ResourceId, ResourceName,
round(sum(EffectiveCost), 2) AS cost
FROM bill
WHERE team IS NULL AND ChargeCategory = 'Usage'
GROUP BY ALL ORDER BY cost DESC;والمخرج سوف يكون كما يلي:
┌───────────────────┬────────────────┬─────────────────┬─────────┐
│ ServiceName │ ResourceId │ ResourceName │ cost │
│ varchar │ varchar │ varchar │ double │
├───────────────────┼────────────────┼─────────────────┼─────────┤
│ AWS Data Transfer │ NULL │ internet egress │ 1037.81 │
│ Amazon VPC │ nat-0prod │ nat-gateway │ 268.49 │
│ Amazon EC2 │ i-loadtest-7 │ m7g.2xlarge │ 93.84 │
│ Amazon EC2 │ i-loadtest-8 │ m7g.2xlarge │ 93.84 │
│ Amazon EC2 │ i-old-worker-2 │ m7g.large │ 70.42 │
│ Amazon EC2 │ i-old-worker-1 │ m7g.large │ 70.42 │
│ Amazon EBS │ vol-0a11 │ vol-0a11 │ 47.6 │
│ Amazon EBS │ vol-0b22 │ vol-0b22 │ 47.6 │
└───────────────────┴────────────────┴─────────────────┴─────────┘وهذه القائمة هي جدول عمل الأسبوع، وفيها نوعان مختلفان تماماً: سطور مشتركة لا يمكن وسمها أصلاً، وهي نقل البيانات و NAT Gateway بمبلغ 1306.30 دولار، وهذه تحتاج قاعدة تقسيم وليس بحثاً عن صاحب، وسطور كان يجب أن تحمل وسوماً: سيرفرا اختبار الحمل، والسيرفران القديمان، والقرصان غير المرتبطين، بمجموع 423.72 دولار، وهذه تحتاج أن تسأل عنها في قناة الفريق ثم تحذفها أو توسمها.
تقرير عرض التكلفة Showback
نقسم الآن التكلفة المشتركة لحساب الإنتاج على الفرق بنسبة تكلفتها المباشرة:
-- name: showback
-- Production only. Shared network lines (egress, NAT) are split by each team's share of direct cost.
WITH prod AS (SELECT * FROM bill WHERE SubAccountId = 111111111111 AND ChargeCategory = 'Usage'),
direct AS (SELECT team, sum(EffectiveCost) AS direct FROM prod WHERE team IS NOT NULL GROUP BY team),
shared AS (SELECT sum(EffectiveCost) AS shared FROM prod
WHERE team IS NULL AND ServiceCategory = 'Networking')
SELECT team,
round(direct, 2) AS direct,
round(shared * direct / sum(direct) OVER (), 2) AS shared_part,
round(direct + shared * direct / sum(direct) OVER (), 2) AS showback
FROM direct, shared ORDER BY showback DESC;والمخرج سوف يكون كما يلي:
┌──────────┬────────┬─────────────┬──────────┐
│ team │ direct │ shared_part │ showback │
│ varchar │ double │ double │ double │
├──────────┼────────┼─────────────┼──────────┤
│ catalog │ 750.04 │ 482.43 │ 1232.47 │
│ checkout │ 560.16 │ 360.3 │ 920.46 │
│ search │ 372.1 │ 239.33 │ 611.43 │
│ platform │ 348.63 │ 224.24 │ 572.87 │
└──────────┴────────┴─────────────┴──────────┘وهذا هو الجدول الذي يرسل إلى قادة الفرق في نهاية الشهر. لاحظ أن التكلفة المشتركة أكبر من نصف التكلفة المباشرة هنا، فنقل البيانات وحده أكثر من ألف دولار، ولذلك فإن طريقة التقسيم تغير ترتيب الفرق، ولهذا يجب أن تتفقوا عليها مكتوبة قبل إرسال أول تقرير. فلو قسمنا بالتساوي مثلاً لتحمل فريق platform الصغير 326.58 دولار من نقل البيانات بدلاً من 224.24 دولار.
تكلفة 1000 طلب
نربط الفاتورة بمقاييس المتجر، ونحسب تكلفة 1000 طلب لكل أسبوع كامل، مرة لحساب الإنتاج وحده ومرة لكل الحسابات:
-- name: unit_cost
-- Cost per 1,000 orders by full week: production only, then every account (production + staging)
WITH daily AS (
SELECT ChargePeriodStart AS day,
sum(EffectiveCost) FILTER (WHERE SubAccountId = 111111111111) AS prod_cost,
sum(EffectiveCost) AS all_cost
FROM bill GROUP BY day)
SELECT date_trunc('week', d.day)::DATE AS week,
sum(m.orders) AS orders,
round(1000 * sum(d.prod_cost) / sum(m.orders), 2) AS prod_per_1k,
round(1000 * sum(d.all_cost) / sum(m.orders), 2) AS all_per_1k,
round(100 * sum(d.all_cost) / sum(m.active_customers), 2) AS cents_per_customer
FROM daily d JOIN metrics m ON m.day = d.day
GROUP BY week HAVING count(*) = 7 ORDER BY week;والمخرج سوف يكون كما يلي:
┌────────────┬────────┬─────────────┬────────────┬────────────────────┐
│ week │ orders │ prod_per_1k │ all_per_1k │ cents_per_customer │
│ date │ int128 │ double │ double │ double │
├────────────┼────────┼─────────────┼────────────┼────────────────────┤
│ 2026-09-07 │ 31779 │ 26.42 │ 26.83 │ 4.2 │
│ 2026-09-14 │ 31549 │ 26.28 │ 26.69 │ 4.15 │
│ 2026-09-21 │ 31638 │ 26.42 │ 43.43 │ 6.75 │
└────────────┴────────┴─────────────┴────────────┴────────────────────┘والصورة التالية تبين نفس الأرقام:

وهنا فائدة اقتصاديات الوحدة: تكلفة الإنتاج للطلب ثابتة عند 26.4 دولار لكل 1000 طلب، أي أن المنتج نفسه لم يصبح أغلى، بينما تكلفة الشركة للطلب ارتفعت من 26.69 إلى 43.43 دولار. ولو نظرت إلى تكلفة الإنتاج وحدها لما رأيت المشكلة، ولو نظرت إلى إجمالي الفاتورة لما عرفت هل الزيادة نمو أم هدر.
اكتشاف الشذوذ Anomaly
نقارن الآن تكلفة كل يوم لكل حساب وخدمة بمتوسط الأيام السبعة التي قبله، ونعتبره شذوذاً إذا زاد عن المتوسط بخمسين بالمئة وبأكثر من 20 دولاراً معاً، والشرط الثاني حتى لا ننبه على خدمة تكلفتها دولار واحد ارتفعت إلى دولارين:
-- name: anomaly
-- Daily cost per account and service against the average of the 7 days before it
WITH daily AS (
SELECT ChargePeriodStart AS day, SubAccountId AS account, ServiceName AS service,
sum(EffectiveCost) AS cost
FROM bill WHERE ChargeCategory = 'Usage' GROUP BY ALL),
w AS (
SELECT *, avg(cost) OVER (PARTITION BY account, service ORDER BY day
ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) AS baseline
FROM daily)
SELECT day, account, service, round(baseline, 2) AS baseline, round(cost, 2) AS cost,
round(cost / baseline, 1) AS x
FROM w
WHERE day > DATE '2026-09-07' AND cost > 1.5 * baseline AND cost - baseline > 20
ORDER BY day;والمخرج سوف يكون كما يلي:
┌────────────┬──────────────┬────────────┬──────────┬────────┬────────┐
│ day │ account │ service │ baseline │ cost │ x │
│ date │ int64 │ varchar │ double │ double │ double │
├────────────┼──────────────┼────────────┼──────────┼────────┼────────┤
│ 2026-09-21 │ 222222222222 │ Amazon EC2 │ 1.84 │ 76.92 │ 41.7 │
│ 2026-09-22 │ 222222222222 │ Amazon EC2 │ 12.57 │ 76.92 │ 6.1 │
│ 2026-09-23 │ 222222222222 │ Amazon EC2 │ 23.29 │ 76.92 │ 3.3 │
│ 2026-09-24 │ 222222222222 │ Amazon EC2 │ 34.02 │ 76.92 │ 2.3 │
│ 2026-09-25 │ 222222222222 │ Amazon EC2 │ 44.74 │ 76.92 │ 1.7 │
└────────────┴──────────────┴────────────┴──────────┴────────┴────────┘ونرى من أين جاء الشذوذ:
-- name: anomaly_detail
SELECT coalesce(team, '(untagged)') AS team, env, ResourceName, count(DISTINCT ResourceId) AS servers,
min(ChargePeriodStart) AS first_day, round(sum(EffectiveCost), 2) AS cost_so_far
FROM bill WHERE ResourceId LIKE 'i-loadtest-%' GROUP BY ALL;┌────────────┬─────────┬──────────────┬─────────┬────────────┬─────────────┐
│ team │ env │ ResourceName │ servers │ first_day │ cost_so_far │
│ varchar │ varchar │ varchar │ int64 │ date │ double │
├────────────┼─────────┼──────────────┼─────────┼────────────┼─────────────┤
│ search │ staging │ m7g.2xlarge │ 6 │ 2026-09-21 │ 563.04 │
│ (untagged) │ NULL │ m7g.2xlarge │ 2 │ 2026-09-21 │ 187.68 │
└────────────┴─────────┴──────────────┴─────────┴────────────┴─────────────┘الاستعلام وجد الشذوذ في اليوم الأول نفسه، 21 سبتمبر، بتكلفة 41.7 ضعف المعتاد في حساب الاختبار، ووجد أن ستة سيرفرات تحمل وسم فريق search، فيعرف التنبيه إلى من يذهب، وأن اثنين بلا وسوم، وهذان سوف يبقيان دون صاحب حتى يسأل أحد عنهما.
نكسر الطريقة: الشذوذ يصبح هو المعتاد
لاحظ في مخرج الشذوذ شيئاً مهماً: الاستعلام نبه خمسة أيام فقط، من 21 إلى 25 سبتمبر، ثم توقف رغم أن السيرفرات الثمانية ما زالت تعمل حتى نهاية الشهر. والسبب أن المتوسط المتحرك للأيام السبعة السابقة يتعلم الشذوذ، فكل يوم يدخل فيه يوم مرتفع جديد حتى يصبح 76.92 دولار هو المعتاد، ولا يعود اليوم التالي مختلفاً عنه. وهذه مشكلة حقيقية في كل طريقة تعتمد على الماضي القريب، فإذا لم يتصرف أحد في الأيام الأولى فإن النظام يقبل الهدر ويسكت.
والحل أن تجمع بين طريقتين: تنبيه الشذوذ لما لا تتوقعه، وميزانية ثابتة لكل حساب لا تتعلم من الماضي. والاستعلام التالي يضع لحساب الاختبار ميزانية 100 دولار في الشهر، ويحسب لكل يوم المجموع حتى ذلك اليوم وتوقعاً بسيطاً لنهاية الشهر:
-- name: budget
-- Staging has a monthly budget of $100. Running total and a simple month-end forecast for each day.
WITH daily AS (
SELECT ChargePeriodStart AS day, sum(EffectiveCost) AS cost
FROM bill WHERE SubAccountId = 222222222222 GROUP BY day),
run AS (
SELECT day, cost, sum(cost) OVER (ORDER BY day) AS to_date FROM daily)
SELECT day, round(cost, 2) AS cost, round(to_date, 2) AS to_date,
round(to_date / dayofmonth(day) * 30, 2) AS forecast,
CASE WHEN to_date > 100 THEN 'OVER BUDGET'
WHEN to_date / dayofmonth(day) * 30 > 100 THEN 'FORECAST OVER' ELSE '' END AS alert
FROM run WHERE day BETWEEN DATE '2026-09-19' AND DATE '2026-09-23' ORDER BY day;والمخرج سوف يكون كما يلي:
┌────────────┬────────┬─────────┬──────────┬─────────────┐
│ day │ cost │ to_date │ forecast │ alert │
│ date │ double │ double │ double │ varchar │
├────────────┼────────┼─────────┼──────────┼─────────────┤
│ 2026-09-19 │ 1.84 │ 35.02 │ 55.3 │ │
│ 2026-09-20 │ 1.84 │ 36.86 │ 55.3 │ │
│ 2026-09-21 │ 76.92 │ 113.78 │ 162.54 │ OVER BUDGET │
│ 2026-09-22 │ 76.92 │ 190.69 │ 260.04 │ OVER BUDGET │
│ 2026-09-23 │ 76.92 │ 267.61 │ 349.06 │ OVER BUDGET │
└────────────┴────────┴─────────┴──────────┴─────────────┘والميزانية الثابتة تبقى تنبه كل يوم حتى نهاية الشهر لأن المجموع لا ينخفض، وهذا ما تريده. ولاحظ أن التوقع هنا بسيط جداً، فهو يفترض أن متوسط الأيام الماضية يستمر، ولذلك لم ينبه قبل يوم 21، بينما أدوات المزودين تستخدم نماذج أدق، ولكن الفكرة واحدة.
وفي الأخير نحذف ال Container الذي أنشأناه:
docker rm -f lab-fin-duckdbمن يفعل ماذا في فريق صغير؟
أغلب الشركات العربية الناشئة والمتوسطة لا يوجد فيها فريق FinOps، ولا تحتاجه، وإنما تحتاج أن توزع الأدوار على من هو موجود. والجدول التالي توزيع عملي لشركة فيها فريقان أو ثلاثة فرق هندسية:
| العمل | من يقوم به | كل متى |
|---|---|---|
| سياسة الوسوم وفرضها في كود البنية | مهندس المنصة أو DevOps | مرة، ثم مراجعة كل ربع |
| تقرير عرض التكلفة لكل فريق | مهندس المنصة، يرسله آلياً | كل أسبوع أو كل شهر |
| مراجعة قائمة ما لا صاحب له | قادة الفرق في اجتماع قصير | كل أسبوعين |
| استقبال تنبيهات الشذوذ والميزانية | الفريق صاحب الحساب أو الوسم | عند حدوثها |
| الحجم الصحيح وحذف الهدر | كل فريق لخدماته | كل شهر |
| شراء الالتزامات | مدير التقنية مع المالية | كل ربع |
| تكلفة الوحدة والتوقع للسنة | المالية مع مدير المنتج | كل شهر |
ولا يحتاج أي من هذه الأعمال أداة مدفوعة في البداية، فملف الفاتورة واستعلامات مثل التي في المختبر تكفي، وعندما يكبر العمل فإن الأدوات التي نشرحها في أدوات FinOps توفر الوقت. وإذا كان القرار يحتاج إقناع الإدارة أو فرق أخرى، فقد شرحنا طريقة عرضه في مواءمة أصحاب المصلحة.
أخطاء شائعة
- البدء بالأداة قبل الوسوم: أي أداة ترسم لك رسماً جميلاً لما لا صاحب له، وتبقى المشكلة نفسها.
- التوزيع بعمود الفاتورة BilledCost: يجعل الفريق الذي تغطيه الالتزامات يبدو مجانياً، ويضع الخصم في سطر بلا صاحب.
- التحميل Chargeback قبل أن تثق الفرق بالأرقام: أول خطأ في الخصم يجعل كل تقرير بعده نقاشاً عن الأرقام.
- الاعتماد على تنبيه الشذوذ وحده: كما رأينا، الشذوذ الذي لا يعالج في أيامه الأولى يصبح هو المعتاد.
- شراء الالتزامات قبل الحجم الصحيح: تلتزم بالهدر لسنة كاملة.
- قياس التوفير بدل القيمة: خفض الفاتورة عشرين بالمئة ليس إنجازاً إذا أبطأ الإصدارات أو أسقط الخدمة، والمقياس الصحيح تكلفة الوحدة.
- التقرير الذي لا يقرؤه أحد: التقرير يجب أن يصل إلى من يستطيع التصرف، مع ما تغير عن الأسبوع الماضي، وليس جدولاً من مئة سطر.
تدريب عملي
- غير في
gen_billing.pyوسوم السيرفرين القديمين إلىteam=catalog، ثم أعد تشغيلrun.pyولاحظ كيف تتغير نسبة ما لا صاحب له وتقرير عرض التكلفة. - غير استعلام
showbackليقسم التكلفة المشتركة بالتساوي بين الفرق الأربعة، وقارن ترتيب الفرق بين الطريقتين. - اكتب استعلاماً يجد كل مورد ظهر في الفاتورة لأول مرة خلال آخر سبعة أيام، فهذه هي الموارد التي تستحق السؤال قبل أن تصبح جزءاً من المعتاد.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- FinOps ممارسة تجعل كل فريق يرى تكلفة ما يشغله في وقتها ويتحمل مسؤوليتها، وهدفها القيمة وليس خفض الفاتورة فقط.
- المراحل ثلاث تتكرر: الرؤية Inform ثم التحسين Optimize ثم التشغيل Operate، ولا تحسن ما لا تراه.
- التوزيع يبدأ بحسابات منفصلة للبيئات، ثم وسوم قليلة إلزامية عند الإنشاء، ثم قاعدة مكتوبة لتقسيم المشترك.
- ابدأ بعرض التكلفة Showback، وانتقل إلى التحميل Chargeback فقط عندما تثق الفرق بالأرقام.
- تكلفة الوحدة تخبرك هل الزيادة نمو أم هدر، وإجمالي الفاتورة لا يخبرك بذلك.
- وزع دائماً بعمود EffectiveCost في مواصفة FOCUS، واستخدم BilledCost للمطابقة مع الفاتورة فقط.
- اجمع بين تنبيه الشذوذ والميزانية الثابتة، لأن الشذوذ الذي يستمر يصبح هو المعتاد.
وفي المقال القادم بإذن الله سوف نتحدث عن أدوات FinOps: كيف ترى تكلفة التغيير في طلب الدمج قبل أن ينشر، وكيف توزع تكلفة Kubernetes على ال Namespaces.
سجل التحديثات
- أكتوبر 2026: كتابة المقال، والاطلاع على إطار FinOps Foundation ومواصفة FOCUS 1.4 وصفحات AWS المذكورة بتاريخ 13 أكتوبر 2026، وتشغيل المختبر على Python 3.13.16 وDuckDB 1.5.6 ببيانات مصطنعة.