🔬 BACOT — 15:00 Technical Exploration
📚 Topik: CI/CD — Artifact Immutability & Progressive Delivery
Rantai dua lapis: artifact yang tidak bisa berubah (digest + attestation) lalu pelepasan bertahap (canary/blue-green dengan analisis metrik otomatis). Satu tanpa yang lain rapuh: canary yang mulus tidak berarti apa-apa kalau image yang di-deploy bisa di-push ulang dengan isi berbeda.
Status verifikasi (22 Okt 2026):
| Komponen | Versi terverifikasi | Sumber |
|---|---|---|
| Argo Rollouts | v1.10.1 (rilis 12 Okt 2026) | GitHub Releases |
| Kargo | v1.12.4 (rilis 12 Okt 2026) | GitHub Releases |
| cosign | v3.1.4 (latest), line v2.6.5 masih dirawat | GitHub Releases |
actions/attest-build-provenance |
v4.2.2 (6 Agu 2026) — kini wrapper tipis di atas actions/attest |
GitHub Releases |
| SLSA spec | v1.2 versi current (v1.1 rilis 21 Apr 2025) | slsa.dev |
🎯 Tujuan Belajar
Setelah sesi ini bisa:
- Menjelaskan kenapa tag ≠ identitas dan memaksa digest pinning di manifest GitOps.
- Menerbitkan build provenance yang bisa diverifikasi (
cosign verify-attestation) dan memblokir image tanpa provenance di admission. - Menulis Rollout + AnalysisTemplate yang benar-benar menggating deploy berdasarkan metrik, termasuk menjebak empty result yang bikin canary lolos palsu.
- Membedakan canary traffic-weighted vs canary pod-ratio — dan tahu kapan angkanya bohong.
- Menaikkan level SLSA dari L2 ke L3 hanya dengan memindahkan signing ke reusable workflow.
🏗️ Arsitektur & Alur Teknis
commit SHA ──build──▶ image digest (sha256:…) ──attest──▶ sigstore bundle (provenance)
│ │
│ ▼
│ admission policy (Kyverno/cosign)
▼ │
registry (immutable tag) │ allow/deny
│ │
▼ ▼
Git commit: image: repo@sha256:… ──▶ Argo CD sync ──▶ Rollout
│
canary 20% ─▶ AnalysisRun
│
pass ─▶ naik │ fail ─▶ abort, weight 0
Tiga invarian yang harus dijaga:
- Build sekali, promosikan digest. Promosi = copy blob antar registry (
crane copy,skopeo copy --all), bukan rebuild. Rebuild di tiap environment = artifact berbeda = provenance tidak berlaku. - Digest adalah satu-satunya identitas.
app:1.4.2bisa ditimpa.app@sha256:…tidak. - Gerbang verifikasi ada di admission, bukan di pipeline. Pipeline bisa dilewati (hotfix manual,
kubectl set image). Admission tidak.
Level SLSA Build Track (spec v1.2)
| Level | Syarat | Menangkal |
|---|---|---|
| L1 | Ada provenance (bagaimana dibangun) | Kesalahan dokumentasi |
| L2 | Provenance ditandatangani, dihasilkan hosted build platform | Tampering setelah build |
| L3 | Build platform hardened (isolasi, tidak bisa disusupi saat build) | Tampering saat build |
DIKETAHUI: di GitHub Actions, attestation di hosted runner = L2 secara default. Naik ke L3 butuh provenance dihasilkan dari reusable workflow — verifier jadi bisa memastikan workflow spesifik mana yang menandatangani, dan workflow itu tidak bisa diubah oleh repo pemanggil.
💻 Kode Nyata
1. Build + attest + output digest (GitHub Actions)
actions/attest-build-provenance@v4 — perhatikan, v4 hanya wrapper; implementasi baru disarankan langsung actions/attest.
# .github/workflows/build.yml
name: build
on:
push:
tags: ['v*']
permissions:
contents: read
packages: write
id-token: write # OIDC → sigstore keyless
attestations: write # tulis attestation ke registry
jobs:
build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
# tag berguna untuk manusia; digest yang dipakai mesin
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
provenance: false # provenance buildkit dimatikan, pakai attestation terpisah
sbom: false
- uses: actions/attest-build-provenance@v4
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
steps.build.outputs.digest berisi sha256:<hex> — inilah nilai yang masuk ke Git.
2. Update manifest GitOps dengan digest, bukan tag
# di job promote; butuh: pip install ruamel.yaml / pakai yq
DIGEST="${{ needs.build.outputs.digest }}"
yq -i '.spec.template.spec.containers[0].image =
"ghcr.io/org/app@'"$DIGEST"'"' k8s/base/deployment.yaml
git commit -am "chore: promote app@${DIGEST:0:19}"
Verifikasi lokal (cosign v3 — bundle format jadi default):
# v3: bundle adalah format default output & input; flag lama terdeprekasi
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp '^https://github\.com/org/app/\.github/workflows/build\.yml@refs/tags/v.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/org/app@sha256:abc123... \
| jq -r '.payload' | base64 -d | jq '.predicate.buildDefinition'
--certificate-identity-regexp adalah bagian yang sering dilupakan orang. Tanpa itu, provenance dari repo siapa pun akan lolos — attestation valid tapi bukan buatan Anda. Ini setara "tanda tangan sah dari orang asing".
3. Tolak image tanpa provenance di cluster (Kyverno)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-slsa-provenance
spec:
validationFailureAction: Enforce
rules:
- name: verify-attestation
match:
any:
- resources:
kinds: [Pod]
namespaces: [production]
verifyImages:
- imageReferences: ["ghcr.io/org/*"]
attestations:
- predicateType: https://slsa.dev/provenance/v1
attestors:
- entries:
- keyless:
subject: "https://github.com/org/app/.github/workflows/build.yml@refs/tags/v*"
issuer: "https://token.actions.githubusercontent.com"
4. Rollout canary + analisis yang tidak bisa dibohongi
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: api
spec:
replicas: 10
revisionHistoryLimit: 3
selector:
matchLabels: {app: api}
template:
metadata:
labels: {app: api}
spec:
containers:
- name: api
image: ghcr.io/org/app@sha256:abc123 # digest, bukan tag
ports: [{containerPort: 8080}]
strategy:
canary:
maxSurge: 1
maxUnavailable: 0
trafficRouting:
istio:
virtualService: {name: api-vs, routes: [primary]}
analysis:
templates:
- templateName: success-rate
startingStep: 1 # mulai analisis setelah 20% tercapai
args:
- name: service
value: api.canary.svc.cluster.local
steps:
- setWeight: 20
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 10m}
- setWeight: 100
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service
metrics:
- name: success-rate
interval: 1m
# empty array HARUS eksplisit ditangani, kalau tidak hasilnya ambiguous
successCondition: len(result) == 0 || result[0] >= 0.99
failureLimit: 3
consecutiveErrorLimit: 3
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{
service="{{args.service}}", code!~"5.."}[2m]))
/
sum(rate(http_requests_total{service="{{args.service}}"}[2m]))
Perilaku: AnalysisRun jalan di background selama canary. 3 pengukuran gagal → Rollout Degraded, weight kembali 0, canary di-scale down. Canary selesai semua step → AnalysisRun dihentikan controller, dianggap sukses.
consecutiveErrorLimit membedakan metrik jelek (failure) dari query error (misconfig Prometheus, timeout). Tanpa itu, Prometheus down = canary abort, dan tim belajar mematikan analisis. Itu kegagalan sosial, bukan teknis.
5. Promosi antar-stage (Kargo)
apiVersion: kargo.akuity.io/v1alpha1
kind: Stage
metadata:
name: staging
spec:
requestedFreight:
- origin: {kind: Warehouse, name: app}
sources: {direct: true}
promotionTemplate:
spec:
steps:
- uses: git-clone
config: {repoURL: https://github.com/org/gitops, path: ./out}
- uses: yaml-update
as: update
config:
path: ./out/staging/deployment.yaml
updates:
- key: spec.template.spec.containers.0.image
value: ${{ imageFrom("ghcr.io/org/app").RepoURL }}@${{ imageFrom("ghcr.io/org/app").Digest }}
- uses: git-commit
as: commit
config: {path: ./out, message: "promote to staging"}
- uses: git-push
config: {path: ./out}
- uses: argocd-update
config:
apps:
- name: api-staging
sources:
- repoURL: https://github.com/org/gitops
desiredRevision: ${{ outputs.commit.commit }}
API kargo.akuity.io/v1alpha1 terverifikasi dari docs resmi. Nilai ${{ imageFrom(...).Digest }} membuat promosi hanya mungkin untuk digest yang sudah ada — stage berikutnya tidak bisa membangun versi sendiri.
📊 Angka & Benchmark
Batasan jujur: tidak ada benchmark kuantitatif peer-reviewed untuk "immutability menurunkan insiden X%". Klaim semacam itu di blog vendor umumnya anekdotal.
Yang bisa dipegang:
- DIKETAHUI — interval analisis minimum realistis dibatasi resolution scrape Prometheus (default 15–60s).
interval: 1mdenganrate(...[2m])butuh minimal 2 titik scrape;intervallebih pendek dari scrape interval menghasilkan query kosong berulang → lihat failure mode #3. - DIKETAHUI —
maxSurge: 1, maxUnavailable: 0pada 10 replika berarti canary 20% = 2 pod baru, 8 pod lama. TanpatrafficRouting, "20%" adalah rasio pod, bukan rasio request. Kalau pod baru lebih lambat (cold cache, JIT belum warm), rasio request sebenarnya bisa 35–40% ke versi baru. Angka ini DIINFERENSI dari mekanik load balancing, bukan hasil pengukuran. - BELUM PASTI — angka "MTTR turun 60%" dari case study vendor. Tidak ada metodologi terbuka.
⚖️ Trade-off & Alternatif
| Aspek | Argo Rollouts | Flagger | Plain Deployment + manual |
|---|---|---|---|
| Model | Ganti Deployment → CRD Rollout |
Bungkus Deployment yang ada dengan CR Canary |
— |
| Kontrol step | Eksplisit, per-step | Deklaratif, auto-increment stepWeight |
Tidak ada |
| Invasif | Tinggi (ganti kind) | Rendah | Nol |
| Traffic shaping | Butuh mesh/ingress plugin | Sama | Tidak ada |
| Analisis | CRD terpisah (AnalysisTemplate/AnalysisRun), bisa dipakai standalone |
MetricTemplate inline |
Tidak ada |
| Ekosistem | Argo CD, Kargo | Flux, Argo CD | — |
Trade-off yang sering tidak diucapkan:
- Rollout mengganti
Deployment.kubectl rollout restart deploymentberhenti bekerja. Semua runbook, HPAscaleTargetRef, PDB selector, dan alert berbasis nama Deployment harus diperiksa. Ini biaya migrasi nyata, sering diremehkan. - Analisis otomatis memindahkan wewenang. Tim yang tidak mempercayai metriknya akan menonaktifkan analisis setelah insiden pertama. Sebelum pasang AnalysisTemplate, pastikan metriknya sudah dipercaya 3 bulan.
- Digest pinning mematikan
latestsebagai workflow. Renovate/Dependabot wajib dikonfigurasi untuk update digest, kalau tidak semua dependency beku diam-diam. - Attestation ≠ jaminan kode aman. Hanya membuktikan asal-usul. Supply chain attack lewat dependency berbahaya tetap lolos — itu domain SCA/SBOM, bukan provenance.
Alternatif lebih ringan kalau tim <5 orang: Deployment dengan maxSurge/maxUnavailable konservatif + kubectl rollout status di pipeline + Prometheus alert. Tidak ada auto-abort, tapi nol biaya operasional baru. Rollouts baru bernilai saat frekuensi deploy >10/hari atau blast radius sudah terasa.
🏭 Use Case Produksi & Failure Mode Umum
Cocok dipakai: layanan dengan traffic nyata dan SLO terukur; API backend; migrasi bertahap dengan DB backward-compatible. Tidak cocok: batch job, worker tanpa traffic masuk (tidak ada metrik request — analisis berbasis success rate tidak berarti), atau sistem dengan <100 req/menit (sampel terlalu kecil, successCondition jadi noise).
Failure mode yang benar-benar terjadi:
-
Tag ditimpa.
:latestatau bahkan:1.4.2di-push ulang. Rollout melihat image "sama", tidak trigger apa pun, padahal isi berbeda. Cluster menjalankan kode yang bukan dari commit yang di-review. Dicegah hanya dengan digest + admission. -
Attestation valid, identitas salah.
cosign verify-attestationtanpa--certificate-identity-regexpmenerima provenance dari fork mana pun. Ini setara tidak verifikasi. -
Empty result dianggap sukses. Query Prometheus mengembalikan
[](label salah, scrape mati, jendela waktu kosong). DefaultsuccessCondition: result[0] >= 0.99gagal karena index out of range, tapi varianresult[0] < 0.01untuk error rate akan lolos — canary 0% traffic terlihat "sehat". Dokumentasi resmi menyarankan eksplisit:len(result) == 0 || .... Untuk Datadog, hasil kosong mengembalikannildan errorinvalid operation: < (mismatched types <nil> and float64); workaround resmidefault(result, 0) < 0.05. -
Breaking change keamanan Argo Rollouts v1.10.1. Dashboard kini hanya bind ke localhost secara default — memperbaiki GHSA-366v-5xmx-36vh / CVE-2026-82277. Siapa pun yang mengekspos dashboard lewat Helm chart kustom atau Service wajib menambahkan flag
--address 0.0.0.0, kalau tidak dashboard mati setelah upgrade. Ini contoh konkret kenapa patch keamanan tidak boleh di-merge tanpa membaca release note. -
Canary tanpa traffic routing.
setWeight: 20pada Rollout tanpatrafficRoutinghanya mengubah jumlah pod. Semua request tetap masuk ke semua pod lewat Service. "Canary" menjadi dekorasi. -
Migrasi cosign v2 → v3. v3 menjadikan bundle format, trusted root, signing config, dan statement-based attestation sebagai default; sejumlah flag verifikasi material jadi deprecated. Script verifikasi lama bisa gagal diam-diam atau butuh flag baru. cosign v2.6.x masih dirawat untuk masa transisi — rencanakan, jangan panik.
-
Reusable workflow yang dipanggil lewat
@main. Untuk SLSA L3, verifier harus bisa mengikat ke workflow immutable. Referensi@v1atau@mainmembatalkan sebagian jaminan. Pin ke commit SHA.
🛠️ Latihan Praktis Kecil
Lokal, tanpa cluster:
# 1. Buktikan tag bisa ditimpa, digest tidak
docker pull alpine:3.20
docker inspect --format '{{index .RepoDigests 0}}' alpine:3.20
# tarik ulang setelah upstream update → digest berubah, tag tetap sama
# 2. Copy antar registry hanya dengan digest (simulasi promosi)
crane copy ghcr.io/org/app@sha256:abc123 \
registry.internal/app@sha256:abc123
# tidak ada build, tidak ada rebuild — byte identik
# 3. Inspeksi provenance
cosign download attestation ghcr.io/org/app@sha256:abc123 | jq .
Dengan kind (10 menit):
kind create cluster --name pd
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
kubectl argo rollouts dashboard # perhatikan: sekarang bind localhost saja
Lalu deploy Rollout dengan steps: [setWeight: 20, pause: {}] dan pause: {} tanpa durasi → rollout berhenti menunggu kubectl argo rollouts promote api. Amati bahwa tanpa traffic routing, kubectl get pods menunjukkan 2 pod baru tapi request tetap tersebar rata.
✅ Cek Pemahaman
-
Tim memakai
image: app:1.4.2+imagePullPolicy: Alwaysdan Rollout canary. Kenapa canary bisa hijau sementara produksi menjalankan kode yang berbeda dari yang diuji? Apa satu perubahan minimal yang menutup celah ini? -
AnalysisTemplatedengansuccessCondition: result[0] < 0.05(error rate) dijalankan tapi labelservicesalah ketik. Apa yang terjadi pada canary, dan kenapafailureLimit: 3tidak menyelamatkan? -
Anda sudah punya attestation dari
actions/attest-build-provenance@v4di hosted runner, dipanggil langsung dari workflow repo. Level SLSA berapa yang tercapai, dan apa satu langkah spesifik untuk naik ke L3?
Jawaban
1. `imagePullPolicy: Always` membuat tiap node menarik tag yang **saat itu** menunjuk ke lapisan berbeda. Node yang menarik lebih awal dapat versi lama, node baru dapat versi baru — dua versi berjalan bersamaan di bawah satu nama. Canary menguji campuran acak, bukan versi tertentu. Perbaikan minimal: pin `app@sha256:…` di manifest; `imagePullPolicy` jadi tidak relevan. 2. Query mengembalikan `[]`. `result[0]` → index out of range. Tergantung provider, ini tercatat sebagai **error**, bukan failure. `failureLimit` menghitung measurement yang gagal secara semantik; error dihitung terpisah (`consecutiveErrorLimit`). Kalau error limit longgar atau tidak diset, AnalysisRun bisa menggantung `Running` tanpa pernah fail — canary macet, bukan abort. Yang benar: `len(result) == 0 || result[0] < 0.05`, plus `consecutiveErrorLimit: 3`. 3. L2. Attestation ditandatangani platform hosted, tapi workflow yang menandatangani adalah workflow yang bisa diubah oleh repo itu sendiri. Naik ke L3: pindahkan langkah build+attest ke **reusable workflow** (`on: workflow_call`) yang di-pin oleh repo pemanggil ke commit SHA, dan verifikasi dengan `--certificate-identity-regexp` yang menunjuk ke path reusable workflow tersebut, bukan ke workflow pemanggil.🔑 Poin Kunci
- Tag adalah alias, digest adalah identitas. Semua jaminan lain runtuh kalau manifest memakai tag.
- Build sekali, promosikan digest. Rebuild per environment membatalkan provenance.
- Verifikasi di admission, bukan pipeline. Pipeline bisa dilewati; admission tidak.
--certificate-identity-regexpadalah verifikasi sebenarnya. Tanpa itu, attestation hanya bukti bahwa seseorang menandatangani.- Empty result adalah bug canary nomor satu. Selalu
len(result) == 0 ||secara eksplisit. - Canary tanpa traffic routing = pod ratio. Angkanya bukan persentase request.
- Patch keamanan bisa breaking. v1.10.1 mengubah bind address dashboard; baca release note sebelum merge.
- Analisis otomatis butuh metrik yang sudah dipercaya. Kalau tidak, tim akan mematikannya setelah insiden pertama.
➡️ Eksplorasi Lanjut
- SBOM + VEX — provenance membuktikan asal, SBOM membuktikan isi, VEX menyatakan mana CVE yang benar-benar affected. Rantai lengkap:
syft→ attest →grype+ VEX → gate. Ini yang menjawab "apakah CVE ini benar-benar masalah saya". consecutiveErrorLimitvsfailureLimitvsinconclusiveLimit— baca tiga field ini di specAnalysisTemplate; kombinasinya menentukan apakah canary abort, lanjut, atau menggantung. PerilakuInconclusivejarang dipahami dan sering jadi sumber on-call.- Argo Rollouts plugin architecture — docs resmi menyatakan tidak menerima integrasi metrik baru ke core controller; semua provider baru harus plugin (metrics, traffic, steps). Kalau butuh provider internal, ini jalurnya.
- Kargo
Warehouse+ freight — model promosi berbasis artifact, bukan commit. Bandingkan dengan Argo CD ApplicationSet + PR-based promotion: mana yang lebih cocok untuk 3 environment vs 30 microservice. - Blue-green dengan
scaleDownDelaySeconds— untuk layanan dengan koneksi panjang (WebSocket, gRPC streaming), canary berbasis weight salah alat; blue-green dengan delay lebih tepat.