EC2を使ってみて

嫁にPorterの鞄を薦めたら軽くあしらわれた、でおなじみのカタヤマソです。


id:cero-tにシャレコメントしたら、

そんなコンテキストの中では、


id:c9katayama
「ENdo Snipe Cloud Edition」が出せますよ。しかも若干高値で。 


というのも、自然な論理展開です。

とマジレスされてしまったので、このままでは失礼と思い立ち、サービス立ち上げのためにクラウドの母EC2とここ数ヶ月お付き合いしたので、真面目に感想を述べてみます。

ところでなぜEC2

1つ目は、やはり「コスト」です。コストにはイニシャルコストとランニングコストがあると思いますが、自社でやるにしてもホスティングを利用するにしても、やはりイニシャルコストの高さが問題です。
サーバ・ラック・電源・ネットワーク設備だけでもかなりコストがかかりますよね。いきなりお金が入ってくる目算があればいいですが、そうではない場合、私の所属するような小さな会社ではやはりリスクが高いです(だいぶ前ですが、過去に1度ハード先行投資で痛い目にあっています)
実際、2社からホスティング見積もりをもらいましたが、イニシャルコストはEC2に比べると桁が2つぐらい違いました。
(EC2の場合は、ドメイン取得費用とSSL証明書代ぐらいですので)
ランニングコストにしても、やはり1桁は違う感じです。


2つ目は「スケールアップと環境整備の容易さ」です。
id:nowokayさんが言うとおり、EC2はお金を出せばCPUを増やしたり、新しい環境を手に入れることができます。
しかも時間貸しなので、処理やテストが終わったら消してしまえば費用はかかりません。
また各種サーバを設定した環境ごとイメージファイル化できるので、本番環境を丸々テスト環境にもってきたりできます。
はじめにハードウェア構成を考えなくても後で変更できる、という点はまさにElastic!な感じでかなりの利点です。


3つ目は、「普通の環境」だということです。
普通の環境、というのは、EC2だからといって制約があるわけではないということです。
固定IPも取れますし、ドメイン取得してSSL通信もできます。Linuxも動くしTomcatMySQLも動きます。
またクラウド=KVSみたいなイメージがありますが、そういった縛りはEC2にはありません。
特別な環境の場合、いくら安くて高性能でも、それ自体がリスクになると思っています。
EC2の場合は、最悪ホスティングや自社サーバに逃げられるという点でも、普通の環境であるということは決め手でした。

実際に使ってみて

一番怖いのが、うっかりインスタンスを停止してしまうことです。EC2はインスタンスを停止すると、きれいさっぱり消えてしまいます。
管理にはFireFoxのアドイン(ElasticFox)を使用していますが、2クリックで完全消滅します。


なので、本番環境とテスト環境のIDは分けて取得しました。(イニシャルコストがないので、こういう点でも楽です)
操作時に片方の環境しかコンソールに表示されないので、心理的な負担がかなり軽減します。


また、DBのデータやログなど、なくなると困るデータは、EBS(Elastic Block Store)に格納するようにしました。
EBSは外付けHDDのようなイメージで、容量を指定してマウントすることで、OSから利用することができます。
EBSは、インスタンスが停止しても消えないという利点と、スナップショットが取れるという利点があります。
なので、EBSにデータをおいておけば安心感はあります。


サーバの性能については特に不満はありませんが、回線速度の遅さはやはりネックです。
今回作ったサービスはすべてFlexだったので、プログレスバーを出したりモジュール分割をしたりAMF3を使ったりと、わりと小細工できましたが、HTMLベースのアプリケーションだと、ちょっときびしいかもしれません。特にたくさんのファイルを同時に持ってくるというのが厳しい感じです。
ただ、それさえ許容できればメリットはかなり大きいかなと思います。


実際のところ、スナップショットが一番うれしかったかもしれません。バックアップは手間もかかりますし、また簡単に戻したりできませんので。
本番とは別に環境を作って、特定の時点のスナップショットで開始、というのがものの5分で出来るのは、かなりありがたいと思います。

EC2用にやったこと

EC2スペシャル、ということで2つ。


まずはEBSボリュームのスナップショットを定期的に取るようにしました。スナップショットを取って、かつ一定期間過ぎたものを消す、というような処理です。
スナップショットはクライアント側からのリクエストでしか取れないのですが、Amazonが提供しているクライアントツールコマンドライン用のフロントエンドしか提供しておらずちょっと使いづらいため、いろいろ探してみたところ、Typicaというライブラリ(http://code.google.com/p/typica/)がよさげだったので、これを使用しました。
コードはこんなイメージです。

public class SnapShotTask{

  private String accessId;
  private String secretKey;
  private int maxSnapShotNum = 30;

  @Override
  public void run() {
    try{
      Jec2 ec2 = new Jec2(accessId, secretKey);
      //空の配列を渡すと、全ボリュームを取得できる
      List<VolumeInfo> volumes = ec2.describeVolumes(new String[] {});
      for (VolumeInfo volume : volumes) {
        deleteOldSnapshot(ec2,volume);
        createSnapShot(ec2,volume);
      }
    }catch(EC2Exception e){
      log.error(e.getMessage(), e);
    }
  }
  protected void deleteOldSnapshot(Jec2 ec2,VolumeInfo volumeInfo) throws EC2Exception {
    List<SnapshotInfo> snapshots = ec2.describeSnapshots(new String[] {});
    if (snapshots != null && snapshots.size() > maxSnapShotNum) {
      Collections.sort(snapshots, new Comparator<SnapshotInfo>() {
        public int compare(SnapshotInfo o1, SnapshotInfo o2) {
          return o1.getStartTime().compareTo(o2.getStartTime());
        }
      });
      int deleteNum = snapshots.size() - maxSnapShotNum;
      for (int i = 0; i < deleteNum; i++) {
        SnapshotInfo info = snapshots.get(i);
        String id = info.getSnapshotId();
        ec2.deleteSnapshot(snapshots.get(i).getSnapshotId());
      }
    }
  }
  protected void createSnapShot(Jec2 ec2,VolumeInfo info) throws EC2Exception {
    String volumeId = info.getVolumeId();
    ec2.createSnapshot(volumeId);
  }
}

これをデイリーで実行するようにしています。


また、本来的にはスナップショットをとっておけばよいのですが、やはり手元に実データがないと心配なのが人情。
ということで、スナップショットの他にDBのダンプを取って、それを定期的にダウンロードするようにしました。
こちらは、JSchというライブラリ(http://www.jcraft.com/jsch/)を使用しました。

public class FileDownloadTask{

  //インスタンスに接続するためのキーペアファイルのパス
  private String keypairFilePath;
  //ホストのpublicキーが入ったファイル
  private String knownHostsFilePath;
  //ログインユーザー
  private String user;
  //接続先
  private String host;
  //サーバ上のバックアップファイルパス
  private String backupFileDirPath;
  //ダウンロードしたデータの出力先
  private String outputDirPath;

  @Override
  @SuppressWarnings("unchecked")
  public void run() {

    Session session = null;
    try {
      // SSH login
      JSch jsch = new JSch();
      jsch.addIdentity(new File(keypairFilePath).getAbsolutePath());
      jsch.setKnownHosts(new File(knownHostsFilePath).getAbsolutePath());
      session = jsch.getSession(user, host, 22);
      session.connect();
      ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
      sftp.connect();
      sftp.cd(backupFileDirPath);
      Vector<LsEntry> list = (Vector<LsEntry>) sftp.ls(".");

      File outputDir = new File(outputDirPath);
      if (outputDir.exists() == false) {
        outputDir.mkdirs();
      }
      File[] oldFiles = outputDir.listFiles();
      //出力ディレクトリにないファイルはダウンロード
      list:
      for (LsEntry entry : list) {
        SftpATTRS attr = entry.getAttrs();
        if (attr.isDir() == false && attr.isLink() == false) {
          String filename = entry.getFilename();
          for (int i = 0; i < oldFiles.length; i++) {
            if (oldFiles[i].getName().equals(filename)) {
              continue list;
            }
          }
          InputStream is = sftp.get(entry.getFilename());
          File backupFile = new File(outputDir, filename);
          FileOutputStream fout = new FileOutputStream(backupFile);
          IOUtils.copyStream(is, fout);
        }
      }
    } catch (Exception e) {
      log.error(e.getMessage(), e);
    } finally {
      session.disconnect();
    }
  }
}

サーバのバックアップスクリプトの周期と合わせて、動かすようにしています。

あとTipsとか

本番用とテスト用、IDを2つにするとお互いのインスタンスやEBSを見れなくなってしまいますが、S3を介してやればインスタンスを交換することができます。
たとえばテスト用IDで作った環境を本番用IDの環境で動かす場合は、

テスト環境で、本番用IDの証明書を使用してインスタンスのイメージを作成
 ↓
テスト用IDのS3バケットに転送
 ↓
S3バケットのアクセス許可リストに本番用IDを追加
 ↓
本番用IDを使ってS3バケットのインスタンスを登録

という感じやればうまくいきます。
EBSは、イメージ作成時に含めてしまって、移動先で別のEBSにコピーすることで移動ができます。

まとまりなく長くなりましたが

とりあえずイニシャル0円なので、ともかくやってみるとよいかと思います。


って普通のシメですねすみませんm(_ _)m