← semua materi
15:00 Teknis · 22 Oct 2026 · 15:03

🔬 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:

  1. Menjelaskan kenapa tag ≠ identitas dan memaksa digest pinning di manifest GitOps.
  2. Menerbitkan build provenance yang bisa diverifikasi (cosign verify-attestation) dan memblokir image tanpa provenance di admission.
  3. Menulis Rollout + AnalysisTemplate yang benar-benar menggating deploy berdasarkan metrik, termasuk menjebak empty result yang bikin canary lolos palsu.
  4. Membedakan canary traffic-weighted vs canary pod-ratio — dan tahu kapan angkanya bohong.
  5. 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:

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:


⚖️ 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:

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:

  1. Tag ditimpa. :latest atau bahkan :1.4.2 di-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.

  2. Attestation valid, identitas salah. cosign verify-attestation tanpa --certificate-identity-regexp menerima provenance dari fork mana pun. Ini setara tidak verifikasi.

  3. Empty result dianggap sukses. Query Prometheus mengembalikan [] (label salah, scrape mati, jendela waktu kosong). Default successCondition: result[0] >= 0.99 gagal karena index out of range, tapi varian result[0] < 0.01 untuk error rate akan lolos — canary 0% traffic terlihat "sehat". Dokumentasi resmi menyarankan eksplisit: len(result) == 0 || .... Untuk Datadog, hasil kosong mengembalikan nil dan error invalid operation: < (mismatched types <nil> and float64); workaround resmi default(result, 0) < 0.05.

  4. 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.

  5. Canary tanpa traffic routing. setWeight: 20 pada Rollout tanpa trafficRouting hanya mengubah jumlah pod. Semua request tetap masuk ke semua pod lewat Service. "Canary" menjadi dekorasi.

  6. 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.

  7. Reusable workflow yang dipanggil lewat @main. Untuk SLSA L3, verifier harus bisa mengikat ke workflow immutable. Referensi @v1 atau @main membatalkan 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

  1. Tim memakai image: app:1.4.2 + imagePullPolicy: Always dan Rollout canary. Kenapa canary bisa hijau sementara produksi menjalankan kode yang berbeda dari yang diuji? Apa satu perubahan minimal yang menutup celah ini?

  2. AnalysisTemplate dengan successCondition: result[0] < 0.05 (error rate) dijalankan tapi label service salah ketik. Apa yang terjadi pada canary, dan kenapa failureLimit: 3 tidak menyelamatkan?

  3. Anda sudah punya attestation dari actions/attest-build-provenance@v4 di 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


➡️ Eksplorasi Lanjut