katsura

はじめまして、入社半年のkatsuraです。初投稿です。よろしくお願いします。

品質管理グループにてHTMLメールのテストを日々の業務で行っていた中で、テストを補助するツールを作成しました。今回はそのツールについて紹介したいと思います。

HTMLメールのテスト

HTMLメールのテスト項目の一つに、リンクチェックというものがあります。

リンクチェックとは、HTMLメール内のリンクテキストや画像とリンク先のURLが、リンク仕様書通りに正しく記載されているかのテストです(下の図参照)。

これを行うことにより、例えば「商品画像をクリックしたら、別の商品のページが開いてしまった」というような事故を防ぐことができます。
html_link_check
 

このリンクチェックというテストは、誰にでも出来る単純なテストですが、正確にテストしようとすると面倒なテストです。

リンクのURLが上の図のようにわかりやすいものであれば良いのですが、商品URLの場合複雑なものも多いため、ひと目見ただけでは仕様書のURLと一致しているか判断がつかないものが多いです。

ツールを作成する以前、HTMLメール制作部門ではHTMLメールのリンク一つ一つにカーソルを当て、ブラウザのステータスバーに表示されるURLが正しいかどうかを一つづつ目視で確認していました。

しかしリンク一つ一つを正確にチェックする作業は神経を使う作業であり、また人の手で行うものなので、どうしてもミスが出やすいものでした。

そこで、このテストを簡単に行えるようにするための補助ツールを作成することにしました。補助ツールの出力としては、以下の点を満たすものを目標としました。

  1. HTMLメール内に存在するURLが仕様書のどのキャプションに存在するかをHTMLメール内に直接表示する
  2. 仕様書に存在するURLがHTMLメール内に存在するかどうかの結果を表で表示する

これを図で説明すると以下のようになります。

html_link_check_image

実装の選択肢

補助ツールのイメージが出来たところで、まずどの言語あるいは、どの手法を用いてこの補助ツールを実装するか検討しました。

  • RubyやPythonなどのローカル実行のスクリプト言語を用いた実装
    →配布先で言語のインストールが必要であり、プログラマー以外への配布が難しいので見送りました
  • Excel VBAによる実装
    →HTMLの解析をVBAで行うのは大変そうだったので見送りました
  • JavaやC#などを用いたGUIアプリケーションによる実装
    →利用者側のマシンで動かすアプリケーションを考えていたのですが、更新時に利用者側に都度アップロード作業が発生してしまい面倒なため見送りました
  • PHPなどサーバー側の言語による実装
    →サーバーサイドのプログラミング経験があまりなかったので見送りました
  • ブックマークレットという技術を使ったJavaScriptによる実装
    →HTMLとの親和性が高く、ブラウザで実行するので利用者側で特別な準備が不要で利用できる上、更新に際しての再配布も利用者が意識する必要がないため採用しました

というわけで、様々な言語による実装を考え、最終的に行き着いたのがブックマークレットという技術を使ったJavaScriptによる実装でした。

ブックマークレットという形で提供するのであればブラウザさえあれば実行できるので、プログラミングに詳しくない人に使ってもらうのに問題ありません。また、ブックマークレットであれば、ローカルのHTMLファイル対してもWeb上のHTMLファイルに対しても同じ方法で実行できるというメリットもあります。

以降は、ブックマークレットについての簡単な解説と、今回実装したJavaScriptの紹介を行います。

ブックマークレットについて

ブックマークレットについて今更ですが簡単に説明しようと思います。

ブックマークレットとは、一言で言うと 「JavaScriptを実行できるブックマーク(お気に入り)」 のことです。ブラウザのプラグインと比べるとできることには制限がありますが、ブラウザに依存せずに実行でき導入も簡単というメリットがあります。

では、具体的にブックマークレットはどのように使うものなのか、ブックマークレットのサイトにて公開されている、「Internet Archive 検索」というブックマークレットを例に説明していきたいと思います。

Internet Archiveとは世界中のWebサイトのログを保存・公開しているサイトであり、このブックマークレットを使うと任意のWebページのログをワンクリックで表示することが出来ます。

このブックマークレットを登録するためには、ブックマークレットを公開しているページヘいき、ブックマークレットを登録するだけです。

bookmarklet2

 

あとは、ブックマークレットを使いたい画面で、先ほど登録したブックマークレットをクリックするだけです。

bookmarklet3

このように、ブックマークレットという技術を使うと簡単にJavaScriptを実行することができます。

完成した HTML リンクチェック用のブックマークレット

さて、そんなわけで実装完了したブックマークレットを早速ご紹介します。以下のリンクから登録することができます。

対象とするページの文字コードごとにブックマークレットを用意しています。

使い方

まず、ブックマークレット用のリンクをブックマークに登録します。

html_link_check_sample1

登録したブックマークレットを実行するとリンクチェッカが起動します。

ブックマークレットとHTMLファイルの文字コードが一致していないと文字化けしてしまうので、対応する文字コードのブックマークレットを実行するようにしてください。

html_link_check_sample2

タブ区切りで仕様書を入力し、実行を押すとリンクチェックが実行されます。

html_link_check_sample3

実行結果は以下のようになります。

html_link_check_sample4

ソースコード

今回作成したブックマークレットは、ブラウザに登録するJavaScriptとサーバーに設置するJavaScriptの2つで構成されています。

  • ブックマークレット用のJavaScriptはサーバー用のJavaScriptとjQueryを呼びだすことのみをしています。
  • サーバーに設置するJavaScriptはリンクのチェックなどの主要な処理を行います。

ブックマークレット用のJavaScript

javascript:(function(f,s){s=document.createElement("script");s.src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js";document.body.appendChild(s);s=document.createElement('script');s.src='http://lab.tricorn.co.jp/wp-content/uploads/2015/04/linkchecker_simple.js';s.onload=function(){f(jQuery.noConflict(true));};document.body.appendChild(s);})(function($){init($);});

サーバーに設置するJavaScript

var colorRed = "#d74a38", colorBlue = "#0063a4";
var colorYellow = "#ffec67", colorGray = "#e6e6e6";

//起動画面表示
//仕様書の入力欄などを作成
function init($,text){
  if( $("#fixedArea").length != 0 ){
    alert("リンクチェッカは起動済みです!");
    return;
  }
  $("body").append("<div id='fixedArea'></div>");
  $("#fixedArea").css({"position":"fixed"
                     , "top":"0px"
                     , "left":"70%"
                     , "height":"800px"
                     , "width": "30%"
                     , "overflow": "scroll"
                     , "resize" : "vertical"
                     , "background": "white"
                     , "z-index": "99"
                       });
  $("body").prepend("<div id='fixedSpace'></div>");
  $("#fixedArea").append("<div id='inputArea'></div>")
  $("#inputArea").css({"margin":"auto","width":"800px"});
  $("#inputArea").append("<button id='checkAll'>チェック</button>")
  $("#inputArea").append("<textarea name='linkText' id='linkText' rows='6' cols='80'>")
  $("#linkText").val(text);
	//checkAllボタンが押されたらリンクチェックの結果を表示する
  $("#checkAll").click(function(){
    tableShow($);
  });
  $(window).on('load resize', function(){
    $(".linkComment").each(function(){
      a = $(this).closest("a");
      var top = a.position().top;
      var left = a.position().left;
      $(this).css({"left" : "" + left + "px"});
    });
  });
}
//結果の表示
function tableShow($){
  var text = $("#linkText").val();
  var line = text.split("\n");
  var urlList = [];
  var undefined;
  
  //リンク仕様書データの読み込み
  for( var i = 0; i < line.length; i++ ){
    var str = line[i].split("\t");
    if( str.length > 1 ){
      urlList.push([ i, str[0], str[str.length - 1] , 0 ] );
    }
  }
  //リンクの巡回
  $("a").each(function(index){
    var a = $(this);
    var url = a.attr("href");
    if( url ===  undefined ){return true;}
    var flag = false;
    var linkedCount = 0;
    var top = a.position().top;
    var left = a.position().left;
    if(a.children("img").length > 0){
      //画像リンクの場合には赤枠で囲む
      a.children("img").each(function(index2){
        $(this).css({"border-color":colorRed,"border-width":"3px","border-style":"solid"});
        top = $(this).position().top;
        left = $(this).position().left;
      });
    }
		//リンクの説明用のdivを追加
    a.prepend("<div id='link" + index + "' class='linkComment'></div>");
    $("#link" + index ).css({"position":"absolute"
                            ,"display":"block"
                            ,"color":"white"
                            ,"background":colorRed
                            ,"border-color":"white"
                            ,"border-width":"2px"
                            ,"border-style":"solid"
                            ,"width":"180px"
                            ,"height":"20px"
                            ,"font-size":"12px"
                            ,"font-weight":"bold"
                            ,"text-align": "left"
                            ,"scroll":"visible"});
    if( left != 0 ){
      $("#link" + index ).css({"left" : "" + left + "px"});
    }
    var divText = "";
    for( var i = 0; i < urlList.length; i++ ){
      if( urlList[i][2] == url ){
        flag = true;
        divText = divText +"<span id='jump" + i + "_" + urlList[i][3] + "'>" + i + ":" + urlList[i][1] + "</span>";
        urlList[i][3]++;
        linkedCount++;
        $("#link" + index ).addClass("linkComment" + i);
      }
    }
    if(flag == false){
      divText = divText +"<span id='jump" + urlList.length + "_" + 0 + "'>"+urlList.length+"</span>";
      urlList.push([ urlList.length, "HTMLにのみ存在", url, 1 ]);
      linkedCount++;
    }
    $("#link" + index ).css({"height":"" + linkedCount * 12});
    $("#link" + index).html(divText);
  });
  //入力ボックスの消去
  $("#inputArea").remove();
  //閉じるボタンを追加
  $("#fixedArea").append("<button id='close'>閉じる</button>");
  $("#close").click(function(){
    $("#fixedArea").remove();
    $(".linkComment").remove();
    $("img").css({"border-width":"0px"});
  });
  //結果表示の表を追加
  $("#fixedArea").append("<table id='result' style='width:1000px'></table>");
  var headerText = '<tr><th style="width:20px">id</th>'
  headerText = headerText + '<th style="width:60px">出現回数</th>';
  headerText = headerText + '<th style="width:220px">キャプション</th>';
  headerText = headerText + '<th style="width:700px">URL</th></tr>';
  $("#result").append(headerText);
  for( var i = 0; i < urlList.length; i++ ){
    $("#result").append("<tr id='tr" + i + "'>");
    $("#tr" + i).append("<td>" + i + "</td>");
    var tmpText = "";
    for( var j = 0; j < urlList[i][3]; j++ ){
      tmpText = tmpText + "<a href='#jump" + i + "_" + j + "'>" + (j + 1) + "</a> ";
    }
    $("#tr" + i).append("<td>" + urlList[i][3] + "回(" + tmpText +  ")</td>");
    $("#tr" + i).append("<td>" + urlList[i][1] + "</td>");
    $("#tr" + i).append("<td>" + urlList[i][2] + "</td>");
  }
  $("#result").css({"margin":"auto"});
  for( var i = 0; i < urlList.length; i++ ){
    if(i % 2 == 0){
      $("#tr" + i).css({"background":colorGray});
    }else{
      $("#tr" + i).css({"background":"white"});
    }
    if( urlList[i][1].indexOf("HTMLにのみ存在") > -1 ){
      $("#tr" + i).css({"background":colorRed});
      for( var j = 0; j < urlList[i][3]; j++ ){
        $("#jump" + i + "_" + j ).parent().addClass("errorComment");
        $("#jump" + i + "_" + j ).parent().css({"background":colorYellow
                                               ,"color":"black"
                                               ,"border-color":"black"
                                               });
        $("#jump" + i + "_" + j ).html($("#jump" + i + "_" + j ).text() +  ":HTMLにのみ存在!");
      }
    }
    if( urlList[i][3]==0 ){
      $("#tr" + i).css({"background":colorYellow});
    }
  }
  $("#result tr").css({"border-width": "1px"
                   ,"border-style": "solid"
                   ,"border-color": "black"
                   ,"color":"black"
                   ,"font-size": "11px"
                   });
}

最後に

今回、HTMLファイルのリンクチェックツールの作成にJavaScriptによるブックマークレットという技術が使えるということを紹介しました。

JavaScriptによるブックマークレットという技術は、リンクチェックに限らずHTMLファイルに関するツールを作成するのに使うことができると思います。

もし、業務の中でHTMLファイルに関するツールを作成する機会があった際には、JavaScriptによるブックマークレットのことを思い出していただけたらと思います。

参考URL

Tags: , , , ,

morikawa

久しぶりの投稿になります。インフラグループの Morikawa です。
Labs の執筆推進のメンバーなのですがここ数ヶ月更新が滞っておりすいません。

前回の kamo さんの記事投稿で昨年 6 月頃にスタートした Labs 再開プロジェクトが一周しまして、また次の一周をスタートする準備を進めているところです。

今回は再び Zabbix による監視ネタになります。大したものではないのですが、意外に検索しても見つからない情報だったので役に立つ方には役に立つのではないかと。

今回監視したいのは 特定の時間が経過したプロセスの数 です。デーモンなど長期間動作していて当然のプロセスは問題ないのですが、バッチ系の処理を行っているサーバ上で、そのバッチ処理プロセスが残ったままになってしまう、という経験はおありの方もいるのではないでしょうか。もちろんバッチ処理を見なおして残ったままにしないよう改修するのが根本的な良い解決方法となりますが、とりあえずそういった状況を自動検知したいというニーズはあるかと思います。今回はそれを Zabbix にやらせようという話です。

とりあえずサクッと導入できちゃう設定編と、その内容の解説編とに分けて紹介します。

設定編

監視されるサーバ側での設定

Zabbix Agent の設定ファイルである zabbix_agentd.conf か、もしくはそのファイルからインクルードされるファイルなどに以下の UserParameter を記載しましょう。ちなみにこれの動作確認は CentOS で行ってます。

UserParameter=custom.proc.elapsed.num[*],/bin/ps -eo user:32,etime,command | grep -F "$3" | awk '$(1) == "$2" || "$2" == "" {print $(2)}' | sed 's/:\|-/ /g;' | awk '{print $(4)" "$(3)" "$(2)" "$(1)}' | awk 'BEGIN { x=0 ; if ("$1" == "") {min=-1} else {min="$1"+0} } {if ($(1)+$(2)*60+$(3)*3600+$(4)*86400 > min) x+=1 } END {print x}'

Zabbix Server 側でのアイテムの設定

Zabbix Server 側のアイテムの設定では、キーに以下を設定します。

custom.proc.elapsed.num[<ELAPSED_SEC>,<USER>,<CMDLINE>]

ELAPSED_SEC がプロセスの経過時間、USER がプロセスの実行ユーザ、CMDLINE がコマンドラインに含まれる文字列となり、最初の ELAPSED_SEC で指定する秒数よりも長く動作しているプロセスの数が整数値として得られます。

具体的には以下のようになりますね。root で起動していて “sshd:” という文字列を含み 86400 秒以上経過したプロセスの数が取得されます。

custom.proc.elapsed.num[86400,root,"sshd:"]

ちなみに ELAPSED_SEC, USER, CMDLINE はそれぞれ省略可能で、ELAPSED_SEC は -1 秒以上 (つまり全てのプロセスが対象)、USER は全てのユーザが対象、CMDLINE はあらゆるコマンドを対象とすることになります。

Zabbix Server 側でのトリガーの設定

後はトリガーの設定さえ行えば自動検知が可能になります。閾値は対象に応じて様々ですが、例えば上記の例に出したアイテムで取得した数が 1 以上の場合にアラートを上げたいのであれば、トリガーには以下のように設定すれば OK です。

{custom.proc.elapsed.num[86400,root,"sshd:"].last(0)}>0

以上で設定は完了です。

解説編

さて、こちらは特に UserParameter の内容についての解説編になります。上記で UserParameter で指定した内容を改行で見やすく表示してみます。

UserParameter=custom.proc.elapsed.num[*],
  /bin/ps -eo user:32,etime,command 
  | grep -F "$3" 
  | awk '$(1) == "$2" || "$2" == "" {print $(2)}' 
  | sed 's/:\|-/ /g;' 
  | awk '{print $(4)" "$(3)" "$(2)" "$(1)}' 
  | awk '
    BEGIN 
    { 
      x=0 ; 
      if ("$1" == "") {
        min=-1
      } else {
        min="$1"+0
      } 
    } 
    {
      if ($(1)+$(2)*60+$(3)*3600+$(4)*86400 > min) x+=1 
    } 
    END 
    {
      print x
    }'

ps コマンドに上記オプション -eo user:32,etime,command をつけると、以下の様なフォーマットでプロセス情報が取得されます。

apache     5-17:24:45 /usr/sbin/httpd
root     120-02:31:51 smbd
postfix         52:05 pickup -l -t fifo -u

grep で CMDLINE に指定した内容で絞込み、その次の awk で USER で指定した内容で絞り込んで、時間のみ表示させて次に渡します。なお、 USER を省略した際に全てのユーザを対象にするように || “$2” == “” が入ってます。

次に sed で時刻部分の “-” および “:” を空白にすることで、例えば上記の出力は以下のようになります。

  5 17 24 45
120 02 31 51
       52 05

それを awk の “{print $(4)” “$(3)” “$(2)” “$(1)}” で左右をひっくり返して表示させます。

45 24 17   5
51 31 02 120
05 52

最後に、awk の “$(1)+$(2)*60+$(3)*3600+$(4)*86400” で、秒数に置き換えます。空文字は awk は 0 扱いするため、結果は正しくなります。後はその置き換えた数値と ELAPSED_SEC を比較し、前者のほうが大きいものをカウントして数値として返すという挙動になります。BEGIN の中身は、主に ELAPSED_SEC 省略時に文法エラーを生じさせないための処理です。

ちなみに、このひっくり返してから秒・分・時・日を秒に換算するという計算方法はもちろん(?)私のアイディアではなく、Parse ps etime output into seconds にて紹介されていた内容です。見た時は目からウロコでした。

なお、気になる方は気になったかと思いますが、一般に awk では引数を $1, $2, … と表現しますが、$(1), $(2) としていますね。これは $1, $2 が Zabbix の UserParameter の引数としても扱われるため、普通に $1 などと書くとそちらで置換されてしまうためです。awk の引数は Zabbix 側に置換されない $(1) という表記にしています。

まとめ

さて、今回は簡単でしたが、経過時間で判定させたプロセス数の監視について記事にしてみました。awk、grep、sed などと監視ツールを組み合わせるとかなり柔軟に何でも監視できちゃいますね。要望がどれくらいあるかは分かりませんが、お役に立てば幸いです。

ちょっと早いですが、皆さん良いお年を!

Tags: , , ,

kamo

AnsibleLogo_transparent_web

はじめまして、入社1年と数ヶ月のkamoです。初投稿になります。

最近、巷で噂のRaspberry Pi B+を買ってしまいました。

え?この記事を書かずにそんなことしてん?何してんの? 他にもやることあるよね、kamo?
という心の声が聞こえてきましたが、学生の頃、テスト期間中に勉強しなければいけないのに、机の上や部屋の掃除をしてしまう感じで買ってしまいました。すみません。
Raspberry Pi B+で何をするかは、まだ考えていません。色々と片付いたら弄ろうと思っていますので、ご勘弁を。

さて、最近はサーバを構成管理ツールで管理するのが流行っていますが、トライコーンでは Ansible を使っての構成管理を試してみています。

その中でハマったのが shellcommand モジュールを使いながら冪等性を保つ方法です。VagrantとAnsibleで開発環境を構築 – テストとハマったこと でも紹介されていますが、これらのモジュールは普通に使っていると冪等性を保った挙動をしてくれません。

もちろん用意されているモジュールで事足りれば良いのですが、そうでない場合も多々あります。

例えば、Linux で Windows とファイル共有などする場合に重宝する samba を構築する場合を考えてみます。

ざっくり Playbook は以下のようになります。(smb.conf の変更なども実際には必要ですが割愛しています)。shell モジュールはユーザの追加の際に必要になります。コマンド自体は pdbedit を用い、-t オプションを使うことでパスワードを標準入力から読み込むようにしてワンライナーで書いています。

- name: be sure samba is installed
  yum: pkg=samba4,samba4-common,samba4-client state=latest

- name: Linux User Setting for samba
  user: name=smbuser

- name: Add samba user
  shell: echo -e "passwd\npasswd\n" | pdbedit -t -a smbuser

ではこの Playbook を用いて ansible-playbook を実行してみましょう。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
changed: [samba-server] => {"changed": true, "item": "",
      "msg": "", "rc": 0, "results": ["Loaded plugins: fastestmirror,
      presto\nLoading mirror speeds from cached ...
      \n\nComplete!\n"]}

TASK: [servers | Linux User Setting for samba] ****************************
changed: [samba-server] => {"changed": true, "comment": "",
      "createhome": true, "group": 1000, "home": "/home/smbuser", "item": "",
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "stderr": ...
      "system": false, "uid": 1000}

TASK: [Add samba user] ****************************************
changed: [samba-server] => {"changed": true,
      "cmd": "echo -e \"passwd\\npasswd\\n\" | pdbedit -t -a smbuser ",
      "delta": "0:00:00.046766", "end": "2014-10-10 17:24:40.143402", "item": "",
      "rc": 0, "start": "2014-10-10 17:24:40.096636", "stderr": "",
      "stdout": ... }

PLAY RECAP ********************************************************************
samba-server: ok=0    changed=3    unreachable=0    failed=0

無事に samba を利用するためのパッケージのインストールと samba でのアクセス用ユーザ smbuser が作成されたようです。確認してみましょう。

[root@samba-server ~]# rpm -qa | grep samba
samba4-common
samba4-libs
samba4
samba4

[root@samba-server ~]# pdbedit -L
smbuser:1000:

インストールとユーザの作成、無事完了していますね。

さて次に、ansible-playbook コマンドを再度実行してみましょう。
本来 Ansible に期待するのは、冪等性が保たれて何も実行されない、つまり ok=3, change=0 となることです。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
ok: [samba-server] => {"changed": false, "item": "", "msg": "",
      "rc": 0, "results": ["All packages providing samba4 are up to date",
      "All packages providing samba4-common are up to date",
      "All packages providing samba4-client are up to date"]}

TASK: [servers | Linux User Setting for samba] ****************************
ok: [samba-server] => {"append": false, "changed": false, "comment": "",
      "group": 1000, "home": "/home/smbuser", "item": "", "move_home": false, 
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "uid": 1000}

TASK: [servers | Add samba user] ****************************************
changed: [samba-server] => {"changed": true,
      "cmd": "echo -e \"passwd\\npasswd\\n\" | pdbedit -t -a smbuser ",
      "delta": "0:00:00.046766", "end": "2014-10-10 17:24:40.143402",
      "item": "", "rc": 0, "start": "2014-10-10 17:24:40.096636",
      "stderr": "", "stdout": ... }

PLAY RECAP ********************************************************************
samba-server: ok=2    changed=1    unreachable=0    failed=0

おや、おかしいですね。changed=1 が表示されてしまいました。まあ当然は当然で、shell や command モジュールは検査の機構が無く、現状と結果の差を確認せずにコマンドを実行してしまうためです。

pdbedit での同名のユーザを追加しているだけなので実質的には実行前後で同じではあるのですが、本来調べればわかるのに pdbedit で実際にユーザ追加のコマンド叩いちゃってますし、changed=1 って表示もされちゃっているので、Ansible で期待される冪等性は保てていないと言って良いでしょう。

この Playbook を単体で使っているだけであれば「ああ分かってることだし」とスルーもできますが、多数の Playbook からなるインストール処理を行っている際、本来 changed=0 が期待される所、shell や command モジュールを使った箇所の数だけ changed が出てしまうのはやっぱり微妙ですね。

要するに条件判定を自作すればいいわけなのですが、以下のようなタスクを追加すればそれが実現できます。

- name: Samba User List
  shell: pdbedit -L | awk -F":" '{print $1}'
  register: samba_user_list
  changed_when: false
  always_run: yes

実行すると以下のような結果が得られます。

TASK: [servers | Samba User List] *****************************************
ok: [samba-server] => {"changed": false,
      "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ", 
      "delta": "0:00:00.021991", "end": "2014-10-10 17:24:39.250869", 
      "item": "", "rc": 0, "start": "2014-10-10 17:24:39.228878", 
      "stderr": "", "stdout": "smbuser", "stdout_lines":["smbuser"]}

“always_run: yes” のため常に実行され、”changed_when: false” のため常に ok と表示されるようになっています。”register: samba_user_list” によって、pdbedit -L | awk -F":" '{print $1}'の結果が、samba_user_list.stdout (テキスト形式) および samba_user_list.stdout_lines (配列形式) に格納されます。

これを利用して Add samba user タスクを書き直してみましょう。

- name: be sure samba is installed
  yum: pkg=samba4,samba4-common,samba4-client state=latest

- name: Linux User Setting for samba
  user: name=smbuser

- name: Samba User List
  shell: pdbedit -L | awk -F":" '{print $1}'
  register: samba_user_list
  changed_when: false
  always_run: yes

- name: Add samba user
  shell: echo -e "passwd\npasswd\n" | pdbedit -t -a smbuser
  when: samba_user_list.stdout_lines.count("smbuser") < 1

最後の行に、配列 samba_user_list.stdout_lines の中から “smbuser” の数を数え、これが 1 以下 (要するに見つからなかった場合) に実行するよう条件分岐が追加されました。

では再度 ansible-playbook を実行してみましょう。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
ok: [samba-server] => {"changed": false, "item": "", "msg": "",
      "rc": 0, "results": ["All packages providing samba4 are up to date",
      "All packages providing samba4-common are up to date",
      "All packages providing samba4-client are up to date"]}

TASK: [servers | Linux User Setting for samba] ****************************
ok: [samba-server] => {"append": false, "changed": false, "comment": "",
      "group": 1000, "home": "/home/smbuser", "item": "", "move_home": false, 
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "uid": 1000}

TASK: [servers | Samba User List] *****************************************
ok: [samba-server] => {"changed": false,
      "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ", 
      "delta": "0:00:00.021991", "end": "2014-10-10 17:24:39.250869", 
      "item": "", "rc": 0, "start": "2014-10-10 17:24:39.228878", 
      "stderr": "", "stdout": "smbuser", "stdout_lines":["smbuser"]}

TASK: [servers | Add samba user] ****************************************
skipping: [samba-server]

PLAY RECAP ********************************************************************
samba-server: ok=3    changed=0    unreachable=0    failed=0

Add samba user のタスクが skipping となっている事がわかります。残念ながら skipping のタスクは ok のカウントには入りませんが、changed=0 にはなっていることが分かりますね。また不要な pdbedit によるユーザ追加も実施されていません。もちろん “smbuser” ユーザが未作成の場合はユーザの作成が行われ、changed のカウントが一つ増えます。

このように、一手間加えれば shell や command モジュールでも冪等性を保った Playbook を書くことが可能です。いかがでしょうか? 今回は samba を例に紹介しましたが iptables の設定であったりその他いろいろ応用が効くやり方かと思います。

さて、本題は以上なのですが、実際にこのようなタスクを書くにあたり、register で指定した変数にどのような値が格納されているのかが気になりますね。その場合は是非 debug モジュールを使ってみてください。Samba User List タスクの下に以下のような debug モジュールタスクを追加すると、変数の内容が表示されます。

- debug: var=samba_user_list

ansible-playbook を実行すると以下のように表示されます。

【”smbuser” ユーザ作成前】

TASK: [servers | debug var=samba_user_list] *******************************
ok: [samba-server] => {
    "item": "",
    "samba_user_list": {
        "changed": false,
        "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ",
        "delta": "0:00:00.024101",
        "end": "2014-10-12 13:20:44.921479",
        "invocation": {
            "module_args": "pdbedit -L | awk -F\":\" '{print $1}'",
            "module_name": "shell"
        },
        "item": "",
        "rc": 0,
        "start": "2014-10-12 13:20:44.897378",
        "stderr": "",
        "stdout": "",
        "stdout_lines": []
    }
}

【”smbuser” ユーザ作成後】

TASK: [servers | debug var=samba_user_list] *******************************
ok: [samba-server] => {
    "item": "",
    "samba_user_list": {
        "changed": false,
        "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ",
        "delta": "0:00:00.024101",
        "end": "2014-10-12 13:20:44.921479",
        "invocation": {
            "module_args": "pdbedit -L | awk -F\":\" '{print $1}'",
            "module_name": "shell"
        },
        "item": "",
        "rc": 0,
        "start": "2014-10-12 13:20:44.897378",
        "stderr": "",
        "stdout": "smbuser",
        "stdout_lines": [
            "smbuser"
        ]
    }
}

黄色で記載した部分が差分になります (見やすさのためにここでそのように色分けしただけで、ansible-playbook 実行時にこのような色で表示されるわけではありません)。このように実際に取得する情報の前後の差分を確認することで、実際に何を判定に用いればよいかが分かりますね。

なお、Ansible ドキュメントの Conditionals (条件分岐) では、register した文字型の変数に対して find 関数を使っていますが、find は対象の文字列に対して特定の文字列が含まれているかどうかを判定するものであるため、今回の場合だと仮に “smbuser2” というユーザがいると “smbuser” というユーザは作らないことになります。

配列に対して使用する count 関数は配列内から完全に一致するもののみの数を返してくれるため、今回はこちらを採用しました。

これらの関数は Ansible というよりは Python の関数なので、条件判定などをより細かく行いたい場合は Python の関数をいろいろ調べてみると良いのかもしれません。

一番便利なのは正規表現でマッチさせる方法なのですがその方法は調べてみましたがわかりませんでした。Ansible 内の register で保存した変数に対して正規表現でマッチをかける方法をご存じの方は教えて頂けると幸いです。

記事が書き終わりましたので冒頭に書きました Raspberry Pi B+ をどうしようかとさっそく考えてます。
家電をスマホで操作できるようにしようか、消費電力の少ない音楽配信サーバにしようか、
うちのお犬様がケージから脱出する姿を世の中に披露しようか。。。
悩んでるだけで、何もしなくなりそうなので、次の記事にRaspberry Piを掲載できるようにいじり倒します。

Tags: , , ,

iwan

こんにちは、月日は流れ会社ではすっかり古株の iwan です。
今日は、自分の放送局を作ってみたい方に、Podcast サーバを作って iTunes で Podcast を購読するところまでを解説します。

Podcast をご存知でしょうか? Wikipedia によると、「ポッドキャスト(英: Podcast)とは、インターネット上で音声や動画のデータファイルを公開する方法の1つであり、オーディオやビデオでのウェブログ(ブログ)として位置付けられている。インターネットラジオ・インターネットテレビの一種である。」と書かれており、つまり音を綴ったブログですね。

私は 3 年ほど前からボランティアで東海地方のコミュニティ FM 番組の制作・収録を手がけており、番組 MC から番組を世界中に公開できないものか?という相談がきっかけで Podcast サーバを構築することになりました。

ボブディランの歌のタイトルのように、時代は変わり、今では誰もがいつでも気軽にインターネットを使ってブロードキャストできてしまう時代になったのですね。

さて、本題から外れてしまいそうなので、話を戻します。

iTunes の Podcast 関連情報の Podcast を制作する を読むと、 iTunes で Podcast を購読するためには、以下2点をクリアする必要があります。

  1. RSS 2.0 仕様に準拠したフィードを作成する。
  2. メディアファイルと作成した RSS フィードを置く場所を用意する。

今回は、DirCaster という RSS を自動作成してくれるオープンソースのソフトウェアを使用します。

DirCaster を利用することで、事前に設定ファイルで指定したディレクトリにメディアファイルをアップロードすると、自動的に RSS フィードに反映されるようになります。

事前に用意するもの

  • レンタルサーバ(PHPが利用可能なもの)
  • 配信する音源ファイル
    • 今回の記事で用意したのは自作の音源になります

作業の流れは以下のようになります。

  1. DirCaster を展開し、設定ファイルを編集する
  2. DirCaster をレンタルサーバにアップロードする
  3. メディアファイルのアップロード用のディレクトリを作成し、音源をアップロードする
  4. RSSフィードを取得しメディアファイルの情報が反映されていることを確認する
  5. iTunes をインストールする
  6. Apple IDを作成する
  7. iTunes を使って Podcast を購読する
  8. 新しいメディアファイルをアップロードして、フィードが更新されていることを確認する

今回使用するレンタルサーバ

私自身が運用していて、稼働実績があるのは、ロリポップ!レンタルサーバーのチカッパプランです。今回はチカッパプランの利用を想定して進めていきますが、他のレンタルサーバでもだいたい同じようにできると思います。

DirCaster を展開し、設定ファイルを編集

DirCaster をダウンロードします。 2014 年 7 月時点では、最新バージョンは DirCasterV09j.zip です。

DirCasterV09j.zip を展開すると以下の構造となっています。

DirCasterV09j
|-- .htaccess
|-- changelog for DirCaster.txt
|-- config_inc.php
|-- copyto_dircaster.php
|-- dircaster.php
|-- doppler.jpg
|-- DS_Store
|-- index.php
|-- juice_receiver.gif
|-- list.txt
|-- override.1st
|-- override.css
|-- override.js
|-- override.php
|-- override_ajax_json.php
|-- override_ajax_util.php
|-- override_notes.php.htm
|-- podcast_logo_grey.png
|-- ReadMe.1st
|-- Required-Remote-Media-Values.txt
|
|-- lib_getid3
|
`-- override_files

展開したファイルで設定変更が必要なのは、config_inc.phpです。
config_inc.phpで変更が必要な項目について説明します。

$remoteMedia
ローカルか外部のストレージサービスからの読み込みに対応するモードのどちらかを選択する。 今回はシンプルにローカルを参照するので、「0」とします。
$mediaDir
メディアファイルを置くディレクトリを指定します。今回は「audio」入力し、同名のフォルダを作成します。
$titleTAG
番組の名前を入力します。
$descriptionTAG
番組の説明文を入力します。
$linkTAG
番組を紹介しているHPなどのURLを入力します。
$copyrightTAG
著作権表記が必要な場合に入力します。

次に iTune Podcast で必要な項目について説明します。

$summaryTAG
番組の説明文を入力します。
$authorTAG
著者・作者の名前を入力します。
$ownerNameTAG
この番組のオーナーの名前を入力します。
$ownerEmailTAG
この番組のオーナーのE-mailアドレスを入力します。
$topCategoryTAG
iTunes Podcast で登録するカテゴリを選択します。
$subCategoryTAG
iTunes Podcast で登録するサブカテゴリを選択します。

index.phpは、私が使いはじめた頃にはなかったプログラムで、どうやらマイページを表示するようなのですが、今回は取り上げません。

配信するメディアファイルを用意します。
iTunes Podcast がサポートしているファイルフォーマットは以下の通りです。

  • m4a、.mp3、.mov、.mp4、.m4v、.pdf、.epub (参考)

DirCaster では、config_inc.php の $sftypes を以下のように定義しています。今回はこれをそのまま利用します。

    $sftypes = "(.mp3 .m4a .asf .wma .wav .avi .mov .m4b .m4v)";

DirCaster をレンタルサーバにアップロード

config_inc.php の編集が終わったら、契約したレンタルサーバーへソースをアップロードします。
アップロードするデータとして、ボランティアで制作しているラジオ番組データの使用許可をもらい、テスト用に test-radio.mp3 というデータを作りました。今回は FileZilla を使ってアップロードをします。
filezilla1

メディアファイルのアップロード用のディレクトリを作成し、音源をアップロード

audioディレクトリを作成します。
filezilla4
ディレクトリパーミッションを確認します。
filezilla2
filezilla3

[OK] をクリックし、作成した audio フォルダにテスト用のデータをアップロードします。

RSSフィードを取得しメディアファイルの情報が反映されていることを確認

feed1
item のところにアップロードしたファイルのデータが表示されていれば、DirCaster が自動生成し成功していることになります。

iTunes のインストールと Apple ID の作成

まず Apple のサイトから iTunes をダウンロードしてインストールします。簡単ですので詳細は割愛します。

次に Apple ID の作成ですが、今回は iTunes で決済不要のアカウントを作ります。
iTunes を起動して、App Store にアクセスし、適当にアプリを選択し、アプリケーションのアイコンの下にある「無料 App」をクリックします。「Apple ID を作成」のポップアップウィンドウが表示されるので、ウィザードにしたがって作成します。
この手順で進めていくと、お支払い方法の選択で、「なし」が表示されます。
詳しくは、Appleの公式サポートページに載っていますので参照してみてください。

iTunes を使って Podcast を購読

iTunes を起動する メニュー「ファイル」-「Podcast を購読」使って設定したフィードを取得します。
itunes1
itunes2

新しいメディアファイルをアップロードして、フィードが更新されていることを確認

先ほど作成した audio フォルダにファイルをアップロードします。以下は FileZilla を使ってのアップロードの例です。
filezilla5
iTunes に戻り、登録したRSSフィードを更新するとアップロードしたファイルが最新のフィードに反映されます。
itunes3

このように、DirCaster を使えば、お手軽に Podcast サーバを構築することができます。
昨今は、スマートフォンで簡単に録音・編集もできるので自分の放送局を作り楽しんでみてはいかがでしょうか?

参考サイト

Tags: , , , ,

ookoshi

Seleniumでの自動テストの運用方法

Posted Date: 2014-06-12 14:00
Author: / No Comments

こんにちは。品質管理グループのおおこしです。

今回は品質管理グループで行っている Selenium での自動テストの運用方法について書きます。
Selenium は Web アプリケーションのテストツールです。
セミナーでは導入についてよく聞きますが、現場でどのように運用しているかはあまり例を聞かないため、トライコーンの場合をまとめてみます。

日常的に行うこととして、最新状態の開発環境に対してスモークテストとして Selenium を毎朝実行しています。
毎日実行するのにはすぐに差異を見つけることと、テストデータを積み重ねる目的があります。
レポートを品質管理グループのメーリングリストに届くようにして出勤後に結果を確認しています。

失敗していたテストスイートについては内容を参照し、どのテストケースが失敗したかを確認した上で、再実行して再現確認と再現条件の特定をします。
バグであれば修正依頼をし、開発による変更ならテストケースの修正をします。

失敗するテストケースを放置すると修正が追うのが難しくなり、使われないテストケースになってしまうので早めの対応が大事です。
一般に指標として聞かれるのはケース数であったりレポートにカウントされる判定部分になりますが、トライコーンの開発で重視しているのは画面数です。テストのパターン数を増やすのも大事ですが、現在品質管理グループでは網羅性の向上を優先しています。
また、テストスイートはプロダクトの機能ごとにまとめるようにしています。これによりプロダクト側の機能の追加や修正に際し、機能ごとにテストスイートを作成したり修正していけば良いため、テストケース管理の見通しが良くなっています。

テストスイートおよびテスト対象サーバは setting.properties により切り替えを行えるようにしており、(1) 開発サーバに対しての最新のコードのテスト、(2) 本番サーバに対してのリリース版のコードのテスト、(3) テスト用に外部に提供しているサーバに対してのリリース版のコードのテスト、なども簡単に切り替え可能です。

lrwxrwxrwx 1 kreisel kreisel setting.properties -> setting.properties_lion_Kreisel
-rwxr-xr-x 1 kreisel kreisel setting.properties_apibeta_Kreisel
-rwxr-xr-x 1 kreisel kreisel setting.properties_apitest_Kreisel
-rwxr-xr-x 1 kreisel kreisel setting.properties_dev_Kreisel
-rwxr-xr-x 1 kreisel kreisel setting.properties_honban_Kreisel
-rwxr-xr-x 1 kreisel kreisel setting.properties_lion_Kreisel

このように切り替えが容易なため、日次の実行以外に、メンテナンスを行う際のリグレッションテストにも利用しています。リグレッションテスト時はステージング環境で、リリースするコードに対してテストを実施します。

なお、テストスイート及びそこに含まれるテストケースは Subversion で管理しています。

トライコーンのクライゼルやコトシロなどのプロダクトはバージョンアップを継続的に行う派生開発を行っているため、同じテストを何回も繰り返します。自動テストは作成やメンテナンスの工数はかかりますが、日々実行していることと、確実な結果が出せることで元がとれていると思います。

過去の資産が多いことから現在は Selenium 1 を継続利用していますが、Selenium 2 への移行も進めています。これからもプロダクトの開発に合わせた運用方法を探しながら進めていこうと思います。

Tags: , , ,

muu

アウトラインプロセッサでメモをとる

Posted Date: 2014-04-03 13:00
Author: / No Comments

初めまして、初投稿になります入社3年目のmuuと申します。

今回技術的な話ではないですが、私が普段使っている、メモを取ることができるツールでアウトラインプロセッサというものを紹介します。

私は開発の過程で見たり聞いたりして知ったことがあれば後で忘れても大丈夫なようにメモをとります。会議などPCから離れる場合はノートにメモをしますが、操作中にパッと見れるようにパソコン上にメモの内容を写します。最初はメモ帳を使っていましたが不便さを感じ、色々探しました。

最初は付箋のソフトを使いましたが、長く保存しておきたいということと量が増えるとデスクトップが散らかるという理由で少ししか使いませんでした。
とはいえ、現状メモ帳に節ごとにファイルを作っていくとファイル数が増えるだけでなく、見たいメモを見るときもいちいちファイルを開いたりと大変でした。

更に文字しか表現できないため、疑似的に表を作ることは可能ですが、図や画像を入れることができないので後で見返すと自分でメモしておきながら何(どこのこと)を言っているのかわからなくなることも多々ありました。重要な語句もメモの中から探し出すのも大変でした。(@や#で目立たすことはできますが、場所をとりますし同一色なので見づらくなります)

そこで現在の不満をニーズとして、以下の条件を満たしてくれそうなソフトを探しました。

  • 節ごとに区切ることができる
  • 見たい節をすぐに表示できる
  • 図や画像を挿入できる
  • 重要な部分をわかりやすくしたいので文字装飾ができる

色々探してみたところ、アウトラインプロセッサというものに行き当たりました。
アウトラインプロセッサはツリー型で個々のノードを階層管理できる文書作成ソフトウェアです。

元々アウトラインプロセッサは長文を書く時などに使うソフトで、思いついたことをメモして最後にまとめるといった使い方をされていてアイディアプロセッサとも呼ぶそうです。以下はアウトラインプロセッサの一つ NanaTerry のスクリーンショットになります。

NanaTerry Overview

図1 NanaTerry画面

特徴

一括管理

複数のノード(節)を階層(ツリー)管理することができ、親子関係を付けることでジャンルごとに分けることができます。そのため、目的のメモに素早くたどり着くことができます。

画像挿入や文字装飾

メモ帳と比べたときの一番の利点だと思います。文字だけではわかりにくくても図や画像をくっつけることで見直したときに思い出しやすく、他の人に見せても説明しやすいです。
メモの中でも重要な語句には色を付けたり文字のサイズを変えるなどしてパッと目につきやすくできます。

文書全体からの検索(置換)機能

文書全体に対して検索や置換をすることができるので、階層が複雑で節が多くなっても探し出すことができます。(ソフトによってはできないかもしれません)

利用時の注意:バックアップ重要

これは特徴というよりはアウトラインプロセッサ利用時の注意になります。

一括管理ができる素晴らしいソフトですがそれ故に欠点もあり、1ファイルなのでそれが壊れてしまえばファイル内の全てのメモが読めなくなってしまいます。
メモ量もそうですが、画像を沢山入れてしまうとその分ファイルサイズが大きくなり、動作も重くなりがちなので処理中に色々動かしてしまうとフリーズすることもあり、それが原因でファイルが壊れてしまう事もあり得ます。

せっかく書き溜めてきたメモが全て無くなってしまうのは悲しいですので、アウトラインプロセッサに限った話ではありませんが、定期的なバックアップはとっておいた方がいいです。(ソフトによっては自動でとってくれるものもあるようです)

変遷

アウトラインプロセッサはフリーソフトでも多くの種類があり、最初に「Marinotes」を使いました。
上記の要望を満たすことはもちろん、他にも

  • あるノードを編集中に別のノードを見ることができる
  • パスワード設定ができる
  • テキストファイルに変換出力

など多彩な機能がありしばらく使っていました。

その後、他にもソフトを色々見ていまして次に発見したのが「NanaTree」というソフトでした。
NanaTreeも要望を満たしてくれるほかに、

  • URLを記述するときにリンクを張ることができる
  • オブジェクトの挿入ができる

といった機能があります。オブジェクトの挿入とは、外部ソフト(表計算ソフトやプレゼン資料作成ソフトなど)の内容を貼り付けることができる機能です。

表計算ソフトで作ったグラフであれば画像として張り付けられますが、この機能でセルも埋め込むことができ、アウトラインプロセッサ上から編集も可能なので編集がとても楽です。なので、データのセルから作ったグラフをまるまる入れ込むこともでき、メモの幅が広がりました。(別途ソフトがインストールされている必要がありますが)

※図2はNanaTerryですが、NanaTreeでもできます

NanaTerry Object Function

図2 NanaTerry オブジェクト機能 (グラフ内のデータはただのダミーです)

残念ながら、このソフトは現在は開発終了しているため、これ以上のバージョンアップが期待できませんでした。が、開発を引き継がれる方々が「NanaTerry」を開発していて、現在も積極的に開発が行われています。

私は今のところ、この NanaTerry を利用しています。アウトラインプロセッサは長文を書くというの本来の使い方以外にも、技術者にとって技術メモや備忘録をとるのにも便利なツールではないかと思います。

Tags: , , ,

y.kimura

こんにちは、R&D グループの y.kimura です。

今回は Amazon Web Service の EC2 に Route53 を利用して、
お好きなホスト名でアクセスできるようにしようというお話です。

AWS に固定ホスト名でアクセスするには、以下の方法が有名です。

  1. アクセスする端末の hosts ファイルを書き換える
    メリット: 余計な費用が掛からない
    デメリット: EC2 側の変更に追従するのが面倒
     
  2. EC2 の Elastic IPを割り当てる
    メリット: 5 つまでなら無料で割り当てられる
    デメリット: 5 つ以上 EC2 インスタンスに割り当てると、申請+追加料金が必要
     
  3. Route53 に登録する
    メリット: EC2 インスタンス数の制限が無いに等しい
    デメリット: 元々使用していれば問題ないですが、そうでない場合はRoute53 をこれだけのために利用する手間と費用がかかる
     

それぞれ一長一短ありますが、
私の関わっているプロジェクトで実際に採用している 3 番目の方法をご紹介します。

やることはいたって単純で、次の通りになります。

EC2インスタンスの起動/再起動時に次の処理を実行するスクリプトを走らせる。

  1. EC2インスタンス自身のメタデータを取得する
  2. メタデータ(もしくはタグ情報)を元にホスト名を決定する
  3. ホスト名を Route53 から削除する(古いホスト名)
  4. ホスト名を Route53 に登録する(レコード名=ホスト名 の CNAME に PublicDNS を登録)

 

EC2 の API は、様々な言語の SDK が用意されていますが、今回は Ruby の SDK を使用します。
Ruby 本体のバージョンは 2.1.1、AWS-SDK のバージョンは 2014 年 2 月時点での最新 (1.34.1) です。

Route53 の設定、EC2 インスタンスの作成は実行済み、
EC2 インスタンスの OS は CentOS6.4 64bit とします。

1. Ruby のインストール

rvm を使ってインストールするのが簡単ですね。せっかくなので現在の最新版の 2.1.1 をインストールします。

$ curl -L https://get.rvm.io | bash -s stable

一旦ログアウトして再度ログイン。rvm コマンドが使用できるようになります。

rvm で ruby 本体をインストールする前に、epel のリポジトリをインストール

# wget http://ftp-srv2.kddilabs.jp/Linux/distributions/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
# rpm -Uvh epel-release-6-8.noarch.rpm

必要なパッケージをインストール

# yum install -y patch libyaml-devel libffi-devel glibc-headers gcc-c++ glibc-devel readline-devel zlib-devel openssl-devel autoconf automake libtool bison

ruby 本体のインストール

$ rvm install 2.1.1

続いて AWS-SDK のインストール

$ gem install --no-ri --no-doc aws-sdk

これで準備完了です。

2. スクリプトを設置

次の ruby スクリプトを ruby をインストールしたユーザの home に設置します。

hostname_to_route53.rb

#!/usr/bin/env ruby
require 'rubygems'
require 'aws-sdk'

## 設定項目

# AWS のアクセスキーIDとシークレットアクセスキー
ACCESS_KEY_ID     = "アクセスキーID"
SECRET_ACCESS_KEY = "シークレットアクセスキー"

# EC2のリージョン=Asia Pacific/Tokyo
EC2_ENDPOINT = "ec2.ap-northeast-1.amazonaws.com"

# Route53のHosted-zone id
HOSTED_ZONE_ID = "Hosted Zone ID"

# ホスト名の元になるドメイン名
# 最後にピリオドを必ず付けること!
DOMAIN_NAME = "ec2.tricorn-labs.jp."

# TTL
TTL = 300

## 実行コード

SELF_META_URL  = "http://169.254.169.254/latest/meta-data"

# EC2インスタンスのメタデータ
MY_PUBLIC_DNS  = `curl #{SELF_META_URL}/public-hostname 2>/dev/null`
MY_PUBLIC_IP   = `curl #{SELF_META_URL}/public-ipv4     2>/dev/null`
MY_INSTANCE_ID = `curl #{SELF_META_URL}/instance-id     2>/dev/null`

# APIキー、秘密鍵の設定
AWS.config({:access_key_id => ACCESS_KEY_ID, :secret_access_key => SECRET_ACCESS_KEY})

# APIキーに対応したアカウントのEC2インスタンスコレクション
ec2 = AWS::EC2.new(:ec2_endpoint => EC2_ENDPOINT)

# このEC2インスタンス情報の取得
my_ins = ec2.instances[MY_INSTANCE_ID]

# たまにメタデータの取得に失敗する 念のためチェック
raise "#{MY_INSTANCE_ID.to_s} was not found on your EC2" unless my_ins.exists?

sub_domain = my_ins.tags['sub-domain'] ? my_ins.tags['sub-domain'] : my_ins.instance_id
hostname = "#{sub_domain}.#{DOMAIN_NAME}"
r53 = AWS::Route53::Client.new()

rrsets = AWS::Route53::HostedZone.new(HOSTED_ZONE_ID).rrsets
rrset = rrsets[hostname, 'CNAME']

# レコードセットの削除
begin
  cur_rec = rrset.resource_records[0][:value]
rescue => error
  cur_rec = ''
end

if !cur_rec.empty? then
  batch = AWS::Route53::ChangeBatch.new(HOSTED_ZONE_ID)
  batch << AWS::Route53::DeleteRequest.new(hostname, 'CNAME', :ttl => TTL, :resource_records =>[{:value => cur_rec}])
  batch.call
end

# レコードセットの登録
batch = AWS::Route53::ChangeBatch.new(HOSTED_ZONE_ID)
batch << AWS::Route53::CreateRequest.new(hostname, 'CNAME', :ttl => TTL, :resource_records => [{:value => MY_PUBLIC_DNS }])
batch.call

なお、設定するアクセスキーID・シークレットアクセスキーに対応する IAM ユーザの権限としては以下が割り当てられていれば動作します (不要なものもあるかもしれませんが)。

  • ec2
    • DescribeInstanceAttribute
    • DescribeInstanceStatus
    • DescribeInstances
    • DescribeTags
  • route53
    • * (全ての権限)

では、スクリプトに実行権限を付けましょう

$ chmod 700 hostname_to_route53.rb

EC2インスタンスに、「sub-domain」というタグが設定されていると、
そちらをサブドメインとして使用します。
タグがない場合は、インスタンスID (i-XXXXXXXX) をサブドメインとして使用します。

スクリプト内の DOMAIN_NAME が 「ec2.tricorn-labs.jp」となっており、「sub-domain」タグに「web01」が設定されている EC2 インスタンスで実行した場合、Route53 に登録されるホスト名は、「web01.ec2.tricorn-labs.jp」となります。
※「tricorn-labs.jp」というドメインは実際には存在しません。

複数の EC2 インスタンスで使用する場合は、
「sub-domain」タグの値が重複しないよう注意してください。

さて試しに実行してみましょう。

$ ./hostname_to_route53.rb

マネジメントコンソールの Route53 で確認すると、新たにレコードが登録されていることがわかります。

CNAME に PublicDNS を登録するため、
同一リージョンの EC2 同士でホスト名を使用して通信した場合、
PrivateIP に変換されるため外部ネットワークを経由せずに通信することができます。

3. サーバ起動時に実行する

最後に、EC2 の PublicIP,PublicDNS が変更される
「Stop」⇒「Start」時に自動的にスクリプトを実行するようにします。

以下の様な init スクリプトを用意します。
ruby をインストールしたユーザを仮に ec2-user、スクリプトのパスを /home/${USER}/aws-tools/hostname_to_route53.rb としていますが、適宜変更して下さい。

r53

#!/bin/sh
#
#    /etc/rc.d/init.d/r53
#
# Register hostname to Route53
#
# chkconfig: 345 44 56
#
USER=ec2-user
SCRIPT=/home/${USER}/aws-tools/hostname_to_route53.rb

start() {
    su -l $USER -c "$SCRIPT"
    return $?
}

stop()
{
    return 0
}

restart() {
    stop
    start
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        restart
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        RETVAL=2
esac
exit $RETVAL

/etc/init.d/ 以下に設置し、実行権限を付与した上で、chkconfig で起動時に実行されるよう登録します。

# mv r53 /etc/init.d/
# chown root:root /etc/init.d/r53
# chmod 700 /etc/init.d/r53
# chkconfig r53 on

さて、準備が出来ましたので試しに EC2 インスタンスを「Stop」アンド「Start」してみます。
しばらく待つと、Route53 にホスト名が登録されます!

このように、EC2 インスタンスの boot 時自動的に
Route53のレコードを更新するような仕組みを入れた AMI を作成しておけば、
PublicDNSは長すぎる!
IPだと覚えにくい!
うかつにインスタンスを停止できない!(するな)
などの悩みから解消されます。

Tags: , ,

uzuki

初めまして。セールスフォースグループの uzuki です。

他のメンバーは Linux や PHP のことについてよく書いていますので、趣向を変えて Windows のことについて書きたいと思います。

Windows には Linux と同じく CUI で操作を行うことが可能な「コマンドプロンプト」という機能があります。MS-DOS 世代の方ならご存じかと思いますが、要は Linux のシェルとほぼ同じような機能です。詳しくは コマンド プロンプト: よく寄せられる質問 に記載がありますので、ご一読頂ければと思います。

また、Linux のシェルスクリプトと同じく、あらかじめファイルに書いておいた命令を行わせることが可能です。このファイルのことを「バッチファイル」と呼びますが、今回はバッチファイルを作成して、任意のアプリケーションを再起動させる方法について書きたいと思います。

バッチファイル とは

先程記載した通り、バッチファイルとは言わば Windows 版のシェルスクリプトのようなものです。作成方法は単純で、拡張子を「.bat」にするだけでバッチファイルとなります。
Cドライブ配下に「test」フォルダを作成し、その中に「hello.bat」というファイルを作成します。hello.bat の上で右クリック⇒編集をクリックし、以下の内容を記載します。

echo hello!
pause

echo はその名の通り、引数を出力するコマンドです。pause を末尾に入れておかないと、コマンドプロンプトのウィンドウがすぐに閉じてしまうため入れています。

このファイルを実行すると、以下の内容で出力されるかと思います。

C:\test>echo hello!
hello!

C:\test>pause
続行するには何かキーを押してください . . .

実行したコマンドが表示されており、なんだか不格好なので、実行したコマンドを非表示にしたいと思います。先程の hello.bat に 1 行加えるだけで実行したコマンドは非表示になります。

@echo off
echo hello!
pause

実行結果は以下の通りとなります。

hello!
続行するには何かキーを押してください . . .

以上がおおまかなバッチファイルの作成方法となります。

任意のアプリケーションを再起動させる手段(コマンドプロンプト編)

バッチファイルの応用例として、任意のアプリケーションをワンタッチで (強制的に) 再起動する方法を紹介しようと思いますが、まずその前段として、コマンドプロンプトで任意のアプリケーションを再起動する方法からご説明します。

例として iexplore.exe (InternetExplorer) を再起動させてみます。

  1. 実行中の iexplore.exe のプロセスIDを検索・表示する
  2. 実行中のプロセスID (PID) を表示させる tasklist コマンドを利用します。
    tasklist は、オプション指定で任意のアプリケーション名 (tasklist コマンドにおける「イメージ名」) に対応するプロセスのみを表示させることが可能です。

    c:\test>tasklist /fi "IMAGENAME eq iexplore.exe"
    
    イメージ名                     PID セッション名     セッション# メモリ使用量
    ========================= ======== ================ =========== ============
    iexplore.exe                  5688 Console                    1     26,584 K
    iexplore.exe                  7316 Console                    1     55,076 K
    iexplore.exe                  4812 Console                    1     44,200 K
  3. プロセスを終了させる
  4. とある名前のアプリケーションに対応するプロセスを終了させるには taskkill コマンドを利用します。taskkill は、オプション指定を行うことにより、アプリケーション名でプロセスを終了させることができます。

    c:\test>taskkill /im iexplore.exe /f
    成功: プロセス "iexplore.exe" (PID 5688) は強制終了されました。
    成功: プロセス "iexplore.exe" (PID 7316) は強制終了されました。
    成功: プロセス "iexplore.exe" (PID 4812) は強制終了されました。

    /fは強制終了オプションです。

    InternetExplorer は複数タブを開いている状態で終了しようとすると確認ダイアログが出てきますので、強制終了オプションを指定しない場合は OK ボタンを押さない限り終了されません。
    そういった事情のあるアプリケーションの場合、強制終了オプションを指定することにより上記のように確認ダイアログによる問い合わせなしに停止可能です (ただし、強制終了で何かしら実害が生じるようであればこの強制終了オプションは利用しないで下さい)。

  5. アプリケーションを起動する
  6. アプリケーションを起動するには start コマンドを利用します。start コマンドはアプリケーションを起動するコマンドになります。第一引数は実行ファイル名ではなくウィンドウのタイトルになるため、例えば下記のように空白文字列を与えます。

    c:\test>start " " "C:\Program Files\Internet Explorer\iexplore.exe"

ではバッチファイルで実行させる方法を紹介します。

任意のアプリケーションを再起動させる手段(バッチファイル編)

では、上記でコマンドプロンプトで実施した内容をバッチファイルにしてみます。

今回やることはそう難しくないので簡単そうですが、バッチファイル(というかコマンドプロンプト)のクセを知らないとドツボにハマります。私はハマりました。

さて、アプリケーションの再起動の順序としては、

  1. 再起動させたいアプリケーションの名前と場所を変数に代入
  2. 実行中のプロセスから再起動させたいアプリケーション名のリストを取得
  3. リストがあればアプリケーションを終了させる
  4. 再起動させたいアプリケーションを起動する

早速ソースを見てみましょう。

@echo off
rem 再起動させたいアプリケーションの実行ファイル名とそのパスを変数に代入
set APP_NAME=iexplore.exe
set APP_FOLDER=C:\Program Files\Internet Explorer\

rem 対象のアプリケーションが起動しているかどうかを、tasklist コマンドと 
rem for 文の組み合わせで判定。tasklist コマンドの結果のうち、
rem /f オプションにより先頭の文字列 (空白またはタブ等の区切り文字まで) 
rem のみを取得し、%%i に代入
for /f %%i in ('tasklist /fi "IMAGENAME eq %APP_NAME%" 2^>^&1') do (
    rem 先頭の文字列がアプリケーション名と等しければ一つ以上のプロセスが
    rem 実行中であり、アプリケーションが起動していると見なせるため、
    rem taskkill で停止処理を実施
    rem (一致がなければアプリケーションは停止していると見なし何もしない)。
    if /i %%i==%APP_NAME% (
        rem taskkill でアプリケーション名を対象にして複数のプロセスを一度に停止
        taskkill /im %APP_NAME% /f
        rem アプリケーション停止後、breakなんて気の利いた機能はないので 
        rem goto でループから抜ける。
        goto ENDLOOP;
    )
)
:ENDLOOP

rem 再起動させたいアプリケーションを起動
start " " "%APP_FOLDER%%APP_NAME%"

わざわざ tasklist でリストを取得・for で処理する必要があるのか? taskill 後に start すればいいじゃないか! とのご指摘もあるかと思います。こんな周りくどいことをしている理由は、アプリケーションが起動している時のみ taskkill で停止する動きとしたかったからです。

例えば、存在しないアプリケーションを taskkill しようとすると以下のような「エラー(Error)」が生じます。

c:\test>taskkill /im iexplore.ex /f
エラー: プロセス "iexplore.ex" が見つかりませんでした。

一方で tasklist でアプリケーション名を指定し、検索を行った場合、そのアプリケーションが動作していない状況では「情報(Information)」として結果が返ってきます。

c:\test>tasklist /fi "IMAGENAME eq iexplore.ex"
情報: 指定された条件に一致するタスクは実行されていません。

分かっていてエラーを生ずるようなコマンドを実行するよりは、必要な時だけ taskkill を実施するような動きとなった方が筋が良いかなと思い、tasklist でアプリケーションのプロセスの有無を確認した上で taskkill を実施しています。その方法としてあまり for 文は他の言語では利用しないかもしれませんが、コマンドプロンプトで利用できるコマンドと制御構造においては比較的妥当な方法かなと思っています (もちろん他にもっとスマートな方法もあると思いますが)。

なお、バッチファイルのソースを書く上で注意しなければならないのは、for文内のキャレット(^)です。% & | > < ( ) などの特殊文字はパイプや入出力として判断されますので、キャレットでエスケープしなければなりません。少し調べてみると、相当めんどくさい仕様となっているようなので、エスケープには気を付けた方がいいです。ドツボにハマります。詳しくは こちら に解析記事があります。

作成したバッチファイルをデスクトップやタスクバーに置いたり、ランチャーやマウスジェスチャーに登録すると、ダブルクリックでサクッとアプリケーションが再起動できて幸せになれます。

Tags: , ,

1 / 1712345...10...最後 »