پروژه های Laravel و CI/CD
وجود ابزارهای مختلف برای تست و استقرار پروژه خیلی خوبه ، ولی وقتی ارزش دارن که ازشون استفاده کنید. یکی از بهترین ابزار های این حوزه هم Gitlab CI/CD است که تو این پست استفاده ازش برای پروژه های Laravel رو توضیح میدم.
این روز ها همه چیز داره خودکار میشه ، چنین روندی توی حوزه کاری ما بسیار مشهود تره. تا چندسال پیش وقتی یک تیم پروژه رو توسعه میداد مشکلات زیادی برای تست و بررسی و ادغام و غیره وجود داشت ولی الان با وجود ابزار های CI یا Continuous Integration تمام این مسائل به راحتی خوردن آب برطرف شده.
الان که این متن رو مینویسم ابزارهای قدرتمند و معروفی مثل Travis, Circle, Gitlab CI/CD, Semaphore, Drone و غیره موجود هستن که هرکدام ویژگی های خاص خودشون رو دارن. از بین این ها خودم با Drone و Gitlab تجربه بیشتری دارم و نکته مهمی که میتونم بهش اشاره کنم ، ادغام سورس کنترلر و CI که برای این مورد Gitlab میتونه بهترین گزینه باشه. از اونجایی که عمومیت بیشتری داره در ادامه میخوام راه اندازی و استفاده از Gitlab CI/CD رو برای پروژه های Laravel توضیح بدم.
پیش نیاز ها
برای استفاده از خود Gitlab مشکلی وجود نداره ولی از اونجایی که یکسری توضیحات در مورد Gitlab Runner میخوام بدم ، ادامه این آموزش برای افراد/شرکت هایی که Gitlab CE استفاده می کنن و روی سرور های خودشون نصب کردن مناسب تره.
برای نصب Runner میتونید به آموزش خود گیتلب در این آدرس مراجعه کنید :
https://docs.gitlab.com/runner/install/docker.html
به راحتی میشه Gitlab Runner رو با استفاده از داکر اجرا کرد.
پروژه Laravel
مسلما اگه تا حالا با Laravel کار نکرده باشید و از این فریمورک عالی برای پروژه هاتون بهره نبرده باشید ، حداقل اسمش رو شنیدید. پروژه های لاراول یکی از بهترین نمونه ها برای پیاده سازی Pipeline ها هستن ، چرا ؟
اجرا و مدیریت یک پروژه لاراول کامل از بخش های زیر تشکیل شده :
- Composer برای نصب نیازمندی ها و کتابخانه های PHP
- Yarn/NPM/Webpack برای مدیریت کتابخانه های JavaScript
- PHPUnit برای تست کردن پروژه
- Seeder برای پر کردن دیتابیس با داده های ساختگی یا استفاده از داده های پیشفرض پروژه
- تست های مربوط به CodeStyle و امنیت جهت بهبود کدهای نوشته شده
- ساخت مستندات خودکار از روی کدها ( به این لینک مراجعه کنید )
- ...
همینطور که می بینید کارهای زیادی برای انجام دادن وجود داره که اگر بخوایم تمام این موارد رو دستی بررسی کنیم هم وقت زیادی ازمون گرفته میشه هم باعث اعصاب خوردیه ... در نظر بگیرید چند نفر روی یک پروژه کار می کنن و هرکس کدهای خودش رو اضافه میکنه. در طول روز چند بار باید این موارد اجرا بشه ؟
اینجاست که به استقبال CI میریم !
تنظیم Runner
ابتدا باید Gitlab Runner رو تنظیم کنیم. این مورد با توجه به زیرساخت و شرایط شما میتونه متفاوت باشه. فایل runner در آدرس /etc/gitlab-runner/config-main.toml
به این صورته :
concurrent = 2
check_interval = 0
[[runners]]
name = "hatamiarash7/laravel"
url = "https://gitlab.hmd.d1"
token = "<<<It's secret>>>"
executor = "docker"
[runners.docker]
tls_verify = false
image = "php:7.2"
privileged = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0
به طور پیش فرض برای تست ها از PHP نسخه 7.2 استفاده می کنیم. همچنین به طور همزمان اجازه اجرای 2 Job مختلف داده شده است.
تعریف Stage ها
خب بریم سراغ قسمت اصلی کار. تمامی روند اجرا و کار با CI از طریق فایل gitlab-ci.yml
انجام میشه. اینجا Stage ها و Job های مختلفی تعریف می کنیم تا کل موارد شرح داده شده در بالا رو پوشش بدیم. چنین Stage هایی نیاز داریم :
- preparation
- building
- testing
- security
و لیست Job ها :
- composer
- yarn
- build-assets
- db-seeding
- phpunit
- codestyle
- phpcpd
- sensiolabs
نمای کلی فایل gitlab-ci.yml
ما به این صورت میشه :
stages:
- preparation
- building
- testing
- security
composer:
stage: preparation
yarn:
stage: preparation
build-assets:
stage: building
db-seeding:
stage: building
phpunit:
stage: testing
codestyle:
stage: testing
phpcpd:
stage: testing
sensiolabs:
stage: security
اگه دقت کنید در هر Stage بیشتر از یک Job داریم. از اونجایی که در تنظیمات Runner اجازه ی اجرای همزمان 2 کار داده شده ، این وظایف دوتا دوتا اجرا میشن. البته این مورد بستگی به زیرساخت خودتون داره ، اگر فضای Ram و تعداد هسته CPU خالی بیشتری دارید میتونید این مقدار رو به 3 یا 4 افزایش بدید ... شاید هم بیشتر ! توجه کنید که اجرای همزمان وظایف ، مصرف منابع خیلی بالایی هم به همراه داره. برای مثال این تصویر مربوط به اجرای همزمان 2 وظیفه است :
اجرای Asset Building قبل از تست ؟
اگه به ترتیب Stage ها دقت کنید متوجه میشید که building رو قبل از testing قرار دادم. شاید بپرسید چرا ؟ ما برعکس انجام میدیم همیشه !
دلیلش ساده است. تستی مثل این مورد رو در نظر بگیرید :
public static function boot()
{
/* ... */
$startServerCommand = '
php -S localhost:9000/ -t \
./tests/Server/public > /dev/null 2>&1 & echo $!
';
$pid = exec($startServerCommand);
ممکنه در حین نوشتن/اجرای تست ها نیاز به وب سروری داشته باشیم تا موارد کلیدی رو بررسی کنیم :
- شناسایی Uptime & downtime
- بررسی لینک ها و محتوای سایت
- بررسی Header ها
- ...
خیلی از تست هایی که به Front-End مربوط میشن نیازمند این هستن تا فایل های JS و CSS ما کامپایل بشن. اینجا Laravel Mix این وظیفه رو به عهده داره و اگر قبل از اجرای تست ها ، asset هامون آماده نباشه کل تست ها با خطا مواجه میشه.
مرحله به مرحله به نوشتن Job ها می پردازیم :
composer
composer:
stage: preparation
script:
- php -v
- composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
- cp .env.example .env
- php artisan key:generate
artifacts:
paths:
- vendor/
- .env
expire_in: 1 days
when: always
cache:
paths:
- vendor/
در این مرحله کتابخانه های PHP مورد نیاز پروژه با استفاده از composer نصب میشه. همچنین فایل env نمونه هم کپی شده و با استفاده از دستور php artisan key:generate
کلید مورد نیاز لاراول برای امور مختلف مثل رمزنگاری ایجاد میشه.
از اونجایی که دایرکتوری vendor
محل ذخیره سازی کتابخانه های نصب شده است ، به صورت artifacts و cache ذخیره اش می کنیم.
در انتها این دو مورد رو کامل توضیح میدم
yarn
yarn:
stage: preparation
script:
- yarn --version
- yarn install --pure-lockfile
artifacts:
paths:
- node_modules/
expire_in: 1 days
when: always
cache:
paths:
- node_modules/
این Job وظیفه نصب کتابخانه های جاوااسکریپت مورد نیاز برای FrontEnd رو بر عهده داره. برای این کار از Yarn استفاده می کنیم. در این مرحله هم دایرکتوری ذخیره سازی کتابخانه ها node_modules
بوده که در cache و artifacts قرارش میدیم.
build-assets
build-assets:
stage: building
# Download the artifacts for these jobs
dependencies:
- composer
- yarn
script:
- yarn --version
- yarn run production --progress false
artifacts:
paths:
- public/css/
- public/js/
- public/fonts/
- public/mix-manifest.json
expire_in: 1 days
when: always
این Job وظیفه کامپایل و ساخت تمام asset های مارو داره. قبل از شروع ، لازمه تا artifacts های دو مرحله قبل دانلود بشن چون Laravel Mix به هردو مورد نیاز داره. روند کامپایل هم به صورت production اجرا شده و خروجی های مورد نظر در artifacts مجددا ذخیره میشه.
توجه کنید که فایل
public/mix-manifest.json
هم جزو نیازمندی های پروژه است.
db-seeding
db-seeding:
stage: building
services:
- name: mysql:8.0
command: ["--default-authentication-plugin=mysql_native_password"]
# Download the artifacts for these jobs
dependencies:
- composer
- yarn
script:
- mysql --version
- php artisan migrate:fresh --seed
- mysqldump --host="${DB_HOST}" --user="${MYSQL_USER}" --password="${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" > db.sql
artifacts:
paths:
- storage/logs # for debugging
- db.sql
expire_in: 1 days
when: always
حالا نوبت به عملیات seeding یا پر کردن دیتابیس میرسه. اینجا از mysql نسخه 8 استفاده می کنیم. توجه کنید که دوباره باید artifact های مورد نیاز دانلود بشه.
ابتدا با استفاده از دستور php artisan migrate:fresh --seed
جداول مورد نیاز رو میسازیم و اونها رو seed می کنیم. سپس با دستور mysqldump
کل دیتابیس ساخته شده رو به صورت فایل sql ذخیره می کنیم.
در این مرحله به دو artifacts نیاز داریم. اولین مورد پوشه logsتا در صورت بروز خطا راحت بتونیم از دلیل اون آگاه بشیم. مورد دوم هم که همون فایل sql ساخته شده است.
phpunit
phpunit:
stage: testing
services:
- name: mysql:8.0
command: ["--default-authentication-plugin=mysql_native_password"]
# Download the artifacts for these jobs
dependencies:
- build-assets
- composer
- db-seeding
script:
- php -v
- sudo cp /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.bak
- echo "" | sudo tee /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
- mysql --host="${DB_HOST}" --user="${MYSQL_USER}" --password="${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" < db.sql
- ./vendor/phpunit/phpunit/phpunit --version
- php -d short_open_tag=off ./vendor/phpunit/phpunit/phpunit -v --colors=never --stderr
- sudo cp /usr/local/etc/php/conf.d/docker-php-ext-xdebug.bak /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
artifacts:
paths:
- ./storage/logs # for debugging
expire_in: 1 days
when: on_failure
حالا نوبت به مرحله تست پروژه رسیده. ابتدا artifact های مورد نیاز رو دانلود می کنیم. قبل از اجرای phpunit نیازه تا xdebug رو غیرفعال کنیم. از اونجایی که داریم با داکر کار می کنیم این مورد به عنوان extension جدا نصب شده.
از فایل docker-php-ext-xdebug.ini
نسخه پشتیبان تهیه میکنیم و پس از آن با خالی کردن محتوای اون ، عملا غیرفعالش می کنیم.
سپس داده های دیتابیس رو مجدد فراخوانی می کنیم و phpunit رو برای اجرای تست ها فراخوانی می کنیم. در آخر هم فایل پشتیبانی که از xdebug داشتیم رو برمیگردونیم.
اینجا هم پوشه logs رو برای دیباگ پروژه ذخیره می کنیم.
codestyle
codestyle:
stage: testing
image: lorisleiva/laravel-docker
script:
- phpcs --extensions=php app
dependencies: []
در این مرحله بحث Code Style رو بررسی می کنیم. بهترین کتابخانه برای این کار PHP_CodeSniffer است. از اونجایی که نیاز نیست درگیر نصب و تنظیم phpcs بشیم میتونیم از ایمیج های آماده ای که وجود داره استفاده کنیم. یکی از بهترین مواردی که دیدم ایمیج laravel-docker که مخصوص تست پروژه های لاراول ساخته شده. تمامی کتابخانه های مورد نیاز از قبل توش قرار داده شده و کارتون رو خیلی ساده می کنه.
با استفاده از چنین دستوری میتونیم Code Style پروژه رو بررسی کنیم :
phpcs --extensions=php app
از اونجایی که فقط کد های php مد نظر ماست از --extensions=php
استفاده می کنیم. همچنین تست رو فقط به پوشه app محدود می کنیم چون کدهای اصلی پروژه که توسط ما نوشته شده در این دایرکتوری موجوده.
phpcpd
phpcpd:
stage: testing
script:
- test -f phpcpd.phar || curl -L https://phar.phpunit.de/phpcpd.phar -o phpcpd.phar
- php phpcpd.phar app/ --min-lines=50
dependencies: []
cache:
paths:
- phpcpd.phar
یکی دیگه از بهترین مواردی که برای تست پروژه میتونیم استفاده کنیم ، CPD ها هستن. Copy/Paste Detector تمامی کدهای تکراری پروژه رو شناسایی میکنه. این مورد میتونه به شما کمک کنه تا کدهای تکراری رو حذف کنید و اونا رو ادغام کنیم. مثلا به جای نوشتن یک کد در 3 جای مختلف ، تابعی براش در نظر بگیرید تا کدهای تمیز تر و کوتاه تری داشته باشید.
برای این منظور از کتابخانه phpcpd استفاده می کنیم. از اونجایی که این کتابخانه باید به صورت فایل phar دانلود بشه ، برای استفاده های بعدی اون رو cache می کنیم.
sensiolabs
sensiolabs:
stage: security
script:
- test -d security-checker || git clone https://github.com/sensiolabs/security-checker.git
- cd security-checker
- composer install
- php security-checker security:check ../composer.lock
dependencies: []
cache:
paths:
- security-checker/
آخرین مرحله مربوط به بررسی های امنیتی پروژه است. مجموعه ی SensioLabs که از زیرمجموعه های Symfony است یکی از بهترین ابزار های تست امنیتی رو در اختیار ما گذاشته. این کتابخانه با بررسی فایل composer.lock
پروژه کلیه مشکلات امنیتی مربوط به dependency ها رو اطلاع میده.
جهت استفاده از این کتابخانه باید ریپازیتوری اون رو که در این آدرس وجود داره دانلود کرده و اقدام به نصبش کنیم. در نهایت security-checker اجرا میشه و نتایج مربوطه به دست میاد. همچنین ریپازیتوری دانلود شده در پوشه security-checker
برای استفاده های بعدی cache میشه.
تمام Job های ما این موارد بود که بخش های مختلف اجرا و تست رو شامل میشن. در کنار لیست وظایف مواردی رو باید در نظر داشته باشید.
از اونجایی که فایل پیشفرض env استفاده میشه پس باید اطلاعات مهم مورد نیاز مانند تنظیمات دیتابیس رو جدا اعمال کنیم. برای این مورد چنین قسمتی باید به فایل gitlab-ci.yml
اضافه بشه :
variables:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: arash_ci
MYSQL_PASSWORD: arash_secret
MYSQL_DATABASE: arash_ci
DB_HOST: mysql
همچنین اطلاعات دیتابیس در فایل .env.example
را نیز چنین تنظیم می کنیم :
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=arash_ci
DB_USERNAME=arash_ci
DB_PASSWORD=arash_secret
برای Image اصلی هم یک نمونه خیلی مناسب وجود داره. این docker image مخصوص تست برنامه های PHP در Gitlab CI درست شده :
در نهایت کل فایل gitlab-ci.yml
اینطور میشه :
image: edbizarro/gitlab-ci-pipeline-php:7.3
stages:
- preparation
- building
- testing
- security
variables:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: arash_ci
MYSQL_PASSWORD: arash_secret
MYSQL_DATABASE: arash_ci
DB_HOST: mysql
cache:
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
composer:
stage: preparation
script:
- php -v
- composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
- cp .env.example .env
- php artisan key:generate
artifacts:
paths:
- vendor/
- .env
expire_in: 1 days
when: always
cache:
paths:
- vendor/
yarn:
stage: preparation
script:
- yarn --version
- yarn install --pure-lockfile
artifacts:
paths:
- node_modules/
expire_in: 1 days
when: always
cache:
paths:
- node_modules/
build-assets:
stage: building
# Download the artifacts for these jobs
dependencies:
- composer
- yarn
script:
- yarn --version
- yarn run production --progress false
artifacts:
paths:
- public/css/
- public/js/
- public/fonts/
- public/mix-manifest.json
expire_in: 1 days
when: always
db-seeding:
stage: building
services:
- name: mysql:8.0
command: ["--default-authentication-plugin=mysql_native_password"]
# Download the artifacts for these jobs
dependencies:
- composer
- yarn
script:
- mysql --version
- php artisan migrate:fresh --seed
- mysqldump --host="${DB_HOST}" --user="${MYSQL_USER}" --password="${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" > db.sql
artifacts:
paths:
- storage/logs # for debugging
- db.sql
expire_in: 1 days
when: always
phpunit:
stage: testing
services:
- name: mysql:8.0
command: ["--default-authentication-plugin=mysql_native_password"]
# Download the artifacts for these jobs
dependencies:
- build-assets
- composer
- db-seeding
script:
- php -v
- sudo cp /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.bak
- echo "" | sudo tee /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
- mysql --host="${DB_HOST}" --user="${MYSQL_USER}" --password="${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" < db.sql
- ./vendor/phpunit/phpunit/phpunit --version
- php -d short_open_tag=off ./vendor/phpunit/phpunit/phpunit -v --colors=never --stderr
- sudo cp /usr/local/etc/php/conf.d/docker-php-ext-xdebug.bak /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
artifacts:
paths:
- ./storage/logs # for debugging
expire_in: 1 days
when: on_failure
codestyle:
stage: testing
image: lorisleiva/laravel-docker
script:
- phpcs --extensions=php app
dependencies: []
phpcpd:
stage: testing
script:
- test -f phpcpd.phar || curl -L https://phar.phpunit.de/phpcpd.phar -o phpcpd.phar
- php phpcpd.phar app/ --min-lines=50
dependencies: []
cache:
paths:
- phpcpd.phar
sensiolabs:
stage: security
script:
- test -d security-checker || git clone https://github.com/sensiolabs/security-checker.git
- cd security-checker
- composer install
- php security-checker security:check ../composer.lock
dependencies: []
cache:
paths:
- security-checker/
حالا با هربار اجرای Pipeline چنین صحنه زیبایی نمایان میشه :
تمام Job ها اجرا شده و پروژه تست و بررسی میشه
لزوم استفاده از artifacts و cache
وقتی یه pipeline اجرا میشه job های زیادی وجود داره که ممکنه یک پیشنیاز یکسان برای چندتا از اون ها بخوایم. برای مثال اگر دوباره فایل بالا رو مشاهده کنید ما توی چند job به پوشه ی vendor
که محل ذخیره سازی کتابخانه های PHP نیاز داریم. اگر قرار باشه توی هر مرحله دستور composer install
اجرا بشه منطقی نیست. زمان اتمام pipeline زیاد میشه ، منابع سخت افزاری تلف میشه و در بسیاری موارد job های طولانی هم درست می کنیم. اینجا است که باید از artifacts استفاده کنیم. در واقع artifacts هر چیزی است که در خارج از پروسه job باید حفظ بشه و به مرحله های بعدی انتقال داده بشه.
و اما cache ... تصور کنید کدهای شما مشکل داشته باشه و چندبار پشت سرهم بخواید pipeline رو اجرا کنید ، اینجا job های طولانی مثل نصب کتابخانه های PHP و JS داریم یا اگر یادتون باشه یه ریپازیتوری دانلود کردیم. اینکه چنین کارهایی همیشه انجام بشه مجدد باعث اتلاف منابع سخت افزاری و از همه مهمتر وقت شما میشه. پس خیلی راحت با Cache کردن چنین داده هایی روند اجرای CI بهینه تر میشه.
فایل مربوط به این آموزش در ریپازیتوری زیر موجوده :
اگر سوالی در مورد CI داشتید در تماس باشید