AWS Certified CloudOps Engineer - Associate
أساسيات CloudFormation
كيف يصف قالب CloudFormation بنيتك التحتية: أقسام القالب، وParameters بحواجز حقيقية، والدوال الضمنية، ورسم الاعتماديات الذي تبنيه الخدمة نيابةً عنك، وسمات الموارد التي تقرّر ما ينجو من الحذف.
- ميّز بين القالب والحزمة والمعرّف المنطقي والمعرّف الفعلي
- حدّد أقسام القالب واشرح أيّها المطلوب
- اختر نوع Parameter وقيوده المناسبة لرفض المدخل الخاطئ قبل إنشاء أي مورد
- اختر الدالة الضمنية الصحيحة لكل إشارة، واشرح لماذا لا يتبادل Ref وFn::GetAtt المواقع
- توقّع ترتيب إنشاء الموارد، واعرف متى يصبح DependsOn ضرورياً
- قارن بين Fn::ImportValue وFn::GetStackOutput ومخرجات الحزم المتداخلة لتمرير القيم
- أبقِ بيانات الاعتماد خارج القالب عبر dynamic references، وحدّد ما لا يحميه NoEcho
بنى فريق بيئة staging باليد: VPC، وشبكتان فرعيتان، ومجموعة أمان، وموازن حمل، ومجموعة Auto Scaling، ونسخة RDS. استغرق ذلك بعد ظهيرة كاملة من النقر. والآن تحتاج بيئة الإنتاج الشيء نفسه، في منطقة ثانية، ولا أحد يتذكّر قواعد مجموعة الأمان بدقّة. فيفتح أحدهم الـ Console بجوار staging ويبدأ النسخ.
هذه هي المشكلة التي يزيلها CloudFormation. تصف البنية التحتية مرّة واحدة في ملف، وتتولّى AWS بناءها. وابنِها مجدّداً الأسبوع القادم في حساب آخر فتحصل على النتيجة نفسها، لأن الوصف صار مصدر الحقيقة بدل ذاكرة شخص.
القالب والحزمة ونوعا المعرّف
ثلاث كلمات تحمل معظم المعنى، وخلطها يجعل كل مفهوم لاحق أصعب.
القالب (template) هو الملف. JSON أو YAML، يعيش في Git، ويُراجَع كما تُراجَع الشيفرة، ولا يفعل شيئاً بذاته.
والحزمة (stack) هي ما تحصل عليه حين يعالج CloudFormation ذلك القالب: مجموعة موارد AWS حيّة تُدار وحدةً واحدة. حدّث الحزمة فيستنتج CloudFormation أي الموارد يجب أن تتغيّر. واحذفها فيذهب كل ما فيها افتراضياً.
وداخل القالب لكل مورد معرّف منطقي (logical ID)، وهو الاسم الذي تختاره أنت:
Resources:
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTPS from the load balancer
VpcId: !Ref AppVpc
WebSecurityGroup هو المعرّف المنطقي، ولا وجود له خارج هذا القالب وهذه الحزمة. وحين ينشئ CloudFormation المجموعة تمنحها AWS معرّفاً فعلياً (physical ID) مثل sg-0a1b2c3d4e5f. فالمعرّف المنطقي هو ما تشير به وأنت تكتب، والمعرّف الفعلي هو ما يوجد فعلاً في الحساب.
وهذا التمييز يصير حمّالاً لاحقاً. غيّر معرّفاً منطقياً ولا يرى CloudFormation إعادة تسمية، بل يرى مورداً حُذِف ومورداً آخر أُنشئ. والمعرّف الفعلي هو أيضاً ما يتغيّر حين يُستبدَل مورد أثناء تحديث، وهذا هو الفرق بين تحديث بريء وانقطاع خدمة.
وتأطير أخير يفيدك طوال هذا الموضوع: القالب تصريحي (declarative). أنت تصف الحالة النهائية لا الخطوات. لا تكتب أبداً «أنشئ الـ VPC، ثم انتظر، ثم أنشئ الـ subnet». تقول إن الـ subnet ينتمي إلى الـ VPC، ويستنتج CloudFormation الترتيب.
أقسام القالب
للقالب عشرة أقسام عليا ممكنة، واحد منها فقط مطلوب.
| القسم | الغرض |
|---|---|
Resources | مطلوب. موارد AWS المراد إنشاؤها، لكل منها معرّف منطقي وType وProperties |
Parameters | قيم تُمرَّر وقت الإنشاء أو التحديث، فيخدم قالب واحد بيئات متعدّدة |
Mappings | جدول بحث ثابت يُقرأ بـ Fn::FindInMap، مفتاحه عادةً المنطقة أو البيئة |
Conditions | تعابير منطقية مسمّاة تقرّر هل يُدرَج مورد أو خاصية |
Outputs | قيم تُرجَع بعد بناء الحزمة، ويمكن تصديرها لحزم أخرى |
Transform | ماكرو يُشغَّل على القالب، ومنه AWS::Serverless (SAM) وAWS::LanguageExtensions |
Metadata | بيانات إضافية حرّة عن القالب، منها تلميحات واجهة الـ Console |
Rules | يتحقّق من parameter أو من تركيبة parameters قبل بدء التوفير |
AWSTemplateFormatVersion | إصدار صيغة القالب، وهو 2010-09-09 |
Description | وصف نصّي للقالب |
والقالب الذي لا يعرّف سوى Resources صالح تماماً. وكل ما عداه يكسب مكانه بجعل القالب أكثر قابلية لإعادة الاستخدام أو أكثر أماناً أو أسهل قراءةً.
وترتيب الأقسام في الملف لا يعني CloudFormation شيئاً، ولا ترتيب الموارد داخل Resources. وهذا يفاجئ من يتوقّع سكربتاً.
Parameters: مدخلات بحواجز
القالب المثبّتة قيمه قالب تستخدمه مرة واحدة. وParameters هي كيف يخدم ملف واحد بيئات dev وstaging وprod.
Parameters:
EnvironmentName:
Type: String
AllowedValues: [dev, staging, prod]
Description: Which environment this stack represents
AppVpcId:
Type: AWS::EC2::VPC::Id
Description: The VPC to deploy into
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
ثلاثة أنواع مختلفة تؤدّي هناك ثلاث وظائف مختلفة، والفرق بينها أهمّ مما توحي به الصياغة.
EnvironmentName نوعه String عادي مقيّد بـ AllowedValues. والقيود (AllowedValues وAllowedPattern وMinLength وMaxLength وMinValue وMaxValue) تُفحَص قبل أن يُمَسّ أي مورد، فيفشل الخطأ المطبعي في ثوانٍ بدل أن يفشل في منتصف نشر مدّته 12 دقيقة. وأضف ConstraintDescription فتصير رسالة الخطأ «يجب أن تكون dev أو staging أو prod» بدل رمي تعبير نمطي في وجه المستخدم.
وAppVpcId يستخدم نوعاً خاصاً بـ AWS. يتحقّق CloudFormation من وجود الـ VPC فعلاً في هذا الحساب وهذه المنطقة، وتعرض الـ Console قائمة منسدلة بدل صندوق نصّ. وتوجد أنواع لمناطق التوفّر ومعرّفات AMI والنسخ وأزواج المفاتيح ومجموعات الأمان والشبكات الفرعية والوحدات والـ VPCs ومناطق Route 53 المستضافة، ولكل منها صيغة List<>.
وLatestAmiId يستخدم نوع SSM. تمرّر مفتاحاً في Parameter Store، فيجلب CloudFormation قيمته الحالية وقت الإنشاء أو التحديث. وهذا السطر الواحد يحلّ محلّ جدول ربط المناطق بمعرّفات AMI الذي كانت القوالب القديمة تحمله، وهو سبب وجود المعامل العام /aws/service/ami-amazon-latest/.... وانتبه لمعنى «الحالية» هنا: القيمة تُحلّ لحظة تشغيل عملية الحزمة، فالحزمة التي أُنشئت الشهر الماضي ما زالت تحمل معرّف AMI الشهر الماضي حتى تحدّثها.
والأنواع الأساسية هي String وNumber وList<Number> وCommaDelimitedList. ويستطيع القالب تعريف 200 parameter كحدّ أقصى.
والوهم الذي يستحقّ القتل الآن: Parameters ليست آلية أسرار. صحيح أن NoEcho: true يخفي قيمة الـ parameter بنجوم في describe-stacks وdescribe-stack-events، وهذا مفيد، لكنه مرشّح عرض لا أكثر. لا يخفي القيمة في قسم Outputs ولا في Metadata ولا في سمة Metadata على مورد، والقيمة تسافر مع استدعاء الـ API ومع سجلّات الـ CI. مكان الأسرار dynamic reference، وسيأتي في هذا الدرس.
الدوال الضمنية
القالب مضطرّ إلى الإشارة إلى أشياء لم توجد بعد. النسخة تحتاج معرّف مجموعة الأمان، ومجموعة الأمان بلا معرّف قبل أن ينشئها CloudFormation. والدوال الضمنية (intrinsic functions) هي كيف تكتب «أياً كان ما ستؤول إليه».
Ref يُرجع المعرّف الأساسي لمورد، أو قيمة parameter.
SubnetId: !Ref PrivateSubnetA # يُرجع subnet-0abc123
InstanceType: !Ref InstanceType # يُرجع قيمة الـ parameter
وما يُرجعه Ref يخصّ كل نوع مورد وموثَّق لكلٍّ منها. فهو لـ AWS::EC2::Instance معرّف النسخة، ولـ AWS::S3::Bucket اسم الدلو، ولـ AWS::IAM::Role اسم الدور لا الـ ARN، وهذا سطر يسقط كثيراً.
و**Fn::GetAtt** يُرجع خاصية مسمّاة بدل المعرّف.
RoleArn: !GetAtt AppRole.Arn
DnsName: !GetAtt AppLoadBalancer.DNSName
اقرأ الحدّ بينهما مرّة واحدة وتكفّ عن التخمين: Ref يعطيك المعرّف الواحد الذي يرشّحه نوع المورد، وGetAtt يعطيك أي خاصية منشورة أخرى بالاسم. وحين يطلب نصّ السؤال ARN بينما Ref لذلك النوع يُرجع اسماً، فالجواب GetAtt.
و**Fn::Sub** يستبدل داخل النصوص، وهو البديل المقروء لسلاسل Fn::Join المتداخلة.
BucketName: !Sub "${EnvironmentName}-app-logs-${AWS::AccountId}"
Arn: !Sub "arn:${AWS::Partition}:s3:::${LogBucket}/*"
وقيم ${AWS::...} تلك تُسمّى pseudo parameters: قيم يوفّرها CloudFormation دون أن تعرّفها. منها AWS::Region وAWS::AccountId وAWS::StackName وAWS::StackId وAWS::Partition وAWS::URLSuffix وAWS::NoValue. واستخدامها بدل القيم المثبّتة هو ما يجعل القالب قابلاً للنقل بين الحسابات والمناطق والأقسام مثل GovCloud.
وبقية مجموعة العمل باختصار:
| الدالة | تستخدمها لـ |
|---|---|
Fn::FindInMap | قراءة قيمة من قسم Mappings |
Fn::ImportValue | قراءة قيمة صدّرتها حزمة أخرى في الحساب والمنطقة نفسيهما |
Fn::GetStackOutput | قراءة أي مخرَج حزمة، بما في ذلك عبر المناطق والحسابات |
Fn::Join وFn::Split | بناء نصّ من أجزاء أو تفكيك نصّ |
Fn::Select | اختيار عنصر من قائمة برقم موضعه |
Fn::GetAZs | الحصول على مناطق التوفّر في منطقة ما كقائمة |
Fn::Base64 | ترميز user data |
Fn::Cidr | اقتطاع نطاقات CIDR للشبكات الفرعية من نطاق الـ VPC |
Fn::If وFn::Equals وFn::Not وFn::And وFn::Or | منطق الشروط |
Fn::ForEach وFn::Length وFn::ToJsonString | حلقات ومساعدات، متاحة فقط مع transform الاسمه AWS::LanguageExtensions |
والدوال الضمنية ليست صالحة في كل مكان. تستطيع استخدامها في خصائص الموارد وفي Outputs وفي سمات Metadata وسمات update policy وفي الشروط. ولا تستطيع استخدامها داخل قسم Parameters، ولهذا لا يمكن أبداً أن تكون قيمة Default محسوبة.
رسم الاعتماديات الذي لم تكتبه
هنا تكفّ كلمة «تصريحي» عن كونها مجرّدة.
Resources:
AppVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Web tier
VpcId: !Ref AppVpc
WebInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref LatestAmiId
SecurityGroupIds:
- !Ref WebSecurityGroup
لا شيء في هذا القالب يذكر ترتيباً. لكن WebSecurityGroup يشير إلى AppVpc، وWebInstance يشير إلى WebSecurityGroup، فيبني CloudFormation رسماً موجّهاً من هذه الإشارات وينشئ الموارد بترتيب يرضيه: الـ VPC، ثم مجموعة الأمان، ثم النسخة. والموارد التي لا حافة بينها تُنشَأ بالتوازي، ولهذا لا تستغرق حزمة من 40 مورداً أربعين ضعف حزمة من مورد واحد.
والحذف يسير على الرسم نفسه بالعكس. ولهذا لا تستطيع حذف حزمة VPC ونسخة منها ما زالت قائمة، ولهذا يعني ترتيب الحذف غير المتوقّع إشارةً غير متوقّعة.
وأحياناً يجب ترتيب موردين رغم أن أياً منهما لا يشير إلى الآخر. والحالة الكلاسيكية عنوان Elastic IP داخل VPC: يحتاج ربط الـ internet gateway قبل أن يُخصَّص، ولا تذكر أي خاصية من خصائصه ذلك الربط. ولهذه الحالة وحدها تعلن الحافة بنفسك:
NatEip:
Type: AWS::EC2::EIP
DependsOn: AttachGateway
Properties:
Domain: vpc
استخدم DependsOn حين تعجز الإشارة عن التعبير عن الترتيب، لا احتياطاً عاماً. فنثره في كل مكان يسلسل حزمةً كانت ستُنشَر بالتوازي، ويخفي العلاقات الحقيقية عن القارئ التالي.
Conditions وMappings: قالب واحد لبيئات كثيرة
بيئة الإنتاج تحتاج قاعدة بيانات Multi-AZ ومضيف bastion. وبيئة dev لا تحتاج أياً منهما، والدفع مقابلهما عبث. وآليتان تعالجان هذا دون تفريع القالب.
Mappings جدول بحث ثابت:
Mappings:
EnvConfig:
dev:
InstanceType: t3.small
MinSize: 1
prod:
InstanceType: m6i.large
MinSize: 3
Resources:
AppGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
MinSize: !FindInMap [EnvConfig, !Ref EnvironmentName, MinSize]
وConditions تقرّر هل يوجد الشيء أصلاً:
Conditions:
IsProduction: !Equals [!Ref EnvironmentName, prod]
Resources:
BastionHost:
Type: AWS::EC2::Instance
Condition: IsProduction
Properties:
# ...
AppDatabase:
Type: AWS::RDS::DBInstance
Properties:
MultiAZ: !If [IsProduction, true, false]
DBSnapshotIdentifier: !If [IsProduction, !Ref SnapshotId, !Ref "AWS::NoValue"]
وتفصيلان في الكتلة الأخيرة يستحقّان وقفة. سمة Condition على مورد تتحكّم في إنشاء المورد نفسه، أما Fn::If داخل خاصية فيتحكّم في قيمة تلك الخاصية. وAWS::NoValue هو الـ pseudo parameter الذي يعني «احذف هذه الخاصية تماماً»، وهو الطريقة الوحيدة للتعبير شرطياً عن غياب خاصية اختيارية.
وقاعدة القرار: Mappings للقيم التي تختلف، وConditions للموارد والخصائص التي توجد أو لا توجد.
Outputs ومشاركة القيم بين الحزم
الحزمة التي تبني VPC قليلة النفع إن لم يستطع شيء آخر معرفة معرّفات الشبكات الفرعية.
Outputs:
AppVpcId:
Description: VPC created by this stack
Value: !Ref AppVpc
Export:
Name: !Sub "${AWS::StackName}-VpcId"
Value يجعل المخرَج ظاهراً على الحزمة. وإضافة Export تنشره باسم يجب أن يكون فريداً لكل حساب ومنطقة، ولهذا صار البدء بـ ${AWS::StackName} عادةً قياسية.
ثم تستهلكه حزمة أخرى:
AppSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !ImportValue network-stack-VpcId
صارت لديك الآن ثلاث طرق لتمرير قيمة من حزمة إلى أخرى، وهي تختلف في القوة والمدى لا في الصياغة.
Fn::ImportValue | Fn::GetStackOutput | مخرجات الحزم المتداخلة | |
|---|---|---|---|
يحتاج Export من المنتج | نعم | لا | لا |
| الحساب نفسه والمنطقة نفسها | نعم | نعم | نعم |
| عبر المناطق | لا | نعم، بتمرير المنطقة | لا |
| عبر الحسابات | لا | نعم، بتمرير RoleArn | لا |
| قوة الإشارة | قوية: لا يمكن حذف المنتج ما دامت القيمة مستوردة | ضعيفة: تُحلّ وقت النشر فقط | يملكها الأب |
| هل تتغيّر القيمة المصدَّرة أثناء الاستخدام؟ | لا، الـ export مقفل | نعم، وبصمت | تُدار مع الأب |
عمود القوة هو المقايضة كلها. Fn::ImportValue يعطيك سلامة مرجعية: يرفض CloudFormation حذف الحزمة المصدّرة، ويرفض تغيير export تستورده حزمة أخرى أو إزالته. وهذه الحماية قيّمة فعلاً ومزعجة فعلاً، لأنها تعني أنك لا تستطيع إعادة تسمية export لشبكة فرعية قبل فكّ كل مستهلكيها. وFn::GetStackOutput يتنازل عن الحماية ليشتري المدى عبر المناطق والحسابات، ويحلّ القيمة من جديد في كل عملية.
إبقاء بيانات الاعتماد خارج الملف
لا تضع كلمة مرور في قالب أبداً. الموافقة على هذا سهلة ومخالفته سهوًا أسهل، فاستخدم الآلية المصنوعة لذلك: dynamic references.
AppDatabase:
Type: AWS::RDS::DBInstance
Properties:
MasterUsername: '{{resolve:ssm:/app/db/username}}'
MasterUserPassword: '{{resolve:secretsmanager:prod/app/db:SecretString:password}}'
وتوجد ثلاثة أنماط:
{{resolve:ssm:parameter-name:version}}لقيمة نصّية في Parameter Store.{{resolve:ssm-secure:parameter-name:version}}لمعامل من نوع SecureString.{{resolve:secretsmanager:secret-id:SecretString:json-key:version-stage:version-id}}لسرّ في Secrets Manager.
يحلّ CloudFormation الإشارة حين يحتاج القيمة ولا يخزّنها أبداً. ودوّر السرّ فتلتقط العملية التالية القيمة الجديدة دون تغيير القالب.
والحدود صغيرة لكنها حقيقية: يحمل القالب 60 dynamic reference كحدّ أقصى، وهي غير مدعومة داخل metadata الخاصة بـ AWS::CloudFormation::Init ولا داخل UserData في EC2، ولا تُحلّ قبل تشغيل transform. والإشارة المنتهية بشرطة مائلة عكسية لا تُحلّ إطلاقاً.
سمات الموارد التي تغيّر نطاق الضرر
سمات الموارد تجلس بجوار Type وProperties، وهي تغيّر ما يفعله CloudFormation بالمورد لا ما هو المورد.
DeletionPolicy تقرّر ما يحدث حين يغادر المورد الحزمة.
| القيمة | الأثر |
|---|---|
Delete | يحذف المورد. وهي الافتراضية لكل نوع تقريباً. |
Retain | يبقي المورد ويخرجه من نطاق CloudFormation، ويستمرّ في المحاسبة. |
RetainExceptOnCreate | مثل Retain، إلا أن المورد الذي أنشأته عملية ثم تراجعت يُحذَف. |
Snapshot | يأخذ لقطة ثم يحذف. مدعومة على وحدات EBS، وعناقيد ونسخ RDS وDocumentDB وNeptune، وعناقيد ومجموعات النسخ في ElastiCache، وعناقيد Redshift. |
والاستثناء الذي يوقع الناس: AWS::RDS::DBCluster، وأي AWS::RDS::DBInstance لا تحدّد DBClusterIdentifier، يتخلّفان إلى Snapshot لا Delete. احذف حزمةً كهذه فتحصل على لقطة باقية وفاتورة باقية.
و**UpdateReplacePolicy** تؤدّي الوظيفة نفسها لحدث مختلف. DeletionPolicy تغطّي خروج المورد من الحزمة، وUpdateReplacePolicy تغطّي استبداله أثناء تحديث، وهو ما ينشئ فيه CloudFormation مورداً فعلياً جديداً ويحذف القديم. وضبط DeletionPolicy: Retain على قاعدة بيانات ونسيان UpdateReplacePolicy: Retain يتركك محمياً من حذف الحزمة ومكشوفاً أمام الاستبدال العرضي وهو الأرجح بكثير. اضبط الاثنتين.
AppDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
# ...
و**Metadata** تلحق بيانات حرّة بالمورد. ولها استخدام تشغيلي واحد يستحقّ الحفظ: لا يعتبر CloudFormation تغيير سياسة حذف أو سياسة تحديث أو شرط أو تعريف مخرَج تحديثاً، فتعديل القالب الذي يمسّ هذه وحدها ينتج «No updates to be performed». وتغيير أي قيمة metadata يعطي CloudFormation شيئاً يراه، فيمضي التحديث.
أما CreationPolicy وUpdatePolicy، وهما السمتان اللتان تتحكّمان في انتظار الإشارات وفي التحديث التدريجي لمجموعات Auto Scaling، فمكانهما مع عمليات الحزم وتأتيان في الدرس التالي.
نصائح للاختبار
Refيُرجع المعرّف الأساسي، وFn::GetAttيُرجع أي خاصية أخرى بالاسم. وإن طلب النصّ ARN بينماRefلذلك النوع يُرجع اسماً، فالجوابGetAtt.- «القالب نفسه يجب أن يعمل في أي منطقة أو حساب» يشير إلى pseudo parameters وإلى الأنواع الخاصة بـ AWS وأنواع SSM، ويبتعد عن معرّفات AMI وأرقام الحسابات المثبّتة.
Fn::ImportValueللحساب نفسه والمنطقة نفسها فقط. وأي نصّ يذكر منطقة ثانية أو حساباً ثانياً ويطلب مخرَج حزمة يستبعده ويشير إلىFn::GetStackOutput.- «تعذّر حذف الحزمة، الـ export مستخدَم من حزمة أخرى» سلوك متوقّع لا خلل. أزل الاستيراد أولاً.
NoEchoيخفي مخرجات الوصف. وأي إجابة تعامله تشفيراً، أو حمايةً لقيم نسختها إلىOutputs، خاطئة.- القيم الحسّاسة في قالب تعني dynamic references (
ssm-secureوsecretsmanager) لا parameters. - افتراضية
Snapshotفي RDS مفضّلة لدى واضعي الأسئلة. اقرأ «لا DeletionPolicy محدّدة» مع «RDS» لقطةً، والكلمات نفسها مع «دلو S3» حذفاً. - اضبط
UpdateReplacePolicyمعDeletionPolicyكلما كانت هناك بيانات. والأسئلة عن قاعدة بيانات اختفت أثناء تحديث روتيني تختبر هذا بالضبط. - «No updates to be performed» بعد تعديل سياسة حذف أو تحديث أو شرط أو مخرَج فقط سلوك متوقّع. والعلاج تغيير قيمة
Metadataعلى مورد. DependsOnللترتيب الذي لا تعبّر عنه أي إشارة. وربط الـ internet gateway مع Elastic IP هو الزوج المعتاد.
الفكرة التي تحملها معك أن القالب وصف لحالة نهائية مرغوبة زائد رسم علاقات، وأن CloudFormation يستنتج كل ما عدا ذلك منه: ترتيب الإنشاء، وترتيب الحذف، وما يمكن أن يعمل بالتوازي، وما يجب أن يتغيّر حين تعدّل سطراً واحداً.
وهذا يطرح السؤال الذي تركه هذا الدرس مفتوحاً عمداً. صار لديك قالب ينشئ حزمة صحيحة في المرة الأولى. والدرس التالي عن المرة الثانية وكل مرة بعدها: كيف تغيّر حزمة عاملة دون كسرها، وكيف ترى ما سيفعله التحديث قبل أن يفعله، وماذا تفعل حين يعبث أحدهم بمواردك من الـ Console.
