← blog · 7 Ekim 2026

Bir Kez Derle, Özetle Terfi Ettir: Test Edilen İmajın Kendisini Yayınlamak

Sürüm etiketini gördükten sonra yeniden derlemek, test edilen baytla yayınlanan baytı birbirinden ayırır. Adayı özetiyle itmek, testlerden sonra aynı özete etiket koymak, çok mimarili indeksler, OCI açıklamaları, imaj dışı paketler, imza ve terfi adımının tuzakları.

Yaygın bir yayın hattı şöyle çalışır: her commit'te testler koşar, biri v1.4.2 etiketini itince ayrı bir yayın işi depoyu yeniden çeker, imajı sıfırdan derler ve kayıt defterine iter. Aynı commit, aynı Dockerfile; sorun yok gibi görünür. Oysa testlerin gördüğü imaj ile kullanıcıya giden imaj iki ayrı derlemenin ürünüdür. Bu yazı ikisini aynı baytlara indiren deseni anlatıyor: bir kez derle, çıkan özeti (digest) test et, testler geçince aynı özete sürüm etiketini koy.

Aynı commit, farklı bayt

Yeniden derlemenin aynı sonucu vermesi için bütün girdilerin sabit olması gerekir. Birkaçı neredeyse hiç sabit değildir:

  • FROM python:3.12-slim gibi bir taban imaj etiketi, yamalar çıktıkça başka bir özete taşınır.
  • Sürümü sabitlenmemiş apt-get install, kilit dosyası olmadan çalışan npm install ya da aralıkla yazılmış pip bağımlılıkları derleme anında en yeni ne varsa onu getirir.
  • Zaman damgaları ve derleme argümanı olarak geçirilen bir tarih, işlevsel olarak aynı imaja bile farklı bir özet verir. "Aynı mı" sorusuna bayt düzeyinde cevap veremezsiniz.

Sonuç: yayınlanan imaj hiçbir testten geçmemiştir. BuildKit zaman damgalarını SOURCE_DATE_EPOCH ile sabitleyebilir, ama paket depolarını dondurmaz. Bir kez derlemek, tam yeniden üretilebilirlikten çok daha ucuzdur ve aynı garantiyi doğrudan verir.

Desen: aday, test, terfi

Commit'te bir kez derlenir, imaj etiketsiz ve yalnızca özetiyle itilir. Testler o özeti çeker; geçerlerse aynı özete sürüm etiketi eklenir ve hiçbir şey yeniden derlenmez.

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --output type=image,name=registry.example.com/app,push-by-digest=true,push=true \
  --metadata-file build.json .

DIGEST=$(jq -r '."containerimage.digest"' build.json)

İşler arasında etiket değil, registry.example.com/app@sha256:... biçimindeki tam referans taşınır; test ortamı da bununla kurulur. Terfi şu komutlardan biridir:

crane tag "registry.example.com/app@${DIGEST}" 1.4.2
oras tag "registry.example.com/app@${DIGEST}" 1.4.2 latest
docker buildx imagetools create -t registry.example.com/app:1.4.2 "registry.example.com/app@${DIGEST}"

test "$(crane digest registry.example.com/app:1.4.2)" = "$DIGEST"

Son satırdaki ölçüm boş bir tören değildir. imagetools create tek kaynakla ve açıklama eklenmeden çağrılırsa bir indeksi olduğu gibi etiketler. Kaynak tek mimarili bir manifestse varsayılan --prefer-index=true onu yeni bir indekse sarar ve özet değişir; --annotation eklemek de özeti değiştirir. Başka bir kayıt defterine taşırken crane copy özeti korur, skopeo copy ise --all verilmezse yalnızca çalıştığı makinenin mimarisini kopyalar.

Çok mimarili imajlarda hangi özet

Çok mimarili bir imaj bir manifest listesidir (OCI'de image index). İndeksin kendi özeti vardır, her mimarinin manifesti de ayrıca kendi özetini taşır; docker buildx imagetools inspect ikisini birlikte gösterir. Terfi edilen özet indeksinki olmalıdır, yoksa sürüm tek mimariyle yayınlanır.

Daha az fark edilen nokta testin kapsamıdır. Bir amd64 makinede indeksi çeken test yalnızca amd64 manifestini çalıştırır; arm64 hiç test edilmemiştir. Ya testleri her mimaride koşun ya da hangi mimarinin test edildiğini kayda geçirin. Mimariye özgü özeti crane digest --platform linux/arm64 verir.

Mimariler ayrı makinelerde derleniyorsa her biri özetiyle itilir, bir birleştirme adımı indeksi üretir:

docker buildx imagetools create -t registry.example.com/app:sha-3f9c2e1 \
  registry.example.com/app@sha256:<amd64-özeti> \
  registry.example.com/app@sha256:<arm64-özeti>

Aday indeks burada doğar, dolayısıyla sıra derleme, birleştirme, test, terfi olmalıdır. Kaynak kanıtı açıksa BuildKit doğrulama manifestlerini de indekse unknown/unknown platformuyla ekler; tek mimarili bir derleme bile indeks olarak itilir.

İmaj kendini anlatsın

Özet, hangi commit'ten çıktığını söylemez. OCI'nin org.opencontainers.image.revision, org.opencontainers.image.version ve org.opencontainers.image.source anahtarları bunun içindir. Label'lar imaj yapılandırmasına, açıklamalar (annotation) ise manifest ya da indekse yazılır.

docker buildx build \
  --label "org.opencontainers.image.revision=$GIT_SHA" \
  --annotation "index,manifest:org.opencontainers.image.revision=$GIT_SHA" \
  --annotation "index,manifest:org.opencontainers.image.version=$VERSION" ...

Bunlar derlemede yazılır; terfide eklenen açıklama yeni bir özet doğurur. Buradan bir karar çıkar: sürüm numarası derlemeden önce belirlenir. İkili --version çıktısında sürümünü yazdırıyorsa ve numara testlerden sonra seçiliyorsa, onu içeri koymanın tek yolu yeniden derlemektir. Ben numarayı aday derlenirken veriyorum; aday testten kalırsa numara yanar, sonraki aday sonraki numarayı alır. Sürüm numarası ucuzdur, yeniden derlenmiş bir sürüm değildir.

Son kontrol olarak ikiliye sorun: docker run --rm "registry.example.com/app@${DIGEST}" --version çıktısındaki sürüm ve commit etiketle birebir eşleşmelidir. Go'da bunları -ldflags "-X main.version=$VERSION -X main.commit=$GIT_SHA" ile açıkça geçirin; konteyner derlemesinde .git çoğunlukla bağlamın dışında kaldığı için go version -m çıktısında vcs.revision hiç yer almayabilir.

İmaj dışı çıktılar

Masaüstü kurulum paketleri, CLI ikilileri ve arşivlerde özetin yerini SHA-256 alır. Derleme işi dosyaları ve bir SHA256SUMS listesini artifact olarak saklar, test işi onu indirir. Yayın işi derlemez; aynı artifact'ı indirir, doğrular ve yükler:

- uses: actions/download-artifact@v8
  with:
    name: dist
    run-id: ${{ inputs.candidate_run_id }}
    github-token: ${{ secrets.ARTIFACT_READ_TOKEN }}
- run: sha256sum -c SHA256SUMS

Kod imzalama sırayı değiştirir: Windows'ta Authenticode, macOS'ta codesign imzası dosyanın baytlarını değiştirir. İmzayı yayın adımında atarsanız test edilen dosya ile dağıtılan yine farklıdır. İmzayı aday aşamasında atın, imzalı dosyayı test edin.

İmza ve kaynak kanıtı

cosign imzası özete bağlanır. Etiketi değil registry.example.com/app@${DIGEST} referansını imzalayın; etiket verilince cosign uyarır, çünkü etiket o anda başka bir imajı gösteriyor olabilir. Terfide eklenen etiket imzayı bozmaz. Asıl kazanç dağıtım tarafındadır: küme yalnızca belirli bir kimlikle imzalanmış özetleri kabul ederse, bir köşede yeniden derlenmiş imzasız imaj çalışamaz ve "bir kez derle" alışkanlıktan mekanik bir kapıya dönüşür.

cosign sign "registry.example.com/app@${DIGEST}"
cosign verify registry.example.com/app:1.4.2 \
  --certificate-identity="$BUILD_IDENTITY" --certificate-oidc-issuer="$OIDC_ISSUER"

Kaynak kanıtı (SLSA provenance) imajın hangi kaynaktan, hangi adımlarla üretildiğini yanında taşır. BuildKit onu --provenance=mode=max ile üretir; mode=max derleme argümanlarının değerlerini de içerdiği için argümanla sır geçirmeyin. Dosyalar için GitHub artifact doğrulamaları gh attestation verify ile denetlenir.

Tuzaklar

Adayın ömrü. Etiketsiz imajlar, ECR yaşam döngüsündeki tagStatus: untagged gibi temizlik kurallarına takılabilir. CI artifact'ları da kalıcı değildir: GitHub Actions'ta varsayılan saklama 90 gündür, GitLab'da süreyi artifacts:expire_in belirler. Onay uzun bir hafta sonunu beklerse aday silinmiş olabilir ve tek seçenek yeniden derlemek kalır. Adaylara sha-<commit> gibi bir etiket verin ve saklama süresini en uzun onay beklemenizden uzun tutun.

Terfi adımı yayın kanallarını atlar. Eski yayın işi yalnızca imaj itmiyordu; güncelleme kanalının dosyasını yazıyor, paket deposunun indeksini yeniliyor, sürüm notunu yayımlıyordu. Onu "özete etiket koy" adımıyla değiştirince bu yan işler sessizce kaybolur: etiket doğru yerdedir ama kullanıcı yeni sürümü görmez. Eski işin yaptığı her şeyi listeleyip terfiye taşıyın ve terfiyi imagetools create --dry-run gibi kuru koşularla deneyin.

Aynı etiketi ezmek. Yayınlanmış 1.4.2 başka bir özete taşınırsa imajı önbellekte tutan makineler eskisini çalıştırmaya devam eder; Kubernetes'te :latest dışındaki etiketlerde varsayılan imagePullPolicy IfNotPresent olduğundan aynı adla iki farklı kod koşar. Sürüm etiketlerini değiştirilemez yapın (ECR'de IMMUTABLE, Harbor'da değişmezlik kuralları), kopyalarken crane copy --no-clobber kullanın; yalnızca latest gibi gezici etiketler hareket etsin.

Etiketle test etmek. Test işi app:candidate etiketini çekerken paralel bir hat aynı etikete yeni aday iterse, test başka bir imajı doğrulamış olur. İşler arasında daima özeti taşıyın.

Kısa kontrol listesi

Her commit bir kez derlenir ve özetiyle itilir. Testler, dağıtımlar ve imzalar özete başvurur. Sürüm ve commit derlemede yazılır, terfi hiçbir şey eklemez ve sonrasında özet ölçülür. Çok mimarili imajlarda indeks terfi eder. Sürüm etiketleri değiştirilemez, adaylar onay süresinden uzun yaşar ve eski yayın işinin bütün yan etkileri terfi adımına taşınmıştır. O zaman "test ettiğimizi mi yayınladık" sorusunun cevabı tek bir karşılaştırma komutuna iner.