【Terraform】archive_fileで意図しない差分が出る原因と対策

【Terraform】archive_fileで意図しない差分が出る原因と対策

みなさん、こんにちは!

今回は筆者が実務で発生したTerraformのarchive_fileの問題を取り上げ、実務の問題を深堀ながらarchive_fileの動作原理について詳しく解説していきたいと思います。

archive_file のソースコードは以下で公開されているので、ご参考までに共有しておきます。

https://github.com/hashicorp/terraform-provider-archive/tree/main

本記事では、

  1. archive_file を使っていたら、何も変更していないのに差分が出るようになってしまった!
  2. archive_file の動作原理を理解したい!

という方には参考になる内容になっているかと思います。

それでは、さっそく見ていきましょう

問題の共有

筆者が実務でTerraformを使ってGCPのCloud Functionsのソースをzip化してGCSバケットにアップロードしていたところ、ある日突然なぜかCloud Functionsのソースコードを変更していないにもかかわらず、terraform planで差分が出るようになっていました。

例としては、以下のようなイメージです。

# data.archive_file.source_zip will be read during apply
# (config refers to values not yet known)
<= data "archive_file" "source_zip" {
      id                  = (known after apply)
      output_base64sha256 = (known after apply)
      output_path         = "files/application.zip"
      # (その他の属性は既知)
    }

# google_storage_bucket_object.archive will be updated in-place
  ~ resource "google_storage_bucket_object" "archive" {
        bucket         = "my-app-bucket"
        id             = "my-app-bucket:application.zip"
        name           = "application.zip"
      ~ detect_md5hash = "vW7P8xQ2jA/N9uYm..." -> (known after apply)
        # archive_fileのハッシュが確定しないため、アップロードが必要か判断できず
        # 実行(Apply)時に再計算され、差分があれば上書きされる状態
    }

Plan: 0 to add, 1 to change, 0 to destroy.

こちらがplan実行のたびに出てしまうという状況でした。 本来は、Cloud Functionsのソースに変更があった場合のみ、差分として出る想定でした。

解決策

いきなり解決策ですが、筆者の場合はarchive_fileのハッシュ値を使わずに、localsでCloud Functionsソースのハッシュ値を直接計算するようにしたという方法を取って解決しました。

理由としては、archive_fileで計算されるハッシュ値がCloud Functionsソースの差分以外(タイムスタンプ、パーミッションなどのメタデータとか)も含めて計算されている可能性があるからです。 それであれば、archive_fileとは別でハッシュ値を直接計算してしまった方が考えることが少なく確実だと考えました。

実際にこちらに変更したところ、Cloud Functionsを編集した場合しか差分が出なくなりました。

仮説検証

ここからはなぜarchive_fileのハッシュ値ではソースコードを変更していないにも関わらず差分が出てしまっていたのかについて調査してみたいと思います。

今回の問題の原因として考えられるのはざっと以下くらいだと思います。

  1. ハッシュ値にタイムスタンプも含まれていることによる差分
  2. ハッシュ値にパーミッションも含まれていることによる差分
  3. ハッシュ化する際のファイル順序による差分
  4. ハッシュ値に隠しファイルが含まれていることによる差分

それぞれ見ていきます。

仮説1. タイムスタンプによる差分

ハッシュ値を計算する際に、ファイルのタイムスタンプも見てハッシュ化しているのではないかという説ですね。

実際のソースコードは以下のようになっていました。

// internal/provider/zip_archiver.go

fh.SetModTime(time.Time{})

time.Time{} というのが、Go言語の時刻におけるゼロ値ですね。つまり、0001年1月1日 です。 実際のところ時刻がいつなのかはどうでもよく、重要なのは時刻が固定されているというところです。 この時点で、タイムスタンプによって差分が発生していたという説はなくなります。

仮説2. パーミッションによる差分

次にハッシュ値を計算する際に、ファイルのパーミッションも見てハッシュ化しているのではないかという説ですね。

実務では筆者はこの説は考えていなかったため、ここは深く調べていませんでした。 そのため、この調査は後日談となります。

実際の元ソースコードでは以下のようになっています。

// internal/provider/zip_archiver.go

fh, err := zip.FileInfoHeader(fi)

...

if a.outputFileMode != "" {
	filemode, err := strconv.ParseUint(a.outputFileMode, 0, 32)
	if err != nil {
		return fmt.Errorf("error parsing output_file_mode value: %s", a.outputFileMode)
	}
	fh.SetMode(os.FileMode(filemode))
}

実は、archive_fileではoutput_file_modeでパーミッションを指定することができます。

output_file_mode = "0644"

そのため、指定がある場合は指定のパーミッションで固定し、なければローカルOS上のパーミッションがそのまま保持される挙動になっていそうでした。

筆者の実務環境ではoutput_file_modeが設定されていなかったため、パーミッションの説は有力です。

仮説3. ファイル順序による差分

次にハッシュ化する際のファイル順序によって差分が発生している可能性を調べます。

該当の元ソースコードは以下のようになっています。

// internal/provider/zip_archiver.go

err = filepath.Walk(indirname, a.createWalkFunc("", indirname, opts, &isArchiveEmpty, true))

zip化されるディレクトリを探索する際に、Go言語のfilepath.Walkを使っていました。 これは、指定したディレクトリ配下のファイルを辞書順でリストアップする関数です。

よって、指定ディレクトリ配下のファイル名更新や追加を行わない限り、ファイル順序は常に一定となり差分が発生することはないです。これでファイル順序の説もなくなりました。

仮説4. ハッシュ値に隠しファイルが含まれていることによる差分

最後に隠しファイルを含めてハッシュ値を計算していたことで、開発者によって.DS_Storeなどが含まれており差分が発生していた可能性を探りたいと思います。

実はこちらは実務でも可能性はあるかなと思い、調査していたところでした。

元ソースコードは以下のようになっています。

// internal/provider/zip_archiver.go

func checkMatch(fileName string, excludes []string) (value bool, err error) {
	for _, exclude := range excludes {
		if exclude == "" {
			continue
		}

		match, err := doublestar.PathMatch(exclude, fileName)
		if err != nil {
			return false, err
		}

		if match {
			return true, nil
		}
	}
	return false, nil
}

除外ファイルについてはexcludesを指定することで、明示的にファイルを除外することができます。 そのため、ハッシュ化に含めたくない隠しファイルなどを指定しておけば除外することができます。

excludes    = [".DS_Store"]

元のソースコードでは、このオプションが付いている場合のみ除外処理が実行されるようになっているようです。

実際に実務でもこちらを試していましたが、差分はなくなりませんでした。 このことから、今回筆者がぶつかった差分問題は少なくとも隠しファイルによるものではなかったと思います。

まとめ

まとめると、archive_fileによる意図しない差分が発生した場合は、archive_fileによって計算されるハッシュ値を使うのではなく、直接該当のソースコードをハッシュ化する関数を作ることを考えてみようとなります。

また、なぜ今回archive_fileのハッシュ化により意図しない差分が出ていたかについては、おそらくoutput_file_modeによるパーミッションの固定化を行っていなかったことが原因ではないかと考えています。

Go言語に慣れている方でarchive_fileの詳細な挙動を確認したい方は、直接ソースコードを読んでみるとおもしろいかもしれません。