GoogleComputeEngineで提供されているPreemptible VMの活用方法を考えてみました。
Preemptible VMは格安のインスタンスで最大で24時間しか継続して稼動ができない分、使用料金を大幅に安くできるというものです。最大で70%の値引きだとか。

利用している最中にいきなりGoogleから停止されてしまうこともありますので、可用性を必要とするシステム構築には使用できないですが、お遊びや試験環境を作るには最適です。

ただし1点難点があります。それはいきなり停止されてしまうので、停止される度に自分でサーバを起動させなければいけない点です。

使っている途中にパタっと止まってしまうと毎回起動するのが結構面倒で大幅にモチベーションが下がってしまいます。

そこで今回はGoogleに停止されるのを検知すると、自動でリモートからサーバを立ち上げる仕組みを考えてみました。

1.はじめに。Preemptible VMの安さ

Preemptible VMを使うと月額いくらになるのか。
最初にコストシミュレーションをします。

n1-standardでCPU1個、メモリ3.75G、HDD100Gで見積もった場合です。月$32ですね。VM費用
これをPreemptible VMにすると。VM費用P

おおお$16になった。半額です!これは安い。

AWSと比較してもこの例だとm1.smallに近いスペックで月$30ぐらいしますのでやっぱり安いですね。

2.仕組みの概要

概要図です。Googleからの要求でPreemptible VMが終了されるとサーバ側からslackに終了通知を行います。
それをHUBOTが検知をしてGoogleSDKを使ってサーバを起動させるというものです。概要

3.Preemptible VMの自動停止を検知、slackに投稿

Googleの自動停止は停止サーバに対してacpiシグナルが飛んでくるので終了処理にslack通知処理を追加します。
公式リファレンスを参考に停止スクリプトを組みます。

centOS 6.7の場合は/etc/acpi/events/power.confに記載されている/etc/acpi/actions/power.shが終了時に実行されるようです。


$cat /etc/acpi/events/power.conf

# ACPID config to power down machine if powerbutton is pressed, but only if
# no gnome-power-manager is running

event=button/power.*
action=/etc/acpi/actions/power.sh

power.shにslack通知用の処理を追加で書きます。


$cat /etc/acpi/actions/power.sh

PATH=/sbin:/bin:/usr/bin

# Preemptible Sig Send Slack
perl /root/test/slackPostMessgeSimple.pl stop > /root/test/log.txt

slackPostMessgeSimple.plの中身はこんな感じ。追加ライブラリ不要で動くようにcurlでslackにメッセージを投稿します。

#!/usr/bin/perl
use strict;
use Sys::Hostname;

my $inputText = $ARGV[0];
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
my $yyyymmdd = sprintf("%04d%02d%02d%02d%02d", $year + 1900, $mon + 1, $mday,$hour,$min);

print $yyyymmdd . " Preemptible sig shutdow!!! slack start\n";

if ($inputText eq "stop") {
  $inputText = "[gce alert]" .hostname() . " stop signal (orz) $yyyymmdd";
} else {
  exit 0;
}

# POST準備
my $url = 'https://slack.com/api/chat.postMessage';

$inputText =~ s/(\W)/'%'.unpack("H2", $1)/ego;  #urlエンコード

#接続パラメータ
my %postdata = (
 'token' => 'XXXXXXXXXXXXXXXXXXXXXXXX', #token
 'channel' => '#XXXXX',#chanelName
 'text' => $inputText,
 'as_user' => 'true'
 );

#print $postdata{"token"};
my $post_url = "curl -XPOST \"https://slack.com/api/chat.postMessage?";                                         e
$post_url .= "token=$postdata{'token'}&channel=%23gcp7nmdev&as_user=true&";
$post_url .= "text=$inputText\"";
#print $post_url;

my $result = `$post_url`;
print "$result\n";

print $yyyymmdd . " Preemptible sig shutdow!!! slack complate\n";

これで自動停止が発生するとslackに投稿ができます。ででん!2016-04-26_010847 - コピー

4.HUBOTからサーバを再起動

前回書いた記事「【GCE】hubotとslackを使ってサーバを起動・停止させる」のまんまで、この時に作成したスクリプトがそのまま使えます。この[gce alert]という文言がslackに投稿された時にhubotが自動でVMを起動させるようにします。すると・・・・
2016-04-26_010847こんな感じで簡単にhubot(この例だと1go)が停止したサーバを起動させてくれます。簡単ですね。

これでPreemptible VMがいきなり停止されてもhubotが自動でサーバを起動してくれるようになりました。人間が毎回起動する手間が省けたのでだいぶ楽になりました。いやーめでたし、めでたし・・・・

4.注意点

ただし。この方法には1点注意すべきポイントがあります。それは停止されるサーバ側ではGoogleからの自動停止なのか、ユーザの手動停止(gcloud computeコマンド使用)なのかが判断が難しいという点です。
具体的に言うと/etc/acpi/actions/power.shの処理はGoogleからの自動停止の場合も、ユーザの手動停止の場合も呼び出しがされてしまいます。なのでユーザが停止した場合でもslackには[gce alert]test-host-name stop signal・・・と投稿がされてしまいます。
これで困る点としてはユーザから停止した通知をHUBOTを受け取り、勝手にサーバを起動をすると、hubotがサーバ起動→サーバが終了通知→hubotがサーバ起動の無限ループが発生してしまいます。

この件はGoogleサポートにも確認しましたが、サーバ側ではシグナルの識別が困難という回答が来ました。また推奨はしないがカーネルをチューニングすればできるよ!みたいな回答も・・・。たしかに自分でもsyslog等を調べたりもしてみましたが、二つの停止方法の違いを見出すことができず断念しましたTT

とは言え、100点は取れないまでも、現時点では上手くいった方法がありますのでそれを使ってます。

gcloud compute operations listコマンドを使う

このコマンドはサーバに対して操作を行ったログが残ります。これは公式リファレンスにも書いてありますが、Preemptible VM(サーバ)が横取りされたログも残ります。そのため、このログを見て横取りの痕跡「compute.instances.preempted」があればGoogleからの自動停止だ!と判断することができます。さらに厳密に判断する場合には、ユーザ操作のstop命令がないことも一緒に見ると精度が高くなると思います。

これは前回記事にあるプログラムの抜粋ですが、slackに停止通知が発生した5分以内に対象サーバにて横取りが発生しているかをチェックしています。

#サーバアラートの場合は、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;
  }
}

横取りが発生してかつ現在停止された状態であればGoogleからの自動停止確定!として判断をしています。2箇所sleepを入れています。1つ目はログが取れるようになるまで若干時間がかかるので10秒待機。2つ目はGoogleから停止要求が出てから実際にサーバが停止するまで30秒~60秒程度かかるので60秒待機。実際に運用しておりますが、この方法でほぼ100%に近い検知ができています。

5.まとめ

若干作りこみや仕掛けが面倒ですが、一度仕組みを作ってしまえばあとは楽です。Preemptible VMは安いので本当にオススメです。次回はLBとPreemptible VM使った格安WEBサイト構築をやってみたいです。