tail my trail

作るのも使うのも、結局は、人なのだ

MHAを使ってZabbixをカジュアルに冗長化してみる

Onpremiseな環境でHAなDBクラスタを組んだり監視サーバを構築したりなんて作業は、ちょっと前であればごくごく当たり前のものだったけど、最近はクラウドSaas/Paasをどれだけ活用して、よりビジネスの本流に集中しようというご時世。 でも、やっぱりOnpremise、どうしても必要な時はある。今のレールに頼り切るのもいいけど、やっぱり勘所はおさえておきたいので、カジュアルにZabbixをHAした時のことを備忘録しておきます。

やりたいこと

Zabbix serverを冗長構成にしたい。 Zabbix serverの稼働に必要なものはざっくり以下。

  • Zabbix server (zabbix_server: 監視サーバプロセス)
  • MySQL (mysqld: データベース)
  • Apache + PHP (httpd: フロントエンド)

Zabbix serverをActive-Activeで稼働させてしまうと、データベースに二重で書き込みが行われてしまうので、Active-Standbyにしておく必要がある。障害時に手動切替なんて面倒、かつ二次障害起こしかねないので避けたい。MySQLもReplication組むだけでは弱いので自動フェイルオーバーくらいさせておきたい。そもそも監視サーバなので手間隙かけたくないし、むしろちょっとしたノード障害くらい放置しておきたい。

MySQLには5.6以降、GTID + failoverが標準搭載されているが、5.6出たばかりの時にGTIDのバグを幾つか踏んで痛い目にあったのと、上述の通りZabbix serverもまとめてHA化したいが幾つものクラスタウェア使って運用するのは腰が重い。カジュアルに一括りにしてよろしくやってほしい。

ということで、MHA。MySQLのReplicationのラグをよしなに埋めてくれる素敵なものにもかかわらず、挙動や構成が大変シンプル、拡張ポイントも多く、カジュアルにZabbixまで含めたクラスタウェアとして使えそうだったので採択決めました。

MHAいろは

MHAのアーキテクチャや仕組みは、詳しくは 公式ドキュメント を参照。要点をまとめるとMHA自体の責務は大きく以下。

  • MySQLの監視
    • Master/Slave ノードの監視
    • Replicationステータス/各ノードのPOSチェック/遅延状況
    • Binlog出力有無
    • Read Only有無
    • など
  • Failover
    • 故MasterからのBinlog救出
    • 昇格候補の選出
    • 新Masterへの差分データ適用
    • 新MasterへのRead only無効化
    • 他Slaveへの差分データ適用
    • 他SlaveのMaster host切替 (CHANGE MASTER)

Architecture of MHA

実システムでDBのSlaveを昇格させるには、これ以外に業務や環境に依存する様々な処理が必要になるが、MHAでは様々なフックポイントが用意されているので、適切な場所に拡張スクリプトを仕込めば、実システムにあわせたFailover作業を全自動で行うことができる。 拡張スクリプトPerlスクリプトで実装する。MHAがPerlで実装されており、これが内部でcallされるため。詳しくはソースコード参照。

構成は、以下の図のとおり、MySQL監視プロセスとしてのMasterHA-Manager (masterha_managerプロセス) と、MySQL各サーバ上で動作するMasterHA-Nodeが必要となる。Managerは監視プロセスとなるため常駐させる必要がある。Nodeは、Failover時にManagerから適宜SSH経由で発行される幾つかのコマンドを処理するだけとなるので、MasterHA-Nodeの動作に必要なものをインストールしておくだけで良い。 MHA Components いずれも軽量なプロセスなので、好きなように構成組めば良い。例えばこんなところか。

松: 昇格用slaveと参照用DBを分ける

MHA manager [192.168.10.1]

Master [192.168.10.11, VIP: 192.168.10.10]
├── Slave (cadidate_master) [192.168.10.12]
├── Slave (no_master, Read only) [192.168.10.13]
└── Slave (no_master, Read only) [192.168.10.14]

竹: Slave 1機, Manager用のノードは別筐体

MHA manager [192.168.10.1]

Master [192.168.10.11, VIP: 192.168.10.10]
└── Slave [192.168.10.12]

梅: 最小構成

Master [192.168.10.11, VIP: 192.168.10.10]
└── Slave (w/ MHA manager) [192.168.10.12]

MHA自体は単体で動作し、あくまでクラスタの監視と、Failover時のリカバリとフックポイントを提供するのみなので、既に稼働中の環境でもねじ込める点も良い。非常に軽量な作りなので、新設/既設いずれも親和性が高いだろう。

MHAはRPMが公開されておりインストールは瞬殺で終わるので割愛。 Managerの他、MySQLが稼働している全サーバにMHA Nodeを入れる必要がある点に注意。 詳細は、Installation を参照。

MHAの拡張ポイント

MHAの設定ファイルで、以下のParameterを定義できる。Perlスクリプトのpathを記載することで、これらが呼び出されるようになる。 https://code.google.com/p/mysql-master-ha/wiki/Architecture#Custom_Extensions

代表的なものを紹介すると、

master_ip_failover_script

※詳しくは 公式ドキュメント を参照

Failover時の挙動を指定できる。代表的なものは、仮想IPの付け替え、クラスタウェアの再起動、アプリケーションの参照先DBの切替など。

以下の3地点で、これらの拡張Perlスクリプトがcallされる。 Failoverの瞬間だけ実行されるわけではない点に注意して実装すること。

MHA Manager calls master_ip_failover_script three times. First time is before entering master monitor (for script validity checking), second time is just before calling shutdown_script, and third time is after applying all relay logs to the new master. MHA Manager passes below arguments (you don't need to set these arguments in the config file).

要するに以下のタイミングで発火されるので、"command" パラメータで条件分岐するようなPerlスクリプトを仕込めば良い。その他、スクリプトに渡される引数もあわせて、以下に記載しておく。

  1. Checking phase : MHA-Manager起動時(--command=status)
    • --command=status
    • --ssh_user=(current master's ssh username)
    • --orig_master_host=(current master's hostname)
    • --orig_master_ip=(current master's ip address)
    • --orig_master_port=(current master's port number)
  2. Current master shutdown phase : Masterサーバシャットダウン時(--command=stop or stopssh)
    • --command=stop or stopssh
    • --ssh_user=(dead master's ssh username, if reachable via ssh)
    • --orig_master_host=(current(dead) master's hostname)
    • --orig_master_ip=(current(dead) master's ip address)
    • --orig_master_port=(current(dead) master's port number)
  3. New master activation phase : フェイルオーバー完了時(--command=start)
    • --command=start
    • --ssh_user=(new master's ssh username)
    • --orig_master_host=(dead master's hostname)
    • --orig_master_ip=(dead master's ip address)
    • --orig_master_port=(dead master's port number)
    • --new_master_host=(new master's hostname)
    • --new_master_ip=(new master's ip address)
    • --new_master_port(new master's port number)
    • --new_master_user=(new master's user)
    • --new_master_password(new master's password)

サンプルコードは以下 https://github.com/yoshinorim/mha4mysql-manager/blob/master/samples/scripts/master_ip_failover

shutdown_script

※詳しくは 公式ドキュメント を参照

オプションで指定するPerlスクリプトで、Failover後のshutdown処理を指定できる。Split brain防止のために使用する。例えば、IPMIやiDRAC, ilo経由, ESXi cli, その他Hypervisor経由の強制シャットダウン、電源OFFなど。 発火タイミングは、 master_ip_failover_script --command=stopssh|stop の直後。SSH reachableな場合とそうでない場合で挙動を変えられる。

  1. SSHで接続可能な場合
    • --command=stopssh
    • --ssh_user=(ssh username so that you can connect to the master)
    • --host=(master's hostname)
    • --ip=(master's ip address)
    • --port=(master's port number)
    • --pid_file=(master's pid file)
  2. SSHで接続できない場合
    • --command=stop
    • --host=(master's hostname)
    • --ip=(master's ip address)

サンプルコードは以下 https://github.com/yoshinorim/mha4mysql-manager/blob/master/samples/scripts/power_manager

例えば、ESXiの場合、vim-cmd で以下のようなコマンドを実行するイメージ。statusチェック, 電源OFF, 電源ONできるので、これを含むPerlスクリプトを仕込めば実現できる。

# VMID=`vim-cmd vmsvc/getallvms|grep zabbix-test01|awk '{print $1}'` && if [ -n "$VMID" ]; then vim-cmd vmsvc/power.getstate $VMID; fi
Retrieved runtime info
Powered on

# VMID=`vim-cmd vmsvc/getallvms|grep zabbix-test01|awk '{print $1}'` && if [ -n "$VMID" ]; then vim-cmd vmsvc/power.off $VMID; fi
Powering off VM:

# VMID=`vim-cmd vmsvc/getallvms|grep zabbix-test01|awk '{print $1}'` && if [ -n "$VMID" ]; then vim-cmd vmsvc/power.on $VMID; fi
Powering on VM:

MHA managerプロセスのバックグラウンド起動

普通に起動するとフォアグラウンド。公式ドキュメントDaemonTools使っているが、UpstartLinuxのサービス管理ツールでもシンプルに実現できるので、この辺りでデーモン化してあげると手軽で良いだろう。 MHA MnanagerプロセスはFailoverすると終了するライフサイクルになっているので、自動起動やrespawn処理はあまり入れないほうが良さそうな気がする。以下サンプル。

description     "MasterHA manager services"

chdir /var/log/masterha
exec /usr/bin/masterha_manager --conf=/etc/mha.cnf >> /var/log/masterha/masterha_manager.log 2>&1
pre-start exec /usr/bin/masterha_check_repl --conf=/etc/mha.cnf
post-stop exec /usr/bin/masterha_stop --conf=/etc/mha.cnf

起動/終了/ステータスチェックはこんな感じ。

  • start masterha-manager
  • status masterha-manager
  • stop masterha-manager

MySQL インストール

詳細は割愛。 MySQL 5.6でも5.7でも動くぽいので、好きなモノを選べば良いと思う。 注意点は以下。

  • MySQL
    • MySQL 5.7の罠があなたを狙っている - Slideshare
    • Semi Replicationは有効化
      • Master - Slave間の欠損が限りなく0に近くなるので、Failoverが早くなる。
      • ただし、レイテンシとのトレードオフで判断すること
    • relay_log_info_repository=TABLE はやめておけ
      • MHA利用時、これを入れていると以下のようなエラーが出た。relay_log情報はMHAがよろしく見てくれるので、my.cnfではあえて有効化しておく必要はなし。
      • Getting relay log directory or current relay logfile from replication table failed on

    • relay_log_purge = 0
      • MHAでrelay_logを参照することと、purge_relay_log スクリプトが提供されており、こちらに委ねたほうが望ましいため無効にしておく。推奨
      • むしろ無効にしていないと、エラーログにwarningが出る。
      • Wed Nov 4 18:00:09 2015 - [warning] relay_log_purge=0 is not set on slave zabbix-test02(192.168.10.12:3306).

    • read_only は set文で。my.cnfには書くな。
      • 元のSlaveがMasterに昇格した時に、my.cnfに read_only = 1 がセットされていると再起動のタイミングで戻り障害を引き起こす可能性があるため危険。

Zabbix インストール

ようやくZabbix。インストールの詳細は例によって割愛。

ポイントは、冒頭で触れたとおりMHAでZabbixまで含めてクラスタ化するため、MySQLのMasterと同一サーバ上でZabbix serverを稼働させるようにすること。 Zabbix serverプロセスをPacemaker等、別の機構でクラスタ化しても良いのだが、実績上Zabbix serverプロセスが落ちる可能性よりもホストがポシャることの方が多かったので。監視サーバなので、という割り切りです。なので、設定は以下のような感じ。

  • zabbix_server.conf
    • DBHostはlocalhost (default) でOK
    • DBSocketも Socket (mysql.sock)
    • SourceIPは指定なし
      • VIPを指定してもよいが、監視を行うときに逆に足枷になることが多いので、できれば指定しないほうが運用上楽。

Zabbix web (Apache + PHP) は、複数Activeで動作可能ので上述のクラスタとはわけて考えて良い。 何人ものエンジニアが大量のグラフが貼られたスクリーンを定期リロードすることを考えると、むしろ別サーバにApache + PHP専用機並べてバランシングさせておきたいくらい。 なので、PHPからDBの接続はVIP経由でアクセスさせる。以下、Webの初期設定のポイントを列挙しておく。それぞれのWebサーバに対して設定行うこと。

  1. http://$HOSTNAME/zabbix/ にアクセス
  2. インストール画面が出てくるので進めていく
  3. DB接続情報は以下で入力
    • Database Host: VIP
    • Database User: zabbix
    • Database Password: 設定したパスワード
    • Test Connectionして接続確認
  4. Zabbix server Detailは以下で入力
    • Host: VIP
    • Name: 任意の名前 (環境が分かると良い)
  5. 確認ダイアログが出るので、間違いないか確認。
  6. これを全てのApache/PHPに対して行う (↑で生成された設定ファイル撒くでも構わない)

master_ip_failoverスクリプト for zabbix

最後の仕上げ。MHAで障害検知時は、MySQLの他、VIPとZabbix serverもセットでFailoverさせるようなスクリプトを用意しておく。あとは、/etc/mha.cnf の master_ip_failover に指定しておけば良い。 VIPの付け替えを行うのでARPキャッシュの更新を忘れないように。さもないと、フェイルオーバーしたのにつながらないなんてことのないように。 以下、サンプルです。

#!/usr/bin/env perl

#
#  If you wanna know about the paramaster, "master_ip_failover_script",
#  read the below document.
#
#  https://code.google.com/p/mysql-master-ha/wiki/Parameters#master_ip_failover_script
#
#  [Usage]
#  master_ip_failover_zabbix \
#    --virtual_ip=192.168.10.10/16 \
#    --orig_master_vip_eth=eth0 \
#    --new_master_vip_eth=eth0
#
#  [Description]
#  --virtual_ip            => Virtual IP / mask
#  --orig_master_vip_eth   => Device name that is attached virtual ip on origin master host.
#  --new_master_vip_eth    => Device name that is attached virtual ip on new master host.
#

use strict;
use warnings FATAL => 'all';

use Getopt::Long;
use MHA::DBHelper;

my (
  $command,        $ssh_user,         $orig_master_host,
  $orig_master_ip, $orig_master_port, $new_master_host,
  $new_master_ip,  $new_master_port,  $new_master_user,
  $new_master_password,
  $virtual_ip,  $orig_master_vip_eth, $new_master_vip_eth
);
GetOptions(
  'command=s'             => \$command,
  'ssh_user=s'            => \$ssh_user,
  'orig_master_host=s'    => \$orig_master_host,
  'orig_master_ip=s'      => \$orig_master_ip,
  'orig_master_port=i'    => \$orig_master_port,
  'new_master_host=s'     => \$new_master_host,
  'new_master_ip=s'       => \$new_master_ip,
  'new_master_port=i'     => \$new_master_port,
  'new_master_user=s'     => \$new_master_user,
  'new_master_password=s' => \$new_master_password,
  'virtual_ip=s'          => \$virtual_ip,
  'orig_master_vip_eth=s' => \$orig_master_vip_eth,
  'new_master_vip_eth=s'  => \$new_master_vip_eth,
);

exit &main();

sub main {
  # for debug
  print("---------- start master_ip_failover script ----------");
  if ( defined $command )             { print("  command => $command\n") };
  if ( defined $ssh_user )            { print("  ssh_user=s => $ssh_user\n") };
  if ( defined $orig_master_host )    { print("  orig_master_host => $orig_master_host\n") };
  if ( defined $orig_master_ip )      { print("  orig_master_ip => $orig_master_ip\n") };
  if ( defined $orig_master_port )    { print("  orig_master_port => $orig_master_port\n") };
  if ( defined $new_master_host )     { print("  new_master_host => $new_master_host\n") };
  if ( defined $new_master_ip )       { print("  new_master_ip => $new_master_ip\n") };
  if ( defined $new_master_port )     { print("  new_master_port => $new_master_port\n") };
  if ( defined $virtual_ip )          { print("  virtual_ip => $virtual_ip\n") };
  if ( defined $orig_master_vip_eth ) { print("  orig_master_vip_eth => $orig_master_vip_eth\n") };
  if ( defined $new_master_vip_eth )  { print("  new_master_vip_eth => $new_master_vip_eth\n") };

  # For current mastre shutdown phase
  # execute the below flow.
  #   1. unbind virtual ip from the origin master host.
  #   2. stop zabbix server process.
  if ( $command eq "stop" || $command eq "stopssh" ) {
    my $exit_code = 1;
    eval {
      `ssh $orig_master_host -o 'ConnectTimeout=5' '/etc/init.d/zabbix-server stop; /sbin/ip addr del $virtual_ip dev $orig_master_vip_eth'`;
      $exit_code = 0;
    };
    if ($@) {
      warn "Got Error while shutdown phase: $@\n";
      exit $exit_code;
    }
    exit $exit_code;
  }
  # For new master activation phase
  # execute the below flow.
  #   1. start zabbix server process.
  #   2. bind virtual ip on the new master host.
  #
  # Notice:
  #   * You don't need to unable the paramster "read_only = 1" and enable binary log,
  #     because they are already defined MHA common module.
  #
  elsif ( $command eq "start" ) {
    my $exit_code = 10;
    eval {
      my $ping_interface = join(".", $virtual_ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/ );
      `ssh $new_master_host -o 'ConnectTimeout=15' '/sbin/ip addr add $virtual_ip dev $new_master_vip_eth && /sbin/arping -U $ping_interface -c 3 && /etc/init.d/zabbix-server start'`;
      $exit_code = 0;
    };
    if ($@) {
      warn "Got Error while activation phase: $@\n";
      # If you want to continue failover, exit 10.
      exit $exit_code;
    }
    exit $exit_code;
  }
  elsif ( $command eq "status" ) {
    # do nothing
    exit 0;
  }
  else {
    &usage();
    exit 1;
  }
}

sub usage {
  print
"Usage: master_ip_failover --command=start|stop|stopssh|status --orig_master_host=host --orig_master_ip=ip --orig_master_port=port --new_master_host=host --new_master_ip=ip --new_master_port=port --virtual_ip=ip --orig_master_vip_eth=eth --new_master_vip_eth=eth\n";
}

以上で、カジュアルにZabbix serverが冗長化出来ます。 Zabbix serverプロセスが落ちたりVIP剥がれてもFailoverしないので、そのあたりは割りきって参考にして下さい。

まとめ

MHAはソースもPerlでそこまでボリューム多くないので気軽に読めちゃいます。 日本語の情報も多い(なにせ作者が日本人: 元DeNA, 現Facebookの松信さん)ので、他メンバーへの技術トランスファーも楽ちん、運用容易性が非常に高いのでおすすめです。

結局、MHAの宣伝みたいになってしまいました。。

iPhone 6Sが出たのでiPhone6 + MVNO に乗り換えてみた

iPhone 6S / 6S plus が2015年9月25日に発売されました。 2年前にSoftbankで購入したiPhone 5Sの2年分割払 + 月月割 が10月で切れるので、次をどうするか悩んだのですが、タイトルのとおり、このタイミングでiPhone 6 + MVNO への切替を試してみました。 試算上、端末代含めた今後2年間の出費を半額程度に減らすことができました。

  • 以下、10月時点の情報です。
  • 当ブログの情報は個人的に調べて試算したものなので間違いがあるかもしれません。これによる一斉の責任は負いかねます。
  • もし数値や認識に間違いなど見つかったらご指摘頂けますと幸いです。

iPhone 6 + MVNOにした背景

今までSoftbankの機種変のレールにそのまま乗っかり、2年分割払いで購入して月々割引 & 旧端末下取のパターンでiPhone 4S, 5Sと使ってきました。 Facetime オーディオやLINE電話などを使うことが多いので通話料はあまり取られないのですが、機種の分割払いも含めて月々7,000円前後かかっていました。年間 84,000円。 こうしてみると、結構かかってる。 今年は子どもも生まれ、色々と支出も増える年になったので、出来るだけ余計な出費は抑えたい。 仮にこの固定費を半額程度に抑えられたら、ルンバでも買って時間捻出できるなと思ったのがきっかけでした。

iPhone 6Sのアップデート内容を見ると、1200万/500万画素のカメラ、4Kビデオ、3D touchなどそこそこアップデートはありましたが、会社で検証端末は手に入るので、私物として確保したいと強く思えるような、ビビッと来るものがなかったので、現状キープか型落ちのiPhone 6を調達するで良いかと思い始めました。

また、ここ1〜2年で急激に盛り上がりを魅せているMVNOも試してみたい思いがあったので、まずはフラットに検討してみようと思ったのが背景です。

料金比較

ということで、支出がどの程度になるか、幾つかのパターンでシミュレーションしてみました。

前提

自分の生活と用途を考えて、以下の制約をおきました。

月々の通信量3GB以内

自宅や職場にいる時はWifiにつないでいるので、月々の通信量は2GB程度に収まっています。 なので、3GB程度のプランで十分そうでした。

音声通話

Facetime オーディオやLINE電話などを使うことが多いですが、緊急時などいざというときに音声電話はほしいです。また、電話番号変わると各調整が非常に手間なので、キャリア変えるにしてもMNPして電話番号はキープしておきたいです。

iPhoneを購入する場合ストレージは16GB

今までは "大は小を兼ねる" という言葉に従い、面倒な容量切り詰め作にかかる時間を買う目的で64GBを使ってきました。ただ、この4年間の運用で不要なアプリやデータが貯まり散らかる一方だし、写真などのデータは端末に入れておく理由は全くなく、共有フォトストリームやGoogleフォトなどクラウドサービスに放り込んでおいたほうが利便性は高いので、仮に機種を返るなら不要物を根こそぎ消す前提で16GBを候補にしました。 あと、自分は手が大きい方ではなく画面が大きいと辛いので、Plusではなく小さい方を候補に選びました。

MVNO使う場合、購入するiPhone6docomo

SIMフリー版でもいいんですが、値段が大きく変わってくるので。10月時点で値段に3〜4万程度の開きがありました。 MVNOの多くはdocomo SIMなので、選択肢の多いほうが望ましいと考えました。

Apple iPhone 6 16GB シルバー 【docomo 白ロム】MG482J

Apple iPhone 6 16GB シルバー 【docomo 白ロム】MG482J

候補

ということで、上がった候補が以下。

利用中のキャリアが、"実質0円で機種変できるキャンペーン" をやっていたので、比較のため候補にあげてみました。 docomo乗換に関しては、代理店がキャッシュバックキャンペーンをやっていたりするので、もう少し調べれば大手キャリアに分のある選択肢も出てくるのかもしれません。10月時点では、iPhone6も台数限定で一括0円キャンペーン + キャッシュバックなどやっていたようです。 ただ、複雑怪奇な契約体型から脱したい思いもあったし、キャッシュバックやっている店舗を調べるのも足を運ぶのも時間がかかり過ぎる印象があり、"台数限定" に焦るのもバカバカしかったので、そういったものは対象外としました。

MVNO業者は、どこもプランが横並びなので、メジャーなIIJmio (みおふぉん) をサンプルにおきました。

  1. 現状維持: iPhone 5S (64GB) + Softbank (ホワイトプラン + パケットし放題フラット for 4G LTE)
  2. 機種/プラン変更: iPhone 6S (16GB) + Softbank (スマ放題ライト + データ定額パック5GB)
  3. docomoMNP: iPhone 6S (16GB) + docomo (カケホーダイライトプラン + データMパック5GB)
  4. MVNOMNP: iPhone 6S (16GB) + MVNO (3GB程度通信 + 音声通話)
  5. MVNOMNP: iPhone 6 (16GB) + MVNO (3GB程度通信 + 音声通話)

見積もってみると

あくまで試算ですが、こうしてみると結構な開きが出ました。

型落ちのiPhone6で、MVNOで運用するとさすがに安いです。新しい機種を買うにも関わらず、現状stayと比べると先2年の総出費が倍以上変わります。

面白いのが、Softbankで機種変をしてもしなくてもTotalの出費はほとんど変わらないという点です。

1.現状維持 2.機種変更 3. docomoMNP 4. MVNOMNP 5. MVNOMNP
音声通話基本料 ¥1,008 ¥1,836 ¥1,836 ¥756 ¥756
データ通信基本料 ¥6,156 ¥5,400 ¥5,400 ¥972 ¥972
インターネット接続サービス ¥324 ¥324 ¥324 ¥0 ¥0
端末代 24ヶ月分割 ¥0 ¥3,900 ¥3,888 (※1) ¥3,906 (※1) ¥2,354
機種変サポート(24ヶ月間) ¥0 -¥2,835 -¥3,456 ¥0 ¥0
MNP特典 (※2) -¥450 ¥0 ¥0
特典 (12ヶ月間) ¥0 ¥0 -¥1,350 ¥0 ¥0
下取り ¥0 -¥1,065 (※3) -¥625 (※3) -¥625 (※3) -¥625 (※3) -¥625
1年目 月々支払額 ¥7,488 ¥7,560 ¥5,567 ¥5,009 ¥3,329
2年目 月々支払額 ¥7,488 ¥7,560 ¥6,917 ¥5,009 ¥3,329
2年間 総支払額 ¥179,712 ¥181,440 ¥149,808 ¥120,216 ¥79,900

ということで、10月中旬〜11月中旬の2年契約更新期間 (違約金が発生しない1ヶ月) のタイミングで、プラン5の 型落ちiPhone6 + MVNO に切り替えることにしました。

iPhone調達とMVNO切替

iPhoneAmazonで購入しました。 今はさらに値下がりしているようですが、私が購入した時は上記見積もりの¥56,500で購入することが出来ました。 赤シム引いてしまうと後で悲しい思いをするので白ロム保証を明記しているところを選ぶと良いと思います。 iPhoneは注文して2日後に届きました。

MVNOはいくつか比較検討した結果、IIJmioのみおふぉんを選びました。理由は通信実績です。 MVNOは競争が激しく、特に新興はキャンペーンを打ち出して料金も割引されることがあります。 ユーザが少ないうちは通信も快適でメリットが多いのですが、ユーザが増えた途端速度が落ちたなんてこともあるようです。 あくまで10月までの実績ベースですが、IIJmioは比較的通信が安定しており、ユーザ増加にあわせて増強対応も進められているようでした。

参考にしたのはこちらのサイトです。

格安SIM(MVNO)の速度比較 - androidlover.net

ちなみに、解約違反金は音声通話を入れている場合は¥0ではないですが、月々その額は減っていき、1年経つとそれ以降は違約金はかからなくなるそうです。

IIJmioAmazonで購入しました。こちらは注文して翌日にMNP用の書類が届きました。

IIJ IIJmio SIM 音声通話 パック みおふぉん IM-B043

IIJ IIJmio SIM 音声通話 パック みおふぉん IM-B043

MNPの場合は書類のみ届きます。キャンペーンコードが入ってるので、IIJmioのWebサイトで申請を進めると、数日後にSIMカードが届きます。

なお、手続き前にキャリアからMNP予約番号を取得しておくこと。softbankMNP予約番号はMySoftbankから申請できず、店頭か電話での申し込みになります。

SIMカードが届いたら開通手続きを行えば、1時間程度で新しいSIMが使えるようになります。 全て自宅で行えるのでありがたいですね。

揃ったものがこちら。 SIMは当然docomo SIMでした。 f:id:uorat:20151115172528j:plain f:id:uorat:20151115172520j:plain

64GB→16GBへの移行とiPhone5S売却

データの整理は思った以上に大変でした。

4SでiPhoneを使い出して以来、ずっと64GBで運用してきたので不要なアプリや写真、聞かない音楽が貯まり散らかりまくっていたので。 16GBといえども、iOSメタデータが乗ってくるので、実際に使える領域は10GB程度と予め割りきっておくと良いです。 私は音楽の取捨選択に最も苦労しました。

移行が無事完了したら、あとは旧端末iPhone5Sの売却です。 私は周囲の買取評判が比較的良かったゲオのSmarketを利用しました。

スマートフォンの買取ならゲオのSmarket | iPhone・Androidなど高価買取

申し込みをすると、数日後段ボール箱が届くので、手順通り初期化とアクティベートを行ってから箱に収納し送り返します。 数日後査定結果がメールで送られてくるので、同意すればその額が銀行口座に振り込まれます。 この時のために2年前iPhoneの箱は捨てずにとっておいたので、傷はありながらも条件は少しは良くなるのではと期待。結果、私は¥17,500程度で買い取ってもらえました。 iPhone6の購入価格が ¥56,500 だったので、実質手に入れたので、実質 ¥39,000 程度で手に入れたことになります。

移行してみて

もうすぐ一ヶ月程度経ちますが、不自由なく快適に過ごしています。

通信状況はちゃんと計測していないですが、私の生活範囲では体感むしろ向上した気がします。 ランニングコストも大幅に削減できたし、複雑怪奇な料金プラン/契約から脱することもできました。

節約バンザイ。自由バンザイ。

AWS CLI のprofileを簡単に切り替える

意外と知らない人がちらほらいたので、書き留めておく。

AWSAPIとIAMについて

基本的にAWSの全てのサービス / リソースの操作はAPIによって行われます。 (Management Consoleの操作も内部的には全て同じAPIアクセス) S3への画像のuploadから、EC2 Instanceの増設/設定変更から、CloudWatchの参照、Support Caseの起票まで、あらゆる操作をAPIで行うことができるので、 単純なWeb Applicationだけでなく、オペレーション自動化など多種多様な用途で活用することになり、IAM User/Access Keyもあわせて複数用意することが多いと思います。

環境やサービスによってAWSのアカウント自体を分ける運用も多いと思いますが、そうなると扱うKeyの数は益々増えていきます。

AWS CLI

何かAWSのリソースを使ってものづくりをする際に、デバッグなどで AWS CLI を使うことは多いと思います。 なんたってワンライナーで手軽にAPI叩けますからね。 蛇足ですが、S3 のオブジェクト一覧見る時なんか、Management Console見るよりも、AWS CLI叩くことのほうが私は多いです。そのほうが速い。

本題

だいぶ引き伸ばしましたが、本題です。 幾つものUser, Keyを併用して使っている時に、ユーザを切り替えて権限確認、動作確認したい場合の方法です。 至ってシンプル。 --profile オプションを使いましょう。 以下、Helpより引用。

$ aws help
      
      ( ...omitted ...)

       --profile (string)

       Use a specific profile from your credential file.

       ...


$ aws configure help

      ( ...omitted ...)

SYNOPSIS
          aws configure [--profile profile-name]

      ...

aws configure コマンドで --profile *profile-name* を付与して設定を進めると、指定した名前で別の設定を加えることができます。 default (無名) は消えず別の内容で登録され、他サブコマンド実行時に同様に --profile *profile-name* オプションを付与するだけでアカウントを切り替えできます。

以下設定例。

$ aws configure --profile bob
AWS Access Key ID [AKIA************hoge]:
AWS Secret Access Key [****************hOgE]:
Default region name [ap-northeast-1]:
Default output format [None]:

設定内容は $HOME/.aws/config, credentials ファイルに INI形式で保存されます。 中身を見ると以下のように、default (無名) はそのままに、上のprofile-name で指定した情報が追記されています。

$ cat .aws/config
[profile default]
region = ap-northeast-1

[profile bob]
region = ap-northeast-1

$ cat .aws/credentials
[default]
aws_access_key_id = AKIA************DMKA
aws_secret_access_key = ************************************hoge

[bob]
aws_access_key_id = AKIA************hoge
aws_secret_access_key = ************************************hOgE

切替を試してみましょう。 defaultはrootアカウントのKey, S3の任意のbucketのみ権限があるようなIAM Userをbobとしてprofile登録しています。

# root権限でbucket一覧確認
$ aws s3 ls
2015-10-15 19:28:40 fuga
2015-10-15 19:29:17 hogehoge

# bobユーザでfuga bucketの中身をls
$ aws s3 ls s3://fuga/ --profile bob
2015-10-15 19:44:10      14152 test.png

# hogehoge bucketにアクセス (権限なし)
$ aws s3 ls s3://hogehoge --profile bob

A client error (AccessDenied) occurred when calling the ListObjects operation: Access Denied

終わりに

jq コマンド、補完入力設定と同じくらい必須のTIPsだと思います。 環境変数にcredential書く手もありますが、その場合profileオプションで切り替えられず少々不便になるので、こちらのほうがオススメです。

AWS CLIは鬼のようにできることが多いのと、たまにManagement Consoleではできない操作なんかもあったりするので、 移動中などでちまちま暇つぶしがてらリファレンス呼んでみると面白いです。 aws — AWS CLI 1.8.12 documentation

Ansible入門&ハンズオン資料を公開しました

少し時間が経ってしまいましたが、 先日Ansible入門というイベント でAnsibleの説明とハンズオンをする機会を頂きました。 資料は SlideShare に公開しているので、これからAnsible始めたいという方はご参考にしていただければ幸いです。 資料の後半にハンズオン用の題材があり、Playbookの例はGithubに公開しています。

概要

Ansibleに興味がある未経験者〜使い始めた or 別の構成管理は使ったことがあるくらいのAnsible初心者をターゲットとし、受講後自走できるようにという思いで作りました 資料自体は、「構成管理とは」「なぜやるのか」という導入から始まり、Ansibleの世界観や登場人物をざっくりと説明しています。 読めばキーワードがざっと入って脳内インデックスができ、一時間半程度のハンズオンを経ればある程度勘所はつかめるようになるので、あとは公式ドキュメントなど見ながら自主学習してね、というスタンスの資料です。 ハンズオンは計3つです。

  1. 基礎編
    1. yumモジュールでパッケージインストール
    2. templateモジュールでバナーファイル作成
    3. yumモジュールでNginxインストール、serviceモジュールでNginx自動起動 (serviceモジュール実行時のdry-run時のエラーは無視する)
    4. copyモジュールで静的ページを配置
  2. adhoc編
    • ansibleコマンドでadhocにshellモジュールを実行する
  3. 応用編
    1. shellモジュールを活用してEC2 instanceにスワップ領域を追加する
      • register使って冪等性ちゃんと担保すること
      • スワップファイル作成は fallocateコマンドで
      • mkswap, swaponはもちろん、自動マウントも忘れずに
    2. yumモジュールでパッケージインストール
      • epelリポジトリ指定
      • rpmパッケージ指定 (これも冪等性ちゃんと担保するように注意)
    3. デプロイ
      • gitモジュールでデプロイ
      • Nginxの設定ファイルをtemplateモジュールで生成
      • デプロイないしは設定ファイルに変更があったらnotify使ってでNginxを再起動する

※ "冪等性ちゃんと担保する": 要するに、dry-run モード ansible-playbook --check で実行してchangedにならないようにする。

Playbookの基礎を抑えている内容となっていると思うので、大体の勘所がつかめ、すぐに業務自動化に励めると思います。 Ansibleが素晴らしいのは、エージェントレスでアーキテクチャも記法もシンプルなので、季節の環境を汚すことなくスモールスタートでねじ込める点です。 さくっと運用便利ツールや監視エージェントをばらまく、脆弱性対応するくらいのカジュアルな使い方から運転できるのは大きいですね。 私は去年まで1年ちょっとChefに使っていましたが、Ansibleのこのシンプルさはとても好きです。

SlideShare

ハンズオンの回答例

uorat/ansible-handson · GitHub

感想

当日は台風の影響もあり、申し込み数ほどに参加者は集まらなかったのですが、参加頂いた方は皆熱心に臨んでくださったので救われましたw "収穫が多かった、タメになった" という感想を幾つか頂きました。 小さいイベントですが、せっかく来て頂いた方には何かを持ち帰ってアクションに繋げられるようにしたいと思ったことと、この資料も今回に閉じず社内勉強会などで使いまわしたいという思いがあり、それなりに考えてコンテンツ作ったので、こういう声は純粋に嬉しいですし、今後の励みになります。 ご協力頂いた皆さま、ありがとうございました。

なお、ハンズオンのgit module使ってデプロイするというお題の中で、 @yteraoka さんの ansible-tutorial - Github を参照させていただきました。

以下のgithub.ioですね。ServerspecでCI回すところまで含まれていて素晴らしいTutorialです。 是非こちらも参考になさって下さい。

Ansible チュートリアル | Ansible Tutorial in Japanese

あと、この本もおすすめです。

入門Ansible

入門Ansible

naoyaさんのChef入門と同じくらいのボリューム感ですね。

入門Chef Solo - Infrastructure as Code

入門Chef Solo - Infrastructure as Code

Socket.io with Websocket の SSL/TLS 対応

※ (2016/9/19 追記) Nginx 使った対応方法も記載しているので、あわせて参考にして下さい。


昨今のサービスにおいて、暗号化はもはや必須の流れである。 GoogleFacebookなど主要なサービスはずいぶん前からHTTPS通信を標準としているし、HTTPS化対応しているサイトはSEO的にも優遇されるようになる という方針が出ていたりする。

前記事でSocket.IO + Redis PubSubを用いたリアルタイムメッセージ配信の仕組みをまとめたが、このままWebSocketを利用すると当然インターネット上を平文のテキストが流れてしまう。 また、チャット機能を呼び出す親元のWebページがHTTPSで提供されているものであれば、Mixed Content でブラウザによっては暗号化されていないWebSocket通信をブロックされることもあるだろう。 後からSSL/TLS対応を入れると往々にしてハマるので、ハナから対応しておくに越したことはない。

ということで、Socket.IOを用いたWebSocket通信をSSL/TLS対応させる。 以下、プログラム側での対応方法を記したが、NginxをWebSocket Proxyとして利用 できるので、NginxでSSL Terminationさせる手もある。

ポイント

f:id:uorat:20150830185442p:plain

  • ブラウザによってはWebSocket通信が利用できないことがあるため、Socket.IOでは、クライアントに適した通信プロトコルを自動選択する仕組みが実装されている。
  • セッションを張り続けて通信を行うWebSocketを使う以上、ボトルネックになりがちなロードバランサーを介することは推奨できない。
    • ロードバランサは最初の接続確立のみ利用し、WebSocket通信はクライアントからサーバに対して直刺しさせる

例えばAWSで運用している場合、SSL TerminationをELBに任せることが多いと思うが、WebSocket通信をELBを介さず行うため、サーバサイド (Socket.js on Node.js) で暗号化させる必要がある。

実装

サーバサイド

サーバに事前に配置したサーバ証明書、中間CA証明書、秘密鍵のセットをロードさせて起動することで、SSL対応が可能。 この対応で、Listenしたポートでhttps, wss通信が可能となる。

var fs = reqquire('fs');
var io = require('socket.io').listen(3000, {
    key : fs.readFileSync('/etc/pki/tls/private/your.domain.com.key').toString(),
    cert: fs.readFileSync('/etc/pki/tls/certs/your.domain.com.crt').toString(),
    ca: fs.readFileSync('/etc/pki/tls/certs/your.domain.com.cer').toString(),
    'log level':1
});

当然、証明書のドメインとWebSocket接続時にクライアントから指定するドメインは揃える必要がある。 ワイルドカードで証明書を作成していれば、例えば hostname.domain.com で接続させれば良い。

DNSにホスト名で名前解決できるようにA recordを登録しておく必要あり

クライアントサイド

以下のとおり、https で指定する。 "wss"ではない。 前に述べたとおり、handshake時にどのプロトコル (WebSocket / xhr-polling) で対話するかを採択するが、その通信は httpないしはhttpsで行われる。 handshakeが成功すると、wss または xhr-poolling over TLS で接続確立される。

<script src="https://hostname.domain.com:3000/socket.io/socket.io.js"></script>
<script>
    var socket = io.connect('https://hostname.domain.com:3000');

    ... your logic ...
</script>

ソース (※ 2016/5/15 追)

※ローカルのメモを転記しておく。

Socket.IOのdocumentaionにはSSL/TLS対応に関する記載が見当たらなかったが、ソース読んでみると明解。

Socket.IO

Socket.IO のコードを読んでみると、 `require('socket.io').listen した時に options.key があれば https#CreateServer 呼びだすロジックになっている。 ちょっと古いが、socket.io (v0.9.13) /lib/socket.io.js l63 - l66 抜粋

if (options && options.key)
  server = require('https').createServer(options);
else
  server = require('http').createServer();

Node.js - https.createServer

Official Documentationにあるとおり、 https.createServer の options は tls.createServer と同様で、以下のフィールドでSSL/TLS通信に必要なフィールドを指定できる。

https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener

終わりに

以上、Socket.IOでのWebSocket通信のSSL対応の一例としてプログラム側での対応方法を記したが、NginxをWebSocket Proxyとして利用 できるので、NginxでSSL Terminationさせるという方法もある。 手っ取り早く試すなら上記でも良いが、既にNginxを投入済みのサイトであればNginx WebSocket Proxyを用いても良いかも。

※ (2016/9/19 追記) Nginx 使った対応方法も記載しているので、あわせて参考にして下さい。

実践Node.js プログラミング (Programmer's SELECTION)

実践Node.js プログラミング (Programmer's SELECTION)

Socket.io + Redis PubSubでリアルタイムメッセージ配信

とあるサービスに「チャット機能」を追加しようという話になり、急ピッチで仕組みを用意することになった。 仕様/要件はふわっとしているものの、 2週間後にはリリースというケツは決まっている。 はてさてどうしたものかとその瞬間は思ったものだが、無事仕組みとして載せられたので、備忘記しておく。

リアルタイムチャット機能

要件は以下。

  • クライアントはWebブラウザとネイティブアプリ (iOS, Android)
  • 視聴者に軽量なテキストメッセージをbroadcastする
  • メッセージの永続化必須
  • 可用性/負荷分散も考慮する

例えば、動画を視聴しているとして、その動画の横に、自分含む視聴者のコメントが流れてくる、 ようなものを想像してもらえれば良い。

Socket.IO

「クライアント - サーバ間のリアルタイムなメッセージのやり取り」ということで、

という方針は早々に決まった。自動的にサーバは Node.js に決定。 ブラウザからのアクセスがあるため、xhr-polling / WebSocket をSocket.IO が両方サポートしていることは有難かったし、WebSocket通信を数行で始められる手軽さも魅力的だった。 ケツが2週間後なので実装コストがなるべく少なく済み、ビジネスロジックに集中できる方法を採択する必要があったため。

Socket.IOのポイントをまとめると、

  • WebSocket, xhr-polling をサポート、クライアントの動作環境に応じていずれかを自動採択する
  • "1 : 1" や "1 : N" のようなBasicな特定双方向通信、一斉配信 (broadcast)、接続単位を論理的に分離する 名前空間/namespace や 部屋/room 機能 など様々な配信方法をサポート

インストールは npm で一発 。 実装イメージは以下。

server side

var io = require('socket.io').listen(3000);
io.sockets.on('connection', function (socket) {
  
  ... your logic ...
  
});

client side

<script src="http://your.domain.com:3000/socket.io/socket.io.js"></script>
<script>
  var socket = io('http://your.domain.com:3000');
  socket.on('connect', function() {

    ... your logic ...

  });

  ... and so on...
</script>

ネイティブアプリ用のクライアントライブラリもある。 Socket.IOのGithub を見たところ、 C++ の実装もあるようだ。

Redis PubSub

Socket.IO on Node.js なWeb/Appサーバに対して、各クライアントはWebSocket通信で接続する。 接続数増と耐障害性を担保するためWeb/Appサーバの冗長化を考えた時に、一斉配信するメッセージを複数台のサーバにどのように伝達させるかで一瞬悩んだ。 所謂Publish / Subscribe ... ということで思い出したのがRedis。 "危険なほどのスピード" で有名なオンメモリKey-Value データストアで、扱えるデータ構造がMemcached と比べて豊富、ディスクへの永続化もあり人気のKVSだが、このRedis、PubSub も提供している。

1. 購読者 (subscriber) 、任意のkeyをsubscribeする

$ redis-cli
127.0.0.1:6379> SUBSCRIBE hoge
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hoge"
3) (integer) 1

2. (別のターミナルで) 出版者 (publisher) 、1で指定したkeyでメッセージをpublishする

$ redis-cli
127.0.0.1:6379> PUBLISH hoge "hello pubsub"
(integer) 1

3. 購読者 (subscriber) の端末に、2で配信されたメッセージが表示される

$ redis-cli
127.0.0.1:6379> SUBSCRIBE hoge
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hoge"
3) (integer) 1
1) "message"       ←追加
2) "hoge"          ←追加
3) "hello pubsub"  ←追加

Socket.IO - Redis の連携は容易。Socket.IOのバージョンによりインストール方法が若干変わる点に注意。

  • Socket.IO v0.9 系まで
    • Socket.IO 内に同梱されているため、特に追加インストールは必要なし。
    • 呼出は require('redis')
  • Socket.IO v1.0 以降
    • 外部ライブラリとして切り離された socket.io-redis をインストール
    • 呼出は require('socket.io-redis')

ちなみに、AWSのElasticCacheはRedisをサポートしており、Read Replica & Multi-AZも対応している。

PubSubはReplica Nodeに対しても有効で、PubSub sessionに関しても負荷対策が可能。

  1. Node.jsからReplica Nodeに対してSubscribe sessionを張っておく
  2. コメント投稿はMaster Nodeに対してPublish
  3. Read Replicaに対してSubscribeしている全購読者に配信される

WebSocketとELB

Socket.IO on Node.js サーバを冗長化させた場合の振り分け方も一工夫必要だった。 通常のHTTP通信と異なるため。注意点は以下の2点。

  • Cient - Server間の通信は通常のHTTP通信と異なりWebSocketで接続持続される
  • Socket.ioによるhandshake処理は必ず同一サーバに接続させなければならない

例えば、ロードバランサーを上段に挟む場合、全てのセッションがロードバランサーに対して張られることとなる。 通常のHTTPと異なりWebSocketのセッションを張り続けるという特性上、接続を集約させるロードバランサーボトルネックになる危険がある。 また、ロードバランサーによっては、Socket.IO/WebSocketの間に立てない場合がある。例えば、AWSのELB。

AWSのSolution Architectも、ELBは最初のセッション確立時にのみ利用してdispatchしてWebSocketさせるのが良いと言及している。

教えに従い、接続情報を返すAPIをクライアント側で呼出してから、サーバに対して直接WebSocket通信させる方式とした。

システム構成

行き着いたシステム構成は以下。

主機能がApache / PHPで動くWebアプリケーションのため、APIは既存資産を流用して実装。 Apahce/PHP を Node.js/Socket.IOと同梱させAPIを提供するようにした。 同梱させた理由は、接続情報管理の実装を省き自分のホスト名を返すようにしたかったため。 本来は、APIサーバは切り離して、ステータス含めて接続情報を管理するようにしたほうが望ましいと思う。

f:id:uorat:20150830185442p:plain

次回は、WebSocket接続のSSL/TLS対応をまとめようと思う。

Redis入門 インメモリKVSによる高速データ管理

Redis入門 インメモリKVSによる高速データ管理

Amazon Web Services クラウドデザインパターン設計ガイド 改訂版

Amazon Web Services クラウドデザインパターン設計ガイド 改訂版

Amazon Web Services パターン別構築・運用ガイド

Amazon Web Services パターン別構築・運用ガイド