目の前に僕らの道がある

勉強会とか、技術的にはまったことのメモ

Terraformのtfstateについて考える

この記事は3-shake Advent Calendar 2023の23日目の記事となります。

こちらはSRE Tech Talk #6で話した内容に補足したものです。

資料はこちらとなります。

tfstateとは

Terraformが管理しているリソースの状態を表すjson形式のファイルです。

tfstateとterraformファイルと実際のリソースの状態を比較して、terraformコマンドが実行されます。

一般的には直接変更せずterraform stateコマンドを通して変更を行い、一般ユーザがtfstateに触れることはないです。

tfstateの課題

tfstateについて以下の課題があります。それぞれについて見ていきます。

  • tfstateの管理場所
  • tfstateを管理するリソースの管理
  • tfstateの分割

tfstateの管理場所をどうするか問題

主な保存場所候補としては以下のものがあります。

  • local(デフォルト)
  • クラウドのオブジェクトストレージ
  • Gitレポジトリ統合
    • GitLab
  • SaaS利用
    • Terraform Cloud

local

Terraformのデフォルト保存先です。Terraformを実行する同じディレクトリのterraform.tfstateに保存されます。

1人もしくは変更頻度が著しく低い状況など特殊なとき使えるものとなります。git管理して複数人で使うこともできるが、コンフリクトが発生しうるので、チーム開発には向かないです。

基本的には複数人でterraformを使用するときは非推奨です。

S3/Google Cloud Storage

監理するクラウドのオブジェクトストレージに保存する方法です。これが標準的(当社比)なのかなと思っています。

オブジェクトストレージなので、権限があればどこからでもアクセスすることができます。それゆえ、同時にTerraformが実行されるので排他ロックの処理が必要となります。S3バックエンドを使用した場合はDynamoDBを使用してstate lockを実現します。

Google Cloud Storageは単体でstate lockをサポートしています。

tfstateの参照権限をクラウドのIAMで制御する必要があります。

GitLab

GitLabでtfstateを監理することもできます。tfstateを管理するリソースを管理する必要がないことがメリットとなります。(後述します)

開発にGitLabを使っている場合、親和性が高い方法となります。

Terraform Cloud

GitLabと同様tfstateを管理するリソースを管理する必要がないというところにメリットがあります。

月間500 Managed Rsourcesまで無料で使えます。

web上からリソース差分の確認できたり、applyが可能です。SaaSクラウドのリソース情報を預けることに抵抗がない場合は選択肢としては有望です。

なおTerraformのStateのドキュメントではこういう記述があり、Terraform Cloudを推奨しているようです。

This state is stored by default in a local file named "terraform.tfstate", but we recommend storing it in Terraform Cloud to version, encrypt, and securely share it with your team.

昔はAWSと連携するためにIAM Userのアクセスキーを使わないといけなかったが、OIDC認証もできるようになったので、よりやりやすくなったかと思います。

tfstateを管理するリソースをどう管理する問題

GitLabやTerraform Cloudを使う場合には起きない問題となります。 S3のようなクラウドのオブジェクトストレージを使用する場合は、このS3バケットをどう作るかということが問題となります。コマンドで作る場合、コマンドの管理、terraformで作る場合はそのtfstateはどこに保存するか、そういったことに頭を悩ませます。そこについて考えていきます。

以下の方法が考えられます。

  • aws/gcloudコマンド
  • terraform + local state管理
  • CloudFormation

aws/gcloud コマンド

そもそも作成コマンドしか打たないのであれば、スクリプトをレポジトリに含めておけば良いという考え方はあります。 基本的に一度作れば変えることはないので、これで十分という風に割り切ることはできます。

ただし、tfstateのバケットだけでなく、CI/CD用のIAM RoleやOIDC認証リソースなども初期リソースとして含めて管理したいというユースケースだと、スクリプト管理では力不足になりうります。

terraform + local state 管理

オブジェクトストレージをterraformで作る方法です。ただし、tfstateに関してはlocalに保存し、これをgitも管理します。 かたくなにterraformを使いたい人に向けな方法となります。

デメリットとしては、tfstateもgit管理するのでコミット忘れがあります。また、頻度低いですがterraform自体はローカルで実行せざるを得ないので変更衝突が起きうることです。

CloudFormation / Google Deployment Manager

クラウドごとにコードを変えないといけない。IaCツールを2種類使うというそこはかとない気持ち悪さはあるというデメリットはありますが、gitでインフラ状態管理しなくてすむというメリットがあります。気持ち悪さだけを克服できるなら無難な選択肢だとは思います。

tfstateをどう分割するか問題

第一に考えるのが環境の分離。この分離の仕方だけ他とは系統が違うので独立して説明します。

一部差分があるだけで、以下のような形でほぼ同じ構成の環境を作ることはよくあります。

  • 開発環境
  • ステージング環境
  • 本番環境

これらについてどう分割するのかを考えていきます。

環境分離パターン

大きく2つのパターンを利用することが多いです。それぞれ見ていきます。

ディレクトリ分離パターン

これは環境ごとにディレクトリを分割して、環境ディレクトリを実行単位とします。環境の切り替えはディレクトリ移動することで行います。 環境ごとの差分が大きいときに使うことが多いです。デメリットとしては環境ごとにリソース定義をそれぞれ書くので記述量が多くなるというのがあります。そのため、可能な限りモジュール化して、なるべくパラメータだけの差分にするようにします。

ディレクトリ構成例としては以下の通りです。

.
├── envs
│   ├── dev
│   │   ├── locals.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── prd
│   │   ├── locals.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── stg
│       ├── locals.tf
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
└── modules
    ├── vpc
    │   ├── locals.tf
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── application
    │   ├── locals.tf
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf

backend-configパターン

backend-configオプションとvars-fileオプションを組み合わせて、環境を切り替えるパターンです。

${ENVDIR}/terraform.tfvars に環境ごとの差分パラメータを定義して、${ENVDIR}/backend.tfvars に環境ごとのtfstate保存先を定義します。

terraform initbackend.tfvars を切り替えることで環境の切り替えを行います。

環境ごとに差分が少ないときに向いています。差分は terraform.tfvars に記述されているパラメータだけなので、記述量が少なくて済みます。 ただし差分が多くなるとcount, for_eachで分岐やループを作ることになり読みにくくなるというものがあります。

ディレクトリ構成例としては以下のようになります。

.
├── envs
│   ├── dev
│   │   ├── backend.tfvars
│   │   └── terraform.tfvars
│   ├── prd
│   │   ├── backend.tfvars
│   │   └── terraform.tfvars
│   └── stg
│       ├── backend.tfvars
│       └── terraform.tfvars
├── locals.tf
├── main.tf
├── modules
│   └── vpc
│       ├── locals.tf
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── outputs.tf
├── provider.tf
└── variables.tf

設定ではbackendをs3と指定しておき中身はオプションで指定するようにします。

terraform {
  backend "s3" {}
}

以下のようにterraform initするたびに適用する環境を切り替えることができる。

terraform init --backend-config=${ENVDIR}/backend.tfvars --reconfigure
terraform apply --var-file=${ENVDIR}/terraform.tfvars

workspace

workspaceは同じような環境を複製するときに使ういます。シングルテナント環境を量産する場合や開発環境を複数作る場合などに使います。環境を切り替える用途には作られてないとドキュメントまでは記載されています。

In particular, organizations commonly want to create a strong separation between multiple deployments of the same infrastructure serving different development stages or different internal teams. In this case, the backend for each deployment often has different credentials and access controls. CLI workspaces within a working directory use the same backend, so they are not a suitable isolation mechanism for this scenario.

自分自身がworkspaceを実運用で使ったことがないので多くは語れないです。別でちゃんと使ってから書きたいと思います。

環境分離以外の分割をどうするか問題

小さいサービスでは環境を分離するだけでだいたいは問題ないことがおおいですが、terraformを運用していると運用面、管理面でいろいろ課題が出てくると思います。

管理するリソースが増えるとplan/applyの時間が増えたり、リソースの見通しが悪くなったりしてきます。

特に実行時間が意外に馬鹿にできなかったりします。下手するとplanに数分かかるようになったりします。

そのため、ある程度大きくなったらtrstateを分割して、リソースの管理範囲を分割する必要が出てきます。

これをどうやって分割するかが自分の中で答えが出ていない出てないし、分脈によって解決策は異なるとは思います。

ここで、解決策を考えるうえで、分割するための観点を見ていきましょう。

分割する観点

分割する観点は以下のようなものがあるかと思います。

  • プロバイダー
  • 管理権限
  • 変更頻度

プロバイダーで分割

プロバイダー単位で分割するパターンです。

例としてはAWSとDatadogのようにプロバイダーで分割します。プロバイダー間で依存がない場合は分けやすいかと思います。

また、プロバイダー間で管理主体が違うことも多いので素直な分け方だとは思います。

しかしながら、アプリケーションリソースとアプリケーションの監視を近いところにおいたほうが見通しがよいのではという観点もあるので運用体制にあわせて考えるとよいでしょう。

管理権限で分割

チームの権限で分割するパターンです。ただし、より堅くするなら、ディレクトリではなくレポジトリ自体も分割して、コードの参照権限も分割する方が望ましい場合もあります。

    • ネットワーク ⇒ インフラチーム
    • アプリケーション ⇒ 開発チーム

変更頻度で分割

変更をあまりしないリソースを変更が頻繁なリソースと一緒のplan/applyするのは無駄なので変更の頻度でtfstateを分割するパターンもあります。

    • 変更が少ない ⇒ DB/ネットワーク
    • 変更が多い ⇒ EC2/ECS

依存の方向性で分割

少し観点を変えてみます。実際に分割をした場合に問題となるのはtfstate間のリソースの依存が課題になります。 tfstate間で相互に依存するようなコードを書くとtarget指定してそれぞれのstateのリソースを作成しなくてはなりません。 こうすると管理が煩雑となってしまうので、原則的に片方向だけの依存になるように分割するようにするのが望ましいです。

tfstate間のリソース参照

terraform_remote_state を使うことで、参照元のTerraformでoutputした内容を別のTerraformで利用することができます。

# 参照元 networkアカウント
output "vpc_id" {
  value = aws_vpc.main.id
}
# 参照先 applicationアカウント
# data.terraform_remote_state.network.vpc_id の形式でVPC IDを参照できる
data "terraform_remote_state" "network" {
  backend = "s3"

  config {
    bucket = "terraform-tfstate-network-xxxxx"
    key    = "tfstate"
    region = "ap-northeast-1"
  }
}

まとめ

正直tfstateをどう扱うかに正解はないです。サービス規模や性質によって選択は変わります。

本当に小さい規模であれば、tfstateを分割せず一つで十分でしょうし、チーム開発せず一人で扱うなら、通常であれば推奨されないtfstateのlocal git管理という手段がふさわしい場合もあります。 また、組織やサービスの成長や時間経過によっても最適な選択は変わると思います。大事なのは選んだ技術要素に関しては選定理由を説明できるようにはしておくということです。 選定理由及び不採用理由を明確にしておくことで、変更時に最適な選択の助けになるでしょう。