حالت تاریک
فصل ۴ — رویهٔ اصلی: تعریف همهچیز بهصورت کد
در فصل ۱، سه رویهٔ اصلی را شناسایی کردم که به شما کمک میکنند زیرساخت را سریع و قابلاعتماد تغییر دهید: تعریف همهچیز بهصورت کد، آزمون و تحویل مداوم همهٔ کارهای در جریان، و ساخت قطعات کوچک و ساده.
این فصل به اولین رویهٔ اصلی میپردازد و با پرسشهای ظاهراً ساده شروع میکند. چرا باید زیرساخت خود را بهصورت کد تعریف کنید؟ چه نوع چیزهایی را میتوانید و باید بهصورت کد تعریف کنید؟
در نگاه اول، «تعریف همهچیز بهصورت کد» در بافت این کتاب ممکن است بدیهی به نظر برسد. اما ویژگیهای انواع مختلف زبانها برای فصلهای بعدی اهمیت دارند. بهویژه، فصل ۵ استفاده از زبانهای declarative برای تعریف پشتههای سطح پایین («زبانهای زیرساخت سطح پایین» در صفحهٔ ۵۴) یا سطح بالا («زبانهای زیرساخت سطح بالا» در صفحهٔ ۵۵) را توضیح میدهد، و فصل ۱۶ توضیح میدهد که کد declarative یا programmatic برای ساخت ماژولها و کتابخانههای قابلاستفادهٔ مجدد در چه مواردی مناسبتر است.
چرا باید زیرساخت خود را بهصورت کد تعریف کنید
راههای سادهتری برای تأمین زیرساخت وجود دارد تا اینکه دستهای کد بنویسید و آن را به یک ابزار بدهید. به رابط کاربری وبمحور پلتفرم بروید و با کلیک و اشاره یک خوشهٔ سرور برنامه را به وجود بیاورید. به خط فرمان بروید و با مهارت CLI (رابط خط فرمان) فروشنده، مرز شبکهای غیرقابلشکست بسازید.
اما جدی بگیریم، فصلهای قبل توضیح دادند که چرا بهتر است سیستمهای خود را با کد بسازید؛ از جمله قابلیت استفادهٔ مجدد، یکنواختی و شفافیت (نگاه کنید به «رویهٔ اصلی: تعریف همهچیز بهصورت کد» در صفحهٔ ۱۰).
پیادهسازی و مدیریت سیستمها بهصورت کد به شما امکان میدهد از سرعت برای بهبود کیفیت بهره ببرید. این همان سس مخفی است که عملکرد بالا را بر اساس چهار معیار کلیدی تقویت میکند (نگاه کنید به «چهار معیار کلیدی» در صفحهٔ ۹).
چه چیزهایی را میتوانید بهصورت کد تعریف کنید
هر ابزار زیرساخت نام متفاوتی برای کد منبع خود دارد — مثلاً playbook، cookbook، manifest و template. من در معنای کلی به اینها کد زیرساخت (infrastructure code) یا گاهی تعریف زیرساخت (infrastructure definition) میگویم.
کد زیرساخت هم عناصر زیرساختی که میخواهید و هم نحوهٔ پیکربندی آنها را مشخص میکند. یک ابزار زیرساخت را اجرا میکنید تا کد خود را روی یک نمونه از زیرساخت اعمال کنید. ابزار یا زیرساخت جدید میسازد، یا زیرساخت موجود را طوری تغییر میدهد که با آنچه در کد تعریف کردهاید مطابقت داشته باشد.
برخی از چیزهایی که باید بهصورت کد تعریف کنید عبارتاند از:
- یک پشتهٔ زیرساخت (infrastructure stack)، که مجموعهای از عناصر است که از یک پلتفرم ابری زیرساخت تأمین میشوند. برای اطلاعات بیشتر دربارهٔ پلتفرمهای زیرساخت به فصل ۳ و برای معرفی مفهوم پشتهٔ زیرساخت به فصل ۵ مراجعه کنید.
- عناصر پیکربندی یک سرور، مانند بستهها، فایلها، حسابهای کاربری و سرویسها (فصل ۱۱).
- یک نقش سرور (server role) مجموعهای از عناصر سرور است که با هم روی یک نمونهٔ سرور اعمال میشوند («نقشهای سرور» در صفحهٔ ۱۷۵).
- یک تعریف تصویر سرور (server image definition) تصویری برای ساخت چندین نمونهٔ سرور تولید میکند («ابزارهای ساخت تصویر سرور» در صفحهٔ ۲۰۹).
- یک بستهٔ برنامه (application package) نحوهٔ ساخت artifact قابلاستقرار برنامه را تعریف میکند، از جمله کانتینرها (فصل ۱۰).
- پیکربندی و اسکریپتهای سرویسهای تحویل، که شامل pipelineها و استقرار میشوند («نرمافزار و سرویسهای pipeline تحویل» در صفحهٔ ۱۲۳).
- پیکربندی سرویسهای عملیات، مانند بررسیهای monitoring.
- قواعد اعتبارسنجی (validation rules)، که شامل آزمونهای خودکار و قواعد انطباق میشوند (فصل ۸).
ابزارهایی با پیکربندی خارجیشده انتخاب کنید
Infrastructure as Code، بهتعریف، شامل مشخصکردن زیرساخت در فایلهای مبتنی بر متن است. این فایلها را جدا از ابزارهایی که برای اعمال آنها روی سیستم استفاده میکنید مدیریت میکنید. میتوانید مشخصات خود را با هر ابزاری که بخواهید بخوانید، ویرایش کنید، تحلیل کنید و دستکاری کنید.
ابزارهای خودکارسازی زیرساخت غیرکدی، تعاریف زیرساخت را بهصورت دادهای ذخیره میکنند که نمیتوانید مستقیماً به آن دسترسی داشته باشید. در عوض، فقط میتوانید مشخصات را با خود ابزار استفاده و ویرایش کنید. ابزار ممکن است ترکیبی از GUI، API و رابط خط فرمان داشته باشد.
مشکل این ابزارهای جعبهٔ بسته این است که روشها و گردشکارهایی که میتوانید استفاده کنید را محدود میکنند:
- فقط در صورتی میتوانید مشخصات زیرساخت را version control کنید که ابزار versioning داخلی داشته باشد.
- فقط در صورتی میتوانید از CI استفاده کنید که ابزار راهی برای trigger کردن خودکار job هنگام تغییر داشته باشد.
- فقط در صورتی میتوانید pipeline تحویل بسازید که ابزار version control و promote کردن مشخصات زیرساخت را آسان کند.
درسهایی از کد منبع نرمافزار
الگوی پیکربندی خارجیشده، آینهٔ نحوهٔ کار بیشتر کدهای منبع نرمافزار است. برخی محیطهای توسعه کد منبع را پنهان نگه میدارند، مانند Visual Basic for Applications. اما برای سیستمهای غیرجزئی، توسعهدهندگان مییابند که نگهداشتن کد منبع در فایلهای خارجی قدرتمندتر است.
استفاده از روشهای مهندسی Agile مانند TDD، CI و CD با ابزارهای مدیریت زیرساخت جعبهٔ بسته دشوار است.
ابزاری که برای مشخصات خود از کد خارجی استفاده میکند، شما را به یک گردشکار خاص محدود نمیکند. میتوانید از سیستم version control استاندارد صنعتی، ویرایشگر متن، سرور CI و چارچوب آزمون خودکار استفاده کنید. میتوانید pipeline تحویل را با ابزاری که برای شما بهتر کار میکند بسازید.
کد خود را در یک سیستم version control مدیریت کنید
اگر چیزهای خود را بهصورت کد تعریف میکنید، قرار دادن آن کد در یک سیستم version control (VCS) ساده و قدرتمند است. با این کار به دست میآورید:
ردیابی (Traceability)
VCS تاریخچهٔ تغییرات، کسی که آنها را انجام داده و زمینهٔ «چرا» را فراهم میکند.¹ این تاریخچه هنگام اشکالزدایی مشکلات بیقیمت است.
بازگشت (Rollback)
وقتی یک تغییر چیزی را خراب میکند — و بهویژه وقتی چند تغییر با هم چیزی را خراب میکنند — مفید است بتوانید همهچیز را دقیقاً به حالت قبل برگردانید.
همبستگی (Correlation)
نگهداشتن اسکریپتها، مشخصات و پیکربندی در version control هنگام ردیابی و رفع مشکلات پیچیده کمک میکند. میتوانید با tag و شمارهٔ نسخه بین قطعات همبستگی برقرار کنید.
قابلیت مشاهده (Visibility)
همه میتوانند هر تغییری که به سیستم version control commit شده را ببینند و آگاهی situational تیم را فراهم میکند. ممکن است کسی متوجه شود که یک تغییر چیز مهمی را از قلم انداخته. اگر incident رخ دهد، مردم از commitهای اخیر که ممکن است آن را trigger کرده باشند آگاه میشوند.
قابلیت اقدام (Actionability)
VCS میتواند برای هر تغییر commitشده بهطور خودکار یک action را trigger کند. triggerها jobهای CI و pipelineهای CD را فعال میکنند.
یک چیز که نباید در source control قرار دهید، secretهای رمزنگارینشده مانند رمز عبور و کلیدهاست. حتی اگر repository کد منبع شما private باشد، history و revisionهای کد خیلی راحت leak میشوند. leak شدن secretها از کد منبع یکی از رایجترین علل نقض امنیتی است. برای روشهای بهتر مدیریت secretها به «مدیریت secretها بهصورت پارامتر» در صفحهٔ ۱۰۲ مراجعه کنید.
زبانهای کدنویسی زیرساخت
مدیران سیستم دهههاست از اسکریپت برای خودکارسازی وظایف مدیریت زیرساخت استفاده میکنند. زبانهای اسکریپتنویسی عمومی مانند Bash، Perl، PowerShell، Ruby و Python هنوز بخش ضروری toolkit تیم زیرساخت هستند.
CFEngine استفاده از زبانهای declarative و domain-specific (DSL؛ نگاه کنید به «زبانهای domain-specific زیرساخت» در صفحهٔ ۴۴) برای مدیریت زیرساخت را پیشگام شد. Puppet و سپس Chef در کنار مجازیسازی سرور و IaaS cloud رایج ظهور کردند. Ansible، Saltstack و دیگران دنبال شدند.
ابزارهای stack-oriented مانند Terraform و CloudFormation چند سال بعد با همان مدل DSL declarative ظاهر شدند. زبانهای declarative کد زیرساخت را سادهتر کردند، با جداسازی تعریف «چه زیرساختی میخواهید» از «چگونه آن را پیادهسازی کنید».
اخیراً، روند ابزارهای جدید زیرساختی که از زبانهای برنامهنویسی عمومی موجود برای تعریف زیرساخت استفاده میکنند در حال رشد است.² Pulumi و AWS CDK (Cloud Development Kit) از زبانهایی مانند TypeScript، Python و Java پشتیبانی میکنند. این ابزارها برای رفع برخی محدودیتهای زبانهای declarative ظهور کردهاند.
ترکیب کد declarative و imperative
کد imperative مجموعهای از دستورالعملهاست که مشخص میکند چگونه یک چیز اتفاق بیفتد. کد declarative مشخص میکند چه میخواهید، بدون اینکه بگوید چگونه آن را به وقوع بیندازید.³
بیش از حد کد زیرساخت در production امروز از ترکیب کد declarative و imperative رنج میبرد. معتقدم اصرار بر اینکه یکی از این دو پارادایم زبان باید برای همهٔ کد زیرساخت استفاده شود، خطاست.
یک codebase زیرساخت concernهای مختلفی دارد، از تعریف منابع زیرساخت، تا پیکربندی نمونههای مختلف منابع در غیر این صورت مشابه، تا orchestrate کردن تأمین چندین قطعهٔ وابسته به هم یک سیستم. برخی concernها را میتوان سادهترین با زبان declarative بیان کرد. برخی concernها پیچیدهترند و با زبان imperative بهتر مدیریت میشوند.
بهعنوان practitioners حوزهٔ هنوز جوان کد زیرساخت، هنوز در حال کشف مرزهای بین این concernها هستیم. ترکیب concernها میتواند به کدی منجر شود که پارادایمهای زبان را مخلوط میکند. یک حالت شکست، گسترش syntax declarative مانند YAML برای افزودن شرط و حلقه است. حالت دوم، embed کردن دادهٔ پیکربندی ساده («2GB RAM») در کد procedural است که «چه میخواهید» را با «چگونه پیادهسازی کنید» مخلوط میکند.
در بخشهای مرتبط این کتاب اشاره میکنم که فکر میکنم concernهای مختلف کجا ممکن است باشند و کدام پارادایم زبان در کجا مناسبتر است. اما حوزهٔ ما هنوز در حال تکامل است. بخش زیادی از توصیههای من اشتباه یا ناقص خواهند بود. بنابراین قصد من این است که شما، خواننده، را تشویق کنم دربارهٔ این پرسشها فکر کنید و به همهٔ ما کمک کنید کشف کنیم چه چیزی بهترین کار میکند.
اسکریپتنویسی زیرساخت
قبل از ظهور ابزارهای استاندارد برای تأمین declarative زیرساخت cloud، اسکریپت در زبانهای procedural عمومی مینوشتیم. اسکریپتهای ما معمولاً از SDK (software development kit) برای تعامل با API ارائهدهندهٔ cloud استفاده میکردند.
مثال ۴-۱ از pseudocode استفاده میکند و شبیه اسکریپتهایی است که با AWS SDK در Ruby نوشته بودم. یک سرور به نام my_application_server میسازد و سپس ابزار (خیالی) Servermaker را برای پیکربندی آن اجرا میکند.
مثال ۴-۱. نمونهٔ کد procedural که یک سرور میسازد
text
import 'cloud-api-library'
network_segment = CloudApi.find_network_segment('private')
app_server = CloudApi.find_server('my_application_server')
if(app_server == null) {
app_server = CloudApi.create_server(
name: 'my_application_server',
image: 'base_linux',
cpu: 2,
ram: '2GB',
network: network_segment
)
while(app_server.ready == false) {
wait 5
}
if(app_server.ok != true) {
throw ServerFailedError
}
app_server.provision(
provisioner: servermaker,
role: tomcat_server
)
}این اسکریپت «چه بسازد» و «چگونه بسازد» را مخلوط میکند. ویژگیهای سرور را مشخص میکند، از جمله منابع CPU و حافظه، تصویر OS برای شروع، و نقشی که باید روی سرور اعمال شود. همچنین منطق را پیادهسازی میکند: بررسی میکند آیا سرور به نام my_application_server از قبل وجود دارد تا از ساخت duplicate جلوگیری کند، و سپس قبل از اعمال پیکربندی منتظر میماند تا سرور آماده شود.
این کد نمونه تغییرات ویژگیهای سرور را مدیریت نمیکند. اگر نیاز به افزایش RAM دارید چه؟ میتوانید اسکریپت را طوری تغییر دهید که اگر سرور وجود دارد، هر ویژگی را بررسی و در صورت لزوم تغییر دهد. یا میتوانید اسکریپت جدیدی بنویسید که سرورهای موجود را پیدا و تغییر دهد.
سناریوهای واقعگرایانهتر شامل چندین سرور از انواع مختلف است. علاوه بر سرور برنامه، تیم من سرورهای web و database داشت. همچنین چندین محیط داشتیم، یعنی چندین نمونه از هر سرور.
تیمهایی که با آنها کار کردم اغلب اسکریپتهای سادهای مانند این مثال را به اسکریپت چندمنظوره تبدیل میکردند. این نوع اسکریپت argumentهایی میگرفت که نوع سرور و محیط را مشخص میکرد و از آنها برای ساخت نمونهٔ مناسب سرور استفاده میکرد.
این را به اسکریپتی تبدیل کردیم که فایلهای پیکربندی را میخواند و ویژگیهای مختلف سرور را مشخص میکند.
در حال کار روی چنین اسکریپتی بودم و میپرسیدم آیا ارزش دارد آن را بهعنوان ابزار open source منتشر کنم، که HashiCorp اولین نسخهٔ Terraform را منتشر کرد.
زبانهای declarative زیرساخت
بسیاری از ابزارهای کد زیرساخت، از جمله Ansible، Chef، CloudFormation، Puppet و Terraform، از زبانهای declarative استفاده میکنند. کد شما حالت مطلوب زیرساخت را تعریف میکند، مانند اینکه چه بستهها و حسابهای کاربری باید روی سرور باشند، یا چقدر RAM و CPU باید داشته باشد. ابزار منطق «چگونه آن حالت مطلوب به وجود بیاید» را مدیریت میکند.
مثال ۴-۲ همان سرور مثال ۴-۱ را میسازد. کد این مثال (مانند بیشتر مثالهای کد در این کتاب) یک زبان خیالی است.⁴
مثال ۴-۲. نمونهٔ کد declarative
text
virtual_machine:
name: my_application_server
source_image: 'base_linux'
cpu: 2
ram: 2GB
network: private_network_segment
provision:
provisioner: servermaker
role: tomcat_serverاین کد هیچ منطقی برای بررسی وجود سرور یا انتظار برای بالا آمدن سرور قبل از اجرای server provisioner ندارد. ابزاری که کد را اعمال میکنید از آن مراقبت میکند. ابزار همچنین ویژگیهای فعلی زیرساخت را با آنچه declarative شده مقایسه میکند و تغییرات لازم برای همراستا کردن زیرساخت را محاسبه میکند. بنابراین برای افزایش RAM سرور برنامه در این مثال، فایل را ویرایش و ابزار را دوباره اجرا میکنید.
ابزارهای declarative زیرساخت مانند Terraform و Chef «چه میخواهید» را از «چگونه بسازید» جدا میکنند. در نتیجه کد شما تمیزتر و مستقیمتر است. گاهی کد declarative زیرساخت را به پیکربندی نزدیکتر از برنامهنویسی توصیف میکنند.
آیا کد declarative کد واقعی است؟
برخی زبانهای declarative را صرفاً پیکربندی میدانند، نه «کد واقعی».
من از کلمهٔ code برای هر دو زبان declarative و imperative استفاده میکنم. وقتی نیاز به تمایز دارم، صریحاً «declarative» یا «programmable» یا واریانتهایی از آنها میگویم.
بحث دربارهٔ اینکه آیا زبان کدنویسی باید Turing-complete باشد برایم مفید نیست. حتی regular expressionها را برای برخی اهداف مفید مییابم و آنها هم Turing-complete نیستند. پس شاید وابستگی من به خلوص «برنامهنویسی واقعی» کم باشد.
Idempotency
اعمال مداوم کد رویهٔ مهمی برای حفظ یکنواختی و کنترل کد زیرساخت است، همانطور که در «اعمال مداوم کد» در صفحهٔ ۳۵۰ توضیح داده شده. این رویه شامل اعمال مکرر کد روی زیرساخت برای جلوگیری از drift است. کد باید idempotent باشد تا بتوان آن را بهطور ایمن بهطور مداوم اعمال کرد.
میتوانید کد idempotent را هر تعداد بار بدون تغییر خروجی یا نتیجه اجرا کنید. اگر ابزاری که idempotent نیست را چند بار اجرا کنید، ممکن است وضعیت را به هم بریزد.
این یک مثال از shell script است که idempotent نیست:
textecho "spock:*:1010:1010:Spock:/home/spock:/bin/bash" \ >> /etc/passwdاگر این اسکریپت را یک بار اجرا کنید نتیجهٔ مطلوب را میگیرید: کاربر spock به فایل
/etc/passwdاضافه میشود. اگر ده بار اجرا کنید، ده entry یکسان برای همان کاربر خواهید داشت.با ابزار idempotent زیرساخت، مشخص میکنید چگونه میخواهید باشد:
textuser: name: spock full_name: Spock uid: 1010 gid: 1010 home: /home/spock shell: /bin/bashهر چند بار که این کد را با ابزار اجرا کنید، فقط یک entry برای کاربر spock در
/etc/passwdتضمین میکند. بدون عوارض جانبی ناخوشایند.
زبانهای programmable و imperative زیرساخت
کد declarative وقتی خوب است که همیشه همان نتیجه را بخواهید. اما موقعیتهایی هست که بسته به شرایط نتایج متفاوت میخواهید. مثلاً کد زیر مجموعهای از VLAN میسازد. ارائهدهندهٔ cloud تیم ShopSpinner در هر کشور تعداد متفاوتی data center دارد و تیم میخواهد کدش در هر data center یک VLAN بسازد. پس کد باید بهطور پویا تعداد data centerها را کشف کند و در هر کدام یک VLAN بسازد:
text
this_country = getArgument("country")
data_centers = CloudApi.find_data_centers(country: this_country)
full_ip_range = 10.2.0.0/16
vlan_number = 0
for $DATA_CENTER in data_centers {
vlan = CloudApi.vlan.apply(
name: "public_vlan_${DATA_CENTER.name}"
data_center: $DATA_CENTER.id
ip_range: Networking.subrange(
full_ip_range,
data_centers.howmany,
data_centers.howmany++
)
)
}کد همچنین برای هر VLAN یک محدودهٔ IP اختصاص میدهد، با روش خیالی اما مفید Networking.subrange(). این روش فضای آدرس declarative شده در full_ip_range را بر اساس مقدار data_centers.howmany به فضاهای کوچکتر تقسیم میکند و یکی از آن فضاها — آنکه با متغیر data_centers.howmany index شده — را برمیگرداند.
این نوع منطق را نمیتوان با کد declarative بیان کرد، پس بیشتر ابزارهای declarative زیرساخت زبان خود را برای افزودن قابلیت imperative گسترش میدهند. مثلاً Ansible حلقه و شرط به YAML اضافه میکند. زبان پیکربندی HCL در Terraform اغلب declarative توصیف میشود، اما در واقع سه زیرزبان را ترکیب میکند. یکی expressions است که شامل شرط و حلقه میشود.
ابزارهای جدیدتر مانند Pulumi و AWS CDK به استفاده از زبانهای programmatic برای زیرساخت بازگشتهاند. بخش زیادی از جذابیت آنها پشتیبانی از زبانهای برنامهنویسی عمومی است (همانطور که در «زبانهای عمومی در برابر DSL برای زیرساخت» در صفحهٔ ۴۶ بحث شد). اما برای پیادهسازی کد زیرساخت پویاتر هم ارزشمندند.
بهجای دیدن declarative یا imperative بهعنوان پارادایم درست، باید ببینیم هر کدام برای چه نوع concernهایی مناسبترند.
زبانهای declarative در برابر imperative برای زیرساخت
کد declarative برای تعریف حالت مطلوب یک سیستم مفید است، بهویژه وقتی تنوع زیادی در نتایج مطلوب ندارید. رایج است شکل برخی زیرساخت را که میخواهید با سطح بالایی از یکنواختی تکرار کنید با کد declarative تعریف کنید. مثلاً معمولاً میخواهید همهٔ محیطهای پشتیبان یک فرایند release تقریباً یکسان باشند (نگاه کنید به «محیطهای تحویل» در صفحهٔ ۶۶). پس کد declarative برای تعریف محیطهای قابلاستفادهٔ مجدد، یا بخشهایی از محیطها (طبق الگوی پشتهٔ قابلاستفادهٔ مجدد در «الگو: پشتهٔ قابلاستفادهٔ مجدد» در صفحهٔ ۷۲) مناسب است. حتی میتوانید با پارامترهای پیکربندی نمونه، تغییرات محدود بین نمونههای زیرساخت declarative را پشتیبانی کنید، همانطور که در فصل ۷ توضیح داده شده.
با این حال، گاهی میخواهید کد قابلاستفادهٔ مجدد و قابلاشتراک بنویسید که بسته به موقعیت نتایج متفاوت تولید کند. مثلاً تیم ShopSpinner کدی مینویسد که زیرساخت سرورهای برنامهٔ مختلف را بسازد. برخی سرورها public-facing هستند و به gateway، قواعد firewall، route و logging مناسب نیاز دارند. سرورهای دیگر داخلیاند و نیازمندیهای connectivity و امنیت متفاوتی دارند. زیرساخت ممکن است برای برنامههایی که messaging، ذخیرهسازی داده و عناصر اختیاری دیگر استفاده میکنند هم متفاوت باشد.
وقتی کد declarative تغییرات پیچیدهتر را پشتیبانی میکند، منطق فزایندهای دارد. در نقطهای باید بپرسید چرا منطق پیچیده در YAML، JSON، XML یا زبان declarative دیگر مینویسید.
پس زبانهای programmable و imperative برای ساخت کتابخانهها و لایههای abstraction مناسبترند، همانطور که در فصل ۱۶ با جزئیات بیشتر بحث میشود. و این زبانها معمولاً پشتیبانی بهتری برای نوشتن، آزمون و مدیریت کتابخانهها دارند.
زبانهای domain-specific زیرساخت
علاوه بر declarative بودن، بسیاری از ابزارهای زیرساخت از DSL یا domain-specific language خود استفاده میکنند.⁵
DSL زبانی است که برای مدلسازی یک domain خاص طراحی شده — در مورد ما زیرساخت. نوشتن کد را آسانتر و درک کد را سادهتر میکند، چون نزدیک به چیزهایی که تعریف میکنید map میشود.
مثلاً Ansible، Chef و Puppet هر کدام DSL برای پیکربندی سرور دارند. زبانهای آنها construct برای مفاهیمی مانند package، file، service و user account فراهم میکنند. نمونهٔ pseudocode یک DSL پیکربندی سرور:
text
package: jdk
package: tomcat
service: tomcat
port: 8443
user: tomcat
group: tomcat
file: /var/lib/tomcat/server.conf
owner: tomcat
group: tomcat
mode: 0644
contents: $TEMPLATE(/src/appserver/tomcat/server.conf.template)این کد تضمین میکند دو بستهٔ نرمافزاری jdk و tomcat نصب شوند. سرویسی که باید در حال اجرا باشد را تعریف میکند، از جمله port و user و group. در نهایت کد تعریف میکند که فایل پیکربندی سرور از یک فایل template ساخته شود.
کد نمونه برای کسی با دانش مدیریت سیستم بهراحتی قابلفهم است، حتی اگر ابزار یا زبان خاص را نشناسد. فصل ۱۱ نحوهٔ استفاده از زبانهای پیکربندی سرور را بحث میکند.
بسیاری از ابزارهای مدیریت stack هم از DSL استفاده میکنند، از جمله Terraform و CloudFormation. مفاهیم domain خود — پلتفرمهای زیرساخت — را expose میکنند تا مستقیماً کدی بنویسید که به virtual machine، disk volume و network route اشاره کند. برای اطلاعات بیشتر دربارهٔ این زبانها و ابزارها به فصل ۵ مراجعه کنید.
DSLهای دیگر زیرساخت مفاهیم platform runtime برنامه را مدل میکنند. سیستمهایی مانند application cluster، service mesh یا application. مثالها شامل Helm chart و CloudFoundry app manifest میشوند.
بسیاری از DSLهای زیرساخت بهعنوان extension زبانهای markup موجود مانند YAML (Ansible، CloudFormation، هر چیز مرتبط با Kubernetes) و JSON (Packer، CloudFormation) ساخته شدهاند. برخی internal DSL هستند، بهعنوان subset (یا superset) یک زبان برنامهنویسی عمومی نوشته شدهاند. Chef نمونهٔ internal DSL است، بهصورت کد Ruby. برخی external DSL هستند که توسط کدی به زبان دیگر interpret میشوند. Terraform HCL یک external DSL است؛ کد به زبان Go که interpreter با آن نوشته شده ربطی ندارد.
زبانهای عمومی در برابر DSL برای زیرساخت
بیشتر DSLهای زیرساخت declarative هستند تا imperative. internal DSL مانند Chef استثناست، اگرچه حتی Chef عمدتاً declarative است.⁶
یکی از بزرگترین مزایای استفاده از زبان برنامهنویسی عمومی مانند JavaScript، Python، Ruby یا TypeScript، ecosystem ابزارهاست. این زبانها توسط IDEها بسیار خوب پشتیبانی میشوند،⁷ با قابلیتهای productivity قدرتمند مانند syntax highlighting و code refactoring. پشتیبانی آزمون بخش بهویژه مفید ecosystem یک زبان برنامهنویسی است.
ابزارهای آزمون زیرساخت زیادی وجود دارد، برخی در «اعتبارسنجی: assertion دربارهٔ منابع زیرساخت» در صفحهٔ ۱۳۵ و «آزمون کد سرور» در صفحهٔ ۱۷۶ فهرست شدهاند. اما تعداد کمی از آنها با زبانها یکپارچه میشوند تا unit test را پشتیبانی کنند. همانطور که در «چالش: آزمونهای کد declarative اغلب ارزش پایینی دارند» در صفحهٔ ۱۱۰ بحث میکنیم، شاید برای کد declarative مسئله نباشد. اما برای کدی که خروجیهای متغیرتری تولید میکند، مانند کتابخانهها و لایههای abstraction، unit testing ضروری است.
اصول پیادهسازی برای تعریف زیرساخت بهصورت کد
برای بهروزرسانی و تکامل آسان و ایمن سیستمهای زیرساخت، باید codebase را تمیز نگه دارید: قابلفهم، قابلآزمون، قابلنگهداری و قابلبهبود. کیفیت کد موضوعی آشنا در مهندسی نرمافزار است. اصول پیادهسازی زیر راهنمایی برای طراحی و سازماندهی کد برای پشتیبانی از این هدف هستند.
جداسازی کد declarative و imperative
کدی که declarative و imperative را مخلوط میکند design smell است که پیشنهاد میکند کد را به concernهای جدا تقسیم کنید.⁸
با کد زیرساخت مثل کد واقعی رفتار کنید
بسیاری codebaseهای کد زیرساخت از فایلهای پیکربندی و اسکریپتهای utility به درهمریختگی غیرقابلمدیریت تکامل مییابند. اغلب مردم کد زیرساخت را «کد واقعی» نمیدانند. همان سطح انضباط مهندسی کد برنامه را به آن نمیدهند. برای نگهداشتن codebase زیرساخت قابلنگهداری، باید آن را concern درجهٔ یک بدانید.
کد زیرساخت را طوری طراحی و مدیریت کنید که قابلفهم و قابلنگهداری باشد. از روشهای کیفیت کد مانند code review، pair programming و آزمون خودکار پیروی کنید. تیم باید از technical debt آگاه باشد و برای کمینهکردن آن تلاش کند.
فصل ۱۵ توضیح میدهد چگونه اصول طراحی نرمافزار مختلف را روی زیرساخت اعمال کنید، مانند بهبود cohesion و کاهش coupling. فصل ۱۸ راههای سازماندهی و مدیریت codebaseهای زیرساخت را برای کار آسانتر با آنها توضیح میدهد.
کد بهعنوان مستندات
نوشتن مستندات و بهروز نگهداشتن آن میتواند کار زیادی باشد. برای برخی اهداف، کد زیرساخت از مستندات نوشتهشده مفیدتر است. همیشه سابقهٔ دقیق و بهروزی از سیستم شماست:
- تازهواردان میتوانند کد را مرور کنند تا دربارهٔ سیستم بیاموزند.
- اعضای تیم میتوانند کد را بخوانند و commitها را review کنند تا ببینند دیگران چه کردهاند.
- reviewerهای فنی میتوانند از کد برای ارزیابی بهبودها استفاده کنند.
- auditorها میتوانند کد و history نسخه را review کنند تا تصویر دقیقی از سیستم بگیرند.
کد زیرساخت بهندرت تنها مستندات لازم است. مستندات سطح بالا برای context و strategy مفید است. ممکن است stakeholderهایی داشته باشید که باید جنبههایی از سیستم را بفهمند اما tech stack شما را نمیشناسند.
ممکن است بخواهید این انواع دیگر مستندات را هم بهصورت کد مدیریت کنید. بسیاری از تیمها architecture decision record (ADR) را به زبان markup مینویسند و در source control نگه میدارند.
میتوانید مطالب مفیدی مانند نمودار معماری و مرجع پارامتر را از کد بهطور خودکار تولید کنید. میتوانید این را در pipeline مدیریت تغییر قرار دهید تا هر بار کسی کد را تغییر داد مستندات بهروز شود.
جمعبندی
این فصل رویهٔ اصلی تعریف سیستم بهصورت کد را با جزئیات پوشش داد. شامل بررسی اینکه چرا باید چیزها را بهصورت کد تعریف کنید و کدام بخشهای سیستم را میتوانید بهصورت کد تعریف کنید بود. هستهٔ فصل پارادایمهای مختلف زبان زیرساخت را بررسی کرد. این ممکن است موضوعی abstract به نظر برسد. اما استفاده از زبانهای درست به روشهای درست چالش حیاتی برای ساخت زیرساخت مؤثر است — چالشی که صنعت هنوز حل نکرده. پس پرسش اینکه در بخشهای مختلف سیستم از چه نوع زبانی استفاده کنیم و پیامدهای آن تصمیمها، موضوعی است که در سراسر این کتاب بازمیگردد.
بخش دوم
کار با پشتههای زیرساخت
¹ زمینهٔ «چرا» به این بستگی دارد که مردم commit message مفید بنویسند.
² «اخیراً» همانطور که در میانهٔ ۲۰۲۰ این را مینویسم.
³ گاهی به کد یا زبان imperative «programmable» میگویم، اگرچه دقیقترین اصطلاح نیست، چون intuitiveتر از «imperative» است.
⁴ از این زبان pseudocode برای توضیح مفاهیم بدون وابستگی به ابزار خاص استفاده میکنم.
⁵ Martin Fowler و Rebecca Parsons DSL را در کتاب Domain-Specific Languages (Addison-Wesley Professional) «زبان کوچک، متمرکز بر جنبهٔ خاصی از سیستم نرمافزاری» تعریف میکنند.
⁶ میتوانید کد Ruby imperative را در Chef recipe مخلوط کنید، اما messy میشود. Chef recipeها را در دو فاز interpret میکند: اول compile کد Ruby، سپس اجرای کد برای اعمال تغییرات روی سرور. کد procedural معمولاً در فاز compile اجرا میشود. این Chef code که procedural و imperative را مخلوط میکند را سختفهم میکند. از سوی دیگر، کد imperative هنگام نوشتن Chef provider — نوعی کتابخانه — مفید است. این ایده را تقویت میکند که زبان imperative برای کد کتابخانه و زبان declarative برای تعریف زیرساخت خوب کار میکند.
⁷ Integrated Development Environment، ویرایشگر تخصصی برای زبانهای برنامهنویسی.
⁸ اصطلاح design smell از code smell میآید. «بو» ویژگی observable در سیستم است که نشان میدهد مشکل زیرین وجود دارد. در مثال متن، کدی که constructهای declarative و imperative را مخلوط میکند یک بو است. این بو پیشنهاد میکند کد شما شاید چند کار مختلف انجام میدهد و بهتر است آنها را به قطعات مختلف کد — شاید به زبانهای مختلف — جدا کنید.