TimeTreeAPIを使って共有カレンダーをGoogleカレンダーへ同期する

夫婦のスケジュール共有に2年ほどTimeTreeというアプリを使っています

操作性が良くて気に入っているのですが、TimeTreeの共有カレンダーを外部サービス(例えばGoogleカレンダー)とシンプルには同期できないという不満がありました。

Googleカレンダーと同期することが出来たらアレクサに「今日の予定は?」と聞くだけで予定の確認ができるので便利になるのにと思っていて早2年。運営元に「有料アカウント化してもいいからAPI作って」と問い合わせまで送っていました。

想いが通じて(?)先日、予定取得APIが公開されたので、早速TimeTree共有カレンダー→Googleカレンダー→アレクサで予定確認を作ってみました。

Googleカレンダーへの同期ということで、API取得はGoogleAppsScript(GAS)で作成。TimeTree側に予定が追加されたかを定期的に取得するためにGASにトリガーで定期実行を仕込んでいます。

コードは最後に載せますが、作成にあたっていくつかポイントが

TimeTreeの予定は今日~最大7日分しか取得できない

この制限自体はAPIドキュメントに書いてあるので不便ですが問題ではないでしょう

TimeTreeAPIのタイムゾーンはUTC固定

タイムゾーンを変更できません。
このため日本時間AM9時が日付の切り替わり基準になります。なんとAM9時を過ぎると0時~8時59分までの予定が取得できなくなります。APIドキュメントにUTCと書いてあって嫌な予感したので境界値テストしたら案の定でした。
過去の予定は同期済みであることが期待できるので実用上は大丈夫かと思われます。

GoogleカレンダーAPIの終日予定 EndTimeが1日ズレる

こちらはGoogleカレンダーAPIの仕様のようです。
1月10日~1月13日までという予定をAPIで取得すると、API上は終了日がなぜか1月14日を返してきます。
逆に登録時に 1月10日~1月13日というデータを素直に登録しようとすると、カレンダー上は 1月10日~1月12日の予定として作成されます。
フォーラムでも不可解な動作として疑問を抱いている人が多かったですが、「終日予定の終了時間は翌日の0時0分」というのがGoogleさんの判断のようです。


TimeTree→Googleカレンダーへ同期するGoogleAppsScript

getCalenderId() で同期したい共有カレンダーのIDを調べて、getUpcomingEvents()をトリガーで定期実行する感じです。

Timetreeのパーソナルアクセストークンの取得とかは適当にググりましょう。

//アクセスできるカレンダー一覧の取得
function getCalenderId() {
  //ファイル→プロジェクトのプロパティ→スクリプトのプロパティにて下記プロパティを設定すること
  //プロパティ名:TIMETREE_ACCESS_TOKEN
  //値:Timetreeで発行したパーソナルアクセストークン
  var timetreeAccessToken = PropertiesService.getScriptProperties().getProperty('TIMETREE_ACCESS_TOKEN');
  var options = {
    'method': 'get',
    'contentType': 'application/json',
    'headers': {
      'Accept': 'application/vnd.timetree.v1+json',
      'Authorization': 'Bearer ' + timetreeAccessToken
    }
  };
  
  var timetreeResponse = UrlFetchApp.fetch('https://timetreeapis.com/calendars', options);
  var timetreeJsonData = JSON.parse(timetreeResponse.getContentText());
  for (var i= 0 ;i < timetreeJsonData.data.length ; i++){
    //カレンダー名:カレンダーID をログに表示(表示→ログから確認)
    Logger.log(timetreeJsonData.data[i].attributes.name + ' : ' + timetreeJsonData.data[i].id);
  }
}

function getUpcomingEvents() {
  //ファイル→プロジェクトのプロパティ→スクリプトのプロパティにて下記プロパティを設定すること
  //プロパティ名:TIMETREE_ACCESS_TOKEN
  //値:Timetreeで発行したパーソナルアクセストークン
  var timetreeAccessToken = PropertiesService.getScriptProperties().getProperty('TIMETREE_ACCESS_TOKEN');

  //ファイル→プロジェクトのプロパティ→スクリプトのプロパティにて下記プロパティを設定すること
  //プロパティ名:TIMETREE_CALENDER_ID
  //値:getCalenderId()で取得したGoogleカレンダーと同期したいカレンダーID
  var timetreeCalenderId = PropertiesService.getScriptProperties().getProperty('TIMETREE_CALENDER_ID');
  var options = {
    'method': 'get',
    'contentType': 'application/json',
    'headers': {
      'Accept': 'application/vnd.timetree.v1+json',
      'Authorization': 'Bearer ' + timetreeAccessToken
    }
  };

  //upcoming_events APIでは当日から7日間分のみ取得可能
  //過去および8日以降の予定は取得できない
  //日付の判定はUTCのためAM9時を過ぎると8:59以前の予定は取得できなくなる
  var getDays = 7;//1-7で指定
  
  //TimeTreeの予定を取得
  var timetreeResponse = UrlFetchApp.fetch('https://timetreeapis.com/calendars/' + timetreeCalenderId + '/upcoming_events?days=' + getDays, options);
  var timetreeJsonData = JSON.parse(timetreeResponse.getContentText());

  //ファイル→プロジェクトのプロパティ→スクリプトのプロパティにて下記プロパティを設定すること
  //プロパティ名:GOOGLE_CALENDER_ID
  //値:同期先のGoogleカレンダーID
  var googleCalendarId = PropertiesService.getScriptProperties().getProperty('GOOGLE_CALENDER_ID');
  
  //Googleカレンダーの予定を取得
  var googleCalendar = CalendarApp.getCalendarById(googleCalendarId);

  var startTime = new Date(Utilities.formatDate(new Date,"JST", "yyyy/MM/dd"));
  var endTime = new Date(Date.parse(startTime) + (getDays * 60 * 60 * 24 * 1000));
  var googleCalendarEvents = googleCalendar.getEvents(startTime, endTime);
  var googleCalendarEventsObjects = {};
  //googleカレンダーの予定を確認
  for (var i in googleCalendarEvents) {
    //予定の説明にtimetreeのeventIdを入れているので、eventIdをkeyにした配列を作る
    googleCalendarEventsObjects[googleCalendarEvents[i].getDescription()] = googleCalendarEvents[i];
  }

  //Timetreeの予定を確認
  for (var i= 0 ;i < timetreeJsonData.data.length ; i++){
    var timetreeEvent = timetreeJsonData.data[i];

    if(googleCalendarEventsObjects[timetreeEvent.id]){
      //googleカレンダーに予定が存在する
      var googleCalendarEvent = googleCalendarEventsObjects[timetreeEvent.id];

      //終日予定かチェック
      if(googleCalendarEvent.isAllDayEvent() == true && timetreeEvent.attributes.all_day == true){
        //どちらも終日予定
        var googleCalendarStartDate = Utilities.formatDate(googleCalendarEvent.getStartTime(),"JST", "yyyy/MM/dd");
        var googleCalendarEndTime = new Date(Utilities.formatDate(googleCalendarEvent.getEndTime(),"JST", "yyyy/MM/dd"));
        var googleCalendarEndDate = Utilities.formatDate(new Date(googleCalendarEndTime.getYear(),googleCalendarEndTime.getMonth(),googleCalendarEndTime.getDate()-1),"JST", "yyyy/MM/dd");//googleカレンダーの終日予定の最終日は1日多く返ってくるので減算
        var timetreeStartDate = Utilities.formatDate(new Date(timetreeEvent.attributes.start_at),"JST", "yyyy/MM/dd");
        var timetreeEndDate = Utilities.formatDate(new Date(timetreeEvent.attributes.end_at),"JST", "yyyy/MM/dd");
        //タイトル、開始、終了日が一致するかチェック
        if(
          googleCalendarEvent.getTitle() == timetreeEvent.attributes.title
          && googleCalendarStartDate == timetreeStartDate
          && googleCalendarEndDate == timetreeEndDate
        )
        {
          //予定変更なし。そのまま
        }else{
          //予定が変更されている。Googleカレンダー側を削除して追加しなおす
          googleCalendarEvent.deleteEvent();
          addEvents(googleCalendar,timetreeEvent);
        }
      }else if(googleCalendarEvent.isAllDayEvent() == false && timetreeEvent.attributes.all_day == false){
        //どちらも終日予定ではない
        //タイトル、開始、終了日時が一致するかチェック
        if(
          googleCalendarEvent.getTitle() == timetreeEvent.attributes.title
          && Date.parse(googleCalendarEvent.getStartTime()) ==  Date.parse(new Date(timetreeEvent.attributes.start_at))
          && Date.parse(googleCalendarEvent.getEndTime()) ==  Date.parse(new Date(timetreeEvent.attributes.end_at))
        )
        {
          //予定変更なし。そのまま
        }else{
          //予定が変更されている。Googleカレンダー側を削除して追加しなおす
          googleCalendarEvent.deleteEvent();
          addEvents(googleCalendar,timetreeEvent);
        }
      }else{
        //予定が変更されている。Googleカレンダー側を削除して追加しなおす
        googleCalendarEvent.deleteEvent();
        addEvents(googleCalendar,timetreeEvent);
      }
      //予定の確認が出来たので配列から削除
      delete googleCalendarEventsObjects[timetreeEvent.id];
    }else{
      //evet_idに一致する予定がgoogleカレンダーに存在しないので追加       
      addEvents(googleCalendar,timetreeEvent);
    }
  }
  
  //Timetree側で削除された予定をGoogleカレンダーからも削除
  for(var key in googleCalendarEventsObjects) {
    googleCalendarEventsObjects[key].deleteEvent();
  }  
}

function addEvents(googleCalendar,timetreeEvent) {
  var title = timetreeEvent.attributes.title;
  var options = {
    //予定の説明欄にtimetree側のeventIdを入れておく。修正や削除判定に利用
    description: timetreeEvent.id
  }
  
  var nowDate = new Date();
  
  if(timetreeEvent.attributes.all_day){
    //終日予定
    var startDate = new Date(timetreeEvent.attributes.start_at.slice(0,10));
    var endDate = new Date(timetreeEvent.attributes.end_at.slice(0,10));
    
    
    //終日予定が複数日にまたがるかチェック
    if(timetreeEvent.attributes.start_at == timetreeEvent.attributes.end_at){
      //1日のみの終日予定

      //終了が過去のものは追加せずにスキップ
      var tmpDate = new Date();
      tmpDate.setDate(endDate.getDate() + 1);
      if(nowDate > tmpDate){
        return;
      }
      googleCalendar.createAllDayEvent(title, startDate,options);
    }else{
      //2日以上の終日予定
      endDate.setDate(endDate.getDate() + 1);//createAllDayEventの不具合?終了日が1日少なくなるので加算
      //終了が過去のものは追加せずにスキップ
      if(nowDate > endDate){
        return;
      }
      googleCalendar.createAllDayEvent(title, startDate, endDate,options);
    }
  }else{
    //時間指定の予定
    var startTime = new Date(timetreeEvent.attributes.start_at);
    var endTime = new Date(timetreeEvent.attributes.end_at);

    //終了が過去のものは追加せずにスキップ
    if(nowDate > endTime){
      return;
    }
    
    //予定を追加
    googleCalendar.createEvent(title, startTime, endTime,options);      
  }
}

これにて「アレクサ今日の予定は?」が実現しました。めでたしめでたし。

CMC_Meetup 大阪キックオフ

ブログ書くまでがCMCって小島さんが言ってたような言ってなかったような

小島さんが綺麗になった伊丹空港に降り立ったのをFacebookで見かけて
「あー、そうかCMC meetup大阪あるのか」と気が付いて時間もあることだし顔出してきました

会場はグランフロントのXSERVERさん

オシャレなオフィス=受付にバーカウンター の法則
業務ではAWS、個人ではさくらインターネット派なのですが、何かのご縁とパンフだけは貰っておきました

ということで小島さんのターン

※コミュニティマーケティングについてはこちらもご参照ください。
「広告費ゼロで新規顧客を獲得できるコミュニティマーケティングって何ですか?」(Web担)
https://webtan.impress.co.jp/e/2018/03/07/28085

オンラインで知っていたので、オフラインでお話聞けて良かったです

その後、神戸のAlexaDaysの懇親会でお会いした大学生が登壇していたのですが、なんか知らない間に別人のようになってました


悪い大人たちのせいでコミュニティに巻き込まれた実例を目の当たりに。ほんとビックリした

最近ひさびさにこうやってコミュニティに顔出すようになって実感しているのですが、続いてるコミュニティって新しい人だらけですよね
むしろ昔の人とか全然いなかったりして、巻き込み巻き込まれ発展していってるんだなと常々思います

あと、11月のラスベガスのre:Inventに行こうと誘われたわけですが、AWSの中の人でもない人たちが純粋に誘ってるのは「これ!今のこれが、コミュニティマーケティング!」って感じでした

それなりに良いプロダクトなら絶対にファンはいると思うんですが、まぁ普通はバラバラだったりするのは問題ですよね
バラバラに存在しているファンを束ねるために、今回のようにオフラインの場が1回あるだけで動くこともありますしね

 

re:Inventは前向きに検討します。(行けたら行くわ)

LTE Cat.M1には可能性を感じる件

先週、組み込みとIoTのイベントに行ってきました。

http://www.jasa.or.jp/etwest/

元通信業界の人間としてLTE Cat.M1に期待するところがあって、業界の温度感というか、そういうの見てみたかった感じです

実際行ってみたら、驚きのワイシャツの白さ(洗濯CM並感)だったでした
いつものラフな格好とコート&シエルのリュックスタイルだったので、ブースの説明員の人が「コノ人、声かけていいのか?」と判断迷ってるのを感じた

いろんなブース回っていると、どんな立場や業種なのか聞かれるわけですよ
「エッジ(デバイス)側の人ですか?」みたいな

そんな時は「クラウドの方から来ました」で乗り切っていました。嘘は言ってない
IoTのデータはクラウドで処理というのが定番のようで「クラウドから」は全然通じてた

ブースうろうろして感じたこと

とりあえずIoTやりたいからって「画像処理でディープラーニングして工場のラインを自動化」とか溢れすぎてて、そんなみんなで同じもの作っても売れないよね

NFCとか規格マッチョすぎ、もう人類はQRコードで決済システム十分だと思い始めてないか?

Cat.M1について

そもそも LTEと言えば5Gでしょとなりがち
5Gは高速大容量で期待もされてるし、サービスインしたら魅力的なユーザー体験が訪れるんだろうなとは思う
ただ、5Gを使うほどのコンテンツって限られてしまうと思うんだよね
例えば、リアルタイムでのVRスポーツ観戦とか面白い体験なんだろうけど、そんなに毎日要らないでしょと

高速な5Gとは、いわば真逆の規格がカテゴリーM1(Cat.M1)
低速な代わりに低コスト、省電力
ドコモでは今年中にサービスインの予定

Cat.M1のような低速だけど低コストのような通信規格は結構たくさん出てる
LoRaとかSigfoxや、NB-IoT

その中でも、Cat.M1が個人的には良いなと思うポイント

・セルラー回線なので、ほぼ全国カバー
・移動してもつながる

まぁ利用用途によって選べばいいんだから、どの方式が優れてるとかない
それでも、この2点は「その辺にあるものを繋ぐ」という利用用途では強いと個人的には思っている

今までは、「繋ごう」と思うとwifiとか設定してたわけですよ
パスワード入力したり、AOSS使ってみたり

ディスプレイない機器はもっと大変
Amazon ダッシュボタンみたいに超音波使ったりとか、NetureRemoでも専用アプリから設定とかね

ハッキリ言ってめんどくさいわけですよ
それどころか、パソコン詳しくない普通の人にとって見れば絶望的に「繋がらない」世界なわけですよ

そこへ、Cat.M1のような「sim刺さっててたら通信出来ちゃいます!」
って存在がくると、キャズム超えちゃうんじゃないかと思いません?

LTE4G/3Gのモジュールは5000円とかしてたわけですよ
そんなんでは、数千円の商品に3Gモジュール載せようとか誰も考えない

それに対して、Cat.M1のモジュールって500円くらい
これなら製品に乗せるかどうかの選択肢になりえると思う

「その辺にあるものを繋ぐ」で特に可能性を感じるのは、個人的には「遊び」
その辺のおもちゃが繋がったら楽しいなと思うのは、現実と仮想のクリスマスツリーにも通じてる

通信モジュール入りのボードゲームで一緒に遠くの友達と遊べたりとか
SDカードなんか使わなくても、音声データ追加出来る妖怪ウォッチとか
そういったモノこそ普通の人に望まれてると思う

このへん書くと長くなる

simもSORACOMさんのおかげで、すごく良い市場が出来上がっている
SORACOMが無かったら日本の通信業界の未来はもっと暗かったと本気で思う

そんなこと考えていたらタイムリーなことにSORACOMからCat.M1のIoTボタンが発表された
https://soracom.jp/registration/soracom_ltem_button/

ということでSORACOM最高!という話しでした。(あれ?)

Scratchを使ってみた

プログラミング開発環境Scratchをさわってみたので、その記録

どうせなら子供と一緒にやろうかと思ったもののITリテラシーの低い我が子たちはiPhoneでマインクラフトが限界で、PCのマウス操作ができないので今回は見送り

ブラウザさえあればScratchのプログラムは作れるけど最低でもタブレットを要求されるようで、iPhoneでは動かないみたい。iPadかなにかを買ったら再挑戦しよう

Scratchさわってみた感想は、さすが「マサチューセッツ工科大学のメディアラボが作成した初心者の学習用プログラム言語」、非常に洗練されている

特にリファレンスに頼ることもなく適当に作っても動くものができた。それがコチラ

※iPhone/iPadでは動かないです。PCかFlashが動くAndroid推奨

※音を出さないと意味不明かも。ペンとかクリックしてね。

 

Scratchつかってみて感銘を受けた特徴を書き出してみる

  • 変数は宣言しただけで画面に表示される

    例えばマウスクリックで「Score」という変数を作ると、まずは画面に「Score 0」表示される。画面やコンソールに変数の内容を表示するための(初心者にとって)オマジナイのようなコードは不要。簡単に画面から非表示にすることもできるけど「変数を作ると画面に表示される」がディフォルトであるとか、初心者の気持ちわかってる感がステキ。デバッグも楽

  • オブジェクト指向という考え方を身につけられる

コードは背景画像(ステージと呼ぶ)かキャラクターなどの画像(スプライトと呼ぶ)にしか書けない。あるキャラクターを動かしたかったら、そのキャラクター画像のスプライトにコードを書かないといけない。自然とプログラムをステージやスプライトといったオブジェクトごとに書くことになる。また「何を動かすのか」が解決済みなのでコード部分で指定する必要がないのでシンプルにもなる。ちなみにScratchがオブジェクト指向言語であるかどうかの論議があるらしい。怖いね

  • メッセージ駆動を自然と覚えられる

イベントとしての「メッセージを送る」「メッセージを受ける」が、それぞれ1つのブロックで表現されるので非常に使いやすい。他のオブジェクトに影響を与えたい場合はメッセージを使うことになるのでイベントを受け取るということが自然と理解できてくる。メッセージは複数のオブジェクトで並行して受け取れるので、並列プログラムも気が付いたら組んでいたみたいなことが起きる。スゴイ

  • 「もし~なら」の条件には条件判定のブロックしか入れられない

ブロックのピースに形があってブロックの形が違う組み合わせは入れることができない。視覚的にできないことが示されるので、初心者にありがちなシンタックスエラーの洗礼を避けることができる。しあわせ

  • ブロック塊をクリックするだけで、そのブロック全体をテストできる

実行と編集の区別がないのでブロックをクリックしたら作ってる途中でも一連の処理がバンバン走る。実行されるのはブロックの塊単位なので同じスプライト内でも他のブロック群は処理されない。ユニットテストやりやすい。ステキ

  • 作品をソースコードごと世界に向けて公開できる

Scratch公式トップページで、たくさんの作品がソースコードごと公開されていてる。「中身を見る」というボタンを押すだけでソースの編集画面になり改造することができる。改造した作品はリミックスとよばれて公開される。オープンソースソフトウェアという概念を体験する分かりやすい仕組みが整っている言語がほかにあるのだろうか

 

以上

今度は書籍みながら子供とやってみようかな

LINE BotでMiitomoもどきを作ってみた

LINE Bot流行ってますね
ビジネスアカウントの機能制限版なのに、みなさん嬉しそうでなによりです

ということで流行に便乗してLINE Bot作ってみた

Miitomoもどき
line_bot

Botに話しかけた内容を保存しておいて、他人の発言内容をランダムにチラ見せ

ほぼ拾ったコードのツギハギ
発言の保存するのにDB使ったけど、SSL証明書用意する手間に比べたら楽だよね

ソース

<?php

$json_string = file_get_contents('php://input');
$jsonObj = json_decode($json_string);
$to = $jsonObj->{"result"}[0]->{"content"}->{"from"};
$text = $jsonObj->{"result"}[0]->{"content"}->{"text"};

$url = "https://trialbot-api.line.me/v1/profiles?mids={$to}";
$headers = array(
	'X-Line-ChannelID: xxxx',
    'X-Line-ChannelSecret: xxxx',
    'X-Line-Trusted-User-With-ACL: xxxx'
);

$curl = curl_init($url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$profileJson = curl_exec($curl);

//スタンプとか無視するためにtextに値があることをチェック
if($text != ""){

	$profile = json_decode($profileJson);
	$name = $profile->{"contacts"}[0]->{"displayName"};
	$mid = $profile->{"contacts"}[0]->{"mid"};


	$pdo = new PDO();// DB接続的な何か

	$sql = 'select * from log_line_bot order by id desc limit 50';
	$stmt = $pdo->prepare($sql);
	$stmt->execute();

	$arr_res = array();
	while($row = $stmt -> fetch(PDO::FETCH_ASSOC)) {
		if($row['mid'] != $mid){//自分以外 SQLで絞り込まないのはインデックス面倒だから
			$arr_res[] = $row;
		}
	}

	$sql = 'insert into log_line_bot (mid,name,msg) values (?, ?, ?)';
	$stmt = $pdo->prepare($sql);
	$stmt->execute(array($mid,$name, $text));

	if(count($arr_res) > 0){
		$res = $arr_res[array_rand($arr_res,1)];
		$res_text = "{$res['name']}がさっき「{$res['msg']}」って言ってましたよ";

		$response_format_text = array('contentType'=>1,"toType"=>1,"text"=>$res_text);
		$post_data = array("to"=>array($to),"toChannel"=>"1383378250","eventType"=>"138311608800106203","content"=>$response_format_text);

		$ch = curl_init("https://trialbot-api.line.me/v1/events");
		curl_setopt($ch, CURLOPT_POST, true);
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
		curl_setopt($ch, CURLOPT_HTTPHEADER, array(
		    'Content-Type: application/json; charser=UTF-8',
		    'X-Line-ChannelID: xxx',
		    'X-Line-ChannelSecret: xxx',
		    'X-Line-Trusted-User-With-ACL: xxx'
		    ));
		$result = curl_exec($ch);
		curl_close($ch);
	}
}

テーブル定義

create table log_line_bot(
id INT NOT NULL AUTO_INCREMENT,
mid CHAR(64) NOT NULL,
name VARCHAR(255) NOT NULL,
msg VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;

新入社員の皆さんへ

今日から社会人となった新入社員の皆さん、おめでとう

私も15年前、入社式を経験し社会の一員となりました

皆さんよりもほんの少し(15年だけ)先輩の私から、入社式での挨拶代わりに
伝えたいことをここに残します

これから皆さんは、会社の先輩や上司からいろいろ学び成長をしていくでしょうが
どうか目指す先を目の前の先輩や上司にしないでください

皆さんの目指す先は、もっとはるか遠くにあります

今はまだ自分に実力がなかったとしても、隣の先輩がいかに優秀に見えたとしても
自分自身の可能性を信じてください
先輩を目標にしてしまったら、きっとその先輩を超えることは一生できません

自分には世界を変える力があると本気で信じてください

皆さんが新入社員研修を終えて私たちのチームの一員として最初に行う仕事は
「先輩から与えられた課題」でも、「誰にでもできる仕事」でもなく、いつか
皆さん自身が世界を変えるための「最初の1歩」なのです

そして、始めのうちは先輩や上司が先導してくれるであろう歩みを、なるべく早く
自分のものとしてください

そうでなければ、皆さんに待っているのは「先輩の劣化コピー」です

先輩や上司の意見を真正面から否定し、恐れることなく自論を展開できる
ようになってください
ダイバーシティ(多様性)は歓迎すべきもので、私たちのチームは
先輩や上司の意見を否定することを好ましいことであるとの価値観を
もっています。安心してください

そしていつの日か私たちを老害と呼び、自分たちこそが世界を変えることが
できるんだということを示してください

私は皆さんなら、それができると信じています

入社おめでとう

1日の中のトラフィックの変動に合わせてEC2の起動台数を増減させる方法

JAWS-UG Advent Calendar 2013 4日目の投稿です
昨日は@j3tm0t0さんのg2インスタンスを使って宇宙へ行こう!です

11/2の第9回JAWS-UG大阪で発表したときにはManualScalingとして紹介した方法の解説

ゲームに限らず、アクセス数の多いブログやニュースサイトなんかでは、例えばお昼の12時からの1時間のほうが、その前後の1時間に比べて数倍のトラフィックがくることがよくありますよね

1日のピーク値にあわせてサーバを用意しておくというのはイマイチ「クラウド」っぽくありません
そこで、毎日のトラフィックの増減の予想から、指定時間でサーバを起動させる仕組みを作ってみました

なお、指定時刻でサーバを増減させる仕組みを、AWSのドキュメントではScheduled Scalingっていうらしいです。AutoScalingのコマンドで実現可能です。
詳しくはこちら

この公式のScheduled Scalingは、既に運用面も含めてAutoScaling構成になっている環境では簡単に使えるのですが、今回はあえてAutoScalingを使わない方法をご紹介します

今回ご紹介する方法には、コチラのようなメリットがあります

・サーバ減少時にTerminateではなくStop状態でサーバを保存しておける。ログ消えない
・サーバ増加時にはStop状態のサーバをStartさせる。インスタンスの作成時間を短縮
・最新プログラムをサーバへダウンロードするときは前回Stop時からの差分のみなのでわりと高速
・最新プログラムの反映を待ってからELBにサーバを組み込む

ということで、まずは全サーバに次のような起動スクリプトを仕掛けます

#!/bin/bash
#
# Auto associate Elastic IP , rsync and register ELB
#
# chkconfig: 345 98 6
# description: rsync and register ELB
# processname: rsync-elb
#
export JAVA_HOME=/usr/lib/jvm/jre
export EC2_HOME=/opt/aws/apitools/ec2
export AWS_ELB_HOME=/opt/aws/apitools/elb

# 初期化
RSYNC_SERVER=""
ELB_SERVER=""

# 自分のインスタンスIDを取得
INSTANCE_ID=$(/usr/bin/curl http://169.254.169.254/latest/meta-data/instance-id)

# インスタンスIDを元にtagを取得
TAGS=`/opt/aws/apitools/ec2/bin/ec2-describe-tags -O アクセスキー -W シークレットキー --filter "resource-id=${INSTANCE_ID}"  --region ap-northeast-1|/usr/bin/awk '{print $4,$5}'`

# スペース区切りでtagが返ってくるので配列へ
ARR_TAGS=(`echo $TAGS`)

# tagのキーとバリューが交互に入っているのでループ
for i in `seq 1 ${#ARR_TAGS[@]}`
do
  if [[ 'rsync' == ${ARR_TAGS[$i-1]} ]];then
    RSYNC_SERVER=${ARR_TAGS[$i]}
  fi
  if [[ 'elb' == ${ARR_TAGS[$i-1]} ]];then
    ELB_SERVER=${ARR_TAGS[$i]}
  fi
#  キーとバリューが交互なので1つ飛ばす
  i=`expr $i + 1`
done

if [[ $RSYNC_SERVER != "" && $ELB_SERVER != "" ]]; then
#   rsync実行
  /usr/bin/sudo -u ユーザー /usr/bin/rsync -av --update -e "/usr/bin/ssh -i /home/ユーザー/.ssh/秘密鍵 -o \"StrictHostKeyChecking no\"" --include="いろいろ" --exclude="*"  "${RSYNC_SERVER}:/コピー元ディレクトリ"  /コピー先ディレクトリ
  if [ $? -eq 0 ]; then
#   rsync成功 ELB登録
    /opt/aws/apitools/elb/bin/elb-register-instances-with-lb  ${ELB_SERVER} --instances ${INSTANCE_ID} -O アクセスキー -W シークレットキー  --region ap-northeast-1
    if [ $? -eq 0 ]; then
#      ELB登録成功 Apache起動
      /etc/init.d/httpd start
    fi
  else
#   失敗はシャットダウン
    /sbin/shutdown -h now
  fi
else
# 失敗はシャットダウン
  /sbin/shutdown -h now
fi

ttyがない状態でもsudoが使えるように下記の修正


# visudo
コメントアウト
#Defaults    requiretty

そして、次のようなマネージメントコンソールからタグを仕掛けます
scaling

rsync:rsync先のサーバのPublicDNS
elb:elbのLoad Balancer Name

次に指定時間に増減させたいインスタンスIDを指定した起動・停止用スクリプトを作成します

起動用スクリプト

vi /home/ec2-user/EC2-server-start.sh 
------------------------------------------------------------------------------
#!/bin/sh
/opt/aws/bin/ec2-start-instances  -O アクセスキー -W シークレットキー  --region ap-northeast-1 i-XXXXXXXX i-YYYYYYYY
------------------------------------------------------------------------------
chmod +x  EC2-server-start.sh

停止用スクリプト

vi /home/ec2-user/EC2-server-stop.sh
------------------------------------------------------------------------------
#!/bin/sh
# ELB切り離し
elb-deregister-instances-from-lb ELBの名前 --instances  i-XXXXXXXX,i-YYYYYYYY  -O アクセスキー -W シークレットキー   --region ap-northeast-1

# ELB切り離し後、30秒ほど時間を空ける
sleep 30

# インスタンス停止
/opt/aws/bin/ec2-stop-instances -O アクセスキー -W シークレットキー  --region ap-northeast-1 i-XXXXXXXX i-YYYYYYYY
------------------------------------------------------------------------------
chmod +x  EC2-server-stop.sh

最後にお好みの起動・停止時間をcronで設定して終わり

明日は@ar1さんです。

新人ソフトウェアエンジニアが知るべき13のこと

今年の新入社員に向けて、つらつらと書いてみた。

 

1.学び続ける覚悟を持とう

日々テクノロジーは進歩している。
エンジニアとして生きていくなら一生学び続ける覚悟が必要だ。

2.基礎を学ぼう

ここでいう基礎とは、TCP/IPやOSのファイルシステムなんかだ
基礎は新しいことを学ぶときに、きっと役に立つ。

3.リファレンスを読もう

googleで検索してblogに書かれたプログラムを無条件に信じてはいけない。
たまたま見たblogの内容が良くない方法(アンチパターン)かもしれない。
信じていいのは公式のリファレンスだけだ。

4.ソースも読もう

リファレンスが不十分だったり理解できないときは、躊躇せずソースを読もう。
たとえばPHPはC言語で書かれているが意外と読めるものだ。
ソースを読んで理解することは一人前になる通過儀礼みたいなものだと覚悟しよう。

5.「動けばいい」とは決して考えないようにしよう

君たちが作るプログラムは、必ず利用者がいる。
利用者に使ってもらえるものを作ろう。「動く」だけでは駄目なんだ。
誰にも観てもらえない映画を作る映画監督になりたいかい?

6.バグに遭遇したら、まずは冷静になろう

よく言われているが、「プログラムは思ったようには動かない。書いたとおりに動く。」は真実だ。
99%は目の前に原因がある。原因が目の前にないことは1%くらいだ。
99:1が決して逆ではないと肝に銘じよう。

7.思想を理解しよう

他人のソースを読むとき処理の内容を追うだけでなく「何故そのように作ったのか」を理解することは大切だ。
計算量を減らして負荷軽減を狙っている/不正な値が入らないようにしている/キャッシュ化することで高速化を狙っている等
コードから学ぶというのは思想を理解することに等しい。

8.良書を探そう

この本いいなと思ったらamazonで検索してみよう。
今は便利な世の中で「この商品を買った人は~」と良書っぽものが芋づる式に見つかる。

9.モチベーションを意識しよう

エンジニアとして学び続けるのは時として大変だ。
行動を起こそうとする源、すなわちモチベーションを意識しよう
朝、鏡を見るときに「自分はなぜエンジニアになったのか?」と問いかけるのもいい
君たちのモチベーションを手っ取り早く上げてくれるはずだ

10.勉強会やコミュニティに参加しよう

会社だけでなく外の風に触れることも時には必要だ。
新しい刺激はモチベーションの維持にもつながる。

11.アウトプットしよう

勉強会でLTをするのでもblogを書くのでもなんでもいい。
なにかをアプトプットするだけで世界は大きく変わる。

12.コミュニティ疲れ・勉強会疲れを感じだたら距離を置こう

「疲れ」の意味がわからないうちは気にしなくていい。
これがそうかな?と思ったら立ち止まろう。もう君は大丈夫だ。
また気が向いたり考え方が変わったら参加してみるといい。

13.趣味や夢中になれるものを持とう

できたら技術以外のものが望ましい。
どんなものであれ君たちの人生を豊かにしてくれる。

CentOSでTimeMachineサーバを作る

Macを何台か買ったのを機に、その辺に余っていたハード使ってTimeMachineサーバを立てたメモ

環境

CentOS6.3(Minimalでインストール)

必要なパッケージを入れる


yum -y update
yum -y install wget cracklib-devel openssl-devel quota-devel libtool automake autoconf db4-devel pam-devel tcp_wrappers-devel libgcrypt-devel avahi-dnsconfd avahi-devel libacl-devel openldap-devel make lynx

nss-mdnsのインストール

avahiで使うので導入する


cd /usr/local/src
wget http://0pointer.de/lennart/projects/nss-mdns/nss-mdns-0.10.tar.gz
tar xzvf nss-mdns-0.10.tar.gz
cd nss-mdns-0.10
./configure
make
make install

vi /etc/nsswitch.conf
hostsの行を下記の用に書き換え
hosts:      files mdns4_minimal [NOTFOUND=return] dns mdns4

netatalkのインストール


cd /usr/local/src
wget http://prdownloads.sourceforge.net/netatalk/netatalk-3.0.2.tar.gz
tar xzvf netatalk-3.0.2.tar.gz
cd netatalk-3.0.2
./configure --with-init-style=redhat-sysv
make
make install

バックアップを取りたいmacごとのアカウントの作成


useradd user1
passwd user1
su user1
mkdir ~/TM
exit

useradd user2
passwd user2
su user2
mkdir ~/TM
exit

以下、必要な台数分繰り返し

netatalkの設定


vi /usr/local/etc/afp.conf

[Global]
; Global server settings
vol preset = default_for_all_vol

[default_for_all_vol]
file perm = 0600
directory perm = 0700

[user1TimeMachine]
path = /home/user1/TM
valid users = user1
time machine = yes
vol size limit = 100000

[user2TimeMachine]
path = /home/user2/TM
valid users = user2
time machine = yes
vol size limit = 100000

以下、必要な台数分繰り返し

iptablesの設定


vi /etc/sysconfig/iptables
-A INPUT -m state --state NEW -m tcp -p tcp --dport 548 -j ACCEPT
-A INPUT -m state --state NEW -m udp -p udp --dport 548 -j ACCEPT
-A INPUT -m state --state NEW -m udp -p udp --dport 5353 -j ACCEPT
service iptables restart

netatalkの自動起動


chkconfig netatalk on
service messagebus start
service avahi-daemon start
service netatalk start

これでmac側のTimeMachineのディスク選択に表示されるはず
mac側で見えるようになるまで時間がかかることもあるかも
vol size limitはHDDの容量と相談