Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Terraform のエラーに落ち着いて立ち向かうために

本記事は、Gunosy Advent Calendar 2020 11 日目の記事です。

前回は Shohei Hida さんの「Argo RolloutsによるKubernetesでのCanary Deploy」でした

tech.gunosy.io

はじめに

はじめまして.20 卒で GTL 所属の山本です. 入社して半年以上が経ち,出社は輪読用の本を取りに行った 1 回ですが,先輩方に助けて頂きながら弊社での開発にも徐々に慣れてきました. 今回は入社後,個人的に最も振り回されたツールである Terraform について紹介していきたいと思います.

Terraform は今やインフラ管理をするデファクトと言えるツールであり,気づいたら導入されていたという人も多いのではないでしょうか. Terraform の場合 document が非常に充実していることや,既存実装も流用しやすいという特徴から,普段利用する場面の中では Terraform 自体への体系的な知識をつける場面は少なくなりがちです. その結果,魔法のツールを触っているような感覚を覚え,CI などでエラーを吐いたら(考えられうる影響範囲が広大であることからも)絶望してしまうという状況に陥ることもしばしばあります.

そこで今回は Terraform の仕組みについて深ぼっていき,いくつかエラーが生じた際のケーススタディを示すことで問題の切り分けるための考え方を示していきたいと思います.

(注)今回は具体的な resource の記述方法やプロジェクトの構造について紹介はしません.Terraform を少し使ったことがある方が読みやすい記事だと思います.

Terraform とは

Terraform は HashiCorp 社が提供する Infrastructure as Code を達成するためのツールです. www.terraform.io

公式にもそう言われているので,この定義はもちろんその通りなのですが,ツールそのものに目を向けるともう少し広義に解釈することができます. 実装に着目すると Terraform は CRUD を提供する API に基づき Terraform Language (.tf) とリソースの状態をマッチングし管理するツールだと理解できます. なのでインフラ(e.g. AWS, GCP)を触るためのものではなく,CRUD を提供する API がありコードによって管理したい状態があれば,それは Terraform によって管理されるものの範疇に入ってきます.*1

全体のアーキテクチャ

それでは Terraform 全体のアーキテクチャについて見ていきましょう. https://learn.hashicorp.com/img/terraform/providers/core-plugins-api.png

上記図は Perform CRUD operations with Providers | Terraform - HashiCorp Learn から引用した構成図です.

図を見ると以下のことからわかります

  • Terraform は大きく Terraform Core と Plugin に別れている
  • Terraform Core と Plugin は RPC で通信している *2
  • Target API とのやりとりは Plugin が Client Library を用いて責務を持っている

よって Terraform を利用していくうえで Terraform Core が担う機能と Plugin が担う機能を区別することは重要になってきます.*3

Terraform Core の機能

Terraform Core 主に以下の役割を担います

  • 各種 CLI を整備
  • サービスの状態を tfstate という JSON 形式のファイルで保存し管理
  • tfstate と .tf を比較しつつ,Provider に問い合わせることで plan 時の tfstate 更新や apply 時の差分を埋めるための API 実行

ざっくりとした言い方にはなりますが,サービス(Provider)に依らず Terraform を利用する上で必要な機能は Core にあります.

Provider の機能

Provider は Terraform Core が要求するインターフェースを実装し,gRPC のサーバーとして機能します. これによって Terraform Core の機能と Provider 独自の機能を疎結合にしており拡張性が非常に高くなっています.

Provider では主に以下の機能を実装します

  • Provider が対象とするサービスを利用するための CRUD クライアント
  • Provider が提供する resourcedata を示すスキーマ
  • サービスへアクセスするための認証

例えば terraform-provider-aws の provider.go を起点として中をみていくと上記の内容が実際に把握できます.*4 github.com また,Perform CRUD operations with Providers | Terraform - HashiCorp Learn を進めると手元で API サーバーを立ち上げながら実装して確認することもできます. このチュートリアル 1 時間ぐらいでさっと出来て仕様理解にはオススメです.叩かれる API 側のログも見られるのは理解の助けになります.

Case Studies

それでは最後の上記の構造を意識しつついくつか不具合発生時のケーススタディについて見ていきたいと思います.

plan が通ったのに apply がコケた

よくあるやつですね.plan はスキーマに基づく .tf の表面的なチェックになります. このため apply で実際に API を実行することで明らかになる .tf の問題は検知することができません. これは API サーバーを作成した際に request そのものへの validation で DB のユニーク制約への衝突を検知できないのと同等です. よって,この原因は Provider が提供する API をうまく叩けていないことにあり,エラーメッセージを読みつつ問題があった Provider への知識を利用して修正を試みる必要があります.

何もしていないのにインフラの構成が変わった

何らかの要因で tfstate とコードに差分が生まれ,それに気づかずに apply が叩かれたたことが原因として多いと思います. 構成の復元だけでいうと戻したいコードの状態で apply をすれば戻すことは可能ですが,残念ながら一度飛んでしまったデータなどは復元ができません. 許容しないデグレが生まれる可能性があるのであれば,Terraform の運用ルールを見直す必要があります. その際は Provider への知識よりも,Terraform そのものへの理解が重要になってきます.

意図せぬ tfstate の変更の原因としては以下のようなものが考えられます

  1. コンソール画面ポチポチや CLI を通じてサービスに対して手動で変更を加えた
  2. importstate mv などのコマンドを共用の tfstate に対して打った

1. によるデグレを防ぐには,手動の変更によって tfstate が変わることを理解することが重要です.*5 そうすると,基本的に手動による変更を加えることを許容しないルールや緊急対応を手動で行う場合は Terraform 利用者に通知するルールに繋がっていきます.

2. の対策としては tfstate と Terraform のコマンドについて理解することが求められます.2. で挙げているコマンドは tfstate を操作するものなので,準備段階において共用の tfstate を使う必要はありません. よって,共用の tfstate を手元に引き込んだ上でこれを使ってテストが可能です.以下の記事がとても参考になります. qiita.com

共用の tfstate を壊した

上記の記事を参考にやれば基本的に起きることはないと思いますが,それでも importstate mv を伴う変更をしていると意図せず共用の tfstate を壊してしまうこともあります.*6 これに関しても Terraform そのものや tfstate に対して理解しておくと適切に対処することができます.

ここで重要なのは importstate mv のような操作は tfstate に変更を加えるだけで,Provider に対して副作用のある操作の要求をしないということです. このため tfstate が壊れたからといってインフラの構成そのものが壊れたわけではないです.

よって,まず第一に利用者間でコミュニケーションを取って apply が行われない状況を作りましょう.これにより意図せずインフラ構成が変わることはなくなります. その上で backend が S3 や GCS などでバージョニングを有効にしていた場合は tfstate を正常稼働していた最新の状態に戻し,plan で差分がでないことを確認すれば大丈夫です.*7 不慮の自体に備えて tfstate に関しては必ずバージョニングを有効にして管理するようにしましょう.

余談

backend に S3 を設定し,DynamoDB で tfstate の lock をしている場合は,tfstate を手上げすると下記のようなエラーが出る恐れがあります. その場合はリンク先を参照しつつ対応するとよいです.

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Error refreshing state: state data in S3 does not have the expected content.
This may be caused by unusually long delays in S3 processing a previous state
update.  Please wait for a minute or two and try again. If this problem
persists, and neither S3 nor DynamoDB are experiencing an outage, you may need
to manually verify the remote state and update the Digest value stored in the
DynamoDB table to the following value: ${DigestValue}

pygillier.me

マジで何もしていないのにコマンドが通らなくなった

tfstate も正常で,誰も何も触っていないのにコマンドが通らなくなることがあります.*8 このような場合における原因の例としては,利用している Provider の更新が入り整合性が取れなくなった可能性,Provider の更新そのものに不具合が入っている可能性,Provider が対象としているサービスの API サーバーの不具合の可能性などが挙げられます. まずはエラーメッセージを読み込み原因の切り分けを地道に行いましょう. そして Provider に問題がありそうであれば,Providerレポジトリを見に行き直近で更新がないかを確認すると良いでしょう. もし更新があった場合はバージョンを戻すことも対応策としてあげられます. また,エラーメッセージがサービス側と関連がありそうだった場合は,利用しているサービスに問い合わせると原因がわかることもあります.

まとめ

Terraform は非常に強力かつ素晴らしいツールです. そのため,普段使う分にはその仕組みを意識せずとも利用していくことは可能です. しかしながら,エラーや不具合は必ず起きるものなので,過度なブラックボックス化は予期せぬ出来事が発生した際への対応が難しくなります. この記事を通じて誰かの Terraform への理解が少しでも進み,安心して利用できるようになれば幸いです.

明日は大曽根さんの記事になります!お楽しみに!

*1:Provider の自作さえすればあらゆるものが管理が可能です.実用性があるかどうかは別の話になりますが...

*2:中身はみんな大好き gRPC

*3:ただし本記事では Provisioner には触れません

*4:内部では aws-sdk-go と terraform の schema をつなぐことを目的とした大量のコードがあることがわかります

*5:正確には plan や apply 前に refresh が走って tfstate が書き換えられる

*6:手元の terraform のパッチバージョンが違って失敗したことがあります.

*7:やったことはないので不確実ですが,どうしてもバックアップがない場合は backend を local にして refresh コマンド から state を新しく生成して戻すことも可能な気はしています.

*8:筆者は「共用の tfstate を壊した」後,修正したらこれに出会って絶望しました