TerraformでのAWS構築


オーケストレーションツールとして `Amazon CloudFormation` をちょっと見てみたけど、なんか面倒な感じがしたので流行りそうな [`Terraform`](https://www.terraform.io/) を試してみる。

環境はMac(Yosemite 10.10.2)。
目標はAWSにEC2インスタンスを構築して破棄すること。

### Terraformインストール
Homebrewを使用してGo言語とMecurialを入れ、Githubからソースを落としてコンパイルする。
(MacのZIP版[v0.3.7]だと微妙にバグっぽい感じがあったのでソースが良さげ)

```bash
$ brew install hg
$ brew install go
$ echo 'export GOPATH=${HOME}/.golang' >> ~/.bash_profile
$ echo 'export PATH=${PATH}:${GOROOT}/bin:${GOPATH}/bin' >> ~/.bash_profile
$ source ~/.bash_profile
$ git clone https://github.com/hashicorp/terraform ${GOPATH}/src/github.com/hashicorp/terraform
$ cd ${GOPATH}/src/github.com/hashicorp/terraform
$ make updatedeps
$ make dev
$ terraform -version
Terraform v0.4.0-dev (da7f307e5696c640612173368b8faa4bc68e511a)
```

### どうやって作るか
基本的には以下の流れで進める。

* 作業ディレクトリを作成し、そこに定義ファイル群を配置
* `terraform plan` で構築の確認
* `terraform apply` で構築実行
* `terraform show` で確認
* 破棄する場合は `terraform plan --destory` で確認
* OKなら `terraform destroy` で破壊

### 定義作成
作業用ディレクトリを作成し、そこに設定ファイルと変数用ファイルの2ファイルを作成する。
1ファイルでも可能だが、後々分割することを考えて最初からそうしてみる。

ざっくりとは `provider` を定義して、それに関連する `resource` を定義し、`variable` という変数を適宜使う流れ。詳細は[本家のドキュメント](https://www.terraform.io/docs/index.html)。

なお、設定は `.tf` 、変数は `.tfvars` という拡張子。特に変数は `.tf` だとエラーになる。
`terraform plan` コマンドで定義に問題がないか事前確認する。

```bash
$ mkdir -p ~/terraform/test
$ cd ~/terraform/test
$ vi test.tf
$ vi test.tfvars
$ terraform plan -var-file=test.ifvars
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_instance.test01
    ami:                       "" => "ami-XXXXXXXX"
    availability_zone:         "" => "ap-northeast-1X"
    ebs_block_device.#:        "" => "<computed>"
    ephemeral_block_device.#:  "" => "<computed>"
    instance_type:             "" => "t2.micro"
    key_name:                  "" => "XXXXXXXX"
    private_dns:               "" => "<computed>"
    private_ip:                "" => "X.X.X.X"
    public_dns:                "" => "<computed>"
    public_ip:                 "" => "<computed>"
    root_block_device.#:       "" => "<computed>"
    security_groups.#:         "" => "1"
    security_groups.348470369: "" => "sg-XXXXXXXX"
    subnet_id:                 "" => "subnet-XXXXXXXX"
    tags.#:                    "" => "1"
    tags.Name:                 "" => "TAG_NAME"
    tenancy:                   "" => "<computed>"

```

#### 設定ファイル
変数を宣言し、AWS用の `provider` とEC2インスタンス用の `resource` をそれぞれ定義。
`resource` は `resource TYPE NAME` という感じで、`NAME` は任意。但し、変数化は無理っぽい。

```test.tf
variable "access_key" {}
variable "secret_key" {}
variable "key_path" {}
variable "key_name" {}
variable "region" {}

variable "ami" {}
variable "type" {}
variable "name" {}
variable "az" {}
variable "sg" {}
variable "subnet" {}
variable "ip" {}


provider "aws" {
    access_key = "${var.access_key}"
    secret_key = "${var.secret_key}"
    region = "${var.region}"
}

resource "aws_instance" "test01" {
    ami = "${var.ami}"
    instance_type = "${var.type}"
    availability_zone = "${var.az}"
    security_groups = ["${var.sg}"]
    subnet_id = "${var.subnet}"
    private_ip = "${var.ip}"
    key_name = "${var.key_name}"
    tags {
        Name = "${var.name}"
    }
}
```

#### 変数用ファイル
設定ファイル内で宣言した変数に代入する値を記載。
拡張子は `.tfvars` でないと引数で指定してもエラーとなる。

```test.tfvars
access_key = "XXXXXXXX"
secret_key = "XXXXXXXX"
key_path = "/path/to/key.pem"
key_name = "XXXXXXXX"
region = "ap-northeast-1"
ami = "ami-XXXXXXXX"
type = "t2.micro"
name = "TAG_NAME"
az = "ap-northeast-1X"
sg = "sg-XXXXXXXX"
subnet = "subnet-XXXXXXXX"
ip = "X.X.X.X"
```


変数の値は実行時に与えるのが基本的な考え方っぽい。
なので、設定内では変数宣言のみで、実際の値を変数用ファイルとして用意しておく感じ。


### 構築
`terraform plan` でエラーがなければ実際に構築する。
`-var-file` 引数で変数用ファイルを指定する。

```bash
$ terraform apply -var-file=test.tfvars
aws_instance.test01: Creating...
  ami:                       "" => "ami-XXXXXXXX"
  availability_zone:         "" => "ap-northeast-1X"
  ebs_block_device.#:        "" => "<computed>"
  ephemeral_block_device.#:  "" => "<computed>"
  instance_type:             "" => "t2.micro"
  key_name:                  "" => "XXXXXXXX"
  private_dns:               "" => "<computed>"
  private_ip:                "" => "X.X.X.X"
  public_dns:                "" => "<computed>"
  public_ip:                 "" => "<computed>"
  root_block_device.#:       "" => "<computed>"
  security_groups.#:         "" => "1"
  security_groups.348470369: "" => "sg-XXXXXXXX"
  subnet_id:                 "" => "subnet-XXXXXXXX"
  tags.#:                    "" => "1"
  tags.Name:                 "" => "TAG_NAME"
  tenancy:                   "" => "<computed>"
aws_instance.test01: Creation complete

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate
```

構築後 `terraform.tfstate` と `terraform.tfstate.backup` という2つのファイルが作成される。


### 確認
先ほど作成された `terraform.tfstate` に定義情報が出力されているが、コマンドでも確認可能。

```bash
$ terraform show
aws_instance.test01:
  id = i-XXXXXXXX
  ami = ami-XXXXXXXX
  availability_zone = ap-northeast-1X
  ebs_block_device.# = 0
  ebs_optimized = false
  ephemeral_block_device.# = 0
  instance_type = t2.micro
  key_name = XXXXXXXX
  private_dns = ip-X-X-X-X.ap-northeast-1.compute.internal
  private_ip = X.X.X.X
  public_dns =
  root_block_device.# = 1
  root_block_device.1234567890.delete_on_termination = true
  root_block_device.1234567890.iops = 0
  root_block_device.1234567890.volume_size = 8
  root_block_device.1234567890.volume_type = standard
  security_groups.# = 1
  security_groups.1234567890 = sg-XXXXXXXX
  subnet_id = subnet-XXXXXXXX
  tenancy = default

```


インスタンスIDが分かるので、実際に `aws cli` でも確認してみる。

```bash
$ aws ec2 describe-instances --instance-ids i-XXXXXXXX --query 'Reservations[].Instances[].{ID:InstanceId,IP:PrivateIpAddress,TYPE:InstanceType,SUBNET:SubnetId,SG:SecurityGroups[0].GroupId,NAME:Tags[?Key==`Name`].Value|[0]}[0]'

{
    "SUBNET": "subnet-XXXXXXXX",
    "NAME": "TAG_NAME",
    "IP": "X.X.X.X",
    "TYPE": "t2.micro",
    "SG": "sg-XXXXXXXX",
    "ID": "i-XXXXXXXX"
}
```


### 破壊
まずは破壊対象を確認。
`terraform plan` に `-destroy` 引数を付与すればOK。

```bash
$ terraform plan -destroy -var-file=test.tfvars
Refreshing Terraform state prior to plan...

aws_instance.test01: Refreshing state... (ID: i-XXXXXXXX)

The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

- aws_instance.test01

```

問題なければ破壊。
本当にやっちゃって良いか確認を求められるので `yes` と入力。

```bash
$ terraform destroy -var-file=test.tfvars
Do you really want to destroy?
  Terraform will delete all your managed infrastructure.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_instance.test01: Refreshing state... (ID: i-XXXXXXXX)
aws_instance.test01: Destroying...
aws_instance.test01: Destruction complete

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
```

本当にいなくなったことを確認。

```bash
$ terraform show

$ aws ec2 describe-instances --instance-ids i-XXXXXXXX --query 'Reservations[].Instances[].[State.Name][]'
[
    "terminated"
]
```


以上。