AWS Certified CloudOps Engineer - Associate

أساسيات CloudFormation

كيف يصف قالب CloudFormation بنيتك التحتية: أقسام القالب، وParameters بحواجز حقيقية، والدوال الضمنية، ورسم الاعتماديات الذي تبنيه الخدمة نيابةً عنك، وسمات الموارد التي تقرّر ما ينجو من الحذف.

متوسط 30 دقائق 7 أهداف التعلّم
  1. ميّز بين القالب والحزمة والمعرّف المنطقي والمعرّف الفعلي
  2. حدّد أقسام القالب واشرح أيّها المطلوب
  3. اختر نوع Parameter وقيوده المناسبة لرفض المدخل الخاطئ قبل إنشاء أي مورد
  4. اختر الدالة الضمنية الصحيحة لكل إشارة، واشرح لماذا لا يتبادل Ref وFn::GetAtt المواقع
  5. توقّع ترتيب إنشاء الموارد، واعرف متى يصبح DependsOn ضرورياً
  6. قارن بين Fn::ImportValue وFn::GetStackOutput ومخرجات الحزم المتداخلة لتمرير القيم
  7. أبقِ بيانات الاعتماد خارج القالب عبر 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::ImportValueFn::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.