平田です。
先日のFukuoka Perl Workshop #16で話したBeccoameについて、つらつらと。
資料は英語ですが、喋りは全て日本語でした。
Beccoame = Bogus elastic computing, like Amazon EC2 or Eucalyptus
ざっくり訳すると「Amazon EC2やEucalyptusっぽい偽クラウド」。
Eucalyptusを使っている上で
・-n 40とかで大量のインスタンスを一気に起動すると、処理が追いつかずにタイムアウト扱いになってしまう
・以前のEC2互換であるため、インスタンスの再起動ができない(停止=インスタンス消滅)
などなど、ちょっと困ったなーの部分を解消できないもんかと思い。
簡素化したもので構わないので、勉強ついでに作れないものかと考えたのが実装のきっかけです。
で、libvirtまわりの調査がてらざっくり組んでみたというお話を。
EucalyptusとAmazon EC2の関係を簡単に表すと、こうなります。
で、今回はTatsumakiを採用しました。
理由は単純で、僕が勉強がてらTatsumakiを触ってみたかっただけです。
サーバ環境は以下になります。
Dell PowerEdge T105
CPU: Quad-core AMD Opteron 1354
メモリ: 4GB
HDD: 250GB 7,200rpm
# 要するに、Eucalyptus環境の1台を使いまわしています1 。
これにUbuntu 10.04 LTS Server(x86_64)をインストール。
追加でKVMをapt-getでさくっとインストール。
操作するためのlibvirtもインストールします。
$ sudo apt-get install kvm libvirt |
libvirtは、Linuxの仮想化ライブラリです。
いろんな仮想環境に対する操作を行うAPIを提供します。
付属のvirshを用いれば、いろいろと操作が行えます。
$ virsh virsh にようこそ、仮想化対話式ターミナルです。 入力方法: 'help' コマンドに関するヘルプ 'quit' 終了します virsh # help コマンド: help ヘルプの表示 attach-device XML ファイルからデバイスを追加 attach-disk ディスク装置の接続 attach-interface ネットワークインターフェースを接続します autostart ドメインの自動起動 ... (以下略) ... virsh # |
で、libvirtのPerlバインディングがSys::Virtです。
要は「PerlでKVMなどの仮想環境を操作できる」モジュールです。
# 他の言語のバインディングについては、公式サイトをご覧ください。
# PHPバインディングも存在するんですね。。。
というわけで、必要なCPANモジュール群をインストール。
$ sudo cpan install Tatsumaki $ sudo cpan install Sys::Virt ... (以下略) ... |
で、コードはこんな感じになります。
# 久しぶりに書いたPerlなもので、いろいろ作法違いなどあってもご容赦ください。。。
app.psgi
use strict; use warnings; use Tatsumaki::Application; use RunInstancesHandler; use TerminateInstancesHandler; use DescribeInstancesHandler; package MainHandler; use base qw(Tatsumaki::Handler); sub get { my $self = shift; $self->write("Work well.\n"); } package main; use File::Basename; my $app = Tatsumaki::Application->new([ '/' => 'MainHandler', '/RunInstances' => 'RunInstancesHandler', '/TerminateInstances' => 'TerminateInstancesHandler', '/DescribeInstances' => 'DescribeInstancesHandler', ]); return $app; |
RunInstancesHandler.pm
# run instance handler package RunInstancesHandler; use parent qw(Tatsumaki::Handler); use strict; use warnings; use Sys::Virt; use XML::Simple; use Data::Dumper; use String::Random; use File::Basename; use File::Copy; use File::Spec; sub post { my $self = shift; # create XML::Simple object my $xs = new XML::Simple(forcearray => 1, KeyAttr => [], RootName => ''); # receive XML request my $xmlRequestRef = $xs->XMLin($self->request->content); my $xmlRef = $xs->XMLin($self->request->content); my $currentDir = dirname(__FILE__); # image id (default: lucid) my $imageId = 'lucid'; # image file name my $baseImageFileName = $currentDir . '/images/' . $imageId . '.qcow2'; # base xml file name my $baseImageXmlFileName = $currentDir . '/images/xml/' . $imageId . '.xml'; # generate MAC address script path my $genMacAddrPath = $currentDir . '/tools/gen_mac_addr.sh'; # instance memory size (default: 121072) my $memorySize = '131072'; # Open & read base XML file open my $fh, '<', $baseImageXmlFileName or die "Could not open file: $!"; sysread $fh, my $baseImageXml, -s $fh; # variables my $instanceName, my $uuid, my $macAddress, my $imageFileName, my $imageXmlRef; # get max instance count from request my $maxCount = $xmlRef->{maxCount}->[0]; # connect QEMU my $vmm = Sys::Virt->new(address => "qemu:///system"); my $domain; my $responseXmlTemplate = $currentDir . '/xml/run_instances_response.xml'; my $responseItemXmlTemplate = $currentDir . '/xml/item.xml'; # Open & read base XML file open $fh, '<', $responseXmlTemplate or die "Could not open file: $!"; sysread $fh, my $responseXml, -s $fh; # Open & read item XML file open $fh, '<', $responseItemXmlTemplate or die "Could not open file: $!"; sysread $fh, my $responseItemXml, -s $fh; my $responseXmlRef = $xs->XMLin($responseXml); my $itemXmlRef; my $domainXmlRef; # create images for (my $i = 0; $i < $maxCount; $i++) { # generate new instance name $instanceName = 'i-' . String::Random->new->randregex('[a-f0-9]{8}'); # generate new UUID $uuid = `uuidgen`; # generate MAC address $macAddress = `$genMacAddrPath`; chomp($macAddress); # copy image file $imageFileName = File::Spec->rel2abs($currentDir . '/store/' . $instanceName . '.qcow2'); copy($baseImageFileName, $imageFileName); # create XML Reference for booting $imageXmlRef = $xs->XMLin($baseImageXml); # change XML values $imageXmlRef->{domain}->[0]->{name}->[0] = $instanceName; $imageXmlRef->{domain}->[0]->{memory}->[0] = $memorySize; $imageXmlRef->{domain}->[0]->{currentMemory}->[0] = $memorySize; $imageXmlRef->{domain}->[0]->{uuid}->[0] = $uuid; $imageXmlRef->{domain}->[0]->{devices}->[0]->{disk}->[0]->{source}->[0]->{file} = $imageFileName; $imageXmlRef->{domain}->[0]->{devices}->[0]->{interface}->[0]->{mac}->[0]->{address} = $macAddress; $itemXmlRef = $xs->XMLin($responseItemXml); $itemXmlRef->{instanceId}->[0] = $instanceName; $itemXmlRef->{instanceState}->[0] = 'pending'; $responseXmlRef->{RunInstancesResponse}->[0]->{item}->[0]->{instancesSet}->[0]->{item}->[$i] = $itemXmlRef; $domainXmlRef = ''; # create domain $domain = $vmm->create_domain($xs->XMLout($imageXmlRef)); } $self->response->headers([ 'Content-Type' => 'text/xml; charset=utf-8' ]); $self->write($xs->XMLout($responseXmlRef)); $self->finish; } 1; |
# DescribeInstancesHandler.pmとTerminateInstancesHandler.pmは略。
これをplackupで起動し、
$ plackup & [1] 16392 Twiggy: Accepting connections at http://0.0.0.0:5000/ |
起動したサーバに対して、XMLでリクエストを送信します。
run_instances.pl
#!/usr/bin/perl -w use strict; use LWP::UserAgent; use HTTP::Request::Common; use File::Basename; my $userAgent = LWP::UserAgent->new(agent => 'perl post'); my $requestXmlFile = dirname(__FILE__) . '/run_request.xml'; my $requestUrl = 'http://localhost:5000/RunInstances'; open my $fh, '<', $requestXmlFile or die "Could not open file: $!"; sysread $fh, my $message, -s $fh; my $response = $userAgent->request(POST $requestUrl, Content_Type => 'text/xml', Content => $message); print $response->error_as_HTML unless $response->is_success; print $response->as_string; |
リクエストに使用するXMLは、EC2公式のAPIドキュメントのものに合わせています。
<RunInstances xmlns="http://ec2.amazonaws.com/doc/2010-06-15/"> <imageId>ami-60a54009</imageId> <minCount>1</minCount> <maxCount>3</maxCount> <keyName>example-key-name</keyName> <groupSet/> <placement> <availabilityZone>us-east-1b</availabilityZone> </placement> <kernelId>aki-ba3adfd3</kernelId> <ramdiskId>ari-badbad00</ramdiskId> <blockDeviceMapping> <item> <virtualName>ami</virtualName> <deviceName>sda1</deviceName> </item> <item> <virtualName>root</virtualName> <deviceName>/dev/sda1</deviceName> </item> <item> <virtualName>instancestore0</virtualName> <deviceName>sdb</deviceName> </item> <item> <virtualName>instance1</virtualName> <deviceName>sdc</deviceName> </item> </blockDeviceMapping> <userData version="1.0" encoding="base64"> <data>"VGhpcyBpcyBiYXNlIDY0IQ==</data> </userData> <addressingType>public</addressingType> <monitoring>enabled</monitoring> </RunInstances> |
これを実行すると、
$ perl run_instances.pl HTTP/1.0 200 OK Content-Type: text/xml; charset=utf-8 Client-Date: Wed, 28 Jul 2010 02:56:47 GMT Client-Peer: 127.0.0.1:5000 Client-Response-Num: 1 <RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2006-10-01"> <groupSet> <item> <groupId>default</groupId> </item> </groupSet> <instancesSet></instancesSet> <item> <instancesSet> <item> <amiLaunchIndex>0</amiLaunchIndex> <dnsName></dnsName> <imageId>ami-60a54009</imageId> <instanceId>i-ea19a11c</instanceId> <instanceState>pending</instanceState> <keyName>example-key-name</keyName> </item> <item> <amiLaunchIndex>0</amiLaunchIndex> <dnsName></dnsName> <imageId>ami-60a54009</imageId> <instanceId>i-e89ae6b5</instanceId> <instanceState>pending</instanceState> <keyName>example-key-name</keyName> </item> <item> <amiLaunchIndex>0</amiLaunchIndex> <dnsName></dnsName> <imageId>ami-60a54009</imageId> <instanceId>i-fb30dc5a</instanceId> <instanceState>pending</instanceState> <keyName>example-key-name</keyName> </item> </instancesSet> </item> <ownerId>495219933132</ownerId> <reservationId>r-47a5402e</reservationId> </RunInstancesResponse> |
といった感じで、起動要求を投げたインスタンスの情報がXMLで返ってきます。
レスポンスもEC2公式のAPIドキュメントに準拠する形にしています。
# と言っても、すごく半端な実装です。
# 起動時はmaxCountしか今のところ見てないですし。
# 返ってくるXMLも、instanceIdしか書き換えていない状態です。
実際に起動したかどうかは、virshで確認できます。
$ virsh list Id 名前 状態 ---------------------------------- 47 i-ea19a11c 実行中 48 i-e89ae6b5 実行中 49 i-fb30dc5a 実行中 |
無事に起動できているようです。:-)
と言った感じで、とりあえずここまで。
次は処理を非同期化したり、エラー処理をきちんと行ったりといったところでしょうか。
# 実は各インスタンスのIPアドレスを取得するのが、結構面倒だったり2 しますが。
また、インストールした直後だとNATで構成されているので、これをBridgeに変えたり。
そもそもの互換性がまだ低いので、勉強しながらちまちまと実装していく予定です。
個人的な感想としては、
・TatsumakiがモダンなPerlを学ぶ上で非常によい教材になる
・しばらく仕事でPHPを書いていたので、php-funcref-in-perlが非常に役立った
といったところです。
なお今回、弊社の新サービスを用いてプレゼンを行ないました。
# 終わってみたら、そっちに話を持って行かれて嬉しいやら寂しいやら。
新サービスついては、おいおい当blogのほうで紹介させていただくと思います。