POEed Simple Amzaon Elastic Compute Cloud (EC2) Controller

Web APIネタでもうひとつ

YAPC::Asia 2007 Tokyoまで1週間を切りました.講演者の皆さんは原稿仕上がっているでしょうか.私は全然です・・・
前回作ったPOE版のOpenID Consumer Serverだけだと20分間乗り切る自信がいまひとつなかったので,もう一つネタを作ってみました.AmazonがWeb Serviceとして提供しているElastic Computer Cloudの簡単な管理系です.

Elastic Computer Cloud (EC2)

簡単に言うと,Amazonが提供しているバーチャルホスティングサーバ(実体はXen)で,Web APIで管理できます.安いのと,Simple Storage Service(S3)と連携して使えるってのが売りでしょうか.元々Amazon社内で大規模に使っているサーバをユーザにも商品として提供するって考えで始まったようです.開発者向け情報は英語ですが用意されていますし,http://aws.typepad.com/aws_jp/で定期的(だいたい毎月第3木曜日)に開いているセミナーに行くと,最新の情報を入手することができます.私も2月のワークショップには参加して,Net::Amazon::EC2 - Perl interface to the Amazon Elastic Compute Cloud (EC2) environment. - metacpan.orgを作られたJeff Kimさんに会ってきました.今回のスクリプトでは,このNet::Amazon::EC2を使います.

EC2の使い方

初めて使うときの使いかたはこんな感じになるでしょう.

  1. AWSのサイトを漁って,自分用のAWSAccessKeyIdとSecretAccessKeyを入手する
  2. 起動したサーバ(インスタンスといいます)アクセスするための鍵を作る
  3. インスタンスファイアウォール設定(TCP80番とか22番を開けたりとか)をする
  4. 起動するOS(Amazon Machine Images: AMI)の一覧を入手する
  5. 一覧から必要なAMIを選んで起動
  6. インスタンスの起動確認とfqdnを入手する
  7. 2で作った鍵を使って,sshで起動したインスタンスにアクセスする
  8. インスタンスを終了する

一般的にはAmazonが用意しているjavaベースのCLIツールを使うのですが,ここではNet::Amazon::EC2を使ったPOEアプリケーションを作ってみました.もちろん世の中にはすでに便利な物を作ってる人がいます.例えば,Amazon EC2 Firefox Extensionです.勉強のためでなければ,こういうのを使った方がいいですよ.なんと言っても,テストのためにインスタンスの起動を繰り返すと,そのたびに課金が発生しますからね(w

スクリプト

まず,AWSAccessKeyIdとSecretAccessKeyを格納するファイルを作ります.スクリプトを置くフォルダ内に'.AccessKey'という名前のファイルを作って以下のように記入してください.

---
AWSAccessKeyId: ここにAWSAccessKeyIDを書く
SecretAccessKey: ここにSecretAccessKeyを書く

見ただけでおわかりになる人も多いと思いますが,これはYAMLの記法です.この設定ファイルを読み込んで動くスクリプトは以下のようになります.このスクリプトを作るにあたってはAmazon EC2を簡単操作するWebインターフェースを参考にさせて頂きました.ありがとうございます.

追記

以下のスクリプトにはバグがありました.こちらに記載したスクリプトをご覧ください.

#!/usr/local/bin/perl
use POE qw/Component::Server::HTTP/;
use CGI qw(:standard);
use HTTP::Request::AsCGI;
use Net::Amazon::EC2;
use FindBin;
use YAML qw(LoadFile);
use Smart::Comments;
use strict;

my $KeyFile = File::Spec->catdir( $FindBin::Bin, '.AccessKey' );
die ".AccessKey file not found\n" if ( !-e $KeyFile );
my $keyref = LoadFile($KeyFile);

my $ec2 = Net::Amazon::EC2->new(%$keyref);

my $httpd = POE::Component::Server::HTTP->new(
    Port           => 10080,
    ContentHandler => { '/' => \&handler, }
);

POE::Kernel->run;

sub handler {
    my ( $req, $res ) = @_;
    my $c = HTTP::Request::AsCGI->new($req)->setup;
    my $q = CGI->new;

    print $q->header;
    print $q->start_html( -title => 'Simple EC2 Controller' );

    if ( my $target = $q->param('run_instances') ) {
        my $instance;
        $instance = $ec2->run_instances(
            ImageId  => $target,
            MinCount => 1,
            MaxCount => 1,
            KeyName  => 'POEEC2',
        );

        if ( !$instance ) {
            print $q->h2("Error Occured to run instances");
        }
        else {
            print $q->h2("$instance->{instance}[0]{instanceId} is starting");
        }
    }
    elsif ( $q->param('setup') ) {
        $ec2->authorize_security_group_ingress(
            GroupName  => 'default',
            IpProtocol => 'tcp',
            FromPort   => '22',
            ToPort     => '22',
            CidrIp     => '0.0.0.0/0'
        );
        $ec2->authorize_security_group_ingress(
            GroupName  => 'default',
            IpProtocol => 'tcp',
            FromPort   => '80',
            ToPort     => '80',
            CidrIp     => '0.0.0.0/0'
        );
        $ec2->delete_key_pair( KeyName => 'POEEC2' );
        my $key = $ec2->create_key_pair( KeyName => 'POEEC2' );
        print $q->h2("Your private key is ...");
        print $q->pre( $key->{keyMaterial} );
        print "Save this and change file modes to 600";
    }
    elsif ( my $target = $q->param('terminate_instances') ) {
        $ec2->terminate_instances( InstanceId => $target );
        print $q->h2("$target is shutting down");
    }
    elsif ( $q->param('describe_images') ) {
        my ( $imageref, %images );

        $imageref = $ec2->describe_images( { Owner => [ 'amazon', 'self' ] } );
        for my $image (@$imageref) {
            if (   ( $image->{imageState} eq 'available' )
                && ( $image->{isPublic} eq 'true' ) )
            {
                $images{ $image->{imageId} } = $image->{imageLocation};
            }
        }
        print $q->h2("Which AMI to run?");
        print start_form( -name => 'images' );
        print $q->radio_group(
            -name      => 'run_instances',
            -values    => \%images,
            -linebreak => 'true',
            -default   => (keys %images)[0], 
        );
        print $q->submit('run instances');
        print $q->end_form;
    }
    else {
        my $instanceref = $ec2->describe_instances;
        if ( !$instanceref ) {
            print $q->h2("No instances are there");
        }
        else {
            my %instances;
            for my $instance ( @{$instanceref} ) {
                my $dns =
                  ( $instance->{instance}[0]{dnsName} =~ m/HASH/ )
                  ? ""
                  : "($instance->{instance}[0]{dnsName})";
                $instances{ $instance->{instance}[0]{instanceId} } =
                    $instance->{instance}[0]{instanceId} . $dns . " is "
                  . $instance->{instance}[0]{instanceState}{name};
            }
            print $q->h2("Your instances status are ...");
            print start_form( -name => 'instances' );
            print $q->radio_group(
                -name      => 'terminate_instances',
                -values    => \%instances,
                -linebreak => 'true',
                -default   => (keys %instances)[0],
            );
            print $q->submit('terminate instances');
            print $q->end_form;
        }
    }
    print $q->hr;
    print $q->start_form;
    print $q->submit('setup');
    print $q->submit('describe_images');
    print $q->submit('descibe_instances');
    print $q->end_form;
    print $q->end_html;
    $res->code(200);
    my $c_res = $c->restore->response;
    $res->content( $c_res->content );
    $res->{_headers} = $c_res->{_headers};
    $res->{_msg}     = $c_res->{_msg};
    $res->{_rc}      = $c_res->{_rc};
    return RC_OK;
}

追記

上記のスクリプトにはバグがありました.こちらに記載したスクリプトをご覧ください.

スクリプトの動かし方

起動したら,ブラウザでhttp://localhost:10080にアクセスしてみてください.AWSAccessKeyIdとSecretAccessKeyの設定をお忘れなく.正常に起動していたら,「No instances are there」というメッセージと「setup」「describe_images」「describe_instances」というボタンが出てくるはずです.先ほど書いた,

というふたつの作業を行うのが「setup」,

  • 起動するOS(Amazon Machine Images: AMI)の一覧を入手する

という作業を行うのが「describe_images」,

という作業を行うのが「describe_instances」です.起動時には「describe_instances」の画面が出ています.このボタンを押しても同じ画面が出てきます.

それでは「setup」ボタンを押してみましょう.すると,秘密鍵を表示します.「-----BEGIN RSA PRIVATE KEY-----」から「-----END RSA PRIVATE KEY-----」までの文字列を任意のファイル名で保存してください.ここではkey1というファイル名にしたとしましょう.ただし,ファイルの属性は600にしておいてください.そうしないと後でsshに文句を言われます.これで鍵の設定が終わりました.この段階ではまだインスタンスを作っていないので,「describe_instances」を押しても,さっきと同じ画面しか返ってきません.
次に「describe_images」を押してみましょう.Amazon EC2 Firefox ExtensionだとAMIが一覧で沢山でてきますが,ここでは数個しかでてきません.これはamazon社が提供しているAMI(とあなたが作ったAMI)だけを表示しているからです.ここでは「ec2-public-images/getting-started.manifest.xml」というAMIを選択して「run instances」を実行します.これが,

  • 一覧から必要なAMIを選んで起動

する機能です.ちなみに,起動すると課金されます.よく考えてから実行してください.実行すると「i-0a11f66s is starting」と言ったメッセージがでてきます.この「i-...」というのはインスタンスのidです.インスタンスを起動して,実際にloginできるようになるまでには数十秒かかるようです.ここでお茶でも飲んでください.
お茶はおいしかってですか?では,「describe_instances」ボタンを押してみましょう.すると,「i-0a11f663(domU-12-31-34-00-01-E3.usma2.compute.amazonaws.com) is running」といったメッセージを表示するはずです.()の中身が今起動したインスタンスfqdnです.同じAMIを使っていてもインスタンスを起動する際に割り当てられる名前は変わります.名前の一意性はありません.
ではsshインスタンスにアクセスしてみましょう.これは先ほどの

の機能になります.

ssh -key1 root@domU-12-31-34-00-01-E3.usma2.compute.amazonaws.com

とすれば

         __|  __|_  )  Rev: 2
         _|  (     / 
        ___|\___|___|

 Welcome to an EC2 Public Image
                       :-)

    Getting Started


    __ c __ /etc/ec2/release-notes.txt

[root@domU-12-31-34-00-01-E3 ~]# 

となって,インスタンスへのアクセスができるはずです.ではインスタンスを終了しましょう.終了しないと課金しっぱなしです.注意してください.「terminate instance」を選択すると,「i-0a11f663 is shutting down」と表示します.これで課金は止まりました.「describe_instances」を押すと,「i-0a11f663 is terminated」と表示しているはずです.この表示はかなり長い間でています.たぶん1日ぐらいではないでしょうか.

注意

今回のスクリプトは,動かすと課金されるので,利用には充分注意してください.POEの中でNet::Amazon::EC2がLWP::UserAgentを呼んでいるので,EC2の処理が重かった場合にはブロッキングが発生して予期しない動作を起こす可能性があります.POE版はJeffさんにお願いすると出てくるのかなぁ〜.ぜひ会場で聞いてみたいと思います.では最後に,このスクリプトをご利用の際にはAmazon EC2 Firefox Extensionやamazon純正コマンドの用意をお願いすると供に,試し終わった時にインスタンスを確実に止めることを忘れないでください.よろしくお願いいたします.