الوحدة 4 · بناء شيء حقيقي
API من نوع serverless باستخدام Lambda وAPI Gateway وDynamoDB
17 دقراءة 4 داكتشف الخطأ
الدرس في لمحة
ابنِ الـ API الخاص بتأكيد الحضور في Lantern من HTTP API في Amazon API Gateway، ودالة AWS Lambda بـ role يتبع أقل الصلاحيات، وجدول DynamoDB، مع حماية من التكرار وتكاليف تهبط إلى ما يقارب الصفر عند الخمول.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
أصبحت الواجهة الأمامية تُحمَّل من مواقع الحافة خلال أجزاء من الألف من الثانية. لكن تأكيد الحضور ليس ملفًا ثابتًا: يجب التحقّق منه، وتخزينه، وعدّه. على الخادم القديم كان ذلك مسارًا (route) في تطبيق Node.js، يعمل طوال اليوم من أجل الدقائق القليلة في الأسبوع التي يؤكّد فيها أحدهم حضوره. أما الآن فيصبح ثلاث قطع مُدارة لا تكلّف شيئًا وهي خاملة، وتتوسّع حين تنتشر فعالية ما.
الصورة العامة
مساران يكفيان للإطلاق:
| المسار | ما يفعله |
|---|---|
POST /events/{eventId}/rsvps |
يخزّن تأكيد حضور واحدًا، ويرفض التكرار |
GET /events/{eventId}/rsvps |
يعيد عدد تأكيدات الحضور للفعالية |
الدالة
يمرّر الـ HTTP API في API Gateway إلى Lambda حدث JSON (بصيغة الحمولة 2.0) يحتوي على معاملات المسار (path parameters) والطريقة (method) والمحتوى كنص. وتعيد الدالة رمز حالة وترويسات ومحتوى نصيًا:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
// Created once per execution environment and reused across invocations.
// removeUndefinedValues: an RSVP without a "name" is stored without that attribute.
const db = DynamoDBDocumentClient.from(new DynamoDBClient({}), {
marshallOptions: { removeUndefinedValues: true },
});
const TABLE = process.env.TABLE_NAME;
const reply = (statusCode, data) => ({
statusCode,
headers: { "content-type": "application/json" },
body: JSON.stringify(data),
});
export const handler = async (event) => {
const eventId = event.pathParameters?.eventId;
if (event.requestContext.http.method === "GET") {
const res = await db.send(new QueryCommand({
TableName: TABLE,
KeyConditionExpression: "eventId = :e",
ExpressionAttributeValues: { ":e": eventId },
Select: "COUNT",
}));
return reply(200, { eventId, count: res.Count });
}
const { email, name } = JSON.parse(event.body ?? "{}");
if (typeof email !== "string" || !email.includes("@")) {
return reply(400, { error: "A valid email is required" });
}
try {
await db.send(new PutCommand({
TableName: TABLE,
Item: { eventId, email: email.toLowerCase(), name, createdAt: new Date().toISOString() },
ConditionExpression: "attribute_not_exists(email)",
}));
} catch (err) {
if (err.name === "ConditionalCheckFailedException") {
return reply(409, { error: "You have already RSVPed to this event" });
}
throw err;
}
return reply(201, { eventId, email });
};لاحظ ما ليس موجودًا: لا بيانات اعتماد، ولا ARN للجدول، ولا مجمّع اتصالات (connection pool). تعثر الـ SDK على بيانات اعتماد الـ execution role، ويأتي اسم الجدول من متغيّر بيئة كي يعمل الكود نفسه على جداول التطوير والإنتاج. تتضمّن بيئة تشغيل Node.js الإصدار v3 من AWS SDK for JavaScript، لكن احزم مع الكود الخاص بك الإصدار الذي اختبرته، كي لا يغيّره تحديث لبيئة التشغيل من تحت قدميك.
وهناك حدّ واحد يجب ذكره بصراحة: يقرأ Query الواحد 1 MB كحد أقصى، لذا فإن فعالية بعدد هائل من تأكيدات الحضور ستحتاج في العدّ إلى تقسيم إلى صفحات (pagination)، أو إلى عنصر عدّاد منفصل يُحدَّث مع كل كتابة. أما لفعاليات Lantern، فاستدعاء واحد أكثر من كافٍ.
الصلاحيات
يحصل الـ execution role الخاص بالدالة على ما يستدعيه الكود بالضبط، على جدول واحد بالضبط، إضافة إلى السياسة المُدارة من AWS AWSLambdaBasicExecutionRole كي يستطيع كتابة السجلات:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RsvpTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:eu-west-1:111122223333:table/lantern-rsvps"
}
]
}والاتجاه الآخر هو السياسة القائمة على المورد الخاصة بالدالة: يحق لـ API Gateway استدعاؤها. يضيف الـ console هذه السياسة حين تربط مسارًا؛ ومن الـ CLI تبدو هكذا، مقيّدة بـ API واحد عبر الـ ARN الخاص به:
aws lambda add-permission --function-name lantern-rsvp \
--statement-id allow-lantern-http-api \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:eu-west-1:111122223333:a1b2c3d4e5/*"الـ API
في console الـ API Gateway، أنشئ HTTP API، وأضف المسارين مع تكامل Lambda (Lambda integration) إلى lantern-rsvp، واستخدم المرحلة $default التي تُنشر تلقائيًا. الـ HTTP APIs هي النوع الأرخص والأبسط. واختر REST API بدلًا منها حين تحتاج إلى API keys مع خطط الاستخدام، أو تحديد معدّل لكل عميل، أو التحقّق من الطلبات، أو التخزين المؤقت للاستجابات، أو AWS WAF.
لأن الموقع يُقدَّم من https://lantern.example والـ API يعيش على نطاق آخر، اضبط CORS على الـ HTTP API للسماح بذلك الـ origin، أو أضف الـ API كـ origin ثانٍ في الـ distribution الخاص بـ CloudFront تحت /api/* كي يرى المتصفّح نطاقًا واحدًا. ثم اختبر:
curl -i -X POST \
"https://a1b2c3d4e5.execute-api.eu-west-1.amazonaws.com/events/evt-2026-street-food/rsvps" \
-H "content-type: application/json" \
-d '{"email":"rita@example.com","name":"Rita"}'يعيد الاستدعاء الأول 201؛ أرسله مرة أخرى فتحصل على 409.
كم يكلّف، وكيف تضع له سقفًا
كل قطعة تُحتسب حسب الاستخدام: API Gateway لكل طلب، وLambda لكل طلب ولكل GB-second، وDynamoDB بنمط on-demand لكل قراءة وكتابة. الأسبوع الهادئ لا يكاد يكلّف شيئًا، وأمسية المهرجان تكلّف ما يعادل بضعة آلاف من الطلبات. وللتأكّد من أن بوتًا لا يستطيع تضخيم الفاتورة أو إغراق أي شيء في المراحل التالية، اضبط reserved concurrency على الدالة (سقف لعدد النسخ المتزامنة) وحدًّا للمعدّل (throttling) على مرحلة الـ API.
في الدرس التالي ستراقب هذا الـ API في الإنتاج: السجلات والمقاييس والإنذارات، ومراجعة منظّمة للتصميم كله.
أهم النقاط
- يستقبل الـ HTTP API في API Gateway الطلب، ويستدعي Lambda بحدث JSON، ويعيد أيّ رمز حالة ومحتوى تعيدهما الدالة.
- تحتاج دالة Lambda إلى نوعين من الصلاحيات؛ الـ execution role يتيح لها استدعاء DynamoDB، وسياستها القائمة على المورد تتيح لـ API Gateway استدعاءها.
- الكتابة المشروطة مثل attribute_not_exists تمنع النقرات المزدوجة وإعادة المحاولات من إنشاء عناصر مكرّرة.
- اختر HTTP APIs للـ APIs البسيطة منخفضة التكلفة، وREST APIs حين تحتاج إلى API keys، أو تحديد معدّل لكل عميل، أو التحقّق من الطلبات، أو التخزين المؤقت، أو AWS WAF.
- كل قطعة تُحتسب لكل طلب، فالـ API الخامل لا يكاد يكلّف شيئًا، والـ reserved concurrency يضع سقفًا لمدى توسّع الذروة.