🤖

9つのGoogleCloudプロジェクトをTerraform化する過程で学んだことを公開します part③

2024/11/23に公開

はじめに

こちらの記事は part② の続き になります。

この記事でわかること

  • データソースを活用してより柔軟で動的なコードを記述する方法
  • プラグインのキャッシュを利用してディレクトリサイズを減らす方法
  • リスクの少ないリファクタリング方法
  • ステートファイルをロックしてしまった場合の対応方法

学び ⑧ データソースを活用する

Terraform のデータソースとは 外部サービスやシステム、既存のインフラリソースの情報を参照する機能になります。
もっとざっくりまとめると外部から情報を取得するための機能です。
terraform で管理している既存のインフラリソース、terraform で管理していないリソースや外部サービス API などなどリソース定義で必要な値を外部から参照して取得することで柔軟で動的にリソースの定義を実現できます。

data ブロックを定義して既存のインフラリソースの値を参照する

データソースを使う場面の一つにシークレット値の取り扱いがあります。
「既存のインフラリソースからシークレットな値を取得し、リソースを定義する時に使う」といった場面です。

たとえば「請求アカウント ID を使って GoogleCloud プロジェクトを作成する」想定で確認してみます。
今回の移行作業では SecretManager は Terraform で管理していません。そのため事前に請求アカウント ID の値を登録しておく必要があります。
またデータソースで取得する SecretManager があるプロジェクトを「A プロジェクト」とします。今回はそれを元に新しく「B プロジェクト」を作成します。

次のように data ブロック を定義することで既存の「A プロジェクト」の SecretManager で指定したシークレットから請求アカウント ID を取得し、それを後続の resource ブロック で変数として渡すことができます。

data "google_secret_manager_secret_version" "billing_account" {
  project  = "xxxxxxxxxxxx"
  secret   = "billing_account"
}

resource "google_project" "project_b" {
  name = "project_b"
  project_id = "project_b"
  billing_account = data.google_secret_manager_secret_version.billing_account.secret_data
}

dataブロック の書き方に関してはドキュメントに書かれているのでそちらを参考にすると良いでしょう。
SecretManager の場合は このように記載されています

このように既存のプロジェクトはもちろん、別のプロジェクトからもデータソースを介してリソースの情報を取得して、リソースの構築で使うことができます。
注意点としてはデータソースを使って別プロジェクトのリソース情報を参照する場合は(プロバイダが API 経由でデータを取得するため)当然その別プロジェクトの対象リソースに対して参照する権限を terraform を実行するユーザーに対して付与しておく必要があります。

通常のリソースとの違いとしては dataブロック はあくまでも読み取り専用のリソースを定義するものになります。一方 resourceブロック の定義は Terraform で管理するためのリソースを定義しています。
なのでデータソースはリソースやデータのライフサイクルを管理するものではないことに注意してください。

terraform remote state で別 State の情報を取得する(ローカルバックエンド)

Terraform のデータソースには terraform remote state データソース というのも存在します。
上記のデータソースがプロバイダの API を使ってリソースの情報を取得しているのに対し、別のフォルダやリモートバックエンドで管理しているステートファイルの output を参照する機能です。データソース同様に取得した情報をリソースの定義で使うことができます。

まずはローカルバックエンドの場合を確認していきます。
別のディレクトリで管理している「A プロジェクト」から「B プロジェクト」のステートファイルの output を参照するケースを考えてみます。

.
├── project_a
│   └── main.tf
└── project_b
    ├── main.tf
    └── terraform.tfstate

project_b では Google プロジェクト名を output で出力している

// project_b/main.tf
resource "google_project" "testdesu" {
  provider        = google
  name            = "test-xxxxxx"
  project_id      = "test-xxxxxx"
}

output "project_name" {
  value = google_project.testdesu.name
}

// 出力結果: test-xxxxxx

project_a では project_b のステートファイルの output から Google プロジェクト名を参照する

// project_b/main.tf
data "terraform_remote_state" "testdesu" {
  backend = "local"

  // 参照する値が公開されているステートファイル
  config = {
    path = "../project_a/terraform.tfstate"
  }
}

output "name" {
  value = data.terraform_remote_state.testdesu.outputs.project_name
}

// 出力結果: test-xxxxxx

このようにローカルの場合はステートファイルのパスを指定して参照できます。
project_a から project_b のステートファイルの参照はただ単にローカルのファイルを参照しているだけなのでプロバイダのインストールは不要です。

terraform remote state 別のステートの情報を取得する(リモートバックエンド)

リモートバックエンドで管理されているステートファイルの output を参照できます。
たとえば次のようにすることで、別の GoogleCloud プロジェクトで公開しているステートファイルの値を参照できます。

data "terraform_remote_state" "test" {
  backend = "gcs"

  config = {
    bucket = "state-xxxxxxxxxxxxxx"
    prefix = "state-xxxxxxxxxxxxxx"
  }
}

output "name" {
  value = data.terraform_remote_state.test.outputs.google_p
}

どちらを使うのが良いのか??

ここまででデータソースと terraform remote state データソースについて紹介しましたが、どちらを使った方が良いのでしょうか。
結論データソースの方が良いと思いました。
理由を主に 2 点です。

  1. データソースは terraform を実行するたびに API コールをするため常にリソースの最新の状態を取得できる
    1. terraform remote state はステートファイルの output を参照するため、output を忘れないようにする追加しておく必要がある。またステートファイルを毎回最新の状態であることを保証しておく必要がある
  2. クラウドのストレージとデータソースを組み合わせて使うことでアクセス制御かつプレーンテキストではない状態で参照できる
    1. ステートファイルの参照はファイル全体に対してアクセスをするため、別の output も参照できてしまう。またステートファイルは機密情報が含まれている場合もあるのでプレーンテキストで出力された場合にシークレット値が公開されてしまう

ステートファイルはプレーンテキストで出力されてしまう

Changes to Outputs:
-  name = "xxxxxxxx" // プレーンテキストで出力される
+  name = (sensitive value)

external データソース

あまり使用頻度は高くないと思いますが Terraform には external データソース というのも存在します。
外部プログラムを実行してデータを取得する、データソースの振る舞いをする機能です。

データソースではリソース毎に data ブロックを定義することでリソースの情報を参照しました。
external はリソースではなく、シェルスクリプトなどを実行して外部サービスなどの情報を参照し terraform のコード内で使えるようにします。
terraform コード内で使いたいデータが既存のデータソースでは取得できない場合に一つの代案になります。

ちなみに今回の移行作業では gcloud コマンドを使って情報を取得しコード内で使用するということをやりました。

実装方法については ドキュメント をご確認ください。

データソース(data ブロック)をどこに書くべきか

データソースの data ブロックはどこに書くべきか、についてです。
今回の移行作業時にはこちらの 【terraform】Data Source を使うなら Module ではなく呼び出し元で を参考にさせていただきました。
記事にもある通り、モジュール内でデータソースを定義するとモジュールへの依存が強くなってしまうため、基本的には呼び出し元のルートモジュールで data ブロックを定義するのが良さそうです。

学び ⑨ インストールするプロバイダをキャッシュしてディレクトリサイズを減らす

今回のように複数のプロジェクトを扱う場合は個別でプロバイダのインストールをする必要があります。
プロジェクトが多くなるほど各ディレクトリでプロバイダをインストールする必要があるので、そこそこディレクトリサイズが多くなってしまいます。
試しに $ du -sh で検証してみると約 250MB でした。今回 9 つのディレクトリが存在するためうまくキャッシュを使ってサイズを抑えてみます。
ここでは Provider Plugin Cache というのを利用してプロバイダのキャッシュできるようにしてみます。
コンフィグファイル を使った方法で確認してみます。

キャッシュ用ディレクトリ作成

キャッシュ用のディレクトリは terraform で作ってくれないので自分で作る必要があります。
$ mkdir -p $HOME/.terraform.d/plugin-cache

設定ファイル作成

ユーザーのホームディレクトリ配下に .terraformrc ファイルを作ります。
これは全ての terraform の作業ディレクトリで適用されます。
ファイルの配置場所も 決まっています

$ touch $HOME/.terraformrc

そして作成したファイルにキャッシュするディレクトリのパスを追加することでキャッシュが有効になります。

plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"

プロバイダインストール

$ terraform init を実行してプロバイダのインストールをします。

プロバイダをインストールしたディレクトリの .terraform ディレクトリを確認してみるとキャッシュ用のディレクトリへのシンボリックリンクになっています。

.terraform
├── providers
│   └── registry.terraform.io
│       └── hashicorp
│           ├── external
│           │   └── 2.3.4
│           │       └── darwin_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/external/2.3.4/darwin_amd64
│           ├── google
│           │   └── 6.8.0
│           │       └── darwin_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/google/6.8.0/darwin_amd64
│           └── google-beta
│               └── 6.8.0
│                   └── darwin_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/google-beta/6.8.0/darwin_amd64
└── terraform.tfstate

先ほど作ったキャッシュ用のディレクトリ配下を確認するとプロバイダの実体がインストールされているのがわかります。

plugin-cache
└── registry.terraform.io
    └── hashicorp
        ├── external
        │   └── 2.3.4
        │       └── darwin_amd64
        │           ├── LICENSE.txt
        │           └── terraform-provider-external_v2.3.4_x5
        ├── google
        │   ├── 6.8.0
        │   │   └── darwin_amd64
        │   │       ├── LICENSE.txt
        │   │       └── terraform-provider-google_v6.8.0_x5
        │   └── 6.9.0
        │       └── darwin_amd64
        │           ├── LICENSE.txt
        │           └── terraform-provider-google_v6.9.0_x5
        └── google-beta
            └── 6.8.0
                └── darwin_amd64
                    ├── LICENSE.txt
                    └── terraform-provider-google-beta_v6.8.0_x5

またディレクトリサイズを検証すると次のようになりました。
これでプロバイダのキャッシュを活用してディレクトリサイズを減らすことが確認できました。

4.0K    .terraform

また次のように違うバージョンでもキャッシュを活用できます。

.terraform
├── providers
│   └── registry.terraform.io
│       └── hashicorp
│           ├── external
│           │   └── 2.3.4
│           │       └── darwin_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/external/2.3.4/darwin_amd64
│           ├── google
│           │   └── 6.9.0
│           │       └── darwin_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/google/6.9.0/darwin_amd64
│           └── google-beta
│               └── 6.8.0
│                   └── darwin_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/google-beta/6.8.0/darwin_amd64
└── terraform.tfstate

特に今回のように terraform の作業ディレクトリが多い場合などに特に効果を発揮しそうかなと思います。

学び ⑩ リスクが少ないリファクタリング方法

Terraform でリソースを追加や変更はしないけどローカル名を変えたい場合などの軽微なリファクタリング方法について、安全に実施する方法を考えてみます。

まず前提として 詳解 Terraform 第 3 版 ―Infrastructure as Code を実現する にも書かれていましたが、基本的に一度定義した後はリファクタリングをしない方が良いと思います。
これは何か一つの変更がインフラの破壊に繋がりかねないためです。
そのため「とりあえず定義しておいて後からリファクタリングしよう」のような考え方は持たない方が良いと思います(後々大変コストがかかる作業になりそう)。

例として何も考えずにローカル名を変更した場合どうなるか確認してみます。
次のように GoogleCloud プロジェクトのローカル名を変更してみます。

+ resource "google_project" "testdesu" {
- resource "google_project" "test" {
  provider        = google-beta.no_user_project_override
  name            = "test-xxxxxx"
  project_id      = "test-xxxxxx"
  billing_account = "xxxxxxxxxx"
}

$ terraform plan の結果は次のようになります。
ローカル名の変更だけなのですが、既存のリソースを削除して新規作成するという極めてリスクが高い判定をしています。
これは Terraform がリソースタイプとローカル名で個体識別しているのが理由です(google_project.test / google_project.testdesu)。違う個体として認識されると、変更ではなく削除と新規作成の判定がされます(ドキュメント にも記載あり)。

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

何も考えずにリファクタリングするのは極めてリスクが高いことを確認できました。
次に安全にリファクタリングを実施する方法について確認していきます。

Terraform では moved ブロック というリファクタリングを補助してくれる機能が提供されています。これを使ったリファクタリング方法について確認していきます。

先ほどの修正を moved ブロックを使って次のように定義してみましょう。

+ resource "google_project" "testdesu" {
- resource "google_project" "test" {
  provider        = google-beta.no_user_project_override
  name            = "test-xxxxxx"
  project_id      = "test-xxxxxx"
  billing_account = "xxxxxxxxxx"
}

+ moved {
+  from = google_project.test
+  to   = google_project.testdesu
+}

$ terraform plan を実行すると、次のような判定になります。
先ほどとは違い余計な差分を出さずにローカル名だけの変更ができそうですね。

# google_project.test has moved to google_project.testdesu
  resource "google_project" "testdesu" {
      id                  = "xxxxxxxxxxxx"
      name                = "xxxxxxxxxxxx"
      # (8 unchanged attributes hidden)
  }

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

$ terraform apply を実行してみると次のように成功するはずです!
ステートファイルの方も確認すると変更されていることが確認できると思います。

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

ここまでで moved ブロックを使ったリファクタリング方法について確認しました。
moved ブロックを使うことで「安全」にリファクタリングを実施できることがわかりました。

  • 意図しない差分を出さずにリファクタリングできる
  • plan -> apply の流れで実行できる(通常の流れで実行できる)
    • PR 等で他のメンバーの目を通してから実行できる
    • plan で影響範囲を確認できる

注意したいポイント
moved ブロックはリファクタリング後も残しておくことが 公式ドキュメント で推奨されています。チームの状況によるとは思いますが基本的には残す方向で良いのかなと思います。
また変更履歴がわかるように対象リソースの近くで moved ブロックを定義することをお勧めします。

今回はローカル名を変更する場合のリファクタリング方法について確認しました。
他のケースでのリファクリングについては 公式ドキュメント で解説されているので是非確認してみてください。

学び 11 state ファイルをロックしてしまった場合の対応方法

Terraform では 最初の記事 で書いたようにリモートバックエンドでステートファイルを管理することで(サポートされていれば)ロック機能を使うことができます。
この機能を使うことで $ terraform apply 実行時にロックを取得してステートファイルが同時に更新されるといった事故を防いでくれます。

ただ apply の実行中にキャンセルをするとロックがかかったままの状態になり、以降 terraform を実行しようとすると Error: Error acquiring the state lock というエラーが発生します。
そのためステートファイルをロックしてしまった場合はロックを解除する必要があります。

ロックを解錠する方法はローカルバックエンドとリモートバックエンドで違います。

リモートバックエンドの場合

ロックが発生しているディレクトリで $ terraform force-unlock $LOCK_ID を実行することでロックを解錠できます。このコマンドはインフラの変更はせずにステートファイルのロックを解除するものです。
$LOCK_ID$ terraform plan を実行したときに出てくるエラーで確認できます。

ロックがかかっている状態で $ terraform plan を実行する
エラーで表示されている ID が $LOCK_ID になる

Error message: writing "gs://xxxxxxxxxxxxxxxxxxx" failed: googleapi: Error 412: At least
one of the pre-conditions you specified did not hold., conditionNotMet
Lock Info:
  ID:        1xxxxxxxxxxxx
  Path:      gs://xxxxxxxxxxxxxxxxxxxxxxxx
  Operation: OperationTypeApply
  Who:       xxxxxxxxxxxxxxxx
  Version:   1.x.x
  Created:   2024-xx-xx xx:xx:xx.xxxxxxx +0000 UTC
  Info:

$ terraform force-unlock $LOCK_ID を実行すると本当に解除するか確認されるので yes を入力しましょう。
Terraform state has been successfully unlocked! というメッセージが出て、以降は terraform が実行できるようになると思います。

ローカルバックエンドの場合

リモートバックエンドのように terraform コマンドでロックの解除はできません。
Terraform で State Lock エラーが発生したら に書かれているようにローカルの Terraform のプロセスを kill すればエラー解消できるようです(未検証)。

参考記事

The terraform_remote_state Data Source
Terraform: why data sources and filters are preferable over remote state
Terraform の Provider Plugin Cache を試す
moved ブロックを使ってリファクタリングしてみた
Terraform で State Lock エラーが発生したら
【terraform】Data Source を使うなら Module ではなく呼び出し元で
Terraform の Provider Plugin Cache を試す

GitHubで編集を提案

Discussion