概要
Terraformを使い始めて一年ほど経ったので、設計や運用上のノウハウを公開していこうと思います。
基本的にAWSを使うことが多いので、AWS寄りの話になります。
設計編
利用可能なプロバイダを調べる
TerraformはAWSやGCPといったクラウドサービスをコード化できることで有名ですが、オフィシャルだけでも様々なプロバイダに対応しています。
私はAWSのほか、GitHubやDatadogをコード化して運用してます。
ネットワーク設計
サービスを構築する上で本番環境のほかに開発やステージング環境を作りますが、環境の分け方は大きく分けて次のいずれかになることが多いと思います。
- 1つのVPCに全ての環境を入れる
- 環境ごとにVPCを分ける
- 環境ごとにAWSアカウントを分ける
2のパターンで設計する場合、Terraformで構成を組む際はVPCをdevelopment
、staging
、production
に分離し、更に各VPCで共通となるサービスを配置するためのmanagement
を作成します。management
には踏み台であったり、CI/CDを管理するインスタンスを配置しています。
ネットワーク上はmanagement
から各環境にVPCペアリングで接続できますが、環境間(例えばdevelopment
からstaging
、staging
からproduction
)の接続はルートテーブル上で制限しています。
ディレクトリ設計
Terraformでリソースを管理する場合、悩むことになるのがディレクトリ構成かと思います。私は Code structure で紹介されている方法を元に次のような構成にしてます。
# 構築する対象サービス。EC2、Security Groupといった単位
{service}
# resourceやmodule
main.tf
# main.tfやbackend.tfで参照する変数のリスト
variables.tf
# 出力パラメータのリスト
outputs.tf
# プロバイダ情報
providers.tf
# backendやdataのリスト
backend.tf
インフラは複数のサービスから構成されるため、実際には次のようなディレクトリ構成となります。
providers
aws
# AWS運用上必須となる基盤サービス
base
# リージョンに依存しないサービス
global
iam
route53
...
# 特定のリージョンを構築するサービス
ap-northeast-1
cognito
cloudtrail
...
# アプリケーションを構成する上で必要となるサービス
{APP NAME}
# ワークスペースに依存しないサービス
base
route53
...
# ワークスペースで構成されるサービス
workspace
ap-northeast-1
global
vpc
ecs
...
Terraform Best Practiceを読む
Terraform Best Practiceを読みましょう。中でもNaming conventionsは非常に有用です。いくつか気になるポイントをまとめてみました。
- Resource and data source arguments
- リソース名や変数、出力名の区切り文字には
-
の代わりに_
を使う - リソース名にはリソースタイプを含めない (例えば、
aws_route_table
のリソース名にroute_table
などのタイプを含めない) - 単一リソースや妥当な名前がない場合、リソース名は
this
とする -
tags
、depends_on
、lifecycle
はリソースの最後に記述する
- リソース名や変数、出力名の区切り文字には
- Variables
-
default = []
がある場合、type = "list"
は省略する -
default = {}
がある場合、type = "map"
は省略する -
list
やmap
の変数名は複数形を使用する
-
- Outputs
- 名前は
{name}_{type}_{attribute}
形式が望ましい
- 名前は
リソース名の変更
terraform state mv
コマンドを使います。
# リソース名をfooからbarに変更
$ terraform state mv aws_kms_key.foo aws_kms_key.bar
Workspaceを利用してる場合はWorkspace単位で実行が必要となります。
リソースをモジュール化
基本的にはリソース名の変更と同じです。接頭辞としてmodule.{MODULE_NAME}
を付けるだけです。
# リソースfooをbarモジュール内のaws_kms_key.bazに変更
$ terraform state mv aws_kms_key.foo module.bar.aws_kms_key.baz
実装編
Docker版Terraformを使う
Docker版のTerraformを利用しましょう。
バージョンを指定する
利用するバージョンを明確化することで、無用なトラブルを回避することができます。
Terraform
terraform {
required_version = "= 0.12.4"
}
Docker版Terraformを利用する場合、イメージのバージョンはlatest
ではなく、0.12.4
のように番号で指定することをお勧めします。
プロバイダバージョン
provider "aws" {
version = "= 2.19.0"
}
モジュールバージョン
module "foo" {
source = "terraform-aws-modules/security-group/aws"
version = "3.0.1"
}
尚、source
がGitHubの場合はversion
が使えないので、代わりにref
でバージョンを指定する必要がある。
module "foo" {
source = "github.com/azavea/terraform-aws-acm-certificate?ref=1.1.0"
}
tfstateファイルの管理方法
リソースの状態を管理するtfstateファイルはローカルやGitで管理するべきではありません。TerraformのBackend機能を使うことで、tfstateをDynamoDBやS3で管理することができます。
AWSを利用しているのであれば、S3がお手軽かと思います。
terraform {
backend "s3" {
bucket = "mybucket"
key = "path/to/my/key"
region = "us-east-1"
}
}
尚、S3を使う場合、対象バケットのバージョニングの有効化を強くお勧めします。tfstateを誤って削除したり、Terraformのバージョンアップでファイル構成が書き換わっても、簡単にステートファイルを復元することができます。
Terraform Module Registryを使う
Terraform Module Registryでは、汎用的に使えるモジュールが公開されてます。AWSで言えばEC2やSecurity Groupなど、いくつかの変数を渡すだけでリソースを作成してくれます。
Registryには大きく分けて、Terraformコミュニティが作ったものと、HashiCorpによって検証されたVerifiedモジュールがあります。
運用で使う場合は、検索時にVerified
にチェックを付けることをお勧めします。
フォーマットを整形する
fmt
コマンドでディレクトリ下のファイルに対しまとめて整形してくれます。
# 0.11まで
$ terraform fmt
# 0.12以降
$ terraform fmt -recursive
デバッグ
変数や関数の結果を確認したい場合はconsole
コマンドが便利です。
variable "foo" {
default = "bar"
}
# 変数の確認
$ terraform console
> var.foo
bar
# 関数の実行結果の確認
> lookup({foo="bar"}, "foo", "baz")
bar
環境ごとに異なる値を読み込む方法
変数名はWorkspace名を含めて定義します。
variable "rds_cluster" {
default = {
"default.engine_version" = "5.7.12"
"production.engine_version" = "5.7.22"
}
}
リソース作成時にlookup
関数を使うことで、現在有効なWorkspaceがproduction
であれば5.7.22
、それ以外であれば5.7.11
の値が設定されます。
resource "aws_rds_cluster" "this" {
engine_version = lookup(
var.rds_cluster,
"${terraform.workspace}.engine_version",
var.rds_cluster["default.engine_version"],
)
...
lifecycleでリソースを保護する
リソースにlifecycle
ブロックを指定することで、リソース作成・更新時の動作を変更することができます。lifecycle
にはいくつかのタイプがありますが、prevent_destroy
を指定することで、強制的なリソース再作成が発生したときにエラーを発生するようになります。
lifecycle {
prevent_destroy = true
}
destroy
を実行した結果。
$ terraform destroy
...
Error: Instance cannot be destroyed
on main.tf line 55:
55: resource "aws_eip" "this" {
Resource aws_eip.this has lifecycle.prevent_destroy set, but the plan calls
for this resource to be destroyed. To avoid this error and continue with the
plan, either disable lifecycle.prevent_destroy or reduce the scope of the plan
using the -target flag
リソースを強制的に削除したい場合は、prevent_destroy
をfalse
にするか、ブロックごと削除して、apply
を実行します。
※prevent_destroy
はあくまでTerraform上で削除を保護するためのもので、AWSコンソールやCLIからは削除することが可能です。EC2やRDSなどいくつかのサービスには削除保護のオプションがあるので、別途有効化しておくことをお勧めします。
IAM PolicyのJSONはaws_iam_policy_documentで定義する
以下のリソースはIAMポリシーがヒアドキュメント形式で書かれており、可読性が低いです。
resource "aws_iam_policy" "example" {
# ... other configuration ...
policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
}
POLICY
}
ヒアドキュメントはこのように置き換えることができます。HCLで書けるのでだいぶスッキリします。
resource "aws_iam_policy" "example" {
# ... other configuration ...
policy = data.aws_iam_policy_document.example.json
}
data "aws_iam_policy_document" "example" {
version = "2012-10-17"
statement {
effect = "Allow"
actions = ["*"]
resources = ["*"
}
}
デバッグログを出力する
環境変数TF_LOG
にログレベルを渡すことで、ログを出力することができます。ファイルに出力したい場合はTF_LOG_PATH
も指定すると良いでしょう。
実行速度を改善する
Terraformのコマンドに--parallelism
を渡すことで、APIの同時呼び出し数を変更することができます。
$ terraform plan --parallelism=20
デフォルト値は10
です。パフォーマンスを見て数値を変えて見ると良さそうです。
尚、環境変数で指定する場合はTF_CLI_ARGS_plan
、TF_CLI_ARGS_apply
、TF_CLI_ARGS_destroy
を指定します。
拡張ライブラリ
-
GoogleCloudPlatform/terraformer
- 既存のリソースからtfファイルとtfstateファイルを生成してくれる
-
gruntwork-io/terragrunt
- Terraformのラッパー。stateファイルのロックや、モジュール管理をDRYにする仕組みを提供
-
hashivim/vim-terraform
- Vimmer向け。保存時にフォーマット整形、コード検証
-
vim-terraform-completion
- Vimmer向け。入力補完やコード検証