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

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

フロントエンドエンジニアのnibaです。 先日、iQONのスマホページでviewportの改善を行いました。 その際の技術選定や工夫について述べていきたいと思います。 viewportについて まず初めにviewportに関して説明します。 viewportはHTMLメタ要素の一つです。これを指定することにより、スマホ/タブレットで表示される際の描画領域幅やスケールを決定できます。 viewportは以下のようなタグで指定できます。 < meta name = 'viewport' content = 'width=device-width,initial-scale=1' > 上の例では、描画領域幅widthにデバイス幅を意味するdevice-width、スケールinitial-scaleに1が指定されています。widthには980のようなピクセル固定値やdevice-widthを指定できます。固定値の場合には描画領域が画面幅に合うように自動でスケーリングされます。 昔は幅320pxのデバイスが多かったため、widthに320を指定するケースが多くありました。 しかし、レスポンシブデザインがスタンダードになってきている今、widthに固定値ではなくdevice-widthを指定することをGoogleも推奨しています。 Google Adsenseをはじめとした多くの広告もdevice-widthを前提として作られています。 今回やったこと iQONでは、1年前のリニューアル時にiPhone6のRetinaディスプレイの実ピクセル数に合わせて viewportのwidthに750を指定して実装しました。 しかし、これは現在のスタンダードにそぐわない上にデバイス幅基準の広告にも合いません。 そのため、8月にviewportのwidthにdevice-widthを指定する改修を行いました。 次に実際にどのように改修を行ったかについて述べていきます。 具体的にどのように改修を行ったか 実装で用いる単位の検討 今回の改修では、device-widthによらずレイアウトは一定にすることにしました。 そのため、絶対値のpxを相対値の単位に変更する必要があったので、CSSの相対単位の比較検討を行いました。 以下にCSSの相対単位の代表例を示します。 単位名 定義 対応デバイス rem htmlタグに指定されたfont-sizeを1remとする iOS5~ Android2.1~ em 親要素のfont-sizeを1emとする 全て vw ビューポートの幅に対する百分率。 iOS6.1~ Android4.4~ vh ビューポートの高さに対する百分率。 iOS6.1~ Android4.4~ 最終的に、iQONでは以下の理由でremを採用することにしました。 1remの大きさを自由に調整できる。 要素の包含関係に依存しないので、pxと同様に扱える。 iQONでサポートしているAndroid4.0~で使用できる。 1remの大きさの設定 1remの大きさを決定するにあたり、以下の2つを満たすような実装を考えました。 各デバイスで表示が変わらないようにする 幅750pxのデザインデータから直感的にコーディング出来るようにする ( デザイン上の100px=1rem など) 1.の条件を満たすために、画面幅基準の相対単位vwで1remの大きさを指定するようにしました。 さらに、1remに13.33vwを指定することで デザイン上の100px=1rem の関係になり、2.の条件を満たすことができました。 改修前 h1 { font-size : 20px ; width : 700px ; } 改修後 html { font-size : 13.33 vw; /* 1[vw] = 7.5[px]; 100[px] / 7.5[px/vw] = 13.33vw; */ } h1 { font-size : 0.2rem ; width : 7rem ; } デザイン上の1px=1rem や デザイン上の10px=1rem がより直感的ですが、実際にブラウザでレンダリングされる際に1remが10px未満になってしまいます。10px未満のフォントサイズはブラウザで正しくレンダリングされない為、 デザイン上の100px=1rem にしています。 ここまでvwを用いて1remの大きさを設定してきましたが、vwはAndroid4.4以上しか対応していません。 しかし、Android4.3以前の端末からのアクセスの場合でも、以下のJavaScriptコードで対応できます。 document .querySelector( 'html' ).style.fontSize = 100 * window .innerWidth + MOCKUP_VIEWPORT + 'px' これで、CSSの改修を単純な置換作業に落とし込むことができました。 幾つかハマった点もありましたが、単純な置換作業のみで大方問題なく改修出来ました。 次に、ハマった点をいくつかピックアップしたいと思います。 改修でハマった点 remはレンダリングの過程でpxに変換され、この際に小数が発生してしまいます。小数の処理はブラウザに大きく依存します。そのため、小さな値をremで指定した場合に不具合が発生することがあります。 ここでは実例を3つ紹介したいと思います。 ボーダーが消えてしまうことがある border-widthをremで指定するとデバイスによって1px未満の値になってしまい、 最悪の場合ボーダーが消えます。iQONでは、デバイスごとに表示が変わってしまうことを前提にborder-widthはpx指定を残しました。 border-radiusで描いた円が歪む ブラウザの小数点以下の処理により正円で無くなってしまうことがあります。 デバイスごとに表示が変わってしまうことを前提にpx指定を残しました。 子要素が微妙に隠れてしまう HTMLでは親要素は子要素に合わせて拡大されます。 しかし、子要素のサイズが小数点を含む場合、親要素が小数点以下を切り捨てられたサイズまでしか 広がらず、子要素の一部が隠れることがあります。対応方法はケースバイケースですが、子要素に指定していたスタイルを親要素に移すなどして対応しました。 子要素 親要素 まとめ CSSの相対単位は、レンダリングの過程で小数のpxに変換された際に表示がおかしくなる場合があるので 注意が必要な場合があります。 しかし、remを工夫して用いることによりviewportのdevice-widthへの改修を最小限のコストで行うことができました。 最後に VASILYでは、Webサービスを自らの手で創りたいハングリーな仲間を募集しています。 ご興味のある方は以下のリンク先をご覧ください。 https://www.wantedly.com/projects/61388 www.wantedly.com
こんにちは、神崎( @tknzk ) です。先日公開した ブログ からアップデートがありましたので、まとめておきます。 変更点としては、EBのBase Platformの変更と mackerel-agentの alpineベース化、ECRのセカンダリDocker Registryとしてのセットアップになります。 EB Base platformの変更 Elastic Beanstalkのbase platformを最新の 64bit Amazon Linux 2016.03 v2.1.3 running Multi-container Docker 1.11.1 へ移行しました。 これにより、container側のdocker も 1.11系を利用することができるようになりました。hostとcontainerのdocker versionの差異により、containerが起動できない問題が改善出来ました。 しかし、先日 1.12 がリリースされているので、またhostとcontainerのversionがずれる状況が起きる可能性がありますが、適宜最新にアップデートをしていきたいと思います。。 mackerel-agent のalpineベース化 課題であった host側のdocker versionとの組み合わせ問題と、alpine:3.3で起きていたmackerel-agent-plugin-linuxが起動できない問題が alpine:3.4で解決されたことから mackerel-agentのimageもalpineをベースにしたものをbuildしてproductionへ投入しています。 動かしているpluginは下記のとおりで、他のpluginでは問題が起きる可能性があるので、alpine linuxベースでmackerel-agentを動かす場合は、検証の上設定してください。 mackerel-agent mackerel-plugin-linux mackerel-plugin-nginx mackerel-plugin-docker Docker RegistryのSPOF問題への対応 以前のブログや、先日登壇した AWSUGコンテナ支部 でも発表させていただきましたが、過去に一度オペレーションミスによる障害をおこしており、オペレーションミスはできるだけ防ぐとともに、利用しているDocker Registryのquay.ioが万が一ダウンしてしまった場合に備えての冗長化を行いました。 セカンダリのDocker RegistryとしてAmason ECRを採用し、適当なタイミングで Docker imageをpushしておく仕組みを作りました。 以下のリンクにもあるとおり、東京リージョンにもECRがやってきたので、採用するには良いタイミングかと思います。 https://aws.amazon.com/jp/about-aws/whats-new/2016/08/amazon-ec2-container-registry-region-expansion/ 必要なdocker imageは EBの設定ファイルである Dockerrun.aws.jsonに集約してあるので、そこから tagを含めたimage名を取得し docker pullして imageを取得 docker tag コマンドで tagを付け替えを行い、 docker push で Amazon ECRへpushする簡易的なscriptを作りました。 # Docker image sync quay.io to Amazon ECR # require docker login over aws cli # command here : # aws ecr get-login --region ap-northeast-1 # and exec display docker login command # $(aws ecr get-login --region ap-northeast-1) # require ' json ' def targets # Dockerrun.aws.json json = open( ' ./Dockerrun.aws.json ' ) do | file | JSON .load(file) end images = [] json[ ' containerDefinitions ' ].each do | caontainer | images << caontainer[ ' image ' ] end images end def docker_pull (image) puts image ` docker pull #{ image }` end def tag_change (image) new_tag = replace_repository(image) puts new_tag ` docker tag #{ image } #{ new_tag }` end def docker_push (image) new_tag = replace_repository(image) ` docker push #{ new_tag }` end def replace_repository (image) repository, tag = image.split( ' : ' ) repository.gsub!( %r{ quay . io/vasilyjp } , ecr_host) return "#{ repository } :latest " if ARGV [ 0 ] == ' latest ' "#{ repository } : #{ tag }" end def ecr_host ' xxxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com ' end targets.each do | image | docker_pull(image) tag_change(image) docker_push(image) end 上記のscriptを適当なタイミング(imageに変更があったタイミング)で手動で実行して Amazon ECRに docker imageをpushしておき、プライマリなDocker Registryに事故が発生した際には、 Dockerrun.aws.jsonの 各ContainerDefinitionsのimageの部分と、認証情報の部分を書き換えるだけで、ElasticBeanstalkをデプロイすることできる状態を確保することができました。 切り替えのデプロイは手動による対応が必要になりますが、最低限の冗長化を行うことができました。 参考までにテストでECRのimageからデプロイした時の状態です [ec2-user@ip-xx-xx-xx-xx ~]$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 33783cbab565 xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/mackerel-agent:latest "/startup.sh" About a minute ago Up About a minute ecs-awseb-ad-server-stg-ammpxnj9ag-362-mackerel-agent-f6f3b99ddbb2baddee01 cc1018ff8e35 xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/nginx:latest "nginx -g 'daemon off" About a minute ago Up About a minute 0.0.0.0:80->80/tcp, 443/tcp ecs-awseb-ad-server-stg-ammpxnj9ag-362-nginx-proxy-f28e9092c5d9dba2ee01 f65afdb19efb xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/ad_server:latest "supervisord" About a minute ago Up About a minute 3000/tcp ecs-awseb-ad-server-stg-ammpxnj9ag-362-ad-server-c4e7d395e5d28abe9c01 94b3bf57a71a xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/td-agent:latest "td-agent --log /var/" About a minute ago Up About a minute 0.0.0.0:24224->24224/tcp ecs-awseb-ad-server-stg-ammpxnj9ag-362-fluentd-84e2a5e6b58c84e36400 aa66d70ad211 amazon/amazon-ecs-agent:latest "/agent" About a minute ago Up About a minute 127.0.0.1:51678->51678/tcp ecs-agent まとめ EB Base platformが docker 1.11 に対応したことで、docker imageの alpineベース化をすすめ、ECRを使うことで、Docker RegistryのSPOFの改善をおこいました。 以上、 ElasticBeanstalk w/ multi-container docker の最新事情でした。 最後に VASILYでは、一緒に開発をしてくれる仲間を募集しています。 Dockerを使った開発/運用をしてみたい方は以下のリンクをご確認ください!
はじめまして。サーバーサイドエンジニアのrihoです。 私は、とあるweb系大企業に新卒として入社し、1年働いた後にVASILYに転職してきました。 VASILYで働いて3ヶ月が経ち、同じweb業界でも職場環境が大きく違うことを実感しています。 そこで大企業とベンチャー企業の2社で働いた私から見た、それぞれの企業の特徴をまとめてみました! web業界での就職を考えている方や、現在就活中の方の参考になれば幸いです! 大企業編 専門分野に特化して学べる 前職では効率的に業務を進めるため、部署や業務が細分化されていました。そのおかげで、自分が担当している業務や技術に集中して学ぶことができ、業務に対する深い知識やスキルを身につけることができたと思います。 また、部署内にその分野のスペシャリストがいて普段から接することにより、スキルを高めやすい環境であることもよかったと思います。 大規模サービスの裏側を知ることができる 多くの人が利用しているサービスや、トラフィックが膨大なサービスをどのように運用して支えているのかを知り、携わることができるのはすごく良い経験となりました。 また大規模サービスには、それを支える素晴らしいフレームワークやノウハウが存在し、それらを学ぶことができるのは大企業ならではのことです。 知名度がある やはり知名度があることは大企業の魅力の1つです。 私の場合、インターネット業界に疎い親族でも会社を知っているということが、大企業に就職を決めた理由の1つでもありました。 やはり親元を離れて就職する方には、大企業の場合の方が両親の理解も得られやすいと思います。 また社内のエンジニアの方から、大企業は社会的信用の高さから、ローン、クレジットカードの審査や賃貸契約も通りやすいとも伺いました。 ベンチャー企業が信用度が低いというわけではないですが、やはり知名度のある大企業の方が審査が通りやすいそうです。 大きなプロジェクトに関わることができる ビジネスの規模が大きく、世間に影響力のあるプロジェクトに参加することができたことは、前職で良い経験になったと思っています。 大きなプロジェクトに参加することで、職種の違う様々な部署の人達とどうやってプロジェクトを進めるのかを知ることができますし、異職種の人々と連携する力がつきます。 ベンチャー編 スピードが速い 転職して感じた大企業との一番の違いはスピード感です。 前職の場合、仕方ないことだとは思うのですが、他部署の処理待ち時間が存在してしまい、ストレスを感じることが多々ありました。 その点ベンチャー企業は社員数が多くなく役職がフラットなので、他部署の待ち時間はあまり発生せず自分のペースで開発が行えます。 また意思決定のスピードも早く、事業の方向性や戦略が変わることもあり、変化への対応力も身につきます。 役員との距離が近い VASILYでは同じフロアに全社員が在籍しているので、必然と役員との距離も近くなります。 したがって前職に比べ、圧倒的に企業の方向性や役員の考えが伝わってきやすいです。 私の場合、隣の隣がCTOの席なのですが、やはり尊敬するCTOが近くにいると、開発のやる気が出て身が引き締まりますね! 勤務時間が柔軟 会社によるかもしれませんが、大企業の場合、始業時刻が決まっていることが多いです。 VASILYでは始業時刻が厳密には決まっておらず、個人が一番パフォーマンスを発揮出来る時間帯に働くことができます。 例えば、子育てのために、朝早くに出社して夕方早めに退社する人もいれば、昼過ぎに出勤して深夜まで勤務するエンジニアもいます。 通勤時間が長い私にとっては、満員電車を避けて通勤できることがすごく嬉しいです。 新しい技術に意欲的 前職では依存関係や制約により、新しい技術を取り入れたり挑戦することが難しい環境でした。 そして新しい技術に対して意欲的な人が周囲に少なかったように思います。 ベンチャー企業の方が、新しい技術に対して意欲的です。 またVASILYでは、会社の社風として技術的挑戦を挙げているので、社内エンジニアが日々新しい技術に貪欲です。 週に一度、社内のエンジニアで集まって、興味を持った技術の共有を行う会があるのですが、どこから見つけてくるんだろうと思うくらいたくさんの情報が共有され、知見がたまります。 まとめ web業界の大企業とベンチャー企業、全ての企業が同様の特徴を持っているわけではありません。 しかし、私が働いた経験が参考になればと思い今回の記事を書かせていただきました。 私の場合、大企業で安定して働くのではなく、スピード感ある会社でエンジニアとして多くを学び成長したい、と思い転職を決めました。 また、VASILYを志望したのは、 ”HIPSTER” という会社独自の理念に共感したからです。 今回は大きく大企業とベンチャー企業と分けましたが、やはり1社1社環境が違うので、納得のいくまで企業や人に会って会社理解して決断することが大事だと思います。 そして自分の納得のいく就活をしてください!!! そして現在、VASILYではiQONを一緒にガッツリ開発してくれるエンジニアを募集しています! ご興味のある方は以下のリンクからご応募ください。
Androidエンジニアのnissiyです。 iQONではアプリの収益化のために、ページによってネイティブ広告や動画広告の掲載を行っています。 先日アプリのアップデートで動画広告に関してのアップデートを行いましたが、案件の都合で広告再生の仕組みを外部アドネットワークのSDKを使用せずに自作することにしました。 しかし、実装に多くの時間を割くことができなかったため、Androidの標準の仕組みをベースにポイントを押さえながら最短で実装を行いました。 実装した動画広告は以下のようなインタースティシャルの動画広告になります。 アプリのOSバージョンと導入スピードからVideoViewを選択 現在Androidには動画や音楽を提供するための仕組みがいくつか存在します。 代表的なものだと、APIレベル1から存在する標準の仕組みである MediaPlayer 、2014年に登場したGoogle製の外部ライブラリである ExoPlayer があります。 本当は新しい仕組みであるExoPlayerを使用して実装したかったのですが、ExoPlayerは内部で MediaCodec を使用しているのでAPIレベル16以上でないと動作しないため、ExoPlayerの導入を諦めました(iQONはminSdkVersionが15で、APIレベル15の端末のユーザーが多くいる)。 そこでMediaPlayerを利用することに決めましたが、導入スピードを考えてMediaPlayerとSurfaceViewを内包して作られている VideoView を使用することにしました。 VideoViewはAPIレベル1から存在する標準の仕組みであり、レイアウトファイルに記述すれば最小で以下のコードだけで動画を再生することができます。 ネットワーク上の動画を再生する場合 VideoView videoView = (VideoView) findViewById(R.id.video_view); videoView.setVideoURI(Uri.parse(<動画ファイルのURL>)); videoView.start(); ローカルの動画を再生する場合 VideoView videoView = (VideoView) findViewById(R.id.video_view); videoView.setVideoPath(<動画ファイルのパス>); videoView.start(); 動画広告が再生されても、他のメディアプレイヤーの再生を止めないようにする VideoViewをそのまま使用する場合、Google Play Musicなどの音楽プレイヤーを使用している状態で動画広告の再生をスタートしてしまうと音楽プレイヤーの再生が止まってしまいます。 動画がメインのアプリの場合は全く問題ありませんが、動画広告や無音動画しか再生されないアプリの場合にこの挙動は非常に不便です。 なぜこのようなことが起こるかというと、VideoView内で明示的に他のプレイヤーの音を止める処理が書かれているからです。 // VideoView.java private void openVideo() { if (mUri == null || mSurfaceHolder == null ) { // not ready for playback just yet, will try again later return ; } // we shouldn't clear the target state, because somebody might have // called start() previously release( false ); // この2行が他のプレイヤーの音を止める処理 AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); am.requestAudioFocus( null , AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); // 以下省略... この2行を無効にするために今回は こちらのGist を参考にVideoViewをカスタマイズしました。 カスタマイズしたViewを使用することで、動画広告が再生されても他のプレイヤーの音が止まることは無くなりました。 VideoViewをラップしたCustomViewで広告画面を実装 透過したマスクをかけた上にインタースティシャルの動画広告を再生させるために画面全体をCustomViewで実装しました。 レイアウトは以下のような構成になります。 <jp . vasily . iqon . ui . InterstitialMovieLayout xmlns : android = "http://schemas.android.com/apk/res/android" android : id = "@+id/interstitial_movie_layout" android : layout_width = "match_parent" android : layout_height = "match_parent" android : background = "#b2000000" android : clickable = "true" android : paddingLeft = "16dp" android : paddingRight = "16dp" android : visibility = "gone" > <jp . vasily . iqon . ui . CustomVideoView android : id = "@+id/video_view" android : layout_width = "match_parent" android : layout_height = "match_parent" android : layout_gravity = "center" /> </jp . vasily . iqon . ui . InterstitialMovieLayout> 最初は広告画面全体を別Activityとして実装していましたが、VideoView内のSurfaceViewが厄介で広告の表示・非表示の制御が上手くいきませんでした。 そのためCustomViewで全体を覆うようにして、Visibilityの変更をトリガーに広告画面の表示・非表示と、動画の再生・停止を制御するようにしました。 最後に 動画広告を再生するための仕組みを紹介しましたが、広告は案件次第で使うAPIやLibraryに制限が出る場合があるため、今回のVideoViewを使った実装はあくまで一例に過ぎないことを覚えておいてください。 また、今回の内容は動画広告だけでなくライトな動画を再生するためにも有効な方法だと思いますので、別の用途でも参考にしていただければと思います。 VASILYではiQONを一緒に開発してくれるAndroidエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。
こんにちは、VASILYバックエンドエンジニアの塩崎です。 今回はApache Solr(以下、Solr)で商品検索のサジェスターを作ったので、それを紹介します。 サジェスターを作るにあたり、どのようにスキーマやサーチコンポーネントを定義すれば良いのかを説明します。 なお、この記事はsolr 4.10.4を対象にした記事です。 それ以外のバージョンでは設定項目が変わってくる場合があります。 サジェスターとは サジェスターとは、ユーザーが検索用のフォームに単語を入力している途中に、その入力途中の単語を補完する機能です。 例えば、Google検索でサジェスターについて調べようとした時に、「さじぇ」と入力した時点で以下のように「さじぇ」に続く単語が候補として現れます。 このような機能を実装することによって、ユーザーがテキストを入力する手間が省けたり、入力間違いをした単語で検索をしてしまうことを防げたりする効果があります。 日本語のサジェスターの難しいところ サジェスターの基本動作は、検索対象のドキュメント中の単語リストを保持し、ユーザーの入力途中の単語で前方一致検索をかけることです。 しかし、日本語に対応したサジェスターを作る場合は、英語などの他の言語にはない、日本語特有の難しさがあります。 主な課題は「単語の抽出」と「漢字の読み」です。 単語の抽出 英語の文章は単語と単語との間がスペースや記号などで区切られています。 そのため、文章を単語に分解する処理を簡単に書くことができます。 しかし他方で日本語の文章にはそのような機械的に単語分割できるような目印はありません。 そのため、日本語の文章を単語分割するためには形態素解析という処理を行う必要があります。 Solrにはkuromojiという形態素解析エンジンが組み込まれているため、これを利用して文章を単語に分割します。 漢字の読み さらに、単語に分割した後には「漢字の読み」の問題もあります。 例えば、「とう」と入力した時に「東京」という単語を返すためには、「東京」という単語の読みが「とうきょう」であるという情報が必要です。 kuromojiで形態素解析を行うと単語の読みの情報も取得できるために、これを利用します。 また、未確定の子音やひらがなにも対応する必要があります。 「とうky」と入力した時には、「東京」や「東急」といった単語を候補として表示する必要があります。 これらのことを解決するために検索対象のドキュメント及び、ユーザーが入力中のテキストをローマ字読みに変換した後に前方一致検索をかける必要があります。 それと同時にローマ字読みに変換せずに前方一致検索をかける必要もあります。 これは「東」というテキストを入力した時に「東京」を候補として表示するためです。 「東」のローマ字読みは「higashi」なので、「tokyo」に前方一致しないからです。 日本語に対応したサジェスターの処理フロー 上記のような点を考慮してサジェスターを作る場合は次のような処理が必要です。 検索対象のドキュメントを単語単位に分割し単語リストを作る。 その時には各々の単語に対して、その単語のローマ字での読みを併せて保持する。 ユーザーがテキストを入力する毎に入力されたテキストをローマ字読みに変換し、上記のリストに対して前方一致検索を行う。 同時に、ローマ字読みに変換せずに前方一致検索も行う。 Solrの設定ファイル さて、サジェスターの作り方が分かりましたので、それをSolrの設定ファイルに落とし込みます。 schema.xml まずは、スキーマ定義です。 ここではtext_ja_for_suggestとtext_ja_romajiという2つの型を定義しています。 text_ja_for_suggestはサジェスター用に文章を単語に分割するための型です。 検索対象のドキュメントに対して、以下に定義されるanalyzerを通すことでサジェスト用の単語リストを作ります。 検索対象のドキュメントの英数字やカタカナの全角半角の表記揺れをICUNormalizer2CharFilterFactoryで統一した後に、JapaneseTokenizerFactoryで形態素解析を行っています。 検索用にこのtokenizerを使用するときには mode=search で使用することが多いですが、この設定をサジェストで使ってしまうと、単語が細かくなりすぎてしまうために mode=normal で使用しています。 分割後の単語に対しては、単語単位での表記揺れの統一や、不要な単語のフィルタリングなどを行っています。 <fieldType name = "text_ja_for_suggest" class = "solr.TextField" positionIncrementGap = "100" autoGeneratePhraseQueries = "false" > <analyzer> <!-- ICU正規化(NFKC) --> <charFilter class = "solr.ICUNormalizer2CharFilterFactory" name = "nfkc" /> <!-- 形態素解析(normal mode) --> <tokenizer class = "solr.JapaneseTokenizerFactory" mode = "normal" /> <!-- 品詞によるフィルタリング --> <filter class = "solr.JapanesePartOfSpeechStopFilterFactory" tags = "lang/stoptags_ja_for_suggest.txt" /> <!-- 同義語 --> <filter class = "solr.SynonymFilterFactory" synonyms = "synonyms_for_suggest.txt" ignoreCase = "true" expand = "true" /> <!-- カタカナ長音の正規化 --> <filter class = "solr.JapaneseKatakanaStemFilterFactory" minimumLength = "4" /> <!-- アルファベットを小文字に正規化 --> <filter class = "solr.LowerCaseFilterFactory" /> <!-- 単語によるフィルタリング --> <filter class = "solr.StopFilterFactory" ignoreCase = "true" words = "lang/stopwords_ja_for_suggest.txt" /> </analyzer> </fieldType> text_ja_romajiは単語をローマ字読みに変換するための型です。 上の型で分割された単語のローマ字読み変換に使用するのに加えて、ユーザーが入力途中のテキストをローマ字読み変換するためにも使用します。 前半部分はtext_ja_for_suggestと同じです。 JapaneseReadingFormFilterFactoryを mode=romaji で使用することによって、単語のローマ字読みを取得します。 ShingleFilterFactoryはユーザーが入力中のテキストが複数単語に分割された時に、それらを結合してから前方一致検索をするために使用しています。 <fieldType name = "text_ja_romaji" class = "solr.TextField" positionIncrementGap = "100" autoGeneratePhraseQueries = "false" > <analyzer> <!-- ICU正規化(NFKC) --> <charFilter class = "solr.ICUNormalizer2CharFilterFactory" name = "nfkc" /> <!--形態素解析(normal mode) --> <tokenizer class = "solr.JapaneseTokenizerFactory" mode = "normal" /> <!-- ローマ字の読みに変換 --> <filter class = "solr.JapaneseReadingFormFilterFactory" useRomaji = "true" /> <!-- トークンNGramの生成 --> <filter class = "solr.ShingleFilterFactory" minShingleSize = "2" maxShingleSize = "99" outputUnigrams = "false" outputUnigramsIfNoShingles = "true" tokenSeparator = "" /> <!-- アルファベットを小文字に正規化 --> <filter class = "solr.LowerCaseFilterFactory" /> </analyzer> </fieldType> そして、サジェスト用の単語が格納されるフィールドである、suggestフィールドを作ります。 copyFieldを使い、商品名が入るフィールドであるtitleフィールドの内容をsuggestフィールドにコピーしています。 <fields> <field name = "suggest" type = "text_ja_for_suggest" indexed = "true" stored = "true" /> </fields> <copyField source = "title" dest = "suggest" /> solrconfig.xml 上記のschema.xmlでフィールド及び型の定義が完了したので、次に、それらを使って検索を行うためのモジュールの設定を書きます。 Solrでは検索を行うためのモジュールはSearchComponentと呼ばれています。 ここでは2つのSearchComponentを併用しています。 SpellCheckComponent 1つ目のSearchComponentがSpellCheckComponentです。 このSearchComponentはローマ字読みに変換した後の単語に対して前方一致検索を行い、その結果を返すSearchComponentです。 suggestAnalyzerFieldTypeとqueryAnalyzerFieldTypeにtext_ja_romajiを指定することでローマ字読みに変換することを指定しています。 <searchComponent class = "solr.SpellCheckComponent" name = "suggest_ja" > <lst name = "spellchecker" > <str name = "name" > suggest_ja </str> <str name = "classname" > org.apache.solr.spelling.suggest.Suggester </str> <str name = "lookupImpl" > org.apache.solr.spelling.suggest.fst.AnalyzingLookupFactory </str> <str name = "storeDir" > suggest_ja </str> <str name = "buildOnStartup" > true </str> <str name = "buildOnCommit" > true </str> <str name = "comparatorClass" > freq </str> <str name = "field" > suggest </str> <str name = "suggestAnalyzerFieldType" > text_ja_romaji </str> <bool name = "exactMatchFirst" > true </bool> </lst> <str name = "queryAnalyzerFieldType" > text_ja_romaji </str> </searchComponent> TermsComponent もう1つのSearchConponentはTermsComponentです。 このSearchComponentはローマ字読みに変換せずに、単純な前方一致検索を行います。 <searchComponent name = "terms" class = "solr.TermsComponent" /> requestHandler 上で定義した2つのSearchComponentをrequestHandlerに登録して、HTTPで叩けるようにします。 サジェスターはユーザーが1文字入力する毎に呼び出されるため、高速に応答する必要があります。 そのため、1回のHTTPリクエストで2つのSearchComponentに対して同時に処理を投げられるように、1つのrequestHanderに2つのSearchComponentをひも付けます。 <requestHandler name = "/suggest_ja" class = "org.apache.solr.handler.component.SearchHandler" startup = "lazy" > <lst name = "defaults" > <str name = "spellcheck" > true </str> <str name = "spellcheck.dictionary" > suggest_ja </str> <str name = "spellcheck.collate" > false </str> <str name = "spellcheck.count" > 10 </str> <str name = "spellcheck.onlyMorePopular" > true </str> <bool name = "terms" > true </bool> <bool name = "terms.distrib" > false </bool> <str name = "terms.fl" > suggest </str> </lst> <arr name = "components" > <str> suggest_ja </str> <str> terms </str> </arr> </requestHandler> 未確定のテキストに対応する 上記設定をそのまま使うと、日本語入力の未確定テキストに対しては正しく結果を返さないことがあります。 text_ja_romaji型は一部の単語に対しては正しくローマ字の読みを返すことができないからです。 例えば、ユーザーが「きゃみ」と入力したときには、「kiゃみ」という結果になってしまいます。 これは「きゃみ」という単語が形態素解析エンジンの辞書にないために起こる現象です。 先ほどschame.xmlで定義したCharFilterをさらに追加して解決してもいいですが、アプリケーション側で行う方が簡単でしたので、一部のテキストに対してはSolrにリクエストを投げる前にアプリケーション側でローマ字変換を行いました。 以下の正規表現にマッチするテキストの場合は形態素解析をせずにローマ字読みに変換することが可能ですので、アプリケーション側でローマ字変換を行います。 ローマ字変換にはromajiというgemを使いました。 require ' romaji ' if word =~ /^( \p{InHiragana} | \p{InKatakana} )+[ a-zA-Za-zA-Z ]{0,3}$/ word = Romaji .kana2romaji(word) end # きゃみ -> kyami # 靴 -> 靴 まとめ Solrを使って日本語に対応したサジェスターを作ることができました。 日本語特有の問題である、単語抽出と漢字の読みの問題も解決し、入力途中のテキストに対してもサジェストを行うことができるようになりました。 参考 Apache Solr Apache Solrの公式サイトです。 http://lucene.apache.org/solr/ kuromoji Javaで形態素解析を行うためのライブラリです。 http://www.atilika.org/ [改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン Solrを扱うならば必読書です。 この記事で紹介したサジェスターの基礎的な部分はこの本を参考にしました。 https://www.amazon.co.jp/dp/4774161632 romaji rubyでひらがな→ローマ字変換を行うgemです。 https://github.com/makimoto/romaji 最後に VASILYではiQONを一緒に作っていく仲間を募集中です。 興味のある方は以下のリンクをご覧ください。
2016年8月9日、スタートアップで活躍したいエンジニア向けのトークイベント、CAREER TALK for Engineerを開催しました。 イベント風景 MERY を運営する株式会社peroli様の開発部長 水島様と、弊社CTOの今村が、大手とベンチャーを比較した自身の考えを、経験に基づいて話す場がありました。大手とベンチャー両方を経験している両者だからこそ、「大手の安定」や「ベンチャーの不安定」などについて本音を皆さんに伝えられたのではないかと思います。 また、両者の若手エンジニアが「ベンチャーで働くメリット/デメリット」など若手視点から話すセッションがありました。 新卒でベンチャー企業を選ぶ人の考え方を表に出す機会はあまり無いため、若手の考え方が伝えられる珍しい場になったのではないかと思います。 おわりに ご参加して下さった皆様、ありがとうございました。 VASILYでは引き続きエンジニアに関するイベントを実施していきます。 今回はベンチャーについてお伝えするイベントを実施しましたが、次回は技術満載なイベント「Fashion Tech Meetup Vol.3」を開催する予定です。 3回目の開催となるFashion Tech Meetupですが、今回もFashion x Technologyに関する会社が それぞれの技術をお伝えします。前回までの内容は下記からご確認いただけますので、 興味を持たれた方は是非Fashion Tech Meetup Vol.3にご参加ください。 fashion-tech.connpass.com fashion-tech.connpass.com
Webフロントエンドエンジニアの権守です。 今回は、iQONのWebアプリのAPIリクエスト部分の仕組みを改善したことについて紹介します。 前提 このブログでも何度か紹介していますが、iQONでは、ネイティブアプリとWebアプリの両方で、共通のAPIを利用して開発を行っています。 そのため、通常のRailsアプリケーションと異なり、iQONのWebアプリ版のモデル部分では、DBへのアクセスを行わずAPIへのアクセスを行い、データを取得します。 こういった形式を扱うGemとしては her などがありますが、iQONでは、完全にREST形式でない、並列でリクエストを行いたいなどの理由から自前で実装しています。 問題 しかし、このモデル部分には次の二つの問題がありました。 APIリクエストの依存関係を記述できないため、実行タイミングを制御する必要がある APIリクエストのリクエスト処理とデータの取得処理を同時に記述できない 一つ目はパフォーマンスに、二つ目は可読性に問題があります。 擬似コードを例に問題について説明します。 旧リクエスト方式の擬似コード Item .find(params[ :id ], :item ) Item .recommend_items(params[ :id ], :recommend_items ) IqonModel .execute @item = get_result( :item ) # ... @itemを使う処理 ... @recommend_items = get_result( :recommend_items ) Item .search({ brand_id : @item [ :brand_id ]}, :brand_items ) # itemのリクエストに依存 IqonModel .execute @brand_items = get_result( :brand_items ) 上のような記述を行った場合、APIリクエストは次の画像に示すタイムラインのようになりえます。 この場合、依存関係を適切に考慮すると、次のようなタイムラインにでき、パフォーマンスは向上します。 この問題は偏に、実行タイミングをコードで制御していることが原因です。 理想的には、依存関係を考慮しつつ、並列度を最大化すべきです。 また、上の擬似コードでは大して気になりませんが、利用するAPIの量が増えるほど、各APIリクエスト処理と結果の取得処理などのコードの分離による可読性の劣化は著しくなります。 結果 上で述べた問題を解決し、次のように記述できるようにしました。 Item .find(params[ :id ], :item ) do | find_results | @item = find_results.first # ... @itemを使う処理 ... Item .search({ brand_id : @item [ :brand_id ]}, :brand_items ) do | results | @brand_items = results end end Item .recommend_items(params[ :id ], :recommend_items ) do | results | @recommend_items = results end IqonModel .wait_all_requests # 全スレッドの処理完了を待つ また、実際に問題を解決したことで、ページによってはサーバーサイドの処理時間が40msほど短縮されました。 実装について コールバック APIリクエストの各メソッドにコールバックを設定できるようにしたことによって、リクエスト処理と結果取得処理が分離しなくなっただけでなく、APIリクエストの依存関係を明示的に記述できるようになりました。 各APIリクエストのメソッドに渡されたブロックがコールバックに相当します。 スレッド 旧リクエスト方式では、IqonModel.executeが実行されたタイミングで、それまでに呼び出されたリクエストをまとめて、並列にリクエストするというものでした。 しかし、今回の改善では、明示的なexecuteを利用しない代わりに、各リクエスト時にスレッドを生成するようにしました。これによって、並列にリクエストしつつ、executeのタイミングを制御する必要がなくなりました。 一方で、スレッドの完了を制御する必要があります。 これについては、IqonModel.wait_all_requestsメソッドでリクエストによって生成されるスレッドに対してjoin処理を実行することで制御しています。 APIリクエスト部分を簡単化したコード class Item def self . find (id, label, &block) IqonModel .request(label, " /item/ #{ id }" , block : block) end end class IqonModel def self . init @request_threads = [] end def self . request (label, path, params : {}, method : ' GET ' , block : nil ) @request_threads << Thread .new do body = request_api(label, path, params, method, timeout, must) results = body[ :results ].present? ? body[ :results ] : [] block.call(results, body[ :info ], body) if block # コールバックに相当 end end def self . wait_all_requests @request_threads .each(& :join ) end end このコードで注意すべき点は、コールバック内で再びAPIリクエストが行われた際には、@request_threadsに新たなスレッドが追加された後に、そのスレッドが終了することです。それによって、wait_all_requestsは全てのAPIリクエストの完了を待つことができます。 複雑な依存関係 ページによっては、複数のAPIリクエストに依存する処理も存在します。そのような場合にも対応できるように以下のような処理を実装しました。 IqonModel .register_callback([ :item , :recommend_items ]) do # :itemと:recommend_itemsのAPIリクエストに依存する処理 end class IqonModel def self . init # ... @statuses = {} @callbacks = {} @callback_mutex = Mutex .new end def self . request (label, path, params : {}, method : ' GET ' , block : nil ) @statuses [label] = :run # ... fire_callback(label) end def self . register_callback (labels, &block) @callbacks [labels] = block end def self . fire_callback (label) @callback_mutex .lock @statuses [label] = :done @callbacks .select { | labels , _v | labels.include? label }.each do | labels , block | block.call if labels.all? { | l | @statuses [l].present? && @statuses [l] == :done } end ensure @callback_mutex .unlock end end 指定されたAPIリクエストが全て完了した時点で渡されたブロック内の処理が実行するために、 Mutexを用いて、並列で実行されるAPIリクエスト処理を確実に一つずつ終了状態に切り替えています。 まとめ Web APIを用いたアプリケーションで問題になりがちなリクエストの効率化について取り組みました。スレッドを使った並列化は場合によってはパフォーマンスの劣化につながることもありますが、今回のように、依存関係を適切に処理することで、パフォーマンスを向上させることができます。 最後に VASILYでは、iQONをよりよくするために新しい仕組みを一緒に作っていけるような仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 www.wantedly.com
2016年8月9日、スタートアップで活躍したいエンジニア向けのトークイベント、 CAREER TALK for Engineer を開催します。今回は MERY を運営する株式会社peroli様と弊社VASILYの2社での開催となります。堅苦しい会ではなく、お酒を飲みながらのトークイベントですのでお気軽にご参加ください。 イベント概要 日時:8月9日(火)19:30〜22:30 場所:渋谷ヒカリエ21F DeNAセミナールーム 参加対象者:スタートアップに興味のあるエンジニア(経験者・新卒問わず) 定員:50名様程度 参加登録(connpass) 何をやるイベントか スタートアップ企業を知ってもらい、興味を持って欲しいというのがイベントの狙いです。 実態が分からないために転職や就職に踏み切れていない方への後押しになればと思っています。 簡単な各社の会社説明の後、「ベテラン」「新卒」「女性」といったメンバーが「ベンチャーで働くメリット/デメリット」などを語ります。 ぜひ普段気になっていること、企業の会社説明会は聞きにくいこととなど、本音を聞き出してください! 皆様のご参加お待ちしております! 参加登録(connpass)
こんにちは、バックエンドエンジニアのjoeです。 みなさんはお気に入りのアプリに月額課金をしたことがありますか?したことがない人は今すぐお気に入りのアプリをみつけて月額課金しましょう! 実際にiOSで月額課金をすると、課金の証明としてAppStoreがレシートを発行します。レシートと言ってもAppStoreが紙のレシートを送りつけてくるわけではなく、電子的な購入情報のことをレシートと呼びます。ユーザーが解約処理をしない限りAppStore側でレシートが自動更新される仕組みになっています。(月額課金の場合) その際に、AppStoreのサーバーにHTTPのPOSTリクエストでレシートを問い合わせ、現在の課金状況を知ることができます。このお問い合わせ処理と、レシートが不正なレシートでないかをチェックする処理を合わせてレシート検証と呼びます。 今回はiOSのレシート検証をクライアントのみでの検証からサーバーサイドでの検証に実装を変更したので、その理由や方法、考慮するべき点などを書きます。 注意 この情報は2016年8月3日現在のものです。 サーバーサイドでのレシート検証が推奨される理由 レシート検証は、クライアント側で完結させることもできます。しかし、クライアント側で完結させてしまうことで2つのデメリットが発生します。 クライアント側で検証処理が閉じているためユーザーによるレシート改ざんが可能になってしまう 複数のプラットフォームで課金サービスを提供している際に、クロスデバイス対応が難しくなる 1に関して、JailBreakやroot化を使って不正レシートをサーバーサイドに送りつける例があるそうなので、サーバーサイドでのレシート検証が推奨されています。 2に関しては、機種変更やデバイスの使い分けをしている方など、iOS/Android/Web等の複数プラットフォームで同じユーザーでログインする場合があります。その場合に、サーバーサイドに検証できる仕組みがあれば別端末にログインした場合でも、より正確な課金の有無の検証が可能になります。 レシート検証の方法 AppStoreのサーバーにHTTPのPOSTリクエストを送ることでレシートを問い合わせることができます。 リクエストURL テスト環境用のURLと、production用のURLで分かれています。 環境 URL 用途 production https://buy.itunes.apple.com/verifyReceipt 本番用 sandbox https://sandbox.itunes.apple.com/verifyReceipt 開発時のテスト環境用 sandboxにはsandboxの、productionにはproduction用のレシートがあり、productionのURLにsandbox用のレシートを送るとエラーが返ってきます。 sandboxを利用するには、Appleのテストアカウントを取得して課金処理のテストを行います。 リクエストbody 下記の2つだけでOKです。 key 値 サンプル receipt-data Base64エンコードしたレシート情報 MIIjwgYJKoZIhvcNAQcCoIIjszCCI... password アプリケーションの共有鍵 fea2ebde5... receipt-dataのサンプルは省略してありますが、実際はかなり長いです。12KB程度のデータです。 サーバーサイドでレシートをBase64エンコードするのではなく、クライアントかAppStoreからBase64エンコード済みのデータを受け取ります。一番最初の購入時に必ずクライアントからBase64エンコード済みのレシートを送ってもらわないとサーバーサイドとAppStoreで直接レシート検証のやり取りができません。 実装上の注意 審査が通るまではsandboxしか使えないのでproduction環境でもsandboxを受け付けられるような実装にしておく必要があります。 rubyのレシートサーバーサイド検証用のgemに venice というgemがありますが、AppStoreのAPIのバージョンによっては動きません。gemを使う場合は、ご自分のバージョンで正常に動作をするかしっかり確認してから使うことをおすすめします。 [2016/08/04:追記] veniceと別のgemを紹介していただきました!参考になれば幸いです。 https://github.com/mbaasy/itunes_receipt_validator サーバーサイドレシート検証の処理構成 月額課金購入時 更新時 弊社では課金処理を始めた当初、クライアント側のみで認証を行っており、途中でサーバーサイドの検証に変更したため、古くから課金しているユーザーはサーバーサイドにBase64エンコード済みのレシートを保持していません。なので、一度クライアント側に問い合わせてレシートを受け取ってから検証を行うようにしました。 アプリ立ち上げ時の課金状況の問い合わせを毎回行うと重くなるため、ユーザーテーブルに保持してある課金期限が過ぎているユーザーのみ行っています。 AppStoreから返ってくるレシート情報の項目 APIが返す項目の公式ドキュメントは下記のリンクです。 公式ドキュメント 注意しなければならないのが、古くから課金コンテンツを営んでいるアプリだとその当時のバージョンのAPIが返ってきます。弊社の課金コンテンツは2014年に開始されているためか、iOS6タイプのAPIが返ってきています。 その証拠に、 only returned for iOS6 と書いてある下記の2つの項目がAPIのレスポンスに含まれています。 途中でサーバーサイド検証に置き換えるような場合、最新ドキュメントの通りにAPIが返らない可能性があるため注意が必要です。ドキュメントだけを信頼せず、実際に返ってくるフィールドを見ながら実装をすすめることをおすすめします。(iOS7以降のAPIバージョンでもiOS6のみが返す項目と書いてある latest_receipt が返るという記事もあったので、公式ドキュメントの全てが正しい情報ではないかもしれません。) ※ [危険!!!!]iOS6タイプのトランザクション形式を使っている人への注意 receipt フィールドに含まれる in_app という項目は、公式ドキュメントの説明文には In the JSON file, the value of this key is an array containing all in-app purchase receipts. と書いてありますが、実際には1ヶ月毎の更新が少し遅れることがあったので最新レシートの認証を行う場合は latest_receipt_info のほうを使うことをおすすめします。こちらのほうはリアルタイムで更新されます。 検証内容 レシートの内容で検証すべき項目 項目 項目の内容 検証内容 status 0であれば正常なレシート、その他は不正なレシート( エラーコード表 参照) AppStoreから正常なレシートが返ってきているか in_app又はlatest_receipt_info 過去の購入履歴 課金履歴が存在しているか bundle_id iTunesConnectで設定したCFBundleIdentifierの値 自分のアプリのものか product_id iTunesConnectで設定したproductIdentifierの値 意図した商品への課金か transaction_id 1ヶ月ごとのレシート毎に発行される固有のid 別のユーザーのレシートを使っていないか original_transaction_id ProductごとのAppStore固有のid 同じAppStoreで別のアカウントが既に課金していないか expires_date レシートの期限 期限は切れていないか 各テーブルに保持すべき項目 ユーザーテーブルに持つべき項目 項目 内容 用途 purchase_device どのプラットフォームで課金しているユーザーなのか クロスデバイス対応の際に有用 premium_expire_time 課金の期限 レシート検証対象のユーザーかどうかをチェック レシートテーブルに持つべき項目 user_idとレシートの内容全部 サーバーサイドのレシート検証で考慮すべきこと クロスデバイス 状況 AndroidからiOSへの機種変更時(逆も然り) 複数デバイスでのログイン 対処方法 ユーザーテーブルに保持したpurchase_deviceを確認し、どのプラットフォームで課金しているユーザーなのかをチェック AppStoreのレシート検証が通らなかったら、別のデバイスの検証で課金の有無をチェック 例) ダブルアカウント 状況(ニッチな状況ですが) アカウントを2つ持っている 過去に片方のアカウントで課金していたことがある 現在つかっているAppStoreのアカウントが過去に課金していたAppStoreと同じアカウント original_transaction_idによる認証をサーバーサイドで行っている(original_transaction_idはProductごとにAppStoreで固有の値なので、前に課金していた時と再度課金する時のどちらも同じ値で返ります) もう片方のアカウントでもう一度課金したい 対処方法 original_transaction_idが一緒な他のユーザーが居ても、片方のアカウントでユーザーテーブルのexpires_dateが過ぎていれば認証OKにする 例) まとめ iOSのサーバーサイドレシート検証の実装のメリットから実際の検証方法、考慮する点をまとめました。 状況によって考慮すべき点や検証すべき項目、ハマるポイントが違うと思うので、公式ドキュメントはもちろんのこと、複数の実装事例の記事を読んでみると良いと思います。 最後まで読んでいただき、ありがとうございました! 参考資料 公式ドキュメント Validating Receipts With the App Store レシート検証プログラミングガイド ブログ 参考になったブログです。ありがとうございます! 自動購読課金について【iOS編】 iOS/Androidアプリ内課金の不正なレシートによる有料会員登録を防ぐ 最後に VASILYでは一緒に開発をしてくれる仲間を募集しています。 興味のある方は是非気軽にオフィス見学にお越しください! https://www.wantedly.com/projects/61389 www.wantedly.com Icon made by Freepik from http://www.flaticon.com/
iOSエンジニアの庄司( @WorldDownTown )です。 iQONのiOSアプリ内部で使われている画面遷移処理をOSSライブラリ化したのでご紹介します。 TL;DR UINavigationController での遷移時に、タップした画像をズームして遷移するトランジション処理をSwiftライブラリ化しました。 エッジスワイプでもズームアウトして戻ることができます。 github.com ライブラリ化した経緯 Pinterestをはじめ、画像がズームインしながら画面遷移するアプリは今や珍しくありません。 この表現を実現するライブラリはいくつか存在しますが、通常の UINavigationController のようにスワイプで戻れなくなったり、スワイプできても通常のスワイプとは違って指の動きに同期しないものが多い印象です。 iQONのアイテム詳細ページではこのジェスチャー周辺の実装がしっかりできているので、OSSとして公開したら需要があるかもと思い、ライブラリ化に踏み切りました。 特徴 エッジスワイプ 通常の UINavigationController と同様に、エッジスワイプで前のViewControllerに戻る事ができます。 上記のアニメーションGIFを見ていただくとわかりやすいと思います。 Objective-Cプロジェクトでも使えます 作成したクラスはすべて Foundation , UIKit のクラスを継承しているため、Objective-Cのコードからも利用できます。 使い方 このライブラリを使って画面遷移のアニメーションを実装するには3つのステップがあります。 UINavigationControllerDelegate設定 画面遷移元のViewController設定 画面遷移先のViewController設定 1. UINavigationControllerDelegate 設定 UINavigationControllerDelegate で画面遷移対象のViewControllerをチェックしてアニメーションをします。 ZoomNavigationControllerDelegate オブジェクトを delegate に設定するだけです。 class NavigationController : UINavigationController { private let zoomNavigationControllerDelegate = ZoomNavigationControllerDelegate() required init ?(coder aDecoder : NSCoder ) { super . init (coder : aDecoder ) delegate = zoomNavigationControllerDelegate } } 2. 画面遷移元のViewController設定 アニメーション対象の UIImageView を返したり、アニメーション中に元の画像を非表示にできるように画面遷移前後の ZoomTransitionSourceDelegate のメソッドを実装します。 extension ImageListViewController : ZoomTransitionSourceDelegate { // アニメーション対象のUIImageViewを返す func transitionSourceImageView () -> UIImageView { return selectedImageView } // スクリーンに対するアニメーション開始位置を返す func transitionSourceImageViewFrame (forward forward : Bool ) -> CGRect { guard let selectedImageView = selectedImageView else { return CGRect.zero } return selectedImageView.convertRect(selectedImageView.bounds, toView : view ) } // 画面遷移直前 func transitionSourceWillBegin () { selectedImageView?.hidden = true } // 画面遷移完了後 func transitionSourceDidEnd () { selectedImageView?.hidden = false } // 画面遷移キャンセル後 func transitionSourceDidCancel () { selectedImageView?.hidden = false } } 3. 画面遷移先のViewController設定 ZoomTransitionSourceDelegate と同様の目的で画面遷移先のViewController向けの設定のため、 ZoomTransitionDestinationDelegate のメソッドを実装します。 extension ImageDetailViewController : ZoomTransitionDestinationDelegate { // 画面遷移完了後、及び、ポップ時のUIImageViewの配置 func transitionDestinationImageViewFrame (forward forward : Bool ) -> CGRect { if forward { let x : CGFloat = 0.0 let y = topLayoutGuide.length let width = view.frame.width let height = width * 2.0 / 3.0 return CGRect(x : x , y : y , width : width , height : height ) } else { return largeImageView.convertRect(largeImageView.bounds, toView : view ) } } // 画面遷移直前 func transitionDestinationWillBegin () { largeImageView.hidden = true } // 画面遷移完了後 func transitionDestinationDidEnd (transitioningImageView imageView : UIImageView ) { largeImageView.hidden = false largeImageView.image = imageView.image } // 画面遷移キャンセル後 func transitionDestinationDidCancel () { largeImageView.hidden = false } } リポジトリに Demo プロジェクトがあるので、そちらもご覧ください。 ライブラリの内部実装 ズームアニメーションの仕組み UIViewControllerAnimatedTransitioning プロトコルを採用した ZoomTransitioning が画像がズームするアニメーション処理を実行しています。 UIViewControllerAnimatedTransitioning による画面遷移アニメーションについては、下記のQiita記事が参考になったので、そちらをご覧ください。 qiita.com スワイプで戻る UIPercentDrivenInteractiveTransition を継承し、 UIGestureRecognizerDelegate を採用した ZoomInteractiveTransition というクラスがスワイプによる画面遷移を実現させています。 let zoomInteractiveTransition = ZoomInteractiveTransition() let gesture = navigationController.interactivePopGestureRecognizer gesture?.delegate = zoomInteractiveTransition gesture?.addTarget(zoomInteractiveTransition, action : #selector(ZoomInteractiveTransition.handlePanGestureRecognizer(_ : ))) UINavigationController の interactivePopGestureRecognizer というプロパティは UIScreenEdgePanGestureRecognizer クラスで、このGestureRecognizerが通常のエッジスワイプで戻る動作を可能にしています。 ZoomInteractiveTransitioning で interactivePopGestureRecognizer のジェスチャー処理を受け取ることで、 ZoomTransitioning が実行するアニメーションを使ってエッジスワイプで戻れるようになります。 // UINavigationControllerDelegate func navigationController (navigationController : UINavigationController , interactionControllerForAnimationController animationController : UIViewControllerAnimatedTransitioning ) -> UIViewControllerInteractiveTransitioning ? { return zoomInteractiveTransition.interactive ? zoomInteractiveTransition : nil } エッジスワイプのジェスチャーを受け取ってナビゲーションを戻るときだけ zoomInteractiveTransition を返して、指の動きを反映したインタラクティブな画面遷移をします。 class ZoomInteractiveTransition : UIPercentDrivenInteractiveTransition { var interactive = false // スワイプで戻るフラグ。ジェスチャーを受け取ったらtrueにする @objc func handlePanGestureRecognizer (recognizer : UIScreenEdgePanGestureRecognizer ) { let view = recognizer.view ! let progress = recognizer.translationInView(view).x / view.bounds.width switch recognizer.state { case .Changed : updateInteractiveTransition(progress) case .Cancelled, .Ended : if progress > 0.33 { finishInteractiveTransition() } else { cancelInteractiveTransition() } default : break } } UINavigationController の interactivePopGestureRecognizer のジェスチャーで呼ばれるメソッドの方では、スワイプする指の位置ごとに、 UIPercentDrivenInteractiveTransition のメソッドを呼び出してアニメーションの進捗を反映させます。そうすると、 ZoomTransitioning のアニメーションが指の位置に合わせて動作します。 複雑な処理に見えますが、もし UIPercentDrivenInteractiveTransition を使わなかった場合、スワイプで戻る時も ZoomTransitionig と同じアニメーション処理を別途実装しないといけません。 さいごに UINavigationController での遷移時に、タップした画像をズームインしながらアニメーションする ZoomTransitioning を紹介しました。 このライブラリは、iQONでの仕様を基に最低限の機能で公開しています。 バグ報告や改善案など (OSS化の真の目的はこれだったり…)、 Pull Request お待ちしております。
こんにちは、iOSエンジニアの遠藤です。 学生の皆さん、夏のインターンシップはもう決めましたか? 各社で様々な形式のインターンシップがあると思いますが、今回はiOSチームを例にVASILYでのインターンシップについて紹介をしたいと思います。 記事には実際にインターンシップに参加した学生の感想を載せていますので、VASILYのインターンシップに興味のある方はぜひチェックしてください! VASILYでのインターンシップについて VASILYのインターンシップの特徴は、なんといっても実際のプロダクトの開発をしてもらうことです。 講義形式やハッカソン形式のインターンシップがあるなか、なぜ実際のプロダクトを開発する形式のインターンシップを行っているのでしょうか? それは、実際のプロダクトを開発する中でエンジニアとしてのスキルだけではなくコミュニケーション力が必要になることや実際にプロダクトを使っているユーザーに価値を届けることの難しさや楽しさを実感してもらいたいと思っているからです。 また、一緒にプロダクトを開発することでVASILYのエンジニアがどういった働き方をしているのかやプロダクトへの思いなども知ってもらいたいと思い、このような形式にしています。 インターンシップの参加条件 インターンシップへの参加条件として、こちらが出す課題をクリアしてもらう必要があります。 プログラミングの勉強に時間をかけるのではなく、ユーザーに価値を届けることに時間を使って欲しいのでVASILYではプログラミングを一から教えるといったことはしません。 実際のプロダクトを開発するので、難しい内容や課題を解決するのにある程度の技術力が必要になってきます。 その中でも、きちんとアウトプットとしてユーザーに価値を提供し成果を上げてもらいたいので、ある程度開発の経験がある方を対象とするために課題を出しています。 また、技術力だけではなく以下のような項目も重要視しています。 プログラミングがとにかく好きか、ものづくりが大好きか 素直で吸収力があるか 技術の力で世の中をもっと便利にしたいと思っているか 数百万人が使うプロダクトの設計や技術、開発スキルに興味があるか インターンシップで学べること VASILYのインターンシップで学べることをiOSを例に紹介したいと思います。 【iOSインターンシップで学べること】 大規模プロダクトでの開発経験 コーディング力 コミュニケーション力 UI実装 大規模プロダクトでの開発経験 インターンシップでは、実際にiQONのプロダクトに関する開発をしてもらいます。 大規模なプロダクトでの設計についてや、コードに対するPull Requestは個人の開発では学べないことを学べます。 コーディング力 小さなコードの変更でも、必ずコードレビューをします。 実装するだけでなくアプリ開発の作法やコードの書き方などしっかりと指摘していくので、コーディング力は上がると思います。 ↓ 過去、インターンに来てくれた学生のPull Requestの様子です。 細部までコードをチェックしていきます。 コミュニケーション力 実装する上で、デザインの確認や仕様の認識を合わせるためにデザイナーや企画の人ともコミュニケーションをとってもらいます。 ただ実装するだけではなく、デザインや企画などの調整もしてもらうのでコミュニケーション力はつくと思います。 UI実装 iQONはデザインにこだわって作られています。 デザイナーが作ったデザインは簡単に実装できるものではありません。 そのデザインを忠実に再現するためにAutoLayoutを駆使して実装をします。 一筋縄では実装できないものが多いので、UIの実装力はあがると思います。 参加者の感想 大阪大学Hくん インターンシップを通して技術的な事は勿論、チームみんなで良いプロダクトを作るために自分は何をすべきなのかを、実際に多くのユーザーがいる大規模なアプリの開発を通して体験しながら学べました。 アプリ開発のメンバーだけではなく、ウェブ開発の人やマーケティングの人、経営側の人とも話をできてとても楽しいインターンシップでした。 早稲田大学大学院Tくん チームとしての開発を経験できるだけでなく、プロダクトに対する考え方も身につくことが出来るため、とても実践的で魅力ある内容でした。これまでの自分は個人での開発がほとんどだったので、VASILYでのインターンシップは得るものが多く、エンジニアとしての自分の糧となりました。 インターンシップからの内定 VASILYでは新卒採用の選考フローにインターンシップへの参加が入っているため、新卒入社するためにはインターンシップの参加が必須になっています。 私自身もVASILYのインターンシップを経て新卒で入社しました。 実際のプロダクトを開発するインターンシップの魅力はその会社の働き方や開発している人たちのプロダクトに対する思いなどを知れることだと思います。それは入社した後に想像と違ったというようなミスマッチを防げます。 最後に iOSチームを例にVASILYでのインターンシップについて紹介しました。 実際のプロダクトに携わることで技術力だけではなく、チームでの開発の進め方やプロダクトに対する考え方を学ぶことができます。 色々な形式のインターンシップがあると思いますが、実際のプロダクトを開発するインターンシップには成長するチャンスがたくさんあります! チーム開発をしてみたい方、プログラミングが好きな方、iQONをつくってみたい方、ぜひともVASILYのインターンシップに応募してみてください。 https://www.wantedly.com/projects/54278 www.wantedly.com
VASILYのiOSエンジニアにこらすです! 今回はプロキシツール mitmproxy の カスタムスクリプト 機能について説明したいと思います。 モバイル開発をする際にAPIリクエストのデバッグツールとして mitmproxy はとても役に立ちます! カスタムスクリプトを使うと何ができるか カスタムスクリプトを書くと、mitmproxyの動作をプログラムでカスタマイズすることができます。 モバイルデバイスのリクエストやレスポンスを書き換えたり、通信を遅延させたりできます。 どうやってスクリプトを書くか mitmproxyはPythonで実装されているので、カスタムスクリプトも python で実装することになります。 mitmproxyにはいろいろなイベントがありますが、各イベントをオーバーライドするメソッドを書くことができます。 例えば、 script_name.py というPythonファイルにオーバーライドしたい処理を書いたなら、mitmproxyの -s オプションでそのファイルを指定するとカスタムスクリプトが動くようになります。 $ mitmproxy -s script_name.py イベントをオーバーライドするメソッドについて mitmproxyのイベントには4つの種類があります。 ライフタイムイベント(スクリプト開始、終了) ネトワーク接続系イベント(クライアントのconnect、disconnect) TCP系イベント HTTPイベント 今回は、APIのデバッグ時によく使う HTTPイベント に注目します。 HTTPイベントメソッドオーバーライド request(context, flow) クライアントがリクエストをする直前にこのメソッドが実行されます。 このタイミングでリクエスト情報をチェックできますし、編集もできます! response(context, flow) このメソッドはサーバーから受け取ったレスポンスを編集することができます。 responseheaders(context, flow) このメソッドはサーバーからレスポンスヘッダを受け取った直後に実行されます。 response(context, flow) より前に responseheaders(context, flow) が実行されることが保証されているので、レスポンスボディを受け取る前にレスポンスヘッダを書き換えることができます。 error(context, flow) エラーが発生した場合にこのメソッドに入ります。 mitmproxyのインラインスクリプトの実例 オーバーライドするための基礎を勉強したので今からちゃんとmitmproxyスクリプトを書いてみましょう! ヘッダを書き換える # リクエストヘッダを編集 def request (context, flow): flow.request.headers[ "language" ] = "jp" # レスポンスヘッダも同じです def response (context, flow): flow.response.headers[ "language" ] = "jp" URLのパスが /users のリクエストだけ強制的に 451 エラーにする def response (context, flow): if "/users" in flow.request.url: flow.response.status_code = 451 60秒レスポンスが返ってこない状況をシミュレートする import time def response (context, flow): time.sleep( 60 ) URLのパスが /users のリクエストのレスポンスのボディを置き換える def response (context, flow): if "/users" in flow.request.url: flow.response.content = b "" # bytesタイプのascii文字列 まとめ 今回はmitmproxyのカスタムスクリプトについて説明しました。 このたった4つのスクリプトを覚えるだけでも、APIの問題を解決できるようになると思います。 もっと詳しい情報や他のカスタムスクリプトについては、mitmproxyの 公式サイト か GitHub を参照してください。 ありがとうございました! ー にこらす
こんにちは、インフラエンジニアの光野(@kotatsu360)です。 開発をしていると本番サーバと開発サーバの乖離が問題になると思います。これについて、先日行われた UZABASE Meetup#4 〜大規模サービスを支えるインフラ〜 にて「1コマンドで本番サーバと開発サーバ (のVMイメージ)を作る話」という発表をさせていただきました。 この記事では、時間とスライドの都合上、省略したbase.jsonについてご紹介いたします。 packer build base.json packerで読み込むjsonは次の4パートに分かれています。 " variables ": { // 変数 } , " builders ": [ // 作成したいプラットフォームごとの設定 ] , " provisioners ": [ // マシンイメージへの初期設定 chef, shell script, ansible ... ] , " post-processors ": [ // 作成したマシンイメージへの後処理 ] 非常にシンプルなのですが、実際に設定していくと細かなパラメータの設定で悩みます。ということで実際に使っているjsonファイル全文をこちらにご用意しました! packer/base.json { " variables ": { " version ": " 1.0.2 ", " role ": " base ", " ami ": " ami-5d38d93c ", " aws_access ": " {{ env `AWS_ACCESS_KEY_ID`}} ", " aws_secret ": " {{ env `AWS_SECRET_ACCESS_KEY`}} ", " gce_source_image ": " ubuntu-1604-xenial-v20160627 ", " gce_secret ": " {{ env `GCE_ACCOUNT_FILE`}} ", " s3_bucket ": " {{ env `AWS_INFRA_S3_BUCKET`}} ", " gce_project_id ": " {{ env `GCE_PROJECT_ID`}} " } , " builders ": [ { " type ": " virtualbox-ovf ", " headless ": " true ", " shutdown_command ": " echo 'ubuntu' | sudo -S shutdown -P now ", " source_path ": " box-source/ubuntu-16.04.ova ", " ssh_password ": " ubuntu ", " ssh_username ": " ubuntu ", " ssh_wait_timeout ": " 20m ", " vboxmanage ": [ [ " modifyvm ", " {{ .Name }} ", " --memory ", " 4096 " ] , [ " modifyvm ", " {{ .Name }} ", " --cpus ", " 2 " ] ] , " virtualbox_version_file ": " .vbox_version ", " vm_name ": " {{user `role`}} ", " guest_additions_mode ": " disable ", " format ": " ova ", " output_directory ": " output-{{build_name}}-{{user `role`}} " } , { " type ": " amazon-ebs ", " access_key ": " {{user `aws_access`}} ", " secret_key ": " {{user `aws_secret`}} ", " source_ami ": " {{user `ami`}} ", " instance_type ": " c3.xlarge ", " region ": " ap-northeast-1 ", " ssh_username ": " ubuntu ", " ami_name ": " packer-ubuntu1604-ruby231-{{timestamp}} ", " ami_regions ": [ " ap-northeast-1 " ] , " ami_description " : " iQON AMI {{user `role`}} Image ", " tags ": { " OS ": " Ubuntu16.04 ", " Ruby ": " 2.3.1 ", " Role ": " {{user `role`}} ", " OriginalAMI ": " ami-5d38d93c " } }, { " type ": " googlecompute ", " account_file ": " {{user `gce_secret`}} ", " project_id ": " {{user `gce_project_id`}} ", " source_image ": " {{user `gce_source_image`}} ", " zone ": " asia-east1-a ", " machine_type ": " n1-highcpu-4 ", " ssh_username ": " ubuntu ", " instance_name ": " packer-{{timestamp}} ", " image_name ": " packer-ubuntu1604-ruby231-{{timestamp}} ", " image_description " : " iQON AMI {{user `Role`}} Image " } ], " provisioners ": [ { " type ": " file ", " source ": " {{pwd}}/../chef-repo ", " destination ": " /tmp/packer-chef-client/ " } , { " type ": " shell ", " inline ": [ " sudo apt-get update ", " sudo apt-get upgrade -y ", " sudo apt-get install -y language-pack-ja curl ", " sudo update-locale LANG=ja_JP.UTF-8 && true ", " sudo ln -sf /bin/bash /bin/sh " ] } , { " type ": " chef-client ", " server_url ": " http://localhost:8889 ", " config_template ": " ../chef-repo/client.rb ", " install_command ": " curl -L https://www.chef.io/chef/install.sh | sudo bash -s -- -v 12.8.1 ", " execute_command ": " sudo chef-client -z -c /tmp/packer-chef-client/client.rb -j /tmp/packer-chef-client/nodes/packer-{{user `role`}}.json ", " guest_os_type ": " unix ", " skip_clean_node ": true , " skip_clean_client ": true } , { " type ": " shell ", " only ": [ " virtualbox-ovf " ] , " inline ": [ " sudo systemctl disable apt-daily.service ", " sudo systemctl disable apt-daily.timer " ] } ] , " post-processors ": [ { " type ": " shell-local ", " only ": [ " virtualbox-ovf " ] , " inline ": [ " rsync --checksum -av output-virtualbox-ovf-{{user `role`}}/ box-source/{{user `role`}} ", " aws s3 sync box-source s3://{{user `s3_bucket`}}/vagrant/box-source " ] } , [ { " type ": " vagrant ", " only ": [ " virtualbox-ovf " ] , " keep_input_artifact ": false , " output ": " packer-output/{{user `role`}}/{{user `role`}}.box ", " override ": { " virtualbox ": { " compression_level ": 0 } } } , { " type ": " vagrant-s3 ", " only ": [ " virtualbox-ovf " ] , " region ": " ap-northeast-1 ", " bucket ": " {{user `s3_bucket`}} ", " manifest ": " vagrant/json/{{user `role`}}.json ", " box_name ": " {{user `role`}} ", " box_dir ": " vagrant/boxes ", " version ": " {{ user `version` }} ", " acl ": " private ", " access_key_id ": " {{user `aws_access`}} ", " secret_key ": " {{user `aws_secret`}} " } ] ] } シークレットや組織固有の部分については環境変数を読み込むようにしています。また実行時に引数として渡す事も可能です。 User Variables in Templates - Packer by HashiCorp base.jsonの中身 packerはinspectというサブコマンドでそのJSONで何が実行されるのかが確認できます。base.jsonを見てみます。 $ packer inspect base.json Optional variables and their defaults: ami = ami-5d38d93c aws_access = {{ env `AWS_ACCESS_KEY_ID`}} aws_secret = {{ env `AWS_SECRET_ACCESS_KEY`}} gce_project_id = {{ env `GCE_PROJECT_ID`}} gce_secret = {{ env `GCE_ACCOUNT_FILE`}} gce_source_image = ubuntu-1604-xenial-v20160627 role = base s3_bucket = {{ env `AWS_INFRA_S3_BUCKET`}} version = 1.0.2 Builders: amazon-ebs googlecompute virtualbox-ovf Provisioners: file shell chef-client shell このbase.jsonでは、9個のユーザ定義変数で動作が制御されており、 ami (EBS-attached) google compute engine image virtualbox ovf (vagrant box用) が作られ、それぞれプロビジョンは file copy remote shellの実行 chef client remote shellの実行 という順番で行われる。ということが分かります。もう少し分解して見ていきます。 variables AWSのトークンやオリジナルAMI (ここではUbuntu16.04) を設定します。 複数回登場する要素はvariablesで定義しておいた方が見通しが良くなります。 シークレットをファイルに含めなくて済むのでGitHubにコミットするときも安全です。 " variables ": { " aws_access ": " {{ env `AWS_ACCESS_KEY_ID`}} ", " aws_secret ": " {{ env `AWS_SECRET_ACCESS_KEY`}} " } , builders Vagrant boxの元となるVirtualBoxと本番で使うAMI/GCE Imageを作っています。 " builders ": [ { " type ": " virtualbox-ovf ", " source_path ": " box-source/ubuntu-16.04.ova ", ... } , { " type ": " amazon-ebs ", ... } , { " type ": " googlecompute ", ... ] AMI/GCE Imageについては見たままです。各プラットフォームが公式で提供しているUbuntu16.04のイメージを使ってインスタンスを立て、後述のプロビジョニングを行い、マシンイメージを保存してインスタンスを削除してくれます。 一方、VirtualBoxについては一手間かけています。 source_path で指定しているovaは Canonicalが提供しているiso を一度VirtualBoxに入れて、OVF2.0でエクスポートしたものです。 Ubuntu14.04だと Canonical公式のbox をtarで解凍したときに出てくるovaをpackerで読み込めたのですが、 Ubuntu16.04のbox から同様に作成したovaは読み込みエラーになったため自分で作成しました。 provisioners " provisioners ": [ { " type ": " file ", " source ": " {{pwd}}/../chef-repo ", " destination ": " /tmp/packer-chef-client/ " } , { " type ": " shell ", " inline ": [ " sudo apt-get update ", " sudo apt-get upgrade -y ", " sudo apt-get install -y language-pack-ja curl ", " sudo update-locale LANG=ja_JP.UTF-8 && true ", " sudo ln -sf /bin/bash /bin/sh " ] } , { " type ": " chef-client ", " config_template ": " ../chef-repo/client.rb ", " execute_command ": " sudo chef-client -z -c /tmp/packer-chef-client/client.rb -j /tmp/packer-chef-client/nodes/packer-{{user `role`}}.json " , } , { " type ": " shell ", " only ": [ " virtualbox-ovf " ] , " inline ": [ " sudo systemctl disable apt-daily.service ", " sudo systemctl disable apt-daily.timer " ] } ], packerで一番ハマったのがこのprovisinersです。大きな流れは、 chefで使うファイルをローカルからVMにコピー 設定をしておかないとそもそもchefが実行できない処理をremote shellで実行 chef clientをlocal modeで実行 virtualbox-ovf 限定で、インスタンス起動時のapt-get updateを停止 ということをやっています。4番目は発表資料の23ページ目で触れているapt-getがchefと衝突するのを避けるためです。 ちなみに、3で参照しているclient.rbの中身はこうなっています。1でコピーしたchef-repoの構造を指示しています。 # coding: utf-8 chef_repo_path " /tmp/packer-chef-client " cookbook_path [ " /tmp/packer-chef-client/site-cookbooks " , " /tmp/packer-chef-client/cookbooks " ] log_location " /var/log/chef-client.log " log_level :info post-processors " post-processors ": [ { " type ": " shell-local ", " only ": [ " virtualbox-ovf " ] , " inline ": [ " rsync --checksum -av output-virtualbox-ovf-{{user `role`}}/ box-source/{{user `role`}} ", " aws s3 sync box-source s3://{{user `s3_bucket`}}/vagrant/box-source " ] } , [ { " type ": " vagrant ", " only ": [ " virtualbox-ovf " ] , ... } , { " type ": " vagrant-s3 ", " only ": [ " virtualbox-ovf " ] , " manifest ": " vagrant/json/{{user `role`}}.json ", ... } ] ] post-processorsはbuildersで作成したVMイメージに対して後処理を行うことができます。ここでは、 virtualbox-ovf の結果を使って、vagrant boxを作成しています。 できたものはS3に保存し、チーム全員で共通のboxを使えるようにしています。この管理方法についてはこちらの投稿を参考にさせていただきました。 Packer で開発環境の Vagrant Box を自作して、post-processors 処理を通して S3 に保存・バージョン管理・ホスティングする - Qiita その他補足 トークンの権限について AWS/GCEのトークンに必要な権限については、公式ドキュメントで丁寧に紹介されていますので上では説明を省略しています。 Amazon AMI - Builders - Packer by HashiCorp Google Compute - Builders - Packer by HashiCorp ビルド対象について packer build base.json と実行すると3つのプラットフォームでビルドが始まりますが、対象を絞ることもできます。 packer build -only='amazon-ebs' base.json この場合、AMIだけビルドが始まります。 packer build - Commands - Packer by HashiCorp AWSのインスタンスタイプについて packerというよりもAWSの話題になりますが、検証中Ubuntu16.04 + {m,c,r}3.largeのインスタンスの場合にカーネルパニックが発生し正常に起動しないという問題がありました。 こちらのissueに近いのですが詳細が確認できず、largeを使わないという対応策を取っています。 Bug #1573231 “Kernel Panic on EC2 After Upgrading from 14.04 to ...” : Bugs : linux package : Ubuntu もう直っているかもしれませんがお気をつけ下さい。 現状の制約 このbase.jsonを使う際、2つ解決できてない問題があります。 一つ目: virtualbox-ovf の一時ファイル packerは一時ファイルが残っていると実行時にエラーが発生します。 基本動作としては消えるはずなのですが、post-provisionersの書き方が悪いのか、ある時から消えなくなってしまいました。 # 2回目の実行 $ packer build base.json virtualbox-ovf output will be in this color. Build 'virtualbox-ovf' errored: Output directory exists: output-virtualbox-ovf-base Use the force flag to delete it prior to building. 手動でディレクトリを消すか、 -force を付けてbuildを実行して下さい。 packer build -force base.json 二つ目:バージョンの手動変更 vagrant boxの管理でmanifest.jsonを内部で生成しており、既に存在するバージョンの場合は最後の vagrant-s3 が失敗します。 " variables ": { " version ": " 1.0.2 ", vagrantの仕様上、 x.x.x の形式にする必要があり、タイムスタンプというわけにもいきません。人間インクリメントなのでなんとかしたいと思っています。 今後やりたいこと まだまだこなれておらず、日々jsonを更新しています。 例えば、EC2に関してはスポットインスタンスを使うよう修正をしているところです。 ハイパフォーマンスのインスタンスが手頃な値段で使えるため、より快適なイメージ更新作業ができると思っています。 まとめ かなり駆け足ではありましたが、VASILYで実際に運用しているpacker用のJSONファイルについてご紹介いたしました。 packerは簡単に始められますが、ちょっと凝ったことをしようと思うとやはりオプションの調整が避けられません。 この記事が何かしら参考になれば幸いです。 逆に「お前のpacker術は間違っている」という部分がありましたら、コメントでご指摘下さい。小躍りして喜びます。 最後に VASILYでは一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 また、VASILYでは今年もエンジニア向けインターンシップを行います。バックエンドチーム(インフラはバックエンドチームに所属しています)でのインターンシップについては、以下のリンクで募集しています。ご興味のあるかたは是非ご応募下さい。
こんにちは、エンジニアの中村( @tn1031 )です。弊社のプロダクト「iQON」には「for You」というレコメンド機能が実装され、個々のユーザに毎日おすすめのファッションアイテムを届けています。 press.vasily.jp 今回はこの「for You」に関連して、レコメンドを実現するアルゴリズムのひとつである Bayesian Personalized Ranking (BPR) を紹介したいと思います。 本記事ではひとつの手法に話題を絞りますが、一般的な協調フィルタリングやレコメンド自体について詳しく知りたい方は、こちらの Netflix Prizeで使われた手法のまとめ がとても参考になります。 協調フィルタリングとBPR 行動ベースの協調フィルタリングではユーザ x アイテムの行列の行列分解(Matrix Factorization)を考えます。 評価の行列を とします。行のインデックス と列のインデックス がそれぞれ1人のユーザ、1個のアイテムと対応しており、行列の 成分の値 はユーザ がアイテム に対して下した評価です。ファクターの個数を与えた時、評価の行列 をユーザ x ファクター行列 とアイテム x ファクター行列 の積に分解します。 これを解く為の手法は様々ありますが、有名なものといえば spark.mllib にも実装されているAlternating Least Squares (ALS)やトピックモデルが一番最初に思い浮かびます。 ALSは観測されたデータと予測した評価値の2乗誤差を最小にするような行列分解を与える手法であり、トピックモデルは観測されたデータの背後に確率分布を仮定し、分布のパラメータを求める事で行列分解を行います。 BPRも行列分解を与える部分は同じですが、上記の手法とは異なるアプローチをとります。 そもそもPersonalized Rankingは、ユーザごとの趣味趣向をランキングとして学習します。アイテムリストをユーザの好みでソートしたリストは、結果としてそのユーザに対するレコメンドになっているということです。BPRはPersonalized Rankingを解くための枠組みであり、今回紹介するのはこの手法を行列分解に適用した場合のアルゴリズムです。 BPRの詳細 ユーザ x アイテム の行列の行列分解の問題をBPRで解くことを考えます。 はじめに扱うデータを定義します。続いてベイズ的アプローチによって問題を定式化し、パラメータの更新式を導出します。 データ BPRで扱う学習データは以下のように表現します。 はすべてのアイテムの集合、 はユーザ が好む(評価が正の)アイテムの集合、 は から を除いたものの集合です。したがって、 の意味は、「ユーザ はアイテム よりアイテム を好む」となります。 のサイズは になります。すべてのデータを学習に用いることは不可能なので、BPRでは学習データを与えられたデータからサンプリングします。 # sample a user u = np.random.randint(userCount) itemList = trainMatrix.getrowview(u).rows[ 0 ] if len (itemList) == 0 : continue # sample a positive item i = random.choice(itemList) # sample a negative item j = np.random.randint(itemCount) while trainMatrix[u, j] != 0 : j = np.random.randint(itemCount) 定式化 分解後の行列を (ただし、 はファクターの数)、ユーザ についての全アイテムの順序を 、 と表記すれば「ユーザ はアイテム よりアイテム を好む」ことを表すとします。 尤度関数は、 と定義します。ここで、 です。 の事前分布を を単位行列として で定義すると、事後確率最大化の式は以下で与えられます。 この式を最大化する を求めることがBPRの目的となります。 第1項で好きなアイテムに関する予測値とそうでないアイテムに関する予測値の差を大きくするように学習します。第2項、第3項は正則化項として機能します。 更新式 勾配法で を求めます。 とおくと、 を学習率として、 となります。 として を代入すると、最終的に以下のようになります。 ただし、 です。 ここまでをコードに起こします。 # BPR update rules y_pos = np.dot(W[u], H[i]) # target value of positive instance y_neg = np.dot(W[u], H[j]) # target value of negative instance exp_x = np.exp(-(y_pos-y_neg)) mult = -exp_x / ( 1.0 + exp_x) for f in xrange (factors): grad_u = H[i, f] - H[j, f] W[u, f] -= lr * (mult * grad_u + reg * W[u, f]) grad = U[u, f] H[i, f] -= lr * (mult * grad + reg * H[i, f]) H[j, f] -= lr * (-mult * grad + reg * H[j, f]) アルゴリズム データのサンプリングとパラメータの更新を収束するまで交互に繰り返します。 repeat 1. (u,i,j)のサンプリング 2. W,Hの更新 until convergence コードの全量は GitHub に公開しておきます。 なお、上記のコードをそのまま実行するとあまりに遅かったため、一部実装を見直しました。 そのときの試行錯誤は Qiita に書いたのでよろしければご覧ください。 BPRの魅力 BPRの魅力は何と言っても計算コストです。 をそれぞれユーザ数、アイテム数、評価の数、ファクターの数とすると、例えばALSは 、速いものだと であるのに対し、BPRは で計算できます。 また、目的関数やアルゴリズムがシンプルで拡張を考えやすい事もメリットといえます。例えば、行動データの他にアイテムの画像情報をモデルに取り入れたいときは こちらの論文 で提案されているような拡張が考えられます。 一方、ALSに比べると並列化が難しいです。ファクター毎であれば可能ですが、ALSの様に全ユーザ/アイテムに対して並列計算させるのは大変かもしれません。 計算資源を惜しみなく投入できる環境であれば、分散環境下において近似なしで計算可能なALSは非常に強力ですが、頻繁に更新したい・1回あたりの計算コストを(金額的にも時間的にも)抑えたいという要件があればBPRも選択肢に含まれると思います。 まとめ 軽量なレコメンドアルゴリズムのひとつであるBPRを紹介しました。Personalized Rankingをベイズ的に定式化したもので、行列分解に適用することでレコメンドを達成します。 最後に VASILYでは一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 また、VASILYでは今年もエンジニア向けインターンシップを行います。データサイエンスチームでのインターンシップについては、以下の記事で紹介していますので、ご興味のあるかたは是非ご覧ください。 tech.vasily.jp
エンジニアの荒井です。現在VASILYでは サマーインターンシップ を開催しています。募集開始後、さっそく多くの方からご応募いただいています。 インターンコースのひとつにフロントエンド開発コースがあるのですが、HTMLを書くのか、サーバーサイド言語を書くのか等、業務範囲に興味がある方が多いようです。そこで今回は、VASILYフロントエンドチームの役割と、インターンシップの内容について紹介したいと思います。 VASILYでのフロントエンドエンジニアの役割 はじめにVASILY内でのフロントエンドエンジニアの役割についてご紹介します。 フロントエンドエンジニアの役割は会社によって様々だと思いますが、VASILYのフロントエンドエンジニアは以下の技術を用いてプロダクト開発をする役割を担っています。 Ruby JavaScript HTML CSS フロントエンドエンジニアと聞いて、RubyやPHPといったサーバサイド言語を扱わないとイメージする方、サーバーサイド言語を用いるがHTMLやCSSはデザイナーがコーディングするというイメージを持って応募される方も多いですが、VASILYではフロントエンドエンジニアが両方のコーディングを担当しています。 もちろんインターンシップでも幅広く経験して頂こうと思っていますので、「Webサービスを作ってみたい!」と思われている学生の方にオススメです。 フロントエンドインターンシップの特徴 フロントエンドチームのインターンシップでは、iQONのPC/スマートフォンサイトの開発を担当していただきます。VASILYのメイン事業であるiQONの開発を実際に行い、インターンシップ中に本番環境へのデプロイまでを体験できます。短期のインターンシップで大規模なサービスに触れることが出来るのが大きな特徴のひとつです。 また、VASILY内でのフロントエンドエンジニアは、デザイナーやディレクターとのやり取りが多く行われる職種でもあります。エンジニア以外とのチーム開発に興味がある方にはとても良い経験になるはずです。 参加条件 フロントエンドチームのインターンシップ参加には課題の提出が必須となります。 内容は簡単なWebアプリケーション作成です。約10日間という短いサマーインターンシップ期間にデプロイまで経験していただくということから、課題を設けています。 作成したアプリケーションとソースコードを確認し、インターンシップ受け入れの合否が出ます。課題が問題なくクリア出来れば、Web業界での経験が無い初心者でも構いません。 過去のインターンシップ事例 過去に多くの学生に参加していただきましたが、2つほど実例を紹介します。 スマートフォンサイトのリニューアル PCトップページのリニューアル どちらの学生にもリニューアルという大きな開発を経験していただきました。 現在のPCトップページはインターンシップの学生が作成したものです。 インターンシップでは1人以上のメンターがつくので、やりがいのある仕事を、しっかりサポートを受けながら、経験することができます。 インターンシップからの内定 実は上記で紹介したリニューアルを経験した2名ですが、今ではVASILYのフロントエンドチームとして大活躍しています。短期インターンシップが終わった後、長期インターン、アルバイトを経験し選考へ進みました。 しっかりとした就業体験をしているので、企業と学生のミスマッチが無いのが内定直結型のインターンの良い所だと思います。 最後に 今回はフロントエンドチームのインターンシップの紹介をしました。 インターンシップで実際の業務を経験することで、普段VASILYのエンジニアがどのように仕事をしているか、企業文化やメンバーの雰囲気まで感じてもらえるはずです。 Webエンジニアを目指している方は、是非この夏VASILYのサマーインターンシップにご応募ください。 https://www.wantedly.com/projects/54278 www.wantedly.com
データサイエンスチームの後藤です。 学生のみなさんはそろそろ夏のインターンの時期ですね。 私も、ちょうど一年前に学生の立場でVASILYのインターンに参加して熱い夏を過ごしたことを思い出します。 本記事では、データサイエンスチームの実際の仕事と夏のインターンについてご紹介します。 記事の最後に、インターン募集の案内も貼っていますので、インターンに参加したいと思ってくれた方はぜひチェックしてください! VASILYのインターンの特徴 エンジニア向けのインターンでは、VASILYのプロダクトであるiQONに直接関わる開発を行っていただきます。よくあるコンペティション形式やハッカソン形式のようなものではなく、メンターと一緒に、実際のプロダクトで動いているコードを触りながら開発を進めていきます。私たちはユーザーに価値を届けることをとても大事にしていますので、インターン生にも、最後まで責任をもってアウトプットの質を高めてユーザーに届けてもらいます。作りっぱなしではなく、必ずユーザーの反応が返ってくるのでやり甲斐があります! インターンへの参加方法 VASILYのインターンに参加するためには、こちらが出す課題を解いてもらいます。課題は汎用的な内容で、とあるデータセットで推薦システムを作るというものです。詳細は応募してからのお楽しみですが、インターンで取り組んでもらう仕事の、準備研究に位置付けています。 課題では、問題設定の理解、アルゴリズムの実装など様々なスキルを測ります。これらはデータサイエンティストの実務での必須能力なので、課題に取り組む中でスキルをどんどん磨いてみてください! データサイエンティストの業務内容 インターン中はデータサイエンスチームと共に過ごすので、社員の業務に触れることもあるでしょう。 VASILYのデータサイエンティストの業務は大きく分けて「データ分析」と「研究開発」の二つがあります。 データ分析 データを目的に応じて適切に集計・可視化していきます。VASILYではユーザーの行動データ、商品データ、検索キーワードなど多種多様のデータを保有しています。それらを分析して仮説の検証、営業資料の根拠、異常検知に利用し、それに続く意思決定を支援します。エンジニアだけでなく、営業チーム、ビジネスチーム、経営陣などすべての部署と直接関わっていきます。これは大企業のデータサイエンティストと異なる部分かもしれませんね。現在は、各部署が追っている200を超える指標を常時最新の状態に保ち、社内に共有して役立てています。 日々追っている指標をTableauで可視化した例 研究開発 VASILYではiQONの新サービスの開発・改善のプロジェクトを複数進めています。目的の機能をつくるために、アルゴリズムの選定・実装をし、実データで検証、最終的に実際に稼働するサービスに仕上げていきます。開発言語は主にPythonを用いますが、webとの連携が強い部分はRubyを使って開発をすることもあります。 例えば、機械学習の国際会議の一つ、ICML2015で発表された多腕バンディッドのアルゴリズムがサービスとして稼働しており、Rubyでの実装も GitHub で公開しています。他にもiQONのサービスの裏側では、ディープラーニング、協調フィルタリング、LDAなどの機械学習の手法を活用しています。 今回のインターンでは、データサイエンスチームの一員として、研究開発の仕事をしてもらいます。定期的なミーティングや突発的に起こる議論にも参加していただきますので、本当の就労体験ができるでしょう。 私たちがどのような思いで仕事をしているのかに答えた記事もありますので、合わせてご覧ください! ビッグデータがあなたのファッションセンスを丸裸にする!ファッションアプリiQONを駆動させる最新データサイエンスの世界 去年の参加者の声 慶應義塾大学 Nさん VASILYのインターンでは実際に使われているデータを使って、実際のサービスに活かす分析が求められるので、非常に実践的で、分析の手法やツールはもちろん分析に対する考え方の面で大変勉強になりました。何よりファッションの大規模データという普段触れないデータを思い切り分析することができてとても楽しかったです! 東京大学大学院 Gさん VASILYのデータはとても整理されていて、分析がしやすい環境が整っていました。環境について不満がない分、自分の知識の足りなさ、実装力など今後の課題も浮き彫りになりました。ユーザーに価値を届けるというマインドの面でも学ぶことが多く、今後の仕事選びの指針になりそうです。エンジニアだけでなく、営業や雑誌編集をしている方などいろんな部署の人と机を並べて仕事ができたのはとても楽しく、かけがえのない体験でした! インターンからの内定 VASILYの新卒採用はインターンへの参加が必須としていますので、インターンはいわゆる内定直結型といえるでしょう。実際に、私は去年の夏のインターンを経て、VASILYに入社しました。 インターンでは、どっぷりiQONの開発に携わるので、マインドやスキルセットが合っているかを見極める良いチャンスです。内定者アルバイトの制度もありますので、入社後のミスマッチも少ないかと思います。 中途社員の経歴も、外資系コンサル、大手通信会社や大手ゲーム会社などなど様々なバックグラウンドのメンバーが揃っており、いろいろな立場の話が聞けます。私がベンチャー感の特に強いVASILYへの入社を決めたのも、社員の率直な意見をたくさん聞けて信頼できたからです。インターン参加中は、いろいろな部署の社員と話せるチャンスです。VASILYで働く人たちになんでも聞いてしまいましょう。 事前に知っておくといい知識 いまでは当たり前のように使っていますが、学生の頃には知らなかった便利なサービスやツールを紹介します。 仕事で触っているうちに使えるようになりますが、事前に動かせるようにしておくと良いスタートダッシュを切ることができるでしょう! GitHub ( https://github.com/ ) コードのバージョン管理をするサービスです。このサービスを使えば、書いたコードを公開したり、コードに変更が加えられた際、誰がどのように変更・追加・削除したのかを管理することができます。様々なコマンドがありますが、実際に触って覚えるのが良いでしょう。 Slack ( https://slack.com/ ) 情報共有ツールのひとつで、VASILYでは部署やプロジェクトごとにチャンネルを作り、チャットで情報を共有します。 ほかにも、アイディアを書き溜めておくチャンネルやBOTがニュースを集めてくるチャンネルなどさまざまなカスタマイズに対応できます。大学の研究室でも使っているところがあるようです。 BigQuery ( https://cloud.google.com/bigquery/what-is-bigquery ) Google Cloud Platformが提供するデータ分析ツールです。ギガバイト単位の大量のデータを圧倒的な速度で集計し抽出することができます。簡単な集計だけで済んでしまうような分析はBigQueryだけで完結させることもできます。最近、結果をそのままSpreadsheetに吐き出せるるようになり、ますます便利になりました。 VASILYではほぼすべてのデータがBigQueryに同期されており、データの結合や集計が素早くできるようになっています。 SQLの書き方を学んで必要な情報を抽出できるようになっておくと、データ分析の前処理がとても早くなります。 Google Cloud Dataproc ( https://cloud.google.com/dataproc/ ) データサイエンティストの業務にはコンピュータリソースが欠かせませんが、業務によって必要となるスペックが異なる場合が多いです。そんなとき、さまざまなスペックの分散クラスタを90秒で立ち上げ、分単位の課金で借りることができるDataprocが便利です。VASILYではスペックの高い分散クラスタを30分だけ起動させて、並列計算を一気に済ませてかかる料金を節約する、といった使い方もしています。 Tableau ( http://www.tableau.com/ja-jp ) コードを書かずにテンポよくデータを可視化することができる便利なツールです。BigQueryやGoogle Analyticsとの連携もでき、自動でデータを抽出して図を最新の状態に保つことができます。個人的に研究ではPythonのMatplotlibを使って作図していましたが、Tableauを使えば圧倒的に時間を節約できます。自動更新もしてくれるので、一度作ってしまえば、営業やマーケターが毎日データをとってきてExcelにコピペするという作業もなくすことができます。 アカデミック版 もあるので学生は気軽に使い始めることができます。 最後に 今回はVASILYのデータサイエンスインターンの紹介をしました。 VASILYではインターンを通年募集していますが、時間が取れる夏休みが絶好のチャンスです! 機械学習やプログラミングが得意な方、iQONが好きで開発してみたい方など、成長できる環境を用意していますので、是非VASILYのインターンに応募してみてください。 夏のインターンはデータサイエンスチームだけでなく、全部署で募集しています!
こんにちは、神崎( @tknzk )です。ElasticBeanstalk w/ multi-container Docker で構成しているad-serverのdocker image を alpine linuxベースのimageに置き換えました。 alpine linuxは、非常に軽量なdistributionで、DockerHubに登録されているmiddlewareなどの公式のdocker imageでも採用が進んでいるOSです。 http://www.alpinelinux.org/ 以前の ブログ にも書いたとおり、ad-serverは ElasticBeanstalkで管理された multi-containerなdockerでクラスタを組んで、アプリケーションを稼働させています。その構成は、下記のようになっています。 本体のアプリケーションがはいったContainer (ad-server) webのリクエストを受け付けるためのnginx logコレクタとしてのtd-agent 監視用のmackerel-agent とあるタイミングの docker imageのサイズは下記のようになっており、docker imageの肥大化がすすんでいました。 image size base os ad_server 924.6MB centos:6 nginx 134.1MB debian:jessie td-agent 448.4MB centos:6 mackerel-agent 423.1MB ubuntu:14.04 肥大化を抑制するための方針として、できるだけ軽量なOSをベースにすること、不必要なパッケージをインストールしないことやbuildするときにだけ必要なパッケージを適宜削除することとして、imageを作成することにしました。 nginx まずは、オフィシャルのimageが対応していたnginxをalpineベースのものに変更しました。 Dockerfileは下記のようになり、FROMとしてオフィシャルのalpineベースのものを指定しています。 FROM nginx:1.11.1-alpine MAINTAINER Takumi Kanzaki COPY nginx.conf /etc/nginx/nginx.conf ad-server ad-sereverはベースとなるruby, supervisord, mysqlをbuildしたimageに ad-server として必要なGemをinstallする imageをbuildするという構成になっていました。alpineをベースにするにあたり、下記のような調整を行いました。 前段のベースとなるdocker image ruby buildに必要なpackageはtemporaryとしてinstallしてuninstall supervisord Dockerfile FROM alpine:3.4 ENV HOME /root WORKDIR /tmp # skip installing gem documentation RUN mkdir -p /usr/local/etc \ && { \ echo 'install: --no-document'; \ echo 'update: --no-document'; \ } >> /usr/local/etc/gemrc # versions ENV RUBY_MAJOR 2.3 ENV RUBY_VERSION 2.3.1 ENV RUBY_DOWNLOAD_SHA256 b87c738cb2032bf4920fef8e3864dc5cf8eae9d89d8d523ce0236945c5797dcd ENV RUBYGEMS_VERSION 2.6.3 ENV BUNDLER_VERSION 1.12.5 # some of ruby's build scripts are written in ruby # we purge this later to make sure our final image uses what we just built RUN set -ex \ && apk add --no-cache --virtual .ruby-builddeps \ autoconf \ bison \ bzip2 \ bzip2-dev \ ca-certificates \ coreutils \ curl \ gcc \ gdbm-dev \ glib-dev \ libc-dev \ libffi-dev \ libxml2-dev \ libxslt-dev \ linux-headers \ make \ ncurses-dev \ openssl-dev \ procps \ # https://bugs.ruby-lang.org/issues/11869 and https://github.com/docker-library/ruby/issues/75 readline-dev \ ruby \ yaml-dev \ zlib-dev \ && curl -fSL -o ruby.tar.gz "http://cache.ruby-lang.org/pub/ruby/$RUBY_MAJOR/ruby-$RUBY_VERSION.tar.gz" \ && echo "$RUBY_DOWNLOAD_SHA256 *ruby.tar.gz" | sha256sum -c - \ && mkdir -p /usr/src \ && tar -xzf ruby.tar.gz -C /usr/src \ && mv "/usr/src/ruby-$RUBY_VERSION" /usr/src/ruby \ && rm ruby.tar.gz \ && cd /usr/src/ruby \ && { echo '#define ENABLE_PATH_CHECK 0'; echo; cat file.c; } > file.c.new && mv file.c.new file.c \ && autoconf \ # the configure script does not detect isnan/isinf as macros && ac_cv_func_isnan=yes ac_cv_func_isinf=yes \ ./configure --disable-install-doc \ && make -j"$(getconf _NPROCESSORS_ONLN)" \ && make install \ && runDeps="$( \ scanelf --needed --nobanner --recursive /usr/local \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u \ )" \ && apk add --virtual .ruby-rundeps $runDeps \ bzip2 \ ca-certificates \ curl \ libffi-dev \ openssl-dev \ yaml-dev \ procps \ zlib-dev \ && apk del .ruby-builddeps \ && gem update --system $RUBYGEMS_VERSION \ && rm -r /usr/src/ruby # SETUP pip supervisord RUN apk add --virtual .supervisord-deps --update \ python \ py-pip && \ pip install -q --upgrade "meld3==1.0.0" "supervisor" 2> /dev/null # SETUP bundler RUN gem install bundler --version "$BUNDLER_VERSION" # SETUP ssl certificatate file RUN ln -s /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem 後段のad-serverのアプリケーション用の docker image Gemのinstall native extension の build に必要なpackage を install/uninstall mysqlの必要ものだけ残して不要なバイナリは削除 Dockerfile #vim: set ft=ruby FROM quay.io/vasilyjp/ruby:2.3.1-alpine_3_4-build ENV LANG ja_JP.UTF-8 # --- SETUP: rubygems --- ADD Gemfile /tmp/Gemfile ADD Gemfile.lock /tmp/Gemfile.lock ENV GEM_HOME /tmp/ad_server/bundle # SETUP middleware deps # build-base : native exetension build # libgsasl : gem memcached # cyrus-sasl-dev : gem memcached # mariadb-dev : gem mysql2 # linux-headers : gem raindrops RUN apk add --virtual .middleware-deps --update \ mariadb-dev \ libgsasl \ cyrus-sasl-dev && \ apk add --virtual .gem-build-deps --update \ build-base \ linux-headers && \ cd /tmp && \ bundle install --clean --jobs=4 && \ apk del .gem-build-deps && \ rm /usr/lib/libmysqld* && \ rm /usr/bin/mysql* # すべての.bundle/configを無効化して、環境変数によって設定を反映させる ENV BUNDLE_IGNORE_CONFIG 1 ENV BUNDLE_GEMFILE /var/app/Gemfile ENV BUNDLE_DISABLE_SHARED_GEMS 1 ENV BUNDLE_JOBS 4 ENV BUNDLE_PATH /tmp/ad_server/bundle VOLUME /var/app WORKDIR /var/app EXPOSE 3000 CMD ["supervisord"] td-agent alpineをベースにすることを検討しましたが、td-agentのbuildが難しく断念しましたが、CentOS:7 にすることで、多少のimage sizeの削減ができました。 # vim: ft=Dockerfile FROM centos:7 ADD td.repo /etc/yum.repos.d/treasuredata.repo RUN rpm --import https://packages.treasuredata.com/GPG-KEY-td-agent && \ yum -q -y install --enablerepo=treasuredata td-agent && \ yum update -q -y \ nss-tools \ nss-util \ nss-softokn-freebl \ nss-softokn \ nss \ bind \ bind-libs \ bind-utils \ openldap \ libuser \ pam \ libssh2 \ libxml2 \ openssl \ sqlite && \ yum clean all CMD [ "td-agent", "-c", "/etc/td-agent/td-agent.conf", "--use-v1-config" ] mackerel-agent aplineベースでmackerel-agent, mackerel-agent-plugin, check-plugins をbuildするものを作成しました。 pull request を投げていますが、コメントにも書いてる通り、alpineのバグがあり一部のpluginが動かない状態です。 alpineで動かすのは厳しいことから、ubuntuベースで 不要なmackerel-agent-pluginを削除し、check-pluginは利用していないのでinstall自体をやめることにして、sizeの削減を行いました。 FROM ubuntu:14.04 # setup mackerel-agent RUN apt-get update \ && apt-get -y install curl sudo ruby docker.io \ && curl -fsSL https://mackerel.io/assets/files/scripts/setup-apt.sh | sh \ && apt-get update \ && apt-get -y install mackerel-agent mackerel-agent-plugins \ && apt-get clean \ && rm -rf /usr/bin/mackerel-plugin-apache2 \ && rm -rf /usr/bin/mackerel-plugin-conntrack \ && rm -rf /usr/bin/mackerel-plugin-elasticsearch \ && rm -rf /usr/bin/mackerel-plugin-gostats \ && rm -rf /usr/bin/mackerel-plugin-haproxy \ && rm -rf /usr/bin/mackerel-plugin-jmx-jolokia \ && rm -rf /usr/bin/mackerel-plugin-jvm \ && rm -rf /usr/bin/mackerel-plugin-mailq \ && rm -rf /usr/bin/mackerel-plugin-munin \ && rm -rf /usr/bin/mackerel-plugin-php-apc \ && rm -rf /usr/bin/mackerel-plugin-php-opcache \ && rm -rf /usr/bin/mackerel-plugin-plack \ && rm -rf /usr/bin/mackerel-plugin-postgres \ && rm -rf /usr/bin/mackerel-plugin-rabbitmq \ && rm -rf /usr/bin/mackerel-plugin-snmp \ && rm -rf /usr/bin/mackerel-plugin-squid \ && rm -rf /usr/bin/mackerel-plugin-td-table-count \ && rm -rf /usr/bin/mackerel-plugin-trafficserver \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* ADD startup.sh /startup.sh RUN chmod 755 /startup.sh # boot mackerel-agent CMD ["/startup.sh"] 現在のproduction環境の docker images 上記のように、ベースのOSを変更したり、 Dockerfileを工夫したりをして、docker imageのsizeを削減することができました。現在のproduction環境で動かしているimageの一覧は下記の通りです。 image size base os ad_server 342.7MB alpine:3.4 nginx 59.63MB alpine:3.4 td-agent 430.8MB centos:7 mackerel-agent 357.5MB ubuntu:14.04 [ec2-user@ip-xx-xx-xx-xxx ~]$ sudo docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE quay.io/vasilyjp/mackerel-agent 0.31.1-ubuntu-14_04_20160617 32eca0f192e6 4 days ago 357.5 MB quay.io/vasilyjp/ad_server 9c2aa373ff9f9c28aa162207b1c4511eb2dacf47 4d76a297c1d0 4 days ago 342.7 MB quay.io/vasilyjp/nginx 1.11.1-alpine_3_4-build 7a4a5e149521 8 days ago 59.63 MB quay.io/vasilyjp/td-agent 0.12.20-centos7 fa60e55fb621 4 weeks ago 430.8 MB amazon/amazon-ecs-agent latest 46e05d110968 5 months ago 9.097 MB まとめ すべてのdocker imageが450MB以下になり、トータルでは既存の6割程度のサイズに落とすことができました。 本当に必要なものだけを指定して構築することで、不要な物がなくなり、セキュリティ的にも安心できる構成が取れたかと思います。 mackerel-agentのところでも触れたように、alpineは少しbuggyなところもありますが、4月末からad-serverのproduction環境に投入し、先日3.4系への移行も行いましたが特に問題なく稼働できています。 最後に VASILYでは、一緒に開発をしてくれる仲間を募集しています。 Dockerを使った開発/運用をしてみたい方は以下のリンクをご確認ください!
こんにちは、エンジニアの堀江( @Horie1024 )です。先日行われた Android Testing Bootcamp #2 で「AndroidのCI環境をCircleCIからWerckerにした話」という内容で発表させて頂きました。発表に使用したスライドはこちらになります。 この投稿では、スライドでは単にリンクを貼って終わらせてしまったなど、詳細を紹介しきれなかった点についてご紹介しようと思います。 移行前に利用していたCircleCIによるCI環境について スライドでも紹介しましたが、iQONの開発では、1年半ほど前からCircleCIを導入していました。導入についての詳細は以下の投稿にまとめてあります。 tech.vasily.jp CircleCIで行っていたことは以下の通りです。 ユニットテスト BetaでのAPK配布 Google Playへのアップロード自動化 図にすると以下のようになります。 ユニットテスト ユニットテストは Robolectric を使いJVM上でのテストを実行しています。使用しているテスティングフレムワークは JUnit 、モックライブラリは mockito です。現状カバレッジは高く無く、モデル部分相当するクラスについて必要に応じて書いています。 UIテスト AWS DeviceFarm を利用して Calabash で書いたテストを実行していました。 tech.vasily.jp Calabashを選択した理由は、CucumberがサポートされGherkinでfeature(テストコード)を書くことができるのが理由です。以下は、ログイン後に画面を目的のコンテンツ位置までスクロールし、ログアウトするfeatureです。自然言語に近い文法で書くことができるため理解しやすくなっています。 ただ、コスト的な問題と Cloud Test Lab(現Firebase Test Lab) がCalabashでのテストの実行をサポートして無いことから、現在では Espresso を利用し実機を使用してローカルでテストを実行しています。 Espresso Test Recorder がGoogle I/O 2016で発表されていますし、EspressoでのUIテストがより行いやすくなるのを期待しています。 APK配布 APKの配布について当初はDeployGateを利用していましたが、Crashlytics(現Fabric)を利用していたこと、iOSがBetaを採用したこともあり、Betaに変更しています。 Google Playへのアップロード自動化 Google Playへのアップロード自動化については、当初PythonのGoogle APIs Client Libraryを使用する予定でした。 qiita.com CircleCIでPythonのGoogle APIs Client Libraryを使用するには、各種ライブラリのインストールや依存関係の解決などを行わなければならず、より簡単な方法を探していたところ gradle-play-publisher プラグインを発見し、現在でも利用しています。以下のスライドでは、CircleCIでののgradle-play-publisherプラグインの利用方法について言及しています。 Androidのビルド用Dockerイメージの作成 Werckerを利用するには、Dockerイメージが必要になります。 Androidプロジェクトをビルド可能なイメージはDockerHubなどで公開されていますが、 Android SDKをDockerイメージに含めてpublicで公開すると 再配布 としてライセンス違反となることと、CIサービス側とローカル環境とのビルド環境のズレを制御するためにイメージを自作しプライベートリポジトリとしてホストしています。SDK VersionやBuild Tools Versionを変更する度Dockerfileを更新する必要がありますが、DockerHubやQuay.ioにはGitHubへのpushをhookしてイメージのビルドを自動的に行うAutomated Buildという機能が用意されているのでメンテナンスコストはほぼ掛かっていません。 qiita.com ビルドしたイメージは弊社のADサーバーで利用するDockerイメージと同様に Quay.io にプライベートリポジトリとしてホストしており、プライベートリポジトリの場合wercker.ymlでのboxセクションの書き方が若干変わります。 boxセクションでプライベートリポジトリを指定する方法は、 こちらのドキュメント が参考になります。そして、yamlは以下のようになり、idにはリポジトリ名、username、passwordについては、WerckerのWeb UIで入力した環境変数を参照するようにします。 box : id : quay.io/knuth/golang username : $USERNAME password : $PASSWORD tag : beta registry : quay.io build : steps : - script : name : echo code : echo "hello world!" また、DockerHubのPrivate Repositoryの場合以下のようなyamlになります。DockerHubの場合registryの指定は不要です。 build : box : id : guido/python username : $USERNAME password : $PASSWORD tag : latest steps : - script : name : echo "hello world!" より詳しい内容は以下の記事にまとめてあります。 qiita.com keystoreや.p12キーファイルの扱い AndroidアプリのCI/CDをどう行うのかを考えるとkeystoreやkey.p12などのファイルを何処に置くか?が問題になることがあります。今回、keystoreなどのファイルをIP制限をかけたs3のバケットに置き、それをWerckerから取得する方法で解決しました。Werckerが使用するIPアドレスをWhitelistとしてs3バケットのバケットポリシーに追加することで、Werckerからのアクセスのみに制限できます。WerckerのソースIDリストは こちら から確認できますが、予告無しに変更される可能性があるため注意が必要です。 qiita.com Wercker cache Werckerでは、Workflowsで繋いだpipeline間で共有出来るディレクトリがあり、キャッシュとして利用できます。キャッシュディレクトリのパスは、環境変数 WERCKER_CACHE_DIR を参照することで取得できます。Wercker cacheの詳細は以下のドキュメントをご覧ください。 http://devcenter.wercker.com/docs/pipelines/wercker-cache.html ビルド速度の改善 WERCKER_CACHE_DIR をGradleのキャッシュディレクトリとして利用することでビルド速度を改善できます。Gradleは実行時に --project-cache-dir を用いることで任意のキャッシュディレクトリを指定できます。詳細は 付録D Gradle コマンドライン をご覧ください。したがって、以下のように指定することで WERCKER_CACHE_DIR をキャッシュディレクトリとして利用できます。 $ ./gradlew --project-cache-dir=$WERCKER_CACHE_DIR testDebug Gradle Wrapperを利用する場合 Gradle Wrapperを利用する場合は、Werckerの環境変数として GRADLE_USER_HOME を定義し $WERCKER_CACHE_DIR を指定します。これでWrapperがダウンロードしたGradleを異なるpipeline間で共有でき、pipeline毎にGradleがダウンロードされることが無くなります。 環境変数は「Settings」の「Environment variables」から定義できます。 WERCKER_CACHE_DIRの有効期限 各pipelineは、スタート時にWERCKER_CACHE_DIRにキャッシュをロードします。ロードされるキャッシュは、14日間以内で最後に成功したビルドのもので、キャッシュの容量は1GBまでとなっています。 キャッシュの消去 キャッシュを消去したい場合、「Settings」の「Options」にある「Clear cache」から削除できます。また、Gradle実行時に --recompile-scripts を付けることでキャッシュが全て破棄され再度コンパイル、保存されます。詳細は 「キャッシング」 の項目をご覧ください。 まとめ 今回Android Testing Bootcampに参加してみて、様々な取り組みや事例、tipsについて知ることができ非常に勉強になりました。また、発表する機会を頂いたことで自分自身の知識を整理することができ、CI/CDを含めた今後のiQONの開発フローをどのようにしていくのかのヒントを多く得られました。次回のAndroid Testing Bootcampへも是非参加できればと思います。 最後に VASILYでは一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 また、VASILYでは今年もエンジニア向けインターンシップを行います。Androidチームでのインターンシップについては、以下の記事で紹介していますので、ご興味のあるかたは是非ご覧ください。 tech.vasily.jp
Androidエンジニアのnissiyです。学生のみなさん!インターンシップに参加していますか? 近年インターンシップに参加する学生が増えているそうですが、VASILYでも2014年からエンジニア向けインターンシップのプログラムを組んで学生を受け入れています。 募集は通年行っていますが、まとまった時間が取れる夏休みを利用して参加される方が多い傾向にあります。 今回は、昨年のAndroidチームを例にVASILYでのインターンシップの紹介をしたいと思います。 VASILYのインターンシップの特徴 VASILYのエンジニア向けインターンシップでは、すべてのチーム例外なく社員と一緒にiQONに関わる開発を行ってもらいます。 インターンシップ期間中に書いたコードは本番に組み込むため、チーム内でしっかりとコードレビューを行います。 よくあるハッカソン的にプロダクトを作って発表するタイプのインターンシップではないため、本当の意味で就業体験を行うことができます。 インターンシップ参加の条件 インターンシップの参加条件としては、こちらが提示する課題をクリアしてもらう必要があります。 実際にiQONを開発していただくということで、少しハードルの高い課題を設定しています。 ちなみに、昨年のAndroidチームのインターンシップ受け入れ課題は以下のような内容になっていました。 InstagramのAPIを使って『iQON』のタグが付いた画像をグリッド表示するアプリを作成し、 Githubにソースコードを公開して、URLを共有すること 【ルール】 ・Android Studioを使って開発すること ・Libraryは自由に使ってOKとする ・グリッド表示にはRecyclerViewを使って実装すること ・ActivityとFragmentは自由に使って構わないがActivityは、AppCompatActivityを継承すること ・Toolbar(ActionBar)を実装すること ・スクロールによってToolbarを隠すか、隠さないかは自由とする 課題の提出後は、チーム内でコードレビューをしたあとにフィードバックを行います。 何度か修正のやりとりを経て、課題クリアか判断します。 課題クリアのポイントはいくつかありますが、以下の項目は重視して見ていました。 Web APIを使った開発ができるか Androidアプリの基本的なお作法を理解しているか コミュニケーションを取った開発ができるか GitやGithubの基本的な使い方を理解しているか 課題遂行において知らない知識があっても自学できるか *提出された課題アプリのキャプチャ インターンシップで取り組んでもらった内容 課題をクリアした方は、VASILYでの楽しいインターンシップ生活が待っています。 昨年のAndroidチームでは、主に以下のような内容に取り組んでもらいました。 SNS連携の改善&グロースハック オンボーディングの改善&グロースハック オンボーディングで使用する特殊なViewの開発 Material Design対応 iQONで利用しているOSSへのコミット など... 希望によって、実装メインかグロースハックメインかで大きく分かれますが、どちらを選んでも難易度が高く面白い内容になっています。 その他、一般的なインターンシップと同様に、CEOからの会社説明があったり、最終日に取締役への成果報告会などがあったりします。 *Pull Requestを送ると暖かいコメントが数多くもらえます VASILYのインターンシップ参加者の声 東京大学Kくん VASILYのインターンに参加したおかげで、Gitなどのツールの使い方を学ぶことができ、自分の開発力が3倍くらいになったと思っています。 Googleにも認められているAndroidアプリのソースコードを見ながら開発できるのは大変勉強になりました!ありがとうございました! 法政大学大学院Kくん 技術的な面はもちろん、サービスやプロダクトに対するマインドといった面も勉強させて頂きました。 大学を卒業し『エンジニアとして生きていく』ということがどういうことなのかを肌で体験できるインターンシップでした。 インターンシップからの内定 他社のインターンシップでは新卒採用の選考とは全く関係ないものもあると思いますが、VASILYでは新卒採用の選考フローにインターンシップへの参加が入っているため、新卒入社するためにはインターンシップへの参加が必須になっています。 インターンシップ参加後に、インターンシップ期間中の成果や、会社とのマッチングを見て、その後の選考を受けるか決めてもらうようにしています。 最後に 簡単ではありますが、昨年のAndroidチームを例にVASILYのエンジニア向けインターンシップを紹介しました。 多くのユーザーが利用しているアプリの開発に携われるため、刺激的な就業体験になること間違いなしです。 チームを開発してみたい方、プログラミングが大好きな方、iQONを作ってみたい方、ぜひともVASILYのインターンシップに応募してみてください。 Androidチームだけでなく、すべてのエンジニアチームで募集中なので、詳しくは下のリンク先で確認してください。
こんにちは、VASILYバックエンドエンジニアの塩崎です。 社会人2年目にも突入し、優秀な後輩たちに抜かされないかと日々ひやひやしています。 さて、今回は1ヶ月程前に完了した、メールサーバーのSendGrid移行について紹介したいと思います。 移行のきっかけ そもそも、なぜVASILYでメール配信の自社管理をやめてクラウドサービスであるSendGridに移行する必要がでたのでしょうか? 以前から使用していたpostfixサーバーではなぜダメだったのでしょうか? それは、大量のメールマガジンを遅延なく配信する必要が生じたからです。 昨年の11月頃からiQONでは、ユーザーさん一人一人にオススメのアイテムを送る、リコメンドメルマガを開始しました。 それにあたり、数万人のユーザーさんに対してそれぞれ内容の少しずつ異なるメールを送る必要が出ました。 このような処理を自社管理のpostfixサーバーで行うことは 非常に面倒臭い 非現実的であったため、面倒な処理を肩代わりしてくれるクラウドサービスを利用することにしました。 SendGridに決めた理由 メール配信のクラウドサービスを決めるにあたって、SendGrid以外にも Amazon SES 、 Mandrill 、 MailChimp などのサービスも比較しました。 それらと比較してSendGridが優れているポイントを紹介します。 ただメールを送るだけのサービスではない SendGridはメールを送るだけのサービスではありません。メール配信に関わる機能一式を提供してくれます。 これが、ただメールを送るだけのサービスであるAmazon SESとの大きな違いです。 個人間で送り合うメールならいざ知らず、メールマガジンのような大量のメールを一斉配信することは想像している以上に困難です。 少しでも怪しいメールを送ると携帯キャリアなどのメール受信業者のブラックリストに入ってしまいます。 そのような扱いを受けないようにするためにSPFやDKIMを適切に設定する必要があります。 SendGridはこれらの認証機能を簡単に使用することができたので導入が非常に楽でした。 また、ユーザーさんのメールボックスに届いた後にユーザーさんがどのような行動をするのかも非常に重要です。 メールを開封するのかどうか、メール中のリンクをクリックするのかどうか、これらの指標はメールマーケティングを行うためには最も重要な指標です。 しかし、これらの機能を自社で作り上げるのは 非常にメンドイ そこそこの人月が必要です。 そのような機能をデフォルトで提供してくれて、しかも綺麗なダッシュボードにまとめてくれるのも嬉しいポイントです。 WEB APIが豊富 SendGridが提供している、ほとんどの機能をWEB APIから使うことができます。 SMTPという旧世紀のプロトコルを使うことなく、近代的な方法でメールの送信を行うことができます。 さらに送信をするためのWebAPIだけでなく、何かイベントが起こった時に特定のエントリーポイントを叩いてくれる機能もあります。 メールの到着、開封、クリックなどのイベントが起こったタイミング毎に指定したエントリーポイントを叩いてくれます。 VASILYではこの機能を利用して、メール配信のログをBigQueryに保存し、それをBIツールであるTableauで可視化しています。 このあたりについては記事の後ろのあたりで、より詳しく紹介します。 日本代理店がある SendGridの日本代理店として 構造計画研究所 さんがあり、いざというときに日本語のサポートがあるのが安心でした。 また、構造計画研究所さんではSendGridの初心者向けセミナーや、個別相談説明会、メールマーケティングに関する ブログ などの活動を精力的に行っており、ただ本国に取り継ぐだけでなく、メール配信のプロがきちんと日本にもいるという実感が持てました。 注) 色々とSendGrid万歳な紹介をしていますが、弊社はSendGridの回し者ではありません。 使い方によっては他のメール配信サービスを使ったほうがいい場合もあります。 例えば、対エンジニア向けのアラートメールなどは、アプリケーションサーバーに同居しているsendmailで送信を行っています。 移行するためにしたこと アドレスリストのクリーニング 移行の本来の目的はメルマガ配信の効率化であったため、保持しているメールアドレスリストが大量配信に耐えられるものかどうかを考える必要があります。 諸所の事情により、当時のアドレスリストはダブルオプトインが確認できているかが不明だったために、バウンス率が非常に高くなってしまう恐れがありました。 そのため、SendGridに移行を行う前にアドレスリストのクリーニングを行いました。 幸いにも、シングルオプトインは当時のログから確認できたため、配信対象のユーザーに対してメルマガを試しに送信してみて、開封ログを取ることにしました。 この時はまだSendGridへの移行をしていなかったので、開封トラッキングの仕組み・開封をしてくれたユーザーさんのダブルオプトイン確認フラグを立てる仕組みを自力で作りました。 車輪の再発明をしている感じしかなく、面倒くさかったです。 数回メルマガを送っても開封してくれないユーザーさんについては、潔く諦めました。 エンゲージメントの低いユーザーさんに送ってもコストの方が嵩むということに加え、ハニーポットに引っかかるのを防止する効果も期待できます。 また、バウンス率の高いメールを一気に送ることによるレピュテーションの急激な低下を防ぐために、メールを「ゆっくり」送るように、スケジューラーを設定しました。 メールアドレスリストを、主要メール受信業社数社分のリストに分け、それぞれのリストに対して一定のペースで送るようなプログラムを自力で作りました。めんどうくさかったです。 さらに、「レピュテーションは資産である」ということを常に念頭に置き、レピュテーションやブラックリストを確認する外部サービスの結果を毎朝確認し続けました。 レピュテーションを確認できるサービスについてはこちらの構造計画研究所さんのBlogが詳しくまとまっていたので参考にしました。 送信レピュテーションを確認する5つの方法 この辺りの作業については、先日のSendGrid Night #4でもLTを行いました。 メールアドレスの文字コード SendGrid移行前は mail-iso-2022-jp を利用して、ISO-2022-JP(俗に言うJISコード)でメールの配信を行っていました。 ですが、その設定のままで送信を行うとメールの文字化けが発生してしまったため、このgemを取り除き、UTF8でメールの送信を行うようにしました。 ActionMailerの設定の修正 以下の記事を参考にして、ActionMailerが指し示すサーバーを自社管理のpostfixサーバーから、SendGridのSMTPサーバーに変更しました。 https://sendgrid.kke.co.jp/docs/Integrate/Frameworks/rubyonrails.html#Configure-ActionMailer-to-Use-SendGrid 移行結果 以上の作業を行った結果、1ヶ月程度前にメールサーバーのSendGrid移行が完全に完了しました。 作業が面倒くさそうな書き方をしていますが、ただ置き換えるだけであればActionMailerの設定を書き換えるだけですので非常にシンプルです。 ダブルオプトイン確認フラグの復旧が面倒だっただけです。 VASILYでは社内KPIをBigQueryで集計、BIツールであるTableauで可視化を行っているので、メルマガの配信結果もそれらを利用して作ってみました。 以下に示すような仕組みでSendGridのEventWebHookから飛んでくるデータを可視化しています。 その結果、このようなダッシュボードをエンジニアだけでなく、ビジネス職の人とも共有し、効果的に施策を考えることができています。 ここは改善してほしいなSendGrid さて、ここまでSendGridを褒めちぎってきましたが、一部使いづらいと思うこともあったので紹介します。 直してくれないかなぁ・・・ 予約配信した時に時刻がズレる → 解決しました SendGridには予約配信機能があり、送信リクエストをしたタイミングと、実際に送信が行われるタイミングをずらすことができます。 メルマガではこの機能を利用し大量のメールを遅延なく配信しています。 しかし、ユーザーさんの手元に届いた時のメールのタイムスタンプが、届いたタイミングではなく送信予約をしたタイミングになってしまいます。 例えば、以下のメールはAM 11:00頃に配信予約をし、PM 9:00にユーザーさんの手元に届いたメールです。 メール中のタイムスタンプが配信予約のリクエストを投げた時刻になってしまっています。 この問題は先日のSendGrid Night #4でSr. Product Support EngineerのScott Kawai氏に質問したところによると、配信予約時にSMTPヘッダーのDateフィールドをPM 9:00に設定すれば良いということを教えていただきました。ありがとうございます。 再送するタイミング SendGridはsoft bounceしたメールを最大で72時間再送しようと試みてくれます。 一方でパスワードリセットや、領収書などのトランザクションメールはどんな時間に届いても価値があるかと思います。 しかし、メルマガのようなマーケティングメールが深夜に届いたらユーザーさんはどのように思うでしょうか? 深夜3時に届いたメーケティングメールで目が覚めてしまうなんて、酷い目覚めではないのかと思います。 そのため、メールによっては日中のみに再送をするような設定ができたらいいなと思います。 まとめ SendGridを実際に使ってみて、SendGridはメールを届ける「だけ」のサービスではないということがわかりました。 メールを届けるのはもちろんのこと、その他メールに関わる色々な面倒臭い処理をまとめて引き受けてくれるサービスであるということがわかりました。 メールをただしくユーザーさんのメールボックスに届けるのが予想外に難しいことを考えると、ほとんどの場合これらの面倒くさい処理をSendGridに任せた方が得策なのではないでしょうか。 終わりに VASILYではモダンなインフラに興味のある仲間を募集しています。 興味のある方は是非こちらからご応募ください。