TerraformのModuleを利用して複数VMを簡単に作成する(CloudStack)


# やったこと
Terrafformを利用して複数VMの作成。

# 背景
[以前の記事](http://qiita.com/akito1986/items/df84e1015d46f4be7b48)に書かせてもらっているが、検証用のインフラ構築の際にブラウザやら単純なClientツールで設定するのが面倒。
Terraformなら簡単に構築できそうなので今検証中という感じ。

# 環境
- OSX: 10.11.1
- Go: 1.7.3

# 前提条件
- IDCFクラウドの以下の情報を取得していること  
  - APIKey
  - SecretKey
- Goのビルド環境があること

# 手順
## Terraformのビルド
[以前の記事](http://qiita.com/akito1986/items/df84e1015d46f4be7b48)に記載の方法でforkしたterraformをビルドする。

## Terraformの設定
Terraformでは共通化可能な設定をモジュールという単位で保持することができる。
今回は、VMの設定とポートフォワーディングの設定をモジュール化して、通常の設定ファイルから参照するようにした。

以下設定ファイルが置かれているディクレトリから見たときのディクレクトリ構造。

```
$ tree .
.
├── cloudstack.tf // 設定ファイル
├── terraform.log // log file
├── terraform.tfstatef
├── terraform.tfstate.backup
├── terraform.tfvars
└── vm_instances
    └── terraform_test // モジュールディレクトリ
        └── test.tf

2 directories, 6 files
$
```

### Moduleの作成
以下のように通常の設定ファイルの形式で設定する。

```vm_instances/terraform_test/test.tf
variable counter {}
variable public_ipaddress_id {}

resource "cloudstack_instance" "vm" {
  name = "${format("terraform-test-0%s", var.counter)}"
  service_offering = "light.S1"
  network_id = "26a035ad-3802-4622-8657-b0df6e33cb0a"
  template = "CentOS 7.2 64-bit"
  zone = "weber"
  keypair = "aueno"
  group = "terraform-test"
  expunge = true
}

resource "cloudstack_port_forward" "pf_ssh" {
  ip_address_id = "${var.public_ipaddress_id}"
  forward {
    protocol = "tcp"
    private_port = 22
    public_port = "${10022 + var.counter - 1}"
    virtual_machine_id = "${cloudstack_instance.vm.id}"
  }
}
```

変数定義されているものは、呼び出す際に渡される変数である。
`counter` はVMの名前に利用しており、`public_ipaddress_id` はポートフォワーディング設定するときに指定するものである。

## 設定ファイルの作成
今回は3台のサーバーを構築してみる。以下のような設定となる。
Moduleに渡すべき引数を設定していることがわかると思う。
またここでは先日リリースした[GPUが使えるIDCFクラウド東日本リージョン2](https://www.idcf.jp/service/deeplearning/?cl=lbnr_gpu)に対して設定を行っている。


```cloudstack.tf
variable api_key {}
variable secret_key {}
variable zone {}
variable network_id {}
variable myips {
  type = "list"
}

provider "cloudstack" {
  api_url = "https://compute.jp-east-2.idcfcloud.com/client/api"
  api_key = "${var.api_key}"
  secret_key = "${var.secret_key}"
}

module "vm_instance_1" {
  source = "./vm_instances/terraform_test"
  counter = 1
  public_ipaddress_id = "${cloudstack_ipaddress.public_ipaddress.id}"
}

module "vm_instance_2" {
  source = "./vm_instances/terraform_test"
  counter = 2
  public_ipaddress_id = "${cloudstack_ipaddress.public_ipaddress.id}"
}

module "vm_instance_3" {
  source = "./vm_instances/terraform_test"
  counter = 3
  public_ipaddress_id = "${cloudstack_ipaddress.public_ipaddress.id}"
}

resource "cloudstack_ipaddress" "public_ipaddress" {
  network_id = "${var.network_id}"
  zone = "${var.zone}"
}


resource "cloudstack_firewall" "myips" {
  ip_address_id = "${cloudstack_ipaddress.public_ipaddress.id}"

  rule {
    cidr_list = "${var.myips}"
    protocol = "tcp"
    ports = ["1-65535"]
  }
}

output "public_ipaddress" {
  value = "${cloudstack_ipaddress.public_ipaddress.ip_address}"
}
```

## 変数ファイルの作成
設定ファイルに変数の定義もすることは出来るが、少し汚くなるかなと思い今回は変数ファイルを作成した。
変数ファイルの内容は以下である。

```terraform.tfvars
network_id = "26a035ad-3802-4622-8657-b0df6e33cb0a"
zone = "weber"
myips = [
  "sss.sss.sss.ss1/32",
  "sss.sss.sss.ss2/32",
]
```

myipsに接続を許可させたいcidrを指定する。

大元の設定ファイルの同一ディレクトリに `terraform.tfvars` というファイルで作成すればオプション指定せずに読み込んでくれます。

## VM作成
### Moduleのインポート
ローカル環境のモジュールの場合もあらかじめ、`terraform get` でモジュールをインポートする必要がある。

```
$ terraform get
Get: file:///Users/aueno/workspace/test/vm_instances/terraform_test
Get: file:///Users/aueno/workspace/test/vm_instances/terraform_test
Get: file:///Users/aueno/workspace/test/vm_instances/terraform_test
$ 
```

### 事前の状態
#### VM一覧
![before_vm.png](https://qiita-image-store.s3.amazonaws.com/0/46472/2a0c56d8-cba8-7039-aa7a-a885a1433102.png "before_vm.png")


### IPアドレス
![before_ipaddress.png](https://qiita-image-store.s3.amazonaws.com/0/46472/8ec83fc2-cb89-c81b-7dfa-3974d35b11dc.png "before_ipaddress.png")


### Apply実行
さてここまでで、CloudStackを弄る準備ができました。
まずは`plan`で何をするのか確認しておきます。
APIKEY, SECRET_KEYはご自身のIDCFクラウドのAPIKey, SecretKeyを指定してください。

```
$ TF_VAR_api_key=$APIKEY TF_VAR_secret_key=$SECRET_KEY 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.


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. Cyan entries are data sources to be read.

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.

+ cloudstack_firewall.myips
    ip_address_id:                        "<computed>"
    managed:                              "false"
    parallelism:                          "2"
    rule.#:                               "1"
    rule.2149869111.cidr_list.#:          "1"
    rule.2149869111.cidr_list.1098773020: "zzz.zzz.zzz.zz/32"
    rule.2149869111.icmp_code:            "<computed>"
    rule.2149869111.icmp_type:            "<computed>"
    rule.2149869111.ports.#:              "1"
    rule.2149869111.ports.1101440173:     "1-65535"
    rule.2149869111.protocol:             "tcp"
    rule.2149869111.uuids.%:              "<computed>"

+ cloudstack_ipaddress.public_ipaddress
    ip_address: "<computed>"
    network_id: "26a035ad-3802-4622-8657-b0df6e33cb0a"
    project:    "<computed>"
    zone:       "weber"

+ module.vm_instance_1.cloudstack_instance.vm
    display_name:     "<computed>"
    expunge:          "true"
    group:            "terraform-test"
    ip_address:       "<computed>"
    keypair:          "aueno"
    name:             "terraform-test-01"
    network_id:       "26a035ad-3802-4622-8657-b0df6e33cb0a"
    project:          "<computed>"
    service_offering: "light.S1"
    template:         "CentOS 7.2 64-bit"
    zone:             "weber"

+ module.vm_instance_1.cloudstack_port_forward.pf_ssh
    forward.#:                              "1"
    forward.~3923475130.private_port:       "22"
    forward.~3923475130.protocol:           "tcp"
    forward.~3923475130.public_port:        "10022"
    forward.~3923475130.uuid:               "<computed>"
    forward.~3923475130.virtual_machine_id: "<computed>"
    ip_address_id:                          "<computed>"
    managed:                                "false"

+ module.vm_instance_2.cloudstack_instance.vm
    display_name:     "<computed>"
    expunge:          "true"
    group:            "terraform-test"
    ip_address:       "<computed>"
    keypair:          "aueno"
    name:             "terraform-test-02"
    network_id:       "26a035ad-3802-4622-8657-b0df6e33cb0a"
    project:          "<computed>"
    service_offering: "light.S1"
    template:         "CentOS 7.2 64-bit"
    zone:             "weber"

+ module.vm_instance_2.cloudstack_port_forward.pf_ssh
    forward.#:                              "1"
    forward.~1844077387.private_port:       "22"
    forward.~1844077387.protocol:           "tcp"
    forward.~1844077387.public_port:        "10023"
    forward.~1844077387.uuid:               "<computed>"
    forward.~1844077387.virtual_machine_id: "<computed>"
    ip_address_id:                          "<computed>"
    managed:                                "false"

+ module.vm_instance_3.cloudstack_instance.vm
    display_name:     "<computed>"
    expunge:          "true"
    group:            "terraform-test"
    ip_address:       "<computed>"
    keypair:          "aueno"
    name:             "terraform-test-03"
    network_id:       "26a035ad-3802-4622-8657-b0df6e33cb0a"
    project:          "<computed>"
    service_offering: "light.S1"
    template:         "CentOS 7.2 64-bit"
    zone:             "weber"

+ module.vm_instance_3.cloudstack_port_forward.pf_ssh
    forward.#:                              "1"
    forward.~1201611294.private_port:       "22"
    forward.~1201611294.protocol:           "tcp"
    forward.~1201611294.public_port:        "10024"
    forward.~1201611294.uuid:               "<computed>"
    forward.~1201611294.virtual_machine_id: "<computed>"
    ip_address_id:                          "<computed>"
    managed:                                "false"


Plan: 8 to add, 0 to change, 0 to destroy.
```

8つのリソースが追加されることがわかると思います。
以下でApply実行。

 ```
  $ TF_VAR_api_key=$APIKEY TF_VAR_secret_key=$SECRET_KEY terraform apply
cloudstack_ipaddress.public_ipaddress: Creating...
  ip_address: "" => "<computed>"
  network_id: "" => "26a035ad-3802-4622-8657-b0df6e33cb0a"
  project:    "" => "<computed>"
  zone:       "" => "weber"
module.vm_instance_2.cloudstack_instance.vm: Creating...
  display_name:     "" => "<computed>"
  expunge:          "" => "true"
  group:            "" => "terraform-test"
  ip_address:       "" => "<computed>"
  keypair:          "" => "aueno"
  name:             "" => "terraform-test-02"
  network_id:       "" => "26a035ad-3802-4622-8657-b0df6e33cb0a"
  project:          "" => "<computed>"
  service_offering: "" => "light.S1"
  template:         "" => "CentOS 7.2 64-bit"
  zone:             "" => "weber"
module.vm_instance_1.cloudstack_instance.vm: Creating...
  display_name:     "" => "<computed>"
  expunge:          "" => "true"
  group:            "" => "terraform-test"
  ip_address:       "" => "<computed>"
  keypair:          "" => "aueno"
  name:             "" => "terraform-test-01"
  network_id:       "" => "26a035ad-3802-4622-8657-b0df6e33cb0a"
  project:          "" => "<computed>"
  service_offering: "" => "light.S1"
  template:         "" => "CentOS 7.2 64-bit"
  zone:             "" => "weber"
module.vm_instance_3.cloudstack_instance.vm: Creating...
  display_name:     "" => "<computed>"
  expunge:          "" => "true"
  group:            "" => "terraform-test"
  ip_address:       "" => "<computed>"
  keypair:          "" => "aueno"
  name:             "" => "terraform-test-03"
  network_id:       "" => "26a035ad-3802-4622-8657-b0df6e33cb0a"
  project:          "" => "<computed>"
  service_offering: "" => "light.S1"
  template:         "" => "CentOS 7.2 64-bit"
  zone:             "" => "weber"
cloudstack_ipaddress.public_ipaddress: Creation complete
cloudstack_firewall.myips: Creating...
  ip_address_id:                        "" => "9692c7c9-f3ca-445a-b375-3a1ba488d886"
  managed:                              "" => "false"
  parallelism:                          "" => "2"
  rule.#:                               "" => "1"
  rule.2149869111.cidr_list.#:          "" => "1"
  rule.2149869111.cidr_list.1098773020: "" => "sss.sss.sss.sss/32"
  rule.2149869111.icmp_code:            "" => "<computed>"
  rule.2149869111.icmp_type:            "" => "<computed>"
  rule.2149869111.ports.#:              "" => "1"
  rule.2149869111.ports.1101440173:     "" => "1-65535"
  rule.2149869111.protocol:             "" => "tcp"
  rule.2149869111.uuids.%:              "" => "<computed>"
cloudstack_firewall.myips: Creation complete
module.vm_instance_1.cloudstack_instance.vm: Still creating... (10s elapsed)
module.vm_instance_2.cloudstack_instance.vm: Still creating... (10s elapsed)
module.vm_instance_3.cloudstack_instance.vm: Still creating... (10s elapsed)
module.vm_instance_2.cloudstack_instance.vm: Still creating... (20s elapsed)
module.vm_instance_1.cloudstack_instance.vm: Still creating... (20s elapsed)
module.vm_instance_3.cloudstack_instance.vm: Still creating... (20s elapsed)
module.vm_instance_3.cloudstack_instance.vm: Creation complete
module.vm_instance_3.cloudstack_port_forward.pf_ssh: Creating...
  forward.#:                             "" => "1"
  forward.1057937353.private_port:       "" => "22"
  forward.1057937353.protocol:           "" => "tcp"
  forward.1057937353.public_port:        "" => "10024"
  forward.1057937353.uuid:               "" => "<computed>"
  forward.1057937353.virtual_machine_id: "" => "597c88c6-9bcf-44bf-8166-5412af937abb"
  ip_address_id:                         "" => "9692c7c9-f3ca-445a-b375-3a1ba488d886"
  managed:                               "" => "false"
module.vm_instance_1.cloudstack_instance.vm: Creation complete
module.vm_instance_1.cloudstack_port_forward.pf_ssh: Creating...
  forward.#:                             "" => "1"
  forward.3480124144.private_port:       "" => "22"
  forward.3480124144.protocol:           "" => "tcp"
  forward.3480124144.public_port:        "" => "10022"
  forward.3480124144.uuid:               "" => "<computed>"
  forward.3480124144.virtual_machine_id: "" => "b1e8ad78-9e3c-4798-9b3d-672cf3eb4363"
  ip_address_id:                         "" => "9692c7c9-f3ca-445a-b375-3a1ba488d886"
  managed:                               "" => "false"
module.vm_instance_2.cloudstack_instance.vm: Creation complete
module.vm_instance_2.cloudstack_port_forward.pf_ssh: Creating...
  forward.#:                             "" => "1"
  forward.1508310808.private_port:       "" => "22"
  forward.1508310808.protocol:           "" => "tcp"
  forward.1508310808.public_port:        "" => "10023"
  forward.1508310808.uuid:               "" => "<computed>"
  forward.1508310808.virtual_machine_id: "" => "5a7858c1-f84a-47b0-837f-4639530f6ef9"
  ip_address_id:                         "" => "9692c7c9-f3ca-445a-b375-3a1ba488d886"
  managed:                               "" => "false"
module.vm_instance_2.cloudstack_port_forward.pf_ssh: Creation complete
module.vm_instance_3.cloudstack_port_forward.pf_ssh: Creation complete
module.vm_instance_1.cloudstack_port_forward.pf_ssh: Creation complete

Apply complete! Resources: 8 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

Outputs:

public_ipaddress = zzz.zzz.zzz.zzz
$
```

並列でVM作成などが行なわれていることがわかると思います。

### Apply後の状態
#### VM一覧
見事にVMが作成されている！
画面で見るとやった感が出ます。
![after_vm.png](https://qiita-image-store.s3.amazonaws.com/0/46472/ad7e67f6-6d58-ed8e-eea5-a1d2dc292957.png "after_vm.png")


#### IPアドレス
新しいPublicIPアドレスが取得されて、Firewallやポートフォワーディングが設定されていることがわかります。
![after_ipaddress.png](https://qiita-image-store.s3.amazonaws.com/0/46472/13c6fa8f-bab9-5ec6-fa9b-ce5dc87fa12f.png "after_ipaddress.png")


# おわりに
Terraformは`状態`を管理するものなのでそこが良いと思うし、GO製のツールなのでタスクが普通に並列化されるところが良いと思う。
ただIDCFクラウドで名前を管理するために利用しているTagの設定ができたらもっと良いかなと思うところ。
自分で実装しろよ、という話だけでも:smile: