لو فاتورة AWS عندك بتزيد كل شهر من غير ما الترافيك يزيد، غالبًا انت بتدفع لموارد محدش بيستخدمها. سكربت واحد بيكشفها ويوفّر مئات الدولارات سنويًا بدون ما تلمس أي workload شغّال.
المستوى المطلوب: محترف. الشرح بيفترض إن عندك حساب AWS، صلاحيات IAM للقراءة على EC2، ومعرفة أساسية بـ Python و boto3.
أتمتة كشف موارد AWS اليتيمة وتقليل الهدر في الفاتورة
المشكلة باختصار
الهدر في السحابة نادرًا بيجي من سيرفر غالي. بيجي من حاجات صغيرة منسية بتتراكم. مهندس عمل terminate لـ EC2 instance، بس الـ EBS volume فضل موجود. حد حجز Elastic IP لتجربة وسابه. lifecycle policy اتلغت فتراكمت اللقطات (snapshots). كل واحدة لوحدها رخيصة. مجموعهم بيبقى بند ثابت في الفاتورة كل شهر.
المشكلة إن AWS مش بتنبهك على ده. الفاتورة بتيجي إجمالية، والـ Cost Explorer بيوريك التصنيف مش "إيه اللي مش مستخدم". لازم حد يدور بنفسه، وده بالظبط اللي بنأتمته هنا.
يعني إيه "مورد يتيم"؟
تخيّل إنك استأجرت مخزن، فضّيته من 6 شهور، ونسيت تلغي الاشتراك. كل شهر بيتخصم منك إيجار مخزن فاضي. انت مش بتاخد أي قيمة، بس الفلوس بتمشي. ده بالظبط المورد اليتيم.
علميًا: المورد اليتيم (orphaned resource) هو مورد provisioned في الحساب، لسه AWS بتحاسبك عليه، لكنه مش مرتبط بأي workload شغّال. أشهر 3 أنواع: EBS volumes في حالة available (مش متركّبة على أي instance)، Elastic IPs غير مرتبطة بـ instance أو ENI، وsnapshots قديمة مالهاش lifecycle policy.
ركّز في نقطة مهمة في الـ Elastic IP: من أول فبراير 2024، AWS بقت بتحاسب على كل عنوان IPv4 عام بسعر 0.005 دولار/ساعة، يعني حوالي 3.6 دولار شهريًا للعنوان الواحد — سواء مرتبط أو لأ. قبل كده العنوان المرتبط كان مجاني. ده غيّر حسابات كتير.
السكربت اللي بيكشفهم
المنطق بسيط: اسأل EC2 API عن الـ volumes المتاحة، والـ IPs غير المرتبطة، واللقطات الأقدم من 90 يوم. احسب التكلفة التقديرية، وابعت الناتج على Slack. السكربت للقراءة فقط — بيبلّغ ولا بيمسح، وده قرار مقصود هنشرحه تحت.
import boto3, os, datetime, json, urllib.request
GP3_GB_MONTH = 0.08 # سعر تخزين gp3 لكل GB شهريًا (us-east-1)
SNAP_GB_MONTH = 0.05 # سعر لقطات EBS لكل GB شهريًا
EIP_MONTH = 3.60 # عنوان IPv4 عام (~0.005$/ساعة)
SLACK = os.environ["SLACK_WEBHOOK"]
ec2 = boto3.client("ec2")
waste, total = [], 0.0
# 1) EBS volumes غير مرتبطة (status = available)
for v in ec2.describe_volumes(
Filters=[{"Name": "status", "Values": ["available"]}])["Volumes"]:
cost = v["Size"] * GP3_GB_MONTH
total += cost
waste.append(f"EBS {v['VolumeId']} ({v['Size']}GB) ~ ${cost:.2f}/شهر")
# 2) Elastic IPs غير مرتبطة
for a in ec2.describe_addresses()["Addresses"]:
if "AssociationId" not in a:
total += EIP_MONTH
waste.append(f"EIP {a['PublicIp']} ~ ${EIP_MONTH:.2f}/شهر")
# 3) لقطات أقدم من 90 يوم
cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=90)
for s in ec2.describe_snapshots(OwnerIds=["self"])["Snapshots"]:
if s["StartTime"] < cutoff:
cost = s["VolumeSize"] * SNAP_GB_MONTH
total += cost
waste.append(f"Snapshot {s['SnapshotId']} ({s['VolumeSize']}GB) ~ ${cost:.2f}/شهر")
if waste:
text = "*موارد AWS يتيمة محتمل توفيرها:*\n" + "\n".join(f"• {w}" for w in waste)
text += f"\n\n*الإجمالي التقديري: ${total:.2f}/شهر*"
body = json.dumps({"text": text}).encode()
req = urllib.request.Request(
SLACK, data=body, headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=10)