# Docker 教學 第 6 堂：GitHub Actions 進階實作與課程總結


## 6-1 GitHub Actions 進階語法（約 1 小時）

### 環境變數

在 workflow 裡可以用環境變數來避免重複寫相同的值：

```yaml
name: CI with Environment Variables

on:
  push:
    branches: [ main ]

env:                           # 整個 workflow 共用的環境變數
  IMAGE_NAME: my-flask-app
  PYTHON_VERSION: &#39;3.11&#39;

jobs:
  build:
    runs-on: ubuntu-latest

    env:                       # 這個 job 專用的環境變數
      FLASK_ENV: testing

    steps:
      - name: 顯示環境變數
        run: |
          echo &#34;Image: ${{ env.IMAGE_NAME }}&#34;
          echo &#34;Python: ${{ env.PYTHON_VERSION }}&#34;
          echo &#34;Flask: $FLASK_ENV&#34;
```

**環境變數的三個層級：**

| 層級 | 範圍 | 寫在哪裡 |
|------|------|---------|
| Workflow 層 | 所有 job 都能用 | 最外層的 `env:` |
| Job 層 | 只有這個 job 能用 | job 底下的 `env:` |
| Step 層 | 只有這個 step 能用 | step 底下的 `env:` |

### 條件判斷

可以根據條件決定要不要執行某個 step 或 job：

```yaml
steps:
  - name: 只在 main 分支執行
    if: github.ref == &#39;refs/heads/main&#39;
    run: echo &#34;This is main branch&#34;

  - name: 只在 PR 時執行
    if: github.event_name == &#39;pull_request&#39;
    run: echo &#34;This is a pull request&#34;

  - name: 前一步失敗才執行（通知用）
    if: failure()
    run: echo &#34;Something went wrong!&#34;
```

### 多個 Job 的依賴關係

```yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo &#34;Running tests...&#34;

  build:
    runs-on: ubuntu-latest
    needs: test                # 等 test 通過才跑
    steps:
      - run: echo &#34;Building...&#34;

  deploy:
    runs-on: ubuntu-latest
    needs: build               # 等 build 完成才跑
    steps:
      - run: echo &#34;Deploying...&#34;
```

**執行順序：**
```
test → build → deploy
（任何一步失敗，後面的都不會跑）
```

### 手動觸發 Workflow

除了 push 自動觸發，也可以加上手動觸發的按鈕：

```yaml
on:
  push:
    branches: [ main ]
  workflow_dispatch:           # 加上這行就能手動觸發
    inputs:
      environment:
        description: &#39;部署環境&#39;
        required: true
        default: &#39;staging&#39;
        type: choice
        options:
          - staging
          - production
```

在 GitHub 的 Actions 頁面就會出現「Run workflow」按鈕，可以選擇參數後手動執行。

---

## 6-2 實作：完整的 Flask CI/CD Pipeline（約 1.5 小時）

### 目標

建立一個完整的 CI/CD 流程：
1. Push 程式碼到 GitHub
2. 自動跑測試
3. 測試通過 → 自動 build Docker Image
4. 自動 push 到 Docker Hub
5. 輸出部署資訊

### 步驟 1：建立 Flask 專案

建立新的專案資料夾或使用之前的 `docker-cicd-demo`。

`app.py`：

```python
from flask import Flask, jsonify

app = Flask(__name__)

@app.route(&#39;/&#39;)
def hello():
    return jsonify({
        &#39;message&#39;: &#39;Hello from CI/CD!&#39;,
        &#39;version&#39;: &#39;1.0.0&#39;
    })

@app.route(&#39;/health&#39;)
def health():
    return jsonify({&#39;status&#39;: &#39;healthy&#39;})

def add(a, b):
    return a &#43; b

def is_positive(n):
    return n &gt; 0

if __name__ == &#39;__main__&#39;:
    app.run(host=&#39;0.0.0.0&#39;, port=5000)
```

`test_app.py`：

```python
import pytest
from app import app, add, is_positive

@pytest.fixture
def client():
    app.config[&#39;TESTING&#39;] = True
    with app.test_client() as client:
        yield client

def test_hello(client):
    response = client.get(&#39;/&#39;)
    assert response.status_code == 200
    data = response.get_json()
    assert &#39;message&#39; in data

def test_health(client):
    response = client.get(&#39;/health&#39;)
    assert response.status_code == 200
    data = response.get_json()
    assert data[&#39;status&#39;] == &#39;healthy&#39;

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

def test_is_positive():
    assert is_positive(1) == True
    assert is_positive(-1) == False
    assert is_positive(0) == False
```

`requirements.txt`：

```
flask
pytest
```

`Dockerfile`：

```dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD [&#34;python&#34;, &#34;app.py&#34;]
```

`.dockerignore`：

```
.git
.github
__pycache__
*.pyc
venv/
.env
*.md
test_*.py
```

### 步驟 2：建立完整的 CI/CD Workflow

`.github/workflows/cicd.yml`：

```yaml
name: Flask CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  IMAGE_NAME: docker-cicd-demo

jobs:
  # ===== Job 1: 測試 =====
  test:
    runs-on: ubuntu-latest

    steps:
      - name: 拉取程式碼
        uses: actions/checkout@v4

      - name: 安裝 Python
        uses: actions/setup-python@v5
        with:
          python-version: &#39;3.11&#39;

      - name: 安裝依賴
        run: pip install -r requirements.txt

      - name: 執行測試
        run: pytest test_app.py -v

  # ===== Job 2: Build 並 Push Docker Image =====
  docker:
    runs-on: ubuntu-latest
    needs: test                    # 測試通過才跑
    if: github.event_name == &#39;push&#39;  # 只有 push 才 build，PR 不 build

    steps:
      - name: 拉取程式碼
        uses: actions/checkout@v4

      - name: 登入 Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: 取得版本資訊
        id: version
        run: |
          echo &#34;sha_short=$(git rev-parse --short HEAD)&#34; &gt;&gt; $GITHUB_OUTPUT
          echo &#34;date=$(date &#43;&#39;%Y%m%d&#39;)&#34; &gt;&gt; $GITHUB_OUTPUT

      - name: Build 並 Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
            ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.sha_short }}

      - name: 輸出部署資訊
        run: |
          echo &#34;Docker Image 已推送！&#34;
          echo &#34;Image: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}&#34;
          echo &#34;Tags: latest, ${{ steps.version.outputs.sha_short }}&#34;
          echo &#34;&#34;
          echo &#34;要在伺服器上部署，執行：&#34;
          echo &#34;docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest&#34;
          echo &#34;docker run -d -p 5000:5000 ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest&#34;
```

### 步驟 3：Push 並觀察完整流程

```powershell
git add .
git commit -m &#34;add complete CI/CD pipeline&#34;
git push
```

到 GitHub Actions 頁面觀察：

```
test（跑測試）
  ↓ 通過
docker（build &#43; push）
  ↓ 完成
Docker Hub 上出現新的映像檔
```

### 步驟 4：驗證映像檔可以使用

```powershell
# 從 Docker Hub 拉下來
docker pull 你的帳號/docker-cicd-demo:latest

# 跑起來
docker run -d --name cicd-test -p 5000:5000 你的帳號/docker-cicd-demo:latest

# 測試
curl http://localhost:5000
curl http://localhost:5000/health

# 清理
docker rm -f cicd-test
```

---

## 6-3 GitHub Actions 實用技巧（約 15 分鐘）

### Badge：在 README 顯示 CI 狀態

在 `README.md` 加上一行，就能顯示 CI 是通過還是失敗的徽章：

```markdown
![CI](https://github.com/你的帳號/docker-cicd-demo/actions/workflows/cicd.yml/badge.svg)
```

效果：別人一進你的 Repository 就能看到目前 CI 狀態是綠色（通過）還是紅色（失敗）。

### 快取依賴加速 CI

每次 CI 都要重新 `pip install` 很慢，可以加上快取：

```yaml
      - name: 快取 pip 套件
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles(&#39;requirements.txt&#39;) }}

      - name: 安裝依賴
        run: pip install -r requirements.txt
```

第一次跑會正常安裝，之後只要 `requirements.txt` 沒變，就會用快取，速度快很多。

### 多版本測試（Matrix）

想同時在多個 Python 版本上測試：

```yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [&#39;3.9&#39;, &#39;3.10&#39;, &#39;3.11&#39;, &#39;3.12&#39;]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install -r requirements.txt
      - run: pytest test_app.py -v
```

這會同時跑 4 個 job，分別在 Python 3.9、3.10、3.11、3.12 上測試。

---

## 6-4 課程總結（約 15 分鐘）

### 六堂課回顧

| 堂次 | 主題 | 學到的重點 |
|------|------|-----------|
| 1 | Docker 基礎概念與安裝 | Docker 是什麼、容器 vs VM、安裝 Docker Desktop |
| 2 | 映像檔與容器操作 | pull、run、ps、stop、rm、Volume、端口映射 |
| 3 | Docker Compose 與進階操作 | compose、MySQL、多容器管理、情境題 |
| 4 | Dockerfile 與映像檔建構 | Dockerfile 語法、分層、優化、push 到 Docker Hub |
| 5 | CI/CD 與 GitHub Actions 入門 | CI/CD 概念、workflow 語法、自動測試、自動 build |
| 6 | GitHub Actions 進階實作 | 完整 pipeline、環境變數、快取、多版本測試 |

### 完整的開發到部署流程

```
寫程式碼 → git push → GitHub Actions 自動跑測試
                         ↓ 測試通過
                    自動 build Docker Image
                         ↓
                    自動 push 到 Docker Hub
                         ↓
                    部署到伺服器
                         ↓
                    使用者看到最新版本
```

### 接下來可以學什麼？

| 方向 | 工具 / 技術 |
|------|-----------|
| 容器編排 | Kubernetes（K8s）、Docker Swarm |
| 雲端部署 | AWS ECS、GCP Cloud Run、Azure Container |
| 監控 | Prometheus、Grafana、ELK Stack |
| 進階 CI/CD | ArgoCD、GitOps |

---

## 課堂練習

### 練習 1：加上健康檢查測試

在 workflow 裡加一個 step，build 完 Docker Image 之後先在 CI 環境裡跑起來，用 `curl` 測試 `/health` endpoint 確認容器正常運行，再 push 到 Docker Hub。

### 練習 2：手動觸發部署

修改 workflow，加上 `workflow_dispatch`，讓你可以手動點按鈕觸發部署，並選擇要部署到 staging 還是 production。

### 練習 3：完整專案挑戰

建立一個完整的 Flask &#43; MySQL 專案（用 Docker Compose），搭配 GitHub Actions 實現：
1. Push 時自動跑測試
2. 測試通過自動 build 並 push 映像檔
3. 在 README 加上 CI 狀態徽章


---

> 作者: luk  
> URL: https://yoru-karu-blog-lalaluk-52581ac5e0cef170a3c8922c19182ecb6f7bd604.gitlab.io/posts/tutorial/docker/docker-session6-github-actions-advanced/  

