با این پست، میخواهم مجموعهای از نکات مرتبط با CI را آغاز کنم که بیشتر بر روی GitLab متمرکز است، چرا که این ابزار اصلی من برای کارهای مربوط به CI/CD است. البته مطمئنم که بسیاری از این نکات بهراحتی میتوانند در سایر سیستمهای CI نیز به کار گرفته شوند.
در حالی که GitLab در بسیاری از موارد (میزبانی مخازن و موارد مرتبط مانند MRها، پایپلاینها، بردهای وظیفه و غیره) عملکرد بسیار خوبی دارد، اما گاهی در برخی از زمینهها فاقد ویژگیهای تخصصیتر است. با توجه به استفاده گسترده از پایپلاینها در پروژههایم، اغلب مجبور بودم به دنبال راهحلهای جایگزین و ترفندهای هوشمندانه باشم. بنابراین، اکنون قصد دارم برخی از آنها را بر اساس تجربیاتم با شما به اشتراک بگذارم.
مسئله
یک تعریف نمونه برای CI میتواند چیزی شبیه به این باشد:
stages:
- test
- build
- deploy
test:
image: golang:1.11
stage: test
script:
- go test ./...
- go vet ./...
build:
image: golang:1.11
stage: build
script:
- go build -o ./bin/my-app ./cmd/my-app
artifacts:
paths:
- bin/
deploy:
stage: deploy
script:
- ... # upload bin/ somewhere
زمانی که از دستورات Bash خام استفاده میکنید، مشکلات زیر ممکن است پیش بیاید:
- شما باید یک کامیت جدید در مخزن برنامه خود ثبت کنید که کل پایپلاین پروژه را اجرا میکند.
- تغییرات شما به دیگر شاخهها منتقل نمیشود، مگر اینکه دیگر توسعهدهندگان کامیت شما را از شاخه
Master
ادغام کنند. - تغییرات شما فقط بر روی این مخزن خاص اعمال میشود. اگر پروژههای مشابه دیگری نیز دارید، باید این تغییرات را در تمامی آنها اعمال کنید.
مخزن مشترک برای اسکریپتها
یکی از چیزهایی که در شروع کار با پایپلاینهای GitLab به آن نیاز داشتم، جایی برای اسکریپتهای مشترک بود که بتوان در چندین مخزن از آنها استفاده کرد. در آن زمان قابلیت include هنوز وجود نداشت و حتی امروز هم به تنهایی پاسخگوی نیازهای من نیست.
استفادهی مجدد از قسمتهایی از تعریف YAML یک موضوع است و استفادهی مجدد از اسکریپتهای استاندارد در همهی پروژهها موضوعی دیگر. من ترجیح میدهم کارهایی مثل “ساخت برنامه”، “بستهبندی برنامه” و “دیپلوی برنامه” در پایپلاین تنها با یک خط انجام شوند.
این روش با ذهنیت DevOps همخوانی دارد: تیم عملیات (Ops) اسکریپتها را آماده میکند و توسعهدهندگان میتوانند بهسادگی از آنها استفاده کنند. این اسکریپتها نیز با همان اصول خوب توسعه نرمافزار، مانند بررسی کد و استفاده از پایپلاینهای اختصاصی شامل تستهای واحد و بررسیهای استاتیک، توسعه داده میشوند.
برای اسکریپتها، اغلب ترکیبی از Bash و Python را به کار میبرم. Bash برای اسکریپتهای ساده خوب است، اما برای موارد پیچیدهتر استفاده از Python (یا هر زبان اسکریپتی دیگری که ترجیح میدهید) در آینده مشکلات کمتری ایجاد میکند. Python معمولاً برای دیگران خواناتر است، اما ممکن است در هر Docker imageای که در Jobهایتان استفاده میکنید، در دسترس نباشد.
پیادهسازی
ایده بسیار ساده است: هر بار که یک Job اجرا میشود، مخزن اسکریپتها را دریافت کنید. این کار به سادگی با افزودن تنظیمات سراسری در فایل .gitlab-ci.yml
قابل انجام است:
variables:
SCRIPTS_REPO: https://gitlab.com/threedotslabs/ci-scripts
before_script:
- export SCRIPTS_DIR=$(mktemp -d)
- git clone -q --depth 1 "$SCRIPTS_REPO" "$SCRIPTS_DIR"
میتوانید متغیرها را به بخش CI/CD Variables پروژه منتقل کنید. یا این تنظیمات را از مکانی دیگر وارد کنید. یا حتی از Docker imageهایی استفاده کنید که مخزن اسکریپتها را در خود دارند و در آنها git pull
اجرا کنید.
نکته: استفاده از گزینه --depth 1
باعث میشود که مخزن بهصورت shallow clone (یعنی فقط شامل آخرین کامیت از شاخه اصلی) کلون شود. این کار باعث کاهش بار اضافی در هر Job میشود. میتوانید اطلاعات بیشتر را در مستندات بخوانید.
اسکریپت نمونه
فرض کنید مجموعهای از برنامههای Golang دارید و میخواهید فرآیند ساخت آنها در همه موارد یکسان باشد.
چرا باید چنین کاری انجام دهید، وقتی go build
کافی است؟ برای مثال، ممکن است بخواهید شماره نسخه را در تمام سرویسهای خود بگنجانید. دانستن نسخهای که اجرا میشود معمولاً مفید است! قرار دادن این مرحله در یک اسکریپت مشترک تضمین میکند که هیچیک از برنامهها این مرحله را نادیده نمیگیرند.
در پست بعدی نحوه ایجاد نسخههای semver را بررسی خواهیم کرد. در حال حاضر از هش به عنوان شماره نسخه استفاده میکنیم.
#!/bin/bash
# Build generic golang application.
#
# Example:
# build-go cmd/server example-server pkg.version.Version
set -e
if [ "$#" -ne 3 ]; then
echo "Usage: $0 "
exit 1
fi
readonly package="$1"
readonly target_binary="$2"
readonly version_var="$3"
readonly bin_dir="$CI_PROJECT_DIR/bin/"
mkdir -p "$bin_dir"
go build -ldflags="-X $version_var=$CI_COMMIT_SHA" -o "$bin_dir/$target_binary" "$package"
نکات مهم:
- قسمت
set -e
را فراموش نکنید، در غیر این صورت اسکریپتهای Bash شما ممکن است بهطور نامحسوس شکست بخورند و اشکالات سختی برای پیدا کردن ایجاد کنند. - این اسکریپت از متغیرهای CI_ که توسط GitLab ارائه شدهاند استفاده میکند، اما این کار باعث پیچیدهتر شدن تستهای محلی میشود. اگر رویکرد واضحتری میخواهید، این متغیرها را به عنوان آرگومان به اسکریپت ارسال کنید.
- اسکریپت را در مخزن اسکریپتها قرار دهید و دسترسی اجرایی (
chmod +x
) را برای آن تنظیم کنید.
نحوه استفاده در مخزن برنامه
در فایل .gitlab-ci.yml
مخزن برنامه خود، از این اسکریپت به این صورت استفاده کنید:
build:
image: golang:1.11
stage: build
script:
- $SCRIPTS_DIR/golang/build . example-server main.Version
artifacts:
paths:
- bin/
احراز هویت برای مخزن خصوصی
اگر از یک مخزن خصوصی برای اسکریپتها استفاده میکنید، باید از نوعی احراز هویت برای کلون کردن آن استفاده کنید. روشهای زیر پیشنهاد میشود:
در مخزن اسکریپت: یک توکن Deploy جدید ایجاد کنید. این گزینه در Settings -> Repository -> Deploy tokens موجود است.
در مخزن برنامه: کاربر و توکن ایجاد شده را بهعنوان متغیرهای جدید به نامهای
SCRIPTS_USER
وSCRIPTS_TOKEN
اضافه کنید. این کار در Settings -> CI/CD -> Variables انجام میشود.متغیر
SCRIPTS_REPO
را در تعریف CI خود تغییر دهید:
variables:
SCRIPTS_REPO: https://$SCRIPTS_USER:$SCRIPTS_TOKEN@gitlab.com/threedotslabs/ci-scripts
نکته: اگر متغیرها را در سطح گروه اضافه کنید، مدیریت secretها در پروژههای متعدد آسانتر است.
آزمایش تغییرات
اگر قبلاً با مخازن متعددی کار کردهاید، احتمالاً میدانید که این کار باعث اضافه شدن سربار هماهنگی میشود. برای مثال، اگر یک اسکریپت ناقص را به شاخهی مستر ارسال کنید، ممکن است بیلدهای همهی پروژههای شما را خراب کند. به همین دلیل است که بازبینی کدها و انجام تستهای واحد بسیار مفید و ضروری هستند.
اگر تست واحد برای اسکریپتها سخت یا غیرممکن است، میتوانید ابتدا آنها را روی شاخههای دیگر آزمایش کنید و سپس به شاخهی مستر ادغام کنید. برای این کار کافی است بعد از کلون کردن مخزن، به شاخهی مورد نظر تغییر مسیر دهید. نکتهی جالب این است که میتوانید یک وظیفهی شکستخورده را فقط با ارسال تغییرات به اسکریپت دوباره اجرا کنید، بدون نیاز به اجرای کامل پایپلاین.
درباره Makefileها
انتقال تمام دستورات Bash به یک Makefile میتواند تعریف CI شما را مرتبتر کند و به توسعهدهندگان این امکان را بدهد که همان بررسیها و آزمایشهایی را که در پایپلاین اجرا میشود، انجام دهند. برای مثال، میتوانید یک هدف به نام make test
معرفی کنید که تستهای واحد را اجرا کند، یا make lint
برای بررسیهای استاتیک و غیره.
متأسفانه، این کار به معنی کپی کردن Makefile در تمام مخازن شما خواهد بود و واقعاً مشکلات ذکر شده در بالا را حل نمیکند. علاوه بر این، Makefileها مشکلات خاص خود را دارند، بنابراین بهتر است مطمئن شوید که دقیقاً میدانید چه کاری انجام میدهید، در غیر این صورت ممکن است با مشکلات غیرمنتظرهای روبرو شوید (مانند نادیده گرفتن بیسر و صدای خطاها).
جمعبندی
اگرچه اسکریپتی که ما استفاده کردیم بسیار ساده بود، اما این نشان میدهد که چگونه میتوان از این یکپارچهسازی برای نگهداری تمام اسکریپتهای مرتبط با CI در یک مکان استفاده کرد. در پستهای بعدی مثالهای پیشرفتهتری را نشان خواهم داد، پس منتظر باشید!