Terraform で AWS環境を実運用する上で困ったことと、その対処

忘備録的なもの。
2017年2月時点、Terraformは0.8.6
-> 2017年3月時点、Terraform v0.9.0-dev(github/master)

# Terraformの最新版が使いたい

## 困ったこと

0.9の機能、はやく使いたい

## 解決策

githubから簡単にビルドできるっぽい。go-langすごい。

```shell-session
$ brew uninstall terraform
$ brew install go
$ cat <<'EOS' >> ~/.bashrc
export PATH=$(go env GOPATH)/bin:$PATH
EOS
$ go install github.com/hashicorp/terraform
$ terraform -v
Terraform v0.9.0-dev
```

# 何かいい参考コードはない？

## 困ったこと

リファレンスだけじゃなくて、例とか、サンプルコードを見たい。

## 解決策

公式のベストプラクティスがある。
https://github.com/hashicorp/best-practices

また、この辺も参考になるかもしれない。
https://github.com/terraform-community-modules


# 操作用AWSアカウントの認証情報の扱い

## 困ったこと
ネットの参考情報だと、awsの認証情報(credentials)を直接書くサンプルが非常に多かった。
しかし、tfファイルを書き換える運用だと、いつか間違えてcommit & pushしてインシデントになりそう。

Terraformを最初に動かすためのユーザーの認証情報は、その性質上大きな権限を持っていることが多いと思うので、慎重になりたい。

## 解決策

AWS-CLIのNamed Profile使おう。

```shell-session
$ aws configure --profile my-profile-name
AWS Access Key ID [None]: xxxxxxxxxx
AWS Secret Access Key [None]: xxxxxxxxxx
Default region name [None]: ap-northeast-1
Default output format [None]: 
```
事前にこうして設定しておけば、認証情報は`$home/.aws/credentials`に名前付きで入る。

スクリプトからはこう参照する。これならばcommitしてしまっても問題ない。

```example1.tf
provider "aws" {
  profile = "my-profile-name"
}
```

あとは、上記の設定手順をREADME.md なんかに書いて、KEYIDとSECRET自体はいつも通り正しく管理してあげればいい。


# .tfstateファイルの扱い

## 困ったこと
.tfstateファイルを紛失してしまうと、作成したインスタンスを、Terraformから管理できなくなってしまうので、最重要ファイルだ。
かといって、gitにcommitするルールだと、commit忘れが怖いし、その際pullし忘れるとつらい。
一方、手動であちこち引き回しても同じことで、別の開発者が古いstateに基づいて、重複したインスタンスを立ててしまうかもしれない…

## 解決策

backend s3 を使おう。
terraform 0.9からの新機能。

```terraform
terraform {
  backend "s3" {
    bucket  = "my-tfstate-store-name-at-s3"
    key     = "hogehoge/terraform.tfstate"
    profile = "my-profile-name"
  }
}
```

としておき、`terraform init`するだけで、S3のtfstateを同期できる。
指定するバケットはacl=privateにしておくこと！！
あと、上記リンクでは、S3のversioningとかもつけておくことを勧めている。(私はやらなかった)

S3以外にも、いろいろ手段は用意されているようだ。

これで普通にgitで管理できる。
`.terraform/terraform.tfstate` に保持されていたデータは、直接s3上のファイルに保存され、ローカルキャッシュはされなくなるので注意。

## 解決策(旧)

Backendの、remote state機能使おう。

ちゃんと公式ドキュメントある。 https://www.terraform.io/docs/state/remote/s3.html

こんな感じ。profileも指定できるので、そちらにregionを書いておけば省略できる。

```shell-session
$ terraform remote config \
    -backend=s3 \
    -backend-config="bucket=my-tfstate-store-name-at-s3" \
    -backend-config="key=hogehoge/terraform.tfstate" \
    -backend-config="profile=my-profile-name"
```
これを実行した時点で、存在していたterraform.tfstateは、./.terraform/terraform.tfstate に移動されるようだ。


あとは自動でtfstateファイルをアップロード、ダウンロードしてくれる。
指定するバケットはacl=privateにしておくこと！！
保存するバケットには、versioningとかもつけておくことを勧めているっぽい。(私はやらなかった)
S3以外にも、保存先は色々用意されているようだ。


# 環境ごとのvariableの扱い

## 困ったこと
ステージングと本番、みたいに分けようと思って、環境ごとにvariableで設定値を与えられるようにしたけど、
-var オプションで引数に全部指定するの辛い

## 解決策
共通の設定ならば、`terraform.tfvars` という名前のファイルに書いておけば、指定しないでも勝手に読み込んでくれるとのこと。
https://www.terraform.io/intro/getting-started/variables.html#from-a-file

環境ごとに違う変数は、-var-fileオプションを使ってスイッチするのがよさそうだ。

```shell-session
$ terraform plan -var-file staging.tfvars
$ terraform plan -var-file production.tfvars
```

的な。

varは後ろに指定したものが有効(上書きされる)とのことなので、上手に使えば強いはず。


# Packerで作ったAMIでEC2インスタンスを生成したい

## 困ったこと
auto-scalingを考えたときに、元となるAMIをちゃんと運用したい。
(注意、私はAWSのAutoScaling をよくわかっていないかも。)

そこで、Packerでイミュータブルなイメージ作ったらすごーいはず。
イミュータブルということは、イメージにDB接続情報がないといけない気がする。
よって、Terraformで先にRDS立てないといけないけど、そのTerraform側でAMIを使いたいからこういう話になっているわけで…
循環参照してしまう。

そもそも、AutoScaling配下のインスタンスを入れ替える際に全インスタンスを落とさないようにする、というのがなかなか厳しいようだ。
AutoScalingとの相性は、改善まち、という感じか。

参考: http://qiita.com/minamijoyo/items/e32eaeebc906b7e77ef8


## 準解決策1 null_resource
最終的にはうまくいかなかったが、最初に試した方法

null_resourceというものがある。
https://www.terraform.io/docs/provisioners/null_resource.html

何もしない代わりに、自身の状態(=変更が起きるトリガー)を定義するtriggers属性と、provisionerを設定できるリソースだ。
これを使って、

```example2.tf
variable "ami_name_prefix" {}

resource "null_resource" "packer" {
  triggers {
    db_host = "${aws_db_instance.hogehoge.address}"
  }

  provisioner "local-exec" {
    command = <<EOS
packer build \
  -var 'DB_HOST=${aws_db_instance.hogehoge.address}' \
  -var 'AMI_NAME_PREFIX=${ami_name_prefix}' \
  packer.json"
EOS
  }
}

data "aws_ami" "packer_ami" {
  most_recent = true
  name_regex = "^${var.ami_name_prefix}.*"
  owners = ["self"]
  depends_on = ["null_resource.packer"]
}

resource "aws_instance" "hoge" {
  ami_id = "${data.aws_ami.packer_ami.id}"
  ...
}
```

とこんな感じ。

しかし、Terraform 0.8.6だと、triggersの中にinterpolationを混ぜると、問答無用でcomputed扱いになってしまうようで、
この記述では毎回AMIが作成されてしまって、差分のみ実行というTerraformの観点からは、使えなかった。


## 準解決策2 イミュータブルを諦める

オートスケーリング自体はこのパターンでは不可能か。AMI化を再度行う必要があると思う。

ほぼ完成品のAMIを組み立てておいて、aws_instanceのprovisionerで最後の仕上げをする。
Dockerなんかは、環境変数で外部情報を読み込ませる、なんてことをするらしいので、この手法に踏み切った。

(2017/3 注: AWS EC2ならば、user_dataという仕組みがある。こちらを使うほうがシンプルかも。コメントで指摘いただいた。)

```shell-session
// .envが欠けた状態でAMIをつくる
$ packer packer.json
```


```example3.tf
data "aws_ami" "packer_ami" {
  most_recent = true
  name_regex = "^packer-ami-.*$"
  owners = ["self"]
  depends_on = ["null_resource.packer"]
}

data "template_file" "envfile" {
  # template = "${file(./env.tpl)}" などとした方が望ましいが例示のため。
  template = <<EOS
export DB_HOST="${db_host}"
export DB_PORT="${db_port}"
....
EOS

  vars {
    db_host = "${aws_db_instance.main.address}"
    db_port = "${aws_db_instance.main.port}"
  }
}

resource "aws_instance" "ec2" {
  ami_id = "${data.aws_ami.packer_ami.id}"
  ...

  # envファイルをuploadする。envファイルはdirenvとかdotenvとかで読み込まれるようにしておく。
  provisioner "file" {
    content = "${data.template_file.envfile.rendered}"
    destination = "/home/ec2-user/.env"
  }

  # 上で入れた環境変数を使ってサービスを立ち上げる。
  provisioner "remote-exec" {
    inline = ["sudo service unicorn start"]
  }

  # provisionerは、実行した環境からssh(のgolang実装。sshコマンドではない)で接続することになる。
  # 当然security_groupとvpc_internet_gatewayが適切に設定されている必要がある。
  connection {
    type = "ssh"
    ...
  }
}
```

# tfファイルを構造化したい

## 困ったこと
コピペや反復は悪だし、再利用性も下がる。

## COUNT

`COUNT = n`で配列のように同じタイプのリソースを複数作れる。
それぞれ微妙に違う値を書きたいなら、

```
COUNT = 2
ATTR = "${element(list("a", "b", "c", "d"), count.index)}"
```

などとできる。
listは変数化するとなおよい。

## module

複数のリソースにまたがっての反復パターンなら、module化してしまうとよい。
module自体の書き方はここでは説明しないが、

./modules/my_module/variables.tf
./modules/my_module/main.tf
./modules/my_module/output.tf

を作り、

```example4.tf
module "use_of_my_module" {
  source = "./my_module"
  var1 = ...
  var2 = ...
}
```

と書くことで使用。

```
$ terraform get
```
で、モジュールをterraformに読み込ませる準備をさせる。(./.terraform/modules/ にsym-linkが作成されるようだ)

様々に公開されているmoduleもあるようなので、むしろ自分でresourceを書かないほうが良いのかもしれない。


# その他
また何かあれば書く。