TECH PLAY

株式会社mediba

株式会社mediba の技術ブログ

167

こんにちは、インフラストラクチャー部の沼沢です。 今年の10月頃に、collectd の CloudWatch プラグインが出たのは記憶に新しいです。 新しい collectd の CloudWatch プラグイン | Amazon Web Services ブログ その後、数々の所謂「試してみたブログ」等もたくさん出回っていますが、あまり具体的な設定の解説をしているものは少ない印象です。 しかも、collectd には日本語のドキュメントは無く、日本語で書かれているブログ等もそう多くはありません。 そのため、深い使い方をするとなると英語のドキュメントを読み解きながら進める必要があり、ここで挫折してしまう方も多いのではないでしょうか。 そんな中、 nginx の各種情報について collectd を使って CloudWatch のカスタムメトリクスを作成する機会がありましたので、1つの具体例としてご紹介したいと思います。 前提 Amazon Linux AMI release 2016.09 collectd 5.4.1 nginx 1.11.6 本投稿の例は、以下と同等の状態ができあがっているという前提で進めます 新しい collectd の CloudWatch プラグイン | Amazon Web Services ブログ collectdのCloudWatchプラグインを試してみた | Developers.IO collectd の設定ファイルについては、デフォルトで記述されいてるコメントアウトの部分は基本的に無視して追記していくスタイルを取ります LoadPlugin だけはコメントアウト解除で対応 stub_status の値を CloudWatch に連携 stub_status とは、コネクション数等を確認する nginx のモジュールです。 詳細は Module ngx_http_stub_status_module をご確認ください。 location /nginx-status { stub_status on; } 上記が /etc/nginx.conf に設定されいてる状態でローカルからこのパスにアクセスすると、以下のような情報が取得できます。 $ curl http://localhost/nginx-status Active connections: 6 server accepts handled requests 142929 142929 100818 Reading: 0 Writing: 1 Waiting: 5 これを CloudWatch でグラフ表示していきます。 1. プラグインをインストール collectd では nginx 専用のプラグイン が用意されていますので、そちらを利用します。 $ sudo yum install collectd-nginx 2. collectd の設定ファイル (/etc/collectd.conf) に以下の設定をする #LoadPlugin nginx のコメントアウトを外す 以下を追記する <Plugin nginx> URL "http://localhost/nginx-status" </Plugin> 3. 設定後、collectd を再起動 $ sudo service collectd restart 4. blocked_metrics に以下のものが追加されていることを確認する $ cat /opt/collectd-plugins/cloudwatch/config/blocked_metrics 〜略〜 nginx--nginx_connections-active nginx--connections-accepted nginx--connections-handled nginx--nginx_requests- nginx--nginx_connections-reading nginx--nginx_connections-writing nginx--nginx_connections-waiting 〜略〜 5. CloudWatch に送る対象として、上記をホワイトリストに追記する $ sudo sh -c 'echo "nginx--.*" >> /opt/collectd-plugins/cloudwatch/config/whitelist.conf' 6. 設定後、collectd を再起動 $ sudo service collectd restart 7. blocked_metrics から追加されたものが消えているのを確認する $ cat /opt/collectd-plugins/cloudwatch/config/blocked_metrics 8. CloudWatch 上でグラフ化されていることを確認する しばらく待つと CloudWatch に以下の通り nginx のメトリクスが追加されます。 $request_time の値を CloudWatch に連携 $request_time とは、nginx がリクエストを受け取り、レスポンスを返してからアクセスログに書き込むまでの経過時間のことです。 Module ngx_http_log_module /etc/nginx.conf に以下のように設定したとします。 〜略〜 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' '"$request_time"'; 〜略〜 上記のように、ログの末尾(※)に $request_time が出力されるように設定した後、nginx を再起動すると設定が反映され、$request_time が記録されるようになります。 (下記の例だと “0.214”) xxx.xxx.xxx.xxx - - [26/Dec/2016:17:33:31 +0900] "GET / HTTP/1.1" 200 (...中略...) "xxx.xxx.xxx.xxx" "0.214" この末尾に記録された $request_time を、CloudWatch に連携してみます。 ※末尾にした理由は、正規表現でマッチさせやすくするためです。 そのため、後述の正規表現を変更すればどの位置でもマッチさせられるので、実際はどの位置でも問題ありません。 ログファイル等、追記型のファイルから情報を取得したい場合には、 tail プラグイン を利用します。 Plugin:Tail - collectd Wiki tail プラグインはデフォルトで組み込まれているため、nginx プラグインのようなインストール作業は不要です。 1. collectd の設定ファイル (/etc/collectd.conf) に以下の設定をする #LoadPlugin tail のコメントアウトを外す 以下を追記する(ログファイルの場所は適宜読み換えてください) <Plugin "tail"> <File "/var/log/nginx/access.log"> Instance "nginx" <Match> Regex "\" \"([0-9.]+)\"$" DSType "GaugeAverage" Type "response_time" Instance "AvgRespTime" </Match> <Match> Regex "\" \"([0-9.]+)\"$" DSType "GaugeMin" Type "response_time" Instance "MinRespTime" </Match> <Match> Regex "\" \"([0-9.]+)\"$" DSType "GaugeMax" Type "response_time" Instance "MaxRespTime" </Match> </File> </Plugin> 上記では、以下をそれぞれ取得するように設定しています。 1つ目の Match: 計測期間内の平均値(DSType “GaugeAverage”) 2つ目の Match: 計測期間内の最小値(DSType “GaugeMin”) 3つ目の Match: 計測期間内の最大値(DSType “GaugeMax”) 3つの Match はそれぞれ同じ値をヒットさせる正規表現を書いていますが、 DSType を指定することでそれぞれ取得する値の性質を分けています。 計測期間というのは Interval という設定で指定でき、デフォルトでは10秒間隔で計測するようになっています。 つまり、10秒のうちの平均値、最小値、最大値を取得しているということになります。 DSType については以下を参考にいろいろ試してみてください。 collectd.conf(5) – collectd – The system statistics collection daemon#plugin_tail 3. 設定後、collectd を再起動 $ sudo service collectd restart 4. blocked_metrics に以下のものが追加されていることを確認する $ cat /opt/collectd-plugins/cloudwatch/config/blocked_metrics 〜略〜 tail-nginx-response_time-AvgRespTime tail-nginx-response_time-MinRespTime tail-nginx-response_time-MaxRespTime 〜略〜 5. CloudWatch に送る対象として、上記をホワイトリストに追記する $ sudo sh -c 'echo "tail-nginx-response_time-.*" >> /opt/collectd-plugins/cloudwatch/config/whitelist.conf' 6. 設定後、collectd を再起動 $ sudo service collectd restart 7. blocked_metrics から追加されたものが消えているのを確認する $ cat /opt/collectd-plugins/cloudwatch/config/blocked_metrics 8. CloudWatch 上でグラフ化されていることを確認する しばらく待つと CloudWatch に以下の通り nginx の response_time のメトリクスが追加されます。 まとめ collectd の CloudWatch プラグインが出たことでカスタムメトリクスがより簡単に追加できるようになっています。 今回は nginx を例に使い方をご紹介させていただきましたが、他のメトリクスを追加する際にはまたブログにしてご紹介できればと思います。
アバター
はじめまして、広告システム開発部の松島です。主にネイティブアプリの開発を担当しております。 アプリの開発と言ったら、AndroidならJava、iOSならSwiftやObjective-Cで行なうことが多いと思いますが、medibaではXamarinでの開発も行っています。 さて、今回は、Xamarin.Formsで高さがバラバラの項目をグリッド表示するサンプルを作成してみましたので、その解説を行います。 サンプル こちら です。 方針 Xamarin.Formsの標準のコントロールでは、表題のようなことはできないので、カスタムレンダラーを使用して独自のコントロールを作成します。カスタムレンダラーでは、プラットフォーム毎で用意されているコントロールを使用して実装していくことになりますが、Androidでは、この独自コントロールの実現のためにRecyclerViewとStaggeredGridLayoutManagerを用いることにします。 今回、独自コントロールは2つ作成します。StaggeredGridViewとStaggeredGridCellです。StaggeredGridViewは、グリッド表示を行うコントロールで、StaggeredGridCellはそのグリッドの項目を表すセルとなります。 実装 まずは、PCLプロジェクトにStaggeredGridViewとStaggeredGridCellを作成します。 StaggeredGridView using Xamarin.Forms; namespace StaggeredGridSample { public class StaggeredGridView : ListView { public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create("RowSpacing", typeof(double), typeof(StaggeredGridView), 0.0); public double RowSpacing { get { return (double)GetValue(RowSpacingProperty); } set { SetValue(RowSpacingProperty, value); } } public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create("ColumnSpacing", typeof(double), typeof(StaggeredGridView), 0.0); public double ColumnSpacing { get { return (double)GetValue(ColumnSpacingProperty); } set { SetValue(ColumnSpacingProperty, value); } } public static readonly BindableProperty SpanCountProperty = BindableProperty.Create("SpanCount", typeof(int), typeof(StaggeredGridView), 2); public int SpanCount { get { return (int)GetValue(SpanCountProperty); } set { SetValue(SpanCountProperty, value); } } } } StaggeredGridViewは、ListViewを継承します。バインディング可能なプロパティとして、行間のスペースの設定のRowSpacing、列間のスペースの設定のColumnSpacing、列数の設定のSpanCountを用意しています。 StaggeredGridCell using Xamarin.Forms; namespace StaggeredGridSample { public class StaggeredGridCell : ViewCell { public static readonly BindableProperty RatioProperty = BindableProperty.Create("Ratio", typeof(double), typeof(StaggeredGridCell), 1.0); public double Ratio { get { return (double)GetValue(RatioProperty); } set { SetValue(RatioProperty, value); } } } } StaggeredGridCellは、ViewCellを継承します。バインディング可能なプロパティとして、横幅に対する高さの割合の設定のRatioを用意しています。 次に、AndroidのプロジェクトにStaggeredGridViewとStaggeredGridCellのカスタムレンダラーを作成していきます。最初にStaggeredGridViewのカスタムレンダラーであるStaggeredGridViewRendererからです。 StaggeredGridViewRenderer using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using Android.Support.V7.Widget; using StaggeredGridSample; using StaggeredGridSample.Droid; [assembly: ExportRenderer(typeof(StaggeredGridView), typeof(StaggeredGridViewRenderer))] namespace StaggeredGridSample.Droid { public class StaggeredGridViewRenderer : ViewRenderer<StaggeredGridView, RecyclerView> { protected override void OnElementChanged(ElementChangedEventArgs<StaggeredGridView> e) { base.OnElementChanged(e); if (Control == null) { var contex = Forms.Context; var nativeControl = new RecyclerView(contex); int padding = (int)Element.ColumnSpacing / 2; nativeControl.SetPadding(padding, 0, padding, 0); var sglm = new StaggeredGridLayoutManager(Element.SpanCount, StaggeredGridLayoutManager.Vertical); var adapter = new StaggeredGridAdapter(contex, Element); var decoration = new SpacesItemDecoration((int)Element.RowSpacing, padding); nativeControl.SetLayoutManager(sglm); nativeControl.SetAdapter(adapter); nativeControl.AddItemDecoration(decoration); SetNativeControl(nativeControl); } } } } Androidで使用するコントロールのインスタンス生成は、OnElementChangedで行います。今回は、前述の通りRecyclerViewを生成します。そして、RecyclerViewのLayoutManagerには、StaggeredGridLayoutManagerをセットしてます。これで、高さがバラバラの項目のグリッド表示ができるようになります。 また、RecyclerViewのAdapterと各項目間にスペース空けるためのItemDecorationであるSpacesItemDecorationもここでセットしています。 次に、RecyclerViewのAdapterであるStaggeredGridAdapter、ViewHolderであるStaggeredGridCellHolderを作成します。 StaggeredGridAdapter using Android.Support.V7.Widget; using Android.Content; using Android.Views; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; namespace StaggeredGridSample.Droid { public class StaggeredGridAdapter : RecyclerView.Adapter { Context _context; StaggeredGridView _gridView; ITemplatedItemsView TemplatedItemsView => _gridView; public StaggeredGridAdapter(Context context, StaggeredGridView gridView) { _context = context; _gridView = gridView; } public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) { var cell = _gridView.ItemTemplate.CreateContent() as Cell; var view = CellFactory.GetCell(cell, null, parent, _context, _gridView); return new StaggeredGridCellHolder(view); } public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (holder is StaggeredGridCellHolder) { Cell cell = ((StaggeredGridCellHolder)holder).Cell; TemplatedItemsView.TemplatedItems.UpdateContent(cell, position); } } public override int ItemCount { get { return TemplatedItemsView.TemplatedItems.Count; } } } } StaggeredGridCellHolder using Android.Support.V7.Widget; using Xamarin.Forms; using AView = Android.Views.View; namespace StaggeredGridSample.Droid { public class StaggeredGridCellHolder : RecyclerView.ViewHolder { Cell _cell; public StaggeredGridCellHolder(AView view) : base(view) { _cell = (view as INativeElementView).Element as Cell; } public Cell Cell { get { return _cell; } } } } StaggeredGridAdapterのOnCreateViewHolderで、StaggeredGridCellHolderの生成、OnBindViewHolderで、StaggeredGridCellHolderへの値の設定を行います。書き方こそはC#ですが、作りはJavaと全く同じですね。 OnCreateViewHolderでのポイントは、CellFactory.GetCellで、StaggeredGridViewで使用するAndroidでのセルを取得しているところです。こうすることで、StaggeredGridCell以外のセルも使えるようにできちゃいます。 OnBindViewHolderでのポイントは、TemplatedItemsのUpdateContentで、値の設定を行っているところです。実を言うと、このメソッドを使わないと、アプリがクラッシュしてしまうことがあります。StaggeredGridViewが、わざわざListViewを継承しているのは、このメソッドを使いたいためだったりもします。 続いて、ItemDecorationのSpacesItemDecorationです。 SpacesItemDecoration using Android.Support.V7.Widget; using Android.Graphics; using Android.Views; namespace StaggeredGridSample.Droid { public class SpacesItemDecoration : RecyclerView.ItemDecoration { int _rowSpace; int _colSpace; public SpacesItemDecoration(int rowSpace, int colSpace) { _rowSpace = rowSpace; _colSpace = colSpace; } public override void GetItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.Left = _colSpace; outRect.Right = _colSpace; outRect.Bottom = _rowSpace; } } } RecyclerViewにおける各項目間のスペースの設定は、ItemDecorationのGetItemOffsetsで行います。SpacesItemDecorationでは、コンストラクで与えた値を、引数のoutRectに設定しています。 最後に、StaggeredGridCellのカスタムレンダラーであるStaggeredGridCellContainerを作成します。 StaggeredGridCellRenderer using Android.Content; using Android.Views; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using StaggeredGridSample; using StaggeredGridSample.Droid; using AView = Android.Views.View; [assembly: ExportRenderer(typeof(StaggeredGridCell), typeof(StaggeredGridCellRenderer))] namespace StaggeredGridSample.Droid { public class StaggeredGridCellRenderer : CellRenderer { protected override AView GetCellCore(Cell item, AView convertView, ViewGroup parent, Context context) { var cell = (StaggeredGridCell)item; IVisualElementRenderer renderer = Platform.CreateRenderer(cell.View); Platform.SetRenderer(cell.View, renderer); return new StaggeredGridCellContainer(context, renderer, cell, (StaggeredGridView)ParentView); } } class StaggeredGridCellContainer : ViewGroup, INativeElementView { StaggeredGridView _parent; IVisualElementRenderer _view; StaggeredGridCell _cell; public StaggeredGridCellContainer(Context context, IVisualElementRenderer view, StaggeredGridCell cell, StaggeredGridView parent) : base(context) { _view = view; _cell = cell; _parent = parent; AddView(view.ViewGroup); } public Element Element { get { return _cell; } } protected override void OnLayout(bool changed, int l, int t, int r, int b) { double width = Context.FromPixels(r - l); double height = Context.FromPixels(b - t); Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(_view.Element, new Rectangle(0, 0, width, height)); _view.UpdateLayout(); } protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.GetSize(widthMeasureSpec); int height = (int)(width * _cell.Ratio); SetMeasuredDimension(width, height); } } } Androidのセルのインスタンス生成は、GetCellCoreで行います。StaggeredGridCellRendererでは、StaggeredGridCellContainerを生成しています。このメソッドが、前述のStaggeredGridAdapterのOnCreateViewHolderで使っていたCellFactory.GetCellで呼ばれています。 セルのサイズは、StaggeredGridCellContainerのOnMeasureで設定します。MeasureSpec.GetSizeで取得した横幅からStaggeredGridCellに設定されたRatioをかけて、高さを求めています。 まとめ 独自のコントローラを作るといったら、少し敷居が高く感じられるかもしれませんが、今回のようにAndroidでの実装方法が分かっていれば、割と簡単に作れてしまうかと思います。 次回の投稿では、iOS編をお届けします。お楽しみに!
アバター
mediba Advent Calendar 24日目です。 フロントエンジニアの苅部からはGoogle Data StudioとWebPagetestについて書こうと思います。 medibaシステム本部ではWebPagetestや Sitespeed を使って継続したパフォーマンス計測を実施しています。 具体的にはユーザー体験(体感速度)に影響を及ぼすCritical Rendering Pathに注視して、SpeedIndexとDomContentLoaded、FirstPaintの改善を進めています。 普段WebPagetestで計測をしている中で、テスト結果を時系列にグラフ化できたらいいなと思っていたのですが、ちょうど良いタイミングでData Studioが日本でもサービス開始されたので、今回はこれらのツールとGoogle Spread Sheet(以下Spread Sheet)とGoogle Apps Scriptと組み合わせる方法を考えてみました。 Google Data Studioとは Google Data Studio(以下Data Studio)は今年Googleからリリースされたダッシュボードツールです。 Google Analytics(以下Analytics)やBig Query,Cloud SQL,AdWords,DoubleClick,Spread Sheet,MySQL,Youtube Analyticsなど様々なデータソースと連携が可能で、表現豊かなビジュアライズができるようになります。 Analyticsのカスタムレポートはプロパティごとに権限の発行が必要でしたが、Data Studioはメールアドレス(ドメイン)単位で共有できるため、社内でのレポート共有などで役立ちそうです。 私自身も、これまでAnalyticsからCSVエクスポートしてExcelで整えていた業務フローをData Studioで代替できないか検討しています。 操作感はGoogle Documentのように軽快で、ドラッグ&ドロップでグラフを配置していく事が可能です。 こんな形で 好きな位置に配置できます。 Analyticsのマイレポートと比べるとレイアウトの制限が減り、より共有・報告に適したレポート作成ができると思います。 今回はWebPagetestのテスト結果をSpread Sheetに蓄積し、それをデータソースとして、Data Studioでグラフ化していきます。 WebPagetestとは WebPagetestはオープンソースで提供されているパフォーマンス向上のためのプロジェクトです。 もともとはAOLが社内向けに構築したツールですが、現在ではGoogleの支援を受けてサービスを継続しています。 過去に遡って複数日時でFilmstripで比較できたり 描画の様子を動画で比較したりすることができます。 ブラウザのAPIで取得できる数値とは違い、Filmstripでは視覚的な速さが把握できるようになります。 そのため「体感的な効果があったのか」といった疑問を検証することができます。 WebPagetestはREST APIが用意されているので、定期的にAPIコール(計測リクエスト)すればパフォーマンスの定点観測ができます。 今回は以下の値をData Studio側で指標として利用できるようにします。 VideoSpeedIndex DomInteractive TTFB(TimeToFirstByte) FirstPaint PageLoad(onLoad) WebPagetestの詳しい使い方は、Google Chrome Developersの動画が参考になると思います(日本語字幕付きです^^) Google Apps Scriptとは Google Apps Script/GAS(以下Apps Script)はGoogleのプロダクトを横断して利用できるサーバサイドプログラミング環境です。 JavaScriptを使ってDocsやSheet、Formsを操作したり、Adsense,Analytics,Calendar,Drive,Gmail,Mapsと連携させたりする事ができます。 今回はApps ScriptをSpread Sheet、WebPagetest間の連携で利用します。 APIコールとデータ連携の流れ 全体の流れとしては以下のような形になります。 1) Apps Scriptの関数から、定期的にWebPagetestのAPIをコールします。 2) WebPagetestからはレスポンスとしてtestIdが返却されます。(テストリクエストはキューイングされ、ある程度遅延した上でテストが実行されます。) 3) Apps Scriptの関数から、testIdを元に定期的にWebPagetest側のテスト結果のJSONをコールし、レスポンスの中の任意の値をSpread Sheetのセルに書き込みます。 4) Data StudioがSpead Sheetのセルを参照し、グラフを描画します。 ついでにAnalyticsで収集している速度指標をData Studioに送り、RUMのデータとして確認できるようにしています。 1. WebPagetestを利用する準備 今回はパブリックインスタンスを使うので、手順に沿ってAPI Keyを取得します。 ・WebPagetest - Get API Key APIのリクエスト制限は200Pageload/Dayとなっています。 1つのURLでfirst view/repeat viewの2つを計測した場合は、"2Pageload"としてカウントされます。 さらに、そのテストをrunsオプションで10回実行したら”20 Pageload”となります。 あくまでAPIコール数ではなく、[テストの実行回数]制限になりますのでご注意ください。 今回は以下のようなパラメータを指定して計測をしています。 変数 値 役割 url ${URL} 計測対象のURLを指定します。 k ${API_KEY} API KEYを指定します。 video 1 録画を有効にします。 f JSON レスポンスのフォーマットをJSONとします。 mobile 1 Chromeによるmobileエミュレートになります。(UA文字列と解像度とviewportをエミュレート) runs 1 テストの試行回数を指定します。複数回実行することで計測結果の数値を丸める事ができます。 fvonly 1 1と指定することで、firstViewのみのテストになります。 location ec2-ap-northeast-1.3GFast 計測地点をEC2の東京リージョン(ap-northeast-1)とし、回線速度のエミュレートを"3GFast"とします。 mobileDevice iPhone5c mobile_devices.iniの中から任意で指定できます。 (対応するUA文字列やviewportがセットされます) ・RESTful APIs - WebPagetest Documentation ・mobile_devices.ini 2. Spread Sheetの準備 必要な指標をヘッダー行に入れ、計測対象ごとにシートを分ける形にしました。 標準偏差はSTDEV関数、平均値はAVERAGE関数、中央値はMEDIAN関数、相関係数(の二乗)はRSQ関数で算出する事ができます。 またTTEST関数を使うことでT検定でp値を算出することができますので、2組の集団(月次データなど)の平均の有意差を判断することもできます。 3. Apps Scriptの準備 関数/変数を以下の4つのgsファイルに分ける形で作りました。 sendTestRequest.gs getDataByTestId.gs util.gs variable.gs 個々の変数,関数はスコープの中に入れなければグローバルになるため、他のgsファイルからそのまま利用する事ができます。 ※突貫で作ったコードなので、Apps Script APIへの負荷の高い処理があるかもしません。。 ・sendTestRequest.gs WebPagetestにテスト実施のリクエストを投げ、レスポンスで返ってくるtestIdをセルに書き込みます。 Apps Scriptでは、UrlFetchApp.fetch()を使う事で簡単にHTTPリクエストが投げられます! ペイロード付きのPOSTリクエストもできるので、いろいろと応用が効きそうです。 ・Class UrlFetchApp | Apps Script | Google Developers function sendTestRequest() { var sheetName,sheet,url,res,jsondata,testId,testIdLastRow; var activeSheet = SpreadsheetApp.getActiveSpreadsheet(); for(var i=0; i < targetObj.length; i++){ sheetName = targetObj[i].sheetName; sheet = activeSheet.getSheetByName(sheetName); url = createTargetUrl(targetObj[i]); res = UrlFetchApp.fetch(url); jsondata = JSON.parse(res.getContentText()); testId = jsondata.data.testId testIdLastRow = sheet.getRange(testIdRow + (getLastRowNumberByColumn(sheet,1) + 1)); testIdLastRow.setValue(testId); } } ・getDataByTestId.gs testIdを元に、WebPagetestのテスト結果のJSONを取得し、セルに書き込みます。 function getDataByTestId(){ var sheetName,sheet,last_row,dateLastRow; var activeSheet = SpreadsheetApp.getActiveSpreadsheet(); for(var i = 0; i < targetObj.length; i++){ sheetName = targetObj[i].sheetName; sheet = activeSheet.getSheetByName(sheetName); lastRow = sheet.getLastRow(); dateLastRow = getLastRowNumberByColumn(sheet,2) + 1; for(var j = dateLastRow; j <= lastRow ; j++){ testId = sheet.getRange(testIdRow + j).getValue(); if(testId != '' && testId != '0'){ setTestValue(sheet, getValueByTestId(testId), j); } } } function getValueByTestId(testId) { var url = WPT_RESULT_URL + '?test=' + testId; var res = UrlFetchApp.fetch(url); res = JSON.parse(res.getContentText()); res = res.data; return res } function setTestValue(sheet,data,row){ var targetRange,val; for (var prop in metricsObj) { targetRange = sheet.getRange(metricsObj[prop].header + row); val = getDescendantProp(data,metricsObj[prop].resValue); if(val){ if (prop == 'date') { val = dateExchange(val); } else { val = Math.round(val); } targetRange.setValue(val); } } } } testIdを取得済みのシートに対してこの関数を実行すると、以下のような形で動作します。 ・util.gs その他の関数を宣言します。 function getDescendantProp(obj, desc) { var arr = desc.split('.'); while(arr.length && (obj = obj[arr.shift()])); return obj; } function dateExchange(unixTimeStamp) { return Utilities.formatDate(new Date(unixTimeStamp * 1000), 'GMT+9', 'YYYYMMddHH') } function createRequestUrl(opt) { return WPT_TEST_URL + '?url=' + encodeURIComponent(opt.url) + '&k=' + WPT_REQUEST_PARAM.k + '&video=' + WPT_REQUEST_PARAM.video + '&f=' + WPT_REQUEST_PARAM.f + '&mobile=' + WPT_REQUEST_PARAM.mobile + '&runs=' + WPT_REQUEST_PARAM.runs + '&fvonly=' + WPT_REQUEST_PARAM.fvonly + '&location=' + WPT_REQUEST_PARAM.location + '&mobileDevice=' + WPT_REQUEST_PARAM.mobileDevice; } function getLastRowNumberByColumn(sheet, column){ var last_row = sheet.getLastRow(); for(var i = last_row; i >= 1; i--){ if(sheet.getRange(i, column).getValue() != ''){ return i; break; } } } ・variable.gs 計測対象や計測指標などの変数を入れておきます。 var WPT_URL = 'https://www.webpagetest.org/'; var WPT_API_KEY = ${API_KEY}; var WPT_TEST_URL = WPT_URL + 'runtest.php'; var WPT_RESULT_URL = WPT_URL + 'jsonResult.php'; var WPT_REQUEST_PARAM = { k: WPT_API_KEY, video: 1, f: 'json', mobile: 1, runs: 1, fvonly: 1, location: 'ec2-ap-northeast-1.3GFast', mobileDevice: 'iPhone5c' }; var testIdRow = 'A'; var targetObj = [ { sheetName: 'mediba_top', url: 'http://www.mediba.jp/' },{ sheetName: 'mediba_blog_top', url: 'http://ceblog.mediba.jp/' } ]; var metricsObj = { date:{ header: 'B', resValue: 'completed' }, firstViewSpeedindex:{ header: 'C', resValue: 'average.firstView.SpeedIndex' }, TTFB:{ header: 'D', resValue: 'average.firstView.TTFB' }, domInteractive:{ header: 'E', resValue: 'average.firstView.domInteractive' }, firstPaint:{ header: 'F', resValue: 'average.firstView.firstPaint' }, domContentLoadedEventStart:{ header: 'G', resValue: 'average.firstView.domContentLoadedEventStart' } } トリガーの設定 以下の2つの関数は定期的に実行したいので、それぞれを時間主導型のトリガーで設定します。 テストリクエストを投げる関数(sendTestRequest) テスト結果を取得する関数(getDataByTestId) ここではテストリクエストを1時間に1回、テスト結果の取得を4時間に1回としています。 4. Data Studioの準備 一通り準備ができたので、あとはData StudioでSpread Sheetに蓄積されたデータを読み込むだけです。 データソースから[Google スプレッドシート]を選択し、任意のスプレッドシート、ワークシートを選択します。 Spread Sheetから取得する日付の値は、Data Studioでは[時間ディメンション]として扱いたいのでタイプを[日付 時]とします。 平均値の値が必要な場合は、既存のフィールドを複製した上で[集計方法]を[平均値]にします。 ※ データソースを[Googleアナリティクス]で選択することでAnalyticsからのデータインポートも可能になります。 完成したレポート 1時間に1回のスパンでテスト実行した結果をレポートにしてみました。 Spread Sheet ・シート その1 ファーストビュー内でA/Bテストが稼働し、さらにネットワーク広告も存在するサービスのデータです。 SpeedIndexのヒストグラムが多峰性になり標準偏差も大きい事がわかります。 ・シート その2 広告など、サードパーティースクリプトの影響が少ないサービスのデータです。 SpeedIndexの標準偏差が少なく、SpeedIndexとDomInteractive,FirstPaintとの相関係数は0.9を超えています。 ・シート その3 サービスごとの数値を横断して見れるようにしています。 複数のサービスでSpeedIndexとFirstPaint,DomInteractiveの相関係数を確認したところ、そのほとんどが0.7を超えていました。(データの分布は単峰性) クリティカルレンダリングパスと描画の関連性を考えると因果関係に近いと思いますが、データとしても強い正の相関があるという事が分かりました。 Spread Sheetには統計で利用できる関数や、図を展開する機能があらかじめ用意されているので、誰でも簡単に統計的な視点でデータを眺める事ができそうです。 Data Studio Grafana風の色合いでレポートを作ってみました。 自由にデザイン変更が可能で、レイアウトのグリッドも綺麗に整います。 こんな形でWebPagetestのデータ(Synthetic)とGoogle Analyticsで収集しているデータ(RUM)が一覧表示ができるようになりました。 期間ツールを入れてるので、Analytics同様に任意の期間で絞り込みできるようになります。 残念ながらData Studioは、1つのグラフに複数のデータソースの指標を入れることができないみたいです。(2016年時点) 今回のようにWebPagetestとAnalyticsのデータソースがあった場合に、それぞれの指標を重ねる事ができません。 実現するためには、それぞれのデータを1つのSpread Sheetの1つのシートにまとめた上で、それをデータソースとして読み込むという形になります。 異なるデータソースで指標を計算する場合も同様です。 (例: [PV]を[特定のクリックイベント]で割り算した[CTR]の変化を折れ線グラフで追う、など) そのため、Data Studioを利用する上ではSpread Sheetを活用する事が必要になるのかなと思いました。 おわりに 最初はData Studio寄りの記事を書こうと思っていたのですが、それ以外のプロダクトが面白くてついつい遊んでしまいました。。 Apps ScriptはSpread Sheet専用ではないので、Gmailなどの他アプリケーションと連携してみると面白いかもしれません。 たとえばA/Bテストを実施している場合に[t検定をApps Script側で実施し、有意差が出た場合に自動的にGmailで送信する]といった事もできるかもです。 また集計データが増えてくると辛くなりそうなのでデータはCloudSQL側で保持してもよさそうです。 今回作った仕組みで、ある程度参考になる集計が可能になりそうなので、今後は同業種のWEBサイトのベンチマークを標準化してパフォーマンス比較してみたいと思います。 ・備考 API制限があるため試行回数を減らしていますが、信頼性の高い数値として扱うには回数が不足しており、またパフォーマンス計測としてのWebPageTestの信頼性も考慮する必要があるかと思います。 ただ、パフォーマンス計測に十分なコストをかけられないような状況も多いと思うので、まずは厳密さに執着せずできる事をやり、課題を抽出する事が大切なのかなと思っています。 参考URL Critical rendering path - Crash course on web performance Speed Index – how it works and what it means Performance Calendar » Speed Index Tips and Tricks Using WebPageTest - O'Reilly Media WebPagetestをご存知ですか? - Akamai Japan Blog TEN THINGS YOU DIDN’T KNOW ABOUT WEBPAGETEST.ORG O'Reilly Japan - パフォーマンス向上のためのデザイン設計 DIY Synthetic: Private WebPagetest Magic Sheets API | Google Developers Apps Script | Google Developers Google Data Studio Google Data Studio: a Nifty, Free, Easy-to-use Data Vis Tool
アバター
全国の Ansible 派のみなさん、こんにちは。 Chef より Ansible 派、インフラストラクチャー部の沼沢です。 Ansible を利用する際に、task の実行結果を register に入れて後続の task で利用したりしますよね。 自分は AWS の構築に Ansible を利用することも多いのですが、例えば以下のように、 aws ec2 describe-instances の実行結果を register で変数に代入して使うというのはよくあることです。 - tasks: - name: numacchi インスタンスの Instance ID 取得 shell: > aws ec2 describe-instances \ --region ap-northeast-1 \ --filters Name=tag:Name,Values="numacchi" \ --query 'Reservations[].Instances[].InstanceId' \ --output text changed_when: False register: instance_id - name: EIP 付与 ec2_eip: region: ap-northeast-1 device_id: "{{ instance_id.stdout }}" 上記のように、Instance ID だけを取りたいならこれで良いのですが、後続の処理で Instance ID や VPC ID、Subnet ID 等、必要な情報が複数ある場合、都度 discribe-instances を実行して1つずつ取得するのは非常に効率が悪いし、何より美しくないですよね。 できれば、1回の task の実行で必要な情報を全て取得して、取得した json を dict オブジェクトとして使いたいですよね。 そこで今回は、 set_fact というモジュールを利用してこの課題を解決する方法をご紹介したいと思います。 set_fact モジュールとは、task 内で変数をセットするモジュールです。 set_fact - Set host facts from a task — Ansible Documentation 実例 では早速、例を使ってご紹介します。 以下は、既存のインスタンス (Name タグの値が “numacchi” のインスタンス) と同じ Subnet に同じスペックのインスタンスをもう1台構築するという例を実現する Ansible です。 - tasks: - name: EC2 Instance 情報取得 shell: > aws ec2 describe-instances \ --region ap-northeast-1 \ --filters Name=tag:Name,Values="numacchi" \ --query 'Reservations[].Instances[].{"Name": Tags[?Key==`Name`]|[0].Value,"SubnetId": SubnetId,"ImageId": ImageId,"VolumeId": BlockDeviceMappings[0].Ebs.VolumeId,"InstanceType": InstanceType,"InstanceProfile": IamInstanceProfile.Arn,"KeyName": KeyName,"SecurityGroups": SecurityGroups[].GroupName}|[0]' changed_when: False register: ec2_info - set_fact: instance: "{{ ec2_info.stdout }}" - name: EBS 情報取得 shell: > aws ec2 describe-volumes \ --region ap-northeast-1 \ --volume-ids "{{ instance.VolumeId }}" \ --query 'Volumes[].{"DeviceName": Attachments[0].Device, "VolumeType": VolumeType, "Size": Size}|[0]' changed_when: False register: ebs_info - set_fact: ebs: "{{ ebs_info.stdout }}" - name: "{{ instance.Name }} インスタンス追加作成" ec2: region: ap-northeast-1 vpc_subnet_id: "{{ instance.SubnetId }}" state: present image: "{{ instance.ImageId }}" instance_type: "{{ instance.InstanceType }}" instance_profile_name: "{{ instance.InstanceProfile | regex_replace('.*/', '') }}" key_name: "{{ instance.KeyName }}" volumes: - device_name: "{{ ebs.DeviceName }}" volume_type: "{{ ebs.VolumeType }}" volume_size: "{{ ebs.Size }}" group: "{{ instance.SecurityGroups }}" instance_tags: Name: "{{ instance.Name }}" 解説 例の Ansible は以下のことを実施しています。 ec2 モジュール実行に必要な情報を、 aws ec2 describe-instances で取得 取得した結果を set_fact モジュールで “instance” という変数に代入 EBS の情報も必要なので、 aws ec2 describe-volumes でそれぞれ取得 取得した結果を set_fact モジュールで “ebs” という変数に代入 取得した情報をもとに、ec2 モジュールでインスタンスを Launch 今回のポイントは、 set_fact モジュールで register の出力結果の json 文字列を渡すことで、 dict オブジェクトとして扱えるようにしている という点です。 “EC2 Instance 情報取得” task では、以下の情報を取得する CLI を実行しています。 Name (Name タグの Value) SubnetId ImageId (AMI の ID) VolumeId (EBS の ID) InstanceType InstanceProfile (IAM ロールの ARN) KeyName (KeyPair の名前) SecurityGroups (アタッチされている Security Group のリスト) 取得される JSON は以下のような形です。 { "Name": "numacchi", "SubnetId": "subnet-xxxxxxxx", "ImageId": "ami-xxxxxxxx", "VolumeId": "vol-xxxxxxxxxxxxxxxxx", "InstanceType": "t2.micro", "InstanceProfile": "arn:aws:iam::xxxxxxxxxxxx:instance-profile/numacchi", "KeyName": "xxxxxxxx", "SecurityGroups": [ "securitygroup1", "securitygroup2" ] } 上記の json 文字列を set_fact で instance という変数に代入し、これを次の “EBS 情報取得” task で、 "{{ instance.VolumeId }}" というように呼び出して利用しています。 同じ要領で、"EBS 情報取得" task で情報を取得し、最後に EC2 の Launch 実行の task を実行しています。 おまけ 例として記載した Ansible 内の AWS CLI の --query オプションでは、出力結果をうまいこと整形する、結構凝った使い方をしてい(ると思い)ます。 この --query オプションの使い方も、参考になれば幸いです。 まとめ 実は自分自身、task 内で何回も同じ CLI を叩いていたので、他に良いやり方が無いか調査や検証をしてこのやり方に至ったという経緯がありました。 今回は AWS CLI の実行結果を例にご紹介しましたが、もちろん、json 文字列の場合はいつでも使えますので、是非ご活用してみてはいかがでしょうか。
アバター
こんにちは、品質管理グループの 山本久仁朗です。 みなさんの組織では、テスト対象端末(スマホ・タブレット・ガラケー・etc)を 選定する際に、どのように選定していますか? 選定方法によっては、不具合を発見するには、あまり効果的ではない アプローチも多々あります。 今回は、我々の組織での機種選定方法について、お話いたします。 より効果的なテストを行うために 2016年3月末の内閣府の調査により、スマートフォンがガラケーの世帯保有数で 逆転したというニュースも記憶に新しいと思います。 http://www.nikkei.com/article/DGXLASDF08H0R_Y6A400C1EE8000/ いまでは多種多様なスマートフォンが発売されていますが、特に Android は日本で発売された機種だけでも500機種以上存在しているために、 すべての機種でテストを実施するのは現実的ではありません。 しかし機種を選定するにしても、考慮すべき要素として OS Ver・メーカー・ 解像度・メモリー・CPU・GPU・GPS・TV機能・ハイレゾ対応・各種センサーの 有無など、組合せが膨大にあります。 プロダクトリスクから、より効率的に動作保証しつつ不具合を検出するために、 効果的なテスト対象機種の選定方法を定義しておく必要があります。 機種選定のポイント 機種選定のポイントは、前述の通り多くの要素があります。 その中でも、アプリとウェブサイト(ブラウザアプリ)のテストを実施する際の ポイントは、下記の項目を軸に考えるのがオススメです。 * 要素選定 * 優先順位付け * インパクトとスコープ * 機種選定 要素選定 OS バージョン なぜ OS バージョンでの選定・網羅が必要なのか? OS のメジャーバージョン・APIレベルに応じてOSの機能が大きくことなり、 表示・動作が異なる場合ために、プロダクトリスクに応じて、対象OS バージョンの選定が必要になってきます。 具体的な例として、メジャーバージョンアップしたとたんに、アプリが 起動しなくなったり、特定の機能が使えなくなったりする場合があります。 その他にも、ウェブサイトの表示において特定のメンテナンスバージョンでのみ JavaScript やウェブアプリが動作しない・表示しないという現象が発生します。 そのOSバージョンですが、iOS・Android の両OSにおいて、下記のように3段階の レベルで表記されています。 【OS バージョン のレベル】 XX.YY.ZZ XX : メジャーバージョン YY : マイナーバージョン ZZ : メンテナンスバージョン(リリース、マイクロ 等と呼ばれる場合もある) 基本的に、発売時のバージョン、その後アップデート可能な各バージョン、 最新バージョンが各々存在します。 Android は、完全初期化を行うことで OS が出荷時のバージョンになる場合もあります。 iOS は、基本的にアップデートした場合、ダウングレード出来ません。 (ベータ版配信期間は、バックアップから復元することも可能な場合があります) Android について、多くの日本メーカーのキャリアから発売されている機種の場合は、 キャリアから配信されるバージョンのみに限定されていますが、海外メーカーの場合は キャリアに依存せずにバージョンアップできる機種があります。 最新のメジャーバージョン・APIを試すためには、海外メーカーの機種を使うことが多いです。 最近ではSIM フリー・格安スマホの台頭により、日本メーカーでも適時バージョンアップ できる機種が発売され始めました。 また iOS の場合は、アップデート可能な機種の場合は Apple から配信される タイミングで更新可能になりますので、非常に多くのバージョンが存在します。 ただし、基本的には各機種でアップデート可能な最新のバージョンにアップデート 可能なために、OSのシェアとしては Android ほどシェアが分散することはありません。 特に メジャーバージョンアップが行われた際には、アップデート可能なユーザーが 2週間程度で70%以上アップデートしているそうです。 メーカー なぜ メーカーでの選定・網羅が必要なのか? Android 端末は、メーカー毎にさまざまな違いがあり、主に HW と SW(アプリ・OS・ファームウェア)の2つの側面があります。 HW については、アプリの機能によっては大きく影響を受けますが、 詳細は後述させていただきます。 SW の中でも個人的に Web系のサービスにとって特に影響が大きいのは、 Android 4.3(4.1-4.3:Jelly Bean)以前の、AOSP Stock Browser (通称 Android標準ブラウザ)をベースの「ブラウザ」アプリの存在です。 「ブラウザ」アプリは、各メーカーで独自実装(魔改造?)をしているために、 動作が異なる場合があります。スマートフォン向けのサービスを開発・提供 している方々なら、特定の機種・メーカーのデバイスだけ下記のような話を 聞いたことがあると思います。 Android 4.4(KitKat)から、代わりに Chrome WebView が提供されるように なり独自性は少なくなったようです。 【標準ブラウザ起因の不具合】 JavaScript の特定の機能が動かない WebView で画像が表示されない(白画面) 画面遷移が行われない etc 【主要メーカー】 ソニー 京セラ シャープ LG HTC サムスン 富士通 Apple 画面解像度 なぜ 画面解像度での選定・網羅が必要なのか? Android・iOS の両方で起きる問題として、小さい画面において文字・ボタン・ バナー等のアイテムが見切れてしまったり、レイアウト全体が崩れてしまったり する場合があります。 大きい画面においてはスマートフォンでPC版のページを 見たときのように、文字やボタンが小さすぎて見づらい操作しづらい等の問題が 発生します。 ここ数年 Full HD(1920×1080)が最も主流になっているために、iPhone4S(960×640)や Androdi2.3・4.0系(800×480)でチャット等のコミュニケーション機能を使おうとすると、 キーボードが表示されるとメッセージ・画面が見えなくなってしまうなども発生します。 【主要画面解像度】(採用機種数順) 1920 × 1080 1280 × 720 960 × 540 800 × 480 2560 × 1440 1136 × 640 960 × 640 内部メモリ容量 なぜ内部メモリ容量での選定が必要なのか? 内部メモリ容量のサイズによっては、特定の機種ではアプリ自体が起動出来ない・ 使用しているとメモリが枯渇して落ちる場合があるからです。 最近は、スマートフォンの性能が向上していますので、あまり気にならなくなって きていますが、Android 2.3 系の端末はほとんどが 512MB という容量だったために、 OS と標準アプリが動作すると 100MB 程度しか空きがなくなってしまいました。 メモリ容量が少なければ、初期動作をしても使い続けるうちに動きが遅くなり、 時には強制的に終了したりする場合もあります。 そんなこともあってか、Android には多くのタスク管理アプリがあり使っていない アプリ・メモリ領域を止めたり開放したりする必要がありました。 【Android OS毎の主流メモリ容量】 Android 2.x : 0.5 ~1.0 GB Android 4.x : 1.0 ~3.0 GB Android 5.x : 2.0 ~3.0 GB Android 6.x : 2.0 ~4.0 GB CPU なぜ CPU での選定が必要なのか? アプリなどは CPU の性能により動作・ユーザー体験が大きく変わります。 端末によっては性能が低すぎてサービス・UXとして成り立たない場合があります。 また最近はほとんど影響がなくなってきていますが、CPUのブランドによる動作の 違いもあります。 特に Android 2.x~4.0.x が主流だった時期にスマホの サービスに関わっていた方らならば、特定のCPU・チップセットで問題が発生する という現象を経験をした人は少なからず、いらっしゃるのではないでしょうか? 現状 Qualcomm・Snapdragon・Mediatek の ARM系アーキテクチャのブランドが 非常に強いですが、ASUS・Lenovo等の PC系のメーカーは Intel Atom プロセッサーを 採用しているようです。 例:ビリヤードなどのゲームで、物理演算と描画が間に合わなくて、 コマ落ちしてしまい球の軌道がまるでわからないために、ゲームとして成り立たない キャリア回線 なぜキャリア回線での選定が必要なのか? とくにキャリア回線のうち、4G(LTE)・3.9G(WiMAX等)と3Gでは、下記のような 違いが有り、速度・つながりやすさ等要因で様々な不具合が発生します。 また、メーカーやキャリア回線だけではなく Wi-Fi を併用した場合の弊害もあります。 さらに、各キャリア毎に使用量の上限になった場合の制限速度も異なります ( 64K ~ 256Kbps )ので、スコープによっては検討する必要があります。 【4G(LTE) と 3G の違い】 4G: 速度 速い(75~100Mbps程度) エリア 拡大中 3G: 速度 遅い(数~14Mbps程度) エリア 人口カバー率が、ほぼ100% 特殊なデバイス条件 カメラなし端末 最近はほとんどないと思いますが、Android タブレットの中には、カメラデバイスが 存在しない端末もあります。 SNS・ECアプリ等でカメラを起動して画像をアップする機能を有するものもありますが、 カメラを認識しないとエラーとなりローカルストレージの画像も取り込めなくなって しまうアプリも幾つか存在しているようです。 SDカードがないと、画像を取れない Android 4.0.x 以前の機種に存在しますが、アプリ内でカメラを使用した場合に 画像を保存できずにエラーになる場合があります。 キーボード付き こちらも最近ほとんど見なくなりましたが、スマートフォンはスライドして キーボードが出てくるタイプには、テンキーとフルキーボード等があります。 キー入力等をする際に、画面内にキーボードが表示されないことなど、 確認できると良いですね。 2画面 ごく一部のスマートフォン・タブレット端末で2画面を採用して、 「折りたためる」ことをコンセプトにしている機種があります。 特別な問題は、ほとんど発生しませんが画像・文字が見切れる、 見づらい等の課題があるようです。 ハイレゾ対応 ハイレゾとは「ハイレゾリューション・オーディオ」(高解像度音源)の略 ですが、音楽再生アプリにおいてハイレゾ音源データを再生できるか非常に 重要だと思います。 もちろんハイレゾ非対応のスマホでも通常音源データを再生できることも 必須だと思います。 フルセグ対応 ここ数年、ワンセグだけでなくフルセグ対応の機種も増えています (30機種弱)、ワンセグとフルセグの違いは簡単に言うとワンセグの ほうが受信しやすく録画サイズが小さい、フルセグのほうが画像が きれいというところですが、アプリによっては、ワンセグ・フルセグの 自動切り替え等の様々な機能を有しています。 ほとんどの端末が TV録画アプリがプリインストールされていますので、端末選定の要素に なることは少ないと思います。 その他にも、下記のような多くの要素(デバイス・性能)が存在しますが、 プロダクト・サービスのテスト目的に応じて必要な要素を選定してください。 例:GPS・モーションセンサー・ジャイロセンサー・カメラ機能 (解像度・感度・シャッター速度・望遠・オートフォーカス・顔認証) ・指紋認証・NFC・防水・etc 優先順位(リスク)付け テクニカル・プロダクトリスクの観点 プロダクト・サービスを実現・継続するために、重要な機能から外せない 要素(SW・HW)を選定する必要があります。 JavaScriptをバリバリ使って実装している場合、ブラウザ・OS Version 等は、 考慮が必須になります。 また、リッチなネイティブアプリの場合は、CPU(・GPU)や内部メモリの考慮が 必須になると考えます。 ビジネスリスクの観点 ビジネスリスクとして、利用者の環境・ユースケースを考慮する必要があります。 企業向けのサービスで、社内 Wi-Fi 経由のみでの利用が想定している場合は、 キャリア回線が対象外になるでしょう。 またサービス対象のキャリアが限定される場合は、対象のキャリア回線で実施 しなけば必須となります。 後述します、ビジネスインパクトとして各種シェアも観点抽出には重要な ファクターになります。 インパクトとスコープ インパクト 前述の優先順位付けで抽出した要素から、下記のような各種シェア&KPIを ベースに各要素から対象を選定します。 近年ではユーザー行動などを解析してデータマイニングなどして、より効果的な 要素を選定している組織も多いのではないでしょうか。 各種シェア 端末シェア(製造台数・販売台数・etc) UU(ユニークユーザー数) PP(ページビュー数) DAU(デイリーアクティブユーザー:1日の利用者数) MAU(マンスリーアクティブユーザー:1ヶ月の利用者数) KPI ARPU(ユーザー1人あたりの平均売上金額) CVR(コンバージョンレート:広告参照後に購入・資料請求に至る割合等) スコープ インパクトと一緒に考えることが多いともいますが、技術&ビジネス リスクからインパクトを算出・想定して、どの程度の範囲・粒度を 品質保証範囲としたいのかに応じて、スコープは変わります。 例えば、過去の事例や技術的な要素から、デバイスの考慮が不要で OS Version もメジャーなものをカバーすれば良いとなるとかなり 機種数が絞られると思います。 別の考え方として、毎月の売上が数十億円以上のサービスの場合は、 技術リスクが低くてもビジネスインパクト的にスコープを広げる必要が 出てくると思います。 機種選定 上記の優先順位付けで選定した要素(SW・HW)とスコープの組合せで、 具体的な機種を選定します。 我々の組織では、下記のような機種数で実施しています。 プロジェクト・プロダクトの状況に応じて、下記のように1つの プロジェクトの中で、複数のスコープを組み合わせる場合があります。 機種選定とテスト目的の組合せの例 全機種   : 起動終了&メイン機能1回実施 メインパス : OS マイナーバージョン & メーカー & 主要ブラウザ 既存機能  : OS メジャーバージョン 我々のチームでは、au端末での確認を主なサービスとしておりますので、 下記のようにレベル分けしプロジェクトに合わせて機種選定しています。 例えば、下記のような組合せで実施しています。 新規アプリの場合 起動終了&主要機能:全機種(レベルA:120端末) 各機能確認:OS×メーカー(レベルE:16端末) 既存アプリでWebView関連改修の場合 各面・機能確認:OS×メーカー(×2)(レベルD:32端末) (各開発コードで、標準ブラウザを網羅する) 新規Webサイト(ブラウザゲーム等も含む) 各面・機能確認:OS × ブラウザ(レベルJ:20端末) まとめ テスト目的に合わせて、機種選定をしないと抜け漏れが発生するだけではなく、 無駄な工数が発生してしまいます。 みなさんも、もう一度機種選定方法を見直しては如何でしょうか? もしかするとテスト工数が効果的に削減できるかもしれません! より良いプロジェクト運営の一助になれば幸いです。
アバター
こんにちは。システム本部の山田です。 (この記事は mediba Advent Calendar 2016 の19日目です) 弊社では機械的に実施する脆弱性診断はOWASP ZAPを利用しており、アプリを立ち上げGUIから脆弱性診断を実施しています。ただ継続的に実施していくには面倒でつい後回しになるため、自動化して開発プロセスに組み込む検討を始めました。 調査の際に作ったOWASP ZAP APIを使ったサンプルの紹介をします。 サンプルコード こちら 利用技術 Docker OWASP ZAP php-owasp-zap-v2 faraday 動作環境 筆者が動作検証した環境は以下になります。 OS X 10.11.6 Docker 1.12.3 構成 security_testing_by_zap-api/ ├── RAEADME.md ├── attack #php-owasp-zap-v2でZAP APIを介して脆弱性診断 │ ├── Dockerfile │ ├── attack.php │ ├── composer.json │ ├── composer.lock │ └── composer.phar ├── docker-compose.yml ├── faraday #脆弱性監査レポート │ ├── Dockerfile │ └── docker-compose.yml ├── php7fpm #診断対象用テストサイト(XSSがあるくそサイト) │ ├── Dockerfile │ ├── index.php │ └── printpost.php ├── report #診断後に出力され、faradayが参照するxml置き場 │ └── workspace │ ├── process │ └── unprocessed └── testsite #診断対象用テストサイトのnginx ├── Dockerfile └── server.conf 概要 1. attackからzapを経由してtarget siteに対して脆弱性診断を実施 2. 診断結果xmlをreport/workspaceに格納 3. faradayで結果を閲覧 手順 Docker起動 $cd security_testing_by_zap-api $docker-compose build #初回のみ $docker-compose up -d 脆弱性診断実行 $docker run -it --rm --link=zap2 --link=testsite -v $PWD/report:/home/attacker/report -u attacker securitytestingbyzapapi_attack php ./attack.php レポート閲覧 faradayの起動 $docker run -it -p 5984:5984 -v $PWD/report/workspace/:/root/.faraday/report/workspace/ infobyte/faraday /root/run.sh レポートイメージ http://127.0.0.1:5984/_ui/#/dashboard/ws/workspace まとめ APIを利用することで効率化、faradayに流し込むことで可視化が出来ることがわかりました。なお、Jenkinsには既にプラグインがありCIに組み込むことはすぐにでも可能です。 今後は品質管理部隊とも連携して導入の仕方を検討していく予定です。 参考 Jenkins と owasp zap で自動診断 - SlideShare Jenkins と OWASP ZAP で自動診断 - Qiita OWASP ZAP の PHP client API を作りました - yukisovのメモ帳
アバター
こんにちは。 インフラストラクチャー部の山下です。 最近、チームのリーダーになってからあまりターミナルを触っていなかったのですが、久々にがっつり触れたので記事にしてみました。 (この記事は mediba Advent Calendar 2016 の15日目です。) 入退社や配属の変更などがあり、EC2のユーザアカウントの管理は頭が痛い問題です。 弊社ではお手製のツールを使って管理を行っていますが、今回はEC2上にLDAPサーバを立ててLDAP認証で各EC2にログインする検証を行ったのでご紹介します。 LDAPとは? Lightweight Directory Access Protoclの略で、ディレクトリサービスにアクセスするためのプロトコルです。 LDAPサーバ内にあるデータベースでユーザなどを一元管理できます。 要件 クライアントのEC2(以降クライアント)にはSSHの鍵もユーザも登録せず、LDAPのスキーマ内にあるSSH鍵を使用してログインする ユーザによって入れるインスタンスを制御する sudoが可能なグループもLDAPで管理する 構成 今回は検証目的なので冗長化などは考慮していません。 同一VPC内にLDAPサーバとクライアント3インスタンスがあるだけです。 各インスタンス同士でLDAPのポート(389/tcp)の通信を許可してあるものとします。 環境 AmazonLinux 2016.09 slapd 2.4.40 OpenSSH_6.6.1p1 Sudo version 1.8.6p3 構築 では、実際に構築していきましょう。 LDAPサーバ側 1. パッケージのインストール $ sudo yum install openldap-servers openldap-clients pam_ldap openssh-ldap 2. デフォルトデータの削除 $ sudo rm -rf /etc/openldap/slapd.d/* /var/lib/ldap/* 3. slapd.confファイルの作成 OpenLDAP2.4以降slapd.confを使用した設定は非推奨となっており、OLC(On-Line Config)にて設定することが推奨となりました。 OLCを用いると、再起動なしでLDAPの設定変更が可能となりますが、スキーマの追加/修正など行う際にLDIFを作成してインポートする必要があり手間なので、 今回はslapd.confを使用して設定を行います。 $ sudo cp /usr/share/openldap-servers/slapd.conf.obsolete /etc/openldap/slapd.conf 修正した箇所は以下のとおりです。 $ diff -u /usr/share/openldap-servers/slapd.conf.obsolete /etc/openldap/slapd.conf --- /usr/share/openldap-servers/slapd.conf.obsolete 2016-08-16 21:31:54.000000000 +0000 +++ /etc/openldap/slapd.conf 2016-12-09 06:19:30.004239349 +0000 @@ -15,6 +15,8 @@ include /etc/openldap/schema/openldap.schema include /etc/openldap/schema/ppolicy.schema include /etc/openldap/schema/collective.schema +include /etc/openldap/schema/sudo.schema +include /etc/openldap/schema/openssh-lpk-openldap.schema # Allow LDAPv2 client connections. This is NOT the default. allow bind_v2 @@ -77,6 +79,14 @@ # security ssf=1 update_ssf=112 simple_bind=64 # Sample access control policy: +access to attrs=userPassword + by dn="cn=Manager,dc=example,dc=com" write + by self write + by anonymous auth + by * none +access to * + by self write + by * read # Root DSE: allow anyone to read it # Subschema (sub)entry DSE: allow anyone to read it # Other DSEs: @@ -98,16 +108,16 @@ # rootdn can always read and write EVERYTHING! # enable on-the-fly configuration (cn=config) -database config -access to * - by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage - by * none +#database config +#access to * +# by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage +# by * none # enable server status monitoring (cn=monitor) database monitor access to * by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read - by dn.exact="cn=Manager,dc=my-domain,dc=com" read + by dn.exact="cn=Manager,dc=example,dc=com" read by * none ####################################################################### @@ -115,16 +125,16 @@ ####################################################################### database bdb -suffix "dc=my-domain,dc=com" +suffix "dc=example,dc=com" checkpoint 1024 15 -rootdn "cn=Manager,dc=my-domain,dc=com" +rootdn "cn=Manager,dc=example,dc=com" # Cleartext passwords, especially for the rootdn, should # be avoided. See slappasswd(8) and slapd.conf(5) for details. # Use of strong authentication encouraged. # rootpw secret -# rootpw {crypt}ijFYNcSNctBYg +rootpw {SSHA}xxxxxxxxxxxxxxx ※ rootpwはslappasswdコマンドで生成したパスワードを記述します。 4. slapd.confを読み込むようにする /etc/sysconfig/ldap に以下の文字列を追加します。 SLAPD_OPTIONS="-f /etc/openldap/slapd.conf" 5. ldap.confの編集 以下の記述を行うことで、ldapaddやldapsearch時にホスト名の指定などが不要になります。 /etc/openldap/ldap.conf に以下を追加します。 BASE dc=example,dc=com URI ldapi://xx.xx.xx.xx/ 6. スキーマファイルの配置 スキーマを配置してincludeすることにより、SSHの鍵やsudoの属性などを登録する事ができるようになります。 $ sudo cp /usr/share/doc/openssh-ldap-6.6.1p1/openssh-lpk-openldap.schema /etc/openldap/schema/. $ sudo cp /usr/share/doc/sudo-1.8.6p3/schema.OpenLDAP /etc/openldap/schema/sudo.schema 7. slapdの起動 $ sudo service slapd start $ sudo chkconfig slapd on 8. 初期データの作成 $ vim init.ldif # ベースドメイン dn: dc=example,dc=com dc: example o: example objectClass: dcObject objectClass: organization # User OU dn: ou=Users,dc=example,dc=com ou: Users objectClass: organizationalUnit # 管理者 dn: cn=Manager,dc=example,dc=com objectClass: organizationalRole objectClass: simpleSecurityObject cn: Manager userPassword: {SSHA}xxxxxxxxx # Group OU dn: ou=Group,dc=example,dc=com objectClass: organizationalUnit ou: Group # admin Unix Group dn: cn=admin,ou=Group,dc=example,dc=com objectclass: posixGroup cn: admin gidNumber: 1000 # developer Unix Group dn: cn=developer,ou=Group,dc=example,dc=com objectclass: posixGroup cn: developer gidNumber: 1001 # sudo OU dn: ou=SUDOers,dc=example,dc=com objectClass: organizationalUnit ou: SUDOers # sudo defaults setting dn: cn=defaults,ou=SUDOers,dc=example,dc=com objectclass: top objectclass: sudoRole cn: defaults sudoOption: !root_sudo sudoOption: !lecture sudoOption: log_host sudoOption: log_year sudoOption: syslog=local3 sudoOption: logfile=/var/log/sudo.log sudoOption: ignore_dot sudoOption: ignore_local_sudoers sudoOption: timestamp_timeout=0 # %adminグループのsudo許可 dn: cn=%admin,ou=SUDOers,dc=example,dc=com objectClass: top objectClass: sudoRole cn: %admin sudoUser: %admin sudoHost: ALL sudoCommand: ALL # yu-yamashita dn: uid=yu-yamashita,ou=Users,dc=example,dc=com objectClass: account objectClass: posixAccount objectClass: ldapPublickey uid: yu-yamashita cn: Yuki Yamashita uidNumber: 1000 gidNumber: 1000 homeDirectory: /home/yu-yamashita userPassword: {SSHA}xxxxxxxxx sshPublicKey: ssh-rsa AAAABxxxxxxxxxxxxxxx description: system_a_admin description: system_b_admin loginShell: /bin/bash # numasawa dn: uid=numasawa,ou=Users,dc=example,dc=com objectClass: account objectClass: posixAccount objectClass: ldapPublickey uid: numasawa cn: numasawa uidNumber: 1001 gidNumber: 1001 homeDirectory: /home/numasawa userPassword: {SSHA}xxxxxxxxx sshPublicKey: ssh-rsa AAAABxxxxxxxxxxxxxxx description: system_a_admin loginShell: /bin/bash # r-adachi dn: uid=r-adachi,ou=Users,dc=example,dc=com objectClass: account objectClass: posixAccount objectClass: ldapPublickey uid: r-adachi cn: r-adachi uidNumber: 1002 gidNumber: 1001 homeDirectory: /home/r-adachi userPassword: {SSHA}xxxxxxxxx sshPublicKey: ssh-rsa AAAABxxxxxxxxxxxxxxx description: system_b_admin loginShell: /bin/bash このLDIFでは ベースドメイン(大元の入れ物)を作成 Userという組織単位(OU)を作成 LDAP管理者ユーザを作成 Groupという組織単位(OU)を作成 adminというUnixグループ developerというUnixグループ sudoという組織単位(OU) sudoの設定(ここではadminグループに属すユーザのみsudo可能) ついでにyu-yamashitaユーザも作成し、adminグループに所属させ、system_a_adminとsystem_b_adminというdescriptionを付与する ついでにnumasawaユーザも作成し、developerグループに所属させ、system_a_adminというdescriptionを付与する ついでにr-adachiユーザも作成し、developerグループに所属させ、system_b_adminというdescriptionを付与する というデータを記述しています。 9. LDIFの投入 上記で作成したデータをLDAPに登録します。 $ sudo ldapadd -x -D "cn=Manager,dc=example,dc=com" -W -f init.ldif 10. データの確認 ゆとりなのでGUIから確認します。 Apache Directory Studio というツールをインストールし開きます。 SecurityGroupで389/tcpを開けるのをお忘れなく。 File -> New を選択し、LDAP Connectionを選択しNextをクリック ConnectionNameは任意の値、HostNameにインスタンスのIPを入力しNextをクリック Bind DN or User に cn=Manager,dc=example,dc=com を入力 Passwordはldifを作成する際に記述したパスワードを入力しFinishをクリック RootDSE -> dc=example,dc=com を辿っていくと、データが登録されていることがわかります。 以上でサーバ側の設定は完了です。 LDAPクライアント側 1. LDAPクライアントインストール $ sudo yum install openldap-clients nss-pam-ldapd openssh-ldap 2. LDAP認証にする $ sudo authconfig --enableldap --enableldapauth --ldapserver=xx.xx.xx.xx --ldapbasedn="dc=example,dc=com" --enablemkhomedir --update 3. Group情報をLDAPから取得するようにする /etc/nslcd.conf に以下を追加する base group ou=Group,dc=example,dc=com 4. SSHの鍵をLDAPサーバから取得する /etc/ssh/sshd_config に以下の記述を行う AuthorizedKeysCommand /usr/libexec/openssh/ssh-ldap-wrapper AuthorizedKeysCommandUser root 5. sudoできるようにする /etc/sudo-ldap.conf に以下を追加する uri ldap://xx.xx.xx.xx/ sudoers_base ou=SUDOers,dc=example,dc=com bind_timelimit 120 host xx.xx.xx.xx base dc=example,dc=com 次に /etc/pam.d/su 内の auth required pam_wheel.so use_uid のコメントを外す。 最後に /etc/nsswitch.conf に以下を追加する sudoers: ldap files 6. SSHの認証にLDAPを使うようにする $ vim /etc/ssh/ldap.conf uri ldap://localhost/ base dc=example-dev,dc=com ssl no 7. sshdの再起動 $ sudo service sshd restart ここまで行えば、SSHの鍵もユーザもクライアントに作らずログイン出来るようになります。 また、adminグループに所属するユーザのみsudoが可能です。 ログインできるホストに制限をかける 次に、ユーザによってログインできるホストを分けてみたいと思います。 上述のLDIFではsystem_a_admin, system_b_adminという3種類のattributeを記述しています。 ここでは、以下のようなルールで制限を行います。 system_a_admin: system_aのインスタンスのみログインできる system_b_admin: system_bのインスタンスのみログインできる 上記のルールの場合、先ほど作ったユーザは以下のようになります。 yu-yamashita: 全てのインスタンスにログインできる numasawa: system_aのインスタンスのみログインできる r-adachi: system_bのインスタンスのみログインできる 以下はsystem_aのインスタンスの設定を行います。 system_bも同じ要領で設定が行なえます。 1. /etc/pam_ldap.confの編集 /etc/pam_ldap.conf に以下を追記する pam_filter description=system_a_admin nss_base_passwd ou=Users,dc=example,dc=com?sub?description=system_a_admin nss_base_shadow ou=Users,dc=example,dc=com?sub?description=system_a_admin nss_base_group ou=Group,dc=example,dc=com?sub?objectClass=posixGroup 2. /etc/ssh/ldap.confの編集 /etc/ssh/ldap.conf に以下を追記する pam_filter description=system_a_admin 3. /etc/nslcd.confの編集 /etc/nslcd.conf に以下を追記する filter passwd (description=system_a_admin) filter shadow (description=system_a_admin) filter group (objectClass=posixGroup) 上記の設定を行うことで、yu-yamashita,numasawaはログイン可能、r-adachiはログイン不可能となります。 まとめ むしろ構築手順のようになってしまいました…。 OSアカウント管理は結構頭の痛い課題ですが、この記事が少しでもお役に立てば幸いです。
アバター
みなさんこんにちは! 12月に入社しました、 インフラストラクチャー部のあだちん(安達)です。 まだ入社したばかりなのに、もうブログ書くの!? てな感じですが笑 さてさて、 medibaでは検証としてAWSを思う存分使える制度があります。 (hoge万円まで)→素晴らしい しかし、開発メンバーらが、そのままインスタンス起動しっぱなしで、 コストが上がったり。。 セキュリティーグループなどぐちゃぐちゃ。。 インフラメンバーが毎回コンソール入って一つ一つ確認するのは とても荷が重い。。。。 今回はLambdaを使用して定期的に EC2インスタンスを自動削除するスクリプトを作ってみました。 今回やりたいこと ・EC2、EBS、ELB、などの動いているサービスを3ヶ月に一回は自動削除する ・タグで判別し、削除したくないものはそのままにする ・せっかくなのでサーバ建てない「Lambda」でスクリプトを動かしたい 準備するもの ・Lambda ・Python2.7 ・boto3 Mac初期設定 1.ローカル(Mac)にaws cliをbrewでインストール 2.IAMで自分のユーザ作成とaws cliを動かせるようにする 基本AWSのインスタンス情報はJSONで返ってきます。 実際にMac から以下のコマンドを打つとダーッと情報が出てくると思います。 $ aws ec2 describe-instances この情報とともにPython(boto3)と組み合わせてプログラムを作るイメージです。 Lambda初期設定 3.IAMにLambda用ロールを新規で作成する ポリシー名は以下を追加します。(これらを追加しないと実行できません。) ・AmazonEC2FullAccess ・CloudWatchActionsEC2Access ・AWSLambdaVPCAccessExecutionRole 4.Lambdaのコンソールを開き、新規で作成する 5.各種設定 6.EC2インスタンスをタグ判別してterminate(削除)するスクリプトの紹介 #Keyがnodelete、Valueがtrue以外は削除 # coding: utf-8 import boto3 # def lambda_handler(event, context): #if __name__ == '__main__': #EC2上で叩く場合 client = boto3.client('ec2', "ap-northeast-1") resp = client.describe_instances() all_list = [] del_list = [] for reservation in resp['Reservations']: for instance in reservation['Instances']: all_list.append(instance['InstanceId']) #print(all_list) if 'Tags' in instance: for tag in instance['Tags']: if tag['Key'] == 'nodelete' and tag['Value'] == 'true': del_list.append(instance['InstanceId']) #print(del_list) diffset = set(all_list) - set(del_list) #print(diffset) targetlist = list(diffset) print(targetlist) #問題なければ以下のコメントアウトを外して削除してみる # ec2.terminate_instances( # InstanceIds=targetlist # ) 今回Keyを「nodelete」Valueが「true」以外 のものはインスタンスterminate(削除) します。 つまり、上記のタグにしとけば消えることはありません。 7.実際にテストをしてみる まずは対象のインスタンスIDが出力されるか確認しましょう。 問題なければterminateする部分(下から3行目)をコメントアウト外して インスタンスがterminateされたらOKです。 8.先程作ったスクリプトを定期的に実行する Cloud Watchからイベントでルールの新規作成をします。 スケジュールを選択肢し、 ターゲットの追加で先程のLambdaを以下のように追加します。 追加するとLambdaのマネジメントコンソールから Triggerに反映されているはずです。 まとめ Python初心者ですが、わざわざインスタンス建てなくても Lambdaで問題なく削除することができました。(コスト削減) まだ開発途中ですが、全サービス削除できるスクリプトが仕上がったら、 またブログでお会いしましょう! (もっと改めて運用考えねば…) 参考: https://boto3.readthedocs.io/en/latest/
アバター
こんにちは、インフラストラクチャー部の沼沢です。 前回 に引き続き、re:Invent 2016 の Keynote Day 2 で発表された新サービスの概要を一挙にご紹介したいと思います。 AWS Opsworks For Chef Automate Chef サーバのフルマネージドサービス Chef のコミュニティにある Cookbook や Tool を利用可能 Amazon EC2 System Manager EC2 とオンプレミスの構成を管理するためのサービス OS パッチの適用、AMI の更新等を自動化できる OS の設定情報等を確認でき、AWS Config と連携することで設定の記録を取ることも可能 AWS CodeBuild CI 等の一連のプロセスに必要なビルドとテストプロセスのフルマネージドサービス CodeBuild の登場により、CodeCommit → CodeBuild → CodeDeploy の一連の流れと、これを CodePipeline で制御する、という開発時の一連のプロセスを全て網羅できるようになった CodeCommit 以外にも、GitHub や S3 との連携も可能 AWS X-Ray アプリケーションの可視化サービス(X 線のように見通せるというイメージらしい) アプリケーションに SDK と Agent を実装し、トレースデータを JSON 形式で X-Ray に送信 トレースデータをもとに、関連サービスのマッピングを行う トレースデータから、レスポンスタイムやレスポンスコードの収集を行う 上記の事から X-Ray は、普段はエラーがあったら ssh してログを確認したり、CloudWatch Logs でフィルタ、分析していたものを楽にしてくれるサービスと捉えることができます。 また、構成の可視化も行えるので、このシステムでは何の AWS サービスをどこからアクセスしているか、等も簡単に確認することができます。 AWS Personal Health Dashboard AWS サービスについてのメンテナンス告知や障害通知について、アカウント毎に影響のあるもの(利用しているもの)のみを表示してくれるダッシュボード CloudWatch Events や Lambda との連携が可能で、通知に対してのアクションを自動化できる AWS Shield マネージドの DDoS 攻撃防御サービス、有料版と無料版がある 一般的な DDoS 攻撃を無料で防御 有料版の場合は Layer 7 のアプリケーショントラフィックの監視、攻撃の履歴のレポートや、コストの保護等の機能が追加される Amazon Pinpoint モバイルアプリ向けのユーザターゲティングサービス 収集したデータソースからユーザの行動を分析 データソースからセグメントを定義し、セグメント単位での Push 通知が行える ターゲットを絞ったキャンペーンを容易に 実施したキャンペーンの結果を評価 異なるメッセージを A/B テストとして送信し、その結果を評価することも可能 サードパーティのデータソースを使用することも可能(Salesforce 等) AWS Glue フルマネージド ETL サービス S3, RDS, Redshift 等の各種 JDBC 対応のデータソースに接続 接続後、データフォーマットを認識し、移動先のターゲットに対して適切な形を提案してくれる ジョブをスケジューリングして定期的な実行が可能 AWS Batch フルマネージド Batch 実行サービス Batch クラスタの構築や管理が不要に 並列処理に適している Batch 処理を管理レスで実行可能 AWS Batch 自体の利用料金は無料 かかるのは使用した EC2 の料金のみ On-Demand や Spot 等は指定可能 ジョブをスケジューリングすることが可能 Blox ECS 向けコンテナ管理のオープンソースプロジェクト ECS のカスタムスケジューラによるタスク制御、クラスタの管理が可能 GitHub で管理 https://blox.github.io/ ※サービスではありません C# In AWS Lambda Lambda の対応言語に C# が追加 AWS Lambda@Edge Lambda を CloudFront のエッジロケーション上で実行するサービス 対応言語は Node.js のみ 利用可能メモリは 128 MB タイムアウト上限は 50ms 実行タイミングは以下の4パターン Viewer リクエスト: キャッシュの有無を問わず、クライアントからリクエストがエッジに来た時に実行 Viewer レスポンス: キャッシュの有無を問わず、クライアントへのレスポンスがエッジを通過する時(レスポンスを返す直前)に実行 Origin リクエスト: エッジにキャッシュが無い場合のリクエスト時に実行 Origin レスポンス: エッジにキャッシュが無い場合のレスポンス時に実行 HTTP リクエストヘッダの変更や、デバイス判定等をオリジンに来る前に実行したり、クライアントには必要のないヘッダをレスポンス時に削除したり等の処理が実行可能に AWS Step Functions Lambda や EC2, ECS 上のアプリケーションを利用したワークフローのデザインを容易にするサービス JSON で定義 まとめ 以上が Keynote Day 2 のちょっと遅いまとめになります。いかがだったでしょうか。 全体的には、 AWS Glue や AWS Batch 等、ユーザの管理/設計コストを削減してくれるサービスが増えた印象です。 ビジネスロジックに集中してほしいという、AWS の願いが具現化してきたように思えます。 個人的にとても興味のあるサービスは AWS Step Functions です。 今まで単独実行が基本だった Lambda をワークフローに乗せることができるというのはとても魅力的に感じました。 正直もう EC2 上にアプリケーション作らなくても良いのではないかぐらい(言い過ぎ)。 今後これらを検証・利用し、積極的にノウハウを公開していきます。 最後に、2日間で発表されたサービスの一覧画像です。お納めください。
アバター
こんにちは、インフラストラクチャー部の沼沢です。 今回は、先日開催された AWS のグローバルカンファレンス「re:Invent」に、弊社山下と山子澤の3人で参加してきましたので、レポートさせていただきます。 2日間の Keynote では、大量のサービスが発表されお腹いっぱいな状況ですので、本稿では1日目の Keynote で発表されたサービスの概要を一挙にご紹介していきたいと思います。 Amazon Lightsail 所謂 VPS のサービス WordPress など、よく使われる構成が、構成済みの状態で使える サーバ・ストレージ・データ転送なども全て込み込みで、月額 $5〜 利用可能 Amazon Elastic GPUs EC2 インスタンスに柔軟にアタッチ・デタッチ可能な GPU サービス 時間単位の従量課金 GPU インスタンスの P2/G2 ファミリーのインスタンスよりも小さい処理性能だが、その分安価で、P2/G2 ファミリー程の性能を求めていない場合に最適 新インスタンスファミリーの登場 T2 ファミリーのラインナップ拡充 t2.xlarge t2.large のおよそ2倍の性能 ベースライン性能は 90 % CPU クレジットは1時間で 54 クレジット回復 t2.2xlarge t2.large のおよそ4倍の性能 ベースライン性能は 135 % !! CPU クレジットは1時間で 81 クレジット回復 R4 ファミリー メモリ最適化インスタンスファミリーの第4世代 R3 インスタンスよりも L3 キャッシュが大きい I3 ファミリー High I/O インスタンスファミリーの第3世代 4KB ランダムアクセスで最大 330万 IOPS を実現 I2 では最大 365,000 IOPS だったので、約9倍に C5 ファミリー コンピューティング最適化インスタンスファミリーの第5世代 Intel の次世代 Xeon プロセッサ「Skylake」を搭載 F1 ファミリー FPGA 用インスタンスファミリー プログラム可能なハードウェア(Xilinx 社製の FPGA)を搭載 PostgreSQL for Aurora PostgreSQL 9.6 との完全互換の Aurora PostgreSQL と比較して約2倍のスループット Amazon Athena S3 上のオブジェクトに対して直接クエリを実行できるサービス クエリは SQL 形式 JSON, CSVやログ等の区切り文字のあるテキスト, Apache Parquet, Apache ORC に対してクエリの実行が可能 Amazon Rekognition 画像認識の AI サービス 状況や物体の認識(昼、自動車、運転中等)、顔認識、表情から感情を読み取るなどのことができる Amazon Polly Text to Speech の AI サービス 26の言語に対応(日本語含む) Amazon Lex Alexa の音声認識エンジンを利用できるサービス 音声認識と自然言語解析処理ができる AI AWS Greengrass デバイス上で Lambda を動かす(デプロイする)サービス 従来は IoT デバイス上で収集したデータを AWS IoT などに送ってから Lambda 等で処理していたが、送る前にデータの整形やフィルタ等の処理をデバイス上で実行することができる Snowball Edge 100TB のストレージ Snowball の2倍 複数個でクラスタリングが可能 Lambda ファンクションを稼働させることが可能(Greengrass) Snowmobile 100PB のストレージ エクサバイト(ペタの上)級のデータを移行するために作られたサービス 26年かかると思われていたことが6ヶ月で可能になる 最大 1Tbps で書き込み可能 米国のみ (Keynote 会場にトレーラー入ってきた時はネタかと思いました) まとめ 以上が Keynote Day 1 のちょっと遅いまとめになります。いかがだったでしょうか。 Amazon Athena の登場で、ちょっとログファイルを検索したい時に、今までは Redshift にロードして検索したり、ローカルにダウンロードして grep したりしていましたが、それらから解放されますね。 また、個人的に気になっているサービスは Amazon Polly で、日本語を読ませることも可能なので、いろんな日本語の読み上げを試してみたいです。 Amazon Lex と Amazon Polly を組み合わせて自動の音声問い合わせシステムを構築することもできそうです。 今後これらを検証・利用し、積極的にノウハウを公開していきます。
アバター
こんにちは。メディアシステム開発部の曽根と申します。 今回は、medibaでのリモート共同開発の取り組みについて書かせていただきたたいと思います。 背景 今さら語るまでもないことですが、日本国内におけるIT人材は不足しています。 2015年の調査では、国内IT企業において人材が「大幅に不足している」「やや不足している」と答えた比率は 合わせて8割以上に登るという調査結果も公開されています ※1 。 数多くのサービス開発を抱えるmedibaでも、エンジニアのリソース確保に慢性的に苦労してる状態が続いているのが現状です。 ニアショア開発 そんな中、medibaで「ニアショア」というキーワードが出てきたのは2015年のことです。  ニアショア開発とは、簡単に言うと「オフショアに比べて近い地域に開発体制を持つんだぜ」ということになるかと思います ※2 。 エンジニアの人材難、費用の高騰に対する施策として、一部の保守開発を地方パートナー企業に外注する検討を始めました。  数ヶ月を掛けてパートナーの選定、業務の引継ぎを実施し2015年10月より北海道のとある企業様に業務を委託しています。 この数ヶ月の間には様々な苦労もあったのですが ※3 今回は割愛します。 リモート共同開発 それ以降、より多くのサービス保守開発業務をニアショアパートナーに移管し続けているのですが、 medibaでのニアショア開発は一般的な外注開発とは異なる業務フローを採用しているところに特徴があります。 従来の外注開発 基本的には要件を伝え、設計からテスト、リリースまでをお任せする、いわゆる「請負外注」のスタイルです。 システム担当の役割としては、進捗管理や成果物の確認、レビューが中心となります。  medibaではこの形での開発も行っています。 リモート共同開発 社内エンジニアが外部リソースと「共同で」開発を行います。 内製開発と同じスタイルを遠隔で行う試みです。 メリットとして 要員の増減がしやすい 内製に近い体制を組むことで、案件対応のスピード感を保持できる 内製で培ったノウハウの流入 → 開発方式の変更や改善がしやすい チームとして複数案件を担当し、流動的にリソースを活用できる などがあり、medibaではこれを「リモート共同開発」と名付け推進しています。  リモート共同開発の目指すところ 私は、リモート共同開発の目指すところを以下のように定義しています。 外部リソースを使いながらも内製開発に極力近づけること 品質、スピードを損なわない 人材の専門性・流動性を保つ 利用できる技術・プロセスは使う その上で、内製以上のメリットを出す 人員確保の優位性 パートナーからの技術流入 基本的には内部で開発機能を持ち、高い品質でメディアをリードしていくのがmedibaのスタイルだと考えています。  パートナーのご協力をいただきつつ、内部のノウハウを上手く融合させクオリティを上げるべく努力しています ※4 。 今回はリモート開発の概要を説明させていただきました。 増えていくリモート体制を上手く回すためのプロセス整備 パートナーを選ぶときに個人的に大事にしていること など、課題や語りたいことはまだまだありますので、いずれ機会があれば続きを書きたいと思います。 ありがとうございました。 ※1)IPA IT人材白書2016より https://www.ipa.go.jp/jinzai/jigyou/about.html こういうのを参考として出すとそれっぽくなるというテクニック。 ※2)ニアショア開発とは http://e-words.jp/w/ニアショア開発.html 「だぜ」とは勿論書いていない。 ※3)古いシステムのドキュメントが存在しない問題、 出張に行くと遊びに行ってる扱いされる問題、 お土産代が馬鹿にならない問題、など。 ※4)各方面に対するアピール。
アバター
みなさんこんにちは。 Chef より Ansible 派、インフラストラクチャー部のぬまっちこと沼沢です。 re:Invent 後ですが、Ansible ネタの投稿です。 今回はバージョンが上がる度に強化される AWS 関連モジュールと、そのサンプルをご紹介したいと思います。 そもそも Ansible とは 構築する台数が1台や2台のうちは手作業での構築で良いのですが、5台、10台…と増えていくと 初回の構築にかかる工数が膨らむ いくら手順化していても人的ミスを引き起こす可能性を秘めている 冗長化しているサーバでは、同じコマンドを何回も実行しなくてはいけない 環境が壊れた等の理由で、再構築が必要な際にまた同じことをしなくてはいけない というようなことがあるため、工数短縮と作業ミスを減らすために、手作業での構築は減らしたいのです。 特にクラウドが普及してきた昨今では、Immutable Infrastructure の登場などもあり、何度も同じ(もしくは少しだけ違う)構成を立てる機会が増えてきています。 その中で、Chef や Ansible 等の構成管理ツールはサーバ(主にミドルウェア)の構築をコード化するのに役立ちます。 Chef ではなく Ansible 派の理由 単に Ruby が書けないから Chef は対象のサーバにエージェントをインストールする必要がありますが、Ansible はエージェントが不要です。実行する環境に Ansible の準備が整えば、そこから ssh が可能なサーバに対して実行することができます。 実行内容は YAML 形式で記述するため、Ruby が書けない方は入りやすいと思います。 ※これは全て執筆者の主観的なものであり、mediba の公式見解ではございません。 Ansible 2.2 時点で使える AWS 関連モジュール Ansible は、通常 OS のミドルウェアのセットアップなどに利用されますが、AWS の各サービスの構築にも利用可能です。 本ブログ執筆時点で 87個 もの AWS 関連モジュールが用意されています。 夢が広がりますね。 Cloud Modules — Ansible Documentation AWS サービス単位で列挙すると以下の通りです。 CloudFormation CloudTrail CloudWatch Events DynamoDB EC2 ECS EFS ElastiCache Lambda IAM Kinesis RDS Redshift Route 53 S3 SNS SQS STS 上記の 18サービス に対応しています。 これだけで大抵の環境はコード化できるのではないでしょうか。 サンプル: EC2 インスタンスを立てる Ansilbe サンプルとして、EC2 インスタンスを起動する Ansible を書いてみました。 本サンプルは以下の要件で動作します。 ansible 2.2.0.0 (恐らく 2.0 以降で OK) boto 2.43.0 boto3 1.4.1 botocore 1.4.65 構成としてはこのイメージで作っています。 Ansible がやっていることは以下の3つです。 VPC 構築 Security Group 作成 EC2 Launch ディレクトリ構造は以下の通りです。 $ tree . ├── build_aws.yml └── roles    ├── ec2     │   └── tasks    │   └── main.yml    ├── securitygroup    │   └── tasks    │   └── main.yml    └── vpc       └── tasks       └── main.yml ではまず build_aws.yml の中身です。 - hosts: localhost connection: local roles: - vpc - securitygroup - ec2 hosts: localhost, connection: local とすることで、どこにも接続せずローカルで実行することを意味します。 次に roles/vpc/tasks/main.yml の中身です。 - name: VPC, Public Subnet, Private Subnet 構築 ec2_vpc: profile: "{{ profile_name }}" state: present cidr_block: "10.0.0.0/16" region: ap-northeast-1 resource_tags: { "Name": "sample-vpc" } internet_gateway: True subnets: - cidr: "10.0.0.0/24" az: ap-northeast-1a resource_tags: { "Name": "sample-subnet-a" } - cidr: "10.0.1.0/24" az: ap-northeast-1c resource_tags: { "Name": "sample-subnet-c" } register: vpc_info - name: Public Subnet に InternetGateway を設定 ec2_vpc_route_table: profile: "{{ profile_name }}" region: ap-northeast-1 state: present lookup: tag vpc_id: "{{ vpc_info.vpc_id }}" subnets: - "10.0.0.0/24" - "10.0.1.0/24" routes: - dest: 0.0.0.0/0 gateway_id: "{{ vpc_info.igw_id }}" tags: Name: "sample-public-rtb" ec2_vpc モジュールで、10.0.0.0/16 の CIDR ブロックを持つ VPC を構築しています。 ec2_vpc モジュール内で、Subnet も同時に定義し、作成しています。 最後に、作成した Subnet に Internet Gateway を設定して、グローバルに通信できるようにしています。 次に、roles/securitygroup/tasks/main.yml の中身です。 - name: Security Group 作成先の VPC ID 取得 shell: > aws ec2 describe-vpcs \ --region ap-northeast-1 \ --filters Name=tag:Name,Values="sample-vpc" \ --query 'Vpcs[0].VpcId' \ --output text \ --profile {{ profile_name }} changed_when: False register: vpc_id - name: Security Group 作成 ec2_group: profile: "{{ profile_name }}" state: present name: "sample-ec2-sg" description: "ec2 security group" vpc_id: "{{ vpc_id.stdout }}" region: ap-northeast-1 rules: - proto: tcp from_port: 22 to_port: 22 cidr_ip: "0.0.0.0/0" Security Group は VPC に紐付くため、その VPC ID を取得し、 ec2_group モジュールで Security Group を作成しています。 最後に、roles/ec2/tasks/main.yml の中身です。 - name: EC2 のキーペア発行 ec2_key: profile: "{{ profile_name }}" region: ap-northeast-1 state: present wait: yes wait_timeout: 300 name: "sample-ec2-key" - name: Subnet ID 取得 shell: > aws ec2 describe-subnets \ --region ap-northeast-1 \ --filters Name=tag:Name,Values="sample-subnet-a" \ --query 'Subnets[0].SubnetId' \ --output text \ --profile {{ profile_name }} changed_when: False register: sub_id - name: EC2 インスタンス作成 ec2: profile: "{{ profile_name }}" region: ap-northeast-1 vpc_subnet_id: "{{ sub_id.stdout }}" state: present image: ami-0c11b26d instance_type: t2.micro count: 1 key_name: sample-ec2-key volumes: - device_name: /dev/xvda volume_type: gp2 volume_size: 8 delete_on_termination: true group: sample-ec2-sg termination_protection: no instance_tags: Name: "sample-ec2" register: ec2 - name: EIP 付与 ec2_eip: profile: "{{ profile_name }}" region: ap-northeast-1 device_id: "{{ ec2.instances[0].id }}" まずは ec2_key モジュールを使用して EC2 のキーペアを作成しています。 インスタンス作成対象の Subnet ID を取得し、 ec2 モジュールを使用してインスタンスを作成しています。 最後に、 ec2_eip を使用して、作成した EC2 インスタンスに EIP を付与しています。 これらの準備が整ったら、以下コマンドで ansible-playbook を実行します。 $ ansible-playbook -i localhost build_aws.yml --extra-vars="profile_name=xxxx" -i は Inventory ファイルの指定をするオプションで、通常は ansible の実行対象を定義したファイルを指定しますが、AWS の場合は localhost を指定して、ローカルで実行するようにします。 また、–extra-vars で profile 名を指定することで、同じ構成を複数の環境に適用することができます。 これで数分待つと、付与された EIP に対して SSH が可能な EC2 インスタンスが作成されます。 まとめ いかがでしたでしょうか。サンプルだけ見ると手動で構築した方が遥かに簡単に見えますが、これがさらに巨大な規模のシステムで、何度も作り直す必要があるとなった時にはやってられませんよね。 ぜひ Ansible を活用して AWS の構築から OS のプロビジョニングまで、全てをコード化してしまいましょう。
アバター
こんにちは。制作部の平尾です。 前回は Animate CC→Javascriptに変換してアニメーション を作ってみたお話をしたのですが、今回はVue.jsを触ってみたお話をしようと思います。 事例みたいなものはネット上にたくさん落ちているので、今回はJavaScript初級くらいでも簡単にできて便利だよ。ということをお伝えしたいと思います。 (この記事は mediba Advent Calendar 2016 の5日目です。) Vue.jsとは 本家のサイト に書いてあるものをだいぶ雑に言い換えてみると、HTML(View)と何かしらのデータ(Model)と、それをコントロールするJS(ViewModel)を分けて書くための便利なフレームワークです。いわゆるMVVMでつくるためのフレームワーク。そしてそれがリアクティブなのがステキなところです。 幸せポイント① 初心者でもすぐ導入できちゃうくらい学習コストが低い。 JavaScript初級者からすると、何かのフレームワークを使うとなるとちょっと構えてしまうというか、React.jsてなに?(ポカーン)から始まるので、仕事の案件で採用するにはハードルが高いと思ってしまうのですが、Vue.jsはとっても簡単でわかりやすいので大丈夫です!基本的な機能だけなら半日で使えるようになると思います。 本家サイトのガイド もちゃんとしているし、日本語対応してくれているのもありがたい(ちょっと読みづらいけど)。 機能がシンプルなので、こう書くとこうなる。みたいなのが理解しやすいです。いろんなやり方をググったりしなくても、本家サイトのガイドを見るだけでだいたい作れちゃいます。 幸せポイント② HTMLをいっぱい書かなくていい。 たとえば、何かのリストを作るとして、同じDOMを複製して中身だけを書き換えてることってよくありますよね。そんなときは、中身をdata化しておけば幸せになれます。 ↓HTMLの方はこんな感じ <ul id="vue-contents"> <li v-for="item in items">{{ item.age }}歳の{{ item.name }}さん</li> </ul> ↓data化しておく var vueData = [ { 'name': 'ねこ', 'age': 1 }, { 'name': 'いぬ', 'age': 2 }, { 'name': 'うさぎ', 'age': 3 } ] ↓new Vueする var vm = new Vue({ el: '#vue-contents', data: { items: vueData } ~~~ 略 ~~~ }) ↓実行されるとこうなる <ul> <li>1歳のねこさん</li> <li>2歳のいぬさん</li> <li>3歳のうさぎさん</li> </ul> みたいなことです。 <li> をいっぱいつくらなくていいので幸せです。 たとえばバックエンドの何かのデータを使って <li> を量産するケースだと、ajaxで返ってきたJSONをVueのdataに入れるだけなので(自分でdata化する必要もない)、そういうときに幸せを感じます。 幸せポイント③ 表示に関する条件分岐がHTMLの方に書ける。 たとえばユーザーのステータスによって表示を切り替えるみたいなケースも多いですよね。ログインとか。条件が複雑だったり、切り替える表示要素が多いとコード量が増えて煩雑になりがちですが、Vue.jsの場合はHTML側に条件が書けるので、シンプルになります。 ↓ログイン/ログアウトボタンをこんな風に切り替えたり <button v-if="isLogin">ログアウト</button> <button v-else>ログイン</button> ( isLogin はVueのdataでtrue/falseを持っている想定) ↓0件表示を切り替えたり <ul v-if="items.length > 0"> <li v-for="item in items">{{ item.list }}</li> </ul> <div v-else> <p>リストは0件です。</p> </div> ↓アクティブのclassをつけたり消したり <button :class="['nya', (item.active) ? 'is-on':'']">HOME</button> (dataのactiveがtrueだと is-on が付け足されて class="nya is-on" になる。falseのときは class="nya" になる。) この条件のときはこれとあれとそれを表示して…みたいなことをJSで書かなくても、HTML側つまりはView側に寄せることができるので幸せになれます。 幸せポイント④ dataをごにょごにょするのもラクちん。 これはdataのつくり次第でもあるのですが、たとえば何かの値段とかの数字をJSONで取得して、そのまま表示しようとしても'1000'とかだと、カンマで区切りたくなります。 そういうときは、いったんメソッドで処理させてreturnさせるだけです。 ↓commaという自作メソッドの引数にdataの中身を渡す <ul> <li v-for="item in items">{{ comma(item.price) }}円</li> </ul> ↓たとえば値段がそのまま数字で返されるとして var jsonData = [ { 'name': '商品名1', 'price': 1000 }, { 'name': '商品名2', 'price': 2000 }, { 'name': '商品名3', 'price': 3000 } ] ↓methodsに自作メソッドを書いてリターンさせる var vm = new Vue({ el: '#vue-contents', data: { items: jsonData }, methods: { comma: function(price){ return price.toString().replace(/(\d)(?=(\d{3})+$)/g , '$1,'); } } ~~~ 略 ~~~ }) ↓実行されるとこうなる <ul><li>1,000円</li> <li>2,000円</li> <li>3,000円</li> </ul> ラクちんです。 オマケ(追記) 社内のVue.jsセンパイにアドバイスいただきました!こういう場合はmethodsよりもcomputedを使ったほうが良いそうです。methods=関数、computed=算出プロパティ。 本家サイトの 算出プロパティ vs メソッド にちゃんと書いてありました。 methodsに登録していると、 item.price が変更されていようがいまいが、何かしらdataがアップデートされると毎回実行されてしまうけど、computedの方にすると、 item.price が変更されたときだけ実行されるみたいです。初回に実行した結果をキャッシュしているので、変更されていないときは即時に初回にキャッシュした結果を返してくれるみたい。Vue.jsさん優秀! ただ、今回の例で扱っているdataが配列なので、直接item.priceの変更を検知させるためには コンポーネント というのを使うことになります。それでこそVue.jsの恩恵が最大限活かされるくらいの機能なのですが、わりと長くなってきたのでまた次の機会に書こうと思います。 そして、社内にVue.jsセンパイがいて、さらに幸せになりました。 幸せポイント⑤ clickとかのイベントをbindするのもHTMLに書ける。 ユーザーがクリックしたら何かが起こるみたいなことは日常茶飯事で書いていると思うのですが、DOMを取得してイベントをbindして…の「DOMを取得して」を省略できます。条件分岐をViewに寄せたのと同じく、イベントもViewに寄せることができちゃいます。 ↓DOMに直接v-onでイベントをbind <button v-on:click="somethingToDo()">ボタン</button> ↓methodsにボタンがクリックされたときの処理を書く ~~~ 略 ~~~ methods: { somethingToDo: function(){ // ボタンがクリックされたときの何かの処理 } } ~~~ 略 ~~~ JSに書くのはメソッドだけなのでスッキリして幸せです。 幸せポイント⑥ dataをアップデートするだけでDOMがアップデートされる。 個人的にはコレがいちばんの幸せポイントです。たとえば、何かのトリガーで表示要素を追加したくなったとすると、elementを作ってappendして…みたいなことになると思うのですが、Vue.jsならdataに追加するだけで勝手にelementを生成して追加してくれます。dataの中身をアップデートすれば、elementの中身もアップデートされます。リアクティブ万歳! ↓HTMLはこんな感じで <p>全部で{{ items.length }}匹</p> <ul> <li v-for="item in items">{{ item.age }}歳の{{ item.name }}さん</li> </ul> ↓dataは最初のサンプルと同じくこんな感じだとします var vueData = [ { 'name': 'ねこ', 'age': 1 }, { 'name': 'いぬ', 'age': 2 }, { 'name': 'うさぎ', 'age': 3 } ] ↓実行されるとこうなる <p>全部で3匹</p> <ul><li>1歳のねこさん</li> <li>2歳のいぬさん</li> <li>3歳のうさぎさん</li> </ul> このあと、何かのタイミングでうさぎさんをはりねずみさんにして、ふくろうさんも追加したくなるイベントが発火したら、配列をアップデートします。 vueData[2].name = 'はりねずみ'; vueData.push({ 'name': 'ふくろう', 'age': 10 }) ↓配列がアップデートされたタイミングで勝手にこうなります <p>全部で4匹</p> <ul><li>1歳のねこさん</li> <li>2歳のいぬさん</li> <li>3歳のはりねずみさん</li> <li>10歳のふくろうさん</li> </ul> 配列の中身を更新しただけなのに!ラクちんすぎる! まとめ シンプルで自由度が高くて扱いやすいのが何より幸せ。 本当はポイントを5個にしたかったのですが、勢いで6個になってしまいました。 設計がとても下手くそで煩雑になりがちな私にとっては、MVVMを学ぶのにもいい機会になりました。 今回はVue.jsの超初歩的なところしか書いていませんが、ミックスインとかコンポーネントの機能を使ったりするとさらに幸せになれそうな予感なので、もう少し踏み込んだ使い方をしてみたくなりました。 AngularJSやReact.jsあたりはちょっとハードルが高そうですが、Vue.jsは本当にシンプルで扱いやすくて簡単なので、是非みなさんもチャレンジしてみてください。 ここまでお付き合いいただいてありがとうございました。 オマケというか告知 Vue.jsを使ってつくった わたしねこ。 というサービスをリリースしました!(まだベータ版ですが…) ひたすらねこさんたちが癒してくれますので、ぜひ会員登録して「いいにゃ」してくださいニャー(●ↀωↀ●)✧ よろしくお願いします!
アバター
 こんにちは、メディアシステム開発部の菅原です。  PHP 7.1 が 2016 年 12 月 1 日(日本時間では 2016 年 12 月 2 日)にめでたく リリース されました。ちょうど良い機会なので、 PHP 7.1 RFC を参考に、新たに追加された機能を見ていきたいと思います。 新機能8選  今回の記事では、PHP 7.1 の RFC の中から構文に関する新機能のうち 8 つの RFC をピックアップして見ていきます。 nullable 型 void 戻り値宣言 クラス定数のアクセスレベル宣言 複数の例外の補足 iterable 型 list のキーによる変数宣言 list 短縮構文 文字列への負数オフセットによるアクセス  後述のサンプルコードは、下記のPHPの環境で実行しています。なお、Fatal Error が発生しているコードについては、適切な部分をコメントアウトして実行しています。 $ php -v PHP 7.1.0RC6 (cli) (built: Nov 9 2016 04:45:59) ( NTS ) Copyright (c) 1997-2016 The PHP Group Zend Engine v3.1.0-dev, Copyright (c) 1998-2016 Zend Technologies 1. nullable 型  PHP 7.0 で、スカラー型もタイプヒントが使えるようになり、タイプヒンティングが型宣言に進化しました。これによって、PHP 本体で全ての型をチェックできるようになりましたが、null を許容する場合は型宣言をしないで記述する必要があり、null safety 1 とは言えませんでした。しかし、PHP 7.1 で nullable 型が導入されることにより、PHP はメソッドに関しては null safety であると言える 2 ようになりました。 <?php // nullable な引数を持つメソッド function sayNullable(?string $name): ?string { return empty($name) ? null : 'My name is ' . $name; } // non-null な引数を持つメソッド function sayNonNull(string $name): string { return 'My name is ' . $name; } var_dump(sayNullable('安部 菜々')); // string(24) "My name is 安部 菜々" var_dump(sayNullable(null)); // NULL // パラメータなしは NULL を渡している訳ではないので例外が発生 var_dump(sayNullable()); // PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function sayNullable() ... var_dump(sayNonNull('緒方 智絵里')); // string(27) "My name is 緒方 智絵里" // null safe なので例外が発生 var_dump(sayNonNull(null)); // Fatal error: Uncaught TypeError: Argument 1 passed to sayNonNull() must be of the type string, null given, ... 2. void 戻り値宣言  PHP 7.0 で戻り値の型を明示的に宣言できるようになりましたが、なぜか void 型には対応していませんでした。それほど大きな問題ではないですが、IDE によっては Doc コメントを自動入力させた時に正しい戻り値にしてくれないものがあるため、直すのが地味に面倒ではありました。  なお、void 型メソッドは、戻り値として null を返してはいけないが、実行時の戻り値を受け取った場合は null という香ばしい仕様になっています。 <?php // return しない function hoge(): void { } // return する function foo(): void { return; } // null で return する function bar(): void { return null; } // void 型の戻り値が定義されているメソッドの戻り値を無理やり受け取ると null になる var_dump(hoge()); // NULL var_dump(foo()); // NULL // void 型は null を戻り値とするわけではないので例外発生 var_dump(bar()); // PHP Fatal error: A void function must not return a value (did you mean "return;" instead of "return null;"?) ... 3. クラス定数のアクセスレベル宣言  クラス定数のアクセスレベル宣言ができるようになります。デフォルトは従来通り public です。const が使える子になってきたので、define がより一層使えない子になってきました。 <?php class Hoge { const CURRENT_CONST = 'public_current'; private const PRV_CONST = 'private'; protected const PRO_CONST = 'protected'; public const PUB_CONST = 'public_new'; public static function getPrivateConst(): string { return self::PRV_CONST; } } class SubHoge extends Hoge { public static function getProtectedConst(): string { return self::PRO_CONST; } } var_dump(Hoge::CURRENT_CONST); // string(14) "public_current" var_dump(Hoge::PRV_CONST); // Fatal error: Uncaught Error: Cannot access protected const Hoge:: PRV_CONST ... var_dump(Hoge::PRO_CONST); // Fatal error: Uncaught Error: Cannot access protected const Hoge::PRO_CONST ... var_dump(Hoge::PUB_CONST); // string(10) "public_new" var_dump(SubHoge::getProtectedConst()); // string(9) "protected" var_dump(SubHoge::getPrivateConst()); // string(7) "private" 4. 複数の例外の補足  Java 7 で導入された例外のマルチキャッチが PHP 7.1 にも導入されます。Java とは異なり、大らかな PHP 3 なので、特に問題なく気軽に使えるはずです。むしろ、同じコードを二度書かないという DRY 原則を守りやすくなるので、メンテナビリティは向上すると思います。 <?php function throwException(?string $type) { try { if (is_null($type)) { throw new BadMethodCallException(); } elseif (empty($type)) { throw new InvalidArgumentException(); } } catch (BadMethodCallException | InvalidArgumentException $e) { echo 'throw exception is ' . get_class($e) . ".\n"; } } throwException(null); // throw exception is BadMethodCallException. throwException(''); // throw exception is InvalidArgumentException. 5. iterable 型  PHP 7.0 までは、C# でいう IEnumerable のような型が存在せず、Traversable 型を実装したクラスと配列は別物でしたので、foreach 可能なオブジェクトに対する型宣言をすることができませんでした。そのため、それらのみを引数として受け取る場合は、引数の型チェックを余分に行う必要がありました。ですが、PHP 7.1 で iterable 型が実装されることにより、型宣言でその制約を付けることが可能になります。  ただし、PHP 7.1 の時点では、count 関数に関するバグ 4 があるため、iterable 型と併用する時は注意が必要です。なぜならば、iterable 型と判定された変数が、Countable とは限らないからです。この問題については、 PHP 7.2 で修正される予定です。 <?php function twice(iterable $object): string { $new_array = []; foreach ($object as $val) { $new_array[] = $val * 2; } return implode(', ', $new_array); } var_dump(twice(new ArrayObject([ 1, 2, 3 ]))); // string(7) "2, 4, 6" var_dump(twice([ 1, 3, 5 ])); // string(8) "2, 6, 10" 6. list のキーによる変数宣言  list 言語構造の代入式の右辺に連想配列を渡した場合に、宣言した変数の値に対応するキーの値を割り当てすることができるようになりました。これにより、連想配列の任意のオフセットにあるキーの名前を指定すれば、その値を変数の値にできるようになったため、list の引数に「,」を異様に並べて書く必要がなくなりました。データベースからレコードを取得してきた時に便利になるのではないかと思います。 <?php $data = [ [ 'id' => 1, 'name' => '安部 菜々' ], [ 'name' => '緒方 智絵里', 'id' => 2 ], [ 3, '輿水 幸子' ] ]; // キーの順序が入れ替わっていても割り当てが可能。取得するキーも選択可能。 list('name' => $name_0) = $data[1]; var_dump($name_0); // string(16) "緒方 智絵里" // キーが定義されていない(連想配列と扱われない)場合 list('id' => $id_2, 'name' => $name_2) = $data[2]; // PHP Notice: Undefined index: id ... // PHP Notice: Undefined index: name ... // 2列目は連想配列ではないので Noticeが出る foreach ($data as list('id' => $id, 'name' => $name)) { echo "Name is {$name} (ID={$id})\n"; } // 0列目: Name is 安部 菜々 (ID=1) // 1列目: Name is 緒方 智絵里 (ID=2) // 2列目: PHP Notice: Undefined index: id ... // PHP Notice: Undefined index: name ... 7. list 短縮構文  list を糖衣構文 [] で記述することが可能になりました。list と同様に PHP 7.1 で追加された新機能であるキーによる変数宣言もできます。 <?php $data = [ [ 'id' => 1, 'name' => '安部 菜々' ], [ 'name' => '緒方 智絵里', 'id' => 2 ], [ 3, '輿水 幸子' ] ]; // キーの順序が入れ替わっていても割り当てが可能。取得するキーも選択可能。 [ 'name' => $name_0 ] = $data[1]; var_dump($name_0); // string(16) "緒方 智絵里" // キーが定義されていない(連想配列と扱われない)場合 [ 'id' => $id_2, 'name' => $name_2 ] = $data[2]; // PHP Notice: Undefined index: id ... // PHP Notice: Undefined index: name ... // 2列目は連想配列ではないので Noticeが出る foreach ($data as [ 'id' => $id, 'name' => $name ]) { echo "Name is {$name} (ID={$id})\n"; } // 0列目: Name is 安部 菜々 (ID=1) // 1列目: Name is 緒方 智絵里 (ID=2) // 2列目: PHP Notice: Undefined index: id ... // PHP Notice: Undefined index: name ... 8. 文字列への負数オフセットによるアクセス  文字列およびオフセット指定可能な全ての配列関連のビルトイン関数は、負数のオフセットをサポートするようになりました。 <?php $string = 'mediba'; var_dump($string[-2]); // string(1) "b" var_dump('mediba'{-3}); // string(1) "d" var_dump(strpos($string, 'i', -3)); // int(3) まとめ  近年の PHP は、柔軟性と厳格性の程よくバランスが取れた進化をしていると言えると思います。今回の PHP 7.1 のリリースでも、新しく追加される機能はすべて使い勝手に優れた良いものが並んでいます。特に、型関連の機能は PHP 5 時代から考えると、大きな進化を遂げていて、PHP 7.0 でタイプヒンティングが型宣言に進化し、PHP 7.1 ではさらに nullable 機能が追加されました。これによって、完全ではありませんが null safety を手に入れ 5 、また、型安全性も高くなり、より優秀な言語になってきました。  今後の進化で期待するところといえば、null safety や型安全性に関する言語機能です。妄想になりますが、下記のように 6 、安全呼び出しや変数の型宣言、総称型が実装されると、もっと良くなるのではないかと思います。 // 1. 安全呼び出し(safe call) $obj?->foo($x); // 2. 変数の型宣言(null safety) private $val_int: int; public $val_str: ?string; // 3. 総称型(※半角の大小記号が消えるので全角にしています) class Gene<T> { } null safety は、 Kotlin というプログラム言語由来の用語です。これは、null が原因となる実行時エラーを起こさない性質のことを表しています。なお、Kotlin は、言語機能として null でない変数の場合にだけ、そのメンバにアクセスする safe call(安全呼び出し)という機能を持っています。  ↩︎ non-null と nullable の区別はありますが、安全呼び出しに該当する機能がないことから、完全な null safety ではありません。  ↩︎ Java とは異なり、検査例外とか非検査例外とかそういう例外種別を気にしなくても良い(SPLにはそれっぽいのはありますが)のは良いところです。  ↩︎ count 関数に Countable インターフェイスを継承していないオブジェクトなどを引数として渡した場合に 1 を返却する不具合です。  ↩︎ 他に、null safety ではない言語は、 C言語、C++、C#、Go、Java、Objective-C、Javascript、Ruby、Python などがあります。  ↩︎ どこかで見たことがあると思ったあなた、正解です。Hack/HHVM で実装されているんです。PHP にもフィードバックしてほしいですね。  ↩︎
アバター
こんにちは。広告システム開発部の杉本です。 10/13(金) - 10/14(土)にかけて開発合宿を実施しました。今回はその報告を。 今後実施する際の記録と、思い出。あとは、他社の方が実施する際の参考になれればと思いブログとして残させてもらいました。 合宿に参加したメンバーは、自分含めて41名。 特段の事情で参加できなかったメンバーを除く、medibaシステム本部の正社員ほぼ全員が参加する合宿となりました。 メンバー構成としてはこんな感じ 制作部 11名(デザイン・コーディング・JSなどを主としたフロントエンジニア) メディアシステム開発部 12名(auのメディアと自社メディア作ってる) 広告システム開発部 6名(広告系のシステムと課金系のシステム作ってる) インフラストラクチャー部 4名(AWS・オンプレの構築/運用とか) 情報システム部 5名(品質管理・情シスとか) その他 3名(偉い人・すごい人) これだけのメンバーなので、いわゆる開発合宿で思い浮かぶ、 みんなで何か作ろうぜ!! と言ったことを趣旨とした合宿ではなく、 今後のmedibaを成長させていくために開発部門としてなにをしていくけばいいか、より質・量ともに高いアウトプットを出すためにはどう考え進めていけばいいのか といった、マインドセットの共有を軸にした研修的な合宿としています。 medibaの場合、こういった研修は外部のコンサル会社を利用することも多いのですが、今回は自分たちの手作りでの実施です。 まずは合宿施設の紹介 セミナーハウス クロス・ウェーブ府中 ( http://x-wave.orix.co.jp/fuchu/ ) 今回は40人を越す大所帯、しかも開発の社員全員ということもあって、よくある温泉付きの民宿で雑魚寝でみたいな場所ではなく、都内の大きな会議室が用意された研修施設をチョイスしています。 施設決定に当たって重視したポイントとしては 無線LANが完備されている 貸会議室 + プロジェクタ + ホワイトボード 宴会場 個室(シングルルーム) いざとなったら会社に帰れる距離 全体の流れ 二日間に及ぶ合宿全体の流れとしてはこんな感じです。 二日目については、休日ということもあり午前中のみとしました。 一日目 集合 10時着席 まずは挨拶 全社・部門などの方針発表 / インプット お昼 アンケート結果発表 グループ会社ABCでの開発の進め方 グループディスカッション 宴会 二日目 朝食 技術的なインプット ディスカッション ファシリテーター・社長からの所感 解散 12時過ぎ 一日目 一日目としては上記流れを通じて、メンバーみんなからこれからどうしていくかの Working Agreement を導き出すところがメインとなっています。 4月に買収を行ったAppBroadCastの開発部長としての ベンチャー企業のエンジニアとしてやるべきこと についての発表や、事前に行ったアンケート結果の発表などから多くの刺激がありました。 それらのインプットを通じ、グループディスカッション、そしてWorking Agreement。 詳細はコンプライアンス上一応触れずに…。問題ない気はしますが。 みんな真剣な面持ちですね。 二日目のもう1つのメインイベントは大宴会。 ビュッフェ形式での立食パーティです。 みんな楽しそうですね。 二日目 二日目は、一日目と比べると、少し肩の力を抜いたかたちで。 最近の技術動向についてのインプットを行いそこから今後必要な技術や抑えておくべきコトをみんなで話し合いました。 データ分析や機械学習、IoTなどの分野についての注目度が高かったように伺えます。 まとめ 企画構想から含めると3ヶ月に及ぶプランで、上期評価や、下期目標の時期と重なったこともあり、運営サイド(管理部門)としてはかなり疲弊した部分もあったのですが、全体としては収穫の多い合宿になったのではないかと感じています。 グループディスカッションのファシリテーターとしてグループ全体を見ていましたが、最初の方では広がらなかった議論も回を重ねる毎により深まった意見が出るようになっていったのは感じました。 主管したCTOだけではなく、メンバー全員が前向きに進めてくれた結果かと思っています。 また、合宿中正社員が出払った中で業務を支えてくれていた契約さん、派遣さん、委託さんのおかげによる部分も多く感謝しています。ありがとうございました!
アバター
広告システム開発部の佐藤禎章です。 今回は、Firebaseサービスの一つである Cloud Messaging を使用して、ブラウザへ Push 通知を送る仕組みを揃えてみました。 試しやすく単純な仕組みに出来るので、 node を使用して全部 JavaScript で書いてあります。 node : v4.6.0 npm : 2.15.9 今回作成するものの全体は、下記 Github リポジトリに投入してあります。 これをそのまま clone して、API Key などの設定を投入すれば、そのまま使えます。 https://github.com/medi-y-sato/fcm-test-node 前提知識 : Service Worker とは 今回の Push 送受信には Service Worker というものを使用します。 これは何かとざっくり言うと、 Webページとは別にバックグランドで動くスクリプト です。 オフライン時でも動作する Web アプリを作るため仕組みとして整備されてきたもので、リクエストのキャッシュや同期などの API が含まれています。 その中に Push 通知を受ける( Service Worker が Push 通知の通信を契機に Active になり、処理を行う)機能があるので、今回はそれを使用します。 詳しく知りたい場合は、下記などを参照下さい。 Service Worker の紹介 By Matt Gaunt Firebase での下準備 Push 通知の仕組みとして Firebase を使用するので、 https://firebase.google.com/ からプロジェクトを一つ作成して下さい。 出来上がったプロジェクトの [プロジェクトの設定]から、[クラウドメッセージング]タブを選択し、サーバキーと送信者IDを控えておいて下さい。 また、[ウェブアプリに Firebase を追加]から取得できる Javascript のコードも、同様に控えておいて下さい。 クライアントサイドの設定 クライアントサイドの設定を入れていきます。 index.html 通知を受けるブラウザで開く HTML / JavaScript になります。 index.html の全体はこちらになります。 https://github.com/medi-y-sato/fcm-test-node/blob/master/index.html 設定の投入が必要です、下記。 <script src="https://www.gstatic.com/firebasejs/3.4.1/firebase.js"></script> <script> var config = { apiKey: "<<CHANGE HERE>>", authDomain: "<<CHANGE HERE>>", databaseURL: "<<CHANGE HERE>>", storageBucket: "<<CHANGE HERE>>", messagingSenderId: "<<CHANGE HERE>>" }; firebase.initializeApp(config); </script> [ウェブアプリに Firebase を追加]の所で控えたコードを、そのまま貼り付けます。 これで、このページ内で Firebase のサービスが使用できるようになります。 <link rel="manifest" href="manifest.json"> Web アプリの設定を記述する manifest ファイルを指定します。 中身については後述。 if ('serviceWorker' in navigator) { console.log('Service Worker is supported'); navigator.serviceWorker.register('./serviceworker.js').then(function(reg) { console.log('Service Worker is ready :^)', reg); reg.pushManager.subscribe({userVisibleOnly: true}).then(function(sub) { console.log('subscribed:' , sub) console.log('endpoint:', sub.endpoint); var iframe = document.createElement("iframe"); iframe.src = './send?endpoint=' + sub.endpoint; iframe.width = 1 iframe.height = 1 document.body.appendChild(iframe); }); }).catch(function(error) { console.log('Service Worker error :^(', error); }); } ブラウザに Service Worker が存在した場合に、 serviceworker.js を登録、成功したら Push 通知の購読を行い、それにも成功したら iframe を作成してサーバ側に Endpoint を送信します。 ….別にこんな広告っぽい情報送信の仕方しなくても良いんですけど、今回はお手軽さ重視で自動送信するようにしてみました。実際に使用する場合はボタン契機で XMLHttpRequest するようにしてみてもいいですし、ユーザ登録などのフローに混ぜ込む形にしてもいいと思います。 serviceworker.js こちらが Service Worker として登録されるスクリプト本体になります。 serviceworker.js の全体はこちらになります。 https://github.com/medi-y-sato/fcm-test-node/blob/master/serviceworker.js install / activate / push / notificationclick のイベントリスナを仕込んでありますが、実際にコードを書いてあるのは push と notificationclick の2つになります。 self.addEventListener('push', function(event) { console.log('Push message received', event); event.waitUntil( self.registration.showNotification('Push Received', { body: 'メッセージを受信しました', icon: './corp_logo.png', tag: 'push-notification-tag' }) ); }); こちらが Push 通知を受け取った際の処理。 今回は通知ウインドウをそのまま表示してみることにしました。 self.registration ( ServiceWorkerRegistration ) の showNotification メソッド を呼ぶと、ブラウザが通知をよしなに表示してくれます。 もちろん何の処理を書いてもいいので、 push を契機に特定の API を叩きに行って Service Worker 内のキャッシュを更新するとか、 showNotification で表示したい中身を改めて API アクセスして取得するとか、色んな事が出来ます。 self.addEventListener("notificationclick", function(event) { console.log('notification clicked:' + event) event.notification.close(); }, false); こちらは通知ウインドウをクリックされた時の処理。単に通知ウインドウをクローズしています。 clients.openWindow() とかを使って、通知ウインドウがクリックされたら特定のURLを開く、なんてことも出来るでしょう。 manifest.json manifest.json は web アプリの設定を記述する所です。 manifest.json の全体はこちらになります。 https://github.com/medi-y-sato/fcm-test-node/blob/master/manifest.json 設定が必要な場所があります、下記。 "gcm_sender_id": "<<CHANGE HERE>>", 先ほど控えた 送信者ID を、ここに記載して下さい。 gcm (google cloud messaging) とか書いてありますが、 fcm (firebase cloud messaging) でもこのフィールド名を使用します。 これで、この Service Worker が Firebase Cloud Messaging の待ち受けを行えるようになります。 サーバサイドの設定 サーバサイドは二段構えです。 index.html などのファイルをホストする server.js と、実際に push 通知を送信する pushMessage.js コマンドラインツール。 server.js Service Worker の稼働条件として https もしくは localhost が必要 、というものがあったので、 localhost でサーバ立てるために用意しました。 実は先ほど用意した index.html などをローカルのブラウザなどで開いても、Service Worker は稼働してくれないのです。 とはいえいちいち Firebase Web Hosting などの https 環境にデプロイするのも面倒だったので、手軽にローカルで稼働できるサーバを作っちゃいました。 server.js の全体はこちらになります。 https://github.com/medi-y-sato/fcm-test-node/blob/master/server.js こちらを作るにあたっての大部分について、 node.jsでシンプルなwebサーバー を参考にさせていただきました、ありがとうございます。 下記のようにして起動します。 $ node server.js デフォルトでポート 8888 で待ち受けるので、 http://localhost:8888 をブラウザで開いて下さい。 こちらの要になるのは下記。 if (uri === '/send'){ if(req.method=='GET') { var url_parts = url.parse(req.url,true); var match = /https:\/\/android.googleapis.com\/gcm\/send\/(.*)/ endpoint = match.exec(url_parts.query.endpoint)[1] console.log('receive endpoint : ' + endpoint) console.log('use this command line to send pus message') console.log('$ node pushMessage.js -e ' + endpoint) res.writeHead(200, {"Content-Type": "text/plain"}); res.write("accepted\n"); res.end(); } } /send がリクエストされた時に発火する処理で、 index.html の iframe から呼び出されたリクエストを受け取り、 endpoint を切り出してコンソールに出力しています。それだけ。 use this command line to send push message $ node pushMessage.js -e <<endpoint>> この行をコピペすれば Push 送信コマンドが送れます。ものぐさですね。 実のところ、 index.html の時点で endpoint を Web ページなり console.log() なりに出力し、それをコピペして送信コマンドをこさえる、という方法でも全く問題ないのですが、Chrome Developer Toolを開くのすら面倒だなあ、と思っちゃったのでこんな仕組みにしてあります。 Push 通知を送るコード pushMessage.js こちらが、 Firebase Cloud Messaging に対して Push 通知を送るよう指示を出すスクリプトになります。 pushMessage.js の全体はこちらです。 https://github.com/medi-y-sato/fcm-test-node/blob/master/pushMessage.js 設定が一つ必要です。 var authKey = '<<CHANGE HERE>>' こちらに、クラウドメッセージングのところで控えた サーバキー を入れて下さい。 送信の要になるのは下記です。 var options = { url: apiUrl, method: 'POST', headers: { 'Content-Type':'application/json', 'Authorization':'key=' + authKey }, json: true, form: { "to" : endpoint } } request を使用して FCM の API を叩くことになるのですが、そのための諸々のパラメータになります。大事なのは下記3つ。 method は POST リクエストヘッダの Authorization にサーバキーを指定 データペイロードは body ではなく form を使用 一応、使い方は下記のようになります。 $ node pushMessage.js -e <<endpoint>> 先ほど server.js が出力してくれた内容をそのままコマンドラインで実行すれば、ブラウザで待ち受けている Service Worker に Push が飛び、 Service Worker に登録されているイベントリスナが発火して、通知ウインドウが表示されます。 まとめ 元々 Cordova / ionic2 で Push 通知を行うために調べ始めた Firebase Cloud Messaging ですが、 意外に簡単に Web Push まで実装できてしまったので驚きました。 この送信部分はそのまま Android / iOS への Push 通知にも使えてしまうので、 Firebase Cloud Messaging を採用すると、バックエンドが考えることがすごく少なく済みそうです。 また、 Service Worker についてもなんか面倒そうなイメージが有りましたが、こと Push 受信に関しては、やっていることはわりかし単純です。 もっと言うと、この Web Push という仕組みは Service Worker に登録されたイベントリスナを発火させる 機能、なので、通知以外の使いかたをいろいろ考えてみると、新しいサービスのネタにもなるかもしれませんね。 今回の記事では全く触れていませんが、 Firebase にはプロバイダを複数から選べる ID 認証の基盤もありますし、リアルタイム Sync が売りのデータベースもあります。 Firebase は SDK が強力なため、殆どコードを書かずにこれらの機能を使用でき、お手軽感が素晴らしいです。 チャットアプリをデプロイするチュートリアル 辺りから、パワフルさを体感してみて下さい。 そんなわけで Firebase Database に検索機能追加希望
アバター
こんにちは、広告システム開発部Emacs派の吉田です。 先月(2016年9月14日)に出たEmacs 25.1で、 Emacsのバッファでwebブラウザが動かせる機能がリリースされました!! しかしXWidgetsはgtk3環境のみ動作なので、CocoaなMacでは動かないんです。 でもX11でどうにか動かす事ができたのでその方法を記載します! 参考: Emacs 25.1 released 参考: Configure.ac#L2590 | Enable xwidgets if GTK3 and WebKitGTK+ are available. 参考: GNU Emacs For Mac OS X | XWidget support ? #52 mac portsをインストール www.macports.org からインストーラー拾ってインストール webkit-gtk3をインストール $ sudo port install webkit-gtk3 Emacs 25.1のソースコードを展開 $ cd /usr/local/src/ $ wget https://ftp.gnu.org/gnu/emacs/emacs-25.1.tar.xz $ tar xzf emacs-25.1.tar.xz (tarはJオプション必要かもしれない) ビルド&インストール $ cd /usr/local/src/emacs-25.1 $ sh autogen.sh $ ./configure --with-xwidgets --with-x-toolkit=gtk3 --without-ns --with-gif=no $ make $ sudo make install (configureのオプションはお好みでどうぞ) 起動 $ /usr/local/bin/emacs XWidgetsを試す M-x xwidget-webkit-browse-url スクリーンショット 感想 おとなしくTerminal版( emacs -nw )使っておきます。
アバター
はじめに はじめまして、メディアシステム開発部の森と申します。 主にauスマートパスの一部サービスの開発周りを担当しています。 皆様はAWS OpsWorksは利用されていますか? AWS OpsWorks(以後OpsWorksと表記)とは、EC2やELBといったAWSリソースをスタックという単位にまとめて管理や運用ができるソリューションです。 Webサービスの保守、運用に必要な情報(インスタンスの台数や稼働状況 )が集約されている上、Chefのレシピさえ作ってしまえばアプリのデプロイからミドルウェアアップデートまで、管理コンソールから一通り熟せる優れものです。 詳細は 公式 をご覧ください。 Time-basedインスタンスって? OpsWorksには稼働スケジュールを “曜日” と “時間” で指定できる Time-basedインスタンス というものを作る事ができます。 これは、アクセスの多い曜日や時間帯が事前に予測できるサービスであれば、 ピークに備えてインスタンス数を増やしたり、逆に深夜帯は必要最低限のインスタンスのみを動かして他は停止しておく、といった稼働スケジュールを予め設定することが可能となる便利な機能です。 個人的な所感ですが、オートスケールではないので、決められた運用コスト内でやりくりするサービスに向いていると思います。(各インスタンスの負荷状況はOpsWorks上で、まとめてモニタリングできますし) 設定変更も簡単で、コンソール上からポチポチとクリックするだけです。 コンソール上の時間の表記はUTCなので注意。 Time-basedのちょっと不便な所 大変便利な機能なのですが、欠点もあります。 設定は1時間単位 コンソール上で設定できるのは 1時間単位 なので、インスタンス数が多かったり24時間全部に対して設定変更が必要だったりすると、延々とクリックをし続ける必要があります。 10台以上の設定をしないといけない場合は挫けそうになります。 設定は曜日ベース 前述の通り設定できるスケジュールは “曜日ベース” ですので、毎月X日に稼働したいor停止させたいといった設定はできません。せいぜいが対象日に当たる曜日設定を数日前に変更して対応といったことが出来る程度です。(それでも充分かもしれませんが) 欠点を無理やり解消してみる 前置きが長くなりましたが、前述の欠点を解消するべく少し工夫してみます。 設定の一括変更 まずはTime-based設定を曜日単位で一気にやる方法から。 といっても、これは特別な工夫も何も必要なく、AWS CLIにTime-basedの設定変更コマンドが存在しますので、それを使えば設定の一括変更は簡単にできます。 公式ドキュメント を元に、一つコマンドを作ってみます。 aws opsworks --region us-east-1 set-time-based-auto-scaling --instance-id インスタンスID(OpsWorks ID) --auto-scaling-schedule '{"Monday": {"0": "on","1": "on"}、,"Friday": {"0": "on","1": "on"}}' 基本的なパラメータは他のCLIと大差なく、このコマンド特有のパラメータは稼働設定の内容を渡すための"–auto-scaling-schedule"くらいでしょうか。 稼働設定は上記例の通りjson形式で設定します。こちらも時間はUTCでの記述となるので、日本の場合は+9時間であることに注意しましょう。 この例の場合、 月曜日と金曜日の9時台および10時台だけ対象のインスタンスを稼働させる というスケジュール設定になります。(正確には9:00からインスタンスを立ち上げ始める) ちなみに {"Monday": {}、,"Friday": {}} という設定だと、 月曜日と金曜日は稼働しない という設定になります。 擬似的な日付ベースの稼働スケジュール設定 CLIのコマンドを利用し、日付ベースで稼働スケジュール設定をするシェルを大雑把にですが作ってみました。 今回作ったシェルの概要は、 前提として毎日深夜に本シェルを実行する。 対象インスタンスに対し、1日単位で稼働スケジュールを下記条件で設定していく 毎月2日と22日( スマパスの日ですね )は9:00~翌9:00稼働させる。 それ以外の日は稼働停止。 シェル実行日当日とその次の日は設定対象から外す。 といった内容です。 3に関しては設定ファイルのミス等により、いきなり当日の稼働スケジュールが変わってしまった等という事態を避ける為の保険です。 まずは"–auto-scaling-schedule"に入れる稼働スケジュール設定のテンプレートを作ります。 今回の稼働スケジュールパターンは2つのみです。 フル稼働設定のテンプレート(time_based_full.json) {"%Target%":{ "0": "on", "1": "on", "2": "on", "3": "on", "4": "on", "5": "on", "6": "on", "7": "on", "8": "on", "9": "on", "10": "on", "11": "on", "12": "on", "13": "on", "14": "on", "15": "on", "16": "on", "17": "on", "18": "on", "19": "on", "20": "on", "21": "on", "22": "on", "23": "on" }} 稼働停止設定のテンプレート(time_based_off.json) {"%Target%":{}} %Target%は、設定対象日の曜日に置換する文字列です。 次に、どの日にどの稼働スケジュールを設定するかのカレンダーみたいなものを作ります。 稼働設定カレンダー(time_based_calendar.json) {"Schedule": { "full": [ "2", "22" ] } } 最後に設定対象とするインスタンスのリストを作ります。今回はAz-aしか使っていませんが、Az-cも対象とする場合は"AzC"の配列を追加すればOKです。 設定対象のインスタンスリスト(instance.json) {"Instance":{ "AzA": [ "インスタンスID(OpsWorks ID)その1", "インスタンスID(OpsWorks ID)その2" ] } } 以上4つの設定jsonファイルを使って、日付ベースによる稼働スケジュール設定の本処理作ります。 実行sh(opsworks_timebased_ctl.sh) 1 #!/bin/bash 2 target_date_list=(`cat ./time_based_calendar.json |jq -r '.Schedule.full[]|@text'`) 3 4 for i in {2..6}; do 5 # スケジュールを操作する対象日を取得します。 6 # [0]・・・日、[1]・・・曜日 7 target_day=(`LANG=en_US.UTF-8 date -v+${i}d "+%d %A"`) 8 9 if `echo ${target_date_list[@]} | grep -q "${target_day[0]}"` ; then 10 schedule_type="full" 11 else 12 schedule_type="off" 13 fi 14 15 target_schedule=`cat ./time_based_${schedule_type}.json |sed -e "s/%Target%/${target_day[1]}/"` 16 17 for instance in `cat ./instance.json |jq -r '.Instances.AzA[]|@text'` 18 do aws opsworks --region us-east-1 set-time-based-auto-scaling --instance-id ${instance} --auto-scaling-schedule "${target_schedule}" 19 done 20 done ・2行目 稼働設定カレンダーから、フル稼働(full設定)させる日を取得し、 target_date_list 配列に入れます。 ・4行目以降のfor分 実行日の明後日〜6日後までの日付を処理します。 6日後までなのは、元々が曜日ベースでの設定だからですね。 ・7行目 設定対象である実行日のi日後の日付(日のみ)と、その曜日を取得して target_day 配列に入れます。 ・9行目~13行目のif文 設定対象日の稼働スケジュールパターンを設定します。 今回の場合、フル稼働設定の対象日配列 target_date_list に設定対象日が存在した場合は、time_based_full.jsonの内容で稼働スケジュールを設定します。 それ以外は停止させるので、time_based_off.jsonの内容を設定します。 ・15行目 設定テンプレートの%Target%を7行目で取得した設定対象日の曜日文字列に差し替えて、 target_schedule に入れます。 ・17行目〜19行目 CLIでTime-basedの設定を、インスタンスの台数分for実行しています。 実行環境等により多少の手直しは必要かもしれませんが、動かすだけであれば上記のファイル一式を同じディレクトリ内に配置すればOKです。 毎日深夜0時など日次でシェルを動かせば、毎月2日と22日に関しては、常にその6日前には対象インスタンスのフル稼働設定が完了しています。 完璧にはまだ遠い 結構大雑把なんで、真に日付ベースの設定に対応させるには、ぱっと思い浮かぶだけでも 月末だけ動かしたいというパターンには対応できていない。 時間が9:00~翌日9:00というUTCベースでの設定となっている。 0:00~翌日0:00といった24時間設定をきちんとやるには、設定対象日と合わせて、その前日の稼働スケジュールも一緒に変更する必要あり。 正直このレベルなら、cronで対象日の早朝にでもインスタンスを立ち上げるCLIを発行するシェルを実行したほうが楽じゃないのか? といった点を解決する必要があります・・・。 まとめ 拙いコードで大変恐縮ではありますが、OpsWorks上で日付ベースのインスタンス稼働スケジュールを設定したい時に、参考として少しでもお役に立てれば幸いです。 まぁ、OpsWorksに日付ベースで稼働設定ができる機能を実装してもらうのがBestなのですが・・・。 コードをガリガリと書くということが最近は減ってきていたのですが、こういった限られた材料から自分の欲しい機能をどう実現するかという試行錯誤は、久々にやってみてもやっぱり楽しかったです。
アバター
こんにちは、インフラストラクチャー部の沼沢です。 複数の AWS アカウントを運用していると、それぞれのアカウントの S3 バケットに CloudTrail のログが溜まっていきますが、そのログ、1箇所に集約して監視や可視化をしたくはありませんか? そこで今回は、複数の AWS アカウント上にそれぞれ保存されている CloudTrail ログを集約・可視化する仕組みについてです。 構成図 ① CloudTrail が S3 にログを Put したのをトリガーに、集約先に用意している Lambda を起動 ② Lambda から S3 へログを取りに行く(Roleで権限委譲して取得) ③ Lambda でログファイルを展開し、内容を無加工で CloudWatch Logs に投入 ④ CloudWatch Logs からストリーミングで Elasticsearch Service にログを流す では、これを構築する手順を解説していきます。 構築手順 000000000000 は A環境(集約元)の AWS アカウント ID で読み替えましょう 999999999999 は B環境(集約先)の AWS アカウント ID で読み替えましょう 今回は東京リージョン(ap-northeast-1)に構築することを前提としています ② に関しては某ブログの会社の「 S3保管したCloudTrailログに別アカウントのLambdaからアクセスする 」を大いに参考にしています B環境に Elasticsearch Service のドメイン作成 AccessPolicy ↓ { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": "es:*", "Resource": "arn:aws:es:ap-northeast-1:999999999999:domain/<設定したドメイン名>/*", "Condition": { "IpAddress": { "aws:SourceIp": "xxx.xxx.xxx.xxx/32" } } } ] } Condition -> IpAddress で、Kibana へのアクセスを xxx.xxx.xxx.xxx のみに制限 AccessPolicy 以外は全て任意に設定 B環境に CloudWatch Logs のロググループ作成 ロググループ名: 任意のロググループ名 ログストリーム名: 任意のログストリーム名 作成したロググループにチェックを入れ、以下の通り設定 アクションから「Amazon Elasticsearch Service へのストリーミングの開始」を選択 Amazon ES クラスター: ↑で作成した Elasticsearch Service のドメインを指定 ログの形式: AWS CloudTrail A環境で権限委譲用の IAM ロール/カスタムポリシーを作成 以下の条件でカスタムポリシーを作成 ポリシー名: 任意 ポリシーJSON ↓ { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:Get*", "s3:List*" ], "Resource": "arn:aws:s3:::<CloudTrailのログ出力先のバケット名>/*" } ] } 以下の条件でロールを作成 ロール名: 任意 ロールタイプ: クロスアカウントアクセスのロール -> 所有している AWS アカウント間のアクセスを提供します アカウントID: 999999999999 ポリシー ↑で作成したカスタムポリシー B環境の Lambda 用の IAM ロール/カスタムポリシーを作成 以下の条件でカスタムポリシーを作成 ポリシー名: 任意 ポリシーJSON ↓ { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "sts:AssumeRole" ], "Resource": [ "*" ] } ] } 以下の条件でロールを作成 ロール名: 任意 ロールタイプ: AWS サービスロール -> AWS Lambda ポリシー ↑で作成したカスタムポリシー AmazonS3FullAccess  ※バケットを絞るなどは適宜行ってください AWSLambdaExecute B環境に Lambda ファンクション作成 ファンクション名: 任意 Runtime: Python2.7 Handler: lambda_function.handler Role: B環境に作成した Lambda 用の IAM ロール Timeout: 10 sec ソースコードは こちら 作成されたファンクションの ARN のメモを取っておきましょう A環境の S3 バケットから B環境の Lambda を実行するための権限を Lambda に付与する 2016年09月現在、ManagementConsole からではからこの設定ができないため、CLI で権限を付与 $ aws lambda add-permission \ --region ap-northeast-1 \ --function-name <対象のLambdaFunction名> \ --statement-id <任意で一意となるID> \ --principal s3.amazonaws.com \ --action lambda:InvokeFunction \ --source-arn arn:aws:s3:::<CloudTrailのログ出力先のバケット名> \ --source-account 000000000000 A環境の CloudTrail 用 S3 バケットにイベント登録 対象のバケットを選択し、プロパティを表示 イベント -> 「通知の追加」 -> 以下を入力/選択して「保存」 名前: 任意 イベント: Put 送信先: Lambda 関数 Lambda 関数の ARN を追加 Lambda 関数の ARN: 作成したB環境の Lambda ファンクションの ARN CloudTrail のログが複数のバケットにある場合は必要な分だけ上記を登録 まとめ 上記の設定が完了すると、S3 に CloudTrail のログが出力される度に Lambda が起動し、CloudWatch Logs を経由して Elasticsearch にログが放り込まれることになります。 あとは、Elasticsearch の Kibana の URL にアクセスし、好きに閲覧してみてください。 なお、途中に CloudWatch Logs を経由していますので、ここでログ監視を入れることも可能です。 また、今回は簡単に可視化を実現するために Elasticsearch Service を利用しましたが、Elasticsearch Service では、構築時にディスクサイズを決める必要があるため、集約したい環境が増えていくと、ディスクが足りなくなる可能性があります。 可視化部分を自分で実装する手間をかけられるのであれば、Lambda から DynamoDB にログを放り込んで、可視化部分を自力で実装するようにすれば、ディスクサイズを気にしなくて済みますね。 この構成をもとに、いろいろアレンジしてみてください。
アバター
こんにちは、広告システム開発部 山浦です。 2回目の投稿となります今回は、LINE BOT APIでBOTを作成してみたいと思います。 LINE BOT APIとは サービスとLINEユーザとのコミュニケーションを可能にするAPI開発が行えるものです。 まだトライアルの状態であるためデフォルトでは友だち上限数が50人までに制限されていたり、使用可能な機能がメッセージの送受信程度に限られています。 2016年に突如ChatBotが盛り上がったのは記憶に新しいところですが、 このLINE BOT APIの発表もその要因の一つだと言われています。 何を作るか 多くの人は毎日なにげなく天気をチェックしていると思います。 いつもの場所ならアプリにお気入り登録しているかもしれませんが、お出かけ先など知らない地名から天気を検索するのはなかなか面倒だったりします。 そんなちょっとしたストレスを解消するために、LINE API BOTと位置情報を使って簡単な天気検索を作ってみることにしました。 作成の前に LINE BOT APIを利用するためには、Developerアカウントが必要なので取得します。 https://business.line.me/ja/products/4/introduction 登録しただけでは利用できず、若干高いハードルを超える必要があります。 Callback URLにはHTTPSを必ず指定すること APIサーバーのOutbound IPアドレスが固定されていること これらをクリアするためにHTTPS通信が行えるHerokuと、Outbound IPを固定できるアドオンFixieを使用しますので、 Herokuにアプリを作成しFixieを適用しておきます。 作成 それでは早速作成していきます。 開発環境 Go 1.6 Heroku + Fixieアドオン OpenWeatherMap API LINE BOT APIにホワイトリストIPアドレスを登録 FixieのAccount Detailsに表示されているIPを、LINE BOT API側に登録します。 LINE BOT APIはこのIPアドレスからのみメッセージを受信しますので、忘れずに設定します。 LINE BOT APIにアクセスする 当記事のメインパートです。 LINEがGo言語用のSDKを提供しているので、そちらを利用するのがよいと思います。 https://github.com/line/line-bot-sdk-go また天気の取得にはOpenWeatherMap APIを利用します。 OpenWeatherMapとは、様々な気象データを無料APIとして提供している大変便利なサービスです。 http://openweathermap.org/ 早速ですが、作成したサンプルコードと作成にあたっての要点をまとめました。 package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "os" "strconv" "github.com/line/line-bot-sdk-go/linebot" ) // Wdata represents Json core fields. type Wdata struct { Weather []Weather `json:"weather"` Info Info `json:"main"` } // Weather represents weather item. type Weather struct { Main string `json:"main"` Icon string `json:"icon"` } // Info represents main item. type Info struct { Temp float32 `json:"temp"` // 気温(ケルビン) Humidity float32 `json:"humidity"` // 湿度 } func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } // Proxyの設定 proxyURL, _ := url.Parse("") client := &http.Client{ Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, } bot, err := linebot.NewClient(Channel ID, "Channel Secret", "MID" linebot.WithHTTPClient(client)) if err != nil { fmt.Println(err) return } //コールバック http.HandleFunc("/callback", func(w http.ResponseWriter, req *http.Request) { // メッセージ受信 received, err := bot.ParseRequest(req) if err != nil { fmt.Println("ParseRequest error:", err) return } for _, result := range received.Results { content := result.Content() if content != nil && content.IsMessage && content.ContentType == 7 { // 緯度経度から天気問い合わせのURLを作成 location, _ := content.LocationContent() lat := strconv.FormatFloat(location.Latitude, 'f', 6, 64) lon := strconv.FormatFloat(location.Longitude, 'f', 6, 64) u := "http://api.openweathermap.org/data/2.5/weather?lat=" + lat + "&lon=" + lon + "&APPID=APP ID" // 天気情報を取得 resp, _ := http.Get(u) defer resp.Body.Close() byteArray, _ := ioutil.ReadAll(resp.Body) jsonBytes := ([]byte)(string(byteArray[:])) wdata := new(Wdata) if err := json.Unmarshal(jsonBytes, wdata); err != nil { fmt.Println("JSON Unmarshal error:", err) return } //メッセージ送信 _, senderr := bot.NewMultipleMessage(). AddText("現在の天気をお知らせします。"). AddText("天気 : "+wdata.Weather[0].Main). AddImage("http://openweathermap.org/img/w/"+wdata.Weather[0].Icon+".png", "http://openweathermap.org/img/w/"+wdata.Weather[0].Icon+".png"). AddText("気温 : " + fmt.Sprintf("%.2f", (wdata.Info.Temp-273.15))). AddText("湿度 : " + fmt.Sprintf("%.2f", wdata.Info.Humidity)). Send([]string{content.From}) if senderr != nil { fmt.Println("message error:", senderr) } } else { bot.SendText([]string{content.From}, "位置情報を送信してください。") } } }) if err := http.ListenAndServe(":"+port, nil); err != nil { fmt.Println("port error:", senderr) } } 要点をまとめていきます。 プロキシの設定 FIXIEのProxy URLを設定することでOutbound IPが有効になりますので忘れずに設定します。 proxyURL, _ := url.Parse("") client := &http.Client{ Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, } 位置情報の取得 どの地域の天気情報が欲しいかをBOTに伝えるため、位置情報(緯度経度)を取得します。 BOTが受信するメッセージはcontentに格納されており、下記のようなパラメータを持っています。 プロパティ名 詳細 id メッセージID contentType メッセージの種類 from 送信者のMID createdTime メッセージ送信時間 to メッセージの受信者 toType メッセージを受信するユーザのタイプ contentMetadata メッセージのメタデータ text 送信されたメッセージ location 位置情報 位置情報メッセージ以外を受信した場合は何も処理したくないので、【位置情報を送信してください。】というメッセージを送信したいと思いました。 この時注目するのが【contentType】で、これにはどのような種類のメッセージをユーザが送ったかが格納されます。 contentTypeの値 詳細 1 テキストメッセージ 2 画像メッセージ 3 動画メッセージ 4 音声メッセージ 7 位置情報メッセージ 8 スタンプメッセージ 10 ユーザ情報メッセージ 処理したいのは contentType : 7の位置情報メッセージのため、それ以外は処理しないように分岐を設ける場合は content.ContentType == 7 とすればよいことが分かります。 位置情報メッセージが送信された場合、locationプロパティに情報が格納されます。(それ以外はnil) プロパティ名 詳細 title 位置情報名、通常は住所 latitude 緯度 longitude 経度 ここでようやく 位置情報(緯度・経度) の取得ができました。 天気情報の取得 天気情報の取得にはOpenWeatherMap APIを利用します。 本題とはズレますが、OpenWeatherMap APIの利用方法を簡単に紹介します。 天気を検索する時のキーには、地域名・地域コード・緯度経度のいずれかが利用可能ですが、緯度経度を取得できているので悩むことなくそれを利用します。 http://api.openweathermap.org/data/2.5/weather?lat= <"latitude">&lon=<"longitude">&APPID=<"App ID"> latには緯度、lonには経度を指定し、APIアクセスを行うと天気情報JSONが返ってきます。 APPIDはOpenWeatherMapのwebページから取得可能なので、別途取得しておきます。もちろん無料です。 メッセージの送信 いよいよメッセージ送信部分です。 ひとえにメッセージ送信といっても色々ありますが、今回は2つの送信方法を実装しています。 どちらも送信先を指定するためにはMIDというユーザ特定IDが必要なのですが、ユーザからのメッセージ受信時に一緒に受け取っている(上記表のfromに該当)ので、それをそのまま使うのが一番簡単です。 位置情報メッセージ以外の場合、Sending messages API bot.SendText([]string{content.From}, "位置情報を送信してください。") 天気情報を送信する場合、Sending multiple messages API bot.NewMultipleMessage(). AddText("現在の天気をお知らせします。"). AddText("天気 : "+wdata.Weather[0].Main). AddImage("http://openweathermap.org/img/w/"+wdata.Weather[0].Icon+".png", "http://openweathermap.org/img/w/"+wdata.Weather[0].Icon+".png"). AddText("気温 : " + fmt.Sprintf("%.2f", (wdata.Info.Temp-273.15))). AddText("湿度 : " + fmt.Sprintf("%.2f", wdata.Info.Humidity)). Send([]string{content.From}) Sending messages APIでは1送信1メッセージですが、Sending multiple messages APIでは1送信nメッセージとなっているのが特徴です。 この他にもインタラクティブなメッセージが送れる Sending rich messages API というものもあり、商用利用する際はメッセージの作りこみが重要になってくるように思います。 Herokuへデプロイ 最後にHerokuへデプロイします。 Herokuの動作に必要なProcfileを作成します。 $ cd $GOPATH/src/line-bot-api $ echo 'web: line-bot-api' > Procfile 続いてライブラリの依存関係をgodepを利用して保存します。 $ go get -u github.com/tools/godep $ cd $GOPATH/src/line-bot-api $ godep save ./... $ godep go install ./... 最後にHerokuへソースをPUSHします。 $ cd $GOPATH/src/line-bot-api $ git init $ heroku git:remote -a $ git add . $ git commit -m "hoge" $ git push heroku master 動作確認 ちゃんとBOTは天気を教えてくれるのでしょうか。渋谷区の天気を聞いてみます。 ちゃんと教えてくれました!地図から場所さえ伝えてあげれば自動的に天気を返してくるBOTができたので、日々のストレスから少し開放されそうです。 (OpenWeatherMapが用意している天気アイコンがボヤケてしまっているのがやや残念です。) 位置情報を送ってから結果が返ってくるまで、おおよそ1~2秒程度なのでストレスを感じることもありませんでした。 まとめ LINE BOT APIはSDK利用することで簡単に利用できることが分かりました。 特に位置情報の利用が非常に簡単なのは驚きでした。 Callback URLはHTTPSしか利用できないことや、固定IPを設定しなければならないなどの若干のハードルはあるものの、アイディア次第で有効活用できるのではないでしょうか。 正式リリースでさらに機能が拡充されるようなので、他にもBOTを作っていければと思います。
アバター