Pipeline CI/CD hoàn chỉnh với Laravel Framework
Chuỗi bài về Jenkins
3. Pipeline CI/CD hoàn chỉnh với Laravel Framework
Mục đích cuối cùng của chuỗi bài: xây dựng 1 CI/CD hoàn chỉnh bao gồm:
- Docker: sử dụng nền tảng container để triển khai
- Laravel: framework PHP để làm website
- Unit testing: Unit test cho PHP
- Feature testing: test chức năng cho service
- Deploy: CD deploy container bằng Pipeline
Giới thiệu những keyword trọng yếu
Laravel
Ai làm PHP thì đều biết về Laravel, framework PHP phổ biến và mạnh mẽ nhất hiện nay. Laravel tích hợp nhiều kỹ thuật vào giúp cho phát triển những ứng dụng trở nên dễ dàng hơn đối với developer. Điển hình:
*
Ngoài ra với cộng đồng những nhà phát triển lớn và hoạt động tích cực thì cũng có vô vàn các package hỗ trợ thêm nhiều tính năng cho Laravel khiến cho framework này ngày càng có nhiều người sử dụng.
Laravel vượt mặt các PHP Framework đình đám vào giữa năm 2013 và vẫn trở nên phổ biến rộng rãi hơn
Testing trong Laravel : các bạn có thể tham khảo thêm tại đây
- Unit testing : thư mục chứa code Unit test của Laravel là
<project_root_path>/tests/Unit
ở đây ta khai báo các class mới extended từ classTestCase
để viết code unit test cho project. Sử dụng$this->assertXXX()
để kiểm tra output của các module cần test. Ví dụ
public function testExample()
{
$this->assertEquals(true, Tools::compare(1,1)); // test kết quả trả về của Tools::compare(1,1) là true
}
- Feature testing : thư mục chứa code Unit test của Laravel là
<project_root_path>/tests/Feature
ở đây ta khai báo các class mới extended từ classTestCase
để viết code unit test cho project. các method so sánh kiểm tra chức năng sẽ được call từ object\Illuminate\Foundation\Testing\TestResponse
mỗi lần ta thực hiện 1 request. Ví dụ:
public function testAddPostTitleInvalid()
{
$title = 'Post's title';
$content = 'Post's content';
$user = User::all()->first(); // lấy user đầu tiên trong db để xác thực
$this->actingAs($user)
->get(route('post.add')); // mở url add post với user vừa lấy từ db
$response = $this->followingRedirects()
->post(route('post.confirm'), [
'title' => $title,
'content' => $content,
'_token' => csrf_token(),
]); // gửi post request lên server, follow theo page khi bị redirect
$response->assertSeeText('The title must be at least 10 characters'); // Kiểm tra có lỗi cảnh báo title phải có ít nhất 10 ký tự
}
Docker
Một thuật ngữ quá hot trong giới DevOps trong khoảng 4 năm trở lại đây, Docker ứng dụng công nghệ container hoá, đóng gói service vào container nhẹ và nhanh. Docker đã thay đổi tư duy về hệ thống từ kiến trúc phần mềm tổ chức bằng các server, máy ảo chuyển hoá sang tổ chức bằng container, Docker giúp cho quá trình phát triển cũng như vận hành các hệ thống trở nên nhanh và hiệu quả hơn bao giờ hết. Jenkins cũng bắt kịp theo xu hướng và đã hỗ trợ rất nhiều cho Docker, vì vậy bài viết này sẽ hướng dẫn cách thiết lập CI/CD cho Laravel trên nền tảng Docker.
Docker compose: là 1 trong 3 công cụ orchestration (điều phối cụm container, docker daemon), ngoài compose ra còn có Docker machine và Docker swarm, Docker compose là công cụ quản lý tập trung nhiều container trên 1 file meta là file docker-compose.yml, file này sẽ định nghĩa mọi thiết lập về các container để tất cả có thể phối hợp với nhau tạo hành 1 service hoàn chỉnh. Bài viết này sử dụng Docker compose để xây dựng 1 web service hoàn chỉnh (bao gồm Nginx + PHP-Fpm + MySQL)
Bài toán
Các yêu cầu của bài toán đặt ra như sau:
- Ta có 1 web service:
- Được viết bằng framework Laravel
- Web service này có 2 loại automation test là Unit test và Feature test (Integration test)
- Tất cả thành phần về infrastructure của web service đều được xây dựng trên nền tảng Docker
- Yêu cầu đặt ra: mỗi commit lên repo của project web service này đều sẽ thực hiện những công việc sau:
- Build lại những image được sử dụng trong hệ thống
- Chạy Unit test trên container chứa source code PHP của service
- Unit test ok thì sẽ deploy lại web service sử dụng Docker compose
- Sau khi deloy xong thì sẽ chạy Feature test để kiểm tra các chức năng của hệ thống đã hoạt động đúng đắn chưa.
Hình mô phỏng
Triển khai
Công việc chính sẽ có 2 giai đoạn sau:
- Thiết kế container : Build hoặc lựa chọn các image cần thiết để xây dựng nên service hoàn chỉnh + thiết lập file
docker-compose.yml
để quản lý các container. - Thiết lập Pipeline trên Jenkins : Tạo file Jenkinsfile để định nghĩa các stage cho mô hình CI/CD bao gồm: build image, chạy Unit testing, Deploy, chạy Feature tesing.
Thiết kế container
Bố cục container như sau:
- Container web service: là container chạy nginx để xử lý các HTTP request
- Container PHP-FPM: là container chạy PHP-FPM để chạy các PHP script tương ứng với request HTTP từ Nginx đẩy qua.
- Container MySQL: chạy MySQL
※Chú ý: Các bạn chưa quen vs Docker có thể đặt câu hỏi là tại sao lại tách Nginx và PHP-FPM ra? -> bạn có thể cho Nginx và PHP-FPM chung 1 container, tuy nhiên tư tưởng của Docker là container ko phải là Virtual Machine, mà nó tương đương với 1 service vì vậy tách Nginx và PHP-FPM ra 2 container ta cũng hiểu như là 2 service thôi, hơn nữa việc tách ra như vậy cho ta một số lợi điểm như là:
- quản lý container dễ dàng hơn, log của Nginx và PHP-FPM không bị lẫn lộn vào nhau.
- Scale Nginx và PHP-FPM là độc lập -> tiết kiệm tài nguyên và giúp sacle linh hoạt hơn.
1. Container web service: ta sử dụng image nginx:latest
(latest là tag của image, ý là sử dụng version mới nhất) để build lại image Nginx mới với config mới phù hợp với service của ta. File Dockerfile
có nội dung như dưới đây (giải thích sẽ được comment ngay trong Dockerfile
)
FROM nginx:latest
ADD ./default.conf /etc/nginx/conf.d/default.conf # config server web
ADD ./src /var/www/blog # copy source code của service vào image
RUN chown www-data:www-data -R /var/www/blog # đổi sở hữu của thư mục web thành user www-data
RUN apt-get update # update package [Option]
RUN apt-get install net-tools -y # cài đặt một số tools để quản lý network [Option]
Nội dung của file default.conf
như dưới đây:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/blog/public;
location / {
index index.php index.html index.htm;
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass web:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Trong file default.conf
thì quan trọng nhất là phần config cho xử lý từ URL của HTTP request gọi đến PHP-FPM để chạy script PHP tương ứng. Trong đó fastcgi_pass web:9000;
các bạn hãy chú ý config này, thông thường khi Nginx và PHP-FPM cài chung trên 1 host (không sử dụng Docker) thì việc giao tiếp giữa Nginx và PHP-FPM hay sử dụng socket (fastcgi_pass unix:/var/run/php-fpm/marketplace.sock;
) còn khi ta tách Nginx và PHP-FPM ra 2 container thì ta sử dụng kiểu tương tác là TCP/IP.
Build image thì ta sử dụng command docker build -f <Dockerfile path> . -t <tag_name>
2. Container PHP-FPM : Ta sử dụng base image là php:7-fpm
(PHP 7), Dockerfile sẽ có nội dụng như dưới đây (giải thích sẽ được comment ngay trong Dockerfile
):
FROM php:7-fpm
RUN apt-get update && apt-get install -y libmcrypt-dev mysql-client \
&& docker-php-ext-install mcrypt pdo_mysql # cài một số package cần thiết
ADD ./php.conf /usr/local/etc/php-fpm.conf # Config PHP
COPY ./php.ini /usr/local/etc/php/ # Config PHP
ADD ./src /var/www/blog # Add source code của service vào image
RUN chown www-data:www-data -R /var/www/blog # đổi sở hữu của thư mục web thành user www-data
RUN apt-get install net-tools -y # cài đặt một số tools để quản lý network [Option]
WORKDIR /var/www/blog # khai báo path của work directory ( mọi tương tác vs container sẽ bắt đầu từ đây)
Nội dung những file config của PHP bạn có thể xem tại link
3. Container MySQL : sử dụng image mysql:5.6
4. Xây dựng file docker-compose.yml
: trong file docker-compose.yml
ta thiết lập cho các container bao gồm những việc như là:
- Port mapping: public những port nào trong container ra ngoài
- Thiết lập network giữa các container
- Khai báo các biến môi trường truyền vào cho container
- Thiết lập resource cho container
- ...
Dockerfile có nội dung như sau:
version: '3'
services:
nginx:
image: kyo88kyo/nginx
ports:
- "9999:80"
networks:
- mynet
links:
- web
deploy:
placement:
constraints: [node.role == manager]
web:
image: kyo88kyo/blog
environment:
- "DB_PORT=3306"
- "DB_HOST=database"
networks:
- mynet
deploy:
mode: replicated
replicas: 4
placement:
constraints: [node.role == worker]
resources:
limits:
cpus: '0.1'
memory: 256M
database:
image: mysql:5.6
environment:
- "MYSQL_ROOT_PASSWORD=Calldb@123456"
- "MYSQL_USER=blog"
- "MYSQL_PASSWORD=Blog@123456"
- "MYSQL_DATABASE=kyo"
ports:
- "33061:3306"
networks:
- mynet
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == worker]
networks:
mynet:
Để test thử ta sử dụng lệnh docker-compose up
thì sẽ thấy có 3 container được tạo ra:
Do các image sử dụng trong docker-compose.yml
đều đã được mình public trên docker hub nên chỉ cần bạn sử dụng file docker-compose.yml
trên và up nó lên thì sẽ truy cập vào web service được
Access vào port 9999 (mapping từ container nginx ra) ta sẽ thấy truy cập được vào web service
Ok vậy là việc thiết lập docker đã xong 99% rồi. Giờ ta thiết lập Pipeline cho web service thôi!!!
Thiết lập Pipeline trên Jenkins
Các bước thiết lập:
- Trên web Jenkins tạo item type là Multibranch Pipeline (mới trigger git push được) và thiết lập git như ảnh dưới ( cần phải fork project laravel-5v5-blog của mình về acc github của bạn )
- Trong project laravel-5v5-blog có sẵn file Jenkinfile với thiết lập như dưới đây
node ('slave'){ // run trên node có label là slave
checkout scm
stage('Build') {
checkout scm
sh 'pwd && cd src && /usr/local/bin/composer install'
docker.build("kyo88kyo/nginx", "-f Dockerfile-nginx .")
docker.build("kyo88kyo/blog")
}
stage('Test') {
docker.image('kyo88kyo/blog').inside {
sh 'php --version'
sh 'cd /var/www/blog && ./vendor/bin/phpunit --testsuite Unit'
}
}
stage('Deploy') {
sh 'cd src && /usr/local/bin/docker-compose down'
sh 'cd src && /usr/local/bin/docker-compose up -d'
sh 'sleep 10 && cd src && /usr/local/bin/docker-compose run web php artisan migrate'
}
stage ('Test Feature') {
sh 'cd src && /usr/local/bin/docker-compose run web ./vendor/bin/phpunit --testsuite Feature'
}
}
Giải thích các stage:
* Build : build lại 2 image là kyo88kyo/nginx
và kyo88kyo/blog
* Test Unit : chạy unit testing ngay bên trong docker container tạo ra từ image kyo88kyo/blog
(container tạo ra rồi bị xóa luôn lúc chạy xong)
* Deploy : dùng docker compose deploy toàn bộ web service ( xóa tất cả container cũ rồi tạo mới lại, đây chỉ làm trên môi trường test thôi còn thực tế trên production sẽ ko làm vậy vì sẽ mất hết database, ta cũng có thể mount volume chứa data vào cho container MySQL thì sẽ ko bị mất dữ liệu)
* Test Feature : chạy feature testing trên container PHP đã deploy
Demo
Nội dung:
- Add thêm 1 dòng text với nội dung " Test webhook" vào homepage của web service
- Push commit thay đổi trên lên github repo
- Kiểm tra trên Jenkins để xem Jenkins trigger tự động từ github và chạy lại pipeline của project
- Sau khi build trên Jenkins chạy xong ta reload lại url của web service để xem thay đổi, xem dòng text " Test webhook" hiển thị trên homepage.
Mời các bạn xem ảnh gif sau để xem lại quá trình demo trên
Kết luận
- Bài viết này là bài viết cuối cùng trong serial bài về Jenkins, hy vọng các bạn đã nắm rõ được cách thiết lập Pipeline trong Jenkins và hình dung được quá trình CI/CD với Docker cho Laravel framework hoạt động như thế nào.
- Mọi thắc mắc xin để comment trong post này hoặc liên hệ skype
longkyo1988
để hỏi nhé.