أساسيات السحابة: خدمات AWS الأساسية

مشروع: طبقة ويب عالية التوافر

18 دقيقة الدرس 10 من 30

مشروع: طبقة ويب عالية التوافر

يقودك هذا الدرس الختامي خطوةً بخطوة عبر نشر طبقة ويب احترافية وعالية التوافر على AWS من الصفر باستخدام AWS CLI. تجمع البنية بين موازن تحميل التطبيقات (ALB)، ومجموعة التحجيم التلقائي (ASG)، وقاعدة بيانات RDS متعددة نطاقات التوافر — وهو النموذج المرجعي الثلاثي الطبقات الذي ستجده في كل حمل عمل جاد على AWS. بنهاية هذا الدرس ستمتلك بنية تصمد أمام فشل مثيل واحد، وفشل نطاق توافر كامل، والارتفاع المفاجئ في حركة المرور، دون أي تدخل يدوي، وستفهم السبب الدقيق وراء كل قرار معماري.

البنية المستهدفة

يوضح المخطط أدناه ما ستبنيه. تدخل حركة المرور عبر ALB (الشبكات الفرعية العامة، نطاقا توافر)، فتُوزَّع على مثيلات EC2 داخل ASG (شبكات فرعية خاصة، نطاقا توافر)، التي تتصل بعد ذلك بمجموعة RDS متعددة نطاقات التوافر (شبكات فرعية للبيانات، نطاقا توافر). لا يمكن الوصول إلى أي مثيل EC2 أو RDS مباشرةً من الإنترنت — فقط ALB هو الذي يكون عاماً.

ALB + ASG + RDS Highly-Available Web Tier Architecture VPC 10.0.0.0/16 Internet Users / DNS Public Subnet — us-east-1a (10.0.1.0/24) Public Subnet — us-east-1b (10.0.2.0/24) ALB Node us-east-1a ALB Node us-east-1b Private Subnet — us-east-1a (10.0.11.0/24) Private Subnet — us-east-1b (10.0.12.0/24) EC2 App ASG · AZ-a EC2 App ASG · AZ-a EC2 App ASG · AZ-b EC2 App ASG · AZ-b Data Subnet — us-east-1a (10.0.21.0/24) Data Subnet — us-east-1b (10.0.22.0/24) RDS Primary Reads + Writes · AZ-a RDS Standby Multi-AZ sync · AZ-b SYNC replication
ALB + ASG + RDS متعدد نطاقات التوافر: البنية الثلاثية الطبقات عالية التوافر. جميع موارد EC2 وRDS تعيش في الشبكات الفرعية الخاصة وشبكات البيانات؛ ALB فقط يكون موجهاً للإنترنت.

الخطوة 1 — VPC والشبكات الفرعية

يعمل كل شيء داخل VPC مخصص يضم ثلاثة طبقات من الشبكات الفرعية لكل نطاق توافر: عامة (ALB)، وخاصة (EC2)، وللبيانات (RDS). يمنحك الفصل بين طبقات الشبكات الفرعية تحكماً مستقلاً في مجموعات الأمان وقوائم NACL عند كل حد.

# إنشاء VPC VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 \ --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=ha-web-vpc}]' \ --query 'Vpc.VpcId' --output text) aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames # الشبكات الفرعية العامة (ALB) PUB_A=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 \ --availability-zone us-east-1a \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=pub-1a}]' \ --query 'Subnet.SubnetId' --output text) PUB_B=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.2.0/24 \ --availability-zone us-east-1b \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=pub-1b}]' \ --query 'Subnet.SubnetId' --output text) # الشبكات الفرعية الخاصة (EC2 / ASG) PRIV_A=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.11.0/24 \ --availability-zone us-east-1a \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=priv-1a}]' \ --query 'Subnet.SubnetId' --output text) PRIV_B=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.12.0/24 \ --availability-zone us-east-1b \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=priv-1b}]' \ --query 'Subnet.SubnetId' --output text) # شبكات فرعية للبيانات (RDS) DATA_A=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.21.0/24 \ --availability-zone us-east-1a \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=data-1a}]' \ --query 'Subnet.SubnetId' --output text) DATA_B=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.22.0/24 \ --availability-zone us-east-1b \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=data-1b}]' \ --query 'Subnet.SubnetId' --output text) # Internet Gateway للشبكات الفرعية العامة فقط IGW=$(aws ec2 create-internet-gateway --query 'InternetGateway.InternetGatewayId' --output text) aws ec2 attach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC_ID # توجيه الشبكات العامة إلى IGW PUB_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID --query 'RouteTable.RouteTableId' --output text) aws ec2 create-route --route-table-id $PUB_RT --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW aws ec2 associate-route-table --route-table-id $PUB_RT --subnet-id $PUB_A aws ec2 associate-route-table --route-table-id $PUB_RT --subnet-id $PUB_B # NAT Gateway لكي تستطيع EC2 الخاصة الوصول للإنترنت (تثبيت الحزم، SSM) EIP=$(aws ec2 allocate-address --domain vpc --query 'AllocationId' --output text) NAT=$(aws ec2 create-nat-gateway --subnet-id $PUB_A --allocation-id $EIP \ --query 'NatGateway.NatGatewayId' --output text) aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT PRIV_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID --query 'RouteTable.RouteTableId' --output text) aws ec2 create-route --route-table-id $PRIV_RT --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT aws ec2 associate-route-table --route-table-id $PRIV_RT --subnet-id $PRIV_A aws ec2 associate-route-table --route-table-id $PRIV_RT --subnet-id $PRIV_B
شبكات البيانات الفرعية لا تملك أي مسار نحو الإنترنت — حتى عبر NAT. مثيلات RDS في شبكات البيانات يمكن الوصول إليها فقط من الموارد الداخلية في VPC. هذا هو الوضع الأمني الصحيح: قاعدة بياناتك لن تكون على بُعد قاعدة أمان واحدة مُعدَّة بشكل خاطئ من الإنترنت.

الخطوة 2 — مجموعات الأمان

تطبق ثلاث مجموعات أمان مبدأ الحد الأدنى من الصلاحيات على مستوى الشبكة. كل مجموعة تفتح فقط المنفذ والمصدر الضروريين — ولا يُستخدم 0.0.0.0/0 أبداً إلا لمستمع ALB.

# مجموعة أمان ALB — تقبل HTTPS من الإنترنت (وHTTP لإعادة التوجيه) ALB_SG=$(aws ec2 create-security-group \ --group-name ha-alb-sg --description "ALB inbound" \ --vpc-id $VPC_ID --query 'GroupId' --output text) aws ec2 authorize-security-group-ingress --group-id $ALB_SG \ --ip-permissions \ IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=0.0.0.0/0}] \ IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges=[{CidrIp=0.0.0.0/0}] # مجموعة أمان EC2 — تقبل فقط من مجموعة ALB على المنفذ 8080 APP_SG=$(aws ec2 create-security-group \ --group-name ha-app-sg --description "App tier inbound" \ --vpc-id $VPC_ID --query 'GroupId' --output text) aws ec2 authorize-security-group-ingress --group-id $APP_SG \ --protocol tcp --port 8080 --source-group $ALB_SG # مجموعة أمان RDS — تقبل فقط من مجموعة EC2 على المنفذ 5432 (PostgreSQL) DB_SG=$(aws ec2 create-security-group \ --group-name ha-db-sg --description "DB tier inbound" \ --vpc-id $VPC_ID --query 'GroupId' --output text) aws ec2 authorize-security-group-ingress --group-id $DB_SG \ --protocol tcp --port 5432 --source-group $APP_SG

الخطوة 3 — RDS متعدد نطاقات التوافر

أنشئ مجموعة شبكات فرعية لقاعدة البيانات تغطي كلتا شبكتي البيانات الفرعيتين، ثم شغِّل مثيل PostgreSQL متعدد نطاقات التوافر. يوفر خيار --multi-az نسخة احتياطية متزامنة في نطاق التوافر الثاني. RDS يتولى تحديث DNS تلقائياً خلال 60-120 ثانية في حال تعطل المثيل الأساسي — يُعيد تطبيقك الاتصال بنفس اسم المضيف.

# مجموعة شبكات فرعية لقاعدة البيانات aws rds create-db-subnet-group \ --db-subnet-group-name ha-db-subnet-group \ --db-subnet-group-description "HA project data subnets" \ --subnet-ids $DATA_A $DATA_B # تشغيل RDS PostgreSQL 16 — متعدد نطاقات التوافر، مشفر، نسخ احتياطي 7 أيام aws rds create-db-instance \ --db-instance-identifier ha-web-db \ --db-instance-class db.t4g.medium \ --engine postgres \ --engine-version 16 \ --master-username appuser \ --master-user-password "$(openssl rand -base64 20)" \ --db-name appdb \ --db-subnet-group-name ha-db-subnet-group \ --vpc-security-group-ids $DB_SG \ --multi-az \ --storage-type gp3 \ --allocated-storage 100 \ --iops 3000 \ --storage-encrypted \ --backup-retention-period 7 \ --preferred-backup-window "02:00-03:00" \ --deletion-protection \ --no-publicly-accessible # الانتظار حتى يصبح المثيل متاحاً (عادةً 5-10 دقائق) aws rds wait db-instance-available --db-instance-identifier ha-web-db DB_ENDPOINT=$(aws rds describe-db-instances \ --db-instance-identifier ha-web-db \ --query 'DBInstances[0].Endpoint.Address' --output text) echo "RDS endpoint: $DB_ENDPOINT"

الخطوة 4 — قالب الإطلاق لـ ASG

يحدد قالب الإطلاق المواصفات الدقيقة لكل مثيل ينشئه ASG. تُقلِّع بيانات المستخدم التطبيقَ عند التشغيل. مرِّر نقطة نهاية RDS عبر SSM Parameter Store — لا تُضمِّن الأسرار أبداً في قالب الإطلاق أو في AMI.

# تخزين نقطة نهاية قاعدة البيانات في SSM aws ssm put-parameter \ --name /ha-web/db-endpoint \ --value "$DB_ENDPOINT" \ --type String --overwrite # سكريبت بيانات المستخدم (مُرمَّز بـ base64 لواجهة CLI) USER_DATA=$(base64 -w0 <<'USERDATA' #!/bin/bash set -euo pipefail yum update -y yum install -y amazon-ssm-agent amazon-cloudwatch-agent DB_HOST=$(aws ssm get-parameter --name /ha-web/db-endpoint --query 'Parameter.Value' --output text --region us-east-1) pip3 install flask gunicorn psycopg2-binary cat > /opt/app/app.py <<'EOF' from flask import Flask app = Flask(__name__) @app.route('/health') def health(): return {'status': 'ok'}, 200 EOF systemctl enable gunicorn-app systemctl start gunicorn-app USERDATA ) # إنشاء قالب الإطلاق LT_ID=$(aws ec2 create-launch-template \ --launch-template-name ha-web-lt \ --version-description "v1" \ --launch-template-data "{ \"ImageId\": \"ami-0c02fb55956c7d316\", \"InstanceType\": \"t3.small\", \"SecurityGroupIds\": [\"$APP_SG\"], \"IamInstanceProfile\": {\"Name\": \"ha-web-instance-profile\"}, \"UserData\": \"$USER_DATA\", \"MetadataOptions\": {\"HttpTokens\": \"required\", \"HttpEndpoint\": \"enabled\"}, \"BlockDeviceMappings\": [{ \"DeviceName\": \"/dev/xvda\", \"Ebs\": {\"VolumeSize\": 30, \"VolumeType\": \"gp3\", \"Encrypted\": true, \"DeleteOnTermination\": true} }] }" \ --query 'LaunchTemplate.LaunchTemplateId' --output text)
اضبط "HttpTokens": "required" في MetadataOptions لكل قالب إطلاق. يُلزم هذا باستخدام IMDSv2، مما يمنع هجمات SSRF من قراءة بيانات اعتماد المثيل عبر نقطة نهاية البيانات الوصفية — وهو ناقل هجوم معروف ضد أعباء عمل EC2.

الخطوة 5 — ALB ومجموعة الأهداف

أنشئ ALB موجهاً للإنترنت في كلتا الشبكتين العامتين. أرفق مجموعة أهداف مع مسار فحص الصحة — يسجل ASG المثيلات الجديدة تلقائياً في هذه المجموعة، ولا يوجِّه ALB حركة المرور إلا للمثيلات التي تجتاز فحص الصحة.

# مجموعة الأهداف (HTTP على المنفذ 8080، فحص الصحة على /health) TG_ARN=$(aws elbv2 create-target-group \ --name ha-web-tg \ --protocol HTTP \ --port 8080 \ --vpc-id $VPC_ID \ --target-type instance \ --health-check-protocol HTTP \ --health-check-path /health \ --health-check-interval-seconds 20 \ --health-check-timeout-seconds 5 \ --healthy-threshold-count 2 \ --unhealthy-threshold-count 3 \ --matcher HttpCode=200 \ --query 'TargetGroups[0].TargetGroupArn' --output text) # ALB موجه للإنترنت يغطي كلتا الشبكتين العامتين ALB_ARN=$(aws elbv2 create-load-balancer \ --name ha-web-alb \ --type application \ --scheme internet-facing \ --security-groups $ALB_SG \ --subnets $PUB_A $PUB_B \ --query 'LoadBalancers[0].LoadBalancerArn' --output text) # مستمع HTTPS (يفترض وجود شهادة ACM مسبقاً) CERT_ARN="arn:aws:acm:us-east-1:123456789012:certificate/your-cert-id" aws elbv2 create-listener \ --load-balancer-arn $ALB_ARN \ --protocol HTTPS --port 443 \ --certificates CertificateArn=$CERT_ARN \ --default-actions Type=forward,TargetGroupArn=$TG_ARN # مستمع HTTP للتحويل إلى HTTPS aws elbv2 create-listener \ --load-balancer-arn $ALB_ARN \ --protocol HTTP --port 80 \ --default-actions \ Type=redirect,RedirectConfig="{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}" ALB_DNS=$(aws elbv2 describe-load-balancers \ --load-balancer-arns $ALB_ARN \ --query 'LoadBalancers[0].DNSName' --output text) echo "ALB DNS: $ALB_DNS"

الخطوة 6 — مجموعة التحجيم التلقائي

تستند ASG إلى قالب الإطلاق، وتمتد على كلتا الشبكتين الخاصتين، وتسجل المثيلات الجديدة في مجموعة الأهداف تلقائياً. استخدم سياسة تتبع الهدف على متوسط CPU: يضبط AWS حجم الأسطول تلقائياً للحفاظ على CPU عند المستوى المستهدف. اجمع هذا مع حد أدنى من مثيلين (واحد لكل نطاق توافر) لضمان التسامح مع أعطال نطاق التوافر في جميع الأوقات.

# إنشاء ASG (الحد الأدنى 2 / المطلوب 2 / الحد الأقصى 10) aws autoscaling create-auto-scaling-group \ --auto-scaling-group-name ha-web-asg \ --launch-template "LaunchTemplateId=$LT_ID,Version=\$Latest" \ --min-size 2 \ --max-size 10 \ --desired-capacity 2 \ --vpc-zone-identifier "$PRIV_A,$PRIV_B" \ --target-group-arns $TG_ARN \ --health-check-type ELB \ --health-check-grace-period 120 \ --default-instance-warmup 60 \ --tags "Key=Name,Value=ha-web-asg,PropagateAtLaunch=true" # سياسة تتبع الهدف — التحجيم للحفاظ على متوسط CPU عند 60% aws autoscaling put-scaling-policy \ --auto-scaling-group-name ha-web-asg \ --policy-name cpu-target-tracking \ --policy-type TargetTrackingScaling \ --target-tracking-configuration '{ "PredefinedMetricSpecification": { "PredefinedMetricType": "ASGAverageCPUUtilization" }, "TargetValue": 60.0, "DisableScaleIn": false }'
اضبط --health-check-type ELB وليس الافتراضي EC2. مع فحوصات صحة EC2، لا يستبدل ASG المثيل إلا حين يكون الجهاز الافتراضي غير قابل للوصول كلياً. أما مع فحوصات صحة ELB، فإذا تعطلت عملية تطبيقك أو بدأت بإرجاع أخطاء 5xx، يُعلن ALB عن المثيل غير سليم، ويُنهيه ASG ويُشغِّل بديلاً تلقائياً. هذا هو الفرق الجوهري بين "الخادم حي" و"التطبيق سليم".

التحقق من صحة البنية

بمجرد أن يصبح كلا المثيلين في ASG بحالة InService في مجموعة الأهداف، نفِّذ خطوات التحقق التالية لتأكيد عمل كل خاصية من خصائص التوافر العالي:

# 1. التحقق من سلامة المثيلات في مجموعة الأهداف aws elbv2 describe-target-health --target-group-arn $TG_ARN \ --query 'TargetHealthDescriptions[*].{Instance:Target.Id,State:TargetHealth.State}' # 2. التأكد من إرجاع ALB استجابة 200 من تطبيقك curl -sI https://$ALB_DNS/health # 3. محاكاة فشل المثيل — إنهاء مثيل ومراقبة ASG يستبدله INSTANCE_ID=$(aws autoscaling describe-auto-scaling-instances \ --query 'AutoScalingInstances[0].InstanceId' --output text) aws ec2 terminate-instances --instance-ids $INSTANCE_ID # خلال دقيقتين تقريباً، يُشغِّل ASG مثيلاً بديلاً # 4. مراقبة سجل نشاط ASG aws autoscaling describe-scaling-activities \ --auto-scaling-group-name ha-web-asg \ --max-items 5 # 5. اختبار الفشل التلقائي لـ RDS (يُجبر على التبديل إلى Multi-AZ، ~60 ثانية توقف) aws rds reboot-db-instance \ --db-instance-identifier ha-web-db \ --force-failover # يجب أن يتعامل تطبيقك مع إعادة الاتصال — pool الاتصالات مع إعادة المحاولة ضروري

أنماط الفشل الإنتاجية الأساسية

البنية صامدة لكنها ليست محصنة ضد جميع أنواع الأعطال. يعرف مهندسو الشركات الكبرى هذه الحالات الطرفية:

  • قطيع الرعد عند التوسع: حين يُشغِّل ASG 6 مثيلات جديدة معاً خلال ارتفاع مفاجئ في حركة المرور، تضرب جميعها pool اتصالات RDS دفعةً واحدة. اضبط max_connections عبر مجموعة المعاملات واستخدم موزع اتصالات كـ PgBouncer أو RDS Proxy في وضع المعاملات لاستيعاب الموجة.
  • فترة التسامح مع فحص الصحة قصيرة جداً: إذا كانت --health-check-grace-period أقل من وقت تشغيل تطبيقك، سيُنهي ASG المثيل قبل أن يكون جاهزاً ويدخل في حلقة تشغيل-إنهاء. اضبطها على 1.5× وقت البدء البارد المُلاحَظ.
  • قالب الإطلاق المتقادم: إذا حدَّثت إصدار قالب الإطلاق لكنك نسيت تحديث مرجع ASG من $Latest إلى إصدار مثبت، قد يُشغِّل حدث توسع متزامن الإصدار الخطأ. ثبِّت إصداراً محدداً في بيئات الإنتاج الحرجة.
  • NAT Gateway واحد نقطة فشل وحيدة: الإعداد أعلاه يستخدم NAT Gateway واحداً في AZ-a. في حال تعطل جزئي لـ AZ-a، تفقد مثيلات AZ-b الوصول للإنترنت الخارجي. للتوافر الحقيقي، نشر NAT Gateway واحد لكل نطاق توافر وتوجيه كل شبكة فرعية خاصة إلى NAT المحلي الخاص بها.