TT Lab
はじめる
学ぶ 学習パス コース

CI/CDパイプライン

ハッシュで名前を付けても再現できるとは限らない

TT Labで続きを見る

一言でいうと

アーティファクトにハッシュで名前を付けることと、同じソースから同じバイト列が出るようにすることは、別の話です。前者はラベルを正確に付ける作業で、後者はビルドを「入力が同じなら出力が同じ関数」にする作業です。ラベルだけ付けておいて再現できると言っているパイプラインは、非常に多くあります。

なぜ必要なのか

「ソースのハッシュでタグを付けたから再現できる」という言葉はよく聞きますが、証明されたことはまれです。同じコミットを2回ビルドして、2つのアーティファクトのSHA-256を直接比べてみると、たいてい違います。それなのに名前は同じです。名前が同じで内容が違うアーティファクトは、3つのものを同時に壊します。キャッシュが嘘をつき、プロモーションのモデルが崩れ、障害の振り返りで「あのとき出ていったのは正確にこれなのか」という問いに誰も答えられなくなります。

再現性が実際に買ってくれるものは3つあります。1つ目は、キャッシュを信頼できるようになることです。同じキーなら同じ結果という前提が成り立ってはじめて、キャッシュのヒットを「ビルドを省略してよい」根拠として使えます。2つ目は、アーティファクトの来歴を、作り直して確認できることです。同じソースと同じ手順でもう一度ビルドしてバイト列が一致すれば、そのアーティファクトがそのソースから出たことを、署名がなくても突き合わせられます。3つ目は、障害のとき、そのときのアーティファクトを復元できることです。レジストリから削除されていても、ソースと手順が残っていれば、同じバイト列をもう一度得られます。

非決定性はどこから入るのか

出どころはほぼ決まっています。新しい言語や新しいツールに出会っても、リストは大きくは変わりません。

どう固定するのか

中心となる約束がSOURCE_DATE_EPOCHです。公式ドキュメントは、この変数を「何かの、通常はソースコードの最終更新時刻を、Unixエポックからの秒数で表した値」と定義しています。ビルドツールが時刻を書かなければならないときに、現在時刻の代わりにこの値を使うことにしたものです。値は、たいていそのコミットの時刻から取得します。

アーカイブは、特に注意が必要です。公式ドキュメントが勧める形は次のとおりです。

export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"
tar --sort=name \
    --mtime="@${SOURCE_DATE_EPOCH}" \
    --owner=0 --group=0 --numeric-owner \
    --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \
    -cf product.tar build
gzip -6 -n < product.tar > product.tar.gz   # -n 은 원본 이름과 시각을 빼고 압축한다

--sort=nameがファイルの順序を、--mtimeが時刻を、--owner/--group/--numeric-ownerが所有者を、--pax-optionがPAXヘッダーに混ざり込むatime/ctimeを、それぞれなくします。この組み合わせには、GNU tar 1.28以上が必要です。そして判定は、目ではなくハッシュで行います。2回作ってsha256sumが同じかを見ること。それ以外に、再現性を確認する方法はありません。

ダイジェストが保証するものと、しないもの

OCIイメージ仕様は、ダイジェストを「バイト列に対する衝突耐性のあるハッシュ」と定義し、これがコンテンツアドレッシングを可能にすると書いています。ダイジェストを安全な経路で受け取ったなら、信頼できない場所から受け取った内容であっても、ハッシュを計算し直して照合することで、改ざんされていないことを確認できるということです。形式は알고리즘:인코딩된값(プレースホルダーはアルゴリズムとエンコードされた値です)で、仕様は、信頼できない出どころの内容は、使う前にダイジェストで検証するよう勧告しています。

この性質は、保存形式にもそのまま組み込まれています。OCIイメージレイアウト仕様は、blobs/<알고리즘>/<인코딩된값>(プレースホルダーはアルゴリズムとエンコードされた値です)に置かれた内容が、必ずダイジェスト알고리즘:인코딩된값と一致しなければならないと求めています。つまり、ファイル名が場所ではなく内容そのものを指します。レイアウトには、oci-layoutと、エントリポイントの役割をするindex.jsonも一緒に必要です。

ここで、線を正確に引く必要があります。ダイジェストが保証するのは、自分が受け取ったバイト列が、そのバイト列で間違いないということまでです。そのバイト列がどのソースから、どんな手順で出てきたのかは、まったく教えてくれません。その結びつきを主張するには、ビルドが何を入力に、どんな手順を経たのかを別に記録する必要があり、それが来歴証明(provenance)の役割です。再現可能なビルドは、その主張を誰でも作り直して検証できるようにするという点で、対になります。

現場での姿

参考

次のラボですること

シェルとtar、sha256sumだけで、同じソースから同じバイト列が出るかを自分で突き合わせます。まず、何の対策もなくアーカイブを2回作ってハッシュが分かれるのを確認し、時刻・順序・所有者・gzipヘッダーを1つずつ固定しながら、どの項目が何バイトを動かしたのかを目で追います。最後に、SOURCE_DATE_EPOCHをコミット時刻から取り出して使うビルドスクリプトにまとめ、2回実行した結果のハッシュが同じかどうかで判定します。このPodではコンテナを起動できないので、イメージ側は、skopeoですでにあるoci-archiveのダイジェストを読んで突き合わせる方法で扱います。