Apache libcloudでAmazon EC2, OpenNebulaクラウドにVM作成

はじめに

正月の休みを利用して、以前から気になっていたApache libcloudを使ってみたときのメモ。libcloudは複数のクラウドAPIを統一的に扱えるPythonライブラリ。AWS EC2, S3やRackspace、OpenStack、OpenNebulaなどに対応してる。対応プロバイダ一覧はこちら。v0.6.2からCloudStackにも対応したとのことなので、研究用にも使えないかなと思っていたんだけれども、時間が取れずに正月の自由課題になってしまった。以下、大晦日から1/3まで38度前後の熱にうなされながら、初めてのPythonプログラミングに四苦八苦しつつ、調査した結果です。

環境

libcloud実行環境 CentOS 6.0, Python 2.6.5
libcloudバージョン v0.7.1
Target Cloud 1 Amazon EC2, US East (Virginia)
Target Cloud 2 OpenNebula v3.1.90 (v.3.2RC)

libcloudはまだあまりドキュメント化されてなく、OpenNebulaに関してはOCCIサービスの有無について書かれていないようだったけれども、libcloudはOCCIのInterfaceを叩いてOpenNebulaにクエリを送るので、OCCIの設定・サーバの起動は必須です。

インストール

公式サイトの説明通り、pipでインストールできます。CentOS 6.0だとこんな感じ。

$ sudo yum install python-setuptools
$ sudo easy_install pip
$ sudo pip install apache-libcloud

Amazon EC2

VMを起動

以下のようなスクリプトをcreate_node_ec2.pyの名前で作成。

from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver

EC2_ACCESS_ID  = 'AWSのACCESS ID'
EC2_SECRET_KEY = '上記ACCESS IDのペアとなるSECRET KEY'

Driver = get_driver(Provider.EC2_US_EAST)
conn = Driver(EC2_ACCESS_ID, EC2_SECRET_KEY)  # 1

vm_name     = 'test'  # 2
image_id    = 'ami-1b814f72'  # 3
size_id     = 't1.micro'  # 4
sshkey_name = 'USE_V_key'  # 5

image = [i for i in conn.list_images() if i.id == image_id][0]  # 6
# print image
print('Image file')
print('  id: ' + str(image.id))
print('  name: ' + str(image.name))
for key in image.extra:
        print("  {0}: {1}".format(key, image.extra[key]))

size = [s for s in conn.list_sizes() if s.id == size_id][0]  # 7
# print size
print('Machine Type')
print('  id: ' + str(size.id))
print('  name: ' + str(size.name))
print('  ram: ' + str(size.ram))
print('  disk: ' + str(size.disk))
print('  bandwidth: ' + str(size.bandwidth))
print('  price: ' + str(size.price))

node = conn.create_node(name=vm_name, image=image, size=size, ex_keyname=sshkey_name)  # 8
print('New node')
print('  id: ' + str(node.id))
print('  name: ' + str(node.name))

Amazon EC2VMを起動する方法は公式サイトにあるサンプルほぼそのまま。EC2のドライバインスタンスを取得して(1), 利用するOSイメージファイル(6)、VMインスタンスタイプ(7)を取得して、VMを起動する(8)だけ。OSイメージはIDで指定している(3)けれども、これは「Basic 64-bit Amazon Linux AMI 2011.09」に該当するイメージ。インスタンスタイプには、今回はマイクロを指定している(4)。ssh公開鍵として「USE_V_key」を指定している(5)けれども、今回は予めAWSコンソールにて、この名前で事前に作成して登録しておいた。なお、conn. ex_create_keypairメソッドを呼び出すことで、keyペアの作成にも対応しているとのこと。
ちなみにlist_*はEC2のサーバに問い合わせを行っているためか、特にlist_imagesでは、大きく時間がかかる。
このファイルを実行すると以下の様に表示される。

$ python create_node_ec2.py
Image file
  id: ami-1b814f72
  name: amazon/amzn-ami-2011.09.2.x86_64-ebs
  owneralias: amazon
  state: available
  architecture: x86_64
  hypervisor: None
  platform: None
  rootdevicetype: ebs
  ownerid: 137112412989
  ispublic: true
  imagetype: machine
  virtualizationtype: paravirtual
Machine Type
  id: t1.micro
  name: Micro Instance
  ram: 613
  disk: 15
  bandwidth: None
  price: 0.02
New node
  id: i-ae6e02cc
  name: test

それぞれ、libcloud.compute.base.NodeImage、libcloud.compute.base.NodeSize、libcloud.compute.base.Nodeクラスのインスタンスなんだけれど、どのようなフィールドがあるか、きちんとドキュメント化されていないのでソースコードをあたる必要があった。
なお、既に同じ名前(この例の場合'test' (2))のVMインスタンスがEC2上に存在する場合、VMの起動には成功するけれどcreate_nodeの戻り値として返ってくるNodeインスタンスのnameフィールドはidフィールドの値となった(この場合は'i-ae6e02cc')。

VMの状態表示

以下のようなスクリプトをshow_node_ec2.pyの名前で作成。

from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver

EC2_ACCESS_ID = 'AWSのACCESS ID'
EC2_SECRET_KEY = '上記ACCESS IDのペアとなるSECRET KEY'

Driver = get_driver(Provider.EC2_US_EAST)
conn = Driver(EC2_ACCESS_ID, EC2_SECRET_KEY)

node = [i for i in conn.list_nodes() if i.name == 'test'][0]  # 1

print('Instance')
print('  id: ' + node.id)
print('  name: ' + node.name)
print('  state: ' + str(node.state))
print('  public ips: ' + str(node.public_ips))
print('  private ips: ' + str(node.private_ips))
print('  size: ' + str(node.size))
print('  image: ' + str(node.image))
for key in inst.extra:
        print("  {0}: {1}".format(key, node.extra[key]))

EC2のドライバインスタンスを取得して、情報を得たいVMインスタンスインスタンスを得ている点は上と同じ。VM起動時にVM名として'test'を指定したので、それをキーとしてインスタンスリストを探索している(1)。
このファイルを実行すると以下の様に表示される。

$ python show_node_ec2.py
Instance
  id: i-ae6e02cc
  name: test
  state: 0
  public ips: ['x.x.x.x']
  private ips: ['y.y.y.y']
  size: None
  image: None
  status: running
  productcode: []
  groups: ['default']
  tags: {'Name': 'test'}
  instanceId: i-ae6e02cc
  dns_name: ec2-z-z-z-z.compute-1.amazonaws.com
  launchdatetime: 2012-01-04T12:03:47.000Z
  imageId: ami-1b814f72
  kernelid: aki-825ea7eb
  keyname: USE_V_key
  availability: us-east-1b
  clienttoken: 
  launchindex: 0
  ramdiskid: None
  private_dns: ip-w-w-w-w.ec2.internal
  instancetype: t1.micro
VMの停止

以下のようなスクリプトをdestroy_node_ec2.pyの名前で作成。

from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver

EC2_ACCESS_ID = 'AWSのACCESS ID'
EC2_SECRET_KEY = '上記ACCESS IDのペアとなるSECRET KEY'

Driver = get_driver(Provider.EC2_US_EAST)
conn = Driver(EC2_ACCESS_ID, EC2_SECRET_KEY)

node = [i for i in conn.list_nodes() if i.name == 'test'][0]  # 1
val = conn.destroy_node(node)
if val:
        print("Successfully delete Node[{0}]".format(node.name))
else:
        print("Failed to delete Node[{0}])".format(node.name))

1までは上と同じで、取得したVMインスタンスインスタンスを引数にdestroy_nodeを実行しているだけ。
このファイルを実行すると以下の様に表示される。

$ python destroy_node_ec2.py
Successfully delete Node[test]

OpenNebula

前提

OpenNebulaのクラウド環境は構築者により変わってくる。今回、僕が構築した環境では以下の様に設定してある。

  • OpenNebula利用者: cldadmin
  • OSイメージ: このページからダウンロードできるttylinuxイメージを使用
    • 次の定義ファイルを作成して、OpenNebulaに登録した。
NAME = ttylinux
PATH = "PATH_TO_IMAGE"
TYPE = OS
  • VMインスタンスタイプ: small
    • OpenNebula標準付属のocci_templateに定義されているsmallを使用
  • VM用ネットワーク: 'Test Network'として定義したネットワークを使用
    • 次の定義ファイルを作成して、OpenNebulaに登録した。
NAME = "Test Network"
TYPE = FIXED

BRIDGE = br0
LEASES = [ IP="x.x.x.a"]
LEASES = [ IP="x.x.x.b"]
VMを起動

以下のようなスクリプトをcreate_node_one.pyの名前で作成。

from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver

ONE_USER_ID = 'cldadmin'
ONE_PASSWD  = 'PASSWORD'
ONE_HOST    = 'URL_OF_OCCI_SERVER'
ONE_PORT    = 4567

Driver = get_driver(Provider.OPENNEBULA)
conn = Driver(ONE_USER_ID, secret=ONE_PASSWD, secure=False, host=ONE_HOST,
              port=ONE_PORT, api_version='3.2')  # 1

vm_name    = 'test'
image_name = 'ttylinux'
size_name  = 'small'
net_name   = 'Test Network'  # 2
context    = {'hostname':'$NAME',
              'files':'/data/cloud/one/ttylinux/init.sh /home/cldadmin/.ssh/id_rsa.pub',
              'target':'hdc',
              'ip_public':'$NIC[ IP, NETWORK=\\"Test Network\\" ]',
              'root_pubkey':'id_rsa.pub',
              'username':'shin',
              'user_pubkey':'id_rsa.pub'}  # 3

image = [i for i in conn.list_images() if i.name == image_name][0]
# print image
print('Image file')
print('  id: ' + str(image.id))
print('  name: ' + str(image.name))
for key in image.extra:
        print("  {0}: {1}".format(key, image.extra[key]))

size = [s for s in conn.list_sizes() if s.name == size_name][0]
# print size
print('Machine Type')
print('  id: ' + str(size.id))
print('  name: ' + str(size.name))
print('  ram: ' + str(size.ram))
print('  disk: ' + str(size.disk))
print('  bandwidth: ' + str(size.bandwidth))
print('  price: ' + str(size.price))
print('  cpu: ' + str(size.cpu))
print('  vcpu: ' + str(size.vcpu))

net = [i for i in conn.ex_list_networks() if i.name == net_name][0]  # 4
# print network
print('Network')
print('  id: ' + str(net.id))
print('  name: ' + str(net.name))
print('  address: ' + str(net.address))
print('  size: ' + str(net.size))
for key in net.extra:
        print("  {0}: {1}".format(key, net.extra[key]))


node = conn.create_node(name=vm_name, image=image, size=size, networks=[net],
                        context=context)  # 5
print('New node')
print('  id: ' + str(node.id))
print('  name: ' + str(node.name))

OpenNebula用のドライバインスタンスを取得しているが(1)、EC2の場合と比較して引数の種類が異なる。また、VMを接続するネットワークも明示的に指定する必要がある(2, 4)。さらに、OpenNebulaではCONTEXTと言う仕組みで、VMインスタンスの初期設定を行うが、このためのパラメータも数多く指定する必要があり(3)、これらを引数にcreate_nodeする必要がある(5)。
これは、以下のファイルを指定してonevm createコマンドを実行してVMを起動することに相当する。

NAME   = test
CPU    = 1
MEMORY = 1024

DISK   = [ IMAGE = "ttylinux" ]

NIC    = [ NETWORK = "Test Network" ]

CONTEXT = [
    hostname    = "$NAME",
    ip_public   = "$NIC[ IP, NETWORK=\"Test Network\" ]",
    files      = "/data/cloud/one/ttylinux/init.sh /home/cldadmin/.ssh/id_rsa.pub",
    target      = "hdc",
    root_pubkey = "id_rsa.pub",
    username    = "shin",
    user_pubkey = "id_rsa.pub"
]

このファイルを実行すると以下の様に表示される。

$ python create_node_one.py
Image file
  id: 1
  name: ttylinux
  fstype: None
  type: 0
  description: None
  size: 40
Machine Type
  id: 1
  name: small
  ram: 1024
  disk: None
  bandwidth: None
  price: None
  cpu: 1
  vcpu: None
Network
  id: 0
  name: Test Network
  address: None
  size: None
New node
  id: 13
  name: test
VMの状態表示

以下のようなスクリプトをshow_node_one.pyの名前で作成。

from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver

ONE_USER_ID = 'cldadmin'
ONE_PASSWD  = 'PASSWORD'
ONE_HOST    = 'URL_OF_OCCI_SERVER'
ONE_PORT    = 4567

Driver = get_driver(Provider.OPENNEBULA)
conn = Driver(ONE_USER_ID, secret=ONE_PASSWD, secure=False, host=ONE_HOST,
              port=ONE_PORT, api_version='3.2')

node = [i for i in conn.list_nodes() if i.name == 'test'][0]

print('Instance')
print('  id: ' + node.id)
print('  name: ' + node.name)
print('  state: ' + str(node.state))
print('  public ips: ' + str(node.public_ips))
print('  private ips: ' + str(node.private_ips))
print('  size: ' + str(node.size))
print('  image: ' + str(node.image))
for key in node.extra:
        print("  {0}: {1}".format(key, node.extra[key]))

ドライバインスタンスの取得のみEC2と異なる。それ以外は同じ。
このファイルを実行すると以下の様に表示される。

$ python show_node_one.py
Instance
  id: 13
  name: test
  state: 4
  public ips: [<OpenNebulaNetwork: uuid=7c3c633a7d389051291c394f534e07ca22023642, name=Test Network, address=x.x.x.a, size=1, provider=OpenNebula ...>]
  private ips: []
  size: <OpenNebulaNodeSize: id=1, name=small, ram=1024, disk=None, bandwidth=None, price=None, driver=OpenNebula, cpu=1, vcpu=None ...>
  image: <NodeImage: id=1, name=ttylinux, driver=OpenNebula  ...>
  context: {'files': '/data/cloud/one/ttylinux/init.sh /home/cldadmin/.ssh/id_rsa.pub', 'username': 'shin', 'target': 'hdc', 'hostname': 'test', 'ip_public': 'x.x.x.a', 'root_pubkey': 'id_rsa.pub', 'user_pubkey': 'id_rsa.pub'}

OpenNebula専用のネットワークオブジェクトが使われていたり、sizeやimageフィールドの型がEC2と異なるため、ドライバ毎に専用にパーズする必要がある。

VMの停止

以下のようなスクリプトをdestroy_node_one.pyの名前で作成。

from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver

ONE_USER_ID = 'cldadmin'
ONE_PASSWD  = 'PASSWORD'
ONE_HOST    = 'URL_OF_OCCI_SERVER'
ONE_PORT    = 4567

Driver = get_driver(Provider.OPENNEBULA)
conn = Driver(ONE_USER_ID, secret=ONE_PASSWD, secure=False, host=ONE_HOST,
              port=ONE_PORT, api_version='3.2')

node = [i for i in conn.list_nodes() if i.name == 'test'][0]
val = conn.destroy_node(node)
if val:
        print("Successfully delete Node[{0}]".format(node.name))
else:
        print("Failed to delete Node[{0}])".format(node.name))

こちらも、ドライバインスタンスの取得のみEC2と異なる。
このファイルを実行すると以下の様に表示される。

$ python destroy_node_one.py
Successfully delete Node[test]

まとめ

今回は単純なVMの起動・状態表示・停止しかしていないため詳細まで理解はできていないが、クラウドプロバイダ毎に個別の処理(ドライバインスタンスの取得や、VMの起動メソッドの引数)が必要になったり、戻り値の型が異なるなど、結局クラウドプロバイダを意識して実装しなければならない。また、標準で定義・実装されている機能は各種クラウドプロバイダの最大公約数であり、一部のクラウドプロバイダでしか提供されていない機能はex_XXXと言った名前で特別実装されているので、libcloud自体がきれいにカプセル化された実装になっていない。なので、今後API周りは大きく変化するのではないかと思っている。
ただ、各種クラウドAPIライブラリを導入しなくても、標準的なクラウド操作をlibcloudをインストールするだけでできるようになるのはうれしい。libcloudが無ければ、OpenNebulaのAPI、EC2 API、CloudStack APIなどを別途インストールしなければならないけれど、libcloud自体のインストールはすごく簡単に済むので。
今後、CloudStackのテスト環境も利用可能になる予定なので、そこでも試してみるつもり。