أتمتة تقرير تكلفة AWS اليومي على Slack
لو بتفتح AWS Console أول الشهر وبتكتشف فاتورة أعلى بـ 300 دولار من المتوقع، المشكلة مش في AWS — المشكلة إنك بتشوف الأرقام متأخر 30 يوم. التقرير اليومي بيوقف المفاجآت دي في 24 ساعة بدل 30.
المشكلة باختصار
Cost Explorer عند AWS بيحدّث الأرقام كل 24 ساعة تقريبًا. Budgets Alerts الرسمية بتصرفك لمّا تتعدّى حد معيّن، لكنها بتعيد التقييم كل 8–12 ساعة ومش بتديك breakdown بالخدمة. لو EC2 instance فضي اشتغل غلط، أو NAT Gateway بيستنزف 4 دولار في اليوم، الـ Budgets مش هتكشفها غير بعد ما الرقم يتراكم.
الحل اللي هنشرحه: Lambda function بتشتغل كل يوم الساعة 9 صباحًا، بتسحب تكلفة أمس مجزّأة بالخدمة، وبتبعتها على Slack. التكلفة الإجمالية للأتمتة: أقل من دولار في الشهر.
ليه يومي بدل شهري؟
تخيّل عندك موقع بيستقبل 50K زائر/يوم، وشغلت CloudFront distribution جديدة بإعداد غلط. الإعداد ده بيخلّي الـ cache hit ratio يبقى 20% بدل 85%، والـ origin بيتحمّل ضغط زيادة. النتيجة: فاتورة data transfer بتزيد من حوالي 12 دولار/يوم إلى 47 دولار/يوم.
بدون تقرير يومي، أنت هتكتشف الموضوع بعد 28 يوم وأنت بتدفع زيادة تقديرية حوالي 980 دولار. بتقرير يومي، هتلاحظ القفزة تاني يوم وتعدّل الإعداد قبل ما الرقم يتراكم.
المفهوم بأسلوب بسيط الأول
فكّر في التقرير اليومي زي كشف حساب البنك. لو البنك بيبعتلك كشف كل 30 يوم، وفيه مصاريف غريبة، الضرر وقع. لو بيبعتلك SMS بعد كل عملية، بتوقف الخطأ في ثواني. التقرير اليومي لـ AWS هو نفس الفكرة بالظبط، بس للفاتورة السحابية.
بشكل أدق: AWS Cost Explorer API بيعرض بيانات التكلفة بدقّة DAILY أو HOURLY. Lambda بتستدعي الـ API، تجمع النتايج، وتبعتها على Slack Webhook. EventBridge بيشغّل Lambda على cron يومي.
الحل — 5 خطوات
- فعّل Cost Explorer من حساب Billing (مستخدم مش admin مش هيقدر).
- اعمل IAM role لـ Lambda فيها صلاحية
ce:GetCostAndUsageوlogs:CreateLogStream. - اعمل Slack Incoming Webhook من channel إداري.
- ارفع الـ Lambda function بالكود اللي تحت.
- اربطها بـ EventBridge schedule يومية.
السكربت الكامل (Python 3.12)
import boto3
import json
import os
from datetime import date, timedelta
from urllib import request
ce = boto3.client("ce")
WEBHOOK = os.environ["SLACK_WEBHOOK_URL"]
THRESHOLD = float(os.environ.get("SPIKE_PCT", "30"))
def lambda_handler(event, context):
today = date.today()
yday = today - timedelta(days=1)
prev = today - timedelta(days=2)
def fetch(start, end):
r = ce.get_cost_and_usage(
TimePeriod={"Start": str(start), "End": str(end)},
Granularity="DAILY",
Metrics=["UnblendedCost"],
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
)
return {
g["Keys"][0]: float(g["Metrics"]["UnblendedCost"]["Amount"])
for g in r["ResultsByTime"][0]["Groups"]
}
curr = fetch(yday, today)
base = fetch(prev, yday)
rows = sorted(curr.items(), key=lambda x: -x[1])
total = sum(curr.values())
lines = [f"*AWS cost — {yday}*", f"Total: *${total:.2f}*", ""]
for svc, amt in rows[:8]:
if amt < 0.01:
continue
prev_amt = base.get(svc, 0) or 0.001
pct = ((amt - prev_amt) / prev_amt) * 100
flag = " :rotating_light:" if pct > THRESHOLD and amt > 1 else ""
lines.append(f"- {svc}: ${amt:.2f} ({pct:+.0f}%){flag}")
payload = {"text": "\n".join(lines)}
req = request.Request(
WEBHOOK,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
request.urlopen(req, timeout=10).read()
return {"ok": True, "total": total}