はてなブックマーク - Beccoame Ver0.1の話 (Tatsumaki, Sys::Virt)
    このエントリーをはてなブックマークに追加

    平田です。
    先日の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の関係を簡単に表すと、こうなります。

    これにBeccoameを挟むと、こんな感じです。

    で、今回は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のほうで紹介させていただくと思います。

    1. 直前はCentOS5.5上でEucalyptus NCが動いていました。 []
    2. arpの結果から引っ張るくらいしか思いつかず。いい方法あったら教えてください。。。 []
      はてなブックマーク - プロセス監視ツール「God」
      このエントリーをはてなブックマークに追加

      はじめまして。
      Fusicエンジニアの山本と申します。

      昨年、大阪のシステム会社より転職して参りまして、
      入社してからは主にruby(on Rails)を使っての開発に携わっています。

      初めての投稿の今回は、プロセス監視ツール「God」について取り上げたいと思います。

      みなさんはプロセス監視ツールは何をお使いでしょうか。
      代表格には、Daemontoolsやmonitがあるかと思います。

      近日公開する弊社のウェブサービスに監視ツールを導入するにあたって、そのサービスが他にない試みであるということもあり、今まで使ったことのない監視ツールを導入したいなと考えておりました。

      そんな中、名前がすごく特徴的で、rubyで動くという「God」に注目しました。

      God – A Process Monitoring Framework in Ruby
      god.rubyforge.org/

      トップのシルエットが印象的ですね。

      Daemontoolsやmonitと比べて優れている点、劣っている点はそれぞれあるようですが、
      ・監視の間隔の時間指定ができる
      ・障害時にメール送信できる
      ・一定のメモリ使用以上でリスタートできる
      という、必要最低限の機能は備えているようですし、すでにrubyを導入しているサーバーであったため、こちらを選択することにしました。

      あとで調べて分かったんですが、有名なレシピコミュニティーサイトのクックパッドさんでも導入されているんですね。
      動作サンプルはサイト上に掲載されていますが、以下の作業だけで導入することができます。
      gemの動作が必須となります。

      ・gemによるインストール
      # sudo gem install god

      ・設定ファイルの作成
      拡張子godのファイルを作成します。
      記述例は以下のとおり。

      # vim sample_ready.god

      God.watch do |w|
        w.name            = "sample_ready"  #この設定の名称
        w.interval        = 60.second  #監視時間間隔
        w.start           = "ruby #{File.dirname( __FILE__ ) + '/sample.rb'}"  #実際に動かすプログラム
        w.log             = "#{File.dirname( __FILE__ ) + '/sample.log'}"  #ログを残す場合の書き込み場所
       
        # プロセスが落ちいていた場合は起動
        w.start_if do |start|
          start.condition(:process_running) do |c|
            c.running = false
          end
        end
       
        # メモリが一定上超えたら再起動
        w.restart_if do |restart|
          restart.condition(:memory_usage) do |c|
            c.above = 10.megabytes
          end
        end
      end

      ・godの起動
      # god -c sample_ready.god

      これだけで監視の開始です。

      試しに、rubyで動かしているプロセスをkillして、60秒後に再度確認してみると起動しているのが確認できます。

      このGODの監視を止めるコマンドは

      # god stop sample_ready

      です。stopのあとに続く文字列は、ファイル名ではなく、w.nameで設定した名称でなければいけません。

      Deamontoolやmonitとの比較についても検証してみたいところですが、
      今回はここまでとさせていただきます。

        はてなブックマーク - TransitionComponent for CakePHPで簡単確認画面実装
        このエントリーをはてなブックマークに追加

        最近はこればかり聴いている小山です。

        今回は、つい先ほど1.0がリリースされたTransition Componentを紹介します。
        (CakePHPのバージョンは1.3.2を想定しています。)

        Transition Component

        Webシステムで、実装が面倒なものとして例えば「確認画面」というものがあります。
        ようは「ページ遷移をともなって完結するような機能」ですね。
        他にも「ウィザード」や「アンケート回答」や「カート」でもいいかもしれません。

        Transition Componentは、そのような「ページ遷移」管理を一手に引き受けてくれるComponentで、hiromi2424さんによって開発がされています。

        Transition Componentは、セッションと、主にCakePHPのモデルバリデーションの機能を利用してページ遷移を管理しており、
        使いこなすとページ遷移管理を非常にすっきりとしたコードで実現することができます。

        今回は簡単な登録機能に確認画面を実装してみます。

        登録機能の実装

        今回はcaketransitionというデータベースにpostsというテーブルを作成します。

        postsの登録機能のMVCは以下のようなソースコードで作成します。

        post.php

        posts/add.ctp

        posts_controller.php

        Post.titleにnotemptyのバリデーションがある簡単な登録機能です。
        ただ、これだとまだ確認画面はありません。

        Transition Componentを利用した確認画面の実装

        ではTransition Componentを利用して実際に確認画面付きの登録機能を実装してみます。
        Modelは特に変更はないので、VとCだけ作成します。

        posts/add.ctp

        posts/add_confirm.ctp

        posts_controller.php

        たった、これだけですっきりした形で確認画面付き登録機能が実装できました。

        ポイント

        ページ遷移を伴う機能を実装するにあたって必要なのは以下の点です。
        また、Transition Componentでは以下のメソッドでそれらを担っています。

        • 正しい操作後の次のページへの遷移と入力データの保持 [TransitionComponent::checkData(), ::automate()]
        • 正しいページを通って来たかどうかのチェック [TransitionComponent::checkPrev()]
        • 各ページで入力されたデータの取得 [TransitionComponent::mergedData(), ::allData()]

        posts_controller.phpを見ても、とても直感的でわかりやすいかと思います。

        まとめ

        今回はTransition Componentの導入として簡単な確認画面付き登録機能を実装してみました。
        本当に簡単な導入部分だけを紹介しましたが、実際に使いこなすと非常に強力な機能を提供してくれます。
        個人的にはTransition Componentはここ最近のNo.1ヒットなCakePHPライブラリです。

        ぜひ使ってみることをお勧めします。

        詳細はREADMEhiromi2424さんのブログを確認してみてください。

        お知らせ

        7月3日(土)に第2回CakePHP勉強会@福岡を開催します。

        メインセッションも県外からのスピーカーセッションや、
        CakePHPベースCMS ”BaserCMS“の開発者の方のセッションなどがあり
        非常に充実していています。

        CakePHPで語りたい皆様、ぜひ参加してください。Transition Componentの使い方の質問なども是非。

        第2回CakePHP勉強会@福岡

        日時 / DATE :2010/07/03 14:00 to 18:00
        定員 / LIMIT :20 人
        会場 / PLACE :Fusic Co., Ltd. (福岡県福岡市中央区大名2-4-22 新日本ビル9F)

        申し込みはこちら→第2回CakePHP勉強会@福岡