使用 GitHub Action 來做簡單的 CI/CD
CI/CD,是由兩個詞彙,持續整合 (Continuous Integration) 與持續交付 (Continuous Deployment) 組合而來:
- CI (Continuous Integration),意即持續整合,在這個階段會建立一個正式環境的副本並進行自動測試,確保程式可以在正式環境上可以正常執行。
- CD (Continuous Delivery),意即持續交付,指的是以自動化的方式,頻繁且持續的將應用程式部屬到正式環境,使應用程式可以快速的進行更新 (例如加入新的功能或是修正 Bug)。
CI/CD 的主要目的,就是將應用程式的建構、測試還有部屬到正式環境的流程自動化,是 DevOps 文化中很重要的一環。
本篇文章會介紹如何使用 GitHub Action 完成一個簡單的 CI/CD,將 Laravel 的應用程式部屬到遠端的正式環境上。
設定自動測試的 yaml 檔案
如果要開始使用 GitHub Action,需要在專案的根目錄上新增一個 .github/workflows 資料夾,並在資料夾 workflows 中新增一個執行自動測試的檔案 tests.yml。
# workflow 的名稱,會在 Github Action 頁面上顯示的名稱(選擇性)
name: CI
# 只有在 push 到 main 分支上才會觸發此 workflow
on:
push:
branches:
- main
# 建立一個 job
jobs:
# 將此 job 的名稱設定為 'tests'
tests:
# 執行在最新版本的 ubuntu runner 上
runs-on: ubuntu-latest
# 設定系統上的環境變數,其中包含第三方服務金鑰或是連線資料庫的設定 (例如 MySQL 還有 redis)
env:
DB_DATABASE: blog_tests
DB_USERNAME: root
DB_PASSWORD: password
BROADCAST_DRIVER: log
CACHE_DRIVER: redis
QUEUE_CONNECTION: redis
SESSION_DRIVER: redis
MAIL_MAILER: smtp
MAIL_HOST: smtp.mailtrap.io
MAIL_PORT: 2525
# 較為機敏的資料,例如第三方服務的金鑰,可以存放在 GitHub Action 的 secrets
# 在 yaml 檔案中可以使用 ${{ secrets.SECRET_NAME }} 取得 secrets 中存放的值
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
MAIL_ENCRYPTION: tls
MAIL_FROM_ADDRESS: [email protected]
SCOUT_PREFIX: dev_posts
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_SECRET: ${{ secrets.ALGOLIA_SECRET }}
# 使用 container 建立會使用到第三方服務 (例如 MySQL 還有 redis),並建立網路連線
services:
mysql:
image: mysql:latest
# 設定 container 中的環境變數
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: blog_tests
ports:
- 3306/tcp
# 測試 MySQL 執行是否正常
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis
ports:
- 6379/tcp
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
# 使用 actions/checkout@v3 這個官方的 action
# 可以查看 workflow 的執行狀況,並對 workflow 的虛擬環境進行指令操作(例如搭建測試環境)
- name: Checkout
uses: actions/checkout@v3
# 設定 PHP 環境
# https://github.com/shivammathur/setup-php
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: mbstring, dom, fileinfo, mysql, redis
coverage: xdebug
- name: Start mysql service
run: sudo systemctl start mysql
- name: Get composer cache directory
# 幫此步驟建立一個 unique id
id: composer-cache
# 使用 workflow command 中的 ::set-outputs 來設定 dir 為 composer cache 的路徑
# 範例 echo "::set-output name=action_fruit::strawberry"
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
# 建立 composer cache 檔案的快取,加快 workflow 的執行速度
# https://github.com/actions/cache
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
# https://github.com/actions/cache/blob/main/examples.md#php---composer
- name: Cache composer dependencies
uses: actions/cache@v3
with:
# 設定要進行快取檔案目錄的路徑
# 使用剛剛 ::set-outputs 指令所設定的 dir,即 composer cache 的路徑
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install composer dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Prepare the application
run: |
php -r "file_exists('.env') || copy('.env.example', '.env');"
php artisan key:generate
- name: Run Migration
run: php artisan migrate -v
env:
DB_PORT: ${{ job.services.mysql.ports['3306'] }}
REDIS_PORT: ${{ job.services.redis.ports['6379'] }}
- name: Test with phpunit
run: vendor/bin/phpunit --coverage-text
env:
DB_PORT: ${{ job.services.mysql.ports['3306'] }}
REDIS_PORT: ${{ job.services.redis.ports['6379'] }}
Action Secrets
需要特別注意的一點,由於設定 workflow 的 yaml 檔案是需要加入版本控制的,所以一些機敏資料,如帳號密碼或是第三方服務的金鑰,就不能直接寫在 yaml 檔案中,而是建議存放在 GitHub Action 的 Secrets 中。
可以在 GitHub Repo,至 Settings → Secrets → Actions 中設定 secret。
點選右上方的 New repository secret 來新增新的 secret。
設定完檔案中使用下方的語法取得 secret 的值。
${{ secrets.HELLO }} # World !
Action Cache
composer 有一個機制,就是會將各個專案下載過的依賴套件,都生成一份 composer cache 存放在本地中,這樣之後依賴套件就不需要再從網路上重新下載,直接從 composer cache 複製檔案即可。
下方這個指令可以取得該專案依賴套件 composer cache 所放置的位置。
composer config cache-files-dir
因為每次執行 workflow 都是生成一個全新的 container 來執行,這代表每次執行 composer install
都是從網路上下載依賴套件。
類似這樣的情況可以使用官方的 action actions/cache@v3
來將 compoaer cache 進行快取,之後在不同的 workflow 或是不同的 step,都可以直接使用這個快取,不用再從網路重新下載,藉此加快 workflow 的流程。
設定部屬到正式環境的 yaml 檔案
在 workflow 資料夾中新增一個 deploy.yml。
name: CD
# 只有在 CI 的 workflow 完成時才會執行此 workflow
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run
on:
workflow_run:
workflows: [ CI ]
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
# 注意前面 workflow_run 的 completed 意思是「完成」,不論執行結果成功或是失敗都算是「完成」
# 但是一般來說測試如果失敗就應該暫停部屬至正式環境
# 因此這裡加上一個 if 判斷,只有 CI 成功才會執行此 workflow
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- run: echo "tests workflow is ${{ github.event.workflow_run.conclusion }}"
- name: Checkout
uses: actions/checkout@v3
# 使用 appleboy/ssh-action@master 這個 action 遠端連線至正式環境
# https://github.com/appleboy/ssh-action
- name: Deployment
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
username: ${{ secrets.SSH_USERNAME }}
# 執行部屬的指令
script: |
cd /var/www/html/blog
echo '啟用 Laravel 內建的維護模式'
sudo -u www-data php artisan down
echo '使用 git pull 更新專案'
sudo -u www-data git pull --ff-only
sudo -u www-data composer install --no-progress --prefer-dist --optimize-autoloader --no-dev
sudo -u www-data npm install
sudo -u www-data npm run production
sudo -u www-data php artisan view:cache
sudo -u www-data php artisan config:cache
sudo -u www-data php artisan route:cache
sudo supervisorctl restart all
sudo -u www-data php artisan up
這裡使用的部屬到正式環境的方式非常簡單,是很單純使用 git pull
更新專案,並用 composer 與 npm 安裝依賴套件。
因為會使用到 composer 與 npm 指令,所以正式環境上就必須先安裝好 composer 與 npm。
另外一種部屬方式是在 container 中設定好環境並把相關的依賴套件安裝好整個打包下載,最後把打包好的應用程式放至正式環境,這樣正式環境就不需要安裝 composer 與 npm,也可以很方便的部屬至多個正式環境。