R2K Level 1(Identify · 身份層)的入門機制 ── 5 行 Dockerfile 改變支援團隊的世界
Release-as-Knowledge(R2K · 軟體發布即知識傳遞)系列・篇 2 / 6
上一篇我們看到 Mary 半小時拼湊一個 80% 正確的答案。
從這篇開始,我們進入解法。
解法的第一塊磚很樸素 : 一個你可能寫過幾百次但從沒認真看待的 Dockerfile 指令:LABEL。
這個指令存在了十年。多數團隊只用它放 maintainer email 跟 build 日期。但如果用對方式,它會變成 Release-as-Knowledge 整套機制的地基。
在進入技術細節前,讓我先把上篇結尾留下的概念講清楚。
我把「軟體發布同時是知識傳遞」這個 thesis 命名為 Release-as-Knowledge,簡稱 R2K,中文「軟體發布即知識傳遞」。
跟 Infrastructure-as-Code 同樣的命名結構 : X-as-Y 表示「把 X 當成 Y 來管理」。
IaC 把基礎建設當成可版本化的程式碼。
R2K 把軟體發布當成可結構化的知識傳遞。
R2K 不是一個工具、不是一個產品。是一個漸進採用的階梯,有 4 個 Level:
| Level | 名稱(中/英) | 主要機制 | 達成成本 |
|---|---|---|---|
| L1 | 身份層 / Identify | OCI 標準 LABEL + dev.releaseasknowledge.* |
1-2 週 |
| L2 | 資產層 / Trust | /r2k/ snapshots + index.yaml |
2-3 週 |
| L3 | 變更層 / Understand | Change Manifest(Mode A/B) | 6-8 週 |
| L4 | 分享層 / Share | OCI Referrers + badge | 4 週 |
像 SLSA 那樣,每一級都可以單獨宣告達成。組織可以說「我們是 R2K Level 2」,就像說「我們是 SLSA Level 3」一樣。
這篇講的是 R2K Level 1 : 身份層(Identify)。
LABEL 是 Dockerfile 的一個指令,把 key-value 嵌進 image 的 config blob。
最簡單的形式:
LABEL maintainer="ops@yourco.com"
LABEL com.yourco.version="1.0.0"
這段一寫,build 出來的 image 就帶著這些 metadata。任何能讀 image 的工具(Docker、Harbor、Trivy、Cosign)都能拿到。
為什麼被低估?因為大多數團隊只用它存 metadata 給人看,而不是給機器查。
它的真正能力是:讓任何 image scanner 不用 pull image 就能拿到結構化資料。
但靜態 LABEL 有個問題 : version="1.0.0" 寫死,每次 build 出來都一樣。
我們要的是每次 build 都帶不同的 git commit、build ID、測試結果。
這就是 build-arg 機制。
R2K Level 1 在 image 上掛兩組必要 + 一組選用的 LABEL:
| 組別 | namespace | 由誰定義 | 角色 |
|---|---|---|---|
| ✅ OCI 標準 | org.opencontainers.image.* |
OCI Image Spec | 任何 container 工具原生支援 |
| ✅ R2K 規格 | dev.releaseasknowledge.* |
R2K v1 | 給 R2K 工具鏈用 |
| ◯ Vendor 擴充 | com.yourco.*(反向域名) |
各自團隊 | 自家業務欄位(case_type、license_modules...) |
兩組必要 namespace 都要掛,不引入新工具的前提下讓 image 同時符合 OCI 慣例與 R2K compliant。OCI label 餵給「現成的 container 生態」,R2K label 餵給「R2K 工具鏈」。

# syntax=docker/dockerfile:1.6
FROM eclipse-temurin:17-jre-alpine
# CI 在 build 時動態傳入
ARG APP_VERSION=unknown
ARG GIT_COMMIT=unknown
ARG GIT_BRANCH=unknown
ARG GIT_TAG=
ARG BUILD_TIME=unknown
ARG BUILD_ID=unknown
ARG TEST_PASSED=0
ARG TEST_FAILED=0
ARG CASE_TYPE=standard
# === OCI 標準 labels(業界慣例,scanner 都認)===
LABEL org.opencontainers.image.title="payment-service" \
org.opencontainers.image.version="${APP_VERSION}" \
org.opencontainers.image.revision="${GIT_COMMIT}" \
org.opencontainers.image.created="${BUILD_TIME}" \
org.opencontainers.image.source="https://github.com/yourco/payment-service" \
org.opencontainers.image.vendor="Your Org" \
org.opencontainers.image.licenses="Apache-2.0"
# === R2K Level 1 · Identify ===
LABEL dev.releaseasknowledge.version="1.0" \
dev.releaseasknowledge.level="1" \
dev.releaseasknowledge.commit="${GIT_COMMIT}" \
dev.releaseasknowledge.branch="${GIT_BRANCH}" \
dev.releaseasknowledge.tag="${GIT_TAG}" \
dev.releaseasknowledge.build-time="${BUILD_TIME}" \
dev.releaseasknowledge.repo="https://github.com/yourco/payment-service" \
dev.releaseasknowledge.spec.url="https://enjtorian.github.io/release-as-knowledge/zh-tw/"
# === Vendor 擴充(反向域名命名空間,跟其他工具不衝突)===
LABEL com.yourco.psp.case_type="${CASE_TYPE}" \
com.yourco.psp.build_id="${BUILD_ID}" \
com.yourco.psp.test_summary="passed=${TEST_PASSED},failed=${TEST_FAILED}"
WORKDIR /app
COPY target/app.jar /app/app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]
CI 端的 build 指令長這樣:
docker build \
--build-arg APP_VERSION="2.4.1" \
--build-arg GIT_COMMIT="$(git rev-parse HEAD)" \
--build-arg GIT_BRANCH="$(git rev-parse --abbrev-ref HEAD)" \
--build-arg GIT_TAG="$(git tag --points-at HEAD | paste -sd, -)" \
--build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--build-arg BUILD_ID="${CI_BUILD_ID}" \
--build-arg TEST_PASSED="$(jq '.passed' test-report.json)" \
--build-arg TEST_FAILED="$(jq '.failed' test-report.json)" \
--build-arg CASE_TYPE="enterprise" \
-t harbor.yourco.com/products/payment-service:2.4.1 .
這就是 R2K Level 1 的全部入口。 5-15 行 Dockerfile 修改、CI 多幾個 --build-arg。

每個 LABEL key 該放哪一組?簡單原則:
org.opencontainers.image.* : 業界已經有定義的 metadata(version、revision、source、created、title、description、vendor、licenses、authors...)。
能用 OCI 標準的就用 OCI 標準,別發明同義詞。所有 scanner 都認(Trivy、Grype、Cosign、Harbor 內建掃描)。
dev.releaseasknowledge.* : R2K 規格自己的事。例如:
version ── 這顆 image 遵循的 R2K 規格版本level ── 自我宣告達到的 R2K Level(1 / 2 / 3 / 4)commit / branch / tag ── source git 三元組;commit 必填,branch / tag 強烈建議。tag 多個用逗號分隔;無 tag 則省略。build-time ── R2K manifest 的 build 時間(RFC 3339)snapshot.path / snapshot.index ── L2 才會填(見篇 3)diff.mode / diff.from ── L3 才會填(見篇 4)spec.url ── 採用的 R2K 規格文件位置com.yourco.* : Vendor 自家業務欄位,標準沒涵蓋的:case_type、license_modules、test_summary、build_id。
用反向域名(像 Java package 命名)避免跟其他組織的 LABEL key 衝突。
R2K Manifesto 第 4 原則「Standards over invention」在這裡體現 ── 三層命名空間清楚分工,OCI 已有的不重做、R2K 規格只管自己的事、vendor 擴充走自己的 prefix。
LABEL 為什麼比其他機制好?因為讀取極便宜。
任何 scanner 透過 Harbor v2 API 兩個 HTTP call 就能拿到所有 LABEL:
GET /v2/<repo>/manifests/<tag>
└─→ 拿到 config descriptor 的 digest, ~ 2 KB
GET /v2/<repo>/blobs/<config-digest>
└─→ 拿到 image config JSON,內含 LABEL Map, ~ 5 KB
加總不到 10 KB、毫秒級回應。沒有 pull image、沒有解壓 layer、沒有任何重操作。
實際數字:
單一 image 索引掃描: ~50 ms
1000 個 image 全掃: ~50 秒
傳輸總量: ~7 MB
這個成本可以每小時跑一次都不傷 Harbor。
對比一下「pull 整個 image 才能讀 metadata」這個替代方案:
單一 image (150 MB): ~8 秒
1000 個 image: 130 分鐘
傳輸總量: 150 GB
效能差 20000 倍。這就是為什麼 LABEL 該是任何 image inventory 的第一站。

有了 inventory 表,Mary 的場景立刻改變。
她打開 PSP UI,搜「Customer A 的 payment-service」,看到:
✓ 客戶部署版本: 2.3.4
✓ R2K Level: 2 (此 image 已附 snapshot)
✓ 最近 build: 2026-04-28 (commit abc1234)
✓ 測試摘要: 1247 passed, 0 failed
✓ Case type: enterprise (含 retail 模組 + audit 模組)
✓ Build pipeline: GitLab CI #4231
1 秒之內,Mary 拿到她以前花 5 分鐘從 4 個系統拼湊的資料。
這不是新發明的查詢介面 : 是一個 PostgreSQL view 加一個 React table。資料源是 image 自己。
工程師不用維護額外文件、PM 不用更新 Confluence、support 不用問人 : 資料就在 image 裡。
要宣告達到 R2K Level 1,你的 production image 必須:
version、revision、created、title、source
dev.releaseasknowledge.{version, level, commit, build-time},強烈建議補 branch / tag
com.yourco.* 形式(如有)1.0.0
達到這 6 條,你就是 R2K Level 1。可以公開宣告、放在工程 blog、寫在 RFP 裡。
成本:1-2 個工程週(Dockerfile 改一次、CI 改一次、scanner 寫一次)。

LABEL 有一個明顯限制 : 容量。
實務上塞個版本號、commit、test 摘要 OK。塞完整 SBOM 或測試報告就不行 : 一個 LABEL 應該保持在幾百 bytes 以內。整個 image config blob 也不該超過 100 KB。
所以 LABEL 解決了「身份層」:
但解決不了:
這就是 R2K Level 2(Trust)要處理的事 : 下一篇我們來看 /r2k/ snapshot + index.yaml 怎麼補上深度查詢。
LABEL 看起來很樸素。但它是 R2K 整套機制的入口。
絕大多數團隊已經在用 LABEL,只是沒用對方式。用 OCI 標準 + R2K 規格 namespace + 反向域名 vendor 擴充 + build-arg 動態注入 : 這四件事就讓 LABEL 從「沒人看的 metadata」變成「秒回 90% 查詢的身份層」。
這個轉變不需要新工具、不需要新平台、不需要說服老闆 : 只需要 5-15 行 Dockerfile 修改 + CI 多幾個 --build-arg。
這就是 R2K Level 1 的承諾:最小變更、最大回報、最快達成。
LABEL 是 image 的目錄頁 : 所有 image 都該有,所有查詢都該先看這裡。
下篇我們進 R2K Level 2(Trust) : 當資料量變大、當你需要塞完整 SBOM、OpenAPI spec、DB schema,LABEL 不夠用,但 OCI 還有另一個機制:metadata layer,並用 /r2k/index.yaml 當入口清單。
R2K 系列導覽
Release-as-Knowledge · R2K · 軟體發布即知識傳遞 · v1 · CC BY 4.0
完整介紹 : https://enjtorian.github.io/release-as-knowledge/zh-tw/
官方網站 : https://www.releaseasknowledge.com