株式会社medibaのブログ - TECH PLAY

TECH PLAY

株式会社mediba

株式会社mediba の技術ブログ

全213件

こんにちは。広告システム開発部の小林です。 現在、データ解析の勉強を行っているので、今回はRFM分析のことを記事にしてみました。 なお、本記事はデータを分類しグラフの描画までを行う内容となっています。 分類後の解析などは本記事には含まれていません。 ︎RFM分析とは RFM分析は、Recency (最新購入日)、Frequency (購入頻度)、Monetary (購入金額)の3つの指標でユーザーを分類し、ユーザーの購買情報を解析する手法です。 RFM分析では、以下のような分析ができます。 Recency が現在に近いユーザーほど将来の収益に貢献する可能性が高い Recency が同じなら Frequency が多いほど常連客になっている Recency が同じなら Frequency や Monetary が高いほど購買力があるユーザー サーバー︎環境 CentOS 6.7 Python 3.5.2 Pythonのパッケージ matplotlib 2.0.0 グラフの描画に使用します numpy 1.11.3 数値の計算に使用します pandas 0.19.2 CSVを読み込み、データの集計に使用します Pythonと各種パッケージのインストールについては割愛させていただきます。 ︎分類方法 Python でのデータの分類方法をご紹介します。 集計対象のデータですが、以下のようなCSVを使用します。 データの内容は、2017/01/01 ~ 2017/01/31 の購入情報となります。 集計日を 2017/02/01 とするため前月となる 2017年1月 のデータを用意しています。 ※サンプルデータなので、グラフの描画に適したデータとなるように加工しています。 order_id,uid,amount,time 1,a,100,1483232400 2,b,200,1483318800 3,c,300,1483405200 4,d,400,1483491600 5,e,500,1483578000 6,f,600,1483664400 7,g,700,1483750800 8,h,800,1483837200 9,i,900,1483923600 10,j,1000,1484010000 . . . 210,ae,50,1485738000 211,ae,50,1485738000 212,ae,50,1485738000 データの内容 order_id : 購入ID(ユニーク) uid : ユーザーID(重複あり) amount : 1回の購入金額 time : 購入日(Unixtime) コードは以下のようになっています。 # 必要なパッケージをimport from mpl_toolkits.mplot3d import Axes3D import numpy as np import pandas as pd import matplotlib.pyplot as plt import datetime as dt # CSVを読み込む conversion_data = pd.read_csv('blog_sample.csv') print(conversion_data.head()) # order_id uid amount time #0 1 a 100 1483232400 #1 2 b 200 1483318800 #2 3 c 300 1483405200 #3 4 d 400 1483491600 #4 5 e 500 1483578000 # 集計日のtimestampを取得 (今回は2017/02/01に指定します) NOW = int( dt.datetime(2017,2,1).strftime('%s') ) # uidを元にグループ化して、集計を行う rfm = conversion_data.groupby('uid').agg({'amount': [np.sum], # 購入金額 'time': lambda x: int( (NOW - x.max()) / 86400 ), # 最新購入日 'order_id': [len] # 購入頻度 }) print(rfm.head()) # amount time order_id # sum <lambda> len #uid #a 100 30 1 #b 400 27 3 #c 800 24 4 #d 450 22 2 #e 1200 20 2 # グラフ描画用の設定 fig = plt.figure() ax = Axes3D(fig) ax.scatter3D(rfm['time'], rfm['order_id'], rfm['amount']) # ️グラフをファイルに保存 filename = "/tmp/3d_output.png" plt.savefig(filename) グループ化している以下の部分は、sum したり 項目の数をカウントするのはすんなりできたのですが、最新購入日の取得に苦労しました。 timestamp のままだと桁数が多いためかグラフ描画時にうまく描画されないので、集計日(2017/02/01)から購入日の timestamp を引いて直近何日前に購入されたかを算出しています。 rfm = conversion_data.groupby('uid').agg({'amount': [np.sum], 'time': lambda x: int( (NOW - x.max()) / 86400 ), 'order_id': [len] }) コードの実行後に[filename]で指定したパスにグラフが画像ファイルとして描画されます。 上記のコードの場合は、[tmp/3d_output.png]に出力されます。 描画されたグラフは以下のようになります。 項目が書かれていないので、ちょっと見づらく感じます。 補足を追記してみました。 所感 Python を書くのが初めてだったので、とても苦労しました。 データ解析の道のりは長いと感じました。 次回はデータの解析に焦点を当てた記事を書こうと思います。 最後まで読んで頂きありがとうございました。
こんにちは。制作部デザイナーの斉藤です。 バナーを作る際、同じ内容で色々なサイズに展開する事が多いと思いますがファイルをいくつも開いて、ちまちまいじるのってものすごく面倒な作業ですよね。 そこでPhotoshopデータセットなどの便利な機能を使ってテンプレートを作り、一発でサクっと書き出しできないか試してみました。 今回やりたかったこと バラバラなサイズのファイルを1ファイルで管理したい 商品画像を一気に差し替えたい 画像を一回でドーンと書き出したい 商品名・価格を一括で変換したい 今回初めて使うデータセットをはじめ、他にはアートボード、いつもお世話になっているスマートオブジェクト、画像アセットの生成を組み合わせてテンプレートを作成しました。 実際にやってみた 1.バラバラなサイズのファイルを1ファイルで管理したい →アートボードで解決! 1つのpsdファイル上に複数アートボードをつくります。 従来はバナーの点数分(約10点前後)ファイルをつくって、それぞれ個別に作業をしていたのですが、同じファイル上で複数のアートボードを作業できる様に作り変えました。 2.商品画像を一気に差し替えたい →スマートオブジェクトで解決! 通常、写真を縮小した後元のサイズに戻すと荒れてしまうのですが、スマートオブジェクト化してから作業するといつでも再編集できます。 また、オブジェクトとして複製できるので、同じデータを使い回したい時とても便利な機能です。 参照: スマートオブジェクト入門~直しに強いPhotoshopデータを作ろう~ 第1回:劣化知らずの「スマートオブジェクト」 3.画像を一回でドーンと書き出したい →画像アセットで解決! 画像の書き出しが自動化できます。 書き出したいアートボードやレイヤーにファイル名と拡張子(.jpg、.png、 .gif)をつけてPhotoshopメニューの ファイル > 生成 > 画像アセット にチェックを入れます。 保存すると自動的にフォルダが生成され、その後も保存毎に上書きされます。 また、拡張子以外にも出力画質や画像の大きさ(相対値または px、in、cm、mm などのサポートされている形式)なども指定できる様です。 参照: レイヤーからの画像アセットの生成 【スライス不要】Photoshop「画像アセット生成」の基本とつまずきがちな5つのコト 4.商品名・価格を一括で変換したい →データセットで解決! これ自体は以前のバージョンからある機能ですが、初めて使ってみて今まで使わなかったのは損してたなーと後悔しました。ざっくり説明すると、変更したいレイヤーに変数名をつけて、csvで差し替えできる機能になります。 同じフォーマットの文字、背景画像を差し替えて大量の画像を書き出す時にとても便利です。 (1)レイヤーに名前をつける 変更したいレイヤーに任意の変数名をつけます(これは必ず半角英数で)。 ここでは商品名のところを設定していきます。 (2)csvファイルをつくる 前項目でつけた名前をcsvファイルに設定して、さらに差し替えたいテキストも入力します。 ここでレイヤーの変数名と項目数がズレるとエラーになってしまうので、注意しましょう。 (3)データセットから読み込む Photoshopメニューの イメージ > 変数 > データセット > 読み込み から先ほど作ったcsvを読み込みます。 (4)該当箇所に反映されました! 参照: Photoshop データセットで大量の差分イメージを作る方法 やってみてどうだったか 良かった事 始めのテンプレート作りにちょっと時間がかかりましたが、毎回の作業がいくつかの工程で終わるので、かなりの時間短縮になったと思います。 これで急な修正があっても慌てずに対応できて、とても楽チンです! いまいちだった事 テキストが文字数固定でカーニング不要の場合には使えそうですが、毎回不特定な商品名・価格で調整が必須な場合など、複雑な文字組には不向きでした。 背景・文字のテーマ色に関しては、一括変換できるプラグインがありますが、有料のため使用しませんでした。もしかしたらAdobe CCのライブラリでそういう機能あるかもしれませんね。 おわりに Photoshopの自動化の存在は知っていたのですが、ちゃんとやってみたのは今回が初めてだったので知らない事がけっこうありました。 しかし参考にさせていただいたサイトにも書いてあったように、そこまで大量に点数がない場合は逆に準備に手間がかかるので、悩ましいところかも知れません。 Adobe CCについても便利な機能がまだまだいっぱい埋もれていそうなので、引き続き探っていきたいと思います。
こんにちは、インフラストラクチャー部の沼沢です。 今回は、CloudFront + S3 での IP アドレスベースのアクセス制限を実現する方法をご紹介します。 実現したかったこと 特定の外部拠点から参照されるファイルを S3 に配置したい 独自ドメインが使いたかったため、CloudFront を前段に用意 ファイルへのアクセスを特定の外部拠点の IP アドレスのみに制限したい S3 の URL への直アクセスはさせたくない これを実現しようとしてググってみると、 CloudFront の IP アドレスでのアクセス制限 S3 の IP アドレスでのアクセス制限 CloudFront のみ、S3 のみの方法は出てくるのですが、CloudFront + S3 の方法が出てこなかったので、自分で試してみました。 やったこと 先に設定した内容を箇条書きにしてみました。 AWS WAF で IP アドレスベースのアクセス制限ルールを適用 S3 直アクセスを防ぎ CloudFront 経由に限定するため、オリジンアクセスアイデンティティを利用 目新しいことはしていません。今まで既にあったことの合わせ技です。 上記の設定をすることで、やりたかったことを実現することができます。 それぞれ以下を参考にさせていただきました。 AWS WAF で CloudFront 環境にIPアドレス制限を設定する | dogmap.jp オリジンアクセスアイデンティティを使用して Amazon S3 コンテンツへのアクセスを制限する - Amazon CloudFront 設定詳細 では、設定内容の詳細を解説していきたいと思います。 S3 にファイルを用意 s3://{BucketName}/limited_access/index.html を配置。 index.html の中身は以下です。 <html> <head> <title>選ばれしHTML</title> </head> <body> ようこそ、選ばれし者よ </body> </html> AWS WAF の設定 次に AWS WAF で IP アドレスによるアクセス制限のルールを作成します。 AWS WAF のコンソールを開き、「Create web ACL」をクリックし、以下のように設定を進めて行きます。 Create conditions に戻ったら「Next」クリック 「Review and create」クリック後、内容に問題なければ「Confirm and create」をクリックして web ACL の作成は完了です。 CloudFront の設定 CloudFront のコンソールを開き、「Create Distribution」をクリックし、以下のようにして Web Distribution の作成を進めて行きます。 Restrict Bucket Access バケットへのアクセス制限をかけるか → Yes Origin Access Identity オリジンアクセスアイデンティティを新規に作成するか既存のものから選ぶか → 新規に作成(Create a New Identity) Grant Read Permissions on Bucket バケットに対する読み取り権限を自動で付与するか → バケットポリシーを更新 (Yes, Update Bucket Policy) ※ No にした場合は、自分でバケットポリシーの設定が必要 これら以外の設定項目は要件に合わせて任意に設定してください。 20〜30分程待つと CloudFront の Distribution の作成が完了します。 動作確認 以下の2パターンのアクセスを、許可 IP アドレス・それ以外の IP アドレスで検証します。 S3 の URL でアクセスする CloudFront の URL でアクセスする 許可 IP アドレスからアクセス S3 の URL でアクセスする CloudFront の URL でアクセスする それ以外の IP アドレスからアクセス S3 の URL でアクセスする CloudFront の URL でアクセスする それぞれ期待通りの動作が確認できました。 まとめ この仕組みを利用することで CloudFront + S3 の状況下でも厳密なアクセス制限が可能になります。 EC2 にファイルを置いて Security Group でアクセスを制限する方法でももちろん同じことが実現できますが、EC2 の保守運用という手間がかかりますよね。 静的なファイルであれば、CloudFront + S3 でも同じことができ、EC2 の保守運用をする必要が無くなるため、運用コストの削減が期待できます。 こうしたことの積み重ねが、AWS を使うメリットを享受するために重要なことだと思います。 こうした工夫で運用を楽にしていくことができるという気付きになれば幸いです。
こんにちは。広告システム開発部の八代です。 今回新しくWaffle.ioというツールを用いてタスク管理をしてみたので、ツールを使ってみた感想を共有したいと思います。 Waffle.ioとは GitHubのIssueやPullrequestを看板として可視化することが出来るツールです。 IssueやPullrequestの状況を看板化することにより、各進捗が1画面で把握することが可能になります。 https://waffle.io/GitHubのユーザ名/GitHubのリポジトリ名 という形でアクセスすると、簡易的にWaffleを使うことが出来ます。 例) Ruby on Railsの公式GitHubをWaffle.ioで見てみる https://waffle.io/rails/rails GitHub標準のKanban機能(Projects)もありますが、このWaffle.ioは標準のKanban機能だけだと実現出来ない痒いところまで手が届く機能が盛り込まれています。 特にラベルの可視化や、複数のIssueを1画面で俯瞰的に見ることができ、1つのIssueに対してのコメントをブラウザ遷移すること無く閲覧・書き込みすることが出来る利便性はとても高いです。 導入方法 GitHubアカウントを持っている状態で https://waffle.io/ にアクセスしてログインをし、アプリケーションをAuthorizeするだけです。 Authorizeには以下の権限を求められます。 後は + Add Project のボタンを押して、どのリポジトリと連携させるかを選ぶだけで導入は完了です。 縦の列を増やしたい場合はProject SettingsからBoard Settingsで移動することが出来ます。 特徴 幾つか特徴はありますが、ここでは何点かを取り上げます。 GitHubのIssueを追加することで看板を増やすことが出来ますが、Waffle上でもIssueを作ることもでき、アサイン、マイルストーン、ラベルも視覚的に見やすく表示されます。 Issueにコメントやテキストを編集してもWaffle側はほぼリアルタイムでレスポンスが返ってきます。 また、Issue単位で重み付けすることができるので、どのくらいのタスクなのかをひと目で把握することが出来ます。(上部には合計値が表示されます) フィルター機能も充実しており、必要なものだけ表示できるようになっています。 また、PullrequestとIssueを関連付けすることもでき、2つの看板を連結(関連付け)させることも出来ます。 使用感 今回私のチームで約2ヶ月使用してみた感想とそこで感じたメリットデメリットをあげます。 導入前 導入する前は、以下の問題点を抱えていました。 プロジェクト全体でどのくらいのタスクが残っているのかわかりにくかった 自分以外のタスクが見えにくかった タスク管理ツールが分散していた (エクセルシートや、taiga.io、redmine、backlogなど) 実際に出来上がっているコードと今行っているタスクが疎結合だった 導入後 導入後にメリットだと感じた点は以下の通り。 1画面にタスクが可視化されて表示されているので、全体が把握しやすかった アサインされている看板の位置がリアルタイムに変わるため、誰が何をやっているのか明確化された GitHubとWaffle.ioだけでタスク管理、ソース管理、WIKI(仕様管理)が行えるようになった PullrequestがどのIssueに対してのものなのかがひと目で分かり、成果物がどのIssueに基づいたものなのかわかるようになった 逆にデメリットだと感じた点 スケジュールが絡むタスクに関しては看板だけでは見づらかった GitHubからIssueを作る際には .github/ISSUE_TEMPLATE.md が機能しているが、WaffleからIssueを作る際は機能していなかった Issueが大量に増えるとその分読み込みに時間がかかってしまう まとめ 今回、導入したチームでは新規プロジェクトかつ、簡易ウォーターフォールモデルで開発していたため、スケジュールを意識したタスク管理方法が好ましかったが、 Waffleのような看板型のタスク管理方法だとスケジュールが絡むタスクに関しては少し扱いが難しかったため、上手く運用に乗せきれず終わってしまいましたが、スクラム開発をする上ではこのWaffle.ioはメリットが多いツールだと感じました。 導入は、現時点(2017年1月時点)で無料で簡単にできるものなので、機会があれば是非試してみてはいかがでしょうか。
全国の Ansible 派のみなさん、こんにちは。 ブログの投稿頻度急上昇中、インフラストラクチャー部の沼沢です。 今回は Ansible で対話型のスクリプトを自動化する際に利用する expect モジュールについてです。 先日、expect モジュールを利用していて罠にハマったので、その内容と対処法をご紹介したいと思います。 Linux には対話形式の入力を自動化する expect というコマンドがあります。 Linuxの対話がめんどくさい?そんな時こそ自動化だ!-expect編- - Qiita Ansible にも、このコマンドと同じことを実現するモジュールがあり、それが expect モジュールです。覚えやすいですね。 expect - Executes a command and responds to prompts — Ansible Documentation では、この expect モジュールを使って対話スクリプトの自動化をしてみます。 前提 本投稿は以下を対象としています。 Ansible 2.1 以上のバージョン 対向: Amazon Linux AMI release 2016.09 ec2:DescribeInstances が許可された IAM ロールが付いた EC2 インスタンス 基本的な使い方 まずは expect モジュールの基本的な使い方を、例を使ってご紹介します。 例えば、以下はユーザ名とパスワードを求められるようなスクリプトを Ansible で実行しようとした場合の例です。 - tasks: - name: ユーザ名とパスワードが必要なスクリプトを実行 expect: command: sh example.sh chdir: /tmp responses: "^Enter User Name: .*": "user1" "^Enter Password: .*": "password" 上記の task では以下のことを実行しています。 /tmp に移動してから sh example.sh を実行 "^Enter User Name: .*" に一致する文字列で入力待ち状態になったら “user1” と入力して Enter "^Enter Password: .*" に一致する文字列で入力待ち状態になったら “password” と入力して Enter 見ての通り、responses では 質問で入力待ちになる際の文字列を指定する正規表現 と、 その際に入力する文字列 を指定します。 ある日、罠にハマる この使い方で、collectd の CloudWatch プラグインのセットアップスクリプト(※)を自動化しようとした際に罠にハマりました。 ※参考: 新しい collectd の CloudWatch プラグイン | Amazon Web Services ブログ setup.py では、以下の4つの入力が求められます。 Choose AWS region for published metrics: 1. Automatic [ap-northeast-1] 2. Custom Enter choice [1]: Choose hostname for published metrics: 1. EC2 instance id [i-xxxxxxxxxxxxxxxxx] 2. Custom Enter choice [1]: Choose authentication method: 1. IAM Role [hogehoge-ec2-role] 2. IAM User Enter choice [1]: Choose how to install CloudWatch plugin in collectd: 1. Do not modify existing collectd configuration 2. Add plugin to the existing configuration Enter choice [2]: Enter choice [x]: という質問でそれぞれ入力待ちになり、未入力で Enter を押した場合は [x] の数字が選択される事になりますが、今回は以下のように入力したいと考えていました。 Choose AWS region for published metrics: 1. Automatic [ap-northeast-1] 2. Custom Enter choice [1]: 1 Choose hostname for published metrics: 1. EC2 instance id [i-xxxxxxxxxxxxxxxxx] 2. Custom Enter choice [1]: 2 Enter hostname [ip-xxx-xxx-xxx-xxx]: web01 ← EC2 の Name タグの値 Choose authentication method: 1. IAM Role [hogehoge-ec2-role] 2. IAM User Enter choice [1]: 1 Choose how to install CloudWatch plugin in collectd: 1. Do not modify existing collectd configuration 2. Add plugin to the existing configuration Enter choice [2]: 2 これを自動化しようとして Ansible を以下のように書きました。 ## ※※注※※ この Ansible は expect モジュールの部分が期待通りに動きません ## - tasks: - name: collectd インストール yum: name=collectd state=present - name: pexpect インストール (expectモジュールの利用に必要) pip: name=pexpect state=present - name: EC2 インスタンス ID 取得 shell: curl http://169.254.169.254/latest/meta-data/instance-id changed_when: False register: instance_id - name: EC2 インスタンスの Name タグの値を取得 shell: > aws ec2 describe-instances \ --region ap-northeast-1 \ --query 'Reservations[].Instances[?InstanceId==`{{ instance_id.stdout }}`].Tags[][?Key==`Name`].Value' \ --output text changed_when: False register: instance_name - name: collectd の CloudWatch プラグインのセットアップスクリプト取得 get_url: url=https://raw.githubusercontent.com/awslabs/collectd-cloudwatch/master/src/setup.py dest=/var/tmp/setup.py mode=0755 - name: collectd の CloudWatch プラグインセットアップ expect: command: ./setup.py chdir: /var/tmp responses: "Enter choice \\[\\\u001b\\[92m1\\\u001b\\[0m\\]: ": "1" "Enter choice \\[\\\u001b\\[92m1\\\u001b\\[0m\\]: ": "2" "Enter hostname \\[\\\u001b\\[92m.*\\\u001b\\[0m\\]: ": "{{ instance_name.stdout }}" "Enter choice \\[\\\u001b\\[92m1\\\u001b\\[0m\\]: ": "1" "Enter choice \\[\\\u001b\\[92m2\\\u001b\\[0m\\]: ": "2" もう既に正規表現がややこしいですね。 上記で書いてある通り、この expect モジュールの書き方では期待している入力を行ってくれません。しかも エラーにもならない ので質が悪いです。 ※そういう書き方をした自分が一番悪い 上記を実行すると、上から “1"、"1"、"1"、"2” が入力されて処理が終わります。 なぜ期待通りに動かないか よく見たらドキュメントにしっかりと書いてありました。 responses で定義した正規表現で同じ質問が連続でマッチした場合は、最初に定義した回答が全ての質問に適用されるようです。 どういうことかというと、今回の対話スクリプトの入力待ちの部分だけに注目すると、 Enter choice [1]: Enter choice [1]: Enter choice [1]: Enter choice [2]: 上記の4つのうち、 3つ目までは入力待ち時の質問の文字列が全く同じ なのです。 で、expect モジュールの responses を再度確認すると、このように書いていました。 "Enter choice \\[\\\u001b\\[92m1\\\u001b\\[0m\\]: ": "1" "Enter choice \\[\\\u001b\\[92m1\\\u001b\\[0m\\]: ": "2" "Enter hostname \\[\\\u001b\\[92m.*\\\u001b\\[0m\\]: ": "{{ instance_name.stdout }}" "Enter choice \\[\\\u001b\\[92m1\\\u001b\\[0m\\]: ": "1" "Enter choice \\[\\\u001b\\[92m2\\\u001b\\[0m\\]: ": "2" 1〜3つ目までは同じ質問が続いているため、 最初に定義してある入力待ちの質問を指定する正規表現が 1〜3つ目までの質問全てと一致している ため、その回答である “1” が1〜3つ目まで全てに入力されるということになります。 2つ目で “2” が入力されてほしいのにスルーされて悲しい気持ちです。 ではどうするか これもドキュメントに書いてありました。 この例のように書くことで、同じ質問が連続しても順番に従って回答してくれるようです。 そして今回の場合には以下のように書くことで期待通りの動きをするようになりました。(抜粋) - name: collectd の CloudWatch プラグインセットアップ expect: command: ./setup.py chdir: /var/tmp responses: "Enter choice \\[\\\u001b\\[92m1\\\u001b\\[0m\\]: ": - "1" - "2" - "1" "Enter hostname \\[\\\u001b\\[92m.*\\\u001b\\[0m\\]: ": - "{{ instance_name.stdout }}" "Enter choice \\[\\\u001b\\[92m2\\\u001b\\[0m\\]: ": - "2" 同じ質問に対して、回答内容を配列で順番に渡すことで、その順番通りに回答をしてくれます。 1回目の Enter choice [1]: では “1” 2回目の Enter choice [1]: では “2” 3回目の Enter choice [1]: では “1” 1回目の Enter hostname [1]: では取得した Name タグの値 1回目の Enter choice [2]: では “2” これで無事にやりたいことが実現できました。 注意 同じ質問に対して別の回答をする機能は、バージョン 2.1 で実装されたものなので、2.1 未満のバージョンを使っている場合は、同じ質問では同じ回答しかできないという悲しい結果になるのでご注意ください。 まとめ 今回は expect モジュールについての小ネタをご紹介しました。 ドキュメントはちゃんと読みましょう。(自戒)
こんにちは、インフラストラクチャー部の沼沢です。 前回の nginx に引き続き、collectd を使って php-fpm の情報を CloudWatch に連携する具体例をご紹介したいと思います。 参考: nginx の各種情報を collectd を使って CloudWatch に連携する 前提 Amazon Linux AMI release 2016.09 collectd 5.4.1 php 関連 5.6.28 本投稿の例は、以下と同等の状態ができあがっているという前提で進めます 新しい collectd の CloudWatch プラグイン | Amazon Web Services ブログ collectdのCloudWatchプラグインを試してみた | Developers.IO collectd の設定ファイルについては、デフォルトで記述されいてるコメントアウトの部分は基本的に無視して追記していくスタイルを取ります LoadPlugin だけはコメントアウト解除で対応 pm.status_path の値を CloudWatch に連携 pm.status_path とは、php-fpm のプロセス数などのステータス情報を取得するための設定です。 詳細は PHP: 設定 - Manual をご確認ください。 ローカルから curl http://localhost/www-status を実行して以下のような結果が返ってくる設定ができているものとします。 $ curl http://localhost/www-status pool: www process manager: static start time: 22/Dec/2016:18:32:27 +0900 start since: 406243 accepted conn: 8806 listen queue: 0 max listen queue: 0 listen queue len: 0 idle processes: 4 active processes: 1 total processes: 5 max active processes: 1 max children reached: 0 slow requests: 0 URL を叩いて返ってきたレスポンスから情報を取得したい場合には、 curl プラグイン を利用します。 Plugin:cURL - collectd Wiki curl プラグインを利用して、このレスポンスの中から、プロセスに関する4つの項目 (idle, active, total, max active) を CloudWatch に連携していきます。 余談ですが、レスポンスが json や xml の場合には curl_json プラグイン や curl_xml プラグイン を利用したほうがスマートに設定できます。 これらはまた別の機会にご紹介したいと思います。 1. プラグインをインストール curl プラグインを利用する場合、 collectd-curl をインストールします。 $ sudo yum install collectd-curl 2. collectd の設定ファイル (/etc/collectd.conf) に以下の設定をする #LoadPlugin curl のコメントアウトを外す 以下を追記する <Plugin curl> <Page "www"> URL "http://localhost/www-status" <Match> Regex "^idle processes: *([0-9]+)" DSType "GaugeLast" Type "phpfpm_processes" Instance "idle" </Match> <Match> Regex "^active processes: *([0-9]+)" DSType "GaugeLast" Type "phpfpm_processes" Instance "active" </Match> <Match> Regex "^total processes: *([0-9]+)" DSType "GaugeLast" Type "phpfpm_processes" Instance "total" </Match> <Match> Regex "^max active processes: *([0-9]+)" DSType "GaugeLast" Type "phpfpm_processes" Instance "max_active" </Match> </Page> </Plugin> 上記では、以下をそれぞれ取得するように設定しています。 1つ目の Match: 計測期間内で取れた最後の idle processes の値 2つ目の Match: 計測期間内で取れた最後の active processes の値 3つ目の Match: 計測期間内で取れた最後の total processes の値 4つ目の Match: 計測期間内で取れた最後の max active processes の値 DSType “GaugeLast” は計測期間(デフォルト10秒)内の最後の数値を取得するという意味です。 DSType については以下ご参考まで。 collectd.conf(5) – collectd – The system statistics collection daemon#plugin_tail 3. types.db に Type を追加 Type には、 types.db ファイルに定義してあるものしか指定できません。 上記の設定では Type “phpfpm_processes” を指定していますが、 phpfpm_processes はデフォルトでは用意されていないため、これを追加する作業を行います。 types.db(5) – collectd – The system statistics collection daemon types.db ファイルは、デフォルトでは /usr/share/collectd/types.db にあると思いますが、types.db ファイルの場所は設定ファイルで指定することができます。 TypesDB "/usr/share/collectd/types.db" このファイルに以下を追記します。 phpfpm_processes value:GAUGE:0:65535 4. 設定後、collectd を再起動 $ sudo service collectd restart 5. blocked_metrics に以下のものが追加されていることを確認する $ cat /opt/collectd-plugins/cloudwatch/config/blocked_metrics 〜略〜 curl-www-phpfpm_processes-idle curl-www-phpfpm_processes-active curl-www-phpfpm_processes-total curl-www-phpfpm_processes-max_active 〜略〜 6. CloudWatch に送る対象として、上記をホワイトリストに追記する $ sudo sh -c 'echo "curl-www-phpfpm_processes-.*" >> /opt/collectd-plugins/cloudwatch/config/whitelist.conf' 7. 設定後、collectd を再起動 $ sudo service collectd restart 8. blocked_metrics から追加されたものが消えているのを確認する $ cat /opt/collectd-plugins/cloudwatch/config/blocked_metrics 9. CloudWatch 上でグラフ化されていることを確認する しばらく待つと CloudWatch に以下の通り php-fpm の メトリクスが追加されます。 まとめ 今回は php-fpm のステータス情報を CloudWatch に連携してみましたが、nginx の時も今回も、こうやっていざやってみると設定はとても簡単な印象があります。 collectd の他のプラグインもどんどん使ってみたいと思っていますので、今後も利用して具体例としてどんどん公開していきたいと思います。
こんにちは、インフラストラクチャー部の沼沢です。 今年の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 にもフィードバックしてほしいですね。  ↩︎