LoginSignup
13
12

More than 5 years have passed since last update.

TerraformをRubyでごにょごにょしてみたメモ

Last updated at Posted at 2016-01-06

最近になってようやくTerraformを触り始めた石器時代人ですこんにちは。

tfstate をignoreするの?しないの?問題

terraform.tfstate はTerraformで作成されたリソースの状態を保持している巨大なJSONファイルです。

何らかのプロジェクトに使うTerraformリポジトリとこのファイルは一蓮托生になる(はず)なので、このファイルもgitで管理した方が一元管理できていいのでは?と思いますよね!!思いませんか?思いますね?

しかし、例えばaws_iam_access_keyを使ったりすると terraform.tfstate にIAM Userのcredentialsが思いっきり平文で書かれるんですね。これは git add したくない!!

書いてる途中に初めて知ったんですが、どうやら terraform.tfstateS3やAtlasで管理できるらしいので、まあそうしろという話なのですが、これをやっている最中の石器時代人は何としてもリポジトリの中で管理したいという夢を捨てきれませんでした。

terraform.tfstateにヤバい情報が含まれてるならば、暗号化して git add すればよくね?という思考に至りました。ChefのEncrypted Data Bag Item的な。

そうだ、暗号化しよう

.gitignore
terraform.tfstate
terraform.tfstate.backup

こいつらはアレなのでaddしません。その代わりに terraform.tfstate.encrypted をgit管理します。

しかしながら、こうなると当然ながら、terraform plan/applyを実行する前後に暗号化・復号化をする必要があるということになります。

  1. terraform.tfstate.encrypted → 何かで復号化する → terraform.tfstate
  2. terraform plan/apply を実行
  3. terraform.tfstate の内容が更新される
  4. terraform.tfstate → 何かで暗号化する → terraform.tfstate.encrypted

この流れが厳かに行われない限り、terraform.tfstate.encrypted が常に最新にならず

  • 古い状態の terraform.tfstate を使って terraform plan/apply が行われてしまう
  • terraform.tfstate.encryptedが最新化されないまま *.tf の変更だけが git commit されてしまう

といったよろしくない事態が起こります。

そうだ、 terraform を直接叩くのをやめよう

前節の1-4の流れをtaskとして順次実行するようにしてしまえばいいのです。そこで rake ですよ。

Rakefile
# 略

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を自動生成するようにしてみます。

Rakefile
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 の前に実行されるようにしておきます。

これで次のような記述ができるようになりました。

iam_users.tf.erb
<%
  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 %>
  ]
}

読みにくいとか言わないで!! :sob:

そして、 .tf.tf.erb の内容がズレてgitに登録されないように、

.gitignore
*.tf

嗚呼、Terraformのリポジトリなのに .tf が排除されてしまった。

せっかくだからTerraform本体もインストールできるようにしよう

Terraformはまだ若いプロダクトで、バージョンアップも頻繁に行われています。当然、同じ .tf ファイルでもTerraformのバージョンが違うと挙動が違う、ということがあり得ます。

このリポジトリのTerraformバージョンはこれ!と固定したいですよね?

そこで、カレントディレクトリ配下に固定されたバージョンのTerraform本体を置けばいいし、そうするtaskも作ればいいじゃんと考えました。

Rakefile
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
.terraform_version
0.6.8

$ rake terraform:install すればterraform本体が vendor/ 以下に置かれます。やったね。

そして terraform:plan terraform:apply はこのterraformを叩くようにします。

Rakefile
# 略

  desc "terraform plan"
  task :plan do
    system "vendor/terraform plan"
    Rake::Task["terraform:encrypt_state"].invoke
  end

# 略

ついでに

direnv という物がありまして、これが非常によいです。

せっかくだからAWSのcredentialsや、terraform.tfstate の暗号化・復号化のために使う鍵ファイルの情報はこれで管理するように強制しました。

こうなった

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12