GoogleComputeEngine(以下GCE)を使って複数サーバを扱っていますが、まとめて複数のサーバを操作したくなるシーンがあります。

例として、自分の場合は試験環境としてGCEを使っているのですが使用しない夜間については停止をしたいです。理由としてはクラウドなので少しでも稼動時間を減らして経済的負担を少なくしたいというものです。

毎回コンソールからサーバを選択して停止をするのもしんどいので、自動停止スクリプトを組みました。停止方法としてはAPIかgcloud compteコマンドのいずれかを使うのが一般的ですが、今回は操作が簡単なgcloud compteコマンドを使ったものをメモとして残しておきます。

スクリプトの要件

  • 複数サーバが一括で開始・停止できること
  • 指定したサーバが開始・停止できること

こんなところでしょうか。スクリプト言語はGCEなのでGoかpythonを使うのがやりやすいですが、今回はcentosに標準インストールされているperlで書きました。

事前準備

gcloud compteコマンドを使うので、Google Cloud SDKを準備します。

次のページに方法が書いてあります。Python2.7を要求と書いてますがCentOS6.7に入っている2.6のものでも問題なく動作しました。gcloud initコマンドでauth認証が行えますので、認証を済ませておきます。

次のコマンドで対象プロジェクトのサーバ一覧が取得できればOKです。

$gcloud compute instances list

スクリプトの内容

引数に従ってサーバを指定し、開始・停止をコントロールできるようにしました。第三引数はプリエンプティブVMの横取りによる処理要求かを判断するモードで通常は使わないです。

controlGCE.pl

#!/usr/bin/perl
use strict;
use Sys::Hostname;
use Time::Local;
	
=pod
第一引数 操作名
            start 開始
            stop  停止
            status ステータス確認
第ニ引数 操作するインスタンス名。allを指定した場合は全台を対象とする
第三引数 起動ルート。GCEのプリエンプティブの横取 1 かユーザ要求 0か確認
         プリエンプティブの横取の場合は、Googleが自動停止をしたか判断をして、
         サーバを開始するものとする

コマンド 例)手動でGCEを起動させる場合
       perl controlGCE.pl start ABC-dev-cms 0
       例)手動でGCEを停止させる場合
            perl controlGCE.pl stop ABC-dev-cms 0
        例)バッチで一斉停止をする場合 使わない時間に落とすとき等
            perl controlGCE.pl stop all 0
        例)バッチで一斉開始をする場合  朝出社したタイミングで起動するときなど
            perl controlGCE.pl start all 0
        例)GCEのプリエンプティブの横取りから自動起動を行う場合
            perl controlGCE.pl start ABC-dev-cms 1
=cut

my $controlStatus = $ARGV[0];
my $serverName = $ARGV[1];
my $serverAleat = $ARGV[2];

#allを指定したときの処理対象外のインスタンスを指定する
my @allBlackList = ('XXXX');

if ($serverName eq "") {
  print "サーバ名を指定してください。\n";
  exit;
}

if ($controlStatus ne 'start'
  && $controlStatus ne 'stop'
  && $controlStatus ne 'status') {
  print "正しい操作名を指定してください\n";
  exit;
}

my $result;

#サーバアラートの場合は、Googleが停止したものなのか判断をする
#Googleからの要求の場合は自動起動を行う。
if ($serverAleat eq "1" && $controlStatus eq "start") {
  my $chkMinuts = 5;
  # 割り込み要求が発生しているか確認をする
  # ログに出ていない場合はがあるので10秒ほど待機
  sleep 10;
  $result = `gcloud compute operations list |grep compute.instances.preempted |grep $serverName  | sed -n '\$p'`;
  # systemevent-XXXXX ※XXXの部分がタイムスタンプ
  my $timeStampStr;
  if( $result =~ /^systemevent-([0-9]{10})[0-9]{3}-.*$/){
    $timeStampStr =  $1;
    #現在の時刻の5分以内にプリエンプティブの横取りが発生しているか
    my ($sec, $min, $hour, $day, $mon, $year) = localtime(time - ((60*$chkMinuts)));
    my $now = sprintf('%04d-%02d-%02d %02d:%02d:%02d', $year + 1900, $mon + 1, $day, $hour, $min, $sec);
    my $timeStamp = &date2timestamp($now);
    if (!($timeStampStr >= $timeStamp)) {
      #Googleが停止していないものとして終了する
      #print "$serverName not.instances.preempted\n";
      exit;
    }
    print "$serverName compute.instances.preempted\n";
    #後続の開始処理をするために60秒待機。停止中のステータスになっていることが多い。
    sleep 60;

  } else {
    #Googleが停止していないものとして終了する
    #print "$serverName not.instances.preempted\n";
    exit;
  }
}

#現状のサーバステータスを取得
$result = `gcloud compute instances list`;

#status確認の場合はステータスを確認して終了
if ($controlStatus eq 'status') {
    print $result;
    exit 0;
}

my @serverList = split(/\n/, $result);
my $wServerName = "";
if ( $serverName eq "all") {
  $wServerName = ".*";
} else {
  $wServerName = $serverName;
}
#処理するサーバの一覧を作成する
my @serverList = grep { /$wServerName/ } @serverList;

#開始、停止処理
foreach my $line (@serverList) {
  my $result = -1;
  my $instanceName;
  my $zone;
  my $status;
  #$1 instanceName $2 zone $3 status
  if ($line =~ /^([^ ]{1,})[ ]{1,}([^ ]{1,}).* ([^ ]{1,})$/ ) {
    $instanceName = $1;
    $zone = $2;
    $status = $3;
    #print "$1  $2  $3" . "\n" ;
  }
  #処理対象外か判定する 処理対象外かヘッダ行
  if ((grep {$_ eq $instanceName} @allBlackList) || $status eq "STATUS") {
      next;
  }
  if (($controlStatus eq "start" && $status eq "TERMINATED")
      ||($controlStatus eq "stop" && $status eq "RUNNING")) {
    # 開始要求で停止中のものは開始をする
    # 終了要求で開始中のものは開始をする
    $result = &cotrolGCE($instanceName,  $zone,  $controlStatus);
  }
  if ($result ==0) {
    print "$instanceName $controlStatus complate!\n";
  } else {
    print "$instanceName $controlStatus Failed! Status[$instanceName,$zone,$status]\n";
  }
}

#サーバコントロール関数
sub cotrolGCE {
  my ($instanceName, $zone, $controlStatus) = @_;
  #print "gcloud compute instances $controlStatus $instanceName --zone $zone \n";
  #my $result;
  my $result = system("gcloud compute instances $controlStatus $instanceName --zone $zone");
  return $result;
}
#タイムスタンプ変換処理 タイムスタンプ→日付
sub timestamp2date {
    my $timestamp = shift;
    my ($sec, $min, $hour, $day, $mon, $year) = localtime($timestamp);
    return sprintf('%04d-%02d-%02d %02d:%02d:%02d', $year + 1900, $mon + 1, $day, $hour, $min, $sec);
}
#タイムスタンプ変換処理 日付→タイムスタンプ
sub date2timestamp {
    my $date = shift;
    my ($year, $mon, $day, $hour, $min, $sec) = ($date =~ /(\d{4})\-([01]\d)\-([0-3]\d) ([0-2]\d):([0-5]\d):([0-5]\d)/);
    # timelocal()
    # Perl 組込関数の localtime 関数の逆
    # localtime 関数は UTC で 1970年1月1日 00:00:00 からの秒数から日時がわかるが、
    # 日付から経過秒数を得るには Time::Local の timelocal を使用する
    return timelocal($sec, $min, $hour, $day, $mon - 1, $year);
}

以上です。汚いプログラムで恐縮ですがこれでGCEをリモートから起動・停止ができようになりました。

このプログラムをcronなどに入れて夜帰る前にサーバを停止し、朝出社前にサーバを起動するイメージですかね。