TerraformでCloudStackのVMに接続するまで


# はじめに
## 今回やること
CloudStack(IDCFクラウド)に対して以下をTerraformコマンド一発で行えるようにすること。

- VM作成
- PublicIPアドレスの取得
- PortForwardingの設定
- Firewallの設定

上記を行うことでVMに対して`ssh`でログインできるようになります。

## モチベーション
仕事柄CloudStack上でVMを作成して簡単なツールの検証環境などを構築したりすることが多いです。　　
今までは自分GUI上やAPI上から、操作していたが、検証環境のマシンスペックやOSは大体同じなので、コマンド一発でやりたいなというモチベーションです。

## 備考
HashiCorp社が開発している `Terraform` を ~~使った~~使おうとしたのですが、そのままではIDCFクラウドの `CloudStack` では動かなかったで、今回はterraformとそこで利用しているCloudStackのライブラリをforkして利用しました。

> terraformの方の修正の一部は試験を書いたらPullRequestを出すつもりです。
ただCloudStackのライブラリについてはCloudStackの開発元とVersionによっては既存の実装のままで正しく動作する、というやり取りがGithub上でされており、マージされる可能性が低いのでPullRequestは出さない予定です。

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

# Terraformとは
https://www.terraform.io/intro/

ここを見るとインフラをコードで管理して状態を管理するためのツールらしい。
`状態` を管理するので、例えばインフラの設定を変更したらその変更部分だけを適用してくれる感じかな。

# 手順
## 環境
- OSX: 10.11.1
- Go: 1.7.3

## インストール
### GOPATHの確認
```
$ env | grep GOPATH
GOPATH=/Users/aueno/.go
$ 
```

### ソースの配置
以下のコマンドでビルド対象のソースを配置するディクレトリを作成しておく。  

```
$ mkdir -p $GOPATH/src/github.com/akito1986
```

以下でビルド対象のソースを `git clone` してくる。

```
$ cd $GOPATH/src/github.com/akito1986
$ git clone https://github.com/akito1986/terraform.git
```
 
### ビルド
ビルドする前に依存するライブラリを取得しておく。

```
$ cd terraform/
$ go get -u
$ ls -1 $GOPATH/src/github.com
akito1986/
hashicorp/
$ 
```

ライブラリを取得後は以下のコマンドでビルドする。
ビルド後は`-o`で指定されたファイル名を実行するとコマンドシンタックスが表示されます。

```
$ go build -o /usr/local/bin/terraform
$ /usr/local/bin/terraform
Usage: terraform [--version] [--help] <command> [args]

The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you're just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.

Common commands:
    apply              Builds or changes infrastructure
    console            Interactive console for Terraform interpolations
    destroy            Destroy Terraform-managed infrastructure
    fmt                Rewrites config files to canonical format
    get                Download and install modules for the configuration
    graph              Create a visual graph of Terraform resources
    import             Import existing infrastructure into Terraform
    init               Initializes Terraform configuration from a module
    output             Read an output from a state file
    plan               Generate and show an execution plan
    push               Upload this Terraform module to Atlas to run
    refresh            Update local state file against real resources
    remote             Configure remote state storage
    show               Inspect Terraform state or plan
    taint              Manually mark a resource for recreation
    untaint            Manually unmark a resource as tainted
    validate           Validates the Terraform files
    version            Prints the Terraform version

All other commands:
    state              Advanced state management
$
```

上記のように表示されれば正常にビルドされています。

## 設定
Terraformは`HCL`か`Json`でインフラを管理するらしいので、ここでは`HCL`設定することにしました。

### HCL(Hashicorp Configuration Language)
書き方は以下が参考になると思います。
http://dev.classmethod.jp/cloud/terraform-getting-started-with-aws/

以下のように記載できるらしいです。

```
// comment_1
# comment_2

variable "uri" {
  default = "https://compute.jp-east.idcfcloud.com/client/api"
}

variable "api_key" {} // TF_VAR_xxxの形式で環境変数を参照しようとする。
variable "secret_key" {} // 値はTF_VAR_secret_keyという環境変数を参照する。

provider "cloudstack" {                                                                     
  api_url = "${var.uri}" // 変数uriの参照
  api_key = "${var.api_key}"
  secret_key = "${var.secret_key}"
}

/* A multi
   line comment. */
resource "cloudstack_instance" "vm_1" {
  name = "terraform-test-1"
  service_offering= "light.S1" // nameで指定できるのが良いですよね。
  network_id = "${var.network_id}"
  template = "CentOS 7.2 64-bit"
  zone = "pascal"
  keypair = "aueno"
}

resource "cloudstack_port_forward" "pf_ssh" {
  ip_address_id = "${cloudstack_ipaddress.public_ipaddress.id}"
  forward {
    protocol = "tcp"
    private_port = 22
    public_port = 10022
    virtual_machine_id = "${cloudstack_instance.vm.id}" // 他のリソースの変数は「resource種別.resource名.属性名」で取得できるようです。
}
...
```

以下変数についての自分なりの理解です。

- provider: どのプラグインを利用するのかを記載する。`CloudStack`のリソースを操作する場合は`cloudstack`と記載
- resource: 操作対象のリソースを記載。第一引数に操作対象のリソース種別、第二引数にそのリソースの名前を記載します。

### CloudStackの設定
https://www.terraform.io/docs/providers/cloudstack/index.html
を参考に以下のように記載する。
今回やりたいことは以下なので、

- VM作成
- Firewall設定
- PortForwarding設定

なので必要な`cloudstack`のリソースは以下となります。

- instance
- ipaddress
- port_forward
- firewall

#### 設定ファイル作成
以下のようなファイルを適当なディレクトリに保存します。
terraformはカレントディレクトリの`.tf`という拡張子を見て設定ファイルかを判断するらしいので末尾は `.tf` である必要があります。

```cloudstack.tf
variable myip {}
variable api_key {}
variable secret_key {}
variable network_id { default = "957483e1-7e8f-4b0a-849b-84425e582130" }
provider "cloudstack" {
  api_url = "https://compute.jp-east.idcfcloud.com/client/api"
  api_key = "${var.api_key}"
  secret_key = "${var.secret_key}"
}

resource "cloudstack_instance" "vm" {
  name = "terraform-test-1"
  service_offering= "light.S1"
  network_id = "${var.network_id}"
  template = "CentOS 7.2 64-bit"
  zone = "pascal"
  keypair = "aueno"
}

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

resource "cloudstack_port_forward" "pf_ssh" {
  ip_address_id = "${cloudstack_ipaddress.public_ipaddress.id}"
  forward {
    protocol = "tcp"
    private_port = 22
    public_port = 22
    virtual_machine_id = "${cloudstack_instance.vm.id}"
  }
}

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

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

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

上記は`terraform-test-1`(VM)を作成して、`public_ipaddress`(public ipaddress)を取得して、`public_ipaddress`の22番portを`terraform-test-1`の22番portにforwardingして、最後に`public_ipaddress`のfirewallについて指定した`myip`アドレスから1-65535ポートまで接続を許可する設定となります。


## 実行
以下のコマンドでTerraformで何をするのかを事前に確認することができます。
api_keyとsecret_key、myipについてはそれぞれ、IDCFクラウドのAPIKey、SecretKey、許可したIPアドレスに読み替えてください。

```
$ TF_VAR_api_key=xxxx TF_VAR_secret_key=yyyy TF_VAR_myip=zzz.zzz.zzz.zzz 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.myip
    ip_address_id:                        "<computed>"
    managed:                              "false"
    parallelism:                          "2"
    rule.#:                               "1"
    rule.1438695593.cidr_list.#:          "1"
    rule.1438695593.cidr_list.3084607623: "126.112.54.50/32"
    rule.1438695593.icmp_code:            "<computed>"
    rule.1438695593.icmp_type:            "<computed>"
    rule.1438695593.ports.#:              "1"
    rule.1438695593.ports.1101440173:     "1-65535"
    rule.1438695593.protocol:             "tcp"
    rule.1438695593.uuids.%:              "<computed>"

+ cloudstack_instance.vm
    display_name:     "<computed>"
    expunge:          "false"
    group:            "<computed>"
    ip_address:       "<computed>"
    keypair:          "aueno"
    name:             "terraform-test-1"
    network_id:       "957483e1-7e8f-4b0a-849b-84425e582130"
    project:          "<computed>"
    service_offering: "light.S1"
    template:         "CentOS 7.2 64-bit"
    zone:             "pascal"

+ cloudstack_ipaddress.public_ipaddress
    ip_address: "<computed>"
    network_id: "957483e1-7e8f-4b0a-849b-84425e582130"
    project:    "<computed>"
    zone:       "pascal"

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


Plan: 4 to add, 0 to change, 0 to destroy.
$
```

4つのリソースが追加されることがわかります。

`apply`コマンドで実際に適用します。

```
$ TF_VAR_api_key=xxxx TF_VAR_secret_key=yyyy TF_VAR_myip=zzz.zzz.zzz.zzz terraform apply
cloudstack_ipaddress.public_ipaddress: Refreshing state... (ID: fb5fc420-0cbb-4599-be01-feb9766f0587)
cloudstack_firewall.myip: Refreshing state... (ID: fb5fc420-0cbb-4599-be01-feb9766f0587)
cloudstack_instance.vm: Creating...
  display_name:     "" => "<computed>"
  expunge:          "" => "false"
  group:            "" => "<computed>"
  ip_address:       "" => "<computed>"
  keypair:          "" => "aueno"
  name:             "" => "terraform-test-1"
  network_id:       "" => "957483e1-7e8f-4b0a-849b-84425e582130"
  project:          "" => "<computed>"
  service_offering: "" => "light.S1"
  template:         "" => "CentOS 7.2 64-bit"
  zone:             "" => "pascal"
.....
cloudstack_instance.vm: Creation complete
cloudstack_port_forward.pf_ssh: Creating...
  forward.#:                             "" => "1"
  forward.2467112059.private_port:       "" => "22"
  forward.2467112059.protocol:           "" => "tcp"
  forward.2467112059.public_port:        "" => "22"
  forward.2467112059.uuid:               "" => "<computed>"
  forward.2467112059.virtual_machine_id: "" => "zzzzzzzzzzz"
  ip_address_id:                         "" => "fb5fc420-0cbb-4599-be01-feb9766f0587"
  managed:                               "" => "false"
cloudstack_port_forward.pf_ssh: Creation complete

Apply complete! Resources: 2 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 = sss.sss.sss.sss // 実際のpublic ipaddressが表示される
$ ssh root@sss.sss.sss.sss
The authenticity of host 'sss.sss.sss.sss (sss.sss.sss.sss)' can't be established.
ECDSA key fingerprint is SHA256:BN7g57iY4sfhMyiDOMbmSi+fBS19/vxRH8rd36X2h0M.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'sss.sss.sss.sss' (ECDSA) to the list of known hosts.
    ________  ______   ______                 __  _          
   /  _/ __ \/ ____/  / ____/________  ____  / /_(_)__  _____
   / // / / / /      / /_  / ___/ __ \/ __ \/ __/ / _ \/ ___/
 _/ // /_/ / /___   / __/ / /  / /_/ / / / / /_/ /  __/ /    
/___/_____/\____/  /_/   /_/   \____/_/ /_/\__/_/\___/_/     
                                                             
[root@terraform-test-1 ~]#
```

ログインできるところまで設定されていることがわかります。
これでいちいちクライアントツールでちまちま設定したり、ブラウザ立ち上げてポチポチしなくても良いですね:smile:
より自分がやりたいことに専念できそうです。

# 補足
## 現在の設定の確認
`show`コマンドで確認することができます。

```
$ TF_VAR_api_key=xxxx TF_VAR_secret_key=yyyy TF_VAR_myip=zzz.zzz.zzz.zzz 
cloudstack_firewall.myip:
  id = fb5fc420-0cbb-4599-be01-feb9766f0587
  ip_address_id = fb5fc420-0cbb-4599-be01-feb9766f0587
  managed = false
  parallelism = 2
  rule.# = 1
  rule.1438695593.cidr_list.# = 1
  rule.1438695593.cidr_list.3084607623 = zzz.zzz.zzz.zzz/32
  rule.1438695593.icmp_code = 0
  rule.1438695593.icmp_type = 0
  rule.1438695593.ports.# = 1
  rule.1438695593.ports.1101440173 = 1-65535
  rule.1438695593.protocol = tcp
  rule.1438695593.uuids.% = 1
  rule.1438695593.uuids.1-65535 = 67f70c34-d1de-4b49-99f5-e3df7ffbf2b7
cloudstack_instance.vm:
  id = c67aa47e-e928-4302-b665-b50edd455720
  display_name = terraform-test-1
  expunge = false
  group = 
  ip_address = 10.6.1.5
  keypair = aueno
  name = terraform-test-1
  network_id = 957483e1-7e8f-4b0a-849b-84425e582130
  project = 
  service_offering = light.S1
  template = CentOS 7.2 64-bit
  zone = pascal
cloudstack_ipaddress.public_ipaddress:
  id = fb5fc420-0cbb-4599-be01-feb9766f0587
  ip_address = 210.140.78.47
  network_id = 957483e1-7e8f-4b0a-849b-84425e582130
  project = 
  zone = pascal
cloudstack_port_forward.pf_ssh:
  id = fb5fc420-0cbb-4599-be01-feb9766f0587
  forward.# = 1
  forward.2467112059.private_port = 22
  forward.2467112059.protocol = tcp
  forward.2467112059.public_port = 22
  forward.2467112059.uuid = 41792d57-e6ea-40cc-8fef-8506ad54375b
  forward.2467112059.virtual_machine_id = c67aa47e-e928-4302-b665-b50edd455720
  ip_address_id = fb5fc420-0cbb-4599-be01-feb9766f0587
  managed = false


Outputs:

public_ipaddress = sss.sss.sss.sss
```

## 再実行のときの動作
`plan`コマンドで確認してみます。

```
$ TF_VAR_api_key=xxxx TF_VAR_secret_key=yyyy TF_VAR_myip=zzz.zzz.zzz.zzz 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.

cloudstack_instance.vm: Refreshing state... (ID: c67aa47e-e928-4302-b665-b50edd455720)
cloudstack_ipaddress.public_ipaddress: Refreshing state... (ID: fb5fc420-0cbb-4599-be01-feb9766f0587)
cloudstack_firewall.myip: Refreshing state... (ID: fb5fc420-0cbb-4599-be01-feb9766f0587)
cloudstack_port_forward.pf_ssh: Refreshing state... (ID: fb5fc420-0cbb-4599-be01-feb9766f0587)

No changes. Infrastructure is up-to-date. This means that Terraform
could not detect any differences between your configuration and
the real physical resources that exist. As a result, Terraform
doesn't need to do anything.
$ 
```

設定がされている状態ではterraformは何もしないことがわかります。

# おわりに
おしまい。