Terraform で archive_file を使った変更検知
この記事の概要
-
archive プロバイダー の
archive_file
データソース は zip ファイルを作成する機能を提供します。 - 作成した zip ファイルのハッシュ値を使用することで、変更検知を行うことができます。つまり、ソースコードが変更された場合にデプロイを行う処理を実現可能です。よくある利用例としては AWS Lambda のデプロイです。
- 特に 2024年7月にリリースされた archive プロバイダーの v2.5.0 で
excludes
パラメーターにディレクトリーやパターンの指定が可能になったことで、とても柔軟に利用できる機能になりました。 - ちなみに、
archive_file
のハッシュの不変性についてはドキュメント内で言及されていないため、実装依存の動作と考えて注意したほうが良いでしょう。
archive_file による変更検知
利用例: Lambda のソースコードの管理
わかりやすい例として、 aws_lambda_function で AWS Lambda 関数を作成する例 を引用します:
data "archive_file" "lambda" {
type = "zip"
source_file = "lambda.js"
output_path = "lambda_function_payload.zip"
}
resource "aws_lambda_function" "test_lambda" {
# If the file is not in the current working directory you will need to include a
# path.module in the filename.
filename = "lambda_function_payload.zip"
function_name = "lambda_function_name"
role = aws_iam_role.iam_for_lambda.arn
handler = "index.test"
source_code_hash = data.archive_file.lambda.output_base64sha256
runtime = "nodejs18.x"
environment {
variables = {
foo = "bar"
}
}
}
このテンプレートでは以下の操作を行っています:
-
lambda.js
を含む zip ファイルlambda_function_payload.zip
を作成する。 - その zip ファイルから Lambda 関数を作成する。
- zip ファイルで Lambda 関数を作る詳細は Lambda 関数の .zip ファイルアーカイブとしてのデプロイ - AWS Lambda に詳しいですが、ここでは単純に「ソースコードの zip ファイルでなんかデプロイができるサービスである」くらいのざっくりした認識で十分です。
-
source_code_hash
で、作成した zip ファイルのハッシュ値 (SHA256) を設定する。
特に重要なのが最後の点で、この指定により、ソースコードが変更されるとハッシュ値が変わり、それによって変更と判断した Terraform が Lambda 関数を再デプロイして、最新のソースコードが反映される仕組みが実現されます。
また逆に、ソースコードに変更がない限りはこのハッシュ値が変わらないため、 Terraform はリソースに変更がないと判断してデプロイ処理を行いません。
なぜ archive_file のハッシュをコードの同一性に使用できるか?
非常に雑に説明すると、 archive_files
の以下の動作によります:
- ファイルの登録順序の一貫性
- ファイルの登録に golang の filepath.Walk を使用しており (コード)、辞書順 (lexical order) にファイルがリストアップされる仕様のため。
- ファイルの日時の固定
- 2049年1月1日 0時0分0秒 に固定されます。
- ファイルのモードの固定
-
ただしこの動作は
output_file_mode
を指定する必要があります 。後述。
-
ただしこの動作は
例えば以下のリポジトリーで make terraform-plan
で作成した source.zip
に対して zipinfo コマンドを使用することで、この動作を確認することができます:
$ zipinfo source.zip
Archive: source.zip
Zip file size: 7011 bytes, number of entries: 4
-rw-r--r-- 2.0 unx 347 bl defN 49-Jan-01 00:00 Dockerfile
-rw-r--r-- 2.0 unx 94 bl defN 49-Jan-01 00:00 factorial/__init__.py
-rw-r--r-- 2.0 unx 18244 bl defN 49-Jan-01 00:00 poetry.lock
-rw-r--r-- 2.0 unx 408 bl defN 49-Jan-01 00:00 pyproject.toml
4 files, 19093 bytes uncompressed, 6509 bytes compressed: 65.9%
$
-rw-r--r--
とファイルモードが固定されていること、 49-Jan-01 00:00
と時刻が固定されていることを確認できます。
これにより、実際のファイルのモードや時刻が変更されても常に同じアーカイブが作成され、ハッシュが同一になることを期待できます。
逆に、一般的な手法で zip ファイルを作成しても上記のような手当が行われていないため、単純に「zip を作成してハッシュ値を取る」では実施タイミングや環境で差分が生まれてしまい変更検知に利用することができません (「付録」で検証)。
output_file_mode によるファイルのモードの固定
output_file_mode により、ファイルのモードを固定することができます。
ファイルのモードはパーミッションとも言われるやつで、「書き込み権限」「読み込み権限」「実行権限」の設定です。
output_file_mode
を指定しない場合、以下の事情から archive_file
の出力するアーカイブのハッシュの一意性を保証できなくなります:
- Linux や Mac の場合、実際のファイルのパーミッションが設定される。このため、 OS の設定 (umask) や、どのようにファイルを作成したかなどで異なるハッシュのアーカイブが作られる場合がある。
- Windows の場合、ファイルのモードがそのまま存在しないので、 Linux や Mac と異なる値が設定される。
- 多分、 Windows ネイティブで terraform を実行した場合と、 Docker Desktop でバインドマウントして terraform を実行した場合も異なる動作になるので、これもまたややこしい話になる。
上記の問題が archive_file produces different results on different OSs #34 で指摘されて導入されたのが output_file_mode
です。
以下のように設定することで、上記の問題を回避することができます:
data "archive_file" "lambda" {
type = "zip"
source_file = "lambda.js"
output_path = "lambda_function_payload.zip"
# ファイルのモードを固定してハッシュが変化することを避ける
output_file_mode = "0644"
}
なお archive_file
で作成される zip には、ファイルのみが含まれディレクトリーが含まれないため、ファイル向けのモードだけ指定すればよいです。
注意事項: ハッシュの不変性は仕様として明記されていない
「アーカイブ対象のファイルとその内容が同一であれば、ハッシュ値が変わらない」という旨はどうやら archive
プロバイダーの仕様としては明記されていません:
- https://registry.terraform.io/providers/hashicorp/archive/2.7.0/docs/data-sources/file
- https://github.com/hashicorp/terraform-provider-archive/blob/v2.7.0/README.md
このため、「archive_file
のハッシュ値による変更検知」は、あくまで実装に依存した手法であると考えたほうが良いでしょう。
具体的には、以下について注意する必要があります:
- arvhive プロバイダーのバージョンを変更した場合に、ファイルに変更がなくてもハッシュ値が変わる可能性があります。
- このため、 ロックファイル を運用するか、プロバイダーのバージョンに範囲指定を使わない(
~> X.XX
などを使用しない)ようにするとよいでしょう。
- このため、 ロックファイル を運用するか、プロバイダーのバージョンに範囲指定を使わない(
- 実行する環境、特に OS を変更した場合に、ファイルに変更がなくてもハッシュ値が変わる可能性があります。
- 特に CI/CD で使用する OS 環境が複数ある場合や、開発チームのメンバーが使用する OS が異なる場合に、
archive_file
由来の想定外の動作があった場合は疑ってみるとよいでしょう。
- 特に CI/CD で使用する OS 環境が複数ある場合や、開発チームのメンバーが使用する OS が異なる場合に、
実装例: コンテナーイメージの変更検知
実装の概要
archive_file を使用して、 コンテナーイメージの作成に使用するソースコードの変更検知を行います。
※正確には、変更検知に使えるハッシュ値を作成するところまでを実装する。
アウトラインは以下の通り:
- ソースコードリポジトリー内のファイルを
archive_file
でアーカイブしてハッシュ値を計算する。 - ただし、
.dockerignore
に含まれるファイルを除外対象に指定する。-
.dockerignore
がちゃんと書かれていることが前提。 -
.dockerignore
では、以下のようなファイルを指定しておくことで、変更がないのにソースコードの変更と判断されてイメージキャッシュが使用されなくなったり、不要なファイルが docker デーモンに送られることで処理が遅くなることを回避できます:-
.git
などの管理用のファイル -
__pycache__
などのキャッシュファイル - その他、アプリケーションの実行には関係ないファイル (テストコード、
README.md
など)
-
-
この実装を行ったコードが以下になります:
「階乗を計算する」というとてもエキサイティングなライブラリーを作成する Python プロジェクトになっており、そこで作成するコンテナーの変更検知を行います。
※ライブラリーだけコンテナーにしても意味がないので、実際に出来上がるコンテナーは特に使い道はない。
.dockerignore の中身
.dockerignore
により、 .git
や terraform
tests
など、アプリケーションの実行に関係ないディレクトリーやファイルを除外します。
また、このプロジェクトにはテストコードやコードチェックの機能も含んでいます。
テストコードやコードチェックを実行すると、以下のように処理に必要ないキャッシュ(グレーアウトされているディレクトリー)が作成されるので、これを除外するように .dockerignore
を設定します:
設定内容:
excludes
指定を作成する
.dockerignore から archive_zip の .dockerignore
の中身に対して以下の操作を行い、 excludes の指定を作成します:
-
.dockerignore
のファイルの中身を改行区切りでリストに変換 - コメント行、空行を除外
-
Dockerfile
または/Dockerfile
の行を除外- Dockerfile に変更があった場合は、コンテナーイメージのソースに変更があったのと同様に扱いたいため。
-
/
から始まる行の場合は、/
を除外して使用 -
/
から始まらない行は、先頭に**/
を追加して使用
特に最後の2つの処理については、 「配下の任意のディレクトリー内で適用するパターン」の指定方法が以下のように逆転した関係になっていることに注意が必要です:
-
.dockerignore
では、特別な指定を使用しない場合はその動作になり、直下のディレクトリーだけにしたい場合に/
を先頭につける。 -
archive_files
のexcludes
では、その動作をさせるためには**
の指定が必要で、特別な指定をしない場合は直下のディレクトリーだけが対象になる。
以下のような実装になります:
この実装によって作成される値は以下のようになります:
+ check_excludes = [
+ "**/.mypy_cache",
+ "**/.pytest_cache",
+ "**/__pycache__",
+ ".git",
+ "terraform",
+ "tests",
+ ".dockerignore",
+ ".gitignore",
+ "docker-compose.yaml",
+ "Makefile",
+ "README.md",
+ "*.zip",
]
適用と実際に作成されるファイル
作成した dockerignore_excludes
を archive_files
に指定します:
実際に作成されるファイルはこんな内容になります。コンテナーイメージの作成に必要なファイルだけになっていることを確認できます:
$ zipinfo source.zip
Archive: source.zip
Zip file size: 7011 bytes, number of entries: 4
-rw-r--r-- 2.0 unx 347 bl defN 49-Jan-01 00:00 Dockerfile
-rw-r--r-- 2.0 unx 94 bl defN 49-Jan-01 00:00 factorial/__init__.py
-rw-r--r-- 2.0 unx 18244 bl defN 49-Jan-01 00:00 poetry.lock
-rw-r--r-- 2.0 unx 408 bl defN 49-Jan-01 00:00 pyproject.toml
4 files, 19093 bytes uncompressed, 6509 bytes compressed: 65.9%
$
付録
archive での時刻の固定
このコードで固定しています:
golang で time.Time
のゼロ値は 0001年1月1日 です。
ただし zip ファイルでは年を 1980 年からの相対値で設定するため、 -1979 を設定しようとしてオーバーフローの結果 69 が設定され、 2049 年になります。
手動で zip を作ってハッシュを固定できるかの検証
実行する OS や使用する zip のバージョンにも依存するので、参考程度にごらんください。
Windows11 上の cygwin で検証を行いました:
zip -v の出力
$ zip -v
Copyright (c) 1990-2008 Info-ZIP - Type 'zip "-L"' for software license.
This is Zip 3.0 (July 5th 2008), by Info-ZIP.
Currently maintained by E. Gordon. Please send bug reports to
the authors using the web page at www.info-zip.org; see README for details.
Latest sources and executables are at ftp://ftp.info-zip.org/pub/infozip,
as of above date; see http://www.info-zip.org/ for other sites.
Compiled with gcc 11.3.0 for Unix (Cygwin) on Jun 16 2022.
Zip special compilation options:
USE_EF_UT_TIME (store Universal Time)
BZIP2_SUPPORT (bzip2 library version 1.0.8, 13-Jul-2019)
bzip2 code and library copyright (c) Julian R Seward
(See the bzip2 license for terms of use)
SYMLINK_SUPPORT (symbolic links supported)
LARGE_FILE_SUPPORT (can read and write large files on file system)
ZIP64_SUPPORT (use Zip64 to store large files in archives)
UNICODE_SUPPORT (store and read UTF-8 Unicode paths)
STORE_UNIX_UIDs_GIDs (store UID/GID sizes/values using new extra field)
UIDGID_NOT_16BIT (old Unix 16-bit UID/GID extra field not used)
[encryption, version 2.91 of 05 Jan 2007] (modified for Zip 3)
Encryption notice:
The encryption code of this program is not copyrighted and is
put in the public domain. It was originally written in Europe
and, to the best of our knowledge, can be freely distributed
in both source and object forms from any country, including
the USA under License Exception TSU of the U.S. Export
Administration Regulations (section 740.13(e)) of 6 June 2002.
Zip environment options:
ZIP: [none]
ZIPOPT: [none]
$
同一のファイルでアーカイブを作成すればハッシュ値が同じになることの確認
$ zip -X source1.zip Dockerfile factorial/__init__.py poetry.lock pyproject.toml
adding: Dockerfile (deflated 30%)
adding: factorial/__init__.py (deflated 30%)
adding: poetry.lock (deflated 66%)
adding: pyproject.toml (deflated 38%)
$ zip -X source2.zip Dockerfile factorial/__init__.py poetry.lock pyproject.toml
adding: Dockerfile (deflated 30%)
adding: factorial/__init__.py (deflated 30%)
adding: poetry.lock (deflated 66%)
adding: pyproject.toml (deflated 38%)
$ md5sum source1.zip source2.zip
f939f0734ca5439dbc4ca939c42d02a4 *source1.zip
f939f0734ca5439dbc4ca939c42d02a4 *source2.zip
$
ちなみに -X オプションを指定しないとハッシュ値は同一になりませんでした。 Extra Timestamp Header が設定されて、ファイルのアクセス時刻が入るためです:
$ zip source1.zip Dockerfile factorial/__init__.py poetry.lock pyproject.toml
adding: Dockerfile (deflated 30%)
adding: factorial/__init__.py (deflated 30%)
adding: poetry.lock (deflated 66%)
adding: pyproject.toml (deflated 38%)
$ zip source2.zip Dockerfile factorial/__init__.py poetry.lock pyproject.toml
adding: Dockerfile (deflated 30%)
adding: factorial/__init__.py (deflated 30%)
adding: poetry.lock (deflated 66%)
adding: pyproject.toml (deflated 38%)
$ md5sum source1.zip source2.zip
b396dd060adfce83ec1018a3ad1ab68d *source1.zip
e8db07684de5bc5e01afb2099b7e92f0 *source2.zip
$
ファイルの順序でハッシュ値が変わることの確認
$ zip -X source1.zip Dockerfile factorial/__init__.py poetry.lock pyproject.toml
adding: Dockerfile (deflated 30%)
adding: factorial/__init__.py (deflated 30%)
adding: poetry.lock (deflated 66%)
adding: pyproject.toml (deflated 38%)
$ zip -X source2.zip Dockerfile factorial/__init__.py pyproject.toml poetry.lock
adding: Dockerfile (deflated 30%)
adding: factorial/__init__.py (deflated 30%)
adding: pyproject.toml (deflated 38%)
adding: poetry.lock (deflated 66%)
$ md5sum source1.zip source2.zip
f939f0734ca5439dbc4ca939c42d02a4 *source1.zip
6e9400e239ae9306599d425e1a9a1207 *source2.zip
$
ファイルのタイムスタンプでハッシュ値が変わることの確認
$ zip -X source1.zip Dockerfile factorial/__init__.py poetry.lock pyproject.toml
updating: Dockerfile (deflated 30%)
updating: factorial/__init__.py (deflated 30%)
updating: poetry.lock (deflated 66%)
updating: pyproject.toml (deflated 38%)
$ touch Dockerfile
$ zip -X source2.zip Dockerfile factorial/__init__.py poetry.lock pyproject.toml
updating: Dockerfile (deflated 30%)
updating: factorial/__init__.py (deflated 30%)
updating: pyproject.toml (deflated 38%)
updating: poetry.lock (deflated 66%)
$ md5sum source1.zip source2.zip
f939f0734ca5439dbc4ca939c42d02a4 *source1.zip
6898103858d8b1cf405ae7b836443dd7 *source2.zip
$
Discussion