冒頭まとめ
Terraform で 422 Unprocessable Entity を受け取ったとき、まず押さえるべきことがあります。このエラーは Terraform が作ったものではありません。Terraform 本体には、要求の内容を検証して 422 を返す仕組みがありません。目にする 422 は、必ずどこかから中継されたものです。
出どころは2つに分かれます。プロバイダが相手先の窓口を叩いた結果として返ってきたものと、HCP Terraform や Terraform Enterprise の窓口を直接叩いた結果として返ってきたものです。前者の場合、応答の中身は相手先の流儀に従います。GitHub の窓口なら GitHub の書式、別のサービスならそのサービスの書式です。したがって、読み方は相手先の規則で決まります。Terraform の文書をいくら読んでも、応答の中身の意味は書かれていません。
もう1つ、共通する性質があります。Terraform の各層は、422 を再試行しません。レジストリ向けの通信で使われている仕組みは、再試行の対象を429と、501を除く500番台に限っています。AWS 向けの実装が再試行の対象として定義しているのも 500・502・503・504 です。どこにも 422 は含まれていません。これは怠慢ではなく設計です。要求を直さない限り結果が変わらないエラーなので、送り直す意味がありません。
エラーの概要
プロバイダ経由の場合、相手先の応答がそのまま、あるいは整形されて表示されます。GitHub を相手にした場合の例です。
Error: PATCH https://api.github.com/repos/example/sample: 422 Validation Failed
[{Resource:Repository Field:default_branch Code:invalid
Message:Cannot update default branch for an empty repository.}]
同じ相手先でも、詳細が空のことがあります。この場合、手がかりは短い文言だけです。
{
"message": "Validation Failed",
"errors": [],
"documentation_url": "https://docs.github.com/rest/reference/repos#update-a-repository"
}
別のサービスを相手にした場合は、書式が変わります。
Bad response statusCode [422]. Status [422 Unprocessable Entity].
Body: [baseType=error, code=InvalidBodyContent, message=must have at least one node pool]
HCP Terraform の窓口を直接叩いた場合は、また別の形になります。整形の規約に沿った構造で、status と title と detail が並びます。
同じ 422 でも書式が3通り出てくるのは、作った相手が違うからです。この点が、Terraform における 422 の扱いを難しくしています。
まず最初に:誰が作った応答かを確定する
第一に、エラー文に含まれる宛先を見ます。プロバイダ経由なら、その相手先の窓口の名前が出ています。ここが分かれば、応答の読み方も決まります。
第二に、その相手先の 422 の規約を確認します。項目ごとの誤りを配列で返す流儀なのか、短い文言だけを返す流儀なのかで、得られる情報量が違います。GitHub のように配列と区分名を返す相手であれば、そこから原因がほぼ確定します(GitHub API の 422 の記事)。
第三に、操作の種類を見ます。作成では通るのに更新で落ちる、という形は、後述する変換の食い違いを示します。
第四に、再試行は考えません。前述のとおり、どの層も 422 を再試行しません。同じ内容を送り直しても結果は変わりません。
よくある原因と解決手順
原因1:相手先が値を変換していて、往復で食い違う
作成のときは通るのに、その後の更新で落ちる、という形です。相手先が受け取った値を内部で書き換えて保存し、Terraform 側は元の値を持ち続けるために起きます。
GitHub の窓口を相手にした場合の実例が報告されています。名前に空白や記号を含む資源を作ると、相手先は空白を別の文字に置き換えて保存します。しかし Terraform 側は元の値を保持しているため、次の更新でその元の値を送り、相手先が受け付けずに 422 になります。
Before(相手先が書き換える値をそのまま指定する):
resource "example_repository" "this" {
name = "Repo Test"
}
After(相手先が保存する形の値を指定する):
resource "example_repository" "this" {
name = "Repo-Test"
}
この形かどうかは、実際に保存されている値を確認すれば分かります。
terraform state show example_repository.this
設定に書いた値と、状態に入っている値、そして相手先が持っている値の3つを並べてください。相手先だけが違っていれば、この形です。
原因2:対象の状態が操作を受け付けない
値そのものは正しいのに、対象が今その操作を受け付けられる状態にない場合です。先の例に出てきた「空の資源に対しては既定の枝を変更できない」という文言が、これに当たります。
対処は、順序を変えることです。必要な内容が入ってから設定する、という依存関係を明示します。
resource "example_default_branch" "this" {
repository = example_repository.this.name
branch = example_branch.this.name
depends_on = [example_content.initial]
}
この形は、初回の作成だけで失敗し、2回目以降は通る、という現れ方をします。毎回同じ場所で止まるのではなく、まっさらな環境でだけ失敗する場合は、順序を疑ってください。
原因3:上限や数量の制約に当たっている
書式にも状態にも問題がなく、単に数が多すぎる場合です。この場合の文言は検証の失敗として返りますが、内容は数の話です。
見分け方は、文言に個数や上限を示す語が含まれているかどうかです。値を直しても解決しないので、既存のものを減らすか、割り当てを増やす必要があります。
原因4:プロバイダが文言の一致で判定している
見落としやすい形です。プロバイダによっては、特定の 422 を「実害のないエラー」として握り潰す作りになっていることがあります。その判定を、相手先が返す文言の文字列一致で行っている場合、相手先が文言を変更した時点で判定が外れます。
実際に、GitHub の窓口が返す文言が変わったために、プロバイダ側の判定が一致しなくなり、これまで通っていた操作が失敗するようになった、という報告があります。
この形の特徴は、自分の設定を何も変えていないのに突然失敗し始めることです。設定を疑って時間を使う前に、次を確認してください。
# プロバイダの版と、直近で更新されていないかを確認する
terraform version
grep -A3 "provider \"" .terraform.lock.hcl
相手先の変更が原因であれば、対処はプロバイダ側の更新を待つか、該当の操作を一時的に避けることになります。自分では直せません。
原因5:HCP Terraform の窓口を直接叩いている
構成管理の実行そのものを窓口から操作している場合です。この場合の 422 は、送った内容の検証に落ちたことを示します。
公式の窓口記事には、実行を作成する要求が 422 で失敗する例が挙げられています。対処として案内されているのは、参照している構成の版の状態を確認することです。つまり、送った内容の書式ではなく、参照先の状態が整っていないことが原因、という構造になっています。
別の記事では、組織の設定に予約された名前と同じ名前の要素を手作業で作ってしまったために、認証の処理が 422 で止まる例が挙げられています。こちらも、送った内容ではなく既存の状態が原因です。
窓口を直接叩いて 422 を受け取ったときは、送った内容だけでなく、参照している対象の状態と、既存の名前の重複を確認してください。
補足:似ているが別のもの
本文が読めない場合は 400 です(Terraform の 400 の記事)。422 は本文が読めたうえで内容が通らなかった場合なので、段階が違います。
対象が見つからない場合は 404、権限が足りない場合は 403 です(Terraform の 404 の記事、403 の記事)。既に存在する、あるいは競合している場合は 409 です(Terraform の 409 の記事)。
要求の頻度が上限を超えた場合は 429 で、こちらは待てば通ります(Terraform の 429 の記事)。422 は待っても通りません。この違いは、再試行してよいかどうかに直結します。
相手先が GCP の窓口である場合、検証の失敗は 422 ではなく 400 になります(GCP の 422 の記事)。相手先によって使う状態コードが違うので、プロバイダごとに前提が変わる点に注意してください。
切り分けの順序
- エラー文から宛先を読み、誰が作った応答かを確定する。
- その相手先の 422 の規約を確認する。項目ごとの誤りが返るなら、そこから原因がほぼ決まる。
- 再試行は考えない。どの層も 422 を再試行しない。
- 作成では通り更新で落ちるなら、設定・状態・相手先の3つの値を並べて食い違いを探す。
- まっさらな環境でだけ失敗するなら、順序と依存関係を疑う。
- 設定を変えていないのに突然失敗し始めたなら、プロバイダの文言一致の判定が外れた可能性を疑う。
- 窓口を直接叩いている場合は、送った内容ではなく参照先の状態と名前の重複を確認する。
確認コマンド集
# 1. 詳細なログを採り、422 の応答本文をそのまま見る
TF_LOG=DEBUG TF_LOG_PATH=./tf-debug.log terraform apply
grep -n -A15 "422" tf-debug.log | head -40
# 2. 状態に入っている値と、設定に書いた値を突き合わせる
terraform state show <資源のアドレス>
# 3. プロバイダの版を確認する(突然失敗し始めた場合)
terraform version
cat .terraform.lock.hcl
# 4. 相手先の窓口を直接叩いて、同じ 422 が返るかを確かめる
curl -sS -i -X PATCH -H "Authorization: Bearer <トークン>" \
-d '{"<項目>": "<値>"}' https://<相手先の窓口>/<パス>
# 5. 対象の現在の状態を相手先で確認する
curl -sS -H "Authorization: Bearer <トークン>" \
https://<相手先の窓口>/<パス> | python3 -m json.tool
Editor’s Note
422 がどこから来たのかを見誤りやすいことを示す記録として、HashiCorp の窓口記事があります(Failed to Create Webhook On Repository 422 Error When Creating a New VCS-Driven Workspace in Terraform Cloud/Enterprise)。
この記事に載っている応答が示唆的です。外側の状態は 400 で、その説明文の中に、別の系から返ってきた 422 がそのまま埋め込まれています。つまり、利用者が受け取る状態コードと、実際に問題が起きた場所の状態コードが一致していません。
さらに、その 422 の原因が変わっています。記事によれば、これは連携先の窓口が定めている、1つの資源あたりの通知設定の個数の上限に当たったものです。対処として案内されているのは、設定画面を開いて既存の通知設定の数を確認し、不要なものを削除することです。送った内容の書式には何の問題もありません。
この1件には、本記事で述べた要素が揃っています。422 は中継されてきたものであること、作った相手が別にいること、そして内容が「値の誤り」ではなく「数量の制約」だったことです。検証に落ちたという名前から、つい項目の書式を疑ってしまいますが、実際には送った内容を1文字も変えずに解決する場合があります。
422 に当たったら、まず宛先を読む。次に、その相手の規約で読む。この2段を踏むだけで、無駄な書き換えをかなり減らせます。
免責事項:本記事の内容は、執筆時点の公開情報をもとに作成したものです。ソフトウェアの仕様は予告なく変更されることがあります。最新の情報は各ツールの公式サポートページをご確認ください。本記事の情報を利用した結果生じたいかなる損害についても、著者および運営者は責任を負いかねます。
この記事でエラーは解決しましたか?