Thử dùng Envoy của Laravel để viết script deploy cho ứng dụng web

Thử dùng Envoy của Laravel để viết script deploy cho ứng dụng web

Giới thiệu về Nginx Cache cho các website sử dụng WordPress
Giới thiệu về Railgun
Rendering client-side so với server-side

Hiện tại, Laravel là framework nổi tiếng nhất trong cộng động lập trình PHP với rất nhiều thư viện có sẵn phụ giúp cho công việc lập trình. Hôm nay chúng ta thử sử dụng Envoy – thư viện hỗ trợ việc viết lệnh thực thi trên remote server chỉ bằng những cú pháp đơn giản.

Tạo một file Envoy

Đầu tiên chúng ta dùng composer để cài đặt Envoy:

composer global require laravel/envoy

Envoy sử dụng cú pháp của template engine Blade, nên chúng ta sẽ tạo ra một file Envoy có tên là Envoy.blade.php.

1. Khởi đầu file, chúng ta khai báo thông tin của remote server mà chúng ta muốn thực thi lệnh trên đó.

@servers(['web' => 'deployer@192.168.1.1'])

servers là từ khóa, tham số theo sau là một mảng với chỉ mục là tên của remote server (ở ví dụ trên là web), giá trị đi kèm là thông tin của remote server: tên user mà chúng ta sẽ login vào và địa chỉ ip (ở ví dụ trên là deployer192.168.1.1)

2. Tiếp theo, chúng ta khai báo giá trị của các biến mà chúng ta dự định sử dụng:

@setup 
  $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git';
  $releases_dir = '/var/www/app/releases'; 
  $app_dir = '/var/www/app';
  $release = date('YmdHis');
  $new_release_dir = $releases_dir .'/'. $release;
@endsetup

Cú pháp khai báo là @setup@endsetup. Những biến phía trên bao gồm $repository là địa chỉ git của dự án, biến $new_release_dir là đường dẫn thư mục mà chúng ta sẽ deploy code lên. Vì muốn lưu trữ lại lịch sử những phiên bản đã deploy, nên mỗi khi deploy code lên chúng ta sẽ tạo ra một thư mục mới có đính kèm thông tin thời điểm deploy vào trong tên thư mục.

3. Tiếp theo, chúng ta khai báo các story được thực hiện trong file. Trong Envoy, mỗi story sẽ là tập hợp của nhiều task. Mỗi task là tâp hợp của nhiều câu lệnh được nhóm lại với nhau

@story('deploy') 
  clone_repository 
  install
  update_symlinks
@endstory

Cú pháp khai bóa là @story @endstory. Với ví dụ trên, chúng ta định nghĩa ra một story có tên là deploy, trong story đó có 3 task: clone_repository để lấy code từ git server về, install để chạy những lệnh cài đặt cho dự án, và update_symlinks để tạo symlink từ thư mục được định nghĩa trong apache server đến thư mục deploy code của chúng ta.

4. Chúng ta sẽ tiến hành định nghĩa chi tiết từng lệnh trong task. Cú pháp khai báo là @task và @endtask. Đầu tiên là task clone_repository:

@task('clone_repository') 
  echo 'Cloning repository' 
  [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }} 
  git clone --depth 1 {{ $repository }} {{ $new_release_dir }} 
  cd {{ $releases_dir }} 
  git reset --hard {{ $commit }} 
@endtask

Bên trên gồm các lệnh để tạo và lấy source code từ git server vào thư mục new_release_dir được định nghĩa ở bước 2. Commit muốn lấy từ git server sẽ được định nghĩa trong biến $commit được truyền vào từ tham số khi chạy file envoy.

Task thứ hai install:

@task('install')
  echo "Starting deployment ({{ $release }})"
  cd {{ $new_release_dir }}
  mkdir -p ./bootstrap/cache
  composer install && composer update
  php artisan migrate
  npm install
  npm run production
@endtask

Bên trên gồm các lệnh để cài đặt các thư viện cho backend(php) bằng composer, và cài đặt các thư viện cho frontend (javascript) bằng npm.

Task thứ ba update_symlinks:

@task('update_symlinks')
 echo "Linking directories"
 rm -rf {{ $new_release_dir }}/storage
 ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage
 ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env
 ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current
@endtask

Bên trên gồm các lênh tạo symlink (vào thư mục đã deploy) cho file môi trường .env;  tạo symlink (vào thư mục đã deploy) cho thư mục storage chứa các file được tạo ra trong quá trình hoạt động của ứng dụng; tạo symlink từ thư mục deploy vào thư mục current để cố định việc khai báo thư mục trong file thiết lập của apache.

5. Tổng kết, chúng ta có một file Envoy như sau:

@servers(['web' => 'deployer@192.168.1.1'])

@setup 
  $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git';
  $releases_dir = '/var/www/app/releases'; 
  $app_dir = '/var/www/app';
  $release = date('YmdHis');
  $new_release_dir = $releases_dir .'/'. $release;
@endsetup

@story('deploy') 
  clone_repository 
  install
  update_symlinks
@endstory

@task('clone_repository') 
  echo 'Cloning repository' 
  [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }} 
  git clone --depth 1 {{ $repository }} {{ $new_release_dir }} 
  cd {{ $releases_dir }} 
  git reset --hard {{ $commit }} 
@endtask

@task('install')
  echo "Starting deployment ({{ $release }})"
  cd {{ $new_release_dir }}
  mkdir -p ./bootstrap/cache
  composer install && composer update
  php artisan migrate
  npm install
  npm run production
@endtask

@task('update_symlinks')
 echo "Linking directories"
 rm -rf {{ $new_release_dir }}/storage
 ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage
 ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env
 ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current
@endtask

Để chạy file Envoy ở trên chúng ta cần thiết lập SSH Keys để có thể kết nối SSH giữa máy chạy file envoy và remote server, giữa remote server và git server. Sau đó vào thư mục chứa file Envoy và chạy câu lệnh như sau:

~/.composer/vendor/bin/envoy run deploy --commit=develop

Bên trên với việc truyền tham số –commit=develop chúng ta sẽ deploy code trên branch develop từ git server lên remote server.

Đưa script deploy vào jenkins server

1. Đầu tiên chúng ta có thể tạo một dockerfile để chuẩn bị môi trường cho Envoy:

FROM centos:7

RUN yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
        http://rpms.remirepo.net/enterprise/remi-release-7.rpm \
        yum-utils && \
    yum-config-manager --enable remi-php72 && \
    yum install -y \
        php-bcmath \
        php-cli \
        php-mysqlnd \
        php-mssql \
        php-xml \
        php-pgsql \
        php-gd \
        php-mcrypt \
        php-ldap \
        php-imap \
        php-soap \
        php-tidy \
        php-mbstring \
        php-opcache \
        php-pdo \
        php-pecl-apcu \
        php-pecl-apcu-bc \
        php-pecl-geoip \
        php-pecl-igbinary \
        php-pecl-imagick \
        php-pecl-redis \
	php-posix \
        unzip && \
    yum clean all

RUN curl -sS https://getcomposer.org/installer | php && \
    mv composer.phar /usr/local/bin/composer

RUN yum install -y openssh-clients

RUN composer global require laravel/envoy && composer global update

Với file docker kể trên, chúng ta đã cài đặt các gói php, composer, openssh-clients và laravel/envoy lên image Centos 7.

2. Chúng ta tạo một pipeline script trên jenkin

node("master") {
    withCredentials([sshUserPrivateKey(credentialsId: "test_aws_webserver", keyFileVariable: 'awsPrivateKey')]) {
        docker.build("docker/deployserver", "usr/local/test_deploy/docker/deploy_server")
	docker.image("docker/deployserver").inside("-u root:root -v'/usr/local/test_deploy/Envoy.blade.php':/usr/local/test_deploy/Envoy.blade.php -v'${awsPrivateKey}':/usr/local/key.pem") {
	    sh 'mkdir -p ~/.ssh'
	    sh 'exec ssh-agent bash'
	    sh 'echo -e "Host *\n\tStrictHostKeyChecking no\n\n"  > ~/.ssh/config'
	    sh "eval \$(ssh-agent -s) && ssh-add /usr/local/key.pem && cd /var/www/test_deploy/ && ~/.composer/vendor/bin/envoy run deploy --commit=develop"
        }
    }
}

Đoạn script trên gồm: lấy đường dẫn private key của server Amazon AWS (được lưu trong jenkins với id là test_aws_webserver) ra biến awsPrivateKey; tạo và khởi động một docker container từ dockerfile với đường dẫn /usr/local/test_deploy/docker/deploy_server; mount awsPrivateKey và file Envoy được lưu trên đường dẫn /usr/local/test_deploy/Envoy.blade.php vào container; add private key awsPrivateKey vào ssh-agent của container; chạy file Envoy.blade.php bên trong container đó với tham số truyền vào là develop.

Như vậy chỉ cần vào jenkins và click nút Build Now là ta đã thành công trong việc deploy toàn bộ source code thuộc branch develop lên server Amazon AWS.

3. Tích hợp vào CI

Thay vì viết một script dành riêng cho deploy ở ví dụ trên, chúng ta có thể chèn nó vào một pipeline script khác đã có sẵn việc tự động chạy unit test.

node("master") {
    checkout scm
    def gitLabUrl = "https://gitlab.com/api/v4/projects/test%2project/statuses/"
    def gitCommit =  sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
    
    withCredentials([string(credentialsId: "gitlab_token1", variable: 'personAccessToken')]) {
        httpRequest(customHeaders: [[name: 'PRIVATE-TOKEN', value: personAccessToken]], httpMode: "POST", 
            url: gitLabUrl + gitCommit + "?name=jenkin-ci&state=running", validResponseCodes: '200:400') 
    }
    
    try {
        stage('Build') {
            withCredentials([sshUserPrivateKey(credentialsId: "spyoyaku_aws_webserver", keyFileVariable: 'awsPrivateKey')]) {
                docker.build("docker/webserver", "${env.WORKSPACE}" + "/docker/web_server")
                docker.image('mysql:5.6.40').withRun('--env-file '+ "${env.WORKSPACE}" + '/docker/mysql_init/env_list'){ c ->
                    docker.image("docker/webserver").inside("-u root:root --link ${c.id}:test.db
                    -v'${env.WORKSPACE}':/var/www/test-project" -v'${awsPrivateKey}':/usr/local/key.pem") {
                        stage('Install') {
                            sh 'cd /var/www/test-project && cp .env.test .env'
                            sh 'cd /var/www/test-project && composer install && composer update'
                            sh 'cd /var/www/test-project && php artisan key:generate'
                            sh 'cd /var/www/test-project && php artisan migrate'
                            sh 'cd /var/www/test-project && php artisan db:seed'
                        }
 
                        stage('Test') {
                            sh 'cd /var/www/test-project && vendor/bin/phpunit'
                        }

                        stage('Deploy') {
                            input("Click to Deploy")
                            sh 'mkdir -p ~/.ssh'
                            sh 'exec ssh-agent bash'
                            sh 'echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
                            sh "eval \$(ssh-agent -s) && ssh-add /usr/local/key.pem && cd /var/www/test-project \
                                && ~/.composer/vendor/bin/envoy run deploy --commit=${gitCommit}"
                        }
                    }
                }
            }
        }

        withCredentials([string(credentialsId: "gitlab_token1", variable: 'personAccessToken')]) {
            httpRequest(customHeaders: [[name: 'PRIVATE-TOKEN', value: personAccessToken]], httpMode: "POST", url: gitLabUrl + gitCommit + "?name=jenkin-ci&state=success")   
        }
    } catch(error) {
        withCredentials([string(credentialsId: "gitlab_token1", variable: 'personAccessToken')]) {
            httpRequest(customHeaders: [[name: 'PRIVATE-TOKEN', value: personAccessToken]], httpMode: "POST", url: gitLabUrl + gitCommit + "?name=jenkin-ci&state=failed")   
        }
        throw error
    }
}

Chúng ta dùng docker container chạy unit test để chạy script deploy luôn. Các file docker và Envoy có thể để ngay trên git server. Sau khi jenkins kéo code về, nó sẽ tạo docker container để cài đặt các gói thư viện của dự án, chạy unit test, rồi chờ người dùng ấn nút để deploy code từ commit mà chúng ta vừa chạy test lên remote server.

Nếu dự án chúng ta đang dùng framework laravel và cũng không có đòi hỏi phức tạp hơn cho việc deploy (như muốn có những câu lệnh tự động quản lý version deploy chẳng hạn), chúng ta có thể cân nhắc sử dụng Envoy vì sự đơn giản và tiện lợi.

COMMENTS