الجزء 22 من سلسلة تعلّم AWS من الصفر

تصميم تطبيقات بدون خوادم

نجمع القطع مع بعض. سير عمل طويل بـ Step Functions، ومستخدمين بـ Cognito، وتصميمين لتطبيقات كاملة، والخدمات المصغرة وربح رخيص مع CloudFront، مع تجارب عملية على ملف يشغّل دالة وسير عمل يقرر.

في هذا المقال
  1. Step Functions: سير العمل
  2. Cognito: مستخدمينك
  3. التصميم الأول: تطبيق جوال
  4. التصميم الثاني: موقع عالمي
  5. الخدمات المصغرة (Microservices)
  6. ربح رخيص: CloudFront قدام اللي عندك
  7. مبادئ التصميم
  8. عملي: ملف يشغّل دالة
  9. عملي: سير عمل يقرر
  10. الخلاصة
  11. اختبر نفسك

في الجزء الحادي والعشرين بنينا API من ثلاث قطع. التطبيق الحقيقي يحتاج أكثر: عملية من عدة خطوات ممكن تفشل في النص، ومستخدمين يسجلون دخول، وأشياء لازم تصير بعد الطلب بدون ما ينتظرها أحد.

هذا الجزء يكمل الصورة بخدمتين زيادة، ثم يطبق كل شي على تصميمين.

Step Functions: سير العمل

الطلب فيه فحص مخزون، ثم تحصيل المبلغ، ثم شحن. تقدر تخلي كل دالة تستدعي اللي بعدها، بس وين تكون إعادة المحاولة؟ وش يصير لو فشل الدفع بعد ما حجزت المخزون؟ ومين يقول لك وين الطلب 1001 الحين؟

Step Functions يطلع هذا المنطق من كودك إلى آلة حالات (State Machine): تعريف للخطوات ووش يصير بعد كل وحدة، وتشوفه كرسم في الكونسول.

فحص المخزون تحصيل المبلغ يعيد المحاولة الشحن إلغاء وإشعار غير متوفر مرفوض
سير العمل يعرف ترتيب الخطوات ووين يروح كل فشل، وكل خطوة تبقى صغيرة وبسيطة.

كل خطوة تقدر تستدعي دالة Lambda أو خدمات AWS كثيرة مباشرة، والتعريف يتكفل باللي كنت بتكتبه بيدك:

القدرة وش تسوي
Retry تعيد الخطوة الفاشلة، وتنتظر أطول كل مرة
Catch ترسل الفشل لخطوة تعالجه، مثل استرجاع المبلغ
Choice تختار المسار حسب البيانات
Parallel وMap تشغل فروع مع بعض، أو خطوة لكل عنصر في قائمة
Wait توقف لفترة، أو لموافقة بشرية تاخذ أيام

هذا شكل إعادة محاولة الدفع والتقاط فشله:

JSON
"TakePayment": {
  "Type": "Task",
  "Resource": "arn:aws:states:::lambda:invoke",
  "Parameters": { "FunctionName": "take-payment", "Payload.$": "$" },
  "Retry": [{
    "ErrorEquals": ["States.TaskFailed"],
    "IntervalSeconds": 2,
    "BackoffRate": 2,
    "MaxAttempts": 3
  }],
  "Catch": [{
    "ErrorEquals": ["States.ALL"],
    "Next": "CancelOrder"
  }],
  "Next": "Ship"
}
Standard Express
أقصى مدة سنة خمس دقايق
التنفيذ مرة واحدة بالضبط مرة على الأقل
المحاسبة لكل انتقال بين الحالات، و4,000 مجانية كل شهر عدد الطلبات والمدة والذاكرة
يناسب عمليات طويلة مثل الطلبات والموافقات أحداث قصيرة ومتكررة جداً

Cognito: مستخدمينك

IAM لفريقك وخدماتك، مو لمليون شخص يستخدمون تطبيقك. Amazon Cognito يعطي تطبيقك مستخدمينه، وله جزئين سهل تخلط بينهم:

User Pool Identity Pool
السؤال اللي يجاوبه من هذا المستخدم؟ وش يقدر يسوي في AWS؟
يعطي توكن بعد الدخول مفاتيح AWS مؤقتة
يستخدم لـ تطبيقك والـ API حقك وصول مباشر لـ S3 أو DynamoDB

User Pools تتكفل بالتسجيل والدخول، واستعادة كلمة المرور، والتحقق بعدة عوامل، والدخول بحسابات مثل Google أو هوية الشركة. وAPI Gateway وموزع الحمل ALB يقدرون يتحققون من توكناته مباشرة. والمجموعات الجديدة تستخدم خطة Essentials افتراضياً، وفيه كمان Lite الأرخص بميزات أقل، وPlus بحماية متقدمة.

Identity Pools تبدل التوكن بمفاتيح مؤقتة مربوطة بدور IAM، ومتغير في السياسة يحصرها في بيانات المستخدم نفسه. مثلاً كل مستخدم يوصل لمجلده فقط في S3:

JSON
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::recipe-photos/users/${cognito-identity.amazonaws.com:sub}/*"
}

التصميم الأول: تطبيق جوال

خلنا نصمم تطبيق لمشاركة الوصفات: المستخدمين يسجلون دخول، ويكتبون وصفات، ويرفعون صور، والوصفات المشهورة تنقرأ أكثر بكثير مما تنكتب. كل متطلب له قطعته:

المتطلب الحل
دخول المستخدمين User Pool في Cognito
API لقراءة الوصفات وكتابتها API Gateway وLambda وDynamoDB
رفع الصور مباشرة Identity Pool بمفاتيح مؤقتة إلى S3
قراءات هائلة لنفس الوصفات DAX قدام الجدول
طلبات متطابقة متكررة كاش API Gateway في REST API
التطبيق User Pool دخول وتوكن API Gateway يتحقق من التوكن Lambda DynamoDB Identity Pool مفاتيح مؤقتة S3 users/ID/‎
الـ User Pool يسجل دخول المستخدم ويعطيه توكن. API Gateway يتحقق منه، والـ Identity Pool يبدله بمفاتيح مؤقتة محصورة في مجلد المستخدم.

ليه الصورة تروح مباشرة لـ S3 بدل ما تمر عبر الـ API؟ لأن API Gateway حده 10 MB، ولأنك بتدفع وقت Lambda بس عشان تمرر بايتات. والروابط الموقّعة خيار ثاني ممتاز هنا، لو ما تحتاج Identity Pools.

ولاحظ وين يقع كل كاش: كاش API Gateway يوقف الطلب قبل ما يوصل للدالة، فيناسب الردود اللي ما تختلف من مستخدم لآخر. وDAX يخلي الدالة تشتغل، ويريح الجدول من القراءة.

التصميم الثاني: موقع عالمي

الحين دليل سفر ينقرأ حول العالم: صفحات ثابتة، وAPI صغير، وإيميل ترحيب لما يسجل أحد، وصور مصغرة للصور اللي يرفعها المستخدمين.

الزوار CloudFront S3 صفحات وصور Lambda صور مصغرة API Gateway Lambda منطقك DynamoDB Global Tables Streams Lambda إيميل ترحيب SES
CloudFront يقدم الصفحات ويمرر طلبات الـ API. وكل شي بعد الكتابة يصير بالأحداث: مستخدم جديد يشغّل الإيميل، وصورة جديدة تشغّل الصورة المصغرة.

تابعه قطعة قطعة:

  • الصفحات: حاوية S3 خاصة وقدامها CloudFront مع OAC، فالحاوية تبقى مقفلة والموقع سريع في كل مكان.
  • الـ API: API Gateway وLambda وDynamoDB، وGlobal Tables تقرب البيانات من المستخدمين في كل منطقة.
  • إيميل الترحيب: الـ API ما يرسله. المستخدم الجديد ينكتب في الجدول، وDynamoDB Streams يشغّل دالة، وهي ترسل عبر SES. ولو فشل الإيميل، التسجيل ما يتأثر.
  • الصور المصغرة: صورة تنزل في S3 تشغّل دالة Lambda تكتب نسخة أصغر. وللتعامل مع الموجات أو الوصول لأكثر من خدمة، حط SQS أو SNS بينهم.

ما فيه سيرفر واحد هنا، ولما ما يكون فيه زوار، الفاتورة تقرب من تكلفة التخزين فقط. والفكرة في آخر مسارين وحدة: الطلب يخلص بسرعة، والباقي يصير بالأحداث.

الخدمات المصغرة (Microservices)

لما يكبر التطبيق، الفرق تقسمه إلى خدمات صغيرة، كل وحدة لها شغلها وقاعدة بياناتها وجدول إطلاقها. وعندهم طريقتين للتواصل:

الأسلوب الأدوات يناسب
متزامن API Gateway أو موزع حمل تحتاج الجواب الحين، مثل سعر
غير متزامن SQS وSNS وKinesis ومشغلات Lambda الشغل ممكن يجي بعدين، مثل فاتورة

وكل خدمة تقدر تختار اللي يناسبها: Lambda للطلبات المتفرقة، وFargate للحمل الثابت، وEC2 للمتطلبات الخاصة. بس لهذا كله ثمن: إطلاقات أكثر، ومراقبة أكثر، وأعطال أصعب في تتبعها بين الخدمات. فلا تبدأ من هنا لتطبيق صغير، وقسّم بس لما تحتاج فعلاً.

ربح رخيص: CloudFront قدام اللي عندك

مو كل تحسين يحتاج إعادة كتابة. تخيل تطبيق على مجموعة Auto Scaling يوزع ملفات تحديث كبيرة. يوم الإطلاق، كل مستخدمينك يحملون بنفس الوقت: السيرفرات تتضاعف، وفاتورة نقل البيانات تقفز.

حط CloudFront قدامه، وما تغير شي ثاني. الملفات ما تتغير، فتناسب الكاش جداً: أغلب الطلبات توقف عند الحافة، والمجموعة بالكاد تكبر، والبيانات الخارجة عبر CloudFront أرخص من الخروج المباشر. شغل دقايق ممكن يرجع لك أكثر من إعادة تصميم.

مبادئ التصميم

المبدأ ليه
خل المعالجة آمنة لو تكررت SQS وSNS وLambda غير المتزامنة توصل مرة على الأقل
طابور فشل لكل شي غير متزامن الفشل الصامت أسوأ أنواع الفشل
دور واحد لكل دالة بأقل صلاحية ثغرة في وحدة ما تفتح كل شي
دوال صغيرة بشغلة وحدة أسهل في الاختبار وأسرع في التشغيل
احسب التكلفة مع حمل كبير وثابت، الحاويات ممكن تطلع أرخص
راقب كل شي الطلب يمر بخدمات كثيرة، وبنغطي الأدوات في الجزء الخامس والعشرين

وما يلزم تبدأ من الصفر. AWS Serverless Application Repository يخليك تلقى تطبيقات جاهزة بدون سيرفرات، وتضبط كم قيمة، وتنشرها بكم ضغطة.

عملي: ملف يشغّل دالة

في الجزء الخامس عشر وعدنا نطبق أحداث S3. بنسوي مسار الصور المصغرة بتبسيط واحد: بدل مكتبة صور، الدالة تحول ملف نصي إلى حروف كبيرة. والمسار نفسه تماماً. من CloudShell:

Bash
BUCKET=series-events-$RANDOM$RANDOM
aws s3 mb s3://$BUCKET

cat > trust.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}
EOF

ROLE_ARN=$(aws iam create-role --role-name series-events-role \
  --assume-role-policy-document file://trust.json \
  --query Role.Arn --output text)

aws iam attach-role-policy --role-name series-events-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

aws iam put-role-policy --role-name series-events-role \
  --policy-name bucket-access --policy-document "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [{
      \"Effect\": \"Allow\",
      \"Action\": [\"s3:GetObject\", \"s3:PutObject\"],
      \"Resource\": \"arn:aws:s3:::$BUCKET/*\"
    }]
  }"

اكتب الدالة وأنشئها. ولو جاك خطأ عن الدور، انتظر كم ثانية وأعد الأمر:

Bash
cat > app.py <<'EOF'
from urllib.parse import unquote_plus
import boto3

s3 = boto3.client("s3")

def handler(event, context):
    for record in event["Records"]:
        bucket = record["s3"]["bucket"]["name"]
        key = unquote_plus(record["s3"]["object"]["key"])
        body = s3.get_object(Bucket=bucket, Key=key)["Body"].read().decode()
        name = key.split("/")[-1]
        s3.put_object(Bucket=bucket, Key=f"processed/{name}", Body=body.upper())
EOF

zip -q function.zip app.py

FN_ARN=$(aws lambda create-function --function-name series-events \
  --runtime python3.13 --handler app.handler --role $ROLE_ARN \
  --zip-file fileb://function.zip \
  --query FunctionArn --output text)

aws lambda wait function-active-v2 --function-name series-events

اسمح لـ S3 يستدعي الدالة، ثم قل للحاوية ترسل لها أحداث مجلد uploads/ فقط:

Bash
aws lambda add-permission --function-name series-events \
  --statement-id s3-trigger --action lambda:InvokeFunction \
  --principal s3.amazonaws.com --source-arn arn:aws:s3:::$BUCKET \
  --query Statement --output text > /dev/null

cat > notify.json <<EOF
{
  "LambdaFunctionConfigurations": [{
    "LambdaFunctionArn": "$FN_ARN",
    "Events": ["s3:ObjectCreated:*"],
    "Filter": {"Key": {"FilterRules": [{"Name": "prefix", "Value": "uploads/"}]}}
  }]
}
EOF

aws s3api put-bucket-notification-configuration --bucket $BUCKET \
  --notification-configuration file://notify.json

ارفع ملف، وانتظر كم ثانية، ثم اقرأ النتيجة:

Bash
echo "hello from s3" > note.txt
aws s3 cp note.txt s3://$BUCKET/uploads/note.txt
Bash
aws s3 cp s3://$BUCKET/processed/note.txt -

يطبع HELLO FROM S3. رفعت ملف، وكود اشتغل لحاله.

نظّف قبل ما تكمل:

Bash
aws s3 rb s3://$BUCKET --force
aws lambda delete-function --function-name series-events
aws logs delete-log-group --log-group-name /aws/lambda/series-events
aws iam delete-role-policy --role-name series-events-role \
  --policy-name bucket-access
aws iam detach-role-policy --role-name series-events-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name series-events-role

عملي: سير عمل يقرر

بنكتب آلة حالات تطالع مبلغ الطلب: المبلغ السالب يفشل، والكبير يروح للمراجعة، والباقي ينقبل. وتستخدم خطوات داخلية فقط، فتحتاج دور بدون صلاحيات:

Bash
cat > trust.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "states.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}
EOF

FLOW_ROLE=$(aws iam create-role --role-name series-flow-role \
  --assume-role-policy-document file://trust.json \
  --query Role.Arn --output text)

cat > flow.json <<'EOF'
{
  "StartAt": "CheckAmount",
  "States": {
    "CheckAmount": {
      "Type": "Choice",
      "Choices": [
        {"Variable": "$.amount", "NumericLessThanEquals": 0, "Next": "Rejected"},
        {"Variable": "$.amount", "NumericGreaterThan": 1000, "Next": "NeedsReview"}
      ],
      "Default": "Approved"
    },
    "NeedsReview": {"Type": "Pass", "Result": {"status": "review"}, "End": true},
    "Approved": {"Type": "Pass", "Result": {"status": "approved"}, "End": true},
    "Rejected": {"Type": "Fail", "Error": "InvalidAmount", "Cause": "The amount must be positive"}
  }
}
EOF

أنشئها. وهنا برضو، لو جاك خطأ عن الدور، انتظر كم ثانية وأعد الأمر:

Bash
FLOW_ARN=$(aws stepfunctions create-state-machine --name series-order \
  --definition file://flow.json --role-arn $FLOW_ROLE \
  --query stateMachineArn --output text)

شغّلها بثلاث طلبات:

Bash
try_order() {
  RUN=$(aws stepfunctions start-execution --state-machine-arn $FLOW_ARN \
    --input "$1" --query executionArn --output text)
  sleep 2
  aws stepfunctions describe-execution --execution-arn $RUN \
    --query "[status,output]" --output text
}

try_order '{"amount": 250}'
try_order '{"amount": 5000}'
try_order '{"amount": -5}'

بيطلع لك approved، ثم review، ثم FAILED. الحين افتح Step Functions في الكونسول، واختر series-order، ثم أي تنفيذ. بتشوف الرسم والمسار اللي مشى عليه ملوّن، وهذا اللي بتطالعه لما يعلق طلب في الإنتاج.

Bash
aws stepfunctions delete-state-machine --state-machine-arn $FLOW_ARN
aws iam delete-role --role-name series-flow-role

الخلاصة

  • Step Functions يطلع الخطوات وإعادة المحاولة والفشل من كودك إلى سير عمل تشوفه.
  • User Pool يجاوب من هو المستخدم، وIdentity Pool يعطيه مفاتيح مؤقتة لـ AWS.
  • التصميم الجيد يخلص الطلب بسرعة، ويترك الباقي للأحداث.
  • خل الملفات الكبيرة تروح مباشرة لـ S3، وحط الكاش وين يوقف أكبر قدر من الشغل.
  • الخدمات المصغرة لها ثمن، فقسّم بس لما تحتاج فعلاً.

اختبر نفسك

قبل ما تكمل، جرّب هذي الأسئلة عشان تتأكد إنك فهمت الجزء. كل سؤال موقف عملي، وبعد ما تجاوب يطلع لك الشرح.

السؤال 1 من 5

الطلب يمر بالمخزون ثم الدفع ثم الشحن. والخطوات الفاشلة لازم تنعاد، وفشل الدفع لازم يطلق استرداد، والطلبات الكبيرة تنتظر موافقة مدير أيام. والفريق يبي يشوف وين وصل كل طلب. وش يناسب؟

اعرض الجواب

الجواب الصحيح: D

سير العمل Standard ممكن يستمر حتى سنة، وتعريفه يحمل إعادة المحاولة والفشل والانتظار، والكونسول يعرض لك مسار كل تنفيذ كرسم.

  • A: هذا يشتغل خمس دقايق كحد أقصى، فما يقدر ينتظر أيام.
  • B: إعادة المحاولة ومعالجة الفشل تصير في كودك، وما فيه شي يوريك وين وصل الطلب.
  • C: هذا يوصّل لكل المشتركين مرة وحدة، بدون ترتيب بين الخطوات.

للمراجعة: Step Functions: سير العمل في هذا الجزء، وChoosing workflow type in Step Functions في وثائق AWS.

السؤال 2 من 5

المستخدمين يدخلون لتطبيق جوال عبر Cognito User Pool. ولازم يرفعون صورهم مباشرة إلى S3، وكل واحد يوصل لمجلده فقط. وش يعطي التطبيق وصول إلى S3؟

اعرض الجواب

الجواب الصحيح: A

الـ Identity Pool يبدل التوكن بمفاتيح مؤقتة مربوطة بدور IAM، والمتغير في السياسة يحصرها في مجلد المستخدم.

  • B: توكن الـ User Pool يقول مين المستخدم، وهو مو مفاتيح AWS.
  • C: IAM لفريقك وخدماتك، مو لأعداد ضخمة من مستخدمي التطبيق.
  • D: أي أحد يقدر يطلع هذي المفاتيح من التطبيق، وبتشتغل على مجلدات الكل.

للمراجعة: Cognito: مستخدمينك في هذا الجزء، وAmazon Cognito identity pools وIAM roles في وثائق AWS.

السؤال 3 من 5

واجهة تسجيل ترسل بريد ترحيب قبل ما ترد. ولما تبطأ خدمة البريد يبطأ التسجيل، ولما تفشل يفشل التسجيل. والمستخدمين الجدد ينكتبون في DynamoDB. وش أفضل تصميم؟

اعرض الجواب

الجواب الصحيح: B

الطلب يخلص أول ما ينكتب المستخدم، والبريد يصير عبر حدث، ففشل البريد ما يأثر على التسجيل.

  • A: المستخدمين بينتظرون أكثر، والفشل لسا يوصل لهم.
  • C: هذا يشيل البداية الباردة، والبطء جاي من البريد.
  • D: إعادة المحاولة داخل الطلب تخلي المستخدمين ينتظرون أكثر.

للمراجعة: التصميم الثاني: موقع عالمي في هذا الجزء، وDynamoDB Streams and AWS Lambda triggers في وثائق AWS.

السؤال 4 من 5

تطبيق على مجموعة Auto Scaling يوزع ملفات تحديث كبيرة. ويوم الإطلاق تكبر المجموعة بقوة وتقفز فاتورة نقل البيانات. وش يعطي أكبر تحسين بأقل تغيير؟

اعرض الجواب

الجواب الصحيح: C

الملفات ما تتغير، فتناسب الكاش جداً. وأغلب الطلبات تنتهي عند الحافة، والمجموعة بالكاد تكبر، والبيانات اللي تطلع عبر CloudFront أرخص من اللي تطلع مباشرة.

  • A: السيرفرات لسا تخدم كل تحميل، ونفس البيانات لسا تطلع.
  • B: هذا إعادة كتابة، وحد API Gateway وهو 10 MB صغير على الملفات الكبيرة.
  • D: هذا يختصر الطريق بس ما يخزن، فكل تحميل لسا يوصل للسيرفرات.

للمراجعة: ربح رخيص: CloudFront قدام اللي عندك في هذا الجزء، وWhat is Amazon CloudFront? في وثائق AWS.

السؤال 5 من 5

موضوع SNS يشغّل دالة Lambda بشكل غير متزامن. وبعض العملاء انخصم منهم مرتين، والأحداث اللي فشلت كثير اختفت بدون أي أثر. أي تغييرين يعالجون هذا؟ (اختر اثنين.)

اعرض الجواب

الجواب الصحيح: A + E

الحدث ممكن يوصل أكثر من مرة، فلازم التكرار ما يضر. وطابور الرسائل الفاشلة يحتفظ باللي فشل عشان تفحصه، بدل ما يضيع بصمت.

  • B: هذا يسرّع الدالة، والتكرار لسا يوصل.
  • C: هذا يخفف البداية الباردة، وما له علاقة بأي مشكلة منهم.
  • D: هذا يبقي البيئات جاهزة، وما له علاقة بأي مشكلة منهم.

للمراجعة: مبادئ التصميم في هذا الجزء، وHow Lambda handles errors and retries with asynchronous invocation وCapturing records of Lambda asynchronous invocations في وثائق AWS.

بهذا نكمل قسم التطبيقات الحديثة. تطبيقك الحين يطلع بيانات كثيرة، بس وش تقول لك؟ في الجزء الثالث والعشرين نبدأ قسم البيانات والذكاء الاصطناعي، ونحلل البيانات بـ Athena وRedshift وGlue.