最近になってようやくTerraformを触り始めた石器時代人ですこんにちは。
tfstate
をignoreするの?しないの?問題
terraform.tfstate
はTerraformで作成されたリソースの状態を保持している巨大なJSONファイルです。
何らかのプロジェクトに使うTerraformリポジトリとこのファイルは一蓮托生になる(はず)なので、このファイルもgitで管理した方が一元管理できていいのでは?と思いますよね!!思いませんか?思いますね?
しかし、例えばaws_iam_access_keyを使ったりすると terraform.tfstate
にIAM Userのcredentialsが思いっきり平文で書かれるんですね。これは git add
したくない!!
書いてる途中に初めて知ったんですが、どうやら terraform.tfstate
をS3やAtlasで管理できるらしいので、まあそうしろという話なのですが、これをやっている最中の石器時代人は何としてもリポジトリの中で管理したいという夢を捨てきれませんでした。
terraform.tfstate
にヤバい情報が含まれてるならば、暗号化して git add
すればよくね?という思考に至りました。ChefのEncrypted Data Bag Item的な。
そうだ、暗号化しよう
terraform.tfstate
terraform.tfstate.backup
こいつらはアレなのでaddしません。その代わりに terraform.tfstate.encrypted
をgit管理します。
しかしながら、こうなると当然ながら、terraform plan/applyを実行する前後に暗号化・復号化をする必要があるということになります。
-
terraform.tfstate.encrypted
→ 何かで復号化する →terraform.tfstate
-
terraform plan/apply
を実行 -
terraform.tfstate
の内容が更新される -
terraform.tfstate
→ 何かで暗号化する →terraform.tfstate.encrypted
この流れが厳かに行われない限り、terraform.tfstate.encrypted
が常に最新にならず
- 古い状態の
terraform.tfstate
を使ってterraform plan/apply
が行われてしまう -
terraform.tfstate.encrypted
が最新化されないまま*.tf
の変更だけがgit commit
されてしまう
といったよろしくない事態が起こります。
そうだ、 terraform
を直接叩くのをやめよう
前節の1-4の流れをtaskとして順次実行するようにしてしまえばいいのです。そこで rake
ですよ。
# 略
namespace :terraform do
# 略
desc "decrypt terraform.tfstate.encrypted to terraform.tfstate"
task :decrypt_state do
# 略
# terraform.tfstate.encrypted を terraform.tfstate に復号化する
end
desc "encrypt terraform.tfstate to terraform.tfstate.encrypted"
task :encrypt_state do
# 略
# terraform.tfstate を terraform.tfstate.encrypted に暗号化する
end
desc "terraform plan"
task :plan do
# 略
# terraform plan を実行する
Rake::Task["terraform:encrypt_state"].invoke
end
desc "terraform apply"
task :apply do
# 略
# terraform apply を実行する
Rake::Task["terraform:encrypt_state"].invoke
end
task :plan => :decrypt_state
task :apply => :decrypt_state
# 略
end
task default: ["terraform:plan"]
渡りに船ということで、暗号化・復号化もRubyでやってしまいます。symmetric_encryptionというgemを使いました。
$ rake -T
rake terraform:apply # terraform apply
rake terraform:decrypt_state # decrypt terraform.tfstate.encrypted to terraform.tfstate
rake terraform:encrypt_state # encrypt terraform.tfstate to terraform.tfstate.encrypted
rake terraform:plan # terraform plan
apply
するときは $ rake terraform:apply
を実行します。 plan
も以下同文ですが、default taskなのでこちらは rake
だけでもOKです。
直接terraformを叩くのは禁止。
せっかくだからERBテンプレートを使えるようにしよう
Terraformの定義ファイル *.tf
はあまりプログラマブルではありません。似たようなリソース定義をコピペで増やすのは面倒ですよね。
せっかくRubyを持ち込んだのだから、ERBで*.tfを自動生成するようにしてみます。
require "erb"
# 略
namespace :terraform do
# 略
desc "build *.tf.erb to .tf"
task :build do
Dir.glob("*.tf.erb").each do |path|
File.open path.sub(/\.erb\z/, ""), "w" do |file|
file.write ERB.new(File.read(path)).result
end
end
end
# 略
task :plan => :build
task :apply => :build
end
$ rake terraform:build
で、カレントディレクトリの *.tf.erb
が全て *.tf
にビルドされます。もちろん、 terraform:plan
terraform:apply
の前に実行されるようにしておきます。
これで次のような記述ができるようになりました。
<%
user_names = %w(
user_a
user_b
user_c
user_d
user_e
user_f
user_g
)
%>
resource "aws_iam_group" "users" {
name = "users"
path = "/users/"
}
<% user_names.each do |user_name| %>
resource "aws_iam_user" "<%= user_name %>" {
name = "<%= user_name %>"
path = "/users/"
}
resource "aws_iam_access_key" "<%= user_name %>" {
user = "${aws_iam_user.<%= user_name %>.name}"
}
<% end %>
resource "aws_iam_group_membership" "users_membership" {
name = "users_membership"
group = "${aws_iam_group.users.name}"
users = [
<% user_names.each do |user_name| %>
"${aws_iam_user.<%= user_name %>.name}",
<% end %>
]
}
読みにくいとか言わないで!!
そして、 .tf
と .tf.erb
の内容がズレてgitに登録されないように、
*.tf
嗚呼、Terraformのリポジトリなのに .tf
が排除されてしまった。
せっかくだからTerraform本体もインストールできるようにしよう
Terraformはまだ若いプロダクトで、バージョンアップも頻繁に行われています。当然、同じ .tf
ファイルでもTerraformのバージョンが違うと挙動が違う、ということがあり得ます。
このリポジトリのTerraformバージョンはこれ!と固定したいですよね?
そこで、カレントディレクトリ配下に固定されたバージョンのTerraform本体を置けばいいし、そうするtaskも作ればいいじゃんと考えました。
namespace :terraform do
desc "install terraform to this directory"
task :install do
terraform_version = File.read(".terraform_version").chomp
download_url = "https://releases.hashicorp.com/terraform/#{terraform_version}/terraform_#{terraform_version}_#{`uname`.match(/darwin/i) ? "darwin" : "linux"}_amd64.zip"
system "curl -L #{download_url} -o terraform.zip"
system "unzip terraform.zip -d vendor/"
system "rm -f terraform.zip"
end
# 略
end
0.6.8
$ rake terraform:install
すればterraform本体が vendor/
以下に置かれます。やったね。
そして terraform:plan
terraform:apply
はこのterraformを叩くようにします。
# 略
desc "terraform plan"
task :plan do
system "vendor/terraform plan"
Rake::Task["terraform:encrypt_state"].invoke
end
# 略
ついでに
direnv という物がありまして、これが非常によいです。
せっかくだからAWSのcredentialsや、terraform.tfstate
の暗号化・復号化のために使う鍵ファイルの情報はこれで管理するように強制しました。