はじめに
この記事は CrowdWorks Advent Calendar 2017 の8日目の記事です。
Terraform職人の @minamijoyo です。Infrastructure as Codeしてますか?
インフラのコード管理に Terraform を使い始めて2年ちょっと、本番環境で運用していると日々色んな学びがあるので、Terraformやってみた系の入門記事では語られない、現場の運用ノウハウ的なものを共有してみようかと思います。
Terraformを使い始めた or 使っている人が、こんなときどうするの?っていうときに参考になれば幸いです。
書き始めたら超長文になりました。概要は以下のとおりです。
- 公式ドキュメントを読もう
- tfファイルを書く技術
- インデントを揃える
- 組み込み関数に親しむ
- lifecycleブロックを使う
- リソースの差分を無視する
- リソース再生成のときに新しいのを作ってから古いのを削除する
- リソースのうっかり削除の保護
- テンプレートを使う
- 外部ファイルを文字列として読み込む
- テンプレートに変数を埋め込む
- モジュールでコードを共通化する
- モジュールの作り方
- production/stagingなどの環境差分を管理する
- モジュール間で値を参照する
- Stateを跨いで値を参照する
- 条件分岐したい
- 条件によって変数の値を変える
- 条件によってリソースを作成したりしなかったり
- Terraformのバージョンを管理する技術
- Terraformをバージョンアップする
- Terraformのバージョンを切り替える
- Terraformのバージョンを固定する
- Terraformプロバイダをバージョンアップする
- Terraformプロバイダのバージョンを固定する
- tfstateファイルを書く技術
- ローカルでミニマムのStateを見てみる
- リモートのStateを見る
- 強制的にリソースを再生成する
- Terraform管理外の既存のリソースをTerraform管理下に入れる
- tfファイルをリファクタリングする
- Terraformをデバッグする技術
- デバッグログを出力する
- Terraformのソースコードをコンパイルする
- デバッグログにソースコードのファイル名と行番号を付ける
- Terraformをデバッガでステップ実行する
本稿執筆時点のTerraformのバージョンはv0.11.1です。普段はAWSをいじってるので、説明の例としてAWSが出てきますが、扱っているトピック自体はAWSだけじゃない汎用的な話がほとんどなので、適宜お使いのクラウドプロバイダーに読み替えて下さい。
公式ドキュメントを読もう
まず最初に、何を当たり前のことを言われそうですが、分からないことはまずは公式ドキュメントを読みましょう。
Terraformは現在も活発に開発されているプロダクトです。ネットでぐぐって出て来る情報は古いことがあるので(この記事もいずれはそうなっちゃうと思いますが)、迷ったら公式ドキュメントを見に行くのが確実です。
https://www.terraform.io/docs/index.html
Terraformで使われている用語が分かると公式ドキュメントなどが読みやすくなるので、最低限理解しておきたい用語についてざっくり説明しておくと、以下の理解でだいたいOK。
用語 | 意味 |
---|---|
Configuration | インフラ設定のコード。要するに *.tf ファイルにDSLで書くTerraformのコードのこと。 |
HCL | HashiCorp Configuration Languageの略。*.tf ファイルで使われているDSLのこと。 |
Resource | Terraformで管理する対象の基本単位。 |
Data Source | Terraform管理外だけど、Terraform内で参照したい参照専用の外部データ。 |
Provider | ResourceやData Sourceなどを作成/更新/削除するプラグイン。aws/google/azurermなど。 |
Provisioner | リソースの作成/削除時に実行するスクリプトなどのプラグイン。local-exec/remote-exec/chefなど |
State | Terraformが認識しているリソースの状態。 *.tfstate ファイルのこと。 |
Backend | Stateの保存先。local/s3/gcsなど。 |
Module | ResourceやData Sourceなどを再利用可能なようにまとめたConfigurationの単位。 |
ところで、 terraform apply
してみたらエラーが出たとかあるあるで、公式ドキュメントの対象のリソースの説明読んでもパラメータの意味とかがよく分からない場合、関連するIssueを探したり、最終的にソースコードを読みに行くしかないです。
Terraformはv0.10からProviderごとにリポジトリが分割されているので、TerraformのIssueやコードを探す場合は、リポジトリ構成を理解しておく必要があります。
Terraformの本体は以下のリポジトリにあります。
https://github.com/hashicorp/terraform
Providerはterraform-providersというOrganizationの下にあって、
https://github.com/terraform-providers
例えばAWSだと以下のリポジトリにあります。
https://github.com/terraform-providers/terraform-provider-aws
ちなみにProvisionerは数が少ないので、Terraform本体のリポジトリのbuiltinディレクトリの下にあります。
tfファイルを書く技術
インデントを揃える
インフラをコードで管理することのメリットの1つは、設定変更がコードレビュー可能になることですが、コードレビューでtfファイルのインデントが揃っていない、とか指摘するのは不毛です。
Terraformでは terraform fmt
というインデントなどのスタイルを揃えるコマンドを公式に提供しているので、これを使いましょう。Terraform自体はGo言語で開発されており、Go言語には go fmt
というコードスタイルを揃えるコマンドがあるので、たぶんそれの影響を受けてるのかなーと思います。いずれにせよ個人の好みではなく、公式にスタイルはかくあるべしというのが提示されているのは、不毛な議論を減らせてありがたいです。
terraform fmt
は、デフォルトでカレントディレクトリにあるファイル全部のスタイルを揃えます。
例えば以下のように微妙にイコールの位置が揃っていないファイルに
provider "aws" {
version = "1.4.0"
region = "ap-northeast-1"
}
terraform fmt
をかけると
$ terraform fmt
以下のようにイコールの位置が修正されます。
provider "aws" {
version = "1.4.0"
region = "ap-northeast-1"
}
ただ毎回手動で実行するのは忘れがちなので、エディタのプラグインなどを使ってファイルの保存時に自動で terraform fmt
を実行するようにしておけば、意識せずに常にフォーマットが揃った状態になるのでオススメです。また自動フォーマットの副次的なメリットとして、括弧などのとじ忘れで構文エラーがあると、フォーマットに失敗するので、構文エラーレベルの初歩的なミスに早期に気付くことができて便利です。
あなたのお気に入りのエディタに、Terraform用のプラグインがないかを調べる価値はあります。
私は普段NeoVimを使ってるので、ここでは(Neo)Vimについて簡単に説明しますが、Emacs/Atom/VSCode/IntelliJなどでも、ぐぐるといろいろ情報が出てくるので、自分の好きなエディタでTerraformのプラグインがないか調べてみて下さい。
(Neo)Vimの場合は、以下の hashivim/vim-terraform
を入れるとシンタックスハイライトや、ファイル保存時の自動フォーマットが使えるようになります。
https://github.com/hashivim/vim-terraform
NeoBundle 'hashivim/vim-terraform'
let g:terraform_fmt_on_save = 1
READMEにも書いてありますが、ファイル保存時の自動フォーマットを有効にするには let g:terraform_fmt_on_save = 1
をセットしておきます。
また juliosueiras/vim-terraform-completion
も入れるとリソースタイプやパラメータ名などのオムニ補完もできるようになります。
https://github.com/juliosueiras/vim-terraform-completion
NeoBundle 'juliosueiras/vim-terraform-completion'
ちなみにこの vim-terraform-completion
は内部的にRubyに依存してるので、Vimの場合は +ruby
オプションが有効になってるか確認して下さい。
NeoVimの場合は gem install neovim
が必要です。
組み込み関数に親しむ
Terraformには組み込み関数がいくつかあって、知ってるとtfファイルを書く時に便利なことがあるので、文字列とかを加工したくなったら、便利な組み込み関数がないか調べてみましょう。
https://www.terraform.io/docs/configuration/interpolation.html#built-in-functions
この関数の挙動よく分からんなぁーってときは terraform console
でインタラクティブなREPLが起動できるので、これで試すと便利です。
$ terraform console
> coalesce("","hoge")
hoge
> coalesce("fuga","hoge")
fuga
lifecycleブロックを使う
リソースの差分を無視する
Terraform使ってると、一部のリソースの設定差分を無視したいと思う時があります。例えば aws_autoscaling_group
を使ってBlue/Greenデプロイメントをしようとすると、アタッチしている load_balancers
を動的に変更したくなります。このように、リソース自体はTerraformで作るんだけど、運用の都合上、手動で設定いじっても無視して欲しい属性は lifecycle
ブロックで ignore_changes
に指定すると差分を無視できます。
resource "aws_autoscaling_group" "app_1" {
name = "app-asg-1"
...
lifecycle {
ignore_changes = ["load_balancers"]
}
}
また ignore_changes
のよくある用途として、パスワードなどをtfファイルに平文でハードコードしたくない場合に使えます。
Terraformでリソース作るときに設定項目としてパスワードなどのクレデンシャルが必要になる場合があります。例えばAWSのRDSインスタンスを立てる場合は、aws_db_instance
の password
にDBのマスターパスワードを設定値として記載する必要があります。
このようなクレデンシャルは、tfファイル上は変数にして、terraform apply
時に渡すこともできますが、この方法はStateにパスワードが平文で記録されてしまい、Stateにアクセス可能な人なら誰でも参照できてしまうので、個人的にはあまりオススメしません。
個人的には、このようなパスワードなども ignore_changes
でTerraformの管理対象外にしちゃうのがオススメです。つまり、初回のterraform applyのときに初期パスワードのみ記載しておき、 ignore_changes
で指定しておけば、あとでパスワードを変えても差分としては無視されるので、パスワード変更可能です。
resource "aws_db_instance" "db_instance" {
identifier = "db"
password = "hoge"
...
lifecycle {
ignore_changes = ["password"]
}
}
リソース再生成のときに新しいのを作ってから古いのを削除する
lifecycle
の使い方の他の例は、リソースの再生成時に古いの削除=>新しいの作成ではなく、新しいの作成=>古いの削除の順に実行したいときです。例えば aws_autoscaling_group
に紐づく aws_launch_configuration
を更新したい場合、 aws_launch_configuration
は更新ができないリソースなので、基本的に古いの削除=>新しいの作成が毎回発生しますが、このままだとリソースの依存として aws_autoscaling_group
からの参照が残っていて古いのを削除できません。 ただ無停止で更新したいので、 aws_autoscaling_group
を削除はしたくありません。このような微妙なリソースの依存関係がある場合は、lifecycleブロックで create_before_destroy = true
を指定することで、新しいの作成=>古いの削除の順に実行できます。
resource "aws_launch_configuration" "as_conf_1" {
image_id = "ami-xxxx"
name_prefix = "app-"
lifecycle {
create_before_destroy = true
}
}
この方法の1点注意点として、新しいの作成=>古いの削除の順に実行するため、一時的に2つのリソースができます。リソースのネーミングでユニーク制約がある場合は、リソースの作成に失敗します。自動採番されるようなリソースであれば問題ありません。
リソースのうっかり削除の保護
lifecycle
には prevent_destroy
というリソースのうっかり削除を保護するフラグがあります。ただ productionのインフラをいじってるとだいたい消しちゃまずいリソースが大半なので、あんまりこれに頼りすぎるのはよくないかなとは思います。ちゃんとplanを見ましょう。
テンプレートを使う
外部ファイルを文字列として読み込む
例えばリソースの設定パラメータがJSONの文字列を受け取る場合、その場でヒアドキュメントで書くことも可能ですが、外部ファイルを文字列として読み込むこともできます。
resource "aws_iam_role" "ec2_role" {
name = "ec2-role"
assume_role_policy = "${file("./ec2_assume_role_policy.json")}"
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
特にJSONは外部ファイルに保存するとエディタでシンタックスハイライトが効いたりして、末尾カンマでエラーになるなどの初歩的なミスが防げるので、外部ファイルに切り出すとよいかと思います。
テンプレートに変数を埋め込む
設定ファイルが読み込めるようになると、変数埋め込みたくなることありますよね。
こーゆーときは変数を埋め込みたい場所に ${変数名}
と書いておいて、
{
"Version": "2012-10-17",
"Id": "key-policy-for-${account_name}-account",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::${account_id}:root"},
"Action": "kms:*",
"Resource": "*"
}
]
}
template_file
のData Sourceを作って、変数を外から差し込みます。
data "template_file" "kms_policy" {
template = "${file("${path.module}/kms_policy.json")}"
vars {
account_name = "dev"
account_id = "123456"
}
}
で、実際に文字列として必要な場所で、renderします。
resource "aws_kms_key" "key" {
policy = "${data.template_file.kms_policy.rendered}"
}
モジュールでコードを共通化する
モジュールの作り方
Terraformのいいところの一つとして、似たような構成のサーバを立てたりするときに、だいたいコピペ&置換で仕事が完了することなのですが、コピペが増えてくると、コードを共通化したくなります。
Terraformで再利用可能なコードの塊を定義する方法として module
があります。
適当なhogeディレクトリを掘って、その中に共通化したい resource
などを定義します。
provider "null" {}
resource "null_resource" "hoge" {}
そして、利用したい場所で、 module
を使って、 source
で相対パスを指定して読み込みます。
module "bar" {
source = "./hoge"
}
module "baz" {
source = "./hoge"
}
このようにディレクトリ掘ってtfファイルをグループ化するだけで、モジュールを使い始められます。
ローカルディレクトリ以外に外部のリポジトリも参照する仕組みがあり、最近Terraform Registryという公開モジュールをオープンに共有する仕組みもできました。
https://registry.terraform.io/
リソースのネーミングルールなどの都合もあるので、公開されているモジュールをそのまま使えるケースはあんまりないんじゃないか?という気もしますが、他の人がどのようなTerraformのコードを書いてるのか参考になるので、見てみるとよいんじゃないでしょうか。
production/stagingなどの環境差分を管理する
モジュールには variable
も渡せるので、production/stagingなどの環境を差分を管理する場合は、共通コードをモジュール化し、差分を variable
で吸収することが可能です。
modules/hoge/*
配下に共通化したいtfファイルを置いて、
services/myapp/production/main.tf
でproduction環境用の設定を、services/myapp/staging/main.tf
でstaging環境用の設定をとディレクトリを分けて管理します。
$ tree
.
├── modules
│ └── ec2
│ └── aws_instance.tf
└── services
└── myapp
├── production
│ └── main.tf
└── staging
└── main.tf
provider "aws" {}
resource "aws_instance" "app" {
...
tags {
Name = "app-${var.service}-${var.env}"
service = "${var.service}"
env = "${var.env}"
}
}
module "app" {
source = "../../../modules/ec2"
service = "myapp"
env = "production"
}
module "app" {
source = "../../../modules/ec2"
service = "myapp"
env = "staging"
}
実はTerraformには1つのディレクトリで複数のStateを扱うWorkspaceという機能もあるのですが、個人的には普通にディレクトリを分けて管理する方が楽です。
production/stagingは完全に同じリソース構成で、設定のパラメータの差分がちょっとだけあるという理想的な世界ではWorkspaceでも運用できるかもしれませんが、現実的には、stagingだけまだリリース前の検証用の一時的なリソースが立ってたりとか。完全に同じ構成にならないことも多いので、モジュールの読み込みの有無や一部の環境だけ存在するリソースなど、差分を吸収できる場所があったほうが都合がよいからです。
また、ディレクトリを分けることの副次的な効果として、production/stagingなどの環境ごとにAWSのアカウントが異なる場合に、AWSアクセスキーなどの環境変数を使い分けないといけないのですが、ディレクトリが分かれていればdirenvなどのツールを用いて、環境変数の切り替えが簡単に行なえます。
複数の環境をまたいでアカウントでグローバルなリソースは、別のディレクトリでStateを分けて、後述のStateを跨いで値を参照する方法を使っています。
モジュール間で値を参照する
モジュール分割を進めると、こっちのモジュールで作った値を、別のモジュールで参照したいということが発生します。
モジュールからの出力は output
で、モジュールへの入力は variable
でできるので、これらを組み合わせることで、モジュール間で値を引き回せます。
provider "aws" {}
variable "service" {}
variable "env" {}
output "hoge_private_ip" {
value = "${aws_instance.hoge.private_ip}"
}
resource "aws_instance" "hoge" {
tags {
Name = "hoge-${var.service}-${var.env}"
service = "${var.service}"
env = "${var.env}"
}
}
module "app" {
source = "../../../modules/ec2"
service = "myapp"
env = "production"
}
module "foo" {
source = "../../../modules/bar"
ip = "${module.app.hoge_private_ip}"
}
variable "ip" {}
Stateを跨いで値を参照する
先ほどの例ではモジュールをまたいだ変数の参照でしたが、Stateが分かれてる場合は terraform_remote_state
のData Sourceを使うことで、他のStateの値を参照することができます。
例えばStateAで作ったリソースをStateBで参照したい場合、まずStateA側で参照したい値をoutputで出力しておきます。
output "hoge_instance_profile_name" {
value = "${aws_iam_instance_profile.hoge.name}"
}
で、参照したい側のStateBで terraform_remote_state
を使ってStateAのData Sourceを作って参照します。
data "terraform_remote_state" "aws_main_iam" {
backend = "s3"
config {
bucket = "my-tfstate-main"
key = "aws-accounts/main/iam/terraform.tfstate"
region = "ap-northeast-1"
}
}
module "hoge" {
source = "../../../modules/hoge"
instance_profile_name = "${data.terraform_remote_state.aws_main_iam.hoge_instance_profile_name}"
}
管理するリソースが増えてStateが肥大化すると、 terraform plan
のリフレッシュが遅くなったり、オペミスで事故った時の影響範囲が広くなるので、適当な粒度でStateを分割しておくとよいでしょう。
条件分岐したい
条件によって変数の値を変える
Terraform書いてるとif文書きたくなることありますよねー。残念ながら今のところif文はないんですが、三項演算子なら書けます。
resource "aws_instance" "web" {
subnet = "${var.env == "production" ? var.prod_subnet : var.dev_subnet}"
}
条件によってリソースを作成したりしなかったり
モジュールでコードを共通化したものの、変数だけで差分を吸収しきれず、条件によって一部のリソースを作ったり作らなかったりしたい時もあります。まぁモジュールを分けてもいいんだけど、ほぼ同じだったりすると、分けるほどでもないかなみたいなとき。
Terraformにはリソースを複数個作成するのに count
というパラメータがあって、これを先ほどの三項演算子と組み合わせると、条件によって作ったり作らなかったりが実現できます。
resource "aws_cloudwatch_log_group" "hoge" {
count = "${var.create_log_group ? 1 : 0}"
name = "hoge"
}
Terraformのバージョンを管理する技術
Terraformをバージョンアップする
Terraformは日々進化しています。バージョンアップしたい理由は様々ですが、最新の機能が使いたい、踏んだ既知のバグが治ってる、など長く運用しているとバージョンアップしたくなることもあるでしょう。
本稿執筆時点のTerraformのバージョンはv0.11.1ですが、まだv1.0に到達していないので、v0.10.x => v0.11.x などマイナーバージョンアップでも非互換な変更が入ります。ここではTerraform本体のバージョンアップについて記載します。ここでTerraform本体と言っているのは、 hashicorp/terraform のことで、Terraformはv0.10から各クラウドプロバイダ(AWS, GCP, Azureなど)に依存したプラグインが terraform-providers/terraform-provider-* に分離されました。プロバイダのバージョンアップについては後述します。
Terraformのバージョンアップをする場合には、いくつか注意点がありますが、まずバージョンアップ前に既に出ている警告を解消することをオススメします。事前にdeprecatedな警告ログを出しておいて、次バージョンで機能削除などがあるので。
このような理由からマイナーバージョンアップでもv0.9.x => v0.11.x などバージョン飛ばしはせずに、v0.9.x => v0.10.x => v0.11.x と刻んで上げた方が無難です。マイナーバージョンはだいたい今のところ、数ヶ月に1回ぐらいの頻度でリリースされてます。
非互換な修正が入る場合は、UpgradeガイドやCHANGELOGに記載があるので、必ず確認しましょう。*.tf
ファイルの修正が必要な場合もあります。
公式のUpgradeガイドはバージョンごとに以下で参照できます。
https://www.terraform.io/upgrade-guides/index.html
CHANGELOGはGitHubの本体のリポジトリにあります。
https://github.com/hashicorp/terraform/blob/master/CHANGELOG.md
チームで複数人で開発している場合は、バージョンを揃えないと事故の元です。バージョンアップに伴い、非互換変更で、*.tf
ファイルの修正が必要な場合は、チーム内でタイミングを見計らって一気に切り替えます。
バージョンアップの具体的な手順は、後述の「Terraformのバージョンを切り替える」を参考にして下さい。
バージョンアップしたら terraform plan
しなおして、意図しない差分が出ていないかや、新しい警告が出ていないかチェックしましょう。変な差分が出てる場合は、関連した変更がないかCHANGELOGを探しましょう。
Terraformのバージョンを切り替える
Terraformは活発に開発されており、バージョンアップも比較的早いプロダクトです。
チームで開発する場合は、全員が同じバージョンを使わないと、微妙なバージョン違いで意図しない差分が出ることがあり注意が必要です。
Terraformはコンパイル済のバイナリとして配布されているので、特定のバージョンをダウンロードしてきてPATHを通すだけで、好きなバージョンが使えるのですが、その辺を簡単にしてくれる tfenv というツールを使うのがオススメです。
tfenvはMac OSXであればHomebrew経由でインストールできます。それ以外の場合は、git cloneしてきて適当にPATHを通すと使えます。詳細はtfenvのREADMEを参照して下さい。
$ brew install tfenv
$ tfenv --help
Usage: tfenv <command> [<options>]
Commands:
install Install a specific version of Terraform
use Switch a version to use
uninstall Uninstall a specific version of Terraform
list List all installed versions
list-remote List all installable versions
利用可能なバージョンを確認します。
$ tfenv list-remote | head
0.11.1
0.11.0
0.11.0-rc1
0.11.0-beta1
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
指定のバージョンをインストールします。
$ tfenv install 0.11.0
[INFO] Installing Terraform v0.11.0
[INFO] Downloading release tarball from https://releases.hashicorp.com/terraform/0.11.0/terraform_0.11.0_darwin_amd64.zip
######################################################################## 100.0%
[INFO] Downloading SHA hash file from https://releases.hashicorp.com/terraform/0.11.0/terraform_0.11.0_SHA256SUMS
tfenv: tfenv-install: [WARN] No keybase install found, skipping GPG signature verification
Archive: tfenv_download.ef4YCM/terraform_0.11.0_darwin_amd64.zip
inflating: /usr/local/Cellar/tfenv/0.6.0/versions/0.11.0/terraform
[INFO] Installation of terraform v0.11.0 successful
[INFO] Switching to v0.11.0
[INFO] Switching completed
$ terraform --version
Terraform v0.11.0
tfファイルを管理しているリポジトリのルートにでも .terraform-version
というファイルを置いて、その中にバージョン番号を書いておくと、 tfenv install
のバージョン番号を省略できます。
$ cat .terraform-version
0.10.7
$ tfenv install
[INFO] Installing Terraform v0.10.7
[INFO] Downloading release tarball from https://releases.hashicorp.com/terraform/0.10.7/terraform_0.10.7_darwin_amd64.zip
######################################################################## 100.0%
[INFO] Downloading SHA hash file from https://releases.hashicorp.com/terraform/0.10.7/terraform_0.10.7_SHA256SUMS
tfenv: tfenv-install: [WARN] No keybase install found, skipping GPG signature verification
Archive: tfenv_download.bHmtrT/terraform_0.10.7_darwin_amd64.zip
inflating: /usr/local/Cellar/tfenv/0.6.0/versions/0.10.7/terraform
[INFO] Installation of terraform v0.10.7 successful
[INFO] Switching to v0.10.7
[INFO] Switching completed
この方法のうれしいところは、tfファイルのリポジトリごとにTerraformのバージョンが違っても、cdすると自動で .terraform-version
を見てバージョンを切り替えてくれるところです。
Terraformのバージョンを固定する
tfenvはバージョンを切り替えてくれる便利ツールですが、開発チーム全員がtfenvを使っていない場合はバージョンの強制にはなりません。使用するTerraformのバージョンを固定して強制するにはtfファイルに terraform.required_version
を定義します。
例として、terraform v0.10.7に固定してみました。
terraform {
required_version = "= 0.10.7"
}
これを例えばv0.11.0で使うと、以下のようにエラーが出ます。
$ terraform --version
Terraform v0.11.0
+ provider.null v1.0.0
$ terraform plan
Error: The currently running version of Terraform doesn't meet the
version requirements explicitly specified by the configuration.
Please use the required version or update the configuration.
Note that version requirements are usually set for a reason, so
we recommend verifying with whoever set the version requirements
prior to making any manual changes.
Module: root
Required version: = 0.10.7
Current version: 0.11.0
バージョンを厳密に強制したい場合は、tfファイル側に書くとよいでしょう。
Terraformプロバイダをバージョンアップする
Terraformプロバイダをバージョンアップする際の注意点は、警告ログやCHANGELOGを確認するなど基本動作はだいたいTerraform本体と同じです。
たとえばAWSの場合は、CHANGELOGは以下にあります。
https://github.com/terraform-providers/terraform-provider-aws/blob/master/CHANGELOG.md
バージョンアップの具体的な手順は terraform init
コマンドに -upgrade
オプションを指定すると、バージョン制約を満たす最新のプロバイダにアップグレードされます。
$ terraform init -upgrade
なので、指定のバージョンにアップグレードするには、プロバイダもバージョン制約で明示的に固定しておくとよいでしょう。
Terraformプロバイダのバージョンを固定する
プロバイダのバージョン制約はproviderブロックに書けます。例えばAWSの場合はこんなかんじです。
provider "aws" {
version = "= 1.3.0"
}
チーム開発する場合は、バージョン間の微妙な差分でハマらないように固定しておくとよいでしょう。
tfstateファイルを書く技術
これ以降はかなり発展的なトピックです。
Terraformを使いこなすには、Stateの状態を記録した terraform.tfstate
の理解は避けて通れません。
tfstateを制すものがTerraformを制すと言っても過言ではないでしょう。
tfstateファイルは内部実装なので、ここに記載したこともTerraformのバージョンが上がれば古くなってしまう可能性が高いです。しかしながら現時点の知見を共有することにはなにがしかの意味があると思うので、理解している範囲で書きます。
お約束: tfstateファイルを手動でいじるのは大変危険ですので、インフラがぶっ壊れても責任は取れません。試すなら壊れて良い環境でまずは試し、自分が何をしているかを理解した上で自己責任で行って下さい。
ローカルでミニマムのStateを見てみる
サンプルで適当なtfファイルを書きます。
provider "null" {}
resource "null_resource" "hoge" {}
terrform apply
すると、Stateが保存されます。
リモートStateを使っていないデフォルトの場合は、ローカルのカレントディレクトリに terraform.tfstate
という名前で保存されます。
ちょっと中身を見てみましょう。
{
"version": 3,
"terraform_version": "0.11.1",
"serial": 1,
"lineage": "6fab4dbe-eca7-405b-8210-c82c5276ac28",
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {
"null_resource.hoge": {
"type": "null_resource",
"depends_on": [],
"primary": {
"id": "1492336661259070634",
"attributes": {
"id": "1492336661259070634"
},
"meta": {},
"tainted": false
},
"deposed": [],
"provider": "provider.null"
}
},
"depends_on": []
}
]
}
見るとわかりますが、ただのJSONです。
各項目の意味は実装詳細としてドキュメント化されていませんが、ソースコードを眺めたかんじ、以下のとおりです。
項目 | 意味 |
---|---|
version | Stateファイルのフォーマットバージョンです。Stateファイル自体のフォーマットが変更された場合にマイグレーションできるようにフォーマット自体のバージョンがあります。 |
terraform_version | Terraformのバージョン。古いTerraformで書かれたStateは新しいTerraformで読めますが、その逆はエラーになります。 |
serial | シリアル番号です。Stateが更新されるたびにインクリメントされます。諸般の事情で手動でStateを編集する場合は、このシリアル番号もインクリメントする必要があります。 |
lineage | Stateの血統です。うっかり間違って無関係のStateを上書いたりしないように、シリアル番号を比較することに意味があるかどうかを判別するため、Stateが初期化されるタイミングでUUIDが発行されます。 |
modules | モジュールの状態です。 |
modulesの中は、moduleごとに状態が記録されていますが、moduleは以下の要素で構成されています。
項目 | 意味 |
---|---|
path | モジュールのツリーのパスです。必ずrootモジュールが1つ存在します。rootモジュールから読み込まれているhoge moduleの場合は、rootから見たツリー上の位置が、 [root, hoge] というように記録されてます。 |
output | モジュールのoutputです。 |
resources | リソースの状態です。 |
depends_on | モジュール間の依存です。 |
resoucesの中は、resourceごとに状態が記録されていますが、resourceは以下の要素で構成されています。ちなみにresourceと言いつつ、data sourceも data.
というprefixがついた 特殊なresourceとして記録されてます。
項目 | 意味 |
---|---|
type | リソースの種類です。 |
depends_on | リソース間の依存です。 |
primary | アクティブなインスタンスの状態です。 |
deposed | 削除予定のインスタンスです。 create_before_destroy のときに使われます。 |
provider | プロバイダの名前です。プロバイダはマルチリージョンしたりするのに、エイリアスを作ったりできるので記録されています。 |
primaryの中が実際のリソースの状態です。
項目 | 意味 |
---|---|
id | リソースIDです。何がIDになりうるかはリソースタイプによって異なります。 null_resouce の場合はただの乱数ですが、一般的にはインスタンスIDなど、APIなどでそのリソース情報を取得するのにユニークに特定し得る値が使われます。 |
attributes | リソース属性です。リソースの設定項目などの具体的な値を保存しているのはここです。 |
meta | Terraformコアからは無視されるけど外部ツールが使うことを意図したメタ情報です。 |
tainted | 汚染フラグです。リソースをtainted状態にすると、次回のplanで削除=>作成で再作成するplanになります。 |
リモートのStateを見る
リモートにStateを保存している場合でも、 terraform state pull
するとStateを標準出力に表示できます。
Stateの状態がおかしくなっちゃったときに状態を確認したり、リダイレクトして簡易バックアップすることも可能です。
{
"version": 3,
"terraform_version": "0.10.7",
"serial": 554,
"lineage": "dd612537-035f-4cf3-94c8-0618bab715cd",
"backend": {
"type": "s3",
"config": {
"bucket": "my-tfstate-main",
"key": "services/myapp/staging/terraform.tfstate",
"region": "ap-northeast-1"
},
"hash": 4435600807653202963
},
"modules": [
{
"path": [
"root"
],
強制的にリソースを再生成する
諸般の理由でリソースを強制的に再生成したいときがあります。
例えば terraform apply
に失敗しちゃって、なんか中途半端な微妙な状態のリソースができちゃったときとか。
そーゆーときは、 terraform taint
でリソースをtainted状態にマークすると、次回のplanで強制的に削除=>作成で再生成されるplanが作られます。
provider "null" {}
resource "null_resource" "hoge" {}
$ terraform taint null_resource.hoge
The resource null_resource.hoge in the module root has been marked as tainted!
ここではrootモジュールのリソースを指定していますが、root以外のモジュール内のリソースをtaintする場合は、 引数 -module=
でモジュールのパスを指定して下さい。
planを見てみると、以下のように -/+
で再生成しようとしていることが分かります。
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
null_resource.hoge: Refreshing state... (ID: 1492336661259070634)
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
-/+ null_resource.hoge (tainted) (new resource required)
id: "1492336661259070634" => <computed> (forces new resource)
Plan: 1 to add, 0 to change, 1 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
Terraform管理外の既存のリソースをTerraform管理下に入れる
すべてがTerraformで管理されている理想的な世界であればよいのですが、既存のプロジェクトにあとからTerraformを導入した場合は、既にあるTerraform管理外のリソースをTerraformの管理下に入れたくなります。気軽に再生成できるリソースであれば、作り直しちゃうのが手っ取り早いですが、productionでもう動いちゃってるものは、現実的にはなかなか作り直しづらいものが多いのも事実です。
これまで見てきたように、Terraformは自身が管理しているリソースの状態をStateとして管理しているので、なんらかの方法でこのStateを作ってやる方法があります。
まず最初に試してみるのは、公式で提供されている terraform import
コマンドです。このコマンドは、importしたいリソースのState上のアドレスとリソースIDを指定すると、Stateに新しいリソースを追加してくれます。注意点として、Configurationを自動で生成してくれるわけではないので、tfファイルは自分で書く必要があります。
provider "aws" {}
ここでは例として、リソースタイプ aws_iam_role
で、Terraform上のリソース名 test_kitchen_ec2_role
として、 AWS上のIAMロール名 test-kitchen-ec2-role
をimportしてみましょう。
おもむろに、tfファイル側に定義がない状態でimportしようとすると、エラーが出ます。
$ terraform import aws_iam_role.test_kitchen_ec2_role test-kitchen-ec2-role
Error: resource address "aws_iam_role.test_kitchen_ec2_role" does not exist in the configuration.
Before importing this resource, please create its configuration in the root module. For example:
resource "aws_iam_role" "test_kitchen_ec2_role" {
# (resource arguments)
}
以前はできたんですが、Stateだけ追加されてConfigurationがないと、うっかりdestroyのplanができちゃうので、安全にエラーになるようになったんでしょう。tfファイルにリソースの枠だけ作ってみます。
provider "aws" {}
resource "aws_iam_role" "test_kitchen_ec2_role" {
}
importしてみます。
$ terraform import aws_iam_role.test_kitchen_ec2_role test-kitchen-ec2-role
aws_iam_role.test_kitchen_ec2_role: Importing from ID "test-kitchen-ec2-role"...
aws_iam_role.test_kitchen_ec2_role: Import complete!
Imported aws_iam_role (ID: test-kitchen-ec2-role)
aws_iam_role.test_kitchen_ec2_role: Refreshing state... (ID: test-kitchen-ec2-role)
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
terraform.tfstate
を見てみましょう。
{
"version": 3,
"terraform_version": "0.11.1",
"serial": 2,
"lineage": "591a7666-d636-4e4f-aea8-8a9df641e460",
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {
"aws_iam_role.test_kitchen_ec2_role": {
"type": "aws_iam_role",
"depends_on": [],
"primary": {
"id": "test-kitchen-ec2-role",
"attributes": {
"arn": "arn:aws:iam::XXXXXX:role/test-kitchen-ec2-role",
"assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
"create_date": "2016-07-12T03:00:40Z",
"force_detach_policies": "false",
"id": "test-kitchen-ec2-role",
"name": "test-kitchen-ec2-role",
"path": "/",
"unique_id": "XXXXXXX"
},
"meta": {},
"tainted": false
},
"deposed": [],
"provider": "provider.aws"
}
},
"depends_on": []
}
]
}
assume_role_policy
などが読み込まれていることが分かります。
planしてみます。
$ terraform plan
Error: aws_iam_role.test_kitchen_ec2_role: "assume_role_policy": required field is not set
assume_role_policyが必須項目だったのでエラーになりました。
assume_role_policyをtfファイル側に追加します。
Stateの値をそのままtfファイルにハードコードしてもよいんですが、改行なしのJSONは読み辛いので、適当にjqで整形して外ファイルを読み込むようにしておきましょう。
$ echo "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" \
| jq . > ec2_assume_role_policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
provider "aws" {}
resource "aws_iam_role" "test_kitchen_ec2_role" {
assume_role_policy = "${file("./ec2_assume_role_policy.json")}"
}
planします。
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
aws_iam_role.test_kitchen_ec2_role: Refreshing state... (ID: test-kitchen-ec2-role)
------------------------------------------------------------------------
No changes. Infrastructure is up-to-date.
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
差分がなくなりました。このようにplan差分がなくなるまで、tfファイルの設定を調整します。
ところで残念なお知らせですが、この terraform import
コマンドはすべてのリソースタイプに対応しているわけではありません。
対応していないリソースタイプの場合は、import実行するとエラーが出ます。
公式のimportコマンドが対応していないリソースタイプの場合は、自分でStateを書く必要があります。
しかし何もないところからおもむろにStateを書くのは、どのようなattributesがあるのか分からないので難しいです。実際には、以下のような流れで実施します。
- Terraformのドキュメントを眺めつつダミーのtfファイルを作る
- ダミーのリソースを
terraform apply
してtfstateの雛形作成 - AWSコンソールを眺めつつターゲットとなるリソースのtfファイルを作る
- tfstateの雛形をコピーしてターゲットとなるリソースのtfstateの枠を作る
-
terraform refresh
して terraformでtfstateを既存リソースの状態に合わせる - ダミーリソースを削除して
terraform plan
で差分がなくなれば完成
Terraform v0.6時代に私が書いた資料で若干古いんですが、実際の例は以下の記事も参考にしてみて下さい。基本的な流れは変わりません。
Terraforming未対応の既存リソースも自力でtfstateを書いてTerraform管理下に入れる
当時からの差分として注意すべきこととしては、StateをRemoteに保存している場合は、デフォルトでローカルに terraform.tfstate
が保存されなくなりました。Stateを編集するには、 terraform state pull
をファイルにリダイレクトして一旦ローカルに保存し、ファイルを編集、 terraform state push
で上書きが必要です。
余談ですが、上記の記事で出て来るterraformingというツールは terraform import
コマンドが公式にできる前からあった、サードパーティーなツールです。
tfファイルをリファクタリングする
Terraformのコードは成長してくると、リファクタリングしたくなってきます。
リファクタリングしたくなる理由は様々ですが、リソース名の表記ゆれを揃えたくなったり、よく使うリソースのセットをmoduleに切り出したりなど。ここで問題になるのがtfstateをどうするかです。
簡単な例を見てみましょう。以下のような null_resource.hoge
リソースがあるとして、
provider "null" {}
resource "null_resource" "hoge" {}
これを null_resource.fuga
リソースにリネームしたいとします。
provider "null" {}
resource "null_resource" "fuga" {}
何も考えずにリネームしてしまうと、以下のように、 null_resource.hoge
を一度削除して、null_resource.fuga
を新しく作成するplanが生成されてしまいます。
$ terraform plan
Terraform will perform the following actions:
+ null_resource.fuga
id: <computed>
- null_resource.hoge
Plan: 1 to add, 0 to change, 1 to destroy.
再生成可能なリソースであればこれでも構わないのですが、実際問題DBなど作り直しづらいリソースも存在します。
こういう場合は terraform state mv
コマンドを使います。
このコマンドはtfstate内のリソースの位置を移動しますが、このとき名前を変更すると同時にリネームもできます。
$ terraform state mv null_resource.hoge null_resource.fuga
Moved null_resource.hoge to null_resource.fuga
リネーム後に再度planしてみると差分がなくなりました。
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
null_resource.fuga: Refreshing state... (ID: 8190077603963424291)
------------------------------------------------------------------------
No changes. Infrastructure is up-to-date.
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
Terraformをデバッグする技術
デバッグログを出力する
Terraformのバグを踏んだかな?と思ったらデバッグログを出力してみましょう。
TF_LOG=DEBUG
を環境変数にセットするとデバッグログが出ます。ログレベルはTRACE, DEBUG, INFO, WARN, ERROR が指定可能です。
$ TF_LOG=DEBUG terraform init
2017/12/05 23:27:52 [INFO] Terraform version: 0.11.1 a42fdb08a43c7fabb8898fe8c286b793bbaa4835+CHANGES
2017/12/05 23:27:52 [INFO] Go runtime version: go1.9
2017/12/05 23:27:52 [INFO] CLI args: []string{"/usr/local/Cellar/tfenv/0.6.0/versions/0.11.1/terraform", "init"}
2017/12/05 23:27:52 [DEBUG] Attempting to open CLI config file: /Users/minamijoyo/.terraformrc
2017/12/05 23:27:52 [DEBUG] File doesn't exist, but doesn't need to. Ignoring.
2017/12/05 23:27:52 [INFO] CLI command args: []string{"init"}
2017/12/05 23:27:52 [DEBUG] plugin: waiting for all plugin processes to complete...
Terraform initialized in an empty directory!
The directory has no Terraform configuration files. You may begin working
with Terraform immediately by creating Terraform configuration files.
Terraformのソースコードをコンパイルする
terraformコマンドは使うだけなら意識することはないですが、内部的にはマルチプロセスで動いています。内部構造もわりと複雑でデバッグログを出してもソースコードとの対応がぱっと見分かりづらいです。デバッグログを眺めつつソースコードを追うのに、ファイル名と行番号も出ると幸せになれそうです。
これには現状ソースコードをちょっといじる必要があります。
というわけでTerraformをソースコードからコンパイルしてみましょう。
TerraformはGo言語で書かれており、 go get
してきて、コードをいじって、 go install
しなおせば修正バージョンを手元で動かすことができます。Go言語の開発環境の構築は割愛します。適宜ググって下さい。
コンパイルに必要なGoのバージョンは .travis.yml
あたりを見るとよいでしょう。現在はGo 1.9.1のようです。
$ go get github.com/hashicorp/terraform
$ cd $GOPAHT/src/github.com/hashicorp/terraform
(コードいじる)
$ go install
これはTerraform本体だけでなくproviderも同じです。普通に $GOPATH/bin
にパスが通ってればproviderも検索対象に含まれ、自分でコンパイルしたバージョンを使用できます。
$ go get github.com/terraform-providers/terraform-provider-null
$ cd $GOPAHT/src/github.com/terraform-providers/terraform-provider-null
(コードいじる)
$ go install
デバッグログにソースコードのファイル名と行番号を付ける
コンパイルの仕方がわかったところで、 デバッグログにソースコードのファイル名と行番号を付けてみましょう。
hashicorp/terraform
の main.go
の中にある main関数の先頭で、ログの設定をちょっといじってみます。
func main() {
log.SetFlags(log.Llongfile) // ★この行を追加
// Override global prefix set by go-dynect during init()
log.SetPrefix("")
os.Exit(realMain())
}
やってること自体は、Goの標準logパッケージの log.Llongfile
をセットするだけです。
コンパイルして $GOPATH/bin
配下にバイナリをインストールします。
$ go install
実行してみましょう。
$ TF_LOG=DEBUG $GOPATH/bin/terraform init
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:120: [INFO] Terraform version: 0.11.2 dev
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:123: [INFO] Go runtime version: go1.9
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:124: [INFO] CLI args: []string{"/Users/minamijoyo/bin/terraform", "init"}
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:239: [DEBUG] Attempting to open CLI config file: /Users/minamijoyo/.terraformrc
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:250: [DEBUG] File doesn't exist, but doesn't need to. Ignoring.
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:198: [INFO] CLI command args: []string{"init"}
/Users/minamijoyo/src/github.com/hashicorp/terraform/vendor/github.com/hashicorp/go-plugin/client.go:235: [DEBUG] plugin: waiting for all plugin processes to complete...
Terraform initialized in an empty directory!
The directory has no Terraform configuration files. You may begin working
with Terraform immediately by creating Terraform configuration files.
ログを出力している箇所のファイル名のフルパスと行番号が出るようになりました。これでコードリーディングが捗りそうです。
Terraformをデバッガでステップ実行する
ログを眺めているとデバッガでステップ実行したくなりますよね。
Go言語には delve というデバッガがあるので、これを使います。
delve自体の使い方の説明は以前書いたことがあるので、こちらの記事も参考にしてみて下さい。
ただterraformでdelveを使う場合注意すべきことがあります。それはterraformは内部的に複数のプロセスで構成されていることです。terraformはpanicをラップして綺麗なログを出すために内部的に mitchellh/panicwrap
というライブラリを使って、自分自身を別のプロセスとして起動し直してます。コードを読めばわかりますが、 TF_FORK=0
という環境変数をセットすると、この動作を回避して直接プロセスを起動できます。
delveでデバッグするときは、デバッグ対象のコマンドに渡す引数は --
以降に記載します。
なので、 terraform init
を動かす場合は、こんなかんじ。
$ TF_LOG=TRACE TF_FORK=0 dlv debug -- init
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x27b28af for main.main() ./main.go:32
(dlv) c
> main.main() ./main.go:32 (hits goroutine(1):1 total:1) (PC: 0x27b28af)
27: const (
28: // EnvCLI is the environment variable name to set additional CLI args.
29: EnvCLI = "TF_CLI_ARGS"
30: )
31:
=> 32: func main() {
33: log.SetFlags(log.Llongfile) // ★この行を追加
34: // Override global prefix set by go-dynect during init()
35: log.SetPrefix("")
36: os.Exit(realMain())
37: }
ところでterraformのメインプロセスと、providerのプロセスは分かれており、内部的にRPCでプロセス間通信を行っています。
このprovider側のプロセスにdelveをアタッチする方法はまだ編み出せていません。delveにはリモートプロセスにアタッチする機能もあるのですが、providerはデーモンのように常時起動しているわけではなく、terraformのコマンドが実行されている間だけ起動しているので、タイミングを見計らってアタッチするのも難しそうです。
現状provider側のコードをデバッグするには、適当なログ出力を仕込む、いわゆるprintデバッグ以上の有効な技を知らないので、誰かもっといいprovider側のデバッグ方法を知ってる人がいたら教えて下さい。
(2019/06/27追記)
providerのプロセスはterraformのコアから起動されるのでdelveで起動したバイナリを差し込むの難しそうだなーって思ってたんですけど、issue漁ってたらproviderをデバッグするのacceptance testをdlv testで起動すりゃいいよって情報を見つけて、なるほどーって思って試したらできました。
https://github.com/go-delve/delve/issues/496#issuecomment-413049532
$ TF_ACC=1 dlv test ./aws -- -test.v -test.run=TestAccAWSIAMRole_basic
おわりに
Terraformを使っていて日々学んだ知見を淡々とまとめてみました。
「だいたい知ってたわー」、「あるある過ぎてツライw」、「こんな超絶技巧テクニックも知ってるよー」
というTerraform職人の皆様は、ぜひ知見を共有して下さい。
(2020/12/11追記)
情報が古くなっちゃった感が否めないので、3年ぶりに続編書いた。こちらもどうぞ↓
Terraform職人再入門2020