استقرار خودکار پروژه های Laravel با Gitlab CI

آخرین مرحله از روند توسعه یک پروژه مربوط به مباحث استقرار اون میشه. خودکارسازی این کار کمک زیادی به صرفه جویی در وقت میکنه و باعث میشه نیروهای فنی مجموعه تمرکز خودشون رو روی موارد دیگه ای قرار بدن. در این پست به بررسی چنین کاری می پردازیم.

استقرار خودکار پروژه های Laravel با Gitlab CI

اگر پست قبلی رو مطالعه کرده باشید ، با استفاده از Gitlab یکسری Pipeline برای امور مختلف ساختیم و بخش اول کار یعنی CI انجام شد.

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

حالا نوبت به استقرار پروژه میرسه. پس از اینکه تمامی تست ها انجام شد و طبق نیازمندی های خودمون مطمئن شدیم آخرین تغییرات Commit شده شرایط مناسب برای استقرار نهایی رو داره ، این کار رو انجام میدیم.

توجه کنید که در این پست ساده ترین روش خودکارسازی استقرار رو توضیح میدم. استفاده از ابزارهایی مانند Kubernets و ادغام اون با CI/CD یکی از بهترین کارهاست ولی در عین حال پیچیدگی های خودش رو داره و هزینه های مازادی ممکنه برای تیم های کوچیک ایجاد کنه.

برای استقرار پروژه های Laravel کتابخانه های متعددی نوشته شده که با جستجوی عبارت Laravel Deployer به لیست بلندی از اون ها بر میخورید. در این پست از این گزینه استفاده می کنیم :

https://deployer.org

نصب

قبل از تنظیم Pipeline باید کتابخانه deployer رو نصب و تنظیم کنیم. برای این کار مسلما از composer استفاده می کنیم :

composer require deployer/deployer deployer/recipes

پس از اینکه پکیج ها نصب شد با استفاده از دستور زیر عملیات initialize را انجام دهید تا فایل deploy.php مربوط به تنظیمات هم برای شما ساخته شود :

./vendor/bin/dep init

به صورت پیش فرض deployer از Git برای استقرار و بروزرسانی پروژه ها استفاده می کنه. به این صورت که به سرور شما از طریق SSH متصل میشه و سپس git pull انجام میده. از اونجایی که ما این عملیات رو در خود Gitlab انجام میدیم پس Pipeline ما مستقیما آخرین تغییرات رو بررسی کرده و build میکنه و در نهایت یک پروژه آماده برای استقرار داریم و در نتیجه نیاز نیست از روش پیش فرض استفاده کنیم. راه جایگزین استفاده از rsync برای کپی کردن مستقیم فایل هاست.

فایل deploy.php را باز کرده و کدهای زیر را به ترتیب قرار بدید :

<?php

namespace Deployer;

require 'recipe/laravel.php';
require 'recipe/rsync.php';

set('application', getenv('CI_PROJECT_NAME'));
set('ssh_multiplexing', true);

set('rsync_src', function () {
    return __DIR__;
});

ابتدا کتابخانه های مورد نظرمون رو وارد می کنیم. سپس مقادیری را باید تنظیم کنیم :

  • application : اینجا باید نام پروژه قرار داده بشه که با استفاده از متغیر CI_PROJECT_NAME این کار انجام میشه.
  • ssh_multiplexing : با فعال کردن این گزینه سرعت کار افزایش پیدا میکنه بیشتر بخوانید
  • rsync_src : اینجا باید مسیر root پروژه تنظیم بشه. اگر ریپازیتوری مستقیما به پروژه اشاره میکنه لازم به تغییر نیست.
add('rsync', [
    'exclude' => [
        '.git',
        '/.env',
        '/storage/',
        '/vendor/',
        '/node_modules/',
        '.gitlab-ci.yml',
        'deploy.php',
    ],
]);

این بخش مانند .gitignore عمل میکنه و تمامی دایرکتوری ها و فایل هایی که نیاز به sync شدن ندارن رو مشخص می کنیم.

// Copy secrets
task('deploy:secrets', function () {
    file_put_contents(__DIR__ . '/.env', getenv('DOT_ENV'));
    upload('.env', get('deploy_path') . '/shared');
});

در این قسمت یک task برای تنظیم env ها نیاز داریم. چرا ؟ خب مسلما پروژه لاراول به فایل .env نیاز داره و از اونجایی که اطلاعات مهمی در این فایل ذخیره میشه امکان قرار دادن اون در ریپازیتوری وجود نداره. پس تمام اطلاعاتش رو به صورت Environment در Gitlab ذخیره کرده و با استفاده از این بخش اون رو به صورت فایل در میاریم.

// Hosts
host('production.app.com') // Name
    ->hostname('165.22.242.104') // Hostname or IP
    ->stage('production') // Deployment stage
    ->user('deploy') // User
    ->set('deploy_path', '/var/www'); // Path

در این بخش باید اطلاعات سرور داده بشه :

  • hostname : شامل آدرس آی پی یا دامنه برای اتصال به سرور
  • stage : با توجه به پروژه استیج مورد نظر رو قرار بدید مثلا production, staging, dev یا هر چیز دیگه
  • user : نام کاربری که از طریق SSH بهش متصل میشید
  • set('deploy_path', ...) : تنظیم دایرکتوری مقصد که باید پروژه قرار بگیره و sync بشه

توجه کنید که اتصال با SSH از طریق کلمه عبور انجام نمیشه و باید کلید های مورد نظر ساخته بشه که در ادامه در موردش صحبت می کنیم

after('deploy:failed', 'deploy:unlock');

task('deploy', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'rsync',                // Deploy code
    'deploy:secrets',       // Deploy secrets
    'deploy:shared',
    'deploy:vendors',
    'deploy:writable',
    'artisan:storage:link', // |
    'artisan:view:cache',   // |
    'artisan:config:cache', // | Laravel commands
    'artisan:optimize',     // |
    'artisan:migrate',      // |
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
]);

در نهایت برای اجرای عملیات deploy از این بخش استفاده میشه. با تنظیم deploy:failed که همانند یک رخداد عمل میکنه می تونیم عملیات های مختلفی رو در صورت بروز خطا انجام بدیم.

در آخرین task دستوراتی اجرا میشن که ترتیب اونها مهمه :

  • ابتدا دستورات مربوط به خود deployer اجرا میشن
  • با استفاده از دستور rsync کل فایل ها کپی میشن
  • با دستور deploy:secrets تسکی که در بالا تنظیم کرده بودیم اجرا میشه
  • در ادامه تمامی دستورات لازم برای لاراول ( با استفاده از artisan ) اجرا میشه

فایل deploy.php ما در نهایت به این صورت خواهد بود :

<?php

namespace Deployer;

require 'recipe/laravel.php';
require 'recipe/rsync.php';

set('application', getenv('CI_PROJECT_NAME'));
set('ssh_multiplexing', true);

set('rsync_src', function () {
    return __DIR__;
});

// Rsync
add('rsync', [
    'exclude' => [
        '.git',
        '/.env',
        '/storage/',
        '/vendor/',
        '/node_modules/',
        '.gitlab-ci.yml',
        'deploy.php',
    ],
]);

// Copy secrets
task('deploy:secrets', function () {
    file_put_contents(__DIR__ . '/.env', getenv('DOT_ENV'));
    upload('.env', get('deploy_path') . '/shared');
});

// Hosts
host('production.app.com') // Name
    ->hostname('165.22.242.104') // Hostname or IP
    ->stage('production') // Deployment stage
    ->user('deploy') // User
    ->set('deploy_path', '/var/www'); // Path

after('deploy:failed', 'deploy:unlock');

task('deploy', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'rsync',                // Deploy code
    'deploy:secrets',       // Deploy secrets
    'deploy:shared',
    'deploy:vendors',
    'deploy:writable',
    'artisan:storage:link', // |
    'artisan:view:cache',   // |
    'artisan:config:cache', // | Laravel commands
    'artisan:optimize',     // |
    'artisan:migrate',      // |
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
]);

تنظیم سرور

همونطور که در بالا مشاهده کردید روند اتصال با SSH با کلمه عبور انجام نمیشه و در نتیجه ما نیاز داریم تا کلید های خصوصی و عمومی خودمون رو برای این کار داشته باشیم. الان که دارید بهش فکر می کنید شاید بگید کار سختیه ولی اصلا اینطور نیست ... پیاده سازی چنین چیزی به راحتی در Gitlab امکان پذیره.

ابتدا کلید های عمومی و خصوصی خودتون رو با استفاده از دستور ssh-keygen بسازید و کلید عمومی رو به سرور معرفی کنید :

ssh-copy-id -i ~/.ssh/mykey user@host

محتوای کلید خصوصی رو برای بعدا نیاز داریم.

یکی دیگه از مواردی که بهش نیاز داریم fingerprint سرور میشه. اگه دقت کرده باشید اولین بار که میخواید به سروری متصل بشید چنین پیغامی برای شما میاد :

با تایید کردن ، آدرس آی پی سرور به فایل ssh/known_hosts./~ اضافه میشه و در واقع شما این سرور رو به لیست سرور های آشنا و مطمئن اضافه می کنید. حالا سوال اینجاست که ما در Gitlab CI چه کنیم ؟ اونجا هم به چنین فایلی نیاز داریم !

برای این مورد هم میتونیم این کار رو از طریق Env انجام بدیم و fingerprint سرور رو به صورت متغیر ذخیره کنیم. برای این کار دستور زیر رو اجرا کنید :

ssh-keyscan -t rsa <server IP>

خروجی این دستور همان fingerprint سرور شماست که باید برای استفاده های بعدی اون رو ذخیره کنید.

تنظیم Gitlab CI

حالا نوبت به آخرین بخش میرسه. برای استفاده از Gitlab CI به فایل gitlab-ci.yml. نیاز داریم که در پست قبلی اون رو ایجاد کردیم :

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

اینجا فقط کافیه تا stage جدیدی برای بحث deploy به اون اضافه بشه :

stages:
  - preparation
  - building
  - testing
  - security
  - deploy

...

deploy:
  stage: deploy
  only:
    - master
  dependencies:
    - phpunit
  before_script:
    - mkdir -p ~/.ssh
    - eval $(ssh-agent -s)
    - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
    - echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add - > /dev/null
  script:
    - dep deploy production --tag=$CI_COMMIT_REF_NAME -vvv

این job مسلما باید پس از اجرای موفق مراحل قبل اجرا بشه. تمامی پیش نیاز ها رو میتونید با توجه به نیازتون قرار بدید ، برای مثال اینجا من فقط phpunit رو تنظیم کردم که قبل از استقرار مطمئن بشم تست ها با موفقیت پشت سر گذاشته شده.

این job مسلما باید روی branch اصلی مخزن اجرا بشه تا احیانا کدهای مشکل دار deploy نشن. حتما این مورد رو مد نظر قرار بدید ( البته خب بستگی به ساختار پروژه و روش استفاده شما از git داره ولی خب راه اصولیش اینه که مستقیم به branch اصلی تغییرات مهم فرستاده نشه ).

خب حالا نوبت به اجرای دستورات مورد نظرمون میرسه تا روند deploy اتفاق بیوفته. اول باید پیش نیازهامون رو داشته باشیم :

  • از اونجایی که همش در مورد SSH صحبت کردیم ، دایرکتوری پیش فرض اون رو میسازیم
  • سپس ssh-agent رو اجرا می کنیم
  • مقدار fingerprint سرور که به صورت Env ذخیره شده رو در فایل مورد نظر قرار میدیم
  • دسترسی ها تنظیم میشه
  • با استفاده از ssh-add کلید خصوصی به gitlab اضافه میشه

و در نهایت با اجرای دستور dep deploy production عملیات deploy آغاز میشه.

ما تا اینجا 3 متغیر تعریف کردیم که باید به صورت ENV به Gitlab اضافه بشن :

  • DOT_ENV : شامل اطلاعات فایل env. مربوط به Laravel میشه که در فایل deploy.php ازش استفاده می کنیم
  • SSH_PRIVATE_KEY : کلید خصوصی ساخته شده جهت اتصال به سرور
  • SSH_KNOWN_HOSTS : داده مربوط به fingerprint سرور مقصد

حالا تمام تغییرات رو Push کنید و از اجرای درست Pipeline مطمئن بشید. با اتمام این بخش یک روند کامل CI/CD برای پروژه لاراول پیاده سازی میشه و نتیجه اش خودکار سازی کلیه امور مربوط به کار شماست. از اجرای تست ها گرفته تا بررسی Code Style و استقرار نهایی پروژه بدون لحظه ای Downtime


توجه کنید که مطالب آموزش داده شده در این 2 پست معیار مناسبی برای تمام پروژه ها نیست و لازمه تا با در نظر گرفتن شرایط ، تغییراتی رو اعمال کنید. شاید نیاز به stage های دیگه ای داشته باشید ، دستورات artisan متفاوتی اجرا کنید و غیره.