はじめまして@atsakiです。今回はTerraformを使用してCloudStackで使う方法を紹介します。
この記事はApache CloudStack Advent Calendar 2014の12/21のエントリです。
昨日は@shida1234さんの素人がCloudStackをインストールしてみた感想でした。
明日は@oracchaさんのAIST Super Green CloudにおけるCloudStack運用話です。
2015/09/02 組み込みのCloudStack Providerを使うよう更新しました
Terraformとは
Terraformは、インフラの構築、変更、バージョン管理を行うためのツールです。基本的にはクラウド上に構築されるインフラを対象としており、AWS, DigitalOcean, Google Cloud Platformなどに対応しています。
特にIaaS専用のツールではなく、追加・削除・更新といった操作が定義できるリソースであれば管理することができます。実際にHerokuやConsulなどといったIaaS以外のサービス・ツールにも対応しています。
Vagrantなどのツールで有名なHashiCorpにより開発されています。
使用例
Terraformの概要について使用例を用いて説明します。
環境
- Mac OSX 10.10.1
- Terraform 0.6.3
- IDCFクラウド
インストール
Terraformはバイナリで配布されておりリンク先からダウンロードして解凍するだけで使えます。
プロバイダの設定
TerraformからCloudStack APIを使用するために必要な情報を設定します。
CloudStack providerに使用するCloudStackのエンドポイント、APIキー、シークレットキーを設定します。
ここではIDCFクラウドを使用する際の設定ファイルの例を示します。
providers.tf
というファイルを以下の内容で作成します。APIキー、シークレットキーもエンドポイント同様にproviders.tf
に直書きすることもできますが、ここでは``terraform.tfvars```という別ファイルに書いた値を参照するようにしています。このように公開してはまずい情報を別のファイルに分けておくことで認証情報を誤ってバージョン管理システムにコミットしてしまうことを防ぐことができます。
Terraformはデフォルトで仮想マシンをデプロイするAPIをPOSTメソッドで実行しますが、現状IDCFクラウドではPOSTメソッドに対応していません。IDCFクラウドを使用する際にはhttp_get_only = true
を指定し全てのAPIをGETメソッドで実行するようにしておきます。
variable "api_key" {}
variable "secret_key" {}
provider "cloudstack" {
api_url = "https://compute.jp-east.idcfcloud.com/client/api"
api_key = "${var.api_key}"
secret_key = "${var.secret_key}"
http_get_only = true
}
api_key = "YOUR_API_KEY"
secret_key = "YOUR_SECRET_KEY"
リソースの作成
Terraformでは仮想マシン、ディスク等をリソースとして扱います。
CloudStackで使用可能なリソースはリンク先を参照してください。
Provider: CloudStack - Terraform by HashiCorp
例としてcloudstack_instance
を使って仮想マシンを作成してみます。
以下の内容でterraform.tf
ファイルを作成します。ゾーン、サービスオファリング、テンプレートの情報はお使いの環境に合わせて下さい。
resource "cloudstack_instance" "vm01" {
name = "vm01"
display_name = "vm01"
zone = "tesla"
service_offering = "light.S1"
network = "network1"
template = "Ubuntu Server 14.04 LTS 64-bit"
expunge = true
}
これで仮想マシンを作成する準備が整いました。
ここで、teraform plan
コマンドを使用し実行計画を表示させてみます。
実行計画を確認してから実際に変更を反映させるという流れはTerraformの特徴の1つです。
terraform plan
コマンドを実行するとリソースにどのような変更がなされるか表示されます。(以下の例は見易さのため出力の一部を省略しています)
1行目の左にある+
はリソースが新規に作成されることを示しています。
<computed>
となっているのはリソース作成の際に値が決定されることを表しています。CloudStackの仮想マシンのIPは仮想マシン作成時に払い出されるためipaddressが<computed>
となっています。
$ terraform plan
+ cloudstack_instance.vm01
display_name: "" => "vm01"
expunge: "" => "1"
ipaddress: "" => "<computed>"
name: "" => "vm01"
network: "" => "network1"
service_offering: "" => "light.S1"
template: "" => "Ubuntu Server 14.04 LTS 64-bit"
zone: "" => "tesla"
内容に問題が無いことを確認したら、terraform apply
コマンドを実行すると実際に仮想マシンの作成が始まります。
完了したらterraform show
コマンドで作成された仮想マシンの情報をみることができます。<computed>
となっていたipaddressに値(例では10.3.0.39
)が入っていることが確認できます。
$ terraform apply
$ terraform show
cloudstack_instance.vm01:
id = 9a0802ad-a509-4774-a0df-18917daa662b
display_name = vm01
expunge = true
ipaddress = 10.3.0.39
name = vm01
network = network1
project =
service_offering = light.S1
template = Ubuntu Server 14.04 LTS 64-bit
zone = tesla
terraform show
の出力にはterrafrom plan
には含まれていなかったid
が含まれています。Terraformは作成したリソースのIDを記録しており、次回実行時にはそのリソースに対して操作が実行されます。
Terraformが作成しリソースの情報は同じディレクトリに作成されているterraform.tfstate
という名前のファイルに記録されています。
$ cat terraform.tfstate
{
"version": 1,
"serial": 0,
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {
"cloudstack_instance.vm01": {
"type": "cloudstack_instance",
"primary": {
"id": "9a0802ad-a509-4774-a0df-18917daa662b",
"attributes": {
"display_name": "vm01",
"expunge": "true",
"id": "9a0802ad-a509-4774-a0df-18917daa662b",
"ipaddress": "10.3.0.39",
"name": "vm01",
"network": "network1",
"project": "",
"service_offering": "light.S1",
"template": "Ubuntu Server 14.04 LTS 64-bit",
"zone": "tesla"
}
}
}
}
}
]
}
リソースの設定変更
Terraformでは設定ファイルを書き換えることで、実際のリソースに変更を反映させることができます。
ここでは作成した仮想マシンの表示名を変更してみます。
terraform.tf
のdisplay_name
をvm01
から VM01
に変更します。
resource "cloudstack_instance" "vm01" {
name = "vm01"
display_name = "VM01"
zone = "tesla"
service_offering = "light.S1"
network = "network1"
template = "Ubuntu Server 14.04 LTS 64-bit"
}
terraform plan
を実行すると、以下のような出力が得られます。仮想マシン作成前のterraform plan
では+
であった一行目のマークが~
に変わっています。このマークはリソースに変更が加えられることを示しています。
~ cloudstack_instance.vm01
display_name: "vm01" => "VM01"
terraform apply
で変更を反映させ、terraform show
で確認するとdisplay_name
が変更されていることが確認できます。
$ terraform apply
$ terraform show
cloudstack_instance.vm01:
id = 9a0802ad-a509-4774-a0df-18917daa662b
display_name = VM01
expunge = true
ipaddress = 10.3.0.39
name = vm01
network = network1
project =
service_offering = light.S1
template = Ubuntu Server 14.04 LTS 64-bit
zone = tesla
次に、nameをVM01に変えてみます。
resource "cloudstack_instance" "vm01" {
name = "VM01"
display_name = "VM01"
zone = "tesla"
service_offering = "light.S1"
network = "network1"
template = "Ubuntu Server 14.04 LTS 64-bit"
}
terraform plan
で実行計画を確認するとdisplay_name
を変更したときと異なり一行目のマークが-/+
になっています。このマークは一度既存のリソースを削除し、新規にリソースを作成することを示しています。CloudStackでは仮想マシンのname
を変更することができず、再作成が必要なためこのような動きとなります。
また、仮想マシン作成時にIPが変わるためipaddressは再び<computed>
になっています。
-/+ cloudstack_instance.vm01
display_name: "VM01" => "VM01"
expunge: "true" => "1"
ipaddress: "10.3.0.39" => "<computed>"
name: "vm01" => "VM01" (forces new resource)
network: "network1" => "network1"
service_offering: "light.S1" => "light.S1"
template: "Ubuntu Server 14.04 LTS 64-bit" => "Ubuntu Server 14.04 LTS 64-bit"
zone: "tesla" => "tesla"
terraform apply
を実行すると、仮想マシンが一旦削除されてから再作成されます。
再作成完了後にterraform show
で情報を確認するとid
, ipaddress
が変わっていることが確認できます。
$ terraform apply
$ terraform show
cloudstack_instance.vm01:
id = 68dace58-46f5-4b50-a242-fa4858c15b1f
display_name = VM01
expunge = true
ipaddress = 10.3.0.165
name = VM01
network = network1
project =
service_offering = light.S1
template = Ubuntu Server 14.04 LTS 64-bit
zone = tesla
リソースの追加
次にこの仮想マシンに1つディスクを追加してみます。terraform.tf
に以下の内容を追記します。
disk01
という名前で10Gのディスクを作成し、先ほど作成した仮想マシンへアタッチします。
Terraformでは作成したリソースの情報を変数を用いて参照することができます。ここではディスクをアタッチする仮想マシンのidをcloudstack_instance.vm01.id
で参照しています。
あるリソースから別のリソースの情報を変数を用いて参照している場合、そのリソースは参照先のリソースに依存することになります。今回の例ではディスク(disk01)が仮想マシン(vm01)に依存しています。Terraformではこのように変数を用いることで簡単にリソース間の依存関係を示すことができます。
resource "cloudstack_disk" "disk01" {
name = "disk01"
disk_offering = "Custom Disk"
size = 10
attach = true
virtual_machine = "${cloudstack_instance.vm01.id}"
zone = "${cloudstack_instance.vm01.zone}"
}
terraform plan
を実行するとディスクが新規に作成されることが分かります。
+ cloudstack_disk.disk01
attach: "" => "1"
device: "" => "<computed>"
disk_offering: "" => "Custom Disk"
name: "" => "disk01"
shrink_ok: "" => "0"
size: "" => "10"
virtual_machine: "" => "68dace58-46f5-4b50-a242-fa4858c15b1f"
zone: "" => "tesla"
terraform apply
, terraform show
を実行すると実際にディスクが作成され仮想マシンにアタッチされたことが確認できます。
$ terraform apply
$ terraform show
cloudstack_disk.disk01:
id = d2cb73d4-bafa-4d43-beb2-1a8dbc850522
attach = true
device = /dev/xvdb
disk_offering = Custom Disk
name = disk01
shrink_ok = false
size = 10
virtual_machine = 68dace58-46f5-4b50-a242-fa4858c15b1f
zone = tesla
cloudstack_instance.vm01:
id = 68dace58-46f5-4b50-a242-fa4858c15b1f
display_name = VM01
expunge = true
ipaddress = 10.3.0.165
name = VM01
network = network1
project =
service_offering = light.S1
template = Ubuntu Server 14.04 LTS 64-bit
zone = tesla
依存関係にあるリソースの設定変更
この状態で仮想マシンのname
を変えてみます。name
を変更した場合は仮想マシンは再作成されますが、このとき仮想マシンと依存関係にあるディスクはどうなるのでしょうか?
仮想マシンのname
をVM01
から vm01
に戻します。
resource "cloudstack_instance" "vm01" {
name = "vm01"
display_name = "VM01"
zone = "tesla"
service_offering = "light.S1"
network = "network1"
template = "Ubuntu Server 14.04 LTS 64-bit"
}
resource "cloudstack_disk" "disk01" {
name = "disk01"
disk_offering = "Custom Disk"
size = 10
attach = true
virtual_machine = "${cloudstack_instance.vm01.name}"
zone = "${cloudstack_instance.vm01.zone}"
}
terraform plan
で実行計画を確認すると、仮想マシンが再作成されることが確認できます。ディスクは新規に作成される仮想マシンにアタッチされるようです。これは期待通りの動作といえるでしょう。
~ cloudstack_disk.disk01
virtual_machine: "68dace58-46f5-4b50-a242-fa4858c15b1f" => "${cloudstack_instance.vm01.id}"
-/+ cloudstack_instance.vm01
display_name: "VM01" => "VM01"
expunge: "true" => "1"
ipaddress: "10.3.0.165" => "<computed>"
name: "VM01" => "vm01" (forces new resource)
network: "network1" => "network1"
service_offering: "light.S1" => "light.S1"
template: "Ubuntu Server 14.04 LTS 64-bit" => "Ubuntu Server 14.04 LTS 64-bit"
zone: "tesla" => "tesla"
apply, showを実行すると仮想マシンは再作成されidが変わっているのに対し、ディスクはidに変化がなく新規に作成された仮想マシンに再アタッチされていることが確認できます。
$ terraform apply
$ terraform show
cloudstack_disk.disk01:
id = d2cb73d4-bafa-4d43-beb2-1a8dbc850522
attach = true
device = /dev/xvdb
disk_offering = Custom Disk
name = disk01
shrink_ok = false
size = 10
virtual_machine = bb7c017f-01c0-4703-9224-6566b92c1b58
zone = tesla
cloudstack_instance.vm01:
id = bb7c017f-01c0-4703-9224-6566b92c1b58
display_name = VM01
expunge = true
ipaddress = 10.3.0.228
name = vm01
network = network1
project =
service_offering = light.S1
template = Ubuntu Server 14.04 LTS 64-bit
zone = tesla
リソースの削除
Terraformでは設定ファイルからリソースを消して、terraform apply
を実行することでリソースを削除することができます。
単純にファイルから削除しても良いですが、コメントアウトすることも可能です。Terraformの設定ファイルでは/* 〜 */
を使ってコメントを表すことができます。(1行コメントでは#
、//
も使用可)
resource "cloudstack_instance" "vm01" {
name = "vm01"
display_name = "VM01"
zone = "tesla"
service_offering = "light.S1"
network = "network1"
template = "Ubuntu Server 14.04 LTS 64-bit"
}
/*
resource "cloudstack_disk" "disk01" {
name = "disk01"
disk_offering = "Custom Disk"
size = 10
attach = true
virtual_machine = "${cloudstack_instance.vm01.name}"
zone = "${cloudstack_instance.vm01.zone}"
}
*/
この状態でplanを確認すると以下のような結果となります。-
はリソースが削除されること示すマークです。terraform apply
で削除が実行されます。
- cloudstack_disk.disk01
全てのリソースをまとめて削除したい場合にはterraform destroy
コマンドで削除を実行することができます。デフォルトでは削除前に確認のメッセージが表示されます。
terraform destroy
の実行計画はterraform plan
コマンドに-destroy
オプションをつけることで確認可能です。
$ terraform plan -destroy
(略)
- cloudstack_disk.disk01
- cloudstack_instance.vm01
$ terraform destroy
プロバイダー・リソースの使い方を調べる
プロバイダーやリソースの使い方はドキュメントにも記載がありますが、ソースコードを調べたほうが早いこともあります。
ここではソースコードからリソースの使い方について調べる方法を紹介します。
TerraformのCloudStackプロバイダー関連のファイルは builtin/providers/cloudstack
にあります。
まず、provider.goファイルを確認します。このファイルをみるとプロバイダを使用するために設定する必要がある値やプロバイダが提供しているリソースが分かります。
プロバイダを使用するために設定する必要がある値は、providerConfigure
関数を確認することで分かります。api_url
, api_key
, secret_key
を設定する必要があることがわかります。
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
APIURL: d.Get("api_url").(string),
APIKey: d.Get("api_key").(string),
SecretKey: d.Get("secret_key").(string),
HTTPGETOnly: d.Get("http_get_only").(bool),
Timeout: int64(d.Get("timeout").(int)),
}
return config.NewClient()
}
次にCloudStackプロバイダが提供しているリソースについて調べてみます。provider.go
の中の以下の部分が使用可能なリソースとなります。ロードバランサやセキュリティグループなどについてはまだ対応できていなさそうということも分かります。
ResourcesMap: map[string]*schema.Resource{
"cloudstack_disk": resourceCloudStackDisk(),
"cloudstack_egress_firewall": resourceCloudStackEgressFirewall(),
"cloudstack_firewall": resourceCloudStackFirewall(),
"cloudstack_instance": resourceCloudStackInstance(),
"cloudstack_ipaddress": resourceCloudStackIPAddress(),
"cloudstack_loadbalancer_rule": resourceCloudStackLoadBalancerRule(),
"cloudstack_network": resourceCloudStackNetwork(),
"cloudstack_network_acl": resourceCloudStackNetworkACL(),
"cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(),
"cloudstack_nic": resourceCloudStackNIC(),
"cloudstack_port_forward": resourceCloudStackPortForward(),
"cloudstack_secondary_ipaddress": resourceCloudStackSecondaryIPAddress(),
"cloudstack_ssh_keypair": resourceCloudStackSSHKeyPair(),
"cloudstack_template": resourceCloudStackTemplate(),
"cloudstack_vpc": resourceCloudStackVPC(),
"cloudstack_vpn_connection": resourceCloudStackVPNConnection(),
"cloudstack_vpn_customer_gateway": resourceCloudStackVPNCustomerGateway(),
"cloudstack_vpn_gateway": resourceCloudStackVPNGateway(),
},
最後にそれぞれのリソースで設定できる項目を確認します。builtin/providers/cloudstack
の中にprovider.go
で調べたリソースと対応するファイルがあるので、その中身を確認します。
今回はcloudstack_instance
を調べて見ます。このリソースに対応しているファイルはresource_cloudstack_instance.go です。
以下の部分に注目すると、例で設定をしたname
やservice_offering
などといった文字列を確認することができます。今回の例では使用しませんでしたがuser_data
なども設定できることがわかります。
func resourceCloudStackInstance() *schema.Resource {
return &schema.Resource{
Create: resourceCloudStackInstanceCreate,
Read: resourceCloudStackInstanceRead,
Update: resourceCloudStackInstanceUpdate,
Delete: resourceCloudStackInstanceDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"display_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"service_offering": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"network": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"ipaddress": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"template": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"zone": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"keypair": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"user_data": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
StateFunc: func(v interface{}) string {
switch v.(type) {
case string:
hash := sha1.Sum([]byte(v.(string)))
return hex.EncodeToString(hash[:])
default:
return ""
}
},
},
"expunge": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
}
}
各設定項目にはデータの型がTypeというキーワードで指定されています。nameのTypeはTypeStringとなっており文字列であることがわかります。expungeのTypeはTypeBoolでありブール値ことが分かります。
Required, Optionalはその項目が必須かオプションであるか示しています。Requiredの項目を設定していないとapplyコマンドなどが実行できません。
ComputedはTerraformが値を取得することを示しています。ipaddressのようにリソース作成時に払い出されるあたいなどに使用します。
ForceNewはその項目を変更した場合リソースを削除し再作成することを示しています。例えば、display_nameはForceNewがついていないので、再作成することなく変更が可能ですが、nameはForceNewがついているので変更した場合は再作成されます。
より複雑なデータの型もあったりしますが基本的にはこのくらい覚えておけば使うのにそれほど不自由しないと思います。それぞれのリソースを使った場合裏で何をしているかについてもソースコード中でAPIのコマンド名を探せばなんとなく分かると思います。
他のツールとの比較
Terraformの使い方を見てきましたがVagrantやChef、Ansibleといった構成管理ツールの違いが分かりづらいかもしれません。実際にそれらのツールを使っても同じようなことを実現できる場合も多いです。しかし、これらのツールが解決しようとしている問題や得手不得手は互いに異なっています。
Vagrant
VagrantはVagrantfileという設定ファイルに仮想マシンの情報を記述することで、仮想マシンの作成・ログイン・プロビジョンなどの操作を簡単に行うためのツールです。
CloudStackのプラグイン(vagrant-cloudstack)もあります。(vagrant-cloudstackの使い方についてはVagrantでIDCFクラウド、Cloud n、ALTUSを使ってみるなどを参照してください。)
Vagrantは仮想マシンを扱うことに特化しておりログイン(vagrant ssh
)やプロビジョンの実行(vagrant provision
)を簡単に行うコマンドが用意されています。Terraformを使って仮想マシンを作成することはできますが、これらに相当するコマンドは用意されていません。
一方でネットワークの設定を柔軟に行いたいといった場合にはTerraformの方が便利です。Vagrantのセキュリティグループやポートフォワーディングの設定機能は仮想マシンへの接続を可能にするために実装されているので、あまり柔軟に設定することはできません。Terraformでは仮想マシンへの接続に関係ない設定も可能ですし、設定ファイルを書き換えることで簡単に実際の構成を変更することもできます。
Vagrantの開発元はTerraformと同じくHashiCorpです。HashiCorpは先日AtlasというHashiCorp(Vagrant, Packer, Consul, Terraform)の製品を組み合わせてインフラを管理するサービスを発表しました。
AtlasのドキュメントはVagrant, Terraformの使いどころの参考になるので一読をオススメします。
Chef, Ansible, SaltStack, Puppetなど
これらの構成管理ツールを使用するとそれぞれの設定ファイルに従って、サーバーにインストールされているパッケージやサービスの設定などを行うことができます。CloudStackに対応していて仮想マシンの作成等が可能な場合もありますが、これらのツールから操作できる項目はかなり限られておりCloudStack上のインフラ全体を管理させるのは難しいと思われます。
Terraformではサーバー内の設定は基本的に行えないので、プロビジョニング機能を使って仮想マシン作成時に構成管理ツールを実行し設定を行うというのが一つの使い方となります。(Terraformのプロビジョニングは仮想マシン作成時にしか実行されないので注意。)
Terraformで仮想マシンを作成する際は、既に設定が投入されているイメージを用いるという手もあります。イメージの元となる仮想マシンを作成する際に構成管理ツールを用いることでイメージの構成・バージョンの管理等が効率的に行えるようになります。(試していませんがpacker-cloudstackもイメージの作成・管理に有用かもしれません。)
CloudFormation, Heat, Stackmateなど
AWSのCloudFormationやOpenStackのHeatを使用するとそれぞれのプロバイダで設定ファイルを用いたインフラの管理ができます。CloudStackではloudFormation Templateを使用可能にするStackmateというプロジェクトがあります。
Terraformとこれらのツールが解決しようとしている問題はほぼ同じですが、Terraformには複数のプロバイダを同時に使用できるという特徴があります。インフラとして複数の環境を用いている場合にはTerraformを用いることで一括で管理できるようになります。
もう一つのCloudStackプロバイダ実装について
筆者が個人的に開発している別の実装(terraform-provider-cloudstack)があります。こちらではロードバランサやセキュリティグループを使用することも可能となっています。できればビルトイン版にマージしていければと思っています。また、色々な構成でCloudStackを構築・検証したいといったケースがあるので、terraform-provider-cloudstackにはビルトインでは難しそうな管理者向けのリソース(ゾーンやオファリングなど)も実装していきたいと考えています。
まとめ
Terraformを使用することでCloudStackを含めたインフラの構成をコード化することができます。また、設定ファイルで変更した内容を実際の構成に反映させることも容易です。
まだ、実装されていない機能も多いですが、うまく使うと管理・運用を効率化できると思いますので是非試してみてください。