使用 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: from@example.com
      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。

2022_03_20_16_11_58_6236e1ce50555.png

點選右上方的 New repository secret 來新增新的 secret。

2022_03_20_16_12_26_6236e1ea9e19d.png

設定完檔案中使用下方的語法取得 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,也可以很方便的部屬至多個正式環境。

參考資料


sharkHead
written by
sharkHead

後端工程師, PHP 基金會每月 5 鎂小額贊助人 稍微擅長 PHP、Python 與 Google Search,偶爾寫寫 TypeScript 對於逗號後面必須加空格有著絕對的堅持