クラウドインフラの構成管理をするデファクトスタンダードとしての地位を確立しているTerraform。
この記事は、以下についてまとめたものである。
- Terraformの概要
- 動作イメージ
- 利用フロー
- Terraformの利用
- ローカル環境の構築方法
- Terraformのディレクトリ・ファイル構成
- .tfファイルの記述方法
- terraformコマンド(主要なもの)
- 公式ドキュメント(参照することが多そうなもの)
(TODOになっているところも今後更新していく予定です)
1. Terraformの概要
-
Terraform
は、HashiCorp社(Vagrant, Vault)によってGo言語で開発されたOSSで、Infratructure as Codeを実現するツール。 -
AWS,GCP,Azureの他、DatadogやHerokuなど様々なクラウドインフラの構成管理ができる。
- 類似ツールに
CloudFormation
があるが、こちらはAWSのインフラリソースのみが対象 - ちなみに、クラウドインフラ以外にローカルのDocker環境の構成管理もできる
- 類似ツールに
-
インフラ構成の定義は、.tfという拡張子のファイルに、各インフラのリソースの定義をコードで記述することによって行う。
- 具体的には、各インフラの初期化、更新、削除といった内容を記述する。
- .tfファイルに記述されたコードに基づいてTerraformが自動でインフラを構築してくれる。
- 利用者は、インフラリソースの構築・設定順を気にする必要はあまりなく、インフラリソース間の依存関係は基本的にTerraform側でいい感じに考慮して制御してくれる。
- 冪等性の担保もしてくれて、コードとして記述している内容が対象のインフラリソースですでに実現されている場合、その構築処理をスキップしてくれる。
- Terraformの主な構成要素は、
TERRAFORM CORE
とTERRAFORM PROVIDER
である。
1.1. 動作イメージ
[.tfファイル]
↓
↓
↓
---------------- Terraform ------------------
[TERRAFORM CORE]
↓
↓(gRPC)
↓
[TERRAFORM PROVIDER]
↓
↓(Golang)
↓
[各社クラウドインフラが提供するSDK(API)]
---------------------------------------------
↓
↓(HTTP(s))
↓
[各社クラウドインフラのリソース]
TERRAFORM PROVIDER
- HassiCorp社とTerraformコミュニティによってGolangで開発されているプラグインである。以下単にプロバイダと呼ぶ。
- Terraform Registyで公開されている
- .tfファイルの設定に基づき、各社クラウドから提供されているAPI, SDKを利用していい感じに各社クラウドインフラリソースを操作してくれるものである。
- TERRAFORM PROVIDERは、ローカルでgRPCサーバとして動作する。
1.2. 利用フロー
Terraformを利用して、インフラリソースの構築・設定反映をする工程は、以下の通り。
- ①インフラリソースの設定ファイル(.tf)を記述
-
②設定ファイルの検証
-
terraform validate
コマンドを実行する
-
-
③作業用ディレクトリの初期化
-
terraform init
コマンドを実行する
→設定ファイルのインフラリソース定義の反映に必要な分のプロバイダがDLされる
-
-
④インフラリソースへ設定を適用するための具体的な操作を確認
-
terraform plan
コマンドを実行する
→「クラウドリソースに対して、Terraformがこれからどんな操作をするのか?」の内容が表示されるので、問題ないかを確認する
-
-
⑤インフラリソースへ設定を適用
-
teffaform apply
コマンドを実行する
-
2. Terraformの利用
2.1. ローカル環境構築
公式ドキュメントを参考に、以下をインストール。
- Terraform(※)
- terraformコマンド補完機能
※terraformのバージョンを管理しながら使いたいならば、tfenv経由でインストールして利用する。
# tfenvをインストール
brew install tfenv
# インストール可能なterraformのバージョンを確認
tfenv list-remote
# terraformをバージョンを指定してインストール
tfenv install <バージョン>
# インストール済みのterraformのバージョンを一覧表示(*付きが現在使用しているバージョン)
tfenv list
# 使用するterraformのバージョンを切り替え
tfenv use <バージョン>
2.2. Terraformのディレクトリ・ファイル構成
設定ファイル(.tf
or .tf.json
)群を同じディレクトリにまとめたものを、モジュールという。
- モジュールは、基本的にディレクトリ直下の設定ファイル群のみで構成される。
- サブディレクトリは完全に別のモジュールとして扱われる
- モジュール呼び出しという仕組みを使わない限り、自動的に読み込まれたりすることはない。
- Terraformはモジュールのディレクトリ直下の設定ファイル内の記述すべてを評価し、それが1つの設定ファイルであるかのように動作する。
- ブロックを別ファイルにわけるのは、純粋に読む人・メンテナンスする人にとってわかりやすくするためだけで、動作には全く影響しない。
- Terraformは単一のルートモジュールを起点として動作する。
- Terraform CLIでは、Terraformを起動する作業用ディレクトリ = ルートモジュール。
- コマンドライン引数で、別ディレクトリをルートモジュールにすることもできるが、実際にはほぼやらないらしい。
- Terraform CLIでは、Terraformを起動する作業用ディレクトリ = ルートモジュール。
2.3. .tfファイルの記述方法
- 文字コード: UTF-8
- 改行コード: 慣習的にLF(CRLFでも可)
- コメント:
#
で一行コメントを記述
基本的な構造、構文と用語
# --- ブロック ----
<ブロックタイプ> "<ラベル>" "<ラベル>" {
# --- ボディ ---
...
# 引数
<引数名> = <値> or <式>
# --- ボディここまで ---
}
# --- ブロックここまで ---
- ブロック: 設定内容のコンテナであり、ブロックタイプ, 任意の数のラベル, ボディ(
{}
で囲まれる部分。引数、他のブロックを含む)から構成される。 - 引数: 引数名に「値」もしくは「式」を紐付ける形で記述する。
- 式: 他の値を参照したり、組み合わせたりするもの。
例えば、以下のような形になる。
resource "aws_vpc" "main" {
cidr_block = var.base_cidr_block
}
2.3.1. ブロックタイプ
terraform
ブロック
Terraform自体の基本的な設定をするブロック。
モジュールに必要なプロバイダ(required_providers
ブロック)や、tfstateの置き場(backend
ブロック)をボディに記述する。
例.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 1.0.4"
}
}
backend "gcs" {
bucket = "tf-state-prod"
prefix = "terraform/state"
}
}
required_providers
ブロック
モジュールで利用する全てのプロバイダを列挙するブロック。
required_providers {
<プロバイダのローカル名> = {
source = "<プロバイダのソースアドレス>",
version = "<プロバイダのバージョン>"
}
}
プロバイダのローカル名はprovider
ブロックで使われる。
backend
ブロック
ステートファイル(.tfstate)の保存方法(バックエンド)を設定するブロック。
backend "<バックエンドタイプ>" {
<バックエンドタイプ固有の設定>
}
ステートファイルは、その名の通りTerraformの管理対象であるインフラ構成の状態を保存するもの。
backend
ブロックで指定できるバックエンドタイプや、その固有の設定については、使用するTerraformバージョンのリファレンスを参照する必要がある。例えばgcs
バックエンドタイプでは、以下のような設定をする。
terraform {
backend "gcs" {
bucket = "tfstate" # 保存先のGCSバケット名
prefix = "terraform/production" # <保存先のバケット名以下のディレクトリ名>/<tfstateファイル名>
}
}
valiable
TODO
provider
ブロック
プロバイダを使って、各種インフラリソースの詳細設定をするにあたっての、前提条件を設定するブロック。
provider <プロバイダのローカル名> {
<プロバイダ固有の設定>
}
ボディにはプロバイダ固有の設定を記述する。
例えばgoogle
プロバイダだとproject, region
という固有の設定がある。
provider "google" {
# resourceブロックで設定するインフラリソースが所属するGoogle Cloudプロジェクトのデフォルト値
project = "acme-app"
# resourceブロックで設定するインフラリソースが所属するGoogle Cloudリージョンのデフォルト値
region = "us-central1"
}
ボディで設定できる項目は、各プロバイダのリファレンスを参照する必要がある。
resource
ブロック
仮想ネットワークなどのインフラリソースの設定をするブロック。
resource "<リソースタイプ>" "<リソースのローカル名>" { # リソースタイプとローカル名の組み合わせはモジュール内で一意
<リソースタイプ固有の設定>
}
対象のリソース(1番目のラベル)に対する詳細設定を記述(ボディ)し、任意のローカル名(2番目のラベル)を与える。
指定できるリソースタイプは、required_providers
ブロックで設定したプロバイダから提供されるもの。
ローカル名はモジュール内で一意である必要があり、そのモジュール内だけで有効である。
ボディで設定できる項目は、provider
ブロック同様、各プロバイダのリファレンスを参照する必要がある。
lifecycle.ignore_changes
resource ブロック内の特定の項目について、差分を無視する設定ができる。
ここを設定した状態で、terraform plan や terraform apply を実行すると、実際には対象項目について差分が発生する場合も no changes と表示されて実行が終了する。が、tfstateには変更後の状態がちゃんと記録される。
「差分は出ないでほしいが、実際には変わっていてほしい」というケースが稀にあるが、そんな場合は terraform show で確認。
不安になったらば
module
ブロック
「複数のインフラリソースを束ねるコンテナ(=module)」の設定をするブロック。
moduleは、
data
TODO
2.3.2. データ型
大区分 | 小区分 | 型名 | 概要 |
---|---|---|---|
プリミティブ | - | string | 文字列型 |
プリミティブ | - | number | 数値型。整数も小数も扱える。 |
プリミティブ | - | bool | ブーリアン型 |
複合 | Collection | list | 配列の型。 配列の全ての要素が同じ型でなければならない。 明示的に宣言する場合は list(<値の型>) とする。 |
複合 | Collection | set | ユニークな値の集合。 全ての要素が同じ型でなければならない。 インデックスやキーを持たず、並び順の概念がない。 |
複合型 | Collection | map | キーバリューを伴う型。 全てのバリューが同じ型でなければならない。 { "<キー>": <値>, ... } or { <キー> = <値>, ... } という形で生成できる。 ただし、数値から始まるキーの場合は、前者の引用符でくくる方でなければならない。 |
複合 | Structural | tuple | 配列の型。 各要素の型は違っていてよい。 明示的に型を宣言する場合は、 tuple([<値の型>, <値の型>, ...]) という形で、要素数・要素の型の並び順ともにきっちり決まっていなければならない。 |
複合 | Structural | object | キーとバリューを伴う型。 値の型はそれぞれ別でよい。 明示的に型を宣言する場合は、 object( { <キー>=<値の型>, <キー>=<値の型> } ) という形で行う。 |
Collection型とStructural型の違い
Collection型は全ての値が同じ型出ないとダメ、Structural型はそうでなくてよい。
複合型の選択(所感)
以下のようなフローで判断すればよさそう。
- 計算量の観点から、利用する際に制約を満たすことができるのであれば、計算量の観点から
set
を選択する - setの制約を満たさないのであれば配列型はlistとtupleを、 キーとバリューを伴う型はmapとobjectを「値の型が全て同じであるかないか」で選択する
参考
Types and Values(公式ドキュメント)
Type Constraints(公式ドキュメント)
2.3.3. 繰り返し処理
2.3.3.1. for_each
類似の内容のresource
or data
or module
ブロックを別々に記述したくないときに使う。
for_each
にmap
or set(string)
型のデータを与えて繰り返し処理する。
例. mapの場合
resource "<リソースタイプ>" "<リソースのローカル名>" {
for_each = tomap({
<キー1>: <値1>
<キー2>: <値2>
...
})
<引数1> = each.key
<引数2> = each.value
}
例. set(string)の場合
resource "<リソースタイプ>" "<リソースのローカル名>" {
for_each = toset(["<要素1>", "<要素2>",...})
<引数1> = each.key
}
each.key
, each.value
ともに繰り返し時にset
の要素値が使われるため、<引数1> = each.value
でも可。
for_eachによって複製された各リソースへアクセスするためには、
<リソースタイプ>.<リソースのローカル名>["<mapのキー> or <set(string)の要素>"]
とすればよい。
参考: The for_each Meta-Argument
2.3.3.2. count
for_each
と同様に類似の内容のresource
or data
or module
ブロックを別々に記述したくないときに使う。
こちらは、number
型(かつ整数)のデータを与えて繰り返し処理させるもの。
例.
resource "<リソースタイプ>" "<リソースのローカル名>" {
count = 3
<引数1> = "xxx-${count.index}"
<引数2> = count.index
}
複製された各リソースへアクセスするためには、
<リソースタイプ>.<リソースのローカル名>[<インデックス(0始まり)>]
とすればよい。
2.3.3.3. for
resource
or data
or module
ブロック単位の繰り返し処理ではなく、単純に 「複合型(list, set, tuple, map, object
)を、式を設定して別の複合型(tuple, object)に変換する」際に使う。
tuple
へ変換する場合
# 複合型の値のみ使ってtupleへ変換する
[for <複合型の値を格納する変数> in <複合型> : <変換後tupleの値の式>]
# 複合型のキー, バリューともに使ってtupleへ変換する
[for <複合型のキー or インデックスを格納する変数>, <複合型の値を格納する変数> in <複合型> : <変換後tupleの値の式>]
具体例.
variable sample_set {
type set(string)
default = ["a", "b"]
}
resource "<リソースタイプ>" "<リソースのローカル名>" {
<引数1> = [for sample in var.sample_set : upper(sample)]
}
variable sample_map {
type map(string)
default = {
"a": "A",
"b": "B"
}
}
resource "<リソースタイプ>" "<リソースのローカル名>" {
<引数1> = [for k, v in var.sample_map : "${k}, ${v}"]
}
objectへ変換する場合
# 複合型の値のみ使ってobjectへ変換する
{for <複合型の値を格納する変数> in <複合型> : <変換後objectのキーの式> => <変換後objectの値の式>}
# 複合型のキー, バリューともに使ってobjectへ変換する
{for <複合型のキー or インデックスを格納する変数>, <複合型の値を格納する変数> in <複合型> : <変換後objectのキーの式> => <変換後objectの値の式>}
つまり、括弧が []
か{}
かの違いで変換後の型がtuple
になるのかobject
になるのか決まる。
具体例.
variable sample_set {
type set(string)
default = ["a", "b"]
}
resource "<リソースタイプ>" "<リソースのローカル名>" {
<引数1> = {for sample in var.sample_set : sample => upper(sample)}
}
variable sample_map {
type map(string)
default = {
"a": "A",
"b": "B"
}
}
resource "<リソースタイプ>" "<リソースのローカル名>" {
<引数1> = {for k, v in var.sample_map : ${v} => ${k}"}
}
参考: for Expressions
2.4. terraform コマンド
主要なコマンド
主要なものをTerraformを使用する際の工程順に並べると以下の通り。
terraform validate
.tfファイルを検証する
terraform init
.tfファイルに基づくクラウドインフラへの設定反映に必要なPROVIDERのダウンロード
terraform plan
クラウドリソースに対する現設定からの変化点の確認
「実際の環境の状態」、「過去にTerraformで適用した際の状態(tfstate)」、「これから適用しようとしている内容(.tf)」を加味し、結果どういう変更がなされるのか?の結果が出力される。
tfstate
Terraformで適用したインフラリソースの状態を保存しておくファイル。
最後に terraform apply した後で、Terraformを介さず手動でリソースの設定を変更してしまった場合、以下のようなメッセージで指摘される。
Terraform detected the following changes made outside of Terraform since the last "terraform apply":
terraform apply
クラウドリソースに.tfファイルの設定を反映
その他コマンド
terraform destory
.tfファイルで定義したクラウドリソースを削除
terraform fmt
.tfファイル内の標準の記述形式に則ってフォーマット
terraform show
tfstate ファイルに記録されている、「最後に terraform apply が実行された際のリソースの状態」を表示
plan,apply,destroy
について
差分に現れる記号の意味
-
+
: 追加 -
-
: 削除 -
-/+
: 置換 (削除からの追加) -
~
: 更新 (削除からの追加じゃなくて、単純な更新) -
<=
: 読み込み? (データリソースへの適用時のみ発生しうるらしい)
特定のリソースに対してだけインフラ設定を反映させたい場合
以下のようにする。
terraform plan -target='<対象のリソース1>' -target='<対象のリソース2>' ...
リソースは、'module.<モジュール名>.<リソースタイプ>.<リソースのローカル名>'
という形で指定するのが無難。
for_eachで暗黙的に生成されるresourceオブジェクトを指定する場合、
'module.<モジュール名>.<リソースタイプ>.<リソースのローカル名>["キー名"]'
みたいな形になるが、'
でくくらずに実行して以下のエラーに遭遇したことがあった
Error: Invalid target "module.<モジュール名>.<リソースタイプ>.<リソースのローカル名>[<キー名>]"
│
│ Index brackets must contain either a literal number or a literal string.
╵
2.5. 公式ドキュメント
以下、特に有用と思われるドキュメント。
3. 参考
4. しばらく実務で使ってみての感想
GCP周りで使う機会があったが、以下のような問題があった。利用するうえではなかなかストレスがたまるところ・・・
- 依存関係を明示的に記述すべき箇所があったりする。
- 各リソースで、意味のない「etag」の差分が一生表示される問題が残っている模様。プロバイダ管理外の情報のため、ignoreすることもできないっぽい。こちらはGitHubプロバイダのものだが事象は同じっぽい(https://github.com/integrations/terraform-provider-github/issues/796)