higehikiのブログ

iPhoneアプリ「ログ雀」の中の人です。

capistrano3 + AWS(オートスケール) でdeployしてやる

オートスケール下でのdeployというと、self-deploy (起動時にスクリプトを実行して、ローカルのソースを最新版にする!) がイケてる気がします!

AWS EC2 capistranoでオートスケーリングインスタンスにデプロイ - cap version2
http://qiita.com/mychaelstyle/items/a550d4a0658c87c1ff30

こちらで紹介されている方法がまさにそれ!

にも関わらず、今回はオートスケールの通常フローで実装してみました。
AWS SDK使ってラクしてみよう!とか思ったわけではないんですが、別案件のスクリプトががっつり使いまわせたからって理由です。<今回のゴール>
capistrano3の動的IP取得 ⇒ ソース反映 + AMI作成 + オートスケール設定更新

先人の方々がcapistrano2でやられているのを参考にちょいちょいとやってみました。

やりたいことの流れ

① ローカルからgit development branchにソースをpush
② TOOLサーバにログインして capistrano3 (production) 実行
③ ELBにぶら下がっている全インスタンスに ソース反映 処理
④ オートスケール用のAMIを作成
⑤ Launch Configuration を新規作成 ( ④のAMIを指定 )
⑥ Auto Scaling group の設定を変更 ( ⑤のConfigurationに向け先を変更 )



ELBにぶら下がっている全インスタンスに ソース反映する設定

まず AWS SDK for Ruby を gem でインストールします。
(実行環境は AmazonLinux ではなく CentOS です。)

# gem install aws-sdk

続いて capistrano の設定です。
production環境のみ必要な対応なので、環境別のrbファイルに記述してみました。
※実装方法が正しいかわかりませんが、動きますw

require "aws-sdk"

set :stage, :production
set :branch, 'production'
set :deploy_to, '***DIR_PATH***'

AWS.config({
  :access_key_id => '***IAM ACCESS KEY ID***',
  :secret_access_key => '***IAM SECRET ACCESS KET***',
  :ec2_endpoint => 'ec2.ap-northeast-1.amazonaws.com', // tokyo region
  :elb_endpoint =>'elasticloadbalancing.ap-northeast-1.amazonaws.com' // tokyo region
})

elb          = AWS::ELB.new
lb_instances = elb.load_balancers["***ELB NAME***"].instances
instance_ips = lb_instances.map { |instance| p instance.private_ip_address }

role :web, instance_ips
instance_ips.each do |ip|
server ip, user: '***ssh user***', roles: %w{web}, ssh_options: { keys: [File.expand_path('~/.ssh/id_rsa')] }
end

roleだけでは上手く動かず、serverを記述しなきゃいけなかったんですが、複数台あるのに配列では受け付けない…ということでeachで回してみました。
あまりキレイな書き方じゃないとは思いますけど。

これで、③ ELBにぶら下がっている全インスタンスに ソース反映 処理 ができました。

PHPAWSを操作するバッチ作成

capistranoでソースを反映後、オートスケールを再設定するための処理をPHPで記述します。
capistranoを実行するサーバー上に一緒に置いておき、deployの最後にキックさせる方法をとります。

なぜRubyではないのか、というと、Rubyがあんまり書けないからです…。

ということで、AWS SDK for PHP 2 をインストールします。

$ cd [インストールディレクトリ]
$ curl -s http://getcomposer.org/installer | php
$ vi composer.json
{
  "require": {
      "aws/aws-sdk-php": "*"
  }
}
$ php composer.phar install

以上でインストールが完了しました。
続いて、バッチプログラムです。

<?php
/**
 * CHANGE AUTOSCALE
 */

//================================================
// 本番用設定
//================================================
// AWS SDK autoload.phpのPATHを指定して下さい・
define('AUTOROAD', ' *** autoload.php PATH ***');

// AWS 接続情報を設定して下さい。
define('KEY' , ' *** IAM ACCESS KEY ID *** ');
define('SECRET' , ' *** IAM SECRET ACCESS KEY *** ');
define('REGION' , 'ap-northeast-1');
// 東京リージョン ap-northeast-1

// AWS AMIを作成する対象のインスタンスID
define('BASE_INSTANCE', ' *** instance id *** ');
define('AMI_NAME', ' *** ami name *** ');
define('AMI_DESCRIPTION', ' *** ami description *** ');
define('BASE_NOREBOOT', true);

// Launch Configurations (for create)
define('LAUNCH_CONFIGURATION_NAME','  *** launch configuration name ***  ');
define('INSTANCE_TYPE',' *** instance type *** ');
define('KEY_PAIR',' *** key name *** ');
define('ASSOCIATE_PUBLICIP_ADDRESS',true);

// AutoScalingGroup (for update)
define('AUTOSCALING_GROUP_NAME',' *** autoscaling group name *** ');

// ------------設定ココまで-------------

// AWS インスタンス ステータス
define('STATUS_running', 'running');

// AWS AMI ステータス
define('STATUS_available', 'available');
define('STATUS_pending', 'pending');
define('STATUS_ok', 'ok');

//===============================================


require_once (AUTOROAD);
use Aws\Ec2\Ec2Client;
use Aws\AutoScaling\AutoScalingClient;

// バッチ処理開始
echo date('Y/m/d H:i:s').": CHANGE AUTOSCALE start\n";

// AMI作成
echo "createAMI start\n";
$ec2 = new AwstarEc2();
$imageId = $ec2->createAMI();
sleep(10);
echo "createAMI end\n";

// 生成AMIのステータスを監視 availableになったら次の処理へ
$amiStatus = '';
while (true){
    $amiInfo = $ec2->describeAMI($imageId);
    foreach ( $amiInfo as $k=>$r ) {
        if($k === 'Images'){
            $amiStatus = $r[0]['State'];
        }
    }
    if($amiStatus === STATUS_available){
        break;
    }
    sleep(20);
}


// オートスケール LAUNCH CONFIGURATIONS 作成
echo "create AUTOSCALE LAUNCH CONFIGURATION start\n";
$launch_configuration = $ec2->createLaunchConfiguration($imageId);


// オートスケール AutoScalingGroup 変更
echo "update AUTOSCALING GROUP start\n";
$ec2->updateAutoScalingGroup($launch_configuration);

sleep(10);

// バッチ処理終了
echo date('Y/m/d H:i:s').": CHANGE AUTOSCALE finish\n";

exit;


/*
 * AwsClass
 */
class AwstarEc2{

    private static $client;
    private static $autoscale;

    /**
     * init
     * Aws\Ec2\Ec2Client にリージョンをセットして初期化
     * Aws\AutoScaling\AutoScalingClient にリージョンをセットして初期化
     */
    private static function init(){
        $config = array(
            'key'    => KEY,
            'secret' => SECRET,
            'region' => REGION,
        );

        self::$client = Ec2Client::factory($config);
        self::$autoscale = AutoScalingClient::factory($config);
    }

  /**
     * createAMI
     * Executes the CreateImage operation.
     */
    public static function createAMI()
    {
        self::init();
        $result = self::$client->createImage(array(
            'InstanceId'  => BASE_INSTANCE,
            'Name'        => AMI_NAME,
            'Description' => AMI_DESCRIPTION,
            'NoReboot'    => BASE_NOREBOOT,
        ));
        return $result['ImageId'];
    }


    /**
     * describeAMI
     * Get infomation of AMI
     */
    public static function describeAMI($imageId)
    {
        $result = self::$client->describeImages(array(
            'ImageIds' => array($imageId),
        ));
        return $result;
    }

  /**
     * createLaunchConfiguration
     * New Create AutoScale Launch Configuration
     */
    public static function createLaunchConfiguration($image_id)
    {
        $options = array(
            'LaunchConfigurationName' => LAUNCH_CONFIGURATION_NAME,
            'ImageId' => $image_id,
            'KeyName' => KEY_PAIR,
            'InstanceType' => INSTANCE_TYPE,
            'AssociatePublicIpAddress' => ASSOCIATE_PUBLICIP_ADDRESS,
        );
        $result = self::$autoscale->createLaunchConfiguration($options);

        return $options['LaunchConfigurationName'];
    }


    /**
     * updateAutoScalingGroup
     * Update Exist AutoScalingGroup
     */
    public static function updateAutoScalingGroup($launch_configuration)
    {
        $options = array(
            'AutoScalingGroupName' => AUTOSCALING_GROUP_NAME,
            'LaunchConfigurationName' => $launch_configuration,
        );
        $result = self::$autoscale->updateAutoScalingGroup($options);

            return $result;
        }
}

ちょっと強引な使い方ですが、これで任意のタイミングでオートスケールの設定を変えることができるようになりました。
※ LaunchConfiguration は、更新ができないようなので、都度作成する必要があります。


あとは、capistranoの role を専用に用意して、deploy実行する際の処理に、このPHPバッチを叩くように書くだけで完成です。

これで手動作業から開放される!

問題は延々と増え続ける Launch Configuration と AMIたち。
deployの度にCreateされるので、世代管理を入れないとコストも管理もだるいっすね…。

ちなみに会社の後輩にどうやってるのか聞いたところ、zabbix + serf + Jenkins + capistrano + chef など盛々で動いており、1個1個構築してたら何日かかるのやら…。