Windows での Terraform Provider TLS 干渉と filesystem mirror 回避策

前記事で bootstrap が終わり、いよいよ Terraform を回そうとした矢先、terraform init「tls: bad record MAC」 という謎のエラーで落ちる現象に遭遇しました。本記事では、その原因究明と回避策を時系列で書きます。

遭遇した症状

terraform/envs/prod ディレクトリで terraform init を実行:

$ terraform init

Initializing the backend...
Successfully configured the backend "s3"!

Initializing modules...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.100.0...
╷
│ Error: Failed to install provider
│
│ Error while installing hashicorp/aws v5.100.0: releases.hashicorp.com:
│ local error: tls: bad record MAC
╵

backend (S3) との通信は成功しているのに、provider 本体(AWS SDK の zip)のダウンロードで TLS エラー。「local error」というのが気になりました。

切り分け①: 同じ URL を curl で叩いてみる

curl -sL -o /tmp/aws.zip \
  https://releases.hashicorp.com/terraform-provider-aws/5.100.0/terraform-provider-aws_5.100.0_windows_amd64.zip \
  -w "HTTP: %{http_code} / Size: %{size_download}\n"

# HTTP: 200 / Size: 10122586

curl だと一見ダウンロードできています。ただし、サイズが 10 MB。本物は HEAD で確認すると 156 MB。明らかに途中で切れている。

curl -sIL "https://releases.hashicorp.com/.../aws_5.100.0_windows_amd64.zip" | grep -i content-length
# Content-Length: 156704800   ← 約 149 MiB が本物

つまり ダウンロード途中で TLS 接続が切れている。Terraform 内部の Go TLS ライブラリは厳密に検証するので「bad record MAC」と判定して落とす。curl は緩いので途中まででも保存してしまう(だから一見「成功」に見える)。

切り分け②: SHA を比較

curl -sL https://registry.terraform.io/v1/providers/hashicorp/aws/5.100.0/download/windows/amd64 \
  | grep -o '"shasum":"[^"]*'
# shasum: 9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93

sha256sum /tmp/aws.zip
# d9b152779b29960fa08754e4a1f1335dab815b48b4d6d75f53aa6ebf6bb54b1b

SHA が完全に違います。ダウンロードされたファイルは公式の zip ではない(または途中で切れた不完全なもの)。

切り分け③: PowerShell の Invoke-WebRequest で再現

Invoke-WebRequest -Uri $URL -OutFile $Out -UseBasicParsing
# The decryption operation failed, see inner exception.
# Got 39816105 bytes (約 38 MiB)

PowerShell でも同じエラー。サイズは違う(10MB → 38MB)が 毎回違う場所で切れる。これで「ネットワーク経路の問題」と確信。

原因仮説: 中継機器の TLS 干渉

調査の結果、原因は以下のいずれかと考えました:

共通するのは、大型バイナリ(100MB 超)の TLS セッションが途中で切られる という挙動。小さい HTTPS リクエストは通るが、大きいダウンロードは中断される。

切り分け④: TLS 1.2 強制で再試行

TLS 1.3 のバグや中継機器の TLS 1.3 干渉を疑って、curl で TLS 1.2 強制 + retry を入れて再試行:

curl --tlsv1.2 --tls-max 1.2 --http1.1 --ipv4 \
     --retry 5 --retry-delay 3 --connect-timeout 30 \
     -L -o aws_tls12.zip "$URL"

結果: 129 MB まで進んで失敗(前回より遥かに長く持った)。約 82% まで来てから切れた。

突破口: resume ダウンロード

「途中まで取れている → 残りだけ取り直せばいい」と気付いて -C -(resume)オプションで再試行:

curl --tlsv1.2 --tls-max 1.2 --http1.1 -C - -L -o aws_tls12.zip "$URL"

# 残り 25 MB をダウンロード
# 25.52M  100% で完走 ✅
sha256sum aws_tls12.zip
# 9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93 ← SHA 一致

これで完全な provider zip が手に入りました。

Terraform に provider を渡す: filesystem mirror

取得した zip を Terraform に「これ使って」と教える方法は filesystem mirror です。手順は 2 つ:

1. zip を所定の場所に配置

$MirrorRoot = 'C:\Users\<ユーザー>\.terraform.d\providers'
$ProviderDir = "$MirrorRoot\registry.terraform.io\hashicorp\aws"

# ディレクトリ作成
New-Item -ItemType Directory -Force -Path $ProviderDir | Out-Null

# zip を移動 (Terraform が期待するファイル名)
Move-Item -Path 'aws_tls12.zip' `
  -Destination "$ProviderDir\terraform-provider-aws_5.100.0_windows_amd64.zip"

2. Terraform の CLI 設定 (~/AppData/Roaming/terraform.rc)

provider_installation {
  filesystem_mirror {
    path    = "C:/Users/<ユーザー>/.terraform.d/providers"
    include = ["registry.terraform.io/hashicorp/aws"]
  }
  direct {
    exclude = ["registry.terraform.io/hashicorp/aws"]
  }
}

これで「AWS provider はローカルのミラーから読み、それ以外は通常通りオンラインから」という挙動になります。

terraform.rc の BOM 落とし穴

このファイル、PowerShell 5.1 の Set-Content -Encoding utf8 で書くと BOM 付き UTF-8 になり、Terraform は illegal char でパースエラーを出します:

Error parsing C:\Users\...\terraform.rc: At 1:1: illegal char

BOM 無し UTF-8 で書き直し:

# PowerShell で BOM 無し UTF-8 を書く
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($rcPath, $rcContent, $utf8NoBom)

# 確認 (先頭バイトが 0xEF 0xBB 0xBF だったら BOM 付き)
$bytes = [System.IO.File]::ReadAllBytes($rcPath)
$bytes[0..2] | ForEach-Object { '{0:X2}' -f $_ }

terraform init 再実行

BOM 修正後に再度 init:

$ terraform init -reconfigure

Initializing provider plugins...
- Installed hashicorp/aws v5.100.0 (unauthenticated)
Terraform has successfully been initialized!

(unauthenticated) は「公式署名検証はスキップしたが、ファイル自体の SHA256 は一致している」状態。サイズも SHA も先に確認済なので問題なし。

archive provider も同じ問題に

後で backend_api モジュールに archive_file data source を追加した時、同じ TLS エラーが hashicorp/archive でも発生しました。同じ手順で zip を取得してミラーに配置することで解決。

結局、ミラーに配置した provider は 2 個:

archive は 6 MB と小さいですが、家庭内ネットワークの相性で同じ症状になることがあるようです。

学び

このハマりで得た教訓:

  1. 「local error: tls: bad record MAC」は中継機器の TLS 干渉のサイン
  2. 大型バイナリのダウンロード失敗は size + SHA で切り分ける
  3. resume (-C -) は強い味方。途中まで取れてれば残りを取り直せる
  4. Terraform は filesystem mirror で「ローカルから provider を読む」運用が可能。CI ではこれを使って毎回ダウンロードしないという最適化もできる
  5. PowerShell 5.1 の -Encoding utf8 は BOM 付き。設定ファイルでハマりがち

恒久対策

本来は中継機器の HTTPS 干渉を解消するのがベストですが、家庭内ネットワークだと特定が難しい。filesystem mirror で運用回避しています。

VPN を使うと、ローカルの干渉が無いネットワーク経路を通せるので解消することもあります。本シリーズでは VPN 無しで filesystem mirror に倒しました。

次の記事

provider が読めるようになったら、いよいよ Terraform でリソースを書いていきます。次の記事では モジュール設計の考え方(modules / envs / providers alias)について書きます。「どこを共通化して、どこを環境ごとに分けるか」の判断基準が見えてきます。

📚 用語集

provider (Terraform プロバイダ)
Terraform で AWS や GCP など各クラウドを操作するための拡張プラグイン。hashicorp/aws など、数百 MB 級の zip でダウンロードされる。
terraform init
Terraform の初期化コマンド。provider のダウンロード・backend 接続・モジュール解決を行う。
TLS (Transport Layer Security)
HTTPS の中で使われる暗号化通信プロトコル。1.2 と 1.3 が現役。MAC(Message Authentication Code)で改ざん検知している。
bad record MAC
TLS の改ざん検知 (MAC) が失敗したときのエラー。途中で内容が書き換わったり、データが切れたときに発生。
SHA256 / SHA-2
ハッシュ関数の一種。256 ビットの固定長ダイジェストを生成。ファイルが改ざんされていないかの検証に使う。
HEAD リクエスト
HTTP メソッドの 1 つ。本文は受信せず、ヘッダ(Content-Length など)だけ取得する。ファイルサイズの事前確認に便利。
curl
コマンドラインの HTTP / HTTPS クライアント。Linux / macOS 標準、Windows 10 以降にも同梱。
--tlsv1.2 / --tls-max 1.2
curl のオプションで、TLS バージョンを 1.2 のみに制限する。1.3 のバグや中継機器の TLS 1.3 干渉を回避する切り分けに使う。
--retry, --retry-delay, --retry-max-time
curl のリトライ系オプション。失敗したリクエストを自動再試行する。一時的なネットワーク不安定の対処。
-C - (resume)
curl の resume オプション。途中まで保存されたファイルがあれば、続きから取得する。Range リクエストを利用。
filesystem mirror
Terraform の CLI 設定で「provider をローカルディレクトリから読む」よう指定する仕組み。~/AppData/Roaming/terraform.rc(Windows)/ ~/.terraformrc(macOS/Linux)に書く。
terraform.rc
Terraform 全体の CLI 設定ファイル。provider_installation ブロックで filesystem mirror を定義。
BOM (Byte Order Mark)
UTF-8 ファイルの先頭に付く 3 バイト(EF BB BF)。HCL / YAML / JSON のパーサが嫌うので、設定ファイルでは外す。
HCL (HashiCorp Configuration Language)
Terraform 等で使われる設定言語。.tf.terraformrc がこれで書かれる。
DPI (Deep Packet Inspection)
ネットワーク機器がパケット内容を検査する技術。セキュリティ目的だが、TLS 1.3 等で誤動作することがある。
HTTPS スキャン (HTTPS inspection)
セキュリティソフトや企業ファイアウォールが、HTTPS 通信を一旦復号して中身をチェックする機能。Terraform 等の厳密な TLS 検証と相性が悪いことがある。