跳到主要內容

DE Day 44 部署與展示

DE Day 44 部署與展示

執行需求:CPU 可跑。今天是專案篇的最後一天實作。我們要把 Day 42 的 GitHub Actions 排程推上雲端、把 Day 43 的指標接到 Streamlit 儀表板、並把這套管線「開放」給所有人瀏覽。具體要做四件事:第一,把 DuckDB 檔案與指標快照變成可下載的 artifact;第二,用 Streamlit 1.41 寫一支儀表板應用(讀 project_config.INDICATORS 與 mart_daily_summary);第三,把儀表板部署到 Streamlit Community Cloud 或自架容器;第四,把 GitHub Actions 的 workflow 完整接上 daily_run、回歸測試、文件檢查。整個部署流程沿用 Day 41-43 的共用設定,Day 45 會用今天的成果做系列總結。

引言

資料工程的「最後一哩」是讓資料被看到、被使用。寫得再好的管線,如果沒有人查詢儀表板、沒有報告引用指標,就只是一堆跑得很快的程式碼。今天我們把專案從「能跑」推進到「能展示」,這個轉變比想像中大:它牽涉到部署、權限、檔案大小、讀延遲、UI 設計、可存取性。我們一步一步把這些都處理好。

「展示」分成兩個層次。第一層是「資料分析師介面」,給 SQL 熟練的人用,他們需要直接 query DuckDB 拿最新資料。第二層是「一般使用者介面」,給完全沒碰過 SQL 的同事或對外民眾用,他們透過 Streamlit 的儀表板看圖表、看指標。本專案的目標使用者包含兩種,所以兩種介面都要做:dbt 的 docs serve 提供第一層、Streamlit 提供第二層。

今天的設計原則是「可重現、可存取、可驗證」。可重現指任何同事都能從空白機器跑 bash scripts/bootstrap.sh 把整個環境建起來;可存取指部署後的 URL 任何人能開、不需要登入;可驗證指部署後的應用要能被自動測試確認「資料有更新、指標有計算、儀表板有渲染」。我們把這三個特性都做到。

第一段部署:把 DuckDB 變成 artifact

GitHub Actions 跑完 daily_run 後,warehouse/de-journey.duckdb 與 logs/indicators/*.json 都應該被儲存起來。預設 GitHub Actions 工作目錄是 ephemeral(每次重來都乾淨),所以我們要把產出物上傳成 artifact。我們在 Day 42 的 workflow 加上上傳步驟:

# de-journey/.github/workflows/daily_aqi.yml(補充)
      - name: 上傳 warehouse 與指標快照
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: aqi-artifacts-${{ github.run_id }}
          path: |
            warehouse/de-journey.duckdb
            logs/indicators/
          retention-days: 30

這段把 DuckDB 檔案與指標快照上傳成 artifact。retention-days: 30 讓 artifact 在 GitHub 保留 30 天(之後會自動刪除,避免無限增長)。if: always() 表示即使前面步驟失敗也上傳,這樣事後除錯時可以拿到失敗當下的資料夾狀態。

另一個做法是用 Pages 把 artifact 直接公開:

      - name: 發布到 gh-pages(讓任何人下讀)
        if: success()
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./public
          destination_dir: data/${{ github.run_id }}

peaceiris/actions-gh-pages@v4 是社群最常用的 Pages 部署 action。destination_dir: data/${{ github.run_id }} 把每次跑的 DuckDB 與快照放到獨立目錄,保留歷史;可下載的 URL 是 https://<org>.github.io/<repo>/data/<run_id>/de-journey.duckdb。這個 URL 對外公開,沒有存取限制。

第二段部署:Streamlit 儀表板

Streamlit 1.41 是 2025 年最受歡迎的 Python 儀表板框架。把指標與事實表視覺化成儀表板只要幾十行程式碼,而且不用寫 HTML/CSS。我們寫一支讀 DuckDB 與指標 JSON 的應用:

"""de-journey/dashboards/aqi_app.py:Streamlit AQI 儀表板。"""
from __future__ import annotations

import sys
from datetime import date, datetime, timedelta, timezone
from pathlib import Path

import duckdb
import pandas as pd
import streamlit as st

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "pipelines"))
from project_config import INDICATORS, MODELS, PROJECT  # noqa: E402

st.set_page_config(page_title="AQI 儀表板", layout="wide")
st.title("全國空氣品質儀表板")
st.caption(f"資料來源:{PROJECT['source']['name']}({PROJECT['license']})")

WAREHOUSE = Path(__file__).resolve().parents[1] / "warehouse" / "de-journey.duckdb"


@st.cache_data(ttl=3600)
def load_fact() -> pd.DataFrame:
    con = duckdb.connect(str(WAREHOUSE), read_only=True)
    df = con.execute(
        "SELECT observed_at, station_id, county, aqi, pm25 "
        "FROM aqi.fct_aqi_hourly "
        "WHERE observed_at >= current_timestamp - INTERVAL '30 days'"
    ).df()
    con.close()
    return df


@st.cache_data(ttl=3600)
def load_indicators(target: str) -> dict:
    p = Path(__file__).resolve().parents[1] / "logs" / "indicators" / f"{target}.json"
    if not p.exists():
        return {}
    import json
    return json.loads(p.read_text(encoding="utf-8"))


df = load_fact()
col1, col2, col3, col4 = st.columns(4)
col1.metric("近 30 天資料筆數", f"{len(df):,}")
col2.metric("測站數", f"{df['station_id'].nunique()}")
col3.metric("平均 AQI", f"{df['aqi'].mean():.1f}")
col4.metric("PM2.5 平均", f"{df['pm25'].mean():.1f}")

st.subheader("全國 AQI 趨勢")
ts = df.set_index("observed_at").resample("D")["aqi"].mean()
st.line_chart(ts)

st.subheader("各測站平均 AQI 排名")
ranking = df.groupby("station_id")["aqi"].mean().sort_values(ascending=False).head(10)
st.bar_chart(ranking)

這段把 Streamlit 儀表板寫完。@st.cache_data(ttl=3600) 是關鍵設計:Streamlit 會把函式結果快取 1 小時,避免每次互動都重新 query DuckDB。st.metric 把指標做成大數字卡片,st.line_chart、st.bar_chart 把趨勢與排名做成內建圖表(不用寫 matplotlib)。整支儀表板不到 50 行程式碼,視覺效果卻相當完整。

執行:

cd de-journey
uv run streamlit run dashboards/aqi_app.py --server.port 8501
# 打開 http://127.0.0.1:8501 就能看到儀表板

本機執行後用瀏覽器開 http://127.0.0.1:8501,會看到四個指標卡片、一張線图(30 天 AQI 趨勢)、一張柱狀圖(測站排名)。整個 UI 響應式設計,手機、平板、桌機都能正常瀏覽。

第三段部署:Streamlit Community Cloud

Streamlit Community Cloud 是免費的 Streamlit 部署服務,只要把 repo 推上 GitHub、連結 Streamlit 帳號就能部署。我們把儀表板部署上去:

# de-journey/.streamlit/config.toml
[server]
headless = true
port = 8501

[theme]
base = "light"
primaryColor = "#1f77b4"

[browser]
gatherUsageStats = false

這個設定檔放在 de-journey/.streamlit/config.toml。headless = true 表示不要開瀏覽器自動跳轉,適合部署環境。primaryColor 是 Streamlit 主色,用藍色(#1f77b4)配合 AQI 儀表板的清爽風格。gatherUsageStats = false 關閉遙測。

部署步驟(在 Streamlit Community Cloud UI 操作):

  1. 登入 share.streamlit.io(用 GitHub 帳號)。
  2. 點「New app」,選 your-org/de-journey、branch main、main file dashboards/aqi_app.py。
  3. 點「Deploy」,等 1–2 分鐘就會拿到一個 https://your-app.streamlit.app 的網址。

部署後的 URL 任何人能開,不需要登入。這是本專案的「公開展示」入口。Streamlit Community Cloud 在 2025 年仍是免費的(公開 repo),對於 side project 與學習型專案是最方便的部署選項。

Streamlit Community Cloud 在部署時會自動讀 requirements.txt(或 pyproject.toml)安裝套件。我們用一份精簡的 requirements.txt 把部署用的套件列出來,避免部署階段安裝 dev 套件導致時間過長:

# de-journey/dashboards/requirements.txt
duckstreamlitplotpandas

這份 requirements.txt 只列部署用的套件,比 pyproject.toml 的 dependencies 精簡很多(沒有 dbt-core、dbt-duckdb、APScheduler 等只在 pipeline 用的套件)。uv pip compile 可以從 pyproject.toml 自動產生這份檔,但我們手動維護更靈活。實務上你也可以讓 Streamlit Community Cloud 直接讀 uv.lock(透過 runtime.txt 指定 Python 3.13 + uv pip install),但需要較多的 CI 設定。

部署後的 Streamlit 應用可以加上一個「地圖檢視」分頁,把測站位置與最近 24 小時 AQI 用 st.map 顯示:

"""de-journey/dashboards/aqi_map.py:地圖檢視分頁。"""
from __future__ import annotations

from pathlib import Path

import duckdb
import streamlit as st

WAREHOUSE = Path(__file__).resolve().parents[1] / "warehouse" / "de-journey.duckdb"


@st.cache_data(ttl=3600)
def load_station_aqi() -> object:
    con = duckdb.connect(str(WAREHOUSE), read_only=True)
    df = con.execute("""
        SELECT s.station_id, s.station_name, s.county,
               s.latitude, s.longitude,
               AVG(f.aqi) AS avg_aqi
        FROM aqi.dim_stations s
            JOIN aqi.fct_aqi_hourly f ON s.station_id = f.station_id
        WHERE f.observed_at >= current_timestamp - INTERVAL '24 hours'
          AND s.latitude IS NOT NULL
        GROUP BY s.station_id, s.station_name, s.county,
                 s.latitude, s.longitude
    """).df()
    con.close()
    return df


st.subheader("近 24 小時各測站 AQI(地圖)")
df = load_station_aqi()
st.map(df, latitude="latitude", longitude="longitude", size="avg_aqi",
       color="avg_aqi")

st.map 是 Streamlit 內建的地圖元件,傳入 latitude、longitude 就會自動渲染地圖。size="avg_aqi" 把圓點大小對應 AQI 平均值,color="avg_aqi" 把顏色深淺也對應 AQI(越紅越糟)。這是「對外展示」最直覺的視覺化,民眾打開儀表板就能看到「我家附近的空氣現在怎麼樣」。

第四段部署:GitHub Pages 部署文件站

Day 40 的文件站也要部署。沿用 mkdocs gh-deploy:

# de-journey/.github/workflows/docs.yml
name: Build and Deploy Docs

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
        with:
          python-version: '3.13'
      - run: uv sync
      - run: uv run mkdocs build --strict
      - uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./site

這個 workflow 在每次 main 分支有 push 時自動建文件站、推到 gh-pages 分支。mkdocs build --strict 是重點:--strict 把任何 warning 視為錯誤,避免「broken link 沒被發現」這種問題。部署後的文件站 URL 是 https://your-org.github.io/de-journey/。

第五段部署:完整 CI 流程

把 daily_run、回歸測試、文件檢查、儀表板測試四個 CI 工作串起來:

# de-journey/.github/workflows/ci.yml
name: CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
        with:
          python-version: '3.13'
      - run: uv sync
      - name: 套件鎖檢查
        run: uv lock --check
      - name: 環境驗證
        run: uv run python scripts/verify_env.py
      - name: 模型清單檢查
        run: uv run python scripts/check_models.py
      - name: 套用合約檢查
        run: uv run python scripts/check_contract.py
      - name: dbt run(CI 子集)
        working-directory: models/aqi_project
        run: uv run dbt run --select staging intermediate
      - name: 文件連結檢查
        run: uv run python scripts/check_links.py
      - name: 文件指令檢查
        run: uv run python scripts/doc_doctor.py

這是 PR 與 push 都會觸發的 CI 工作。uv lock --check 確保 PR 沒有忘記 lock;verify_env.py 確保環境一致;check_models.py 確保模型清單與檔案一致;check_contract.py 跑合約檢查;dbt run --select staging intermediate 只跑 staging 與 intermediate(marts 需要真實資料,CI 跑會比較慢);check_links.py 與 doc_doctor.py 是 Day 40 寫的文件檢查。任何一步失敗 PR 就不能合併。

部署後的完整工作流

把所有段拼起來,整個專案的部署與運行長這樣:

  • 本地開發:uv run python pipelines/daily_run.py --date 2025-12-09,或 uv run streamlit run dashboards/aqi_app.py。
  • 本機排程:uv run python pipelines/scheduler.py(APScheduler 3.11,每天 03:00)。
  • 雲端排程:GitHub Actions daily_aqi.yml(每天定時跑,產生 artifact)。
  • 公開展示:Streamlit Community Cloud(一般使用者看的儀表板)。
  • 公開文件:GitHub Pages(Day 40 的 MkDocs 站)。
  • 品質閘門:GitHub Actions ci.yml(PR 檢查)。

這六個入口都讀同一份 project_config.py、同一個 DuckDB 檔案、同一份合約。任何入口的改動都會反映到其他入口,這就是 Day 41 強調的「共用設定的單一來源」。

驗證部署是否成功

部署完成後要驗證。我們寫一支「部署冒煙測試」腳本,模擬「使用者打開儀表板」的行為:

"""de-journey/tests/e2e/test_dashboard.py:驗證 Streamlit 儀表板跑得起來。"""
from __future__ import annotations

import subprocess
import time
from pathlib import Path

import requests

ROOT = Path(__file__).resolve().parents[2]


def test_dashboard_starts():
    proc = subprocess.Popen(
        ["uv", "run", "streamlit", "run", "dashboards/aqi_app.py",
         "--server.port", "8502", "--server.headless", "true"],
        cwd=str(ROOT), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
    )
    time.sleep(8)   # 給 Streamlit 8 秒啟動
    try:
        resp = requests.get("http://127.0.0.1:8502/_stcore/health", timeout=5)
        assert resp.status_code == 200, f"儀表板沒回應:{resp.status_code}"
    finally:
        proc.terminate()
        proc.wait(timeout=10)
    print("儀表板冒煙測試通過")

這支 E2E 測試啟動 Streamlit、檢查 health endpoint、結束 Streamlit。time.sleep(8) 給 Streamlit 啟動時間;/_stcore/health 是 Streamlit 內建 health endpoint,回 200 就代表啟動成功。這個測試可以接進 CI,每天驗證一次「部署後的版本仍能跑」。

除了儀表板冒煙測試,我們再加一支「artifact 下載驗證」腳本:CI 工作跑完後,從 GitHub API 下載最新一份 artifact、確認 DuckDB 與指標快照都能解開。這是「部署成功的端到端驗證」:

"""de-journey/scripts/verify_artifact.py:驗證最新一份 artifact 可下載且可解開。"""
from __future__ import annotations

import json
import os
import zipfile
from pathlib import Path
from urllib.request import urlopen

REPO = os.environ["GITHUB_REPOSITORY"]
TOKEN = os.environ["GITHUB_TOKEN"]

api = f"https://api.github.com/repos/{REPO}/actions/artifacts?per_page=1"
req = urlopen(urlopen(api).headers.get("Link", "").split(";")[0]
              if "Link" in urlopen(api).headers else api)
arts = json.loads(urlopen(api).read().decode("utf-8"))
if not arts["artifacts"]:
    raise SystemExit("找不到任何 artifact,請確認 daily_aqi workflow 跑過")

latest = arts["artifacts"][0]
print(f"最新 artifact:{latest['name']}({latest['size_in_bytes']} bytes)")
zip_path = Path("artifact.zip")
zip_path.write_bytes(urlopen(latest["archive_download_url"]).read())

with zipfile.ZipFile(zip_path) as zf:
    members = zf.namelist()
    has_duckdb = any(m.endswith("de-journey.duckdb") for m in members)
    has_indicators = any("indicators/" in m for m in members)
    print(f"解壓後 {len(members)} 個檔案:DuckDB={has_duckdb}, "
          f"indicators={has_indicators}")
    if not (has_duckdb and has_indicators):
        raise SystemExit("artifact 缺少必要檔案")

這支腳本透過 GitHub API 拿最新 artifact 的下載 URL,再用 urlopen 抓回本地解壓。沒有用 requests 或 PyYAML 額外套件,全靠標準函式庫。當 artifact 缺 DuckDB 或缺指標快照時,腳本會以非零 exit code 退出,讓 CI 工作失敗。

接著我們寫一支「部署通知」腳本,把今天部署的關鍵指標推到 Slack 或 Email。這是 Day 38 告警流的延伸,從「契約違破」推進到「部署結果通知」:

"""de-journey/scripts/notify_deploy.py:把部署結果寫進 Slack 或 log。"""
from __future__ import annotations

import json
import os
import urllib.request
from datetime import date
from pathlib import Path

REPORT_DIR = Path(__file__).resolve().parents[1] / "logs" / "indicators"
SLACK_URL = os.environ.get("SLACK_WEBHOOK_URL", "")  # 沒設定就只寫 log


def send_slack(text: str) -> bool:
    if not SLACK_URL:
        return False
    payload = json.dumps({"text": text}).encode("utf-8")
    req = urllib.request.Request(
        SLACK_URL, data=payload,
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        return resp.status == 200


if __name__ == "__main__":
    today = date.today().isoformat()
    p = REPORT_DIR / f"{today}.json"
    if not p.exists():
        print(f"{today} 無指標快照,跳過通知")
    else:
        m = json.loads(p.read_text(encoding="utf-8"))
        text = (f"[OK] AQI 部署完成 ({today}): "
                f"AQI={m.get('national_daily_aqi_avg')}, "
                f"PM2.5={m.get('monthly_pm25_avg')}")
        if send_slack(text):
            print("已推 Slack")
        else:
            print(f"未設 SLACK_WEBHOOK_URL,僅顯示:{text}")

SLACK_WEBHOOK_URL 用 os.environ 讀取,不寫死任何金鑰。如果環境變數沒設定,就只把訊息印到終端機、不推 Slack。這是 Day 39 教過的「金鑰管理」紀律的延伸:所有 webhook URL、API token 都不能出現在程式碼。

常見錯誤與踩雷

第一個雷:把 DuckDB 直接 commit 到 repo。DuckDB 是二進位檔案,每次跑 daily_run 都會改變,commit 會讓 git 歷史爆炸。請把 warehouse/ 與 logs/ 加入 .gitignore,改用 artifact 上傳。需要本地開發時,從 artifact 下載即可。

第二個雷:Streamlit Community Cloud 連不到內網資料。Streamlit Community Cloud 跑在他們的雲端,沒辦法存取你的內網服務。如果你的 ingest 需要 VPN 或內網 API,請先把資料同步到公開的雲端(GitHub artifact 或 S3),再讓 Streamlit 讀公開來源。

第三個雷:把 API 金鑰寫進 Streamlit 程式碼。當部署到公開的雲端時,任何人都能 fork 你的 repo。把金鑰放在 st.secrets(Streamlit Community Cloud 的機密管理)或 GitHub Actions 的 secrets,程式碼裡只讀 os.environ["KEY"]。

第四個雷:忘記設定 Streamlit 的 Python 版本。Streamlit Community Cloud 預設用 Python 3.12,但我們的 pyproject.toml 要求 3.13。請在 .streamlit/config.toml 或 Streamlit Cloud UI 明確指定 Python 版本。

第五個雷:MkDocs build 沒用 --strict。--strict 把 warning 升級成 error,能在 PR 階段就抓出 dead link、broken anchor 等問題。Day 40 文件站的範例已經有 --strict,這裡再強調一次。

為了讓部署的入口一目了然,我們再寫一支「部署入口腳本」:把所有部署相關的指令(bootstrap、daily_run、scheduler、streamlit、mkdocs)整理成一個 CLI,方便在文件站的首頁引用:

"""de-journey/scripts/deploy.py:把部署相關指令集中成一個 CLI。"""
from __future__ import annotations

import argparse
import subprocess
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]


def run(cmd: list[str]) -> None:
    print("$", " ".join(cmd))
    subprocess.run(cmd, cwd=str(ROOT), check=True)


def cmd_bootstrap(_args) -> None:
    run(["bash", "scripts/bootstrap.sh"])


def cmd_daily(args) -> None:
    run(["uv", "run", "python", "pipelines/daily_run.py", "--date", args.date])


def cmd_scheduler(_args) -> None:
    run(["uv", "run", "python", "pipelines/scheduler.py"])


def cmd_streamlit(_args) -> None:
    run(["uv", "run", "streamlit", "run", "dashboards/aqi_app.py"])


def cmd_docs(_args) -> None:
    run(["uv", "run", "mkdocs", "build", "--strict"])


COMMANDS = {
    "bootstrap": cmd_bootstrap,
    "daily": cmd_daily,
    "scheduler": cmd_scheduler,
    "streamlit": cmd_streamlit,
    "docs": cmd_docs,
}


if __name__ == "__main__":
    p = argparse.ArgumentParser()
    sub = p.add_subparsers(dest="cmd", required=True)
    sub.add_parser("bootstrap")
    p_daily = sub.add_parser("daily")
    p_daily.add_argument("--date")
    sub.add_parser("scheduler")
    sub.add_parser("streamlit")
    sub.add_parser("docs")
    args = p.parse_args()
    COMMANDS[args.cmd](args)

這支腳本把 Day 40-44 所有部署相關的指令集中到一個 CLI。python scripts/deploy.py bootstrap 等同 bash scripts/bootstrap.sh,python scripts/deploy.py daily --date 2025-12-09 跑 daily_run。文件站 README 只要寫「跑 python scripts/deploy.py bootstrap 就能把環境建好」這一句話,讀者不用記所有細節。deploy.py 的好處是「指令可以從 PR review 開始就保持一致」:當有人加了新的部署步驟,只要在這個檔案加一行 sub-parser,文件站的指示也自動同步。

效能與實務提醒

Streamlit Community Cloud 的免費方案在 2025 年仍可用,限制是「單一 app、有限記憶體、public repo only」。對於學習型專案與 side project 綽綽有餘;如果需要更多資源(GPU、長時間背景任務、private repo),可以升級付費或自架容器。

實務上有三個取捨值得記得。第一,部署選項:Streamlit Cloud vs Hugging Face Spaces vs 自架 Docker——前三個都是免費公開,自架需要 DevOps 知識。對學習型專案,Streamlit Cloud 是最低摩擦的選項。第二,儀表板快取:用 @st.cache_data 把 DuckDB query 快取起來,避免每次互動都重算。對長一點的查詢(最近 30 天),TTL 設 1 小時就夠。第三,CI 速度:CI 工作不要跑太多步驟,否則工程師會放棄 PR。建議只在 main push 跑完整測試,PR 只跑必要的(lint、合約檢查、staging 跑通)。

另一個工程上的提醒:部署後的 URL 是 https://your-app.streamlit.app,沒有自訂網域。對 side project 沒問題;如果要正式上線,可以花錢買自訂網域並用 Cloudflare 設定轉址。整個部署階段的時間估算:第一次部署(從 git push 到看到 Streamlit 畫面)約 5 分鐘;之後每次部署約 2–3 分鐘;GitHub Pages 文件站部署約 1 分鐘。

部署之後的監控與維運也要有心理準備。Streamlit Community Cloud 偶爾會重啟應用(特別是底層升級時),重啟後 @st.cache_data 會失效,需要重新 query DuckDB。建議在儀表板加一個「最後更新時間」顯示,讓使用者知道資料是幾點的快照。如果發現使用者常需要「強制重新整理」,可以把快取 TTL 從 3600 秒調到 600 秒(10 分鐘),這樣重啟後最長 10 分鐘就會有新資料。

小結

今天把專案篇的「部署與展示」蓋起來。我們把 DuckDB 與指標快照上傳成 GitHub artifact,用 Streamlit 寫了儀表板、部署到 Streamlit Community Cloud,把 MkDocs 文件站部署到 GitHub Pages,並把 CI 流程(PR 檢查、文件檢查、模型清單檢查、合約檢查)全部串起來。重點觀念有三個:第一,展示分兩層(SQL 介面給分析師、Streamlit 給一般使用者),兩種介面共用同一個 DuckDB;第二,部署選項要看場景,Streamlit Cloud 是學習型專案最低摩擦的選擇;第三,CI 把「每次 PR 都跑合約與文件檢查」這件事自動化,避免過期。

明天(Day 45)我們會做系列總結:回顧這 45 天的學習路徑、貫穿四天專案篇的所有設計、列出延伸學習方向,並對整個系列的工具鏈做最後一次盤點。Day 41-44 的共用設定(project_config.py、aqi_hourly.yml、七個 dbt 模型、四個層次的評估)在明天會再次出現,作為「這四天我們做到了什麼」的具體證明。今天的部署成果可以整理成一張「部署入口一覽表」放在文件站首頁:

入口 URL/指令 更新頻率 適用情境
Streamlit 儀表板 https://your-app.streamlit.app 1 小時快取 一般使用者
GitHub Pages 文件站 https://your-org.github.io/de-journey/ 每次 push 開發者、文件讀者
dbt docs uv run dbt docs serve dbt run 後 資料分析師
DuckDB 檔案 artifact GitHub Actions artifacts 每日 03:00 SQL 使用者
指標快照 logs/indicators/<date>.json 每日 儀表板、監控

這張表是「Day 41-44 共用部署」的視覺化:同一個專案對外有多個入口,每個入口服務不同的使用者,但底層都讀同一份 DuckDB 與同一份 project_config.py。Day 41 寫的合約(aqi_hourly.yml)與共用設定(project_config.py)在四天內被十幾支程式碼引用,這是「共用設定單一來源」的具體價值——任何指標、模型、排程的變更都集中在一個地方,整個管線因此保持一致。

結語

今天的重點是「讓管線被看到、被使用」。我們從 DuckDB 的 artifact 上傳開始,到 Streamlit 儀表板、文件站、CI 流程,把整個專案從「能跑」推進到「能展示」。讀完這篇你應該能回答:為什麼 DuckDB 要做成 artifact 而不是 commit?Streamlit 儀表板怎麼讀 DuckDB?CI 流程該跑哪些檢查?明天(Day 45),我們會做整個系列的最後一篇「系列總結與延伸路線」,把 45 天的工具、觀念、專案實作串成一張地圖。

延伸資源

  • Streamlit Community Cloud 官方文件(2025):https://docs.streamlit.io/streamlit-community-cloud。免費部署 Streamlit 應用的入口。
  • GitHub Actions Artifact 官方文件:https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts。把 build 產出物儲存到 GitHub 的標準機制。
  • MkDocs gh-deploy 官方文件:https://www.mkdocs.org/user-guide/deploying-your-docs/。mkdocs gh-deploy 推到 GitHub Pages 的標準做法。
  • peaceiris/actions-gh-pages 官方 repo:https://github.com/peaceiris/actions-gh-pages。GitHub Pages 部署的最熱門 action。
  • Day 40 章節(文件化與交接):今天部署的 MkDocs 文件站是 Day 40 的延伸,把文件公開給所有人查。
  • Day 42 章節(管線實作與排程):今天部署的 GitHub Actions workflow 是 Day 42 的延伸,把 daily_run 推到雲端。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...