CI/CD 十關

LV.5/10

Cache

用 cache 保存套件下載,學會 key、policy,以及 cache 與 artifacts 的分工

進階約 20 分鐘cache、key:files、policy、extends

學習目標

  • 會用 cache:paths 保存套件下載目錄。
  • 會用 cache:key:files 讓相依清單改變時自動換新快取。
  • 會用 policy: pull 讓只讀的 job 不上傳快取。
  • 會用隱藏 job(. 開頭)與 extends 共用設定。

觀念

第一次:install 下載 PyYAML ──上傳快取──▶ [lv05-pip-<requirements 的雜湊>]
第二次:install 下載快取 ──▶ pip 顯示 Using cached ──▶ 更快
requirements.txt 改了 ──▶ 雜湊不同 ──▶ 新的快取 key,從頭下載
設定說明
cache:paths要快取的目錄,一定要在專案目錄裡,所以把 PIP_CACHE_DIR 指到 $CI_PROJECT_DIR/.cache/pip
cache:key:files依檔案內容算 key,最多兩個檔案
cache:key:prefix加在 key 前面,避免和別的快取撞名
cache:policypull-push(預設)、pull(只讀)、push(只寫)
extends把隱藏 job 的設定合併進來,hash 會深層合併

check-yaml 用 extends 拿到 .pip-cache 的 key、paths,自己只多加 policy: pull,兩者會合併。

動手做

  1. 連續跑兩次 pipeline,比較第二次 install 的紀錄:會出現 Using cached 與 Successfully extracted cache。
  2. 把 requirements.txt 的版本改成別的,觀察快取 key 改變。
  3. 在「Build → Pipelines」右上角「Clear runner caches」可以清掉所有快取。

常見錯誤

  • 快取專案目錄外的路徑(例如 ~/.cache/pip):GitLab 不會保存。
  • 用 cache 傳建置產物:cache 可能沒命中,下一個 job 會壞掉,要用 artifacts。
  • key 用 $CI_COMMIT_REF_SLUG:每個分支一份,新分支第一次一定沒命中。依相依清單算 key 比較划算。

下一關

Lv.6 用 rules 決定 job 什麼時候要跑。

範例檔案

資料夾 lv05-cache/ 的全部檔案。.gitlab-ci.yml 可以直接複製到自己的專案根目錄執行。

lv05-cache/.gitlab-ci.yml
lv05-cache/.gitlab-ci.yml
# Lv.5 Cache:把下載過的套件留下來,下次 pipeline 直接用
# cache 是「盡力而為」:沒命中也要能跑,只是比較慢。要可靠傳遞的檔案請用 artifacts(Lv.3)。

stages:
  - install
  - check

default:
  image: python:3.13-alpine

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"   # cache 只能存專案目錄裡的檔案
  PIP_DISABLE_PIP_VERSION_CHECK: "1"
  PIP_ROOT_USER_ACTION: "ignore"

# 以 . 開頭的是隱藏 job,不會執行,只拿來給別人引用
.pip-cache:
  cache:
    key:
      files: [lv05-cache/requirements.txt]   # 檔案內容一變,key 就變,自動換一份新快取
      prefix: lv05-pip
    paths:
      - .cache/pip/

install:
  stage: install
  extends: .pip-cache
  script:
    - du -sh .cache/pip 2>/dev/null || echo "快取還是空的(第一次執行)"
    - pip install -r lv05-cache/requirements.txt
    - python3 -c "import yaml; print('PyYAML', yaml.__version__)"

check-yaml:
  stage: check
  extends: .pip-cache
  cache:
    policy: pull               # 只讀不寫,job 結束時不用再上傳快取
  script:
    - pip install -r lv05-cache/requirements.txt   # 命中快取時看得到 Using cached
    - python3 lv05-cache/check_yaml.py
lv05-cache/check_yaml.py
lv05-cache/check_yaml.py
"""Lv.5:解析每一關的 .gitlab-ci.yml,確認 YAML 語法正確,並列出 job 數量。"""
from pathlib import Path
import sys

import yaml


class GitLabLoader(yaml.SafeLoader):
    """GitLab 專用的 !reference 標籤(Lv.7 會用到),一般 YAML 解析器不認得,這裡當成普通陣列。"""


GitLabLoader.add_constructor('!reference', lambda loader, node: loader.construct_sequence(node))
ROOT = Path(__file__).resolve().parent.parent
RESERVED = {'stages', 'variables', 'default', 'include', 'workflow', 'image', 'services', 'cache', 'before_script', 'after_script'}
bad = 0
for path in sorted(ROOT.glob('lv*/.gitlab-ci.yml')):
    try:
        doc = yaml.load(path.read_text(encoding='utf-8'), Loader=GitLabLoader) or {}  # nosemgrep: 自訂的 SafeLoader 子類別
    except yaml.YAMLError as err:
        bad += 1
        print('語法錯誤', path.relative_to(ROOT), err)
        continue
    jobs = [k for k in doc if k not in RESERVED and not str(k).startswith('.')]
    print(f'{path.parent.name:<22} {len(jobs)} 個 job')
sys.exit(1 if bad else 0)
lv05-cache/requirements.txt
lv05-cache/requirements.txt
# Lv.5 只用一個套件示範快取;固定版本,才不會哪天自己變
PyYAML==6.0.3