پروژه های Laravel و CI/CD

وجود ابزارهای مختلف برای تست و استقرار پروژه خیلی خوبه ، ولی وقتی ارزش دارن که ازشون استفاده کنید. یکی از بهترین ابزار های این حوزه هم Gitlab CI/CD است که تو این پست استفاده ازش برای پروژه های Laravel رو توضیح میدم.

پروژه های Laravel و CI/CD

این روز ها همه چیز داره خودکار میشه ، چنین روندی توی حوزه کاری ما بسیار مشهود تره. تا چندسال پیش وقتی یک تیم پروژه رو توسعه میداد مشکلات زیادی برای تست و بررسی و ادغام و غیره وجود داشت ولی الان با وجود ابزار های 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 درست شده :

edbizarro/gitlab-ci-pipeline-php
:coffee: Docker images for test PHP applications with Gitlab CI (or any other CI platform!) - edbizarro/gitlab-ci-pipeline-php

در نهایت کل فایل 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 بهینه تر میشه.


فایل مربوط به این آموزش در ریپازیتوری زیر موجوده :

hatamiarash7/MyWebSite_Projects
Example projects that explained in my website. Contribute to hatamiarash7/MyWebSite_Projects development by creating an account on GitHub.

اگر سوالی در مورد CI داشتید در تماس باشید