TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

あけましておめでとうございます、CTOの今村( @kyuns )です。 このテックブログを購読してくださっている読者の皆さん、いつもありがとうございます。 VASILYテックブログも記事を投稿し始めてから約6年半が経ちました。 今回はテックブログを長年続けてきた振り返りと、長く続けるコツについて紹介したいと思います。今年はテックブログを始めてみたい、という方々の参考になれば幸いです。 振り返り 初めてVASILYテックブログに記事が投稿されたのは 2011年5月9日 、この時から現在までに約6年半の月日が経ちました。余談ですがこの時に紹介した3種の神器は今でも現役です。(QC3はQC30になりましたが) それでは6年半の歴史を軽く振り返っていきましょう。 2011年〜2013年 とりあえず始めてみたフェーズ 2011年から2013年まではエンジニアもまだ5,6名しかおらず、気が向いたら更新する、という運用しかしていませんでした。ブログを書いたことのある人も少なく、皆アウトプットに慣れていなかったのもあり、1つの記事を公開するのも非常に時間がかかっていました。また、通常の開発が忙しい中で、どんどん書いてよと言い出しにくかったということもあり、結果として公開された記事の本数は非常に少ない結果となりました。 2014年〜2015年 手探りフェーズ 2014年になりエンジニアが10名を超えたあたりで、採用強化のためにも本格的にブログを更新していこうと思い様々な試みを行いました。例えばバズりそうな内容の記事を書いてみる、キャッチーなタイトルを付けてみる、記事の公開時間を工夫してみるなど、一般的なブログ運用でも用いられているアプローチを取り入れてみました。 やってみた結果、バズったりホッテントリに掲載されたりする記事もでてきました。ただ、知名度の向上や採用にはいまいちつながっている実感はあまりありませんでした。 原点回帰 時間をかけて書いた記事がバズったりしているにも関わらず、採用にはあまり響きませんでした。 なぜなのでしょうか? 答えはシンプルで、 なぜやるのか? というところが抜け落ちていたのが原因でした。 そもそもエンジニア採用が目的なのであれば、技術力の高さや業務内容の魅力を、記事を通して伝える必要があります。その観点が抜け落ちていたことに気づきました。 結局ただバズるだけの記事を書いても、そこにはVASILYという存在が見えることはなく、ただコンテンツが消費されて終わるだけでした。 上記の反省を踏まえて、記事は 「業務で行った事に関連する内容」 に限定することにしました。 2016〜2017年 原点回帰、運用フェーズ 記事の内容の一新に続けて、ブログの運用も仕組み化するようにしました。 たとえば、 毎週1記事は必ず公開する ことや、 公開前の内容をエンジニア達でお互いに校正する 、などです。毎週1記事は、エンジニア15名程度だと3か月で全員が丁度1回まわるぐらいの頻度です。 また、ブログの効果をお互いにフィードバックしたり、評価に取り入れたり、様々な仕組み化を行うことによって定常的に記事が公開されるようになりました。定常的に記事が公開されることで、次第にテックブログの効果も実感できるようになってきました。 振り返りを経て実感できたメリット 数年間のブログ運用を振り返ってみて、テックブログを続けることは確実に 個人と会社にメリットをもたらす ということが分かってきました。 実際に運用してきた結果を踏まえた上で感じたそれぞれのメリットを紹介したいと思います。 個人としてのメリット 1.アウトプットの能力が身につく 会社のブログに記事を書いてアウトプットするとなると、それなりにアウトプットする内容についてしっかりと調べなければなりませんし、うまく文章にまとめる必要があります。これは記事の公開を繰り返すことによって確実に上達します。 2.着実に実績になる 記事をコンスタントに公開していると、色んな人から「あの記事みました!」とか「うちでも同じような実装をしているので相談したい」という声を聴くようになります。どんな細かい内容だとしても、必ず何らかのフィードバックを世間から得ることができます。 細かいアウトプットが積み重なることで、勉強会などでも「あの記事を書いた○○さん」という形で認識してもらえることもあり、個人としてのPRや実績に繋がります。また記事を見た方々から登壇依頼があったりすることが多数あります。 これは弊社の新卒エンジニアを見ていても全員同じような経験をしているので、突出したエンジニアだけが効果があるというよりは、誰でもアウトプットすることにより確実にメリットが生まれるということを証明していると思います。 例えば現在 「XPath」 「Swagger」 「Standard SQL」 などの単語で検索した時、最上位に出てくる記事はどれも弊社新卒エンジニアが書いた記事ですし、今もなおアクセスが伸び続けています。 会社としてのメリット 1.知名度向上 面接をしていると、テックブログの記事を通してVASILYという会社を知ってくれた方に多くお会いします。VASILYはクローラーや機械学習のことをやっている、という認識は多くの初対面のエンジニアの方々が我々に持っている印象です。 これは弊社の記事だとクローラーや機械学習系があるのですが、この会社は沢山のECサイトをクロールしてるんだとか、ファッションのデータをつかって新しい取り組みを色々行っているんだ、ということを伝えることができている結果だと思います。 クックパッドやメルカリなど誰もが知っているサービスならまだしも、僕らが運営しているサービスは女性向けであり、TVCMをやっていた時期でさえも男性エンジニアの目に触れることはほぼありませんでした。 サービス自体を知らなくてもその中で使われている技術に興味を持って、そこからサービスや会社に興味を持ってくれるという方々がほとんどでした。これはまだ名前の知られていないベンチャーやスタートアップだからこそ発生する問題だと思いますが、だからこそ小さい会社がテックブログで発信する意味があると思います。 会社に興味を持ってもらえる軸はたくさんあっていいと思いますし、記事を更新することで、会社の知名度向上には少なからず役立っていると感じます。 2.採用力強化 テックブログを更新し続けることは採用につながる、と恐らくどの会社も思っていることでしょう。もちろん、僕もそう思っていますが、記事を書いたからと言って直接採用に繋がるものではありません。直接的な採用効果というよりは、間接的に役立っている印象です。 例えば、エンジニアとして選考をうけてくれる方々はみなさんテックブログを読んで来てくれますし、会社全体としての技術力の評価や会話のきっかけになるケースが多くあります。 検索してて記事を読みました、とか仕事で役立ちました、とかそこから興味を持ってくれる人が多くいます。もちろん、クローラーや機械学習以外にもiOS,Androidなど様々なジャンルでいくつも記事を投稿していますし、地道な積み重ねが確実に採用につながっていると言えます。 3.営業力強化 昨年は弊社テックブログの記事を見て、同じことをやりたい、ぜひ一緒に開発してくれないか?という問い合わせが非常に増えました。例えば画像解析やディープラーニング系はGIGAZINEさんで紹介されたこともあり、非常に多くの問い合わせをいただきました。そこから実際にビジネスに繋がった案件もありますし、ダイレクトに会社の技術力をアピールする場所としては最適だと思います。 以上のように、個人や会社にとって非常にメリットがあることが続けていくうちに分かってきました。 次はテックブログを続けてきた中で、長く続けるコツがいくつかあるので紹介したいと思います。 長く続けるコツ テックブログを長く続けていくコツは一体なんでしょうか? 最も必要なことは 「文化の醸成」 です。 文化を作るためには、記事を更新し続けなければなりません。 更新し続けないと、書くメリットも実感できません。 書くメリットを実感し、アウトプットを繰り返すうちにそこからチームとしての 「文化」 が生まれます。文化が生まれるほど組織運営において強力なことはありません。 ではどのようにすれば記事を更新し続けれるような 「文化」 をつくることができるのでしょうか? 我々は以下のような仕組みをつくることによって、アウトプットを出しやすい環境を作るようにしています。 まさかりを恐れない雰囲気を作る ブログを公開するのをためらう心理的障壁の1つとして有名なのが通称「まさかり」と揶揄される行為です。人気になった記事であればあるほど色んな種類のコメントがつきますし、時には自分の知識不足により罵倒されることもあるでしょう。そして100個の賞賛のコメントよりも1個の罵倒コメントのほうが記憶に残るものです。アウトプットに慣れていない人は、コメントを真摯に受け止めすぎて1人で抱え込んでいると辛い思いをすることもあるでしょう。そこで弊社では毎週行われるTECHMTGでこういうコメントついてたよね〜などみんなで話しあったり時には笑い飛ばしたりします。そういうフォローしたりする場があることで、助け合う空気を作り、心理的ハードルを下げることに貢献しています。 しっかりと評価する テックブログを書くという行為に対して、評価に取り入れてる会社や全く評価とは関係ないという会社など、色々評価方針はあるでしょう。しかしながら弊社ではテックブログやQiitaへの投稿、OSSへの貢献など全てのアウトプットは評価に取り入れています。 VASILYには「エンジニアリングマニフェスト」というものがあり、この最後に「インターネットに貢献する」という項目があります。 インターネットに貢献するという項目は、普段我々が利用しているOSSや参考にしている情報は必ず誰かがアウトプットしたものであり、我々もお世話になっている分、同じことをしてインターネットのエコシステムに貢献すべきだという観点から作られた項目です。 この観点からも、テックブログでの発信も評価につながるようになっていますし、もちろん業務時間を用いて記事を書いています。 校正の品質を上げる 記事の校正には非常に時間がかかりますし、自分で読むと意味が伝わると思っていても、違う人が読むと意味がわかりにくかったりする部分があることがよくあります。 弊社では校正の品質を上げるために 自動 と 手動 の2つの校正手法を用いています。 まず、自動校正には textlint を用いており、メンバーがQiita:Teamにテックブログの下書きを投稿すると自動的にLinterが走るようになっており、コメント欄に校正結果が反映されるようになります。このように誰が見てもわかる間違いはbotが指摘してくれるようになっています。 詳しくは Qiita:Team + Hubot + textlintで文章校正を自動で実行する でも解説しています。 また、botでは見つけれないような伝わりにくい表現やわかりにくい場所などは他のメンバーが積極的に指摘してコメントしてくれます。 メンバーの力を借りる さきほどの校正でももちろんチームの力を借りていますが、他の場所でももちろんチームの力を借りている部分があります。 たとえば、弊社ブログは当番制にしているため、自分の番が来週だ、という時に丁度たまたまリリースが重なってしまい、時間の確保が難しくなることもあるでしょう。そのような時はTECHMTGで変わってくれる人を募集したりして、うまく交代することによって記事の公開を続けることができています。 まとめ 以上、テックブログを振り返ってみて気づいたメリットや長く続けるコツを紹介してみました。 テックブログ運用はチームの協力がないと成り立ちませんし、続けていくことは非常に難しいことだと思います。 日頃VASILYテックブログを更新し続けているメンバーには本当に感謝してもしきれません。 運用するのは大変ですが、今年も色んな分野で新しい会社やサービスが誕生すると思いますし、世の中にもっとテックブログが増えていくといいなと思います。 様々なサービスの裏側の技術を覗けることはエンジニアとしても非常に楽しみです。 2018年、VASILYは新しい節目を迎えます。 世の中がもっと便利になるようなサービスをみなさんにお届けしたいと思いますし、その裏側をこのテックブログでも随時紹介していく予定です。 今年もVASILYテックブログをぜひ楽しみにしておいてくださいね。 よければRSSの登録や、はてなブログの購読ボタンをぜひ押してみてください。 VASILYでは一緒にテックブログを書いてくれるメンバーを募集しています、我こそはという方はぜひ以下からご応募ください。 https://www.wantedly.com/projects/61388 www.wantedly.com
アバター
この服装に合う靴を選んでコーディネートを完成させたいと思います。皆さんはどの靴を選びますか? データサイエンティストの中村です。今回、このようなタスクを解くためのシステムを開発しました。本記事ではシステムと裏側の要素技術について紹介したいと思います。 概要 ファッションにおいて、コーディネートは何より大事な要素です。安物の服でもコーディネートが整っていればおしゃれに着ることができますし、逆にハイブランドで固めたとしてもダサく見えてしまうことは充分に考えられます。 コーディネートはアイテムの組み合わせであり、コーディネートをよく見せるには一定の規則 1 に基づく組み合わせの選択が重要です。ところが、この規則は複雑で敷居が高いので、組み合わせに関する表現を直接データから獲得してしまおうというのが今回のトライの内容です。 本記事で紹介するシステムは、コーディネートを学習することで以下のようなタスクを解くことが可能になります。 (1) コーディネートの出来栄えを採点する (2) コーディネートに欠けているアイテムを選択する (3) 任意のアイテムを使ったコーディネートを生成する なお、本記事はHan2017を大いに参考にしています。 入力データ IQONではユーザーがアイテムを組み合わせてコーディネートを作成・公開する機能を提供しています。今回はこの機能で得られたデータを入力として用います。 このように、ひとつのコーディネートは複数のアイテム画像で構成されています。 コーディネートに使うアイテムの数には制約がありません。その為、入力を画像のシーケンスとみなし、可変長の入力を前提としたアプローチを採用しました。 簡単のため、入力するアイテムは主要なカテゴリ 2 に限定し、系列長が4-8となるサンプルのみを対象としています。その他処理を施した結果、学習用のコーディネート数は70,997、アイテム総数は174,653となりました。 ファッションアイテムの相性を求めるタスクでは、Amazon co-purchase data(He2016)やPolyvore(Han2017)が使われますが、IQONデータセットは以下の点で扱いやすいデータセットになっていると思います。 IQONのコーディネートにはAmazonのようなレコメンドによるバイアスが乗らない 人種・民族のばらつきが非常に小さく馴染みがあるので評価しやすい 生活に根ざした日常的なコーディネートが多く、かつおしゃれである モデル CNNとRNNのジョイントモデルを使います。 CNN CNNはアイテム画像 からの特徴抽出を担当します。アーキテクチャはInceptionV3を採用し、ImageNetの学習で獲得したパラメータをそのまま用いました。 RNN Bidirectional LSTM(BiLSTM)を使います。BiLSTMは、入力された系列に対し、前向きと後向き両方向から同時に学習するRNNです。 アイテムの特徴量系列 を逐次入力した時、内部状態を更新しながら次に入力されるアイテムを予測します。次のアイテムを予測できたか否かでモデルを評価するので、コスト関数は以下のようにsoftmaxで定義します。 前向きLSTM 後向きLSTM ここで、 はそれぞれ前向きLSTM、後向きLSTMのパラメータです。 は候補となるアイテム画像の集合です。データセットのすべての画像を含めたいところですが、現実的ではないので、ミニバッチの中から取得します。前向きLSTMのときはミニバッチ内の の集合、後向きLSTMのときは の集合とします。 誤差関数 最終的なコスト関数は以下のようになります。 CNNの重みは固定したので学習の対象はRNNのパラメータ です。 実験 本手法を応用したアプリケーションをいくつか紹介します。 (1) コーディネートの出来栄えを採点する コーディネートを画像の系列として入力した時、以下を計算することでコーディネートの評価を得ることができます。 この式を使えばコーディネートの完成度を定量化して比較することが可能です。 いくつか例を載せます。矢印の左が入力した系列、右側は系列のスコア(大きいほど良い)です。 一番上のコーディネートは自然なのでスコアが大きくなっています。スニーカーをサンダルに入れ替えると不自然さが増し、スコアが減少しました。入力画像がnoisyだったりコーディネートとして成立していない場合もスコアが減少します。 (2) コーディネートに欠けているアイテムを選択する コーディネートをより魅力的に見せるために必要なアイテムを選択することができます。 を候補のアイテム群としたとき、以下の目的関数は の中から最良のアイテムを返してくれます。 結果は以下のとおりです。矢印の左が入力した系列、右側は組み合わせるアイテムの選択肢とそれぞれのスコアを表しています。 1行目は冒頭の問題の解答です。機械はシルバーのサンダルもしくは白のスニーカーが良いと判断しました。 シルバーのサンダルは実際のコーディネートに使われたアイテム、すなわちGround truthです。ただ、この組み合わせなら白いスニーカーでもOKなので、機械の判断は逡巡も含めて正しいと言えます。 (3) 任意のアイテムを使ったコーディネートを生成する アイテム単体を与えた時、そのアイテムを使ったコーディネートを提案することができます。このタスクは前述した2つのタスクを応用することで実現できます。具体的な手順は以下のとおりです。 1. クエリとなるアイテムの前後のアイテムを(2)で予測する 2. 前のステップで得られたアイテムを結合した系列を入力として系列の前後のアイテムを(2)で予測する 3. 2.を末端まで繰り返す 実際は(2)でスコア上位K個のアイテムを取ってきて、K本の系列を生成するようにします。それぞれの系列は(1)により評価可能なので、ビームサーチによる最適化が実行できるようになります。 実際に以下のコーディネートが生成されました。矢印の左側がクエリとなるアイテム、右側が生成されたコーディネートです。 季節感やアイテムの個性を反映した組み合わせになっていると思います。弊社の社員にアンケートしたところ、かなり完成度の高いコーディネートであるとのコメントを貰いました。 スタイルの獲得に向けて 本記事で紹介した手法は、自然なコーディネートを生成可能であることを確認できました。ここで、さらなる改善を考えてみたいと思います。 ファッションにはスタイル 3 という概念が存在します。スタイルはコーディネートの方向性を与えます。同じアイテムがクエリであっても、スタイルが異なれば組み合わせるアイテムは当然変化します。 コーディネートを生成するモデルであれば、スタイルで条件付けて出力をコントロールする機能は備えていたいところです。 出力をコントロールする方法 Han2017やZhao2017はアイテム画像とそれに紐づく属性のマルチモーダル学習を提案しています。画像とテキストを同じ空間に埋め込むことで、アイテムの印象を直感的に操作可能になります。弊社でも過去に生成モデルを使ってトライしました。 Hsiao2017ではトピックモデルを利用してスタイルの認識に取り組んでいます。コーディネートを文書、コーディネートを構成するアイテムの属性を単語と捉え、文書のトピック分布をスタイルと定義しています。 Li2016はユーザーの個性ベクトルを追加して応答文の系列を一貫して変化させることに成功しました。 拡張のアイデアは様々ありますが、今回は以前からずっと気になっていた手法を適用してみます。 LDA2VEC LDA2VEC(Moody2016)はStitchfixが開発し、彼らのユーザーのコメント解析に利用している手法です。単語分散表現に文書分散表現を上乗せし、表現能力を向上させています。元々はlocal(単語)とglobal(文書)の表現を同時に用いることで予測の性能向上を狙ったものと思います。彼らのnoveltyは文書ベクトルを混合モデルとして定義したことで、混合の基底となるベクトルはトピックモデルにおけるトピックと似たような働きをするようです。 詳細に関しては彼らの素晴らしいとしか言いようのない テックブログ をご覧ください。 アイテム画像もベクトルに変換してしまえば単語ベクトルと同じように扱えます。LDA2VECのように、globalな表現であるところのコーデベクトルとlocalな表現であるアイテムベクトルを同時に学習させることで、生成されるコーディネートに変化をつけることができるかもしれません。 LDA2VECは教師なし学習なので簡単に試せますし、「機械が自らスタイルの概念を獲得できるのか」というテーマに興味が唆られたので、実際に実装して実験してみました。 ちなみに、出力を思い通りに制御したいのであれば、条件付きモデルとして拡張するほうが確実だと思います。 モデルのアップデート LDA2VECの文書ベクトルを作成するモジュールを追加します。このモジュールの出力とアイテムベクトルの和がRNNの入力となります。 一見複雑に見えますが、Outfit vector ModuleはLookup tableとLinear layerだけで定義できるので実装は簡単です。 まず、コーディネート毎のトピック分布とトピックベクトルを定義します。 コーディネート のトピック分布 は、コーディネートのトピック混合比を表します。混合比なので を満たします。 スタイルベクトル はスタイル の性格を反映したベクトルで、アイテムベクトル と同じ次元数です。ここで、 はスタイル数です。 両者の内積計算をコーディネート 特有のスタイルとみなします。 これとアイテムベクトルの和をコンテキストと定義し、RNNの入力とします。 Moody2016では混合比 を疎に保つ為、ディリクレ分布を事前分布として利用しています。 上式はディリクレ分布の対数尤度で、 はハイパーパラメータです。 の値が1より大きいと は密になり、1より小さいと疎になるよう作用します。 本手法でも同様に、 を小さく設定したディリクレ分布の負の対数尤度をコスト関数に加えます。 追加実験 (3)のコーディネート生成を、トピックを変化させながら実行しました。トピック毎の特徴は発見できず、スタイルの獲得とはいきませんでしたが、出力をある程度変化させることに成功しました。 所感として、汎用的なアイテム(白いトップスなど)は生成されるバリエーションが多く、主張の激しいアイテム(かごバッグなど)はほとんど変化しませんでした。この現象は直感と一致します。 使ってみての感想ですが、Outfit vector Moduleはなかなか癖がありました。ハイパーパラメータの設定次第では改善余地は充分に考えられます。 まとめ CNNとRNNのジョイントモデルを用いて以下の3つのタスクに挑戦しました。 (1) コーディネートの出来栄えを採点する (2) コーディネートに欠けているアイテムを選択する (3) 任意のアイテムを使ったコーディネートを生成する また、教師無しでのコーデベクトルの学習を試みました。生成されるコーディネートの制御やスタイルの獲得に向けて更に改良と実験を重ねる必要があります。 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 https://www.wantedly.com/projects/109351 www.wantedly.com 参考 Han, X., Wu, Z., Jiang, Y., Davis, L. Learning Fashion Compatibility with Bidirectional LSTMs. In ACM Multimedia, 2017. He, R., Packer, C., McAuley, J. Learning compatibility across categories for heterogeneous item recommendation. In ICDM, 2016. Hsiao, W., Grauman, K. Learning the Latent “Look”: Unsupervised Discovery of a Style-Coherent Embedding from Fashion Images. In ICCV, 2017. Li, J., Galley, M., Brockett, C., Spithourakis, G., Gao, J., Dolan, B. A Persona-Based Neural Conversation Model. In ACL, 2016. Moody, C. Mixing Dirichlet Topic Models and Word Embeddings to Make lda2vec. arXiv, 2016. Zhao, B., Feng, J., Wu, X., Yan, S. Memory-Augmented Attribute Manipulation Networks for Interactive Fashion Search. In CVPR, 2017. 例えば、コーデの基本は3色、目立つ素材や柄物は1つに抑える、などです ↩ アウター・トップス・ボトムス・シューズ・バッグ・ハット・アクセサリー ↩ フェミニン、コンサバ、ガーリー、モード、マニッシュ、エスニックなどなど ↩
アバター
こんにちは。バックエンドエンジニアインターンの田島です。弊社ではIQONの運用を7年間続けています。長年の運用から技術的負債が溜まってきていました。その中の1つに、IQONの本番DBと開発DBの状態が乖離しているという問題があります。この問題をどのように解決したかについて紹介します。 IQONについて IQONはRuby on Railsで運用されており、以下のような環境で動作しています。 Ruby 2.2 Rails 4 MySQL 5.6 IQONのデータベースについて IQONではRDBとしてMySQLを利用しています。DBは本番DB、開発DB、テストDBの3種類に分かれています。スキーマ変更の作業はRailsのマイグレーション機能を利用せず、SQLを直接利用して行っています。これは、サービスの大規模化に伴い、マイグレーション機能だけでは要件を担保できなくなったためです。手順は以下のようになっています。 SQLファイルを作成 1で作成したSQLをdevelopmentDBに反映 db:schema:dump コマンドを利用しschema.rbに変更を反映 一通り確認 本番DBに1で作成したSQLをproductionDBに反映 3の手順を含める理由は、CIでのテストDBを作成時にschema.rbからDBを生成しているためです。 抱えていた問題 IQONのデータベースが抱えていた問題として以下が挙げられます。 本番DBと開発DBのVARIABLESに差異がある 本番DBと開発DBのストレージエンジンに差異がある 本番DBと開発DBのCollationに差異のあるテーブルが存在する 本番DBと開発DBのCollationに差異のあるカラムが存在する 本番DBと開発DBの型に差異のあるカラムが存在する 本番DBにしか存在しないテーブルが存在する 開発DBにしか存在しないテーブルが存在する 開発DBにしか存在しないカラムが存在する 本番DBと開発DBで多くの差異が発覚しました。この状態では、開発環境では見つからないバグが本番環境に入り込んでしまう恐れがあります。 ゴール 開発DBを以下ような状態にすることを課題解決のゴールに設定しました。 本番DBと同様のテーブル構造になっている 現在の開発DBと同様のデータを持つ 解決手順 解決の大雑把な流れを説明します。最初に開発DBからdumpデータを作成します。本番DBと同じ設定の新開発DBを用意します。最後に、作成したdumpデータ新開発DBに適用するという流れです。 詳細を時系列順に説明していきます。 空のDB(新開発DB)を用意しVARIABLESを本番DBに合わせる 本番DBでは存在せず開発DBにのみ存在するカラムを削除する 本番DBと型に差異のあるテーブルに対して型をキャストし本番に合わせたテーブルを作成 開発DBのダンプデータ(データのみ)を作成 RailsプロジェクトにActiveRecord::Mysql::Awesomeを適用 本番環境のDBからschema.rbを作成 新開発DBに対してschema.rbを適用 作成しておいたダンプデータを新開発DBに適用 確認 1.空のDB(新開発DB)を用意しVARIABLESを本番DBに合わせる 空の新開発DBを作成し、VARIABLESを本番DBに合わせます。VARIABLESはMySQLのシステム変数です。文字コードやCollationのデフォルト値などはVARIABLESで定義されているものが使われます。VARIABLESを合わせることによりMySQLの全体的な状態を本番DBと新開発DBで同一にします。 ここでは、問題1が解決されます。 2.本番DBでは存在せず開発DBにのみ存在するカラムを削除する データの移行に際してMySQLのdumpデータを利用します。開発DBにのみ存在するカラムがあると、新開発DBにdumpデータを適用する時、存在しないカラムへのinsertが発生しエラーとなってしまいます。そのため、dumpデータ作成の前に開発DBにのみ存在するカラムをすべて削除する必要があります。幸いこのようなカラムは少なかったので以下のようなSQLをカラムごとに発行しました。 ALTER TABLE テーブル名 DROP 削除するカラム; ここでは、問題8が解決されます。 3.本番DBと型に差異のあるテーブルに対して型をキャストし本番に合わせたテーブルを作成 開発DBと新開発DBのカラムの型が違うことにより、新開発DBへのdumpデータ適用時にエラーが発生してしまいます。そこで、開発DBのカラムを本番DBに合わせた型でキャストしたテーブルを作成します。そのテーブルに対しdumpデータを作ることで、型を本番DBに合わせた状態でdumpデータを作成することができます。以下のように型をキャストしたテーブルを作成します。こちらも数が少なかったので、手動で行いました。 CREATE TABLE tmp_テーブル名 SELECT column1, column2, CAST(column5 AS キャストする型) AS column5, column4 FROM テーブル名; ここでは、問題5が解決されます。 4.開発DBのダンプデータ(データのみ)を作成 やっとダンプデータ作成の段階に入ります。新開発DBではCREATE TABLEの処理を単独で行いたいので、データのみのdumpデータを作成します。dump時に -c オプションを付けることでデータのみdumpファイルを作成できます。また、新開発DBへのdumpデータ適用時に不要なテーブルの除外をしたいです。そこで、テーブルごとにdumpデータを作成します。テーブルごとに1つずつdumpデータを作るのは大変なので、以下のようなスクリプトを利用しました。 #!/bin/bash MYSQL_USER = '' MYSQL_HOST = '' MYSQL_PASS = '' MYSQL_DBNAME = '' DIR =dump__ ${MYSQL_DBNAME} if [ ! -d ${DIR} ] ; then mkdir ${DIR} fi for TABLE in `mysql -u ${MYSQL_USER} -p ${MYSQL_PASS} -h ${MYSQL_HOST} -N -s -e " show tables in ${MYSQL_DBNAME} ; " ` ; do echo $TABLE mysqldump -c -u ${MYSQL_USER} -p ${MYSQL_PASS} -h ${MYSQL_HOST} -t ${MYSQL_DBNAME} $TABLE > dump__ ${MYSQL_DBNAME} / $TABLE.sql done ; これによりテーブルごとに テーブル名.sql という名前でdumpファイルが作られます。 5.RailsプロジェクトにActiveRecord::Mysql::Awesomeを適用 IQONではRails 4を利用しています。Railsではschema.rbでDBのスキーマ情報を保持することができます。しかし、Rails 4ではCollationやストレージエンジンなどの情報をschema.rbに含めることができません。そこで ActiveRecord::Mysql::Awesome を適用することでこの問題を解決しました。 6.本番環境のDBからschema.rbを作成 本番DBのテーブル状態を新開発DBに合わせるためschema.rbを利用しました。本番DBのテーブル情報をRailsの db:schema:dump を利用しschema.rbに反映します。ActiveRecord::Mysql::Awesomeを適用しているのでCollation情報なども正しくschema.rbに反映されます。 7.新開発DBに対してschema.rbを適用 新開発DBに対し db:schema:load を利用することでschema.rbの情報からテーブルを作成します。schema.rbは本番DBのテーブル情報を適用しているので、本番と同じテーブル・カラムのみが生成されます。これにより、本番DBと新開発DBのテーブル状態が同一になります。 ここまでで、問題2,3,4,6が解決されます。 8.作成しておいたダンプデータを新開発DBに適用 新開発DBの状態が整ったので元の開発DBのデータを新開発DBに再現します。作成してあったdumpデータをinsertすることでデータを再現します。作成したdumpデータから新開発DBにのみ存在するテーブルのみをinsertします。ここでも、テーブルごとに1つずつdumpデータを適用するのは大変なので以下のようなスクリプトを利用しました。 #!/bin/bash MYSQL_USER = '' MYSQL_HOST = '' MYSQL_PASS = '' MYSQL_DBNAME = '' TABLES = ( 本番DBに存在するテーブルのリスト ) for TABLE in ${TABLES[ @ ]} ; do echo $TABLE mysql -u ${MYSQL_USER} -p ${MYSQL_PASS} -h ${MYSQL_HOST} -t ${MYSQL_DBNAME} < dump__ ${MYSQL_DBNAME} / $TABLE.sql done ; ここでは、問題7が解決されます。 9.確認 最後にデータが適切に移行されたか確認をします。確認はそれぞれのテーブルのデータ数を比較することで確認しました。ここでも、テーブルごとに1つずつ確認するのは大変なので以下のようなスクリプトを利用しました。 #!/bin/bash MYSQL_USER = '' MYSQL_HOST = '' MYSQL_PASS = '' MYSQL_DBNAME = '' TABLES = ( 新開発DBに存在するテーブルのリスト ) for TABLE in ${TABLES[ @ ]} ; do echo $TABLE mysql -u ${MYSQL_USER} -p ${MYSQL_PASS} -h ${MYSQL_HOST} -t ${MYSQL_DBNAME} -e" select count(*) from ${TABLE} " done ; まとめ 今回のメンテナンスで本番DBと開発DBの差異をなくすことに成功しました。今後の課題として、再びDB同士の差異が出ないようにしなければなりません。そのために、デプロイ前にDB同士での差異がないかの確認の自動化等を検討しています。また、データベースだけでなく改善すべきところはたくさんあります。今後も技術的負債を精算し、より安定しスピード感あるアプリケーション開発めざしていきます。 終わりに VASILYではアプリケーションの側から開発の安定化、効率化を図れるエンジニアを募集しています。興味がありましたら、以下のリンクからご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
アバター
こんにちは。 使うSQLが200行を超えるのが当たり前になってきたデータチームの後藤です。 本記事では、VASILYデータチームで利用しているBigQueryによるデータの前処理のTipsを紹介します。 VASILYではサービスのマスタデータやログデータをGoogle BigQueryに集約して分析に活用しています。機械学習やデータ分析のための前処理を行う際、軽量なデータであれば抽出結果をPythonに渡して処理させることもできます。しかし、分析環境のメモリに載り切らないほど大きなデータを扱う場合、BigQuery内で前処理を済ませてしまうと時間と計算資源の節約になることが多いです。 今回はBigQueryからアクセスできるパブリックデータの1つ、hacker newsのデータを集計しながらTipsを紹介したいと思います。 欠落した日付を埋める 通常のGROUP BY句の場合 SQL Results 指定した日付列を生成する SQL Results 相対的な日付列を生成する SQL Results クロス集計の欠落を埋める フィールドの生成とCROSS JOINを利用する SQL Results 誕生日から年齢を算出する SQL Results 曜日の情報を付与する SQL Results リテンションレートを計算する 基準日に登録したユーザーの継続率を追う SQL Results 大きなデータを取得する LIMIT OFFSETを利用する SQL LIMIT OFFSETの注意点 SQL 画像URLの内容を確認する SQL Results まとめ 最後に 以下に登場するSQLはStandard SQLと呼ばれる仕様にもとづいています。 Standard SQL自体については、弊社の過去の記事が参考になります。 tech.vasily.jp 欠落した日付を埋める GROUP BY句で日毎のレコード数をカウントする際、データに含まれない日付は欠落してしまいます。 通常のGROUP BY句の場合 以下のSQLは、hacker newsに投稿された日毎の記事の数を集計します。 SQL #standardSQL SELECT DATE (time_ts) AS publish_date , COUNT(*) AS article_cnt FROM `bigquery- public -data.hacker_news.stories` WHERE time_ts IS NOT NULL GROUP BY publish_date ORDER BY publish_date LIMIT 10 2006年10月16日や18日〜20日のデータが存在しないため、対応する日付が欠落しています。 Results Row publish_date article_cnt 1 2006-10-09 18 2 2006-10-10 12 3 2006-10-11 5 4 2006-10-12 6 5 2006-10-13 2 6 2006-10-14 2 7 2006-10-15 1 8 2006-10-17 1 9 2006-10-21 1 10 2006-10-22 1 このままでは扱いづらいので、GENERATE_DATE_ARRAY関数を用いて、日付の列を生成することで対処します。生成した日付列にGROUP BY句で集計した結果をLEFT JOINすると日付の欠損がないデータを作成することができます。 指定した日付列を生成する GENERATE_DATE_ARRAY関数に、明示的に日付を渡して日付列を生成します。以下の例では、2006年10月9日〜2006年10月22日までの日付列を事前に生成し、そこに集計結果をLEFT JOINしています。 SQL #standardSQL WITH -- 日付列の生成 date_series AS ( SELECT publish_date FROM UNNEST(GENERATE_DATE_ARRAY( DATE ( '2006-10-09' ), DATE ( '2006-10-22' ) )) AS publish_date) SELECT a.publish_date AS publish_date -- nullを0に置換 , IFNULL(article_num, 0 ) AS article_num FROM date_series AS a LEFT JOIN ( SELECT DATE (time_ts) AS publish_date , COUNT( 1 ) AS article_num FROM `bigquery- public -data.hacker_news.stories` WHERE time_ts IS NOT NULL GROUP BY publish_date) AS b ON a.publish_date = b.publish_date LIMIT 14 上記のSQLを実行すると、以下のように出現しなかった日付に対して0が対応付けられたデータを得ることができます。 Results Row publish_date article_num 1 2006-10-09 18 2 2006-10-10 12 3 2006-10-11 5 4 2006-10-12 6 5 2006-10-13 2 6 2006-10-14 2 7 2006-10-15 1 8 2006-10-16 0 9 2006-10-17 1 10 2006-10-18 0 11 2006-10-19 0 12 2006-10-20 0 13 2006-10-21 1 14 2006-10-22 1 相対的な日付列を生成する CURRENT_DATE()、DATE_ADD()を組み合わせることで「昨日から7日前まで」といった相対的な日付列を生成することもできます。以下の例では、CURRENT_DATE関数に'Asia/Tokyo'を渡して、東京の時刻で処理しています。 SQL #standardSQL WITH date_series AS ( SELECT create_date FROM UNNEST(GENERATE_DATE_ARRAY( DATE_ADD(CURRENT_DATE( 'Asia/Tokyo' ), INTERVAL -7 DAY), DATE_ADD(CURRENT_DATE( 'Asia/Tokyo' ), INTERVAL -1 day) )) AS create_date) SELECT create_date FROM date_series Results Row create_date 1 2017-11-22 2 2017-11-23 3 2017-11-24 4 2017-11-25 5 2017-11-26 6 2017-11-27 7 2017-11-28 クロス集計の欠落を埋める 上記の例と同じ方法で、日付×属性といったクロス集計をした際の欠落も埋めることができます。 WITH句で日付と属性をCROSS JOINした結果を用意し、そこに集計結果をLEFT JOINすることで欠落のないデータを作成することができます。 フィールドの生成とCROSS JOINを利用する 以下の例では、記事の投稿が多いTop10のAuthorが2015年1月1〜2月1日の各日に投稿した記事の数を集計しています。 SQL #standardSQL WITH -- 記事の投稿数が多いTop10ユーザーの集計 top10_users AS ( SELECT author , COUNT( 1 ) AS article_cnt FROM `bigquery- public -data.hacker_news.stories` WHERE author IS NOT NULL GROUP BY author ORDER BY article_cnt DESC LIMIT 10 ), -- 日付列の生成 date_series AS ( SELECT publish_date FROM UNNEST(GENERATE_DATE_ARRAY( DATE ( '2015-01-01' ), DATE ( '2015-02-01' ) )) AS publish_date), -- 欠落の無いフィールド author_cross_date AS ( SELECT author , publish_date FROM -- 暗黙的カンマ CROSS JOIN top10_users , date_series) SELECT a.author , a.publish_date , IFNULL(b.article_cnt, 0 ) AS article_cnt FROM author_cross_date AS a LEFT JOIN ( SELECT author , DATE (time_ts) AS publish_date , COUNT( 1 ) AS article_cnt FROM `bigquery- public -data.hacker_news.stories` GROUP BY author , publish_date) AS b ON a.author = b.author AND a.publish_date = b.publish_date ORDER BY author, publish_date Results Row author publish_date article_cnt 1 ColinWright 2015-01-01 0 2 ColinWright 2015-01-02 1 3 ColinWright 2015-01-03 3 4 ColinWright 2015-01-04 2 5 ColinWright 2015-01-05 0 6 ColinWright 2015-01-06 2 <中略> 315 tokenadult 2015-01-27 0 316 tokenadult 2015-01-28 1 317 tokenadult 2015-01-29 0 318 tokenadult 2015-01-30 1 319 tokenadult 2015-01-31 2 320 tokenadult 2015-02-01 1 以下は、得られた結果を2次元の表としてみたものです。各Authorの投稿がない日には0が入っていることがわかります。 誕生日から年齢を算出する 年齢は日毎に変化するデータなので、誕生日から算出します。日付を'YYYYMMDD'のフォーマットに変換して、 を計算することで算出できます。 例えば、 1988年6月24日生まれの人は2017年12月4日時点で、 となり29歳であることがわかります。 この考え方を使って、hacker newsの各記事が投稿されてからの経過年数を算出してみます。 SQL #standardSQL SELECT id , DATE (time_ts) AS publish_date -- FORMAT_DATE関数を用いて、日付を8桁の整数に変換する , CAST((CAST(FORMAT_DATE( '%Y%m%d' , CURRENT_DATE()) AS INT64) - CAST(FORMAT_DATE( '%Y%m%d' , DATE (time_ts) ) AS INT64)) / 10000 AS INT64) AS age FROM `bigquery- public -data.hacker_news.stories` LIMIT 10 Results Row id publish_date age 1 7330177 2014-03-02 3 2 3671730 2014-05-31 3 3 6059920 2014-05-31 3 4 6528376 2014-05-31 3 5 4697562 2014-05-31 3 6 2249839 2014-05-31 3 7 1578400 2014-05-31 3 8 3563175 2014-05-31 3 9 6969930 2013-12-27 4 10 6990072 2013-12-31 4 曜日の情報を付与する 曜日の情報を付与する場合、dayofweekを利用します。1〜7の整数が振られ、それぞれ日曜日〜土曜日に対応します。 以下のクエリでは曜日ごとの記事の数を集計してみます。 SQL #standardSQL SELECT day_of_week , COUNT(*) AS article_cnt FROM ( SELECT id , DATE (time_ts) AS publish_date , EXTRACT(dayofweek FROM DATE (time_ts)) AS day_of_week FROM `bigquery- public -data.hacker_news.stories` WHERE time_ts IS NOT NULL ) GROUP BY day_of_week ORDER BY day_of_week 以下の結果から、土日の投稿数が平日の投稿数の半数程度であることがわかります。 Results Row day_of_week article_cnt 1 1 160002 2 2 310330 3 3 339530 4 4 333913 5 5 326648 6 6 294343 7 7 169322 リテンションレートを計算する 基準日に登録したユーザーの継続率を追う N日継続率(Retention Rate)とは、ある日にサービスを使い始めたユーザー全体のうち、そのN日後に再度サービスを利用したユーザーの割合のことを指します。毎日利用されることを目指しているサービスでは、この指標を高めることがサービス改善の指針になります。 以下のクエリは基準日に登録したユーザーのN日継続率を集計します。 SQL #standardSQL WITH first_publish_authors AS ( -- 基準日に初めて投稿したユーザーのみ抽出 SELECT author FROM ( SELECT author , MIN( DATE (time_ts)) AS first_publish_date FROM `bigquery- public -data.hacker_news.stories` WHERE author IS NOT NULL AND time_ts IS NOT NULL GROUP BY author ORDER BY first_publish_date) WHERE -- 基準日を指定 first_publish_date = DATE ( '2015-01-01' )) SELECT publish_date -- 基準日に投稿したユーザーの各日のリテンションレート(%)を計算 , COUNT( 1 ) / ( SELECT COUNT( 1 ) FROM first_publish_authors ) * 100 AS retention_rate FROM ( SELECT    -- 基準日に投稿したユーザーがその後に投稿したレコードを集計 author , DATE (time_ts) AS publish_date FROM `bigquery- public -data.hacker_news.stories` WHERE author IN ( SELECT author FROM first_publish_authors) AND time_ts IS NOT NULL GROUP BY author , publish_date) GROUP BY publish_date ORDER BY publish_date LIMIT 5 2015年1月1日に登録されたhacker newsの投稿者の場合、約10%のユーザーがその翌日にも投稿したことがわかります。 Results Row publish_date retention_rate 1 2015-01-01 100.0 2 2015-01-02 9.803921568627452 3 2015-01-03 7.8431372549019605 4 2015-01-04 3.9215686274509802 5 2015-01-05 1.9607843137254901 6 2015-01-06 3.9215686274509802 7 2015-01-07 3.9215686274509802 8 2015-01-08 5.88235294117647 9 2015-01-09 3.9215686274509802 <省略> 大きなデータを取得する サイズの大きな抽出結果は一括で取得できないことがあります。そんな状況ではORDER BY句を使いデータの並びを一意に固定してから、LIMITとOFFSETを利用して少しずつデータを取得します。 LIMIT OFFSETを利用する 以下のクエリではid順にデータをソートした後、OFFSETで指定した最初の1000レコードを飛ばして、1001番目から1500番目までの500レコードを抽出しています。毎回ソートしてから取得するので効率は悪いですが、OFFSETを増やしていくことで最終的にすべてのデータを取得することができます。 SQL SELECT id , title FROM `bigquery- public -data.hacker_news.stories` ORDER BY id LIMIT 500 OFFSET 1000 LIMIT OFFSETの注意点 抽出するデータのサイズが巨大な場合、OFFSETが大きくなるに従ってメモリ使用量を圧迫します。その結果、上記のクエリでは一定のOFFSETを超えると、以下のようなエラーを吐いて落ちることがあります。 Query Failed Error: Resources exceeded during query execution: The query could not be executed in the allotted memory. ORDER BY operator used too much memory.. このようなエラーを防ぐには、利用するメモリの量を減らす必要があります。取得するidを先に抽出し、そのあとtitleをLEFT JOINすることでサイズの大きなデータを取得できるようになります。 (このSQLは非常に効率が悪いと思っています。より良い表現があればご教授いただきたいです) SQL #standardSQL SELECT a.id AS id , b.title AS title FROM ( SELECT id FROM `bigquery- public -data.hacker_news.stories` WHERE title IS NOT NULL AND author IS NOT NULL ORDER BY id LIMIT 500 OFFSET 1000 ) AS a LEFT JOIN ( SELECT id , title FROM `bigquery- public -data.hacker_news.stories`) AS b ON a.id = b.id 画像URLの内容を確認する 画像URLが入ったフィールドを抽出した際、どんな画像が入っているかを把握したい場合に手軽に確認できるテクニックです。 以下のクエリでは、hacker_newsのコメントデータからJPEG画像のURLを抽出します。 SQL SELECT id , URL FROM ( SELECT id , REGEXP_EXTRACT_ALL(text, r '(?i:(?:(?:(?:ftp|https?):\/\/)(?:www\.)?|www\.)(?:[\da-z-_\.]+)(?:[a-z\.]{2,7})(?:[\/\w\.-_\?\&]*).jpg\/?)' ) AS URL FROM `bigquery- public -data.hacker_news.comments` WHERE -- '.jpg'が含まれているレコードだけを対象にする text LIKE '%.jpg%' ORDER BY id), UNNEST(URL) AS URL GROUP BY id , URL LIMIT 100 クエリの抽出結果をSend to Gogle Sheetsボタンを押してスプレッドシートに保存します。 URLが含まれるセルをimage関数に渡します。image関数は画像のURLを渡すと、URL先の画像を表示する関数です。 Results 以下のように、URLの画像の内容が把握できました。 まとめ 本記事では、VASILYデータチームが活用しているBigQueryのTipsを紹介しました。データによって必要となる処理は様々だとは思いますが、ここに記載したSQLの一部でも参考になったなら幸いです。 より体系的に分析のためのSQLを学びたい方には、以下の書籍がおすすめです。高度なデータ分析のためのSQLの例がPostgreSQL、Hive、Redshift、BigQuery、SparkSQLの5つの仕様に対応して上手く書き分けられている良書です。 最後に 弊社では、ファッションに関するデータに強い関心がありデータ分析や機械学習の腕に覚えのある方を募集しています。
アバター
こんにちは。インフラエンジニアの光野です。 AWS re:Invent 2017で次々と新発表があり、ワクワクがとまりません。また最近はACMがDNS検証で証明書を発行できるようになったり、SpotFleetがELB Auto Attachできるようになったり、個人的に嬉しいアップデートが続いています。来年もますますAWSのファンになりそうです。 さて、そんなキラキラ(?)した話題は一旦おいて、本記事では泥臭くAmazon S3の権限管理について考えたいと思います。 S3と権限管理 ご存知の通り、S3は99.999999999%の耐久性と実質無限の容量を持つオブジェクトストレージです。弊社では主にファッションアイテムの画像とコーデ画像の保存先として利用しています。 保存は俺に任せろと言わんばかりのS3ですが、一方でオブジェクトの権限管理は利用者に委ねられています。 「誰が・どのオブジェクトに対して・何ができるか」を適切に把握し管理しない場合、重要なデータがインターネットから閲覧可能になっていた。といったインシデントが発生しかねません。 本記事では、実際に運用することをイメージしつつ権限を設定してみたいと思います。 権限管理 本記事では以下の4要素を使ってS3の権限を操作します。 S3 Bucketポリシー VPCエンドポイントポリシー インスタンスプロファイル(IAMロール) IAMユーザ 状況設定 さて、このような構成を考えます。 要件は次の通りです。 インスタンスはS3へのPut権限を持つ 画像の参照はCloudFront経由に限定する 画像の更新はVPCエンドポイント経由に限定する VPC内からアクセスできるS3 Bucketを限定する IAMユーザは、運用作業でデータを削除することがある データは暗号化する 1つずつ設定していきたいと思います。 インスタンスはS3へのPut権限を持つ 手始めに、インスタンスに対してPutObjectの権限を与えます。 設定箇所:インスタンスプロファイル { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Action ": [ " s3:PutObject " ] , " Resource ": [ " arn:aws:s3:::a-service-static/* " ] } ] } データを保存するだけであれば PutObject 1つで可能です。 " Resource ": [ " arn:aws:s3:::a-service-static/* " ] Bucket以下の任意の要素に対して権限を付与してやります。 画像の参照はCloudFront経由に限定する 次に保存された画像を参照することを考えます。 S3においた画像を公開する場合、オブジェクトのACLをPublicとすることもできますが、ここではCloudFrontを経由して配信します。 OAI(Origin Access Identity)を使って、オブジェクトに対してCloudFront経由でのアクセスを許可します。 設定箇所:S3 Bucketポリシー { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Principal ": { " AWS ": " arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ABCDEFGHIJKLMN " } , " Action ": " s3:GetObject ", " Resource ": " arn:aws:s3:::a-service-static/* " } ] } OAIはCloudFrontで事前に作成しておく必要があります。 ABCDEFGHIJKLMN の部分がOAI毎のユニークなIDです。 OAIという要素(Principal)に対して権限を付与するので、オブジェクトのACLにPublicを設定する必要はありません。むしろ、Publicを指定してしまうとS3のURL( https://a-service-static.s3.amazonaws.com/... )でもアクセス可能になってしまいます。不要なアクセス経路が増えるのは望ましくありません。 ここで更にCloudFront以外からのアクセスを明示的に拒否してみます。 設定箇所:S3 Bucketポリシー { " Version ": " 2012-10-17 ", " Statement ": [ { " Sid ": " DenyGet ", " Effect ": " Deny ", " NotPrincipal ": { " AWS ": " arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ABCDEFGHIJKLMN " } , " Action ": [ " s3:Get* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] } , { " Effect ": " Allow ", " Principal ": { " AWS ": " arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ABCDEFGHIJKLMN " } , " Action ": " s3:GetObject ", " Resource ": " arn:aws:s3:::a-service-static/* " } ] } NotPricipal を使って「OAI以外の要素からの参照」を全て却下します。実際の所、このままだとWebコンソールなどでBucketの中を覗くこともできなくなるため、後で少し変更します。 画像の更新はVPCエンドポイント経由に限定する 次に画像の更新をVPCエンドポイント経由に限定してみます。更新をVPCエンドポイント経由に限定することで、不正な更新・削除を防ぐ効果を期待します。 設定箇所:S3 Bucketポリシー { " Version ": " 2012-10-17 ", " Statement ": [ { " Sid ": " DenyUpdate ", " Effect ": " Deny ", " Principal ": " * ", " NotAction ": [ " s3:Get* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] , " Condition ": { " StringNotEquals ": { " aws:sourceVpce ": " vpce-12345678 " } } } ] } (参照をOAI経由に限定する部分は省略しています) まず Principal です。 " Principal ": " * ", 任意の要素を対象とします。 次に NotAction です。 " NotAction ": [ " s3:Get* ", " s3:List* " ] , 参照系の否定で更新系を表現します。 最後に Condition です。VPCエンドポイントを使わない場合はNATなどのgIPを指定しますが、VPCエンドポイントを通す場合はVPCエンドポイントIDで管理します。 " Condition ": { " StringNotEquals ": { " aws:sourceVpce ": " vpce-12345678 " } } つまり、任意の要素からの更新系についてアクセス元が特定のVPCエンドポイント以外であれば全て拒否するというポリシーになります。 VPC内からアクセスできるS3 Bucketを限定する ここまでで、a-service-staticに対するアクセスは「参照:CloudFront経由」「更新:VPCエンドポイント経由」に限定できました。 しかし、これだけではVPC内から a-service-static 以外のBucketに対して書き込み(持ち出し)されてしまうかもしれません。 今度はこれを防止します。VPCエンドポイントを使う場合、VPCエンドポイントポリシーを使ってアクセス先Bucketを限定することが可能です。 デフォルトのVPCエンドポイントポリシーは以下のように任意のアクセスを許可する形になっています。 { " Statement ": [ { " Action ": " * ", " Effect ": " Allow ", " Resource ": " * ", " Principal ": " * " } ] } 明示的にアクセス先を限定します。 { " Version ": " 2008-10-17 ", " Statement ": [ { " Effect ": " Deny ", " Principal ": " * ", " NotAction ": [ " s3:Get* ", " s3:List* " ] , " NotResource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] } , { " Effect ": " Deny ", " Principal ": " * ", " Action ": [ " s3:Get* ", " s3:List* " ] , " NotResource ": [ " arn:aws:s3:::apt.mackerel.io ", " arn:aws:s3:::apt.mackerel.io/* " ] } , { " Effect ": " Allow ", " Principal ": " * ", " Action ": " s3:PutObject ", " Resource ": [ " arn:aws:s3:::a-service-static/* " ] } , { " Effect ": " Allow ", " Principal ": " * ", " Action ": " s3:GetObject ", " Resource ": [ " arn:aws:s3:::apt.mackerel.io/* " ] } ] } 突然、 apt.mackerel.io という要素が登場していますが、順を追って説明します。 まず、大まかな流れは以下のとおりです。 特定のBucket以外に対する更新系を禁止 特定のBucket以外に対する参照系を禁止 特定のBucketに対する更新系を明示的に許可 特定のBucketに対する参照系を明示的に許可 VPCエンドポイントポリシーには注意すべき点が2つ存在します。 1つ目は「明示的な許可」です。アクセスを許可する要素を明示的に指定しない場合、VPCの内側に存在する要素(例えばEC2インスタンス)が対象へのアクセス権限を持っていたとしてもアクセスが却下されてしまいます。 2つ目は「自己所有以外のS3 Bucketに対する考慮」です。弊社で採用している 監視ツール:Mackerel を例にします。 MackerelのDebianパッケージは apt.mackerel.io というドメインからダウンロードされますが、これの実体はS3 Bucketです。ポリシーで明示しておかないと、インストールに失敗します。 このように、VPCエンドポイントポリシーを設定する場合は自己所有以外のS3 Bucketへのアクセスも洗い出しておかないと、構成管理などで問題となります。これを踏まえて読み解きます。 a-service-static以外への更新系を全て禁止 事前に定められたBucket以外への書き込みを禁止 apt.mackerel.io以外への参照系を全て禁止 運用上必要なファイル以外の読み込みを禁止 a-service-staticへの更新系を明示的に許可 apt.mackerel.ioへの参照系を明示的に許可 とはいえ、あくまでもBucketへのアクセスだけなので、どこかの知らないサーバに対してWebAPIで持ち出し。といった不正な通信を防ぐ効果はありません。 IAMユーザは、運用作業でデータを削除することがある 最後はユーザ操作に対するフォローです。 ここまでの設定で、アプリケーションに必要な権限は明示的に指定できました。しかし、実運用において、これだけではあまりにも窮屈すぎます。 WebコンソールからBucketの中を確認し、時には人の手で更新する運用が発生するかもしれません。そのため、管理者が触れる用にポリシーを変更します。 設定箇所:S3 Bucketポリシー { " Version ": " 2012-10-17 ", " Statement ": [ { " Sid ": " DenyUpdate ", " Effect ": " Deny ", " NotPrincipal ": { " AWS ": [ " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , " NotAction ": [ " s3:Get* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] , " Condition ": { " StringNotEquals ": { " aws:sourceVpce ": " vpce-12345678 " } } } , { " Sid ": " DenyGet ", " Effect ": " Deny ", " NotPrincipal ": { " AWS ": [ " arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ABCDEFGHIJKLMN ", " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , " Action ": [ " s3:Get* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] } ] } わかりづらいですが、NotPrincipalにIAMユーザが追加されています。 " Sid ": " DenyUpdate ", " Effect ": " Deny ", " NotPrincipal ": { " AWS ": [ " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , " Sid ": " DenyGet ", " Effect ": " Deny ", " NotPrincipal ": { " AWS ": [ " arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ABCDEFGHIJKLMN ", " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , あとはこのユーザがS3に対する適切な権限を持っていれば、Webコンソールから参照・更新が可能です。 設定箇所:IAMユーザ { " Version ": " 2012-10-17 ", " Statement ": [ { " Action ": [ " s3:GetObject* ", " s3:PutObject* ", " s3:DeleteObject* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static/* ", " arn:aws:s3:::a-service-static " , ] , " Effect ": " Allow " }, { " Action ": [ " s3:ListAllMyBuckets " ] , " Resource ": [ " arn:aws:s3:::* " ] , " Effect ": " Allow " } ] } AssumeRoleを使うことで、ユーザ一人ひとりを列挙しない方法もありますが、本記事の範囲を外れるため割愛します 折角なのでもうひと工夫します。IAMユーザに対して権限を付与する場合、awscliによるアクセスについても検討する必要があります。 ユーザによって発行されたアクセストークンがあれば実際が誰であってもアクセスできるためです。トークンの発行を禁止するというのも1つの手段ですが、ここではIP制限をかけることでリスク低減を図ります。 設定箇所:S3 Bucketポリシー { " Version ": " 2012-10-17 ", " Statement ": [ { " Sid ": " DenyGet ", " Effect ": " Deny ", " NotPrincipal ": { " AWS ": [ " arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ABCDEFGHIJKLMN ", " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , " Action ": [ " s3:Get* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] } , { " Effect ": " Deny ", " Principal ": { " AWS ": [ " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , " Action ": " * ", " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] , " Condition ": { " NotIpAddress ": { " aws:SourceIp ": " xx.xx.xx.xx/32 " } } } ] } Denyポリシーを1つ増やしました。ユーザに対して、特定のIP以外からのアクセスを禁止するというものです。これをオフィスのgIPとすることで、執務エリア、もしくはVPNを貼った状態でなければアクセスできないという状態を作ることが可能です。 データは暗号化する おまけで、データの暗号化についてもポリシーで強制してしまいましょう。 S3はオブジェクトを保存する際に様々な手法でそれを暗号化することができます。幾つかの暗号化方式に対しては、ポリシーでそれの利用を強制することが可能です。ここでは、SSE-AES256を強制してます。 設定箇所:S3 Bucketポリシー { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Deny ", " Principal ": " * ", " Action ": " s3:PutObject ", " Resource ": " arn:aws:s3:::a-service-static/* ", " Condition ": { " StringNotEquals ": { " s3:x-amz-server-side-encryption ": " AES256 " } } } ] } 暗号化用のヘッダがない場合、オブジェクトの保存が失敗するようになります。 設定内容まとめ 設定内容をまとめます。 設定箇所:S3 Bucketポリシー { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Deny ", " Principal ": " * ", " Action ": " s3:PutObject ", " Resource ": " arn:aws:s3:::a-service-static/* ", " Condition ": { " StringNotEquals ": { " s3:x-amz-server-side-encryption ": " AES256 " } } } , { " Sid ": " DenyUpdate ", " Effect ": " Deny ", " NotPrincipal ": { " AWS ": [ " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , " NotAction ": [ " s3:Get* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] , " Condition ": { " StringNotEquals ": { " aws:sourceVpce ": " vpce-12345678 " } } } , { " Sid ": " DenyGet ", " Effect ": " Deny ", " NotPrincipal ": { " AWS ": [ " arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ABCDEFGHIJKLMN ", " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , " Action ": [ " s3:Get* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] } , { " Effect ": " Allow ", " Principal ": { " AWS ": " arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ABCDEFGHIJKLMN " } , " Action ": " s3:GetObject ", " Resource ": " arn:aws:s3:::a-service-static/* " } , { " Effect ": " Deny ", " Principal ": { " AWS ": [ " arn:aws:iam::123456789098:user/ops-user1 ", " arn:aws:iam::123456789098:user/ops-user2 " ] } , " Action ": " * ", " Resource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] , " Condition ": { " NotIpAddress ": { " aws:SourceIp ": " xx.xx.xx.xx/32 " } } } ] } 設定箇所:VPCエンドポイントポリシー { " Version ": " 2008-10-17 ", " Statement ": [ { " Effect ": " Deny ", " Principal ": " * ", " NotAction ": [ " s3:Get* ", " s3:List* " ] , " NotResource ": [ " arn:aws:s3:::a-service-static ", " arn:aws:s3:::a-service-static/* " ] } , { " Effect ": " Deny ", " Principal ": " * ", " Action ": [ " s3:Get* ", " s3:List* " ] , " NotResource ": [ " arn:aws:s3:::apt.mackerel.io ", " arn:aws:s3:::apt.mackerel.io/* " ] } , { " Effect ": " Allow ", " Principal ": " * ", " Action ": " s3:PutObject ", " Resource ": [ " arn:aws:s3:::a-service-static/* " ] } , { " Effect ": " Allow ", " Principal ": " * ", " Action ": " s3:GetObject ", " Resource ": [ " arn:aws:s3:::apt.mackerel.io/* " ] } ] } 設定箇所:インスタンスプロファイル { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Action ": [ " s3:PutObject " ] , " Resource ": [ " arn:aws:s3:::a-service-static/* " ] } , 設定箇所:IAMユーザ { " Version ": " 2012-10-17 ", " Statement ": [ { " Action ": [ " s3:GetObject* ", " s3:PutObject* ", " s3:DeleteObject* ", " s3:List* " ] , " Resource ": [ " arn:aws:s3:::a-service-static/* ", " arn:aws:s3:::a-service-static " , ] , " Effect ": " Allow " }, { " Action ": [ " s3:ListAllMyBuckets " ] , " Resource ": [ " arn:aws:s3:::* " ] , " Effect ": " Allow " } ] } まとめ 本記事ではS3に対して、可能な限り明示的に権限管理を行いました。 権限管理は案件毎に要件が全く異なるため、本記事の内容がそのまま使えることはないと思いますが、記述の参考にでもなれば幸いです。 権限管理は、正直複雑ですし窮屈になりがちです。とはいえノーガードというわけにもいきません。 これからもより良いバランスを探していきたいと思います。 終わりに VASILYでは安全かつ自由なインフラ管理に興味があるエンジニアを募集しています。 興味ある方は以下のリンクからご応募ください。
アバター
こんにちは! 食欲の秋でいつもお腹が空いているバックエンドエンジニアのりほやんです。 VASILYでは11月の13,14日に第1回開発合宿を開催しました! とても楽しい合宿になったため、本記事では開発合宿の様子をレポートします! 出発 今日はエンジニアとデザイナーで開発合宿です!湯河原いくよー! #VASILY開発合宿 — キュン/今村雅幸 (@kyuns) 2017年11月13日 品川駅に全員集合して、踊り子号で湯河原に向かいます。 行きの電車の中からすでに開発を始めている人が…! 開発始まってる #VASILY開発合宿 pic.twitter.com/AIf4OkGY0H — キュン/今村雅幸 (@kyuns) 2017年11月13日 到着 1時間ほどで湯河原に到着です! 到着後は、お昼ごはん! みんな昼ごはん同じ場所〜〜 #vasily開発合宿 pic.twitter.com/Jg6Bu3roG7 — りほ (@rllllho) 2017年11月13日 #vasily開発合宿 pic.twitter.com/Bbzv35XaD6 — ホリエ (@Horie1024) 2017年11月13日 お蕎麦美味しそう。 ねこかわいい。 普段、あまり喋る機会がないメンバーとも交流することができました! 宿到着 昼ごはんを食べた後は、駅からタクシーで宿に向かいます。 10分ほどで今回宿泊するお宿、おんやど恵さんに到着です! 中に入るとロビーに、昔ながらの野球盤があったり今流行りのフォトプロップスがありひと盛り上がり。 開発開始 CTO今村から開会宣言を行ってもらい開発スタートです! 今回は『普段できない開発を行う』をテーマに、各自事前に開発テーマを考えてきてもらいました。 2日目に、1人5分程度のLTで、開発合宿で行ったことを各自発表してもらいます。 なんと今回は、CTO今村から特別に今村賞を用意していただきました! 今村賞めざしてみんな頑張ります! もくもく! #vasily開発合宿 pic.twitter.com/E75sdGqOpI — りほ (@rllllho) 2017年11月13日 夜ご飯 一通り開発したところで夜ご飯です! おいしそうなご飯が並びます。 かんぱーい! みなさん思い思いに楽しんでいます。 日本酒を楽しむ人たちもいます。 みなさん自由に夜ご飯を楽しんだようです! 開発再開? 夜ご飯後は自由時間です。 お酒飲んだり、開発したりみなさん思い思いに楽しんでいます。 獺祭解禁。 お酒飲みながら開発も楽しそうです! また、おんやど恵さんには足湯がありwifiもつながるため、足湯コーディングしている人もいました。 足湯コーディングが最高すぎる #vasily開発合宿 — ホリエ (@Horie1024) 2017年11月13日 足湯ビール開発♨️🍺👨‍💻 最高としか。 #VASILY開発合宿 https://t.co/xas74gWytq — WorldDownTown (@WorldDownTown) 2017年11月13日 足湯大好評です! 朝ごはん 徹夜した人もいたようですが、私は朝5時に起きて朝風呂に入っていました。朝風呂最高でした。 ぐっすり寝た後は、朝ごはん! 朝ごはんもおいしかったです! LT発表 最後は、開発合宿の成果発表LTです! みなさん、今村賞目当てにやる気満々です。 発表順は、CTOがその場でシャッフルして決めます。 水着の着せ替えができるGANの開発を行った人もいたり、 VASILY社員のためのシャッフルランチアプリをデザインする人もいました! みなさん限られた時間で開発をやりきり、デモを含めた発表までやりきっていました! 表彰式 最後にCTO今村からの表彰です。 特別賞:アナログ計算機はニューラルネットワークの夢を見るか(塩崎) バックエンドエンジニアの塩崎は、アナログ計算機の手法を用いて階層型ニューラルネットワークの実装を行いました。ニューラルネットワークの構成要素をOPアンプで実装し、動作確認を回路シミュレータLTSpide上で行いました。 「内容はよくわからないけど評価しておかないともったいない気がする」という理由で特別賞を受賞しました! 第3位:コーポレートサイトのNuxt.js化(権守) フロントエンドエンジニアの権守は、弊社コーポレートサイトをNuxt.jsによる実装に置き換える試みをしました。 Nuxt.jsはVue.jsアプリケーションを構築するためのフレームワークです。 Vue.js単体で構築されていたコーポレートサイトをNuxt.jsに乗せることでSSR(サーバーサイドレンダリング)を簡単に実現できました。 「完成度が高く、実際にすぐに導入できるレベル」という点が特に評価され、3位に選ばれました。 第2位 :実験経過を監視するsomething(中村) データサイエンスチームの中村は、実験経過を監視するextensionを開発しました。 ネットワークの学習中にフレームワークが吐き出すメトリクスをslackにpostして学習のステータスを把握できるようにしました。 「現状抱える課題をうまく解決しており、すでに稼働している」という点が評価され、2位に選ばれました。 第1位 :Rustクライアントサイドフレームワーク龍(茨木) フロントエンドチームの茨木は、Rustをもちいてフレームワーク『Rju』を開発しました。 rjuはRust+ WebAssemblyで動くクライアントサイドフレームワークです。 いわばReact.jsやVue.jsのRust版で、Rustのコード中にHTMLを書くだけでそれが自動でレンダリングされます。 型安全なのに加え、Rustのプラグインマクロ機能によりビルドツールが不要なことが特徴です。 「限られた時間のなかで新しい言語に取り組み、かつ開発物の利点をうまく伝えられたこと、デモの完成度が高かったこと」が評価され見事1位に選ばれました。 以上4名が今村賞を受賞しました! さすがです! 最後に今村から、 「本当にみんなに賞あげたいぐらい良い発表だった」というお言葉が。 みなさん本当におもしろい発表が多かったです。 開発合宿の成果をVASILYアドベントカレンダーに書く人もいるそうなので、ぜひこちらもぜひチェックしてください! qiita.com 帰宅 最後に記念写真をパチリ。 宿を後にして、湯河原駅の周辺でお土産を買います。 個人的に、シュークリームと温泉まんじゅうが本当に美味しかったのでぜひ食べて帰ってもらいたいです。 お土産を買い終えたら、踊り子に乗って帰宅です! 帰りの車内でも、乾杯している人たちがいました。 品川に到着したら解散です! お疲れ様でした! 感想 開発合宿の参加者に、アンケート取ってみた結果です。 開発合宿の満足度を教えてください みなさん大満足なようです。よかったー! 良かったこと 宿が近い&快適で良かった! お宿が綺麗だった 業務のタスク以外の研究開発にまとまった時間を充てられたのが良かった 開発部屋を深夜も使えた。 今村賞があったこと デザイナー組も一緒だったこと 開発内容のバリエーションが多く、LTを聞くことでもインプットが出来た 足湯でwifiが使える 集中できる環境だったこと。息抜きの温泉もとても良かった 次回改善したいこと 2泊にして、飲み会は開発と発表が終わってからにしたい 開発時間をもっと多く取りたい LT後に宴会があれば、開発も宴会も捗りそう 平日ランチで閉まってる店が多い 開発時間がもうちょっと欲しかったという意見が多かったようです。 次回開催時に活かしたいと思います! 気をつけること 実際行ってみていくつか困ったことがあったのでご紹介します。 昼ごはんやっている店が少なかった 駅や宿から徒歩圏内で行けるごはん処が少なかったのと、定休日が重なり昼ごはんの場所探しに苦労しました。 事前にもっと調べていけばよかったなと思ったのでぜひ皆さんは調べてから行ってください! コンビニが遠い(徒歩12分ほど) おんやど恵さんから一番近いコンビニ(セブンイレブン)までは徒歩12分程かかります。 宿内に売店があり、コアラのマーチやチョコレート、ハーゲンダッツなどが売っているのでそちらを活用するのも良いと思います。 まとめ 第1回VASILY開発合宿のレポートでした! 美味しいご飯食べて温泉に入って、普段あまり喋ったことないメンバーとも交流することができ、とても楽しかったです。 また、普段と違う環境で、普段できない開発ができ良い開発合宿になりました。 みなさんもぜひ開発合宿を開催してみてください! 最後に VASILYでは一緒に働くエンジニアを募集しています! ご興味のある方は下のリンクからご応募ください!
アバター
課金とPush通知攻略に邁進中のじょーです。 今回は、ひとつのアプリに自動更新購読型と消耗型を共存させたときのサーバーサイドで行うレシート検証のTipsを紹介します。 自動更新購読型課金のサーバーサイド実装について 自動更新購読型課金単体で実装する場合はこちらの記事が参考になります。 (昔書いた記事で古い情報がある場合があります) 下記の記事では月額課金と呼んでいますが、自動更新購読と同義です。 tech.vasily.jp 消耗型課金のサーバーサイド実装について 消耗型課金単体で実装する場合はこちらの記事が参考になります。 tech.vasily.jp 自動更新購読型課金と消耗型課金を共存させるときのレシート検証 「レシート検証って何?」という疑問については、上記にリンクを載せた記事にすでに書いてあるので、この記事では触れないことにします。 自動更新購読型課金と消耗型課金を同じアプリに共存させようとした場合、気になるのがレシート検証の仕方です。 AppStoreのサーバーから返ってくるレシートの形式によって、レシート検証の仕方がそれぞれ単独で存在していたときとは異なってきます。 例えば、レシートはそれぞれ発行されるのか?ひとつのレシートにどちらの情報も返ってくるのか?その場合新しいJSONキーが増えるのか?等が気になる事項です。 さっそく二種類の購入型を共存させたときに実際に返ってくるレシートを見てみましょう。 共存させたときに返るレシートの内容 { " status "=> 0 , " environment "=>" Sandbox ", " receipt "=> { "receipt_type"=>" ProductionSandbox ", "adam_id"=> 0 , "app_item_id"=> 0 , "bundle_id"=>" your bundle id ", "application_version"=>" 36 ", "download_id"=> 0 , "version_external_identifier"=> 0 , "receipt_creation_date"=>" 2017-10-23 12:15:47 Etc/GMT ", "receipt_creation_date_ms"=>" 1508760947000 ", "receipt_creation_date_pst"=>" 2017-10-23 05:15:47 America/Los_Angeles ", "request_date"=>" 2017-10-25 07:03:22 Etc/GMT ", "request_date_ms"=>" 1508915002492 ", "request_date_pst"=>" 2017-10-25 00:03:22 America/Los_Angeles ", "original_purchase_date"=>" 2013-08-01 07:00:00 Etc/GMT ", "original_purchase_date_ms"=>" 1375340400000 ", "original_purchase_date_pst"=>" 2013-08-01 00:00:00 America/Los_Angeles ", "original_application_version"=>" 1.0 ", "in_app"=> [{ " quantity "=>" 1 ", "product_id"=>" your product id ", "transaction_id"=>" 10000003161787 ", "original_transaction_id"=>" 10000003161787 ", "purchase_date"=>" 2017-07-18 03:20:05 Etc/GMT ", "purchase_date_ms"=>" 1500348005000 ", "purchase_date_pst"=>" 2017-07-17 20:20:05 America/Los_Angeles ", "original_purchase_date"=>" 2017-07-18 03:20:05 Etc/GMT ", "original_purchase_date_ms"=>" 1500348005000 ", "original_purchase_date_pst"=>" 2017-07-17 20:20:05 America/Los_Angeles ", "is_trial_period"=>" false " } , { " quantity "=>" 1 ", "product_id"=>" your product id ", "transaction_id"=>" 10000003457411 ", "original_transaction_id"=>" 10000003457411 ", "purchase_date"=>" 2017-10-23 12:15:43 Etc/GMT ", "purchase_date_ms"=>" 1508760943000 ", "purchase_date_pst"=>" 2017-10-23 05:15:43 America/Los_Angeles ", "original_purchase_date"=>" 2017-10-23 12:15:46 Etc/GMT ", "original_purchase_date_ms"=>" 1508760946000 ", "original_purchase_date_pst"=>" 2017-10-23 05:15:46 America/Los_Angeles ", "expires_date"=>" 2017-10-23 12:20:43 Etc/GMT ", "expires_date_ms"=>" 1508761243000 ", "expires_date_pst"=>" 2017-10-23 05:20:43 America/Los_Angeles ", "web_order_line_item_id"=>" 1000000036650295 ", "is_trial_period"=>" false " }] , "original_json_response"=> { ... }} , "latest_receipt_info"=> [{ " quantity "=>" 1 ", "product_id"=>" your product id ", "transaction_id"=>" 10000003161780 ", "original_transaction_id"=>" 10000003161780 ", "purchase_date"=>" 2017-07-18 03:20:05 Etc/GMT ", "purchase_date_ms"=>" 1500348005000 ", "purchase_date_pst"=>" 2017-07-17 20:20:05 America/Los_Angeles ", "original_purchase_date"=>" 2017-07-18 03:20:05 Etc/GMT ", "original_purchase_date_ms"=>" 1500348005000 ", "original_purchase_date_pst"=>" 2017-07-17 20:20:05 America/Los_Angeles ", "is_trial_period"=>" false " } , { " quantity "=>" 1 ", "product_id"=>" your product id ", "transaction_id"=>" 10000003457411 ", "original_transaction_id"=>" 10000003457411 ", "purchase_date"=>" 2017-10-23 12:15:43 Etc/GMT ", "purchase_date_ms"=>" 1508760943000 ", "purchase_date_pst"=>" 2017-10-23 05:15:43 America/Los_Angeles ", "original_purchase_date"=>" 2017-10-23 12:15:46 Etc/GMT ", "original_purchase_date_ms"=>" 1508760946000 ", "original_purchase_date_pst"=>" 2017-10-23 05:15:46 America/Los_Angeles ", "expires_date"=>" 2017-10-23 12:20:43 Etc/GMT ", "expires_date_ms"=>" 1508761243000 ", "expires_date_pst"=>" 2017-10-23 05:20:43 America/Los_Angeles ", "web_order_line_item_id"=>" 1000000036650295 ", "is_trial_period"=>" false " } , . . . { " quantity "=>" 1 ", "product_id"=>" your product id ", "transaction_id"=>" 10000003464111 ", "original_transaction_id"=>" 10000003457411 ", "purchase_date"=>" 2017-10-25 05:54:36 Etc/GMT ", "purchase_date_ms"=>" 1508910876000 ", "purchase_date_pst"=>" 2017-10-24 22:54:36 America/Los_Angeles ", "original_purchase_date"=>" 2017-10-23 12:15:46 Etc/GMT ", "original_purchase_date_ms"=>" 1508760946000 ", "original_purchase_date_pst"=>" 2017-10-23 05:15:46 America/Los_Angeles ", "expires_date"=>" 2017-10-25 06:54:36 Etc/GMT ", "expires_date_ms"=>" 1508914476000 ", "expires_date_pst"=>" 2017-10-24 23:54:36 America/Los_Angeles ", "web_order_line_item_id"=>" 1000000036672623 ", "is_trial_period"=>" false " } , { " quantity "=>" 1 ", "product_id"=>" your product id ", "transaction_id"=>" 10000003464307 ", "original_transaction_id"=>" 10000003457411 ", "purchase_date"=>" 2017-10-25 06:54:36 Etc/GMT ", "purchase_date_ms"=>" 1508914476000 ", "purchase_date_pst"=>" 2017-10-24 23:54:36 America/Los_Angeles ", "original_purchase_date"=>" 2017-10-23 12:15:46 Etc/GMT ", "original_purchase_date_ms"=>" 1508760946000 ", "original_purchase_date_pst"=>" 2017-10-23 05:15:46 America/Los_Angeles ", "expires_date"=>" 2017-10-25 07:09:36 Etc/GMT ", "expires_date_ms"=>" 1508915376000 ", "expires_date_pst"=>" 2017-10-25 00:09:36 America/Los_Angeles ", "web_order_line_item_id"=>" 1000000036672630 ", "is_trial_period"=>" false " }] , "pending_renewal_info"=> [{ "auto_renew_product_id"=>" your product id ", "original_transaction_id"=>" 1000000345741172 ", "product_id"=>" your product id ", "auto_renew_status"=>" 1 " }]} レシートが一緒になって返ってくる! 返ってきたレシートをよーーーく見ると、どちらの情報も1つのレシートに混ぜこぜになって返ってきています。自動更新購読型と消耗型それぞれ別のレシートが発行されるというわけでも、新しいJSONのキーが増えるわけでもありません。 特に、 in_app と latest_receipt_info の購入履歴を保持する配列に各購入情報が混ざって入ってきますので、注意が必要です。 自動更新購読型と消耗型の情報の見分け方 一緒に返ってくることがわかったところで、購入型の違いによってどのような情報の違いがあるかを見ていきます。 下記のJSONが、それぞれの購読型で返ってくるトランザクションごとの情報です。( in_app や latest_receipt_info の配列の中身) 消耗型課金で返る値 { " quantity "=>" 1 ", "product_id"=>" card_10 ", "transaction_id"=>" 10000003161787 ", "original_transaction_id"=>" 10000003161787 ", "purchase_date"=>" 2017-07-18 03:20:05 Etc/GMT ", "purchase_date_ms"=>" 1500348005000 ", "purchase_date_pst"=>" 2017-07-17 20:20:05 America/Los_Angeles ", "original_purchase_date"=>" 2017-07-18 03:20:05 Etc/GMT ", "original_purchase_date_ms"=>" 1500348005000 ", "original_purchase_date_pst"=>" 2017-07-17 20:20:05 America/Los_Angeles ", "is_trial_period"=>" false " } 自動更新購読型で返る値 { " quantity "=>" 1 ", "product_id"=>" monthly_paid_1 ", "transaction_id"=>" 10000003457411 ", "original_transaction_id"=>" 10000003457411 ", "purchase_date"=>" 2017-10-23 12:15:43 Etc/GMT ", "purchase_date_ms"=>" 1508760943000 ", "purchase_date_pst"=>" 2017-10-23 05:15:43 America/Los_Angeles ", "original_purchase_date"=>" 2017-10-23 12:15:46 Etc/GMT ", "original_purchase_date_ms"=>" 1508760946000 ", "original_purchase_date_pst"=>" 2017-10-23 05:15:46 America/Los_Angeles ", "expires_date"=>" 2017-10-23 12:20:43 Etc/GMT ", "expires_date_ms"=>" 1508761243000 ", "expires_date_pst"=>" 2017-10-23 05:20:43 America/Los_Angeles ", "web_order_line_item_id"=>" 1000000036650295 ", "is_trial_period"=>" false " } 上記で確認できる通り、自動更新購読型と消耗型の見分けがつくkeyは expires_date の有無と、 product_id です。 なので、このどちらかで自動更新購読型か消耗型かを見分けてそれぞれの処理をする必要があります。 各購読型で見るべき項目 in_app と latest_receipt_info はそれぞれの購入型によって配列の中に購入情報が残る条件が違います。 in_app latest_receipt_info 自動更新購読型 購入情報の一部が無期限に残る 課金情報の更新の履歴がすべて残る 消耗型 トランザクションが完了していない情報のみ残る ? Appleの公式ドキュメントには、消耗型は in_app を参照し、自動更新購読型に関しては latest_receipt_info で自動更新された最新のレシートを取得してくださいという説明があります。 消耗型だけのときは、レシートに latest_receipt_info は返ってきません。なので、消耗型は in_app しか見ない、自動更新購読型は latest_receipt_info しか見ないという実装でいいと思います。 ただ、それぞれのkeyの中にはそれぞれの購読型の購入情報が混ざって入ってきてしまうので(なぜ latest_receipt_info の配列の中に消耗型の情報が返ってくるかはわかりません。。)、例えば expires_date というkeyが必ずあるというていでコードを書いてしまうと事故になります。 AppStoreのレシート問い合わせ AppStoreのサーバーへレシートを問い合わせる際のとても大きな注意点が1つあります。これは知らないと大事故になると思うのでぜひ事前に把握しておきたいポイントです。 AppStoreのサーバーへのレシート問い合わせは、HTTPのPOSTリクエストを送ることで問い合わせることができます。 その際に、自動購読型と消耗型それぞれが単独で存在している場合のレシートを問い合わせに必要なリクエストbodyには、下記の違いがあります。 消耗型 key 値 サンプル receipt-data Base64エンコードしたレシート情報 MIIjwgYJKoZIhvcNAQcCoIIjszCCI… 自動購読型 key 値 サンプル receipt-data Base64エンコードしたレシート情報 MIIjwgYJKoZIhvcNAQcCoIIjszCCI… passward アプリケーションの共有鍵 fea2ebde5... 表を見ると分かる通り、消耗型のみのときには共有鍵が不要なのに対し、自動購読型では共有鍵をbodyに指定する必要があります。 では、両購入型を共存させた場合、どのようにリクエストする必要があるでしょうか? レシートがひとつしかないとなると、消耗型を購入した際のレシートでAppStoreに問い合わせる際にも共有鍵が必要になるかが気になるポイントです。 この問題を、下記の手順でテストしてみました。 課金履歴のないAppleのアカウントを用意 消耗型アイテムを購入 その後自動購読型アイテムを購入 消耗型アイテムを購入 結果は下記のようになりました。 状態 共有鍵の必要性 2 必要なし 3 必要 4 必要 このように、自動購入型を購入する前と後で、消耗型アイテムを購入した際のレシート問い合わせ時の共有鍵の必要性が変わってきます。 なので、自動購入型を導入した時点でレシートを問い合わせる際は共有鍵を必ず送るようにしておく必要があります。 実際の課金構造 購入型が混ざっている場合、ネイティブからの課金リクエストは3種類あります。 消耗型のアイテムを購入したとき 自動更新購読型のアイテムを購入したとき StoreKitで発火する、未処理のレシートが存在するとき です。 1, 2に関しては今まで通りそれぞれの購入型のレシートを処理すれば問題ありません。 しかし、3に関しては注意が必要です。なぜなら、StoreKit経由でのリクエストは消耗型の未処理トランザクションによって発火したものか、自動更新購読型の更新によって発火しているリクエストかがわからないためです。 なので、3の場合は下記の図のように、自動更新購読型と消耗型の両方の購入情報をチェックする必要があります。 図1 StoreKitで発火する処理 まとめ 自動購読型と消耗型を共存させたとき、レシートはひとつに統合される 自動購読型の存在の有無によってAppStoreのサーバーにレシートを問い合せる際のbodyの内容が変わる StoreKitで発火する末トランザクションがある場合のリクエストは自動更新購読型と消耗型のどちらの情報もチェックする必要がある これらを把握して、事故のない課金ライフを送りましょう! 終わりに VASILYエンジニアはお金周りに興味があるエンジニアを募集しています。 興味ある方は以下のリンクからご応募ください。
アバター
こんにちは、データチームの後藤です。 VASILYデータチームは2017年11月8日〜11日にかけて、東京大学の本郷キャンパスで行われた第20回情報論的学習理論ワークショップ(以下、IBIS2017)に参加しました。本記事では、発表の様子や参加した感想をお伝えしたいと思います。 IBIS2017について IBIS2017 IBISは機械学習に関する国内最大規模の学会です。機械学習や統計学、情報理論などの理論研究や、機械学習の応用的な研究が対象となります。参加登録数は去年の約2倍の1036人となっており、その規模は加速的に大きくなっています。 初日の懇親会では「IBIS年代記」と題して、20年の歴史を振り返るトークも行われました。絶滅の危機に瀕しているトキ(IBIS)は絶滅寸前のニッチな研究者集団という意味を表している?そうですが、今となってはビッグデータや深層学習のブームと重なり、絶滅寸前の影を微塵も感じさせません。 IBIS年代記 以下は、IBIS2017のプログラムです。 日付 内容 11月8日(水) 招待講演1:Nathan Srebro 招待講演2:Edward Albert Feigenbaum 企画:国際会議採択論文 懇親会 11月9日(木) 企画:自然言語処理への機械学習の応用 ポスターセッション1 企画:実社会への機械学習の応用 11月10日(金) 企画:画像処理への機械学習の応用 ポスターセッション2 招待講演:渡辺澄夫 11月11日(土) チュートリアル 我々は昨年のIBIS2016で 「VAEとGANを活用したファッションアイテムの特徴抽出と検索システムへの応用」 というタイトルで発表し、多くの方々に成果を伝えることができました。その後もサービスの研究・開発を進め、今年はその中から2つの成果を発表することにしました。 発表 D1-50: 学習可能なマスクを用いた柔軟な類似度計算手法 ディスカッショントラック1日目では、インターン生の上月が「学習可能なマスクを用いた柔軟な類似度計算手法」というタイトルで発表しました。 概要 畳み込みニューラルネットワーク(CNN)に画像と属性の組を学習させ、類似度を計算しやすい特徴量を得ることを目指します。その際、特徴量を出力する層に属性の内容に応じたマスクをかけ、属性の内容と特徴量の次元に対応関係を持たせます。例えば、靴のヒールの高さと靴の性別は性質の異なる属性なので、マスクを切り替えて特徴量を抽出します。この工夫により、属性の内容ごとにモデルを作成する手間が省け、一つのモデルで複数の属性を扱うことができるため、サービスへの実装の観点からメンテナンス性の高いモデルを得ることが期待できます。 一方で、靴のヒールの高さと性別は完全に独立な性質のものではなく、互いに共有している特徴も含みます。そこで今回は独自の工夫として、属性固有の独立した特徴と各属性に共有される特徴を明示的に分けて学習するようマスクを設計し、より解釈性の高い表現を得ることを試みました。 実験のデータセットにはUT Zappos50Kという靴の画像と属性(靴の種類、靴の閉め方、性別、ヒールの高さ)を利用しています。以下の図はわかりやすさのために、CNNから得られた特徴量の空間を二次元で表現したものです。サンダル、スニーカー、ブーツといった靴の種類に関する属性は、それぞれ距離を置いて位置づけられています。この技術は属性を変化させながら画像検索する機能の実装などに利用できます。 発表ポスター PDF版はこちら D2-23: ブランドコンセプトを反映したファッションアイテム類似検索 ディスカッショントラック2日目に、中村が「ブランドコンセプトを反映したファッションアイテム類似検索」というタイトルで発表しました。 概要 ファッションに関する商品データには、ブランド毎にサンプル数の偏りがあるため、ブランド判別問題において十分な学習サンプルが得られないブランドが存在します。 この問題を解決するために、IQONユーザーのブランドLIKEのデータをword2vecに学習させて得られる、ブランドの分散表現を活用します。ブランドの分散表現をクラスタリングしてメンバーの内容を解釈すると、データの生成元であるユーザーのペルソナが浮かび上がってきます。 Deep Visual Semantic Embedding Model (DeViSE)を使って、CNNで抽出する画像特徴量がブランドの分散表現に寄るようにモデルの学習を進めます。 このようにして学習したCNNはブランドの意味表現を反映した画像特徴量を抽出するようになることが期待されます。 以下の図は、クラスタのサンプルサイズとAccuracy(左)、Precision@5(右)の関係を表しています。赤線上が等しいスコアを意味し、サンプルサイズを色で表しています。図の赤線の上側に位置する点はDeViSEが勝っているクラスタになります。Accuracyの観点では、ほとんどすべての場合でDeViSEよりもSoftmaxのほうが勝っています。一方、Precisionの観点では、主にサイズの小さいクラスタ(青点)に対するPrecisionに大きな向上がみられることがわかるかと思います。Presicionが重要になるタスクにおいて、サンプルサイズの小さいブランドを分散表現のような補助情報を用いて学習することで上手く活用できることがわかりました。 発表ポスター PDF版はこちら その他の研究 [招待講演1] Supervised Learning without Discrimination 講演者:Nathan Srebro先生 Toyota Technological Institute at Chicago(TTIC) の研究者、Srebro先生による講演です。機械学習システムが差別的判断をしないためにどう取り組めばよいかというフレームワークのお話でした。信用評価や広告の出し分けなどのタスクにおいて、意図せず人種や性別の差別をしている機械学習システムが存在します。差別につながる変数Aを利用しないという方法も考えられますが、予測値YはAに依存する可能性があります。このような問題設定の場合に、講演中で紹介されたdemographic parityやequalized oddsなどの考え方が参考になります。 以下が参考文献です。 "Equality of Opportunity in Supervised Learning" https://arxiv.org/abs/1610.02413 "Learning Non-Discriminatory Predictors" https://arxiv.org/abs/1702.06081 [招待講演2] Advice to Young and New AI Scientists 講演者:Edward Albert Feigenbaum先生 「エキスパートシステムの父」こと、Feigenbaum先生から、若手研究者へのメッセージです。講演では、若手研究者に対して、大勢の人が取り組む積み上げ型のサイエンスではなく、分野にブレイクスルーをもたらすような研究を行うよう奨励していました。その際、Deep LearningのようなPerceptual AI(問題に対して反射的に回答するAI)の分野ではなく、課題の多いCognitive AI (深い思考をするAI、判断の説明ができるAI)の分野をやるべきだとのことです。ほかには、ブレイクスルーを起こすには、頭で考えるだけでなくあらゆる可能性を実験で確かめることが大切だというメッセージもありました。 Learning from Complementary Labels (NIPS 2017) 講演者:石田隆さん "Learning from Complementary Labels" https://arxiv.org/abs/1705.07541 データとラベルが正しいかどうかの二値のデータは、多クラスデータの正確なラベル付けよりも簡単に得られます。このようなComplementary Labelを持つデータセットの多クラス判別を学習する枠組みを提案しています。クラウドソーシングなどを利用したデータアノテーションへの応用が利きそうなお話でした。 D1-41: IILasso:相関情報を罰則項に導入したスパースモデリング 発表者:髙田正彬さん Lassoは変数間の相関が強い場合に、互いに相関の強い変数を選択しやすいため、モデルの解釈性や汎化誤差が悪化します。提案手法(Independent Lasso)では、相関情報を罰則項に取り入れて学習します。実験では汎化誤差が改善しているようです。Lassoの一般化にあたり、とても汎用性の高い手法であると思いました。 D1-27: 教育ログデータからの解釈性を重視した潜在スキル推定 発表者:玉野浩嗣さん 学習者が問題に取り組んだ正誤ログから、学習者が持っている潜在的なスキルの時系列変化と、実際に問題を解くのに必要な潜在スキルを同時に推定する手法を提案しています。確率モデルを使った問題の定式化が納得感のあるものとなっており、得られた結果の解釈性も高い研究でした。問題設定に適したオープンデータが少ないために検証に苦労しているようです。 D1-32: Maximum mean discrepancyに基づく分布マッチングを用いた教師なしドメイン適応 発表者:熊谷充敏さん 教師なしでドメイン適応をする研究です。Maximum mean discrepancyに基いて、教師ありで学習した元ドメインと教師なしの目標ドメインの特徴分布がおなじになるように変換則を学習します。目標ドメインのラベルが無くても適応可能である点と、分布を圧縮しないので情報のロスが少ない点が良さそうです。 T2-09: カウントデータに関する多次元ヒストグラムのビン幅最適化 発表者:武藤健介さん カウントデータのヒストグラムの最適なビン幅を推定する研究です。MISEからビン幅に依存する部分を抽出すると、ビン幅は真の確率密度に依存しないということがわかり、最適なビン幅が求めらます。多次元ヒストグラムでも同様の性質が成り立ち、最適なビン幅が得られるそうです。最適なビン幅と実験機器の分解能を関係付けると必要なカウント数が求められ、実験者が観測時間を設定できるのが良い点です。 T2-16: Generative Adversarial Networksを用いた確率的識別モデルから訓練データ生成分布の推定 発表者:草野光亮さん 公開された予測モデルから訓練データの生成分布を推定するという研究です。学習に利用されたデータが手元にないより厳しい場合でも、ドメインの異なる補助データを利用することで、訓練データ生成分布を推定できることを示しています。例えば、個人情報を学習データに使った場合に、悪意のある攻撃者からデータの分布を推測され悪用されるという危険性を示しています。 D2-12: クラウドソーシングを用いた半教師あり学習のための深層生成モデル 発表者:新 恭兵さん クラウドソーシングによるデータアノテーションはワーカー毎の作業品質にばらつきがあるという欠点があります。ワーカーが付与したラベルの生成プロセスをモデル化し学習することで、精度向上のみならず、ワーカーの特徴をパラメトライズしたり、ワーカーを分類したりすることができるようになります。論文はちょうど投稿したばかりとのことで、まだ読めないようです。 感想 得るものが多い三日間でした。普段は追えていない車の自動運転やロボット分野の研究を学ぶことができてとても刺激的でしたし、レベルの高い研究に感化されてチームとして研究に対する熱意が高まっている状態です。また、VASILYの研究内容やデータに興味を持っている学生たちとお話することができ、今後につながっていく出会いがあったのもよかったです。 業務への応用という意味では、データの少ない・ラベルが無い・ラベルにノイズが多いなど、様々なデータ事情に対する課題に取り組まれている研究が多く、参考になりました。Webから得られるデータを扱うと、このような場面に出くわすことが多々あるからです。また、クラウドソーシングによるデータのアノテーションを想定した研究もあり、効率的に価値の高いデータを生み出す方法には今後も注目していきたいと思いました。 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。本記事のように、業務の一環として研究発表や学会に参加することを認められる環境です。 興味のある方はこちらからご応募ください。
アバター
こんにちは、フロントエンド開発部の荒井です。 先日VASILYでは開発合宿が行われました。本記事では私が合宿で使用したHeadless Chrome + Puppeteerを紹介したいと思います。 開発合宿のテーマ決め 合宿での開発内容は個人に委ねられており、普段出来ない開発を自由に行うことが出来ました。各々興味深いテーマを持ち寄っており、非常に面白い開発合宿でした。私も何をテーマにするか非常に悩みましたが、今後の業務のことも考え、久しく触れていなかったヘッドレスブラウザを使用した開発を行うことにしました。 ヘッドレスブラウザ GUIを持たないブラウザで、フロントエンドの自動テストやSPA(Single Page Application)のスクレイピングにも用いられます。ヘッドレスブラウザとしてはPhantomJSが有名だと思いますが、メインメンテナーが終了を宣言したため、今回はHeadless Chromeを採用しました。ヘッドレス環境でChromeを動作させるHeadless ChromeはChrome 59から導入されています。 Headless Chromeを使用する Headless Chromeを使用するのは非常に簡単です。 Chromeがインストールされていれば、以下の手順で使用することが出来ます。 1. Chromeがインストールされているパスを指定 alias chrome =" /Applications/Google \ Chrome.app/Contents/MacOS/Google \ Chrome " 2. --headlessオプションを付けてChromeを実行 chrome --headless --disable-gpu --screenshot https://www.google.com 上記サンプルでは--screenshotオプションを使用してwww.google.comのスクリーンショットを撮っています。実行すると実行時のディレクトリにscreenshot.pngというスクリーンショットがあるはずです。このように簡単な機能であれば コマンドラインフラグ を使用することで達成出来ます。 Puppeteer コマンドライン機能により、簡単にHeadless Chromeを使用することが出来ました。しかし、実際に自動テストやスクレイピングを行うにはプログラムの記述が必要になってきます。そこで今回使用したのがNode.jsからHeadless Chromeを簡単に扱える Puppeteer です。 PuppeteerにはDev Tools ProtocolでChromiumを制御するAPIが提供されています。Node.jsからHeadless Chromeを扱うためのライブラリはいくつか存在しますが、PuppeteerはそのFAQにもある通り、Chrome DevToolsチームがメンテナンスを行なっています。今回採用した一番の決め手となりました。 動作例 それではPuppeteerをインストールしてsample.jsを作成してみます。 最初のサンプルはGoogleにてVASILY, Incと検索するコードです。 インストール yarn add puppeteer sample.js const puppeteer = require( 'puppeteer' ); puppeteer.launch( { headless: false , // フルバージョンのChromeを使用 slowMo: 300 // 何が起こっているかを分かりやすくするため遅延 } ).then(async browser => { const page = await browser.newPage(); await page.setViewport( { width: 1200, height: 800 } ); // view portの指定 await page. goto ( 'https://www.google.co.jp/' ); await page.type( '#lst-ib' , 'VASILY, Inc' ); await page.click( '.lsb' ); await page.waitFor(3000); // デモのための遅延 browser.close(); } ); 実行 node sample.js デモのためヘッドレスモードをオフにしています。サンプルコードを見て頂いても分かる通り、基本的なAPIが用意されており、Headless Chromeを簡単に扱えます。Puppeteer APIは こちら をご参照ください。 SPAサイトの操作 次にVASILYの コーポレートサイト にアクセスしてみます。 VASILYのコーポレートサイトはVue.jsによるSPAであり、ソースコードを確認するとbody内はdivが1行とscrpitタグが存在するだけとなっています 1 。 < body > < div id = "app" > </ div > < script src = "/dist/build.js" ></ script > </ body > このようなSPAのサイトを扱ってみます。ServerSideRenderingをしていないサイトもよく見かけるので スクレイピングなどの参考にしてください。 下記サンプルではメニューのRECRUITをクリックしています。 const puppeteer = require( 'puppeteer' ); puppeteer.launch( { headless: false , slowMo: 300 } ).then(async browser => { const page = await browser.newPage(); await page.setViewport( { width: 1200, height: 800 } ); await page. goto ( 'https://vasily.jp/' ); const recruit = await page.$( '.contents > ul > li:nth-child(4) > a' ); await recruit.click(); await page.waitFor(3000); browser.close(); } ); まとめ Puppeteerを使用したHeadless Chromeの操作は非常に分かりやすく、動作させるまでスムーズに行えました。クライアントでDOMを生成しているサイトのスクレイピングをしたい方、自動テストに興味がある方は是非一度お試しになってください。 また、VASILY開発合宿の様子は後日記事が上がりますので、そちらもご覧ください。 私はHeadless Chrome + Puppeteerを使用して勤怠サイトを操作をしていました(悪巧みはしていません) 最後に VASILYではエンジニアを募集しています。興味ある方はWantedlyからご応募ください! 2017年11月16日現在の情報です。開発合宿でコーポレートサイトへのNuxt.jsの採用が発表されたため、SSRされる可能性があります。 ↩
アバター
こんにちは、フロントエンドエンジニアの権守です。Androidアプリ開発を始めてから2か月が経ちましたが、まだまだ実装に苦戦することも多いです。本記事では特に苦戦した実装の1つである角丸の帯グラフについて実装方法を3パターン紹介します。 満たすべき仕様 今回の実装では API level 16以上 を対象とし、実装する帯グラフは以下のデザイン要件を満たす必要がありました。 両端が角丸である グラフを構成するデータは二種類 並び順は大きい順ではなく固定 色は過半数かどうかで決まる (実際に使う場合は割合を表すラベルも併記することになると思いますが、本記事では省略します) 実装方法 本記事で紹介する実装は github に上げてありますので、必要に応じて参照してください。 1. ShapeDrawableを使った実装 Androidアプリで角丸を実装することを考えると、まず最初に思いつくのは ShapeDrawable でcornersを指定したrectangleを用いる方法でしょう。しかし、この方法では割合が極端な場合に角丸部分をうまく表示できず潰れてしまいます。 そこで、ShapeDrawableのrectangleではなくringを用います。ringは真ん中が透過された円形を描画できるので、それをうまく使い角の部分を背景色で塗りつぶします。 具体的には次のようなDrawableを用意します。 <? xml version = "1.0" encoding = "utf-8" ?> <shape xmlns : android = "http://schemas.android.com/apk/res/android" android : innerRadius = "15dp" android : shape = "ring" android : thickness = "15dp" android : useLevel = "false" > <size android : width = "30dp" android : height = "30dp" /> <solid android : color = "#FAFAFA" /> </shape> (Drawableのプレビュー、黒い部分は透過を表します) このDrawableを ClipDrawable を用いて半分に切ることで角の部分だけ塗りつぶすことができます。 この実装方法の問題点は、Drawable内に大きさや背景色を予め指定する必要がある点です。 2. Canvasによる描画を使った実装 Drawableを使うことを諦め、もっと原始的な実装方法を考えると、Canvasを用いてグラフを表す図形を描画する方法もあります。 しかし、Canvasを使った描画も割合が極端な例(2色から構成される角丸の場合)を考慮すると1の実装と同じくロジックは複雑になります。その場合、drawCircleとdrawRectangleを組み合わせるだけでなく、drawArcもうまく組み合わせて使う必要があります。 具体的には、次のようなステップで描画を行います。 両端に過半数の色で円を描画 右の長方形を描画 右の円弧を描画 左の長方形を描画 左の円弧を描画 右の要素が占める割合毎のグラフの描画ステップは以下になります。 98% 60% 2% 実際のコードは次の通りです。 class RoundedBandGraph3 @JvmOverloads constructor (context: Context, attrs: AttributeSet? = null , defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private val paint = Paint().apply { isAntiAlias = true } private val leftRect = Rect() private val rightRect = Rect() private val leftArcArea = RectF() private val rightArcArea = RectF() var positivePercentage: Int by Delegates.notNull() override fun onDraw(canvas: Canvas?) { super .onDraw(canvas) val radius = height / 2 val positiveRectangleWidth = width * positivePercentage / 100 val negativeRectangleWidth = width - positiveRectangleWidth // ステップ1 paint.color = ContextCompat.getColor(context, R.color.major) canvas?.drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), paint) canvas?.drawCircle((width - radius).toFloat(), radius.toFloat(), radius.toFloat(), paint) // ステップ2 if (positivePercentage >= 50 ) { paint.color = ContextCompat.getColor(context, R.color.major) } else { paint.color = ContextCompat.getColor(context, R.color.minor) } if (positiveRectangleWidth > radius) { rightRect. set (Math.max(width - positiveRectangleWidth, radius), 0 , width - radius, height) canvas?.drawRect(rightRect, paint) } // ステップ3 var x = Math.max( 0 , radius - positiveRectangleWidth) var angle = Math.toDegrees(Math.acos(x.toDouble() / radius.toDouble())).toFloat() rightArcArea. set ((width - height).toFloat(), 0f , width.toFloat(), height.toFloat()) canvas?.drawArc(rightArcArea, 0f - angle, 2f * angle, false , paint) // ステップ4 if (positivePercentage >= 50 ) { paint.color = ContextCompat.getColor(context, R.color.minor) } else { paint.color = ContextCompat.getColor(context, R.color.major) } if (negativeRectangleWidth > radius) { leftRect. set (radius, 0 , Math.min(negativeRectangleWidth, width - radius), height) canvas?.drawRect(leftRect, paint) } // ステップ5 x = Math.max( 0 , radius - negativeRectangleWidth) angle = Math.toDegrees(Math.acos(x.toDouble() / radius.toDouble())).toFloat() leftArcArea. set ( 0f , 0f , height.toFloat(), height.toFloat()) canvas?.drawArc(leftArcArea, 180f - angle, 2f * angle, false , paint) } } このコードの注意すべき点としては、長方形の描画は円弧の領域を除くように大きさを決める点と、円弧の大きさを求める点が挙げられます。 円弧の描画には孤の始点の角度と孤の大きさ(角度)を指定する必要があります。両端の円の半径 r から円弧が占める横幅を引いた大きさを x とし、 arccos(x/r) を求めることで、始点の角度を求めることができます。弧の大きさ(角度)は求めた角度の2倍になります。 3. CanvasのClipPathを使った実装 Canvasの ClipPath を使うと、指定した領域のみ描画するということができます。 しかし、ClipPathはAPI level 18以降でしか動きません。これは、Hardware AccelerationでClipPathをサポートしているのがAPI level 18以降だからです。コード上でバージョンを判定し、明示的にHardware Accelerationを無効にすることで、API level 16でも意図した動作を行えます。 class RoundedBandGraph4 @JvmOverloads constructor (context: Context, attrs: AttributeSet? = null , defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private val paint = Paint().apply { isAntiAlias = true } private val leftRect = Rect() private val rightRect = Rect() private val path = Path() var positivePercentage: Int by Delegates.notNull() init { // Hardware accelerated drawing modelでClipPath()がサポートされていのはAPI Level 18以降 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { setLayerType(LAYER_TYPE_SOFTWARE, null ); } } override fun onDraw(canvas: Canvas?) { super .onDraw(canvas) val radius = height / 2 val positiveRectangleWidth = width * positivePercentage / 100 val negativeRectangleWidth = width - positiveRectangleWidth path.addCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), Path.Direction.CCW) path.addCircle(width - radius.toFloat(), radius.toFloat(), radius.toFloat(), Path.Direction.CCW) path.addRect(radius.toFloat(), 0f , width - radius.toFloat(), height.toFloat(), Path.Direction.CCW) canvas?.clipPath(path) if (positivePercentage >= 50 ) { paint.color = ContextCompat.getColor(context, R.color.major) } else { paint.color = ContextCompat.getColor(context, R.color.minor) } rightRect. set (width - positiveRectangleWidth, 0 , width, height) canvas?.drawRect(rightRect, paint) if (positivePercentage >= 50 ) { paint.color = ContextCompat.getColor(context, R.color.minor) } else { paint.color = ContextCompat.getColor(context, R.color.major) } leftRect. set ( 0 , 0 , negativeRectangleWidth, height) canvas?.drawRect(leftRect, paint) } } pathに両端の円と真ん中の長方形を追加することで描画すべき領域を作成し、その後、単純に左右それぞれ割合に応じた大きさの長方形を描画すれば、意図したグラフが得られます。 まとめ 帯グラフは一見簡単に思える図形ですが、角丸にすることで意外と複雑な実装が必要になります。今回紹介した実装そのものは、一般の帯グラフとは異なる動作をするものではありますが、一般の帯グラフなど様々な図形を実装する際の参考になれば幸いです。 最後に VASILYでは新しいことに挑戦できるエンジニアを募集しています。 興味のある方は以下のリンクからぜひご応募ください。
アバター
こんにちは。 季節の中では秋が好き、バックエンドエンジニアのりほやんです。 近年、Facebookログインを使うサービスがとても増えています。 VASILYでもFacebookログインとFacebook Graph APIを使用した機能を実装しました。 本記事では、Facebook Graph APIを用いてユーザー情報を取得する方法と注意点について紹介します。 これからFacebook Graph APIを使用する方の参考になれば幸いです。 注意 この情報は2017年11月2日現在のものです。 Graph APIのバージョンはv2.10です。 Graph APIとは Graph APIの公式サイトには、Graph APIについて下記のように説明されています。 Graph API グラフAPIは、Facebookのソーシャルグラフにデータを取り込んだり、データを取り出したりするための主な方法です。データのクエリ、新しい記事の投稿、写真のアップロード、アプリで行う必要があるその他のさまざまなタスクに利用できるローレベルのHTTPベースのAPIです。 Graph APIを用いることで、ユーザーの情報を取得・投稿することができます。 通信の流れ Graph APIとの通信の流れはざっくりと以下のようになります。 ユーザーがFacebookにログイン、ユーザーデータへのアクセス許可を行う FacebookがAccess Tokenを発行する Access Tokenをサーバーサイドに渡す Access Tokenの不正チェックを行う ユーザーの情報を取得する 今回はサーバサイド側に注目し、4,5について説明します。 Access Tokenの不正チェックを行う 1〜3の過程でFacebookから発行されたAccess Tokenを用いて、Facebookとの接続を行います。 その際、トークンハイジャックを防ぐためにクライアントから送られたAccess Tokenが、自分のFacebookアプリケーションに対して発行されたAccess Tokenかどうかをチェックする必要があります。 Access Tokenが不正なものではないかどうかをチェックする際には、 /debug_token エンドポイントを使用します。 エンドポイント /debug_token?input_token={ユーザーのAccess Token}&access_token={app_id}|{app_secret} /debug_token エンドポイントのリクエストパラメーターにはinput_tokenとaccess_tokenが必要です。 ユーザーのAccess Tokenが正しいことをチェックする際には、ユーザーのAccess Tokenは、acccess_tokenではなくinput_tokenを記述する という点に注意してください。 access_tokenには、Facebookアプリケーションの情報を記述します。 具体的には、 {Facebookアプリケーションのapp_id}|{Facebookアプリケーションのapp_secret} を記述します。 例えば、 ユーザーのAccess Tokenがuser、Facebookのapp_idが123、app_secretがsecretとした場合、下記のようなリクエストになります。 /debug_token?input_token=user&access_token=123|secret レスポンス 正しいinput_token, access_tokenを入力した場合は、下記のようなレスポンスが返ります。 { "data" : { "app_id" : app_id, "type" : "APP" , "application" : アプリケーション名, "is_valid" : true , "scopes" : [ ] } } 不正なAccess Tokenを入力した場合は下記のようなレスポンスが返ります。 { "error" : { "message" : "Invalid OAuth access token." , "type" : "OAuthException" , "code" : 190, "fbtrace_id" : "" } } また、違うアプリケーションのAccess Tokenを入力した場合は下記のようなレスポンスが返ります。 { "error" : { "message" : "(#100) The App_id in the input_token did not match the Viewing App" , "type" : "OAuthException" , "code" : 100, "fbtrace_id" : "" } } エラーの種類が複数あることに注意してください。 エラーが発生していない場合、Access Tokenが有効なものであると判断できます。 参考: アクセストークン:デバッグとエラー処理 Access Tokenの確認後、ユーザーの情報を取得します。 ユーザー情報の取得 ユーザーの基本情報(名前・年齢など) ユーザーの基本情報を取得したい場合、 /{user_id} というエンドポイントを使用します。 また、自分の情報に限り /me というエンドポイントで情報を取得できます。 今回は /me を使用します。 /me エンドポイントのパラメーターに、取得したいフィールドを記述します。 例えば、ユーザーのID、名前、プロフィール画像を取得したい場合は /me?fields=id,name,picture と記述します。 実際に /me?fields=id,name,picture エンドポイントを叩いた際は、下記のレスポンスが返ってきます。 エンドポイント /me?fields=id,name,picture レスポンス { "id" : ユーザーID, "name" : 名前, "picture" : { "data" : { "is_silhouette" : 初期ユーザー画像かどうか(プロフィール画像を設定してない場合trueになる), "url" : 画像のURL } } } filedsに指定できるフィールドは他にも多くあります。 参考: Graph API Reference User 友人・友人数を取得したい場合 ユーザーの友人数や、同じアプリを使用している友人を取得したい場合は、 /me のfilelsにfriendsを指定します。 このエンドポイントを叩く際には このエンドポイントで取得できる友人は、ユーザーのすべての友人ではなく同じFacebookアプリケーションを使用している友人のみが取得できる という点に注意してください。 エンドポイント /me?fields=friends レスポンス { "data" : [ { "name" : 友人の名前, "id" : ユーザーID } , ... ] , "summary" : { "total_count" : ユーザーの友人数 } } summary の中の total_count がユーザーの総友人数です。 また友人一覧は、 data に配列として返ってきます。 data内の配列には、友人それぞれの名前とユーザーIDが記載されています。 プロフィール画像のサイズを指定したい場合 プロフィール画像を取得したい場合はfieldsにpictureを指定して /me?fields=picture となります。 プロフィール画像など、取得する画像のサイズを変更したい場合は、 width , hight というパラメータをつけることによって画像のサイズを指定することができます。 エンドポイント /me?fields=picture.width(720).hight(720) レスポンス { "picture" : { "data" : { "height" : 720, "is_silhouette" : false , "url" : 指定したサイズの画像のURL, "width" : 720 } } , "id" : ユーザーID } アクセス許可の確認したい場合 初回認証時にユーザー情報への読み取りが許可されている場合でも、ユーザー自身が アプリ設定画面 から自由に、アプリのアクセス許可を変更することができます。 ユーザーが下記のようにチェックを外し、アクセス許可を変更することで、今まで取得できていたデータが取得できなくなります。 そのため、ユーザー情報の取得する前にAccess Tokenのアクセス許可を確認する必要があります。 アクセス許可を確認する場合は /me エンドポイントのfieldsにpermissionsを指定します。 エンドポイント /me?fields=permissions レスポンス { "permissions" : { "data" : [ { "permission" : "public_profile" , "status" : "granted" } , { "permission" : "publish_actions" , "status" : "declined" } , { "permission" : "email" , "status" : "declined" } , ] } , "id" : ユーザーID } アプリに付与されているアクセス許可それぞれの名前とステータスが返ってきます。 statusについての説明は下記の様になります。 status 説明 granted アクセス許可されている declined アクセス許可されていない statusが declined のアクセス許可があった場合は、ユーザーに再度アクセス許可を求める必要があります。 参考: User permissions Tips テストアプリケーション Facebookアプリケーションをリリースまで公開したくない場合、開発環境用のアプリケーションを作成することができます。 アプリ一覧画面 から、アプリを選択し『Create Test App』ボタンから開発環境用のテストアプリケーションをつくることができます。 Graph API Explorer APIのリクエストパラメーターやレスポンスを試したいときは、 Graph API Explorer が便利です。 1. アプリケーションを選択 まず確認を行いたいアプリケーションを選択します。 APIのレスポンスを確認したいだけのときはGraph API Explorerのままで良いと思います。 2. アクセス許可を行いAccess Tokenを取得 『Get Token』→『Get User Access Token』で、取得したい情報のアクセス許可をチェックしAccess Tokenを取得します。 3. エンドポイントとパラメーターを記述し実行 確認したいエンドポイントとパラメータを記述し『Submit』ボタンを押すとレスポンスの確認ができます。 Access Tokenのアクセス許可を確認したいとき Access Tokenのアクセス許可を確認したい場合には、 Access Token Debugger が便利です。 Access Tokenを入力すると、下記のようにユーザー名、許可しているアクセス許可一覧(Scopes)などが確認できます。 まとめ 本記事では、Facebook Graph APIの利用方法と注意点について紹介しました。 Access Tokenの不正チェックの際はAccess Tokenをinput_tokenに記述すること、友人は同じアプリケーションを使用している友人しか取得できないという点は特に注意してください。 参考資料 Graph API ドキュメント Facebookログインについて Wantedly VASILYでは、ウェブエンジニアを募集しています! 興味のある方は以下のリンクからぜひご応募ください。 Icon made by Freepik from http://www.flaticon.com/
アバター
フロントエンドチームの茨木です。 前回ブログを執筆したときにはiOSアプリを開発していましたが、先月からAndroidアプリを開発しています。 本記事では、Androidで美しいバウンドのアニメーションを手軽に導入できるSpring Animationをご紹介します。 Spring Animationとは Spring AnimationはGoogleが公式にサポートしているアニメーションのライブラリで、名前の通りばねの動きを模しています。今年のGoogle I/Oで紹介されました。 Spring Animationを使うとわずか数行のコードで美しいバウンドのアニメーションを実装できます。しかし、実際の物理現象を模しているため調整にはちょっとコツが要ります。 とりあえず使ってみる Spring Animationを使うにはサポートライブラリが必要なので、build.gradleに一行追記しましょう。 dependencies { . . . implementation "com.android.support:support-dynamic-animation:27.0.0" } これによりAPI 16以上でSpring Animationが使えるようになります。Spring Animationを読み込めるようになったら、早速簡単なアニメーションを実装してみましょう。ここでは四角形がバウンドするようなアニメーションを作ってみたいと思います。まず、レイアウトに四角形のビューを追加してみましょう。 <View android : id = "@+id/square" android : layout_width = "50dp" android : layout_height = "50dp" android : layout_gravity = "center|top" android : layout_marginTop = "20dp" android : background = "#f00" /> 次に、以下のコードをアクティビティの任意の場所に追加します。 val square: View = findViewById(R.id.square) val animation = SpringAnimation(square, DynamicAnimation.TRANSLATION_Y, 500f ) animation.spring.apply { dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY stiffness = SpringForce.STIFFNESS_VERY_LOW } animation.start() あとはビルドするだけです。上手く行けば次のようなアニメーションが表示されるはずです。 以上でSpring Animationの導入は終わりです。ここでstiffnessとdamping ratioという2つのパラメータが出てきました。実際にアニメーションの動きを調整する場合にはこれらのパラメータを変えていくことになります。どちらもあまり聞き慣れない言葉だと思いますが、ばねの運動を司る重要な物理パラメータです。次節ではばねの運動について触れていきます。 ばねの運動について ばねは引っ張って手を離すと振動を始め、だんだん減衰していきます。このように、ばねの運動は振動と減衰という2つの要素を持ちます。先程触れたstiffnessとdamping ratioはこれらに対応する物理パラメータです。 stiffness(ばね定数) stiffnessという単語は日本語に訳すと剛性です。いわゆる高校物理で出てくるばね定数に当たります。stiffnessはばねの固さを表しており、stiffnessが大きいほどばねが固くなって振動が細かくなります。振動の細かさを表す固有振動数とstiffnessには以下の関係があります。 \[ \omega=\sqrt{\frac{k}{m}}\\ \begin{align} \omega &: \mbox{固有振動数}\\ k &: \mbox{stiffness}\\ m &: \mbox{質量}\\ \end{align} \] 上の関係から、振動の細かさを2倍にしたい場合はstiffnessを2 x 2 = 4倍、3倍にしたい場合はstiffnessを3 x 3 = 9倍…とすれば良いことがわかります。 damping ratio(減衰比) damping ratioはばねの減衰の大きさを表しており、大きいほど減衰が早くなります。これにより振動の持続時間を調整できます。ばねの振動はdamping ratioの値により3つのモードに分類できます。 \(\mbox{damping ratio = 0}\) : 単振動 減衰せずに一定の振幅でずっと振動が続くモードです。高校で物理を選択していた方なら一度習ったことがあるはずです。 \(0 < \mbox{damping ratio} < 1\) : 減衰振動 名前の通り減衰しながら振動するモードです。 \(1 \leq \mbox{damping ratio}\) : 臨界減衰or過減衰 振動せずに減衰していくモードです。\(\mbox{damping ratio = 1}\)の臨界減衰が最も早く減衰します。 これらをグラフに書いたのが下の図です。 どのモードを使うかはお好みですが、実際の開発では減衰振動を使うケースが多いと思います。 Spring Animationの動きを変えてみる ここまででばねの運動を簡単に説明しました。ここから実際にSpring Animationの動きを調整していきましょう。 適用するプロパティ・初期値・最終値を設定する まずはSpringAnimationのプロパティ・初期値・最終値を設定してみましょう。 val animation = SpringAnimation( square, // 適用するビュー DynamicAnimation.TRANSLATION_Y, // 適用するプロパティ 500f // 最終値 ) animation.setStartValue( 200f ) /*初期値(ここで指定しなかった場合はビューに設定された値が初期値になります)*/ アニメーションを適用するプロパティはDynamicAnimationの定数で指定できます。 ビューのプロパティ DynamicAnimationの定数 alpha DynamicAnimation.ALPHA rotation DynamicAnimation.ROTATION rotationX DynamicAnimation.ROTATION_X rotationY DynamicAnimation.ROTATION_Y scaleX DynamicAnimation.SCALE_X scaleY DynamicAnimation.SCALE_Y scrollX DynamicAnimation.SCROLL_X scrollY DynamicAnimation.SCROLL_Y translationX DynamicAnimation.TRANSLATION_X translationY DynamicAnimation.TRANSLATION_Y translationZ DynamicAnimation.TRANSLATION_Z x DynamicAnimation.X y DynamicAnimation.Y z DynamicAnimation.Z SpringAnimationは1つのインスタンスで1つのプロパティしか設定できません。複数のプロパティにアニメーションを適用する場合には、それぞれにSpringAnimationのインスタンスを用意する必要があります。 適用するパラメータを指定したら初期値と最終値を指定しましょう。Spring Animationは初期値から開始し、最終値を中心に振動してから収束します。拡大するようなアニメーションの場合には最終値を超える瞬間があるので、レイアウトを組む際に注意が必要です。 物理パラメータを変えてみる 物理パラメータは定数が幾つか用意されていますが、ここでは前節を踏まえてオリジナルの値を設定してみましょう。stiffnessは100ぐらいを基準に調節していくのがおすすめです。 s: stiffness, dr: damping ratio s = 100.0 dr = 0.2 s = 25.0 dr = 0.2 s = 100.0 dr = 0.0 s = 100.0 dr = 0.5 s = 100.0 dr = 1.0 まとめ 物理パラメータで調整するのは一見すると難しく思えますが、意味を知れば大分扱いやすくなると思います。Spring Animationは数行のコードで美しいアニメーションを実現できるので、皆様もバウンドのアニメーションを実装する際にぜひ使ってみて下さい。 最後に VASILYではプラットフォーム問わず高品質なアプリを開発したい人を大募集しています。 興味ある方はWantedlyからご応募ください!
アバター
VASILYのiOSエンジニアにこらすです。 今回のテックブログではiOS・macOS・watchOS・tvOSのUserDefaultsにユーザー設定などを保存するのに便利なラッパーライブラリ Default を作ったので紹介します。 github.com Defaultとは? Defaultは、Codableに準拠するカスタムオブジェクトを保存するための拡張機能を提供するライブラリです。プロトコル DefaultStorable を介して、UserDefaultsに以下で説明する新しいインタフェースを提供することで、UserDefaultsを拡張します。 Codableサポート拡張機能とDefaultStorableプロトコル拡張機能いずれかを使うこともできますし、両方つかうこともできます。 Defaultを使うメリットは? UserDefaultsには、保存したキーをtypoしたり、読み書きしている場所を探すのが難しかったりといった難点があります。 ですが、このDefaultを使って保存されるオブジェクト型を定義すると、 DefaultStorable に準拠する型をプロジェクト内で検索することで保存されるデータを追うのが簡単になります。 UserDefaults に格納する専用のオブジェクトを定義すれば、特定のデータを論理的にグループ化することができます。 NSCoding 時代の実装 Swiftでカスタムオブジェクトを宣言した後は、 NSCoding に準拠し、 NSObject から継承し、適切なEncode / Decodeメソッドを実装すれば、 UserDefaults に直接保存することができるようになります。 これを自前で対応しようとすると、以下のような煩雑なコードが必要になります。 クラスを定義し、 NSCoding に準拠し、必要なDecode / Encodeメソッドを実装する class VolumeSetting : NSObject , NSCoding { let sourceName : String let value : Double init (sourceName : String , value : Double ) { self .sourceName = sourceName self .value = value } required init (coder decoder : NSCoder ) { self .sourceName = decoder.decodeObject(forKey : "sourceName" ) as ? String ?? "" self .value = decoder.decodeDouble(forKey : "value" ) } func encode (with coder : NSCoder ) { coder.encode(sourceName, forKey : "sourceName" ) coder.encode(value, forKey : "value" ) } } オブジェクトを作成し、 NSKeyedArchiver を使用してインスタンスを Data にアーカイブして保存する let setting = VolumeSetting(sourceName : "Super Expensive Headphone Amp" , value : 0.4 ) let encodedData = NSKeyedArchiver.archivedData(withRootObject : setting ) UserDefaults.standard. set (encodedData, forKey : "volume" ) 読み出すときは NSKeyedUnarchiver を使う if let data = UserDefaults.standard.data(forKey : "volume" ), let volumeSetting = NSKeyedUnarchiver.unarchiveObject(with : data ) as ? VolumeSetting { // do something } Defaultを使った実装 一方、 Default を使えば、以下のようなシンプルなコードで実現できます。 保存対象のオブジェクトを定義する DefaultStorable プロトコルに準拠したstructを定義します。 struct VisualSettings : Codable , DefaultStorable { let themeName : String let backgroundImageURL : URL ? } 保存処理 let settings = VisualSettings(themeName : "bright" , backgroundImageURL : URL (string : "https://..." )) settings.storeToDefaults() 読み込み if let settings = VisualSettings.fetchFromDefaults() { // Do something } もう一つのメリット このアプローチのもう一つの利点は、すべてのオブジェクトを一つのファイルに定義することで、 UserDefaults に格納されるものを非常に簡単に見ることができることです。 Defaultの設計について UserDefaults にカスタムオブジェクトを保存するためには、そのオブジェクトは NSCoding に準拠する必要があります。 NSCoding に準拠するにはDecoding / Encodingメソッドを実装する必要があり、少し手間がかかります。 一方、嬉しいことにSwiftの Data 型が NSCoding に準拠しています。オブジェクトを Data に変換する方法を見つけることができれば、それを UserDefaults に格納することができます。 Swift 4から追加された Codable プロトコルは簡単に Data に変換する事ができます。 Swift 4以降のプロジェクトであれば、 Codable に準拠したモデルオブジェクトを作ることが多くなると思います。このライブラリは、そういう Codable プロトコルに準拠したオブジェクトを UserDeafaults に読み書きすることができるようになっています。 結構シンプルですね! まとめ Default は、非常に軽くてシンプルなカスタムオブジェクトを扱う UserDefaults のラッパーです。 GitHubに公開してあるので、 Default をインストールして遊んでみたい場合は、Carthage か CocoaPods を使って試してみてください。 VASILYでは、OSSなどSwift 4での開発に興味があるエンジニアを募集しています。ぜひオフィスに遊びに来てください。 ー にこらす 👍
アバター
アプリエンジニアの堀江( @Horie1024 )です。 先日、1つのコードベースからアプリ名やアプリアイコン、アプリの挙動を変更した複数のバージョンのAPKをビルドする必要があり、その際どのように対応したかをご紹介しようと思います。 サンプルコード 本記事内で使用するコードは以下になります。 github.com Androidアプリのビルド Androidビルドシステムは、アプリのリソースとソースコードをコンパイルしAPKにパッケージ化します。Android Studioを使用したAndroidアプリ開発では、 Gradle と Android Plugin for Gradle (以下Android Plugin)が連携することでAndroidアプリのビルドが行われます。また、GradleとAndroid Pluginは、Android Studioから独立していますのでコマンドラインから容易にビルド可能です。 カスタムビルド GradleとAndroid Pluginで構成されるAndroidのビルドシステムは柔軟で、アプリの主要なソースファイルを変更せずにカスタムビルドを設定可能です。カスタムビルドを設定することで、1つのコードベースに対してビルド設定、コード、およびリソースといったソースセットを置き換えるることができます。それによって、アプリ名やアプリアイコン、アプリの挙動を変更したAPKをビルドすることが可能になります。この際重要になる概念が ビルドバリアント(Build Variant) です。 ビルドバリアント ビルドバリアントは以下の2つ要素の組み合わせから構成されます。 ビルドタイプ (Build Type) プロダクトフレーバー (Product Flavor) 例えば、ビルドタイプが debug と release 、プロダクトフレーバーが app1 と app2 である場合以下のような組み合わせになり、ビルドバリアントは app1Debug 、 app2Debug 、 app1Release 、 app2Release の4種類となります。 各ビルドバリアントは、ビルド可能なバージョンのアプリを表しており、特定の組み合わせのビルドを簡単に実行できます。例えば app2Debug の組み合わせでビルドしたい場合、以下のコマンドでビルド可能です。 $ ./gradlew assembleApp2Debug ビルド設定ファイル 先程例として上げたビルドバリアントを構成するビルドタイプとプロダクトフレーバーを作成するには、ビルド設定ファイル( build.gradle )に変更を加え、カスタムビルド作成する必要があります。ビルド設定ファイルにはトップレベルとモジュールレベルがあり、Android Studioでプロジェクトを新規作成すると自動で作成されます。プロジェクトルートにある build.gradle がトップレベルのビルド設定ファイル、 app/ 以下にあるのがモジュールレベルのビルド設定ファイルです。 ビルドタイプ ビルドタイプはモジュールレベルの build.gradle ファイルの android {} ブロック内で作成します。Android Studioでプロジェクトを新規作成するとデバッグおよびリリースビルドタイプが自動的に追加されます。 android {} ブロック内に明示的に指定されているのはリリースビルドタイプのみですが、デバッグビルドタイプも有効になっています。 android { defaultConfig {} buildTypes { release {} } } プロダクトフレーバー プロダクトフレーバーの設定はビルドタイプと同様で、 productFlavors {} ブロックにプロダクトフレーバーを追加して設定します。プロダクトフレーバーでは、 defaultConfig と同様のプロパティがサポートされ、 applicationId や versionCode 、 versionName といった要素も各プロダクトフレーバーで定義できます。 Android Plugin for Gradle 3.0.0以降を使用する場合 flavorDimensions の設定が必須になります。 flavorDimensions については こちら をご覧ください。以下の例では、tierフレーバーディメンションを作成し各プロダクトフレーバーに設定しています。 android { defaultConfig {} buildTypes { debug {} release {} } flavorDimensions "tier" productFlavors { app1 { dimension "tier" } app2 { dimension "tier" } } } 複数バージョンのAPKのビルド 各ビルドタイプとプロダクトフレーバー毎にビルド設定、コード、およびリソースといったソースセットを持つことができます。特定のビルドバリアントでビルドする場合、以下の優先順位に従ってGradleがビルド時に使用するソースセットが判別されます。 ビルドバリアントのソースセット ビルドタイプのソースセット プロダクトフレーバーのソースセット メインソースセット したがって、ビルドバリアント、ビルドタイプ、プロダクトフレーバー毎にソースセットを用意することで複数のバージョンのAPKをビルドすることが可能です。 ビルド設定、コード、リソース、マニフェストについてビルドバリアントを使用した際にどのようにビルドされるのかを見ていきます。 ビルド設定 プロダクトフレーバーでは defaultConfig と同様のDSLを使用できますので、 app1 、 app2 で applicationId を定義することで applicationId を変更可能です。 android { defaultConfig {} buildTypes { debug {} release {} } flavorDimensions "tier" productFlavors { app1 { dimension "tier" applicationId "com.horie1024.app1" } app2 { dimension "tier" applicationId "com.horie1024.app2" } } } この場合、プロダクトフレーバー app1 、 app2 を含むビルドバリアントでビルドすると、それぞれで別アプリとしてビルドされます。メインソースセットよりプロダクトフレーバーのソースセットの優先度が高いためです。Gradleはビルド時により優先度の高いソースセットを利用します。また、プロダクトフレーバー毎に別々の署名設定を追加することも可能です。 コード ソースコードについても各ビルドタイプとプロダクトフレーバー毎に用意することで、ビルドするビルドバリアントに応じて挙動を変化させることができます。 以下のコードは、メインソースセットで定義している MainActivity で、 Greeting クラスの greet メソッドを実行した結果を画面に表示します。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tv: TextView = findViewById(R.id.hello_text) tv.text = "Hello ${ Greeting().greet() } " } } Greeting クラスの実装をプロダクトフレーバー app1 、 app2 のソースセットのディレクトリ src/app1/kotlin/com.horie1024.buildvariantssample/ 、 src/app2/kotlin/com.horie1024.buildvariantssample/ 以下に置きます。 実装は以下の通りです。 greet メソッドを実行した結果が異なります。 class Greeting { fun greet() = "World!" } class Greeting { fun greet() = "Universe!" } ビルドバリアント app1Debug 、 app2Debug でビルドした結果は以下の通りです。表示されるテキストの内容が変化しています。 リソース メインソースセットの res ディレクトリ以下のリソースについても、各ソースセットディレクトリ以下に res ディレクトリを作成することでビルドバリアントに応じて利用するリソースを切り替えることができます。 strings.xml で定義した app_name リソースを例にどうビルドされるか見てみましょう。ここでは app1 、 app2 のソースセットディレクトリに加えて、ビルドバリアント app2Debug に対応するディレクトリを作成します。 各 strings.xml は以下のように定義しています。 src/main/ <resources> <string name="app_name">BuildVariantsSample</string> </resources> src/app1/ <resources> <string name="app_name">app1</string> </resources> src/app2/ <resources> <string name="app_name">app2</string> </resources> src/app2Debug/ <resources> <string name="app_name">app2Debug</string> </resources> app_name リソースはmainソースセットに加えて、 app1 、 app2 プロダクトフレーバー、ビルドバリアント app2Debug の各ソースセットでも同名で定義されています。ビルド時にGradleは優先度に応じてこれらの中からどのソースセットを使うかを判断します。 ビルドバリアント app1Debug でビルドした場合プロダクトフレーバー app1 の src/app1/ 以下に置かれたリソースが使われ、ビルドバリアント app2Debug でビルドした場合 src/app2Debug/ 以下に置かれたリソースが使われます。以下のようにアプリ名としてそれぞれのリソースで定義した app_name の値が使われています。 マニフェスト マニフェストについても同様です。マニフェストの場合、優先順位が低いものから高いものへマニフェストがマージされ最終的に1つの統合済みのマニフェストとなりAPKにパッケージ化されます。 以下の画像は 複数のマニフェスト ファイルの統合 から引用したものになります。 ビルドバリアントでのマニフェストの優先度は以下のようになっており、ソースセットの優先度と同一です。 ビルドバリアントマニフェスト(src/app1Debug/ など) ビルドタイプマニフェスト(src/debug/ など) プロダクトフレーバーマニフェスト(src/app1/ など) フレーバーディメンションを設定している場合、 flavorDimensions での定義順に優先度が付きます。 マニフェストのマージは統合のポリシーは こちら にしたがって行われますが、複数のマニフェストで要素の競合が発生した場合、明示的に統合ルールマーカーを指定して解決します。統合ルールマーカーの詳細は こちら をご覧ください。 ビルドバリアントによるマニフェストの統合を確認するため app1 、 app2 プロダクトフレーバーに対応したソースセットディレクトリに android:theme で指定するstyleを変更した AndroidManifest.xml を用意しました。統合ルールマーカー tools:replace="android:theme" を指定して競合を解決しています。 メインソースセットのAndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.horie1024.buildvariantssample"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> app1プロダクトフレーバーソースセットのAndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.horie1024.buildvariantssample"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/App1Theme" <!-- AppThemeの代わりにApp1Themeを指定 --> tools:replace="android:theme">  <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> app2プロダクトフレーバーソースセットのAndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.horie1024.buildvariantssample"> <application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/App2Theme" <!-- AppThemeの代わりにApp2Themeを指定 --> tools:replace="android:theme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> ビルドバリアント app1Debug 、 app2Debug でそれぞれビルドした結果は以下の通りです。ビルドバリアントによって AndroidManifest.xml で適応されるstyleが異なっています。 コード内でのビルドタイプ、プロダクトフレーバーの参照 以下のように BuildConfig.java が自動生成されるのでビルドタイプ、プロダクトフレーバーの種類で処理を分岐可能です。 public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.horie1024.app1"; public static final String BUILD_TYPE = "debug"; public static final String FLAVOR = "app1"; public static final int VERSION_CODE = 2; public static final String VERSION_NAME = "2.0"; } プロダクトフレーバーが app1 の場合のみ実行したい処理がある場合以下のように書けます。 if (BuildConfig.FLAVOR == "app1" ) // do something Android Studio上でのビルドバリアントの切り替え Android Studioでは、ビルドバリアントを切り替えるUIが用意されています。特定のビルドバリアントに切り替えて挙動を確認したい場合簡単に切り替えられます。 「app」の「Build Variant」をクリックするとドロップダウンでビルドバリアントを選択できます。 まとめ Androidのビルドシステムは、Gradleを採用したことで非常に柔軟にカスタムビルドを作成できるようになっています。ビルド設定ファイルを編集し、ビルドバリアントを利用可能にすれば、主要なコードベースを変更せずともアプリのデザイン・挙動を変化させることができます。 実際に業務で applicationId 、 versionCode 、 versionName 、アプリアイコン、アプリ名の異なる4種類のアプリをビルドする必要があったのですが、ビルドバリアントを利用することでコードを変更することなく対応できました。 さいごに VASILYではアプリエンジニアを大募集しています。是非Wantedlyからご応募ください。
アバター
iOSアプリエンジニアの @hiragram です。VASILYにジョインしてだいたい3か月経ちました。 今回は、僕がジョインしたプロジェクトに導入した、APIリクエストの抽象レイヤーの設計について紹介します。また、記事の最後にこの抽象レイヤーのコードをフレームワークとして切り出したもののリンクがありますので、興味がある方は見てみてください。 課題と方針 当プロジェクトでは、リアクティブフレームワークに RxSwift 、通信ライブラリに APIKit 、JSONマッパーに Himotoki を採用しています。 従来のアプリの実装には、 ViewModelが直接APIKitをインポートして通信しており、通信のトリガーやレスポンスの処理が画面によってバラバラ APIが取得対象のリソースを常に results というキーに配列で返すようになっており、必ず1個しか返さないAPIのレスポンスも results.first とOptionalになる形で取り出している ある画面で取得したデータ(例えば自分のユーザーデータなど)を他の画面に反映するための仕組みが整備されておらず、反映漏れが多発 といった課題がありました。 そこで、今回のAPI抽象レイヤーの導入によって、 VMのレイヤーから通信やAPIの構造を隠蔽し目的のデータのみを意識すればよい設計 Observableベースのリアクティブなアプリ内のデータフロー 取得した結果を他の画面にブロードキャストするためのObservableを用いたシンプルなデータ反映 を獲得することを目的としました。 エンドポイント定義 エンドポイントを定義するための EndpointDefinition というプロトコルを用意しました。 protocol EndpointDefinition { // 返されるJSONがどのような構造か associatedtype Response : ResponseDefinition static var path : String { get } // 接続先(デフォルトは`.production`、 他に`.mock` など) static var environment : Environment { get } // 送られるパラメータ var parameters : [String: Any] { get } // HTTPメソッド(アンダースコアはAPIKitと名前の衝突を防ぐため) var method : _HTTPMethod { get } } 例えば、今日のおすすめメニューを以下のようなJSONで返すエンドポイントを考えます。 { " name ": " チキンカレー ", " price ": 750 } このエンドポイントは以下のように定義できます。 struct RecommendedMenu : EndpointDefinition { // Dishオブジェクトを1つ返す typealias Response = SingleResponse < Dish > static let path = "/menu/recommended" var method : _HTTPMethod = . get var parameters : [String: Any] = [ : ] init () {} } レスポンスのデータ構造 先述の RecommendedMenu の Response で使用している SingleResponse<Dish> は、「Dishオブジェクトとしてデコードできるオブジェクトが1つ」という構造のJSONが返されることを示します。 例えば、おすすめメニューが複数あり配列で返ってくる場合は ArrayResponse<Dish> とすることで [ { " name ": " チキンカレー ", " price ": 750 } , { " name ": " オムライス ", " price ": 700 } ] このようなJSONをパースできるようになります。 SingleResponse 、 ArrayResponse は ResponseDefinition というプロトコルに準拠しています。 protocol ResponseDefinition { // レスポンスの本質であるオブジェクトの型 associatedtype Result // `JSONSerialization.jsonObject(with:options:)` で得たオブジェクトのキャスト先 associatedtype JSON var result : Result { get } // JSONのオブジェクトを受け取って、resultに取り出した値をセットするイニシャライザ init (json : JSON ?) throws } 次に、複数のキーにオブジェクトが入っている複雑なJSONの場合を考えます。例えば、ページングの情報を示す info とメニューの配列を返す dishes があるようなケースです。 { " info ": { " total_count ": 50 , " total_page ": 3 } , " dishes ": [ { " name ": " ポテトサラダ ", " price ": 400 } , { " name ": " シーザーサラダ ", " price ": 450 } , { " name ": " オムライス ", " price ": 700 } , { " name ": " とんかつ定食 ", " price ": 800 } , { " name ": " 和風ハンバーグ ", " price ": 650 } ] } このようなJSONのデータ構造を定義するための CombinedResponse という型があります。 これは DataResponseDefinition に準拠する型を型パラメータに持つジェネリック型です( SingleResponse や ArrayResponse も DataResponseDefinition に準拠しています)。 これを使うと、上記のJSONは CombinedResponse<SingleResponse<Info>, ArrayResponse<Dish>> と表現できます。 CombinedResponse の具体的な実装はここでは省略しますが、興味がある方はコードを見てみてください。 APIKitとのブリッジ さて、ここまででエンドポイントのモデル化が出来ました。次に実際にリクエストを飛ばすためにAPIKitとの境界になる部分を書きます。 struct GenericRequest <Endpoint: EndpointDefinition> { fileprivate let endpoint : Endpoint init (endpoint : Endpoint ) { self .endpoint = endpoint } } GenericRequest は先ほどまでに定義したエンドポイントの型を型パラメータに持つジェネリック型です。この型に、APIKitの Request に準拠するエクステンションを書きます。 extension GenericRequest : Request { typealias Response = Endpoint.Response.Result // プロトコルに準拠するための様々な実装(省略) ... func response (from object : Any , urlResponse : HTTPURLResponse ) throws -> Endpoint.Response.Result { guard let jsonObj = object as ? Endpoint.Response.JSON else { fatalError() } // JSONオブジェクトから値を取り出す処理 return try Endpoint.Response. init (json : jsonObj ).result } } API抽象レイヤーの本体 実際に通信を行うためのAPIKitとのブリッジも出来ました。続いて実際に抽象レイヤーとして機能する部分を書いていきます。 public struct Repository { fileprivate static func request <Endpoint: EndpointDefinition> (_ endpoint : Endpoint ) -> Single <Endpoint.Response.Result> { return Single.create(subscribe : { (observer) -> Disposable in let request = GenericRequest. init (endpoint : endpoint ) let task = Session.send(request, callbackQueue : nil , handler : { (result) in switch result { case .success( let response ) : observer(.success(response)) case .failure( let error ) : observer(.error(error)) } }) return Disposables.create { task?.cancel() } }) } } request(_:) は GenericRequest を使ってリクエストを投げて得られた結果をそのリクエストのオブジェクトが流れてくるRxSwiftの Single を返すメソッドです。 このメソッドはfileprivateにしておき、VMから利用するためのメソッドを生やします。 public extension Repository { public static func recommendedMenu () -> Single <Dish> { let endpoint = Endpoint.RecommendedMenu. init () return request(endpoint) } } これで、VMからは Repository.recommendedMenu().subscribe(onSuccess : { (dish) in print( "本日のおすすめは \(dish.name) ( \(dish.price) 円)です。" ) }).disposed(by : bag ) このようにシンプルなコードでデータにアクセスすることができるようになりました。 取得した結果を他の画面にも反映できるようにする APIの抽象レイヤーができたので、他画面へのデータ反映の仕組みを追加します。具体的には、 各モデルオブジェクト用にブロードキャストのためのストリームを用意して、他の画面の通信結果を受け取りたいVMがそれを購読します。抽象レイヤーは通信に割り込んで得られたオブジェクトをそのストリームに流します。 public final class GlobalStream <T> { fileprivate let subject = PublishSubject < T > . init () func publish (_ element : T ) { subject.onNext(element) } } extension GlobalStream : ObservableType { public typealias E = T public func subscribe <O> (_ observer : O ) -> Disposable where O : ObserverType , O.E == E { return subject.retry().subscribe(observer) } } public extension Repository { public struct GlobalStreams { public static let dish = GlobalStream < Dish > . init () } } ブロードキャスト用のストリームが用意できたので、値を流す側のコードを書きます。 Observableに、流れてきた値をGlobalStreamに流すbranchというメソッドを追加します。 public extension Observable { func branch (to globalStream : GlobalStream <E> ) -> Observable <E> { return flatMap { element -> Observable <E> in globalStream.publish(element) return Observable.just(element) } } } 次におすすめメニューのリクエストにbranchオペレータを追加して、取得したDishオブジェクトをブロードキャストするようにします。 public extension Repository { public static func recommendedMenu () -> Single <Dish> { let endpoint = Endpoint.RecommendedMenu. init () return request(endpoint) .asObservable() .branch(to : GlobalStreams.dish ) .asSingle() } } 他の画面で取得したデータを自分も受け取りたいVMは、GlobalStreamを購読することでデータを受け取ることが出来ます。 Repository.GlobalStreams.dish.subscribe(onNext : { (dish) in print(dish) }).disposed(by : bag ) まとめ APIへのリクエストを抽象化して一箇所でのみ管理する仕組みを導入して上のレイヤーが知りたいことだけ公開する事によって、VMのコードがシンプルになりメンテナンス性が向上します。また、通信を一箇所で集中管理することによってすべての通信で共通して行いたい処理を追加したり、通信ライブラリを入れ替えたりするのが容易になります。 今回紹介したAPI抽象レイヤーを実装するためのプロトコルや構造体をAbstractionKitという名前でフレームワーク化して公開しました。全く同じコードでは無いのですが、今後も業務で得た知見をAbstractionKitに還元していきたいと思っているので、興味がある方はぜひ見てみてください。サンプルアプリもあります。 hiragram/AbstractionKit 次のステップとして、この記事で紹介したエンドポイントの定義をするコードをSwaggerのYAMLドキュメントから自動生成するジェネレータを開発しています。こちらも導入できたらブログで紹介できればと思っています。 VASILYではプロトコル指向なコードがかけるSwiftエンジニアを募集しています。ぜひオフィスに遊びに来てください。
アバター
こんにちは、VASILYのバックエンドエンジニアの塩崎です。 9/17〜9/20にかけて広島で開催されたRubyKaigi2017に、VASILYから4人が参加しました。 3日間で約50個の講演があり、参加者も数百人を超えるであろう大変大規模なカンファレンスでした。 たくさんの講演の中で、VASILYのエンジニアが興味を持ったものを、この記事でいくつか紹介いたします。 The many faces of module Ruby Lauguage Server Ruby and Distributed Storage System Progress of Ruby/Numo: Numerical Computing for Ruby Improve extension API: C++ as better language for extension An introduction and future of Ruby coverage library How Close is Ruby 3x3 For Production Web Apps? Type Checking Ruby Programs with Annotations Busting Performance Bottlenecks: Improving Boot Time by 60% Smalruby : The neat thing to connect Rubyists and Scratchers Writing Lint for Ruby Compacting GC in MRI Development of Data Science Ecosystem for Ruby API Development in 2017 まとめ おまけ 弊社の技術顧問であるMatzさんとの記念写真 弊社CTO今村とマネキン餃子娘 どんなに飲んでも飲み足りない系エンジニアと特大レモンサワー 口いっぱいにおにぎりを頬張るフロントエンドエンジニア スライドの紹介に問題がある場合はご連絡ください。 The many faces of module Rubyのパパであり、弊社VASILYの技術顧問でもある Matz さんの発表です。 Rubyに取り入れたsimulaやLispの機能を紹介しながら、オブジェクト指向での継承の利点と問題点を説明されていました。 特に多重継承の問題点に光を当て、Lispで発明されたflavorという機能の紹介をしていました。 この機能はRubyではMixinという名前で導入されました。 もともとはMixinを実現するためのものであるmoduleが現在では以下の7つの機能を持っていることを紹介していました。 module as mixin module as namespace singleton module as set of methods module as unit od method combination(Module#prepend) module as refinement structural signature(提案段階) 前半の4つについては初期のRubyから備わっている機能ですが、後半のものは最近のRubyで拡張されたmoduleの機能です。 そして、RubyはもはやMatzさん個人の言語ではなく、私たちRubyコミュニティの言語である。 また、Rubyをより良いものにすることによって、世界をより良いものにしたいという言葉によって締められました。 Ruby Lauguage Server Quipperの @mtsmfm さんによる、Ruby用のLanguage Serverの実装に関する話でした。 Language Serverとはエディターに対してシンタックスハイライト、Lint、コードフォーマットなどの機能を提供するプログラムです。 Language Serverがない場合は言語とエディタの組み合わせごとにこれらの機能を実装する必要があります。 Ruby用のLanguage Serverがまだなかったので、開発を始めたそうです。 syntax check機能については、 ruby -cw コマンドを呼び出して対応しているそうです。 また、自動補完や、メソッド定義へのジャンプは開発中らしいです。 トップレベルの自動補完は動的解析によって行われているために、ファイルIOなどの副作用のあるコードを実行すると、実際に実行されてしまうという欠点もあるそうです。 今後の展望としては、まだまだ実装していない機能が多いため、それらを実装することや、syntax checkの指摘箇所を行単位からカラム単位にするなどが挙げられていました。 Ruby and Distributed Storage System Ruby and Distributed Storage Systems from SATOSHI TAGOMORI Treasure Dataの @tagomoris さんによる、planet scaleな分散ストレージシステムであるBigdamについての話でした。 TreasureDataに入力されるデータを最初に受け取る部分を作り直ししているそうです。 世界中にエッジロケーションを配置したplanet scaleなシステムになるそうです。 Bigdamは6つのマイクロサービスからなっており、システムそのものはJavaやGoで書かれているそうです。 Rubyは開発を円滑に進めるために用いたそうです。 開発の初期段階でRubyを使いmock serverやInterface testを作ってから、それぞれのマイクロサービスの開発を行ったそうです。 また、データ量が増加した時に、スケールするのかどうかの検証にもRubyを用いたそうです。 このシステムは最終的にOSSになる予定らしいです。 今から、公開が待ち遠しいですね。 Progress of Ruby/Numo: Numerical Computing for Ruby 筑波大の @masa16tanaka さんによる、Rubyでデータサイエンスを行うために必要なN次元配列演算ライブラリについての講演でした。 データサイエンティストがコードを書くときにはPythonを用いることが多く、Rubyを使ってデータサイエンスを行う人は決して多くはありません。 Pythonが広く使われている理由はpandansやmatplotlibなどの非常に充実したライブラリ群(SciPyスタック)にあるそうです。 そして、それらの土台をなしているのがN次元配列の計算を行うライブラリであるNumPyです。 そのため、Rubyのデータサイエンス用ライブラリを充実させるためには、まずこのNumPyのRuby版が必要です。 Numo::NArrayはまさにそのような目的のライブラリです。 NumPyが提供している363個の関数のうち、217個はすでにNArrayでもカバーされているそうです。 さらに、NumPyが提供している以下の3つの便利機能もNArrayに実装されているそうです。 view of slice broadcasting masking 速度の面ではまだNumPyには敵わない印象を受けましたが、速度に関する最適化には未着手とのことでしたので、これからさらに高速化していく余地は十分にあるそうです。 さらにNumoプロジェクトではNArrayだけではなく、さらに高度な計算を行うためのライブラリも提供されています。 線形代数ライブラリNumo::Linalg、科学計算ライブラリNumo::GSL、高速フーリエ変換ライブラリNumo::FFTWなどです。 内部的にNArrayを使っており、NArrayとの親和性が高いそうです。 これらのライブラリが充実することにより、Rubyでもデータサイエンスが行えるような未来を目指すという内容でした。 参考: Nomoのプロジェクトページ Improve extension API: C++ as better language for extension Improve extension API: C++ as better language for extension ClearCodeの @ktou による、C++を使って拡張ライブラリを実装する話でした。 Rubyの拡張ライブラリを書くためにはCを使うことがほとんどかと思いますが、C APIは冗長な記法になりがちです。 そこで、Ext++というライブラリを作り、C++11で拡張ライブラリを実装できるようにしたそうです。 C++11では、モダンな機能が提供されており、シンプルな記法で拡張ライブラリの実装を行うことができるようです。 例えば、型推論やラムダ式、cast演算子のカスタマイズなどの機能です。 しかし、Rubyの例外は内部でsetjmpを使っているため、RAII(Resource Acquisition Is Initialization)との相性が悪いなどの問題点もあるそうです。 An introduction and future of Ruby coverage library Cookpadでフルタイムコミッターになられた @mametter さんによる、Rubyのテストカバレッジライブラリの紹介とcoverage.soの拡充計画についてのお話でした。 Rubyでは、品質を保証するための実用的な手段はいまのところ、テストしかありません。 そしてテストの良さを計測するのにカバレッジはとても必要なものです。 カバレッジ計測だけでは仕様の漏れなどは発見できませんが、カバレッジ計測をうまく利用して設計の網羅性を高くすることは可能です。 理想的なカバレッジの上げ方は、「テストされていないコードを全体的から探し、どういう観点のテストが足りていないのかを振り返って考え、足りなかった観点のテストを全体的に確認しながら書いていく」という方法だそうです。 Rubyのカバレッジ計測についてですが、SimpleCovがgemとしてよく使用されています。SimpleCovは、coverge.soのラッパーです。 coverage.soは、発表者の遠藤さんが開発されたライブラリで、現在line coverageのみサポートしています。 しかし、line coverageだけでは後置ifなどの条件分岐を網羅することはできず、指標として弱いです。 なぜline coverageしかサポートしていないかというと、coverage.soはRubyのテストを拡充するために作ったものなので、他で使われることをあまり想定していなかったそうです。 そのため現在は、後方互換性を残しつつ機能を拡充する開発を行っているそうです。 実際に、branch coverageとfunction coverageの機能が最近コミットされています。 したいことはたくさんあるようで、メソッドのカバレッジ計測・&.のブランチカバレッジ対応・メソッドチェーンのメソッドごとのブランチカバレッジにも対応したいとおっしゃっていました。 カバレッジ等すべて丁寧にお話しされていたため、とてもためになる発表でした! coverage.soの機能拡充によりRubyの堅牢性向上が期待できますね! 参考: RubyKaigi 2017 の予稿 How Close is Ruby 3x3 For Production Web Apps? AppFolioの @codefolio さんによる、Ruby3x3プロジェクトのために、ベンチマークを開発した話です。 実際のWebアプリケーションを用いて、Rubyのバージョンアップに伴いWebアプリケーション自体どれほど速度改善したかをご紹介されていました。 計測には、 Discourse というコミュニティサイトをテスト対象のアプリケーションとして使用しています。 また、計測を行うための環境を定めたり、railsが起動しているインスタンスをDedicated Hostにしたりするなど、厳密な計測を行おうとする姿勢が伺えました。 結果としては、Rubyのバージョンが上がるについれて、徐々に速度は改善されています。 HTTPリクエストの処理時間については、Discourse自体がバージョンアップにより重くなったのを考慮するとRuby2.0.0からRuby2.4.1では150%程度速度が改善しているそうです。 また、初回リクエストにかかる時間に関しては、Ruby2.0.0からRuby2.4.1で30%程度改善されています。 Matzさんが、Rubyの速度改善にはwarm upの速度改善が重要とおっしゃっていましたが、warm upも少しづつではありますが、速度が改善されているようです。 また、ベンチマークであるrails_ruby_benchは公開されておりAWSやローカルで試すことができます。 参考 発表スライド rails_ruby_bench Type Checking Ruby Programs with Annotations SideCIの @soutaro 、Rubyで型チェックを行う手法が紹介です。 Rubyで型チェックを行う取り組みは以前からありますが、コード内に型定義を書いておき、ランタイムでチェックを行う点がこの手法の特徴です。 型定義には大まかに2通りの記法があります。 1つはコメントでの定義、もう1つは別ファイルでの定義です。後者はC言語のヘッダーファイルでの宣言に少し似ています。 コメントでの型定義(test.rb) # @type const Conference: Conference.module # @type var year: Integer conference = Conference.new(name: :RubyKaigi, year: 2017) year = conference.name 別ファイルでの型定義(hello.rbi) class Conference def initialize:(name: String, year: Integer) -> any def name: -> String def year: -> Integer end ※ 上記のコード例は発表者Soutaro Matsumotoさんの ブログ から引用しました。 この型チェックはsteepというgemになっており、誰でも試すことが出来ます。 型を明記できるのは画期的ですが、一方でRubyらしい簡潔さがやや失われてしまう印象も受けました。 しかし、ここで紹介された手法は、Ruby3に向けた型の議論に大きな影響を与えることになるでしょう。 参考: 発表者であるSotaro Matsumotoさんのブログ記事 発表中で紹介されていた、型チェックを行うためのgem steep Busting Performance Bottlenecks: Improving Boot Time by 60% Shopifyの @jules2689 さんによる、Rubyスクリプトの起動時間改善に関する発表です。 Railsの rails server の起動で数秒待たされた経験が誰しも1度はあるのではないでしょうか。 これを解消するために、まず原因の切り分けを行っています。 具体的にはコンパイル、YAMLの設定ファイル読込、定数参照に切り分けを行い、キャッシュにより起動時間を改善しています。 更にBundlerの速度がバージョン1.15で大きく改善されたことにも触れています。 スクリプトの起動で待たされることがなくなることで、エンジニアのモチベーションや生産性が改善されることが期待されます。 参考: 発表スライド Smalruby : The neat thing to connect Rubyists and Scratchers Kouji TakaoさんとNobuyuki Hondaさんによる、プログラミング初心者用の言語であるScratchと互換の機能をRubyで提供するという内容でした。 Scratchはifやwhileに対応するコード要素をブロックのように組み合わせてプログラミングを行う、Visual Programming言語です。 Scratchと同等の機能を持ったsmalrubyというライブラリを使い実際に島根で小・中学生にプログラミングを教えているそうです。 Scratchのコード要素をRubyに移植した時に、とても自然な形の表現になっていたのが印象的でした。 近年ではプログラミング教育への関心が高まり、2020年からは小学校でのプログラミング教育も始まる予定です。 そのような状況の中で、教育用言語としてのRubyを活用していこうという気概が感じられました。 参考: smalrubyのgithubリポジトリ Writing Lint for Ruby SideCiの @p_ck_ さんによる、静的解析ツールであるLintと、Rubyの静的解析ツールの一つであるRubocopについての話でした。 Lintとはコードを静的に(コードを実際に動かさずに)解析を行い、バグを発見するためのものです。 例えば、以下のようなコードは構文的にはOKですが、実行時にエラーが発生してしまいます。 if 10 < x < 20 このようなコードやエラーは発生しないけど、意味が曖昧になってしまいがちなコードに対して指摘するのがLintです。 この講演ではRubyコードを抽象構文木に変換し、簡単な解析を行うLintの実装や、Rubocopに新しいCop(ルール)を追加する方法について説明していました。 特にLintがコードを静的に解析することによる利点・欠点についての説明が興味深かったです。 実際にプログラムを動かしてメソッドの入出力を確認するテストだけでなく、高速かつ全てのコードを網羅的に確認することができるLintも組み合わせることで、より品質の高いプログラムを書くことができると感じました。 参考: 発表スライド ブログ Compacting GC in MRI Githubという小さなベンチャー(笑)で働いている @tenderlove さんによる、compaction GCに関する発表でした。 GC compactionを行うことによって、Page Faultを減らし、Unicornの動作速度を上げることを目標としていました。 プロセスをforkした直後は親プロセスと子プロセスは同じメモリを共有します。 そして、親子のプロセスの間でメモリに差異が生じた瞬間になって初めてメモリーのコピーが実行されます。 この仕組みはCopy on Writeと呼ばれ、これが発生すると、スピードの低下の原因となります。 Copy on Writeはメモリーのページという単位で行われるため、メモリー内のオブジェクトの配置が断片化していると、Copy on Writeが多くのページで発生してしまいます。 そのため、GCにメモリの断片化を解消するためのcompaction機能を実装したそうです。 オブジェクトの移動を行った後に、そのオブジェクトへの参照を書き換えるという操作を行います。 現状の仕組みでは、Hashのキーになっているオブジェクトや、文字列リテラルなどの移動はできず、典型的なRailsアプリケーションの場合は、約46%のオブジェクトが移動可能だそうです。 しかし、最終的にはCで書かれた拡張ライブラリから参照されているオブジェクト以外の全てのオブジェクトを移動可能にすることを目標としていました。 VASILYでもアプリケーションサーバーとしてUnicornを使用しているため、この機能の実現によるスピードアップが楽しみです。 Development of Data Science Ecosystem for Ruby Speeeの @mrkn さんによる、RubyからPythonの関数を呼ぶためのライブラリであるPyCallに関する発表でした。 Rubyで作られたシステムに機械学習などの機能を組み込む場合には、Rubyだけで頑張る方法と、データ処理部分をPythonで作ってその間をJSON APIで繋ぐという2種類の方法があります。 しかし、前者ではできることに限りがあり、後者ではデータ転送のコストが必要です。 そこで、第3の選択肢としてのPyCallの紹介をされていました。 PyCallではRubyから直接Pythonを呼び出すため、データを転送するコストを低く抑えることができます。 講演の最中にはPyCallを使った、幾つかのデモが行われていました。 その中でも特にSSD300のモデルを読み込ませ、物体検出を行っていたのが印象的でした。 また、その後に開催されたワークショップでは、ハンズオン形式でPyCallの使い方をレクチャーしていただきました。 使ってみての感想としては、予想以上にPython感がなく、すごく自然な記法でPythonのメソッドを呼び出せることに驚きました。 VASILYでもRuby on Railで作られたシステムの中に機械学習を活用した機能を組み込むことが多いため、これらの技術は今後活用する機会があるかもしれません。 参考: PyCallを使ったデモアプリ pandasで可視化を行っています API Development in 2017 Drecomの onk さんによる、JSON API開発についての発表でした。 HTTP通信を受け付けて、JSONを返すようなJSON APIをどのようにして効率的に開発するのかという内容でした。 JSON APIとnative clientを同時に作り結合テストをすると、思っていたのと違うJSONスキーマが返ってきて困った経験をしたことがある人は多いかと思います。 その1つの理由として、RESTfulなAPIはエントリーポイントに、どのリソースが返ってくるのかという情報は埋め込まれているものの、そのリソースがどんなJSON構造で返ってくるのかという情報がないことが挙げられます。 そのため、具体的なJSON構造はAPI serverのお気持ち次第ということになってしまいがちです。 この問題を解決するためにSchema First Developmentという手法を紹介していました。 この手法では、クライアント側とAPI側が最初にOpenAPI形式のSchemaを定義します。 そして、このSchemaに従うようにそれぞれの開発を行います。 Schemaを書かないと絶対にAPI開発ができないようにする仕組みを導入したことによって、Schamaが100%正しいドキュメントとして機能するようになったそうです。 さらに、Schemaとserializerの間で記述が重複していたため、SchemaからSerializerを自動生成したそうです。 また、RESTfulではないAPIとして最近注目されているGraphQLの紹介もありました。 OpenAPI形式はSwaggerで使われているSchema定義の形式であり、弊社のTECH BLOGにも紹介記事がいくつかありますので、よろしければそちらもご覧になってみてください。 tech.vasily.jp tech.vasily.jp まとめ VASILYエンジニアはRubyを使って、サービス開発をしたいエンジニアを募集しています。 興味ある方は以下のリンクからご応募ください。 おまけ 広島食べ歩きの写真などを紹介します。 弊社の技術顧問であるMatzさんとの記念写真 弊社CTO今村とマネキン餃子娘 どんなに飲んでも飲み足りない系エンジニアと特大レモンサワー 口いっぱいにおにぎりを頬張るフロントエンドエンジニア
アバター
こんにちは。iOSエンジニアの遠藤です。 9/15〜17にかけiOSDC Japan 2017が開催され、VASILYでもiOSチーム全員で参加しました。 また、私はLTとしてShift_JISのURLデコードについて発表させていただきました。 以下、発表資料です。 speakerdeck.com https://twitter.com/re___you スタッフ、スポンサー、スピーカー、参加者の皆さんお疲れ様でした。 とても楽しかったです! 今年はトラック数も多く、幅広いジャンルの発表がありました。 どの発表も面白く興味深い内容でしたが、今回はVASILYでも実装やプロダクトに組み込んでいきたい内容を含んだ発表についていくつか紹介します。 iOSDC Japan 2017 iOSDC(iOS Developers Conference)とはiOSと周辺技術を題材としたカンファレンスです。 iosdc.jp 発表紹介 以下、VASILYで取り入れたい内容を含む発表についての紹介です。 インタラクティブ画面遷移の実践的解説 具体例とクイズで学ぶ、Swiftの4種類のエラーの使い分け 両OSやるマンという選択 Build high performance and maintainable UI library RxSwiftのObservableとは何か 15分でわかるバックグラウンドアップロード インタラクティブ画面遷移の実践的解説 speakerdeck.com https://twitter.com/shmdevelop 概要 UIPercentDrivenInteractiveTransition を使用した画面遷移の話です。 モーダル画面、同一画面でのインタラクティブな画面遷移の実装方法を丁寧に説明してあります。 所感 画面遷移のカスタマイズをあまりしたことが無いのですが、実装方法や躓いた点を詳しく説明して頂いたのでとても参考になりました。 インタラクティブな画面遷移を実装することで、一気にアプリがリッチな雰囲気になるので取り入れてみたいです。 具体例とクイズで学ぶ、Swiftの4種類のエラーの使い分け speakerdeck.com https://twitter.com/koher 概要 Swiftで処理が失敗したことを表現する手段として例えば 返り値をOptionalにする throwsにする fatalErrorを使う preconditionを使う などが挙げられますが、それらを「エラーをどのようにハンドリングしたいか、あるいはさせたくないか」という視点から適切に使い分ける方法について解説したトークです。 所感 エラーをSimple domain errors / Recoverable errors / Universal errors / Logic failuresの4つの概念で理解を深めることができました。 それによって普段書いているコードがどれに当たるのかを正しく判断できるということが分かりました。 特にpreconditionとコード最適化の関係など知らなかった点があったので勉強になりました。 両OSやるマンという選択 speakerdeck.com https://twitter.com/jumboOrNot 概要 iOSとAndroidの両OSを一人で開発しているエンジニアの話です。
両OSのアプリを開発する上で、マーケット、デザインガイドライン、開発環境、CI、開発言語などの多くの視点での知見がまとめられたとても実用的な内容でした。 所感 エンジニア向けのイベントなので、SwiftとKotlinの言語的な違いに終始しがちなテーマだと思っていました。 しかし実際は、App Store / Google Playでのアプリの売れ方による違いや、双方のデザインガイドラインの特徴などにも触れられていて参考になりました。 iOS / AndroidエンジニアがこれからAndroid / iOSの開発を始めるにあたって、ざっくりその違いを理解するのにとても役立ちます。 Build high performance and maintainable UI library speakerdeck.com https://twitter.com/k_katsumi 概要 高速なUIを作るための実装についてと、メンテナンスしやすいコードについての発表です。 高速なUIを作る上で何がパフォーマンスを悪くしているのか、改善する上での注意点などの内容でした。 「メンテナンスしやすいコード」とは「テストしやすいコード」です。どのようにすればテストしやすいコードになるか例を用いて説明していました。 具体的には、依存が大きく単体でテストがしづらいViewをどのようにテストしやすいコードに分割できるかという内容でした。 所感 高速なUIを作るには、負荷の高い処理を減らすことが大事です。 しかしそれにはコードの可読性とのトレードオフになるのでしっかり見極めていきたいです。 メンテナンスしやすいコードについては、私たちはなかなかテストを書くことに時間を割けていませんが、テストを書くことでコードの信頼性を高めていきたいと感じました。 まずは、テストを書きやすくするために依存関係の少ないコードの設計、実装からスタートしていきたいです。 RxSwiftのObservableとは何か RxSwiftのObservableとは何か | iOSDC Japan 2017 from GOMI NINGEN www.slideshare.net qiita.com https://twitter.com/gomi_ningen 概要 RxSwiftを構成する要素の1つであるObservableに注目した内容です。 Obsevableを学びReactiveExtensionsについて理解を深めるために、 ObserverパターンからObservableの仕組みまでを丁寧に解説されていました。 所感 最近RxSwiftを使用して実装していますが、あまり内部の仕組みについて理解できていませんでした。 今回の発表ではコードを主体とした説明でとてもわかりやすく、Obsevableがどのようなものなのかを理解が深まりました。 発表で使用されていたコードを実際に動かしてより理解を深め、プロダクト実装で活かしていきたいです。 15分でわかるバックグラウンドアップロード speakerdeck.com https://twitter.com/yimajo 概要 バッググラウンドアップロードの仕様から実装、デバッグで役に立つツールについての発表です。 実装でのハマリポイントなどもあり実用的にまとまっている内容です。 所感 これまで私たちは収集したユーザーの行動ログを、 UIApplicationDelegate の applicationDidEnterBackground(_:) のタイミングでサーバーに送っていました。 しかし、ログが大きい場合全てを送信することはできず、アプリ起動時や復帰時に残ったログを送るようしています。実装が散らばってしまうので今回のバッググラウンドアップロードを使用して全てのログをバッググラウンドで送れるような仕組みにしていきたいです。 最後に 以上がiOSDC Japan 2017の中で取り入れたい内容の紹介でした。 実装やプロダクトに取り入れた際には取り入れてみての感想などを記事で紹介していきます。 ぜひお楽しみに! VASILYでは積極的に技術のインプットをしてプロダクトに取り入れていくエンジニアを募集しています。 興味のある方はこちらからご応募ください。 https://www.wantedly.com/projects/88978 www.wantedly.com
アバター
iOSエンジニアの庄司です。最近Android開発をはじめて、Android Studioのコード補完力の高さに驚かされています。 iOS11のリリースが間近ですが、今回は最近開発したiOSアプリで実装したアニメーションについてご紹介します。 こんなものを作りました GitHubにサンプルプロジェクトを上げておきました。 https://github.com/WorldDownTown/CurvingProgressBarSample ポイント残高や、工程の進捗率を表現したりするのに使えるViewです。 一見すると動きはシンプルなのですが、意外と複雑な実装になっているため説明していきます。 このアニメーションの動作ポイントは下記の4点です。 数値がパラパラと増える ゲージが増加する 数値によってテキストとゲージの色が変わる アニメーションにイージングをかける 何が面倒なのか アニメーション中に色が複数回切り替わる アニメーションの実装といえば、 UIView のクラスメソッドである animate(withDuration:animations:) が手っ取り早い方法です。 ですが、このメソッド実現できるアニメーションは frame や backgroundColor などの状態をA→Bに変更することです。 ゲージの長さを0→100にアニメーションさせている最中に、色が複数回切り替わるような実装は animate(withDuration:animations:) では実装できません。 UILabelの増加具合にもイージングをかける 上で述べたように、 UIView の animate(withDuration:animations:) は状態をA→Bに変更させることができます。 しかし、 UILabel の text をパラパラと切り替わるようなアニメーションを実現できません。 そこにさらにイージングをかけようと思うとひと苦労です。 どう解決するか CADisplayLink ゲージが増加する途中でゲージや文字の色を変更するために CoreAnimation.framework の CADisplayLink を使いました。 CADisplayLinkは画面のリフレッシュレートと同期して描画させるタイマーオブジェクトです。 ざっくり下記のようなコードになります。 (サンプルプロジェクト上では こちら ) displayLink = CADisplayLink(target : self , selector : #selector(updateTimer)) // ディスプレイ描画ごとに updateTimer が実行される displayLink.preferredFramesPerSecond = 60 displayLink.add(to : .current, forMode : .commonModes) displayLink.isPaused = false // アニメーション開始 @objc private func updateTimer () { let duration : TimeInterval = 1.0 // アニメーションは1.0秒 let elapsed : TimeInterval = Date.timeIntervalSinceReferenceDate - startTimeInterval let progress : CGFloat = (elapsed > duration) ? 1.0 : CGFloat (elapsed / duration) // アニメーション時間の進捗率 // cubic bezier let y : CGFloat = unitBezier.solve(t : progress ) // 0.0〜1.0 下で詳細を説明します progressBlock?(y) if progress == 1.0 { displayLink.isPaused = true // 一周したらアニメーションを止める } } ディスプレイの描画ごとに updateTimer() が呼ばれ、アニメーション時間に対する進捗率 ( y ) を計算してクロージャーに渡します。 クロージャー側では、渡された進捗率を元にゲージの量や色、ラベルのテキストを描画します。 イージングを実装 ゲージの増加だけであれば、 CAAnimation と CAMediaTimingFunction を組み合わせて使うことで幾つかの用意されたイージングを実装することができます。 しかし最初のGIFのように、ゲージの増加、色の変更、ラベルテキストの更新をイージングをかけながらアニメーションさせることはできませんでした。 この条件を満たしつつイージングをかけるために、自前のイージング処理を実装しました。 「自前のイージング処理」とは、アニメーションの経過時間を元にアニメーション自体の進捗率を計算する処理のことです。 ベジェ曲線 CAMediaTimingFunction と同じようなイージングを実装するには、ベジェ曲線の計算をすることになります。 (ベジェ曲線の基本は こちら ) ベジェ曲線といえば UIBezierPath を思い出します。 UIBezierPath はベジェ曲線を「描く」ことはできますが、描画したりアニメーションのパスに使うことしかできません。 今回イージングを実装するにあたって、 WebKitのベジェ曲線のC++実装 をSwiftで書き換えてみました。 処理が複雑で長いので、リンクだけ貼っておきます。 UnitBezier.swift こんな風に使います。 let curve : AnimationCurve = .easeInOut let unitBezier = UnitBezier(p1 : curve.p1 , p2 : curve.p2 ) @objc private func updateTimer () { let elapsed : TimeInterval = Date.timeIntervalSinceReferenceDate - startTimeInterval let progress : CGFloat = (elapsed > duration) ? 1.0 : CGFloat (elapsed / duration) // アニメーション進捗率を計算 let animationProgress : CGFloat = unitBezier.solve(t : progress ) progressBlock?(y) if progress == 1.0 { displayLink.isPaused = true } } さらに サンプルコードには、数種類のアニメーションカーブの種類を enum で用意しています。 enum AnimationCurve { case linear, ease, easeIn, easeOut, easeInOut, original(CGPoint, CGPoint) original(CGPoint, CGPoint) を使って、ベジェ曲線の制御点を設定することで自由にイージング具合を調整することができます。 まとめ 今回は複雑な変化を発生させるアニメーションの実装方法を紹介しました。 CADisplayLink とベジェ曲線をつかってイージングを自由にカスタマイズすることで、複数の要素に多方面にイージングを書けることができるようになります。 ゲージの描画やアニメーション開始までの具体的な処理など、紹介しきれていない実装がいくつもあります。サンプルプロジェクトの方も覗いてみてください。 また、アニメーションについて興味があれば、先月公開された Lottieの記事 も御覧ください。 さいごに 弊社では凝ったアニメーション実装が得意なアプリエンジニアを大募集しています。 興味をもっていただけましたら、是非Wantedlyからご応募お願いいたします。
アバター
こんにちは、VASILYバックエンドエンジニアの塩崎です。 RubyKaigi2017の開催時期が間近に迫っていますが、皆さんの広島グルメ探訪の予定はいかがでしょうか? 今年のRubyKaigiにはVASILYから4人が参加する予定で、そのうちの3人は初参加です。 発表の要旨はすでに公開されていて以下のページで確認できますが、まだどれを見て回ろうかを決めかねている人もいるかと思います。 http://rubykaigi.org/2017/schedule そこで、Rubyのパパであり、VASILYの技術顧問でもあるまつもとゆきひろさん(以下、Matzさん)にRubyKaigi2017の見所を聞いてみました。 この記事がRubyKaigiに参加をされる方々の参考になれば幸いです。 注) Matzさんは事前にすべての発表内容を正確に把握しているわけではないため、ここで紹介した内容がRubyKaigi2017で確実に発表されるわけではありません。 また、ここで紹介した発表以外にも素晴らしい発表がいくつもあるので、あくまで一つの参考としてご覧ください。 1日目 How Close is Ruby 3×3 For Production Web Apps? Hanami - New Ruby Web Framework Development of Data Science Ecosystem for Ruby 2日目 Progress of Ruby/Numo: Numerical Computing for Ruby Type Checking Ruby Programs with Annotations Automated Type Contracts Generation for Ruby Ruby Language Server 3日目 Compacting GC in MRI Bundler 2 まとめ 1日目 How Close is Ruby 3×3 For Production Web Apps? これはVASILYさん的には面白いかと思います。 Ruby 3で3倍早くなったかどうかを確認するためのベンチマークの話です。 早くなったことを検証するためには、いいベンチマークがないと意味がないんですよね。 マイクロベンチマークでは3倍になったけど、実際のアプリケーションでは5%しか早くなっていないみたいなのはよくありますよね(笑)。 でも、かといって、ものすごい複雑なベンチマークだと検証が大変ですよね。 そうならないように、ちょうど良いベンチマークを作るように彼に依頼を出しています。 Hanami - New Ruby Web Framework 午後の休憩後にはHanamiの話がありますね。 Ruby on Railsに対するアンチテーゼで新しいフレームワークはこんな感じだよという発表かと思います。 まぁ、私自身はweb開発をしないんで、どちらが優れているかって聞かれてもよくわからないんですが(笑)。 こういうフレームワークもあるんだよという意味では面白いかもしれませんね。 Development of Data Science Ecosystem for Ruby あと、初日ではmrknさんの発表がオススメですね。 データサイエンティストがコーディングをするときにはPythonを使うことが多いと思うのですが、データサイエンスをRubyでやるにはどうやってやるのかの話をすると思います。 現状では3つのアプローチがあるんですが、そのうちの1つであるRubyからPythonの関数を呼ぶ方法の話です。 ちなみに残りの2つはSciRubyとApacheArrowですね。 SciRubyはSciPyやNumPyをRubyで実装するというアプローチです。 3つの中で1番筋は良さそうなんですが、いばらの道ですね。 というのも、Pythonの方が先を走っているので、追いつくためにはPythonよりも早く走らなければならないんです。 ですが、Python側の方が開発者は多いんですよね。 とはいえ、頑張っている人たちもいます。 最後のApacheArrowはプログラミング言語間でデータをやり取りするためのフォーマットです。 ファイルのフォーマットとメモリのフォーマットの両方があります。 計算処理はPythonにやらせてApacheArrow形式で書き出しておくと、Rubyで読むことができます。 いちいちファイルに書き出して、Ruby側でトラバースすると遅いのでそういうコストがなくて早いと思います。 あと、列指向なので、データサイエンスの分野での効率が良いとも聞いています。 2日目 Progress of Ruby/Numo: Numerical Computing for Ruby さっきの話の2つ目のアプローチを採っているのがNumoですね。 数値を効率的に扱うことができるデータ構造です。 いばらの道を頑張っている人です。 Type Checking Ruby Programs with Annotations この発表も面白いと思います。 発表者の松本宗太郎さんは博士論文でRubyに型をつけるための研究をした人ですね。 結果的にはうまくいかなかったんですが、TypeAnnotationをつけて頑張ろうっていうのを余暇でやっているらしく、その発表をしてくれます。 Ruby 3で導入しようとしている型システムは型推論だけでどうにかしようというアプローチなんですが、それとは別のアプローチですね。 Automated Type Contracts Generation for Ruby これも型に関する発表ですね。 Ruby 3とは違うアプローチで型をつけようとしています。 2010年代のプログラミング言語は型ありなのが当たり前になってきているので、Rubyはだいぶ置いてけぼり感(笑)があります。 ですので、コミュニティ内での危機感も高いと思います。 Ruby Language Server Ruby Language Serverに関する発表もありますね。 LanguageServerっていうのはエディターと通信して、リアルタイム構文解析やコード補完の機能を提供するものです。 もともと、コンパイラとエディタは独立しているので、IDEとかは内部的にコードを解析してエラーが出た時にはポップアップを出しますよね。 そういう機能を任意の言語でできるようにするのがLanguage Serverです。 これに対応するとLanguage Serverプロトコルに対応しているあらゆるエディタでコード補完が使えるようになります。 開発効率アップの可能性がありますね。 3日目 Compacting GC in MRI この発表ではRubyのGCの話をするんですが、RubyのGCってCompactionしないんですよね。 Cのスタックでは数値であるかポインタであるかの情報を持っていないので、そこから参照されているオブジェクトを動かすことができないんですよ。 でも、Rubyのオブジェクトからしか参照されていないオブジェクトだったらこの問題が発生しないので動かせるという発想ですね。 CompactionによってRubyがより省メモリになる可能性があります。 Bundler 2 あと、私は使わないけど、bundlerの話もいいかもね。 近いうちにbundlerが標準Rubyに組み込まれることが決まりました。 実はRubyGemsがbundlerに依存するという話が先に出たんです。 そうなるとbundlerも標準Rubyにバンドルするしかないという感じで決まりました(笑)。 まとめ Rubyの開発者であるMatzさんに今年のRubyKaigiでのオススメな発表を伺いました。 どの発表も興味深い内容なので、RubyKaigiが待ち遠しいです。 VASILYではRubyKaigiなどのカンファレンスに参加し、最新技術を取り入れることに積極的なエンジニアを募集しています。 興味のある方は以下のリンクからご応募ください。
アバター
こんにちは。インフラエンジニアの光野です。 弊社ではネットワークの構築と管理に AWS CloudFormation と AWS OpsWorks を導入しました。 本記事では、その効果と導入に際しての工夫について紹介いたします。 目次 Before / After 効果 CloudFormarionとは CloudFormation or Terraform OpsWorksとは SSH/sudo管理 CloudFormationとOpsWorksの役割分担 CloudFormationテンプレートの分離方針 OpsWorksマルチレイヤーによるインスタンスの管理 OpsWorksでのdry-runとdiff YAML版CloudFormationでOpsWorks(Chef)を定義する場合の注意点 まとめ Before / After CloudFormationとOpsWorksを導入するまで、弊社のネットワーク管理は以下のようになっていました。 ネットワークの構築やChefの実行は全て手作業です。これがCloudFormationとOpsWorksの導入後は以下のようになりました。 GitHubにpushすると、syntax checkが走ります。masterにマージされるとCloudFormationにchange setが作成されるため、変更内容を確認して反映させます。 各インスタンスの設定はOpsWorksで管理されており、任意のタイミングでChef実行の依頼を行います。リソース変更やChefコマンドの実行を手作業で行うことはありません。 なおchange setとはこれからどのリソースがどのように変更されるか(中断なし・一時的に中断・置換)を事前に知ることができるものです。 意図しない変更を避けるために最後だけは人の目による変更承認フローを挟むようにしています。 効果 CloudFormationとOpsWorks導入で大きく3つの効果がありました。 再利用性の向上 ノウハウの蓄積 履歴管理 再利用性の向上 OpsWorksごとCloudFormationを使って定義することで、ネットワークとインスタンスの構成管理が一枚のテキストで完結するようになりました。 Webコンソールが気を利かせてくれていた部分も含めてテキストで記述するので、イニシャルコストは大きいですが一度作ってしまえばその後は楽ができます。 アプリケーションは案件ごとに変わっても、ネットワーク要素は定形であることが多く、初期構築の時間を別のことへ回せるようになりました。 ノウハウの蓄積 感覚値として、EC2インスタンスを立てるくらいであれば手作業の方が若干早いです。 上で挙げた通り、複製が簡単。初期構築のハードルが下がる。というのは事実ですが、CloudFormationに作業時間の短縮を期待していると意外に裏切られます。 導入によって得られる効果は、ノウハウの蓄積です。RDSやElastiCacheのパラメータグループのように、設定を忘れると後で痛い目をみる項目も全てテキストで管理されます。そのため、別の誰かへノウハウを伝えるということができるようになりました。 また、後で悩みがちなセキュリティグループやIAMのルールについては積極的にコメントで補足しています。 履歴管理 CloudFormation・OpsWorks共に実行履歴がすべて記録されます。これまではいつどの何を変更したのかを思い出すのに苦労していましたが、今は記録が残るので重宝しています。 CloudFormationとは AWS CloudFormationは、AWSリソースをJSON/YAMLで表現したテンプレートを元に、その構成を自動で構築してくれるサービスです。2016年9月のアップデートでYAMLに対応し可読性と保守性が高まりました。 以下のYAMLは「VPC」「Subnet」「RouteTable」を作って関連付けるサンプルテンプレートです。テンプレートを元に作られるAWSリソースのグループをスタックと呼びます。 --- AWSTemplateFormatVersion : 2010-09-09 Parameters : VPCCidrBlock : Type : String Default : '10.0.0.0/16' PublicFirstCidrBlock : Type : String Default : '10.0.0.0/24' Resources : # VPCを作成 EC2VPC : Type : 'AWS::EC2::VPC' Properties : # [ NOTE ] 作成時にCidrBlockを指定することでテンプレートを使いまわすことができる CidrBlock : !Ref VPCCidrBlock EnableDnsSupport : true EnableDnsHostnames : true InstanceTenancy : 'default' # [ NOTE ] 組み込み変数を参照して定形の名前をつける Tags : - Key : 'Name' Value : !Sub '${AWS::StackName}-vpc' # RouteTableを作成 EC2RouteTablePublic : Type : "AWS::EC2::RouteTable" Properties : VpcId : !Ref EC2VPC Tags : - Key : 'Name' Value : !Sub '${AWS::StackName}-route-table-public' # Subnetを一つ作成 EC2SubnetPublicFirst : Type : "AWS::EC2::Subnet" Properties : # [ NOTE ] スタックを作るリージョンの0番目のAZにSubnetを作る。東京ならaz-a AvailabilityZone : Fn::Select : - 0 - Fn::GetAZs : !Ref 'AWS::Region' CidrBlock : !Ref PublicFirstCidrBlock MapPublicIpOnLaunch : true Tags : - Key : 'Name' Value : !Sub '${AWS::StackName}-public-subnet-first' VpcId : !Ref EC2VPC # SubnetとRouteTableの対応付け EC2SubnetRouteTableAssociationPublicFirst : Type : "AWS::EC2::SubnetRouteTableAssociation" Properties : RouteTableId : !Ref EC2RouteTablePublic SubnetId : !Ref EC2SubnetPublicFirst CloudFormation Designerというテンプレートを視覚的に表示・編集できるツールもあります。これは上のYAMLを読み込ませた例です。 CloudFormation or Terraform ネットワークの構成管理というと Terraform by HashiCorp が有名です。弊社でも導入前に検討を行いました。 CloudFormation Terraform 記述言語 JSON/YAML 独自のDSL dryrun・変更内容の確認 ある(変更セット) ある(plan) 新機能への追従 数か月遅れ コミュニティ次第(自力でも何とかできる) 対応するサービス AWS 様々 Terraformが登場した2014年頃は「CloudFormationではできないことができる」という印象がありましたが、この数年でその差もだいぶ無くなりました。 最終的に、CloudFormation採用の決め手としたのはdryrunの安心感です。ドキュメントを読むとプロパティ1つずつに変更の反映方法が記載されています。 VPCを作成するというテンプレートを例にすると、 CidrBlock と InstanceTenancy は変更すると「置換」という更新が行われます。この場合、新しいリソースが作成されたあと、古いリソースが削除されます。副作用のある更新が事前にわかるのは大変重要です。 EC2VPC : Type : 'AWS::EC2::VPC' Properties : CidrBlock : !Ref VPCCidrBlock # 置換 EnableDnsSupport : true # 中断なし EnableDnsHostnames : true # 中断なし InstanceTenancy : 'default' # 置換 Tags : # 中断なし - Key : 'Name' Value : !Sub '${AWS::StackName}-vpc' cf. AWS EC2 VPC - AWS CloudFormation 一方で、CloudFormationはAWSの新機能への追従がそれほど速くありません。Regional WAFへの対応は1.5か月ほどでしたがこれは比較的早いという印象です。 cf. AWS WAF on ALB with Cloudformation - AWS Developer Forums 特に告知があるわけでもないため、新機能から数ヶ月が経ち、ふとドキュメントを見ると追加されているといったことがあります。 OpsWorksとは AWS OpsWorksスタックとAWS OpsWorks for Chef Automateの2つがあり、弊社で利用しているのは前者です。 Stack > Layer >Instances という階層構造でインスタンスを管理します。 OpsWorksは管理下のインスタンスに対してChefを実行することで構成管理を行います。Chefの実行対象は1インタンスからスタック全台同時まで自由に選べます。 また同一スタック間に所属するインスタンス間では /etc/hosts が常に同期されておりSSHするときに大変便利です。 $ hostname gateway1 $ cat /etc/hosts # This file was generated by OpsWorks # any manual changes will be removed on the next update. # OpsWorks Layer State 127.0.0.1 localhost.localdomain localhost 127.0.1.1 gateway1.localdomain gateway1 < private ip > gateway1.localdomain gateway1 < public ip > gateway1.localdomain-ext gateway1-ext < private ip > web1.localdomain web1 < private ip > web2.localdomain web2 SSH/sudo管理 OpsWorksが提供するのはChefの実行だけではありません。OSがLinuxであれば、IAMユーザとLinuxのユーザを紐付けて管理することができます。 公開鍵を登録すると、IAMユーザに対応したSSHユーザがスタック単位で作られます。 あとは、チェックボックスでSSH許可・sudo許可を選択するだけでそのユーザがインスタンス側に作成されます。 CloudFormationとOpsWorksの役割分担 CloudFormationでもヘルパースクリプトを使うと、インスタンスに対してパッケージのインストールなどを行うことができます。 しかし、継続的に運用するのであれば、diffを見たりdry-runをしたり何が起こるのかを事前に把握してから変更を加えたくなります。そのため、インスタンス内の構成管理は全てOpsWorks(Chef)に集約する形を取りました。OpsWorks自体の設定はCloudFormationで行えるため、いずれの場合でも設定が散逸することはありません。 OpsWorksLayerMemcached : Type : "AWS::OpsWorks::Layer" Properties : # ... CustomRecipes : Setup : - 'memcached' CustomJson : memcached : cachesize : 6554 listen : 0.0.0.0 maxobjsize : 1m # ... CloudFormationでOpsWorksレイヤーと、Chefレシピに渡すattributesを定義しています。 CloudFormationテンプレートの分離方針 CloudFormationを使う上で悩ましいのは、テンプレートをどのように分離するかということです。 同一テンプレート内であれば、組み込み関数を使って互いを参照することが可能です。 そのため、可能な限り1つのテンプレートでリソース定義を行う方が依存を解決しやすくなります。 一方で、1つのテンプレートに全てを記述すると再利用性を損ねる可能性があります。たとえば個人ごとのIAMユーザは、あるアカウント内で1つにしておきたいものです。 そこで、IAMなどをまとめたテンプレートとVPCを基準に分離するテンプレートの2つに分けて定義することにしました。 便宜上、前者をインフラテンプレート、後者をサービステンプレートと呼んでいます。 個人ごとのIAMユーザやグループ、ACMの証明書やCloudTrailはインフラテンプレートにまとめます。 一方、VPCやS3、EC2インスタンスやIAMインスタンスプロファイルはサービステンプレートにまとめます。 開発に伴ってステージング環境や負荷検証用の環境が欲しい場合は、プロパティを変えつつ同じサービステンプレートからスタックを量産します。 また、この時インフラテンプレートで宣言した要素は クロススタック参照 を使って、サービステンプレートで参照させています。 # インフラテンプレート Resources : # ... IAMRoleRdsEnhancedMonitoring : Type : "AWS::IAM::Role" Properties : # ... ManagedPolicyArns : - 'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole' # ... Outputs : OutputIAMRoleRdsEnhancedMonitoringArn : Value : !GetAtt IAMRoleRdsEnhancedMonitoring.Arn Export : Name : !Sub "${AWS::StackName}-iam-role-rds-enhanced-monitoring-arn" # サービステンプレート Parameters : IAMRoleRDSEnhancedMonitoringArn : Type : String Default : '<stack name>-iam-role-rds-enhanced-monitoring-arn' Resources : # ... RDSDBInstance : Type : "AWS::RDS::DBInstance" Properties : # ... MonitoringRoleArn : Fn::ImportValue : !Ref IAMRoleRDSEnhancedMonitoringArn # インフラテンプレートの要素を参照 参照先をべた書きせず組み込みのRef関数で解決したいというのが主な理由ですが、不用意な要素の削除を禁止するという狙いがあります。 クロススタック参照で参照された値は、すべての参照が消えるまで要素の削除が禁止されます。 この例の場合だと、サービステンプレート中で IAMRoleRdsEnhancedMonitoringArn が消えない限り、インフラテンプレートから IAMRoleRdsEnhancedMonitoring を消すことができません。 OpsWorksマルチレイヤーによるインスタンスの管理 OpsWorksでは1つのインスタンスを複数のレイヤーに所属させることが可能です。弊社では、これを積極的に使っており共通の設定をまとめたレイヤーを作成し、全てのインスタンスは必ずそこへ所属させています。この共通設定をまとめたレイヤーは便宜上コモンレイヤーと表記します。 コモンレイヤーの目的は3つです。 Chefの記述をDRYに行うため レイヤー同士の関係性を明らかにするため CloudWatch Logsの設定を簡易にするため Chefの記述をDRYに行うため 元々弊社では、設定をDRYに行うことを目的にChefのロールに親子関係を持たせて管理していました。 base configure # カーネルパラメータや監視設定 -> API configure # Rubyやnginxの設定 -> サービスAのAPI configure # ホスト名やサービス依存の設定 -> サービスBのAPI configure 同様のことをOpsWorksで実現するため、 base configure に相当する部分をコモンレイヤーの設定にまとめ、全インスタンスを所属させています。 レイヤー同士の関係性を明らかにするため 次に、レイヤー同士の関係を明らかにするためです。これはOpsWorksにすることで前々からの課題が解決されました。 例えば、APIとBATCHで同じ親を持っているとします。 2階層であれば憶えられますが、階層が深くなってくると思わぬところで知らない依存ができている可能性があります。 OpsWorksではデプロイ画面でインスタンスを選択すると所属するすべてのレイヤーでチェックがつくため、これが視覚的に分かりやすくなりました。 ただし、OpsWorksのレイヤー自体に親子関係という考え方はありません。親と子に相当するレイヤーをインスタンスに割り当てる部分は、自らで管理する必要があります。後から変更することもできますが、インスタンスが再起動するので気軽には行なえません。 CloudWatch Logsの設定を簡易にするため 最後はCloudWatch Logsとの連携を簡易にするためです。 OpsWorksではレイヤー単位で任意のログを指定して、CloudWatch Logsへ集約することができます。 これはレイヤー単位で設定するものなので、「全台のauth.logを回収したい」という時は、全レイヤーにその設定をして回る必要があります。この項目はCloudFormationで設定できないので手作業です。コモンレイヤーがあれば、一度設定してやるだけで全台で回収してくれます。 OpsWorksとdryrunとdiff OpsWorksによるChefの実行は大変便利なのですが、2つ難点があります。 dryrun(ChefではWhy-runと呼ぶ)ができないこと 変更の差分が見えないこと 実行ログにこういったdiffも出力されないため、成功しても何がどう変わったのかがわかりません。とても不安です。 そこでdryrunとdiffが利用できるようにChefレシピを書いてコモンレイヤーで読み込むことにしました。 dryrun こちらは簡単です。 Chef :: Config [ :why_run ] = node[ ' opsworks_helper ' ][ ' whyrun ' ] Chef::Config[:why_run] が true の時、Chefはdryrunモードになります。 OpsWorksはChef実行時に追加でattributesを渡せるので、そこにtrue/falseを入れてやります。 { " opsworks_helper ": { " whyrun ": true } } diff こちらは少し面倒です。 結論から言えば、OpsWorksが使うChefラッパーを書き換えることで解決します。 以下が、実際に使っているパッチです。 --- chef_command_wrapper.sh.org 2017-07-26 19:39:01.000000000 +0900 +++ chef_command_wrapper.sh 2017-07-26 19:48:45.000000000 +0900 @@ -1,4 +1,4 @@ -#! /bin/bash +#!/bin/bash # Wrapper for chef to catch STDOUT and STDERR into a file, # necessary because this is run with sudo @@ -92,9 +92,8 @@ echo -e "$LOG_LINE_TO_PREPEND" >> "$CHEF_LOG_FILE" fi -exec &> >(tee -a "$CHEF_LOG_FILE") 2>&1 -RUBYOPT="$_RUBYOPT" "$CHEF_CMD" -j "$JSON_FILE" -c "$CHEF_CONFIG" $RUN_LIST -CHEF_RETURN_CODE=$? +RUBYOPT="$_RUBYOPT" "$CHEF_CMD" -j "$JSON_FILE" --format doc -l warn -c "$CHEF_CONFIG" $RUN_LIST 2>&1 | tee -a "$CHEF_LOG_FILE" +CHEF_RETURN_CODE=${PIPESTATUS[0]} if [ -n "$LOG_LINE_TO_APPEND" ] then 重要なのは、execの代わりにパイプを使うことと、 PIPESTATUS を使う部分です。 OpsWorksがChefを実行する場合、 chef_command_wrapper.sh を使います。このスクリプトの実体は /opt/aws/opsworks/current/bin/chef_command_wrapper.sh にあり、Chefを実行する直前に標準出力をexecでteeに流しています。ここのexecがdiffを闇に葬っています。これを回避するため上のパッチではexecを消しパイプでログを受け取るよう変更します。 そして、パイプにしたことで $? ではChefのExitコードが受け取れなくなりました。OpsWorksは CHEF_RETURN_CODE で成功・失敗を判断しているため、常に成功扱いになってしまいます。そのため PIPESTATUS を使ってパイプで繋いだChefのExitコードを取得するようにします。 このパッチもコモンレイヤーのChefレシピに含まれており、サーバ起動後2回目のChef実行からdiffが有効になります。1回目の実行はまっさらなインスタンスが初期化されるだけなので問題になりません。 YAML版CloudFormationでOpsWorks(Chef)を定義する場合の注意点 YAMLが使えるようになったことで、人間的な設定ファイルを書くことができるようになりました。 ただし、YAMLを使ってCloudFormationでOpsWorks(Chef)を設定する場合は注意する必要があります。 attributesがすべて文字列になってしまうという点です。 例えば mackere-agent が勝手に起動しないよう、レシピで start_on_setup を与えたいと思います。 OpsWorksLayerCommon : Type : "AWS::OpsWorks::Layer" # ... CustomJson : mackerel-agent : start_on_setup : false しかしこれは期待通りに動作しません。JSONとして次のように評価されます。 { " mackerel-agent ": { " start_on_setup ": " false " } } falseが文字列です。レシピの中で"false"は真の扱いとなり、 start_on_setup=true になってしまいます。これは他のレシピでも起こりえます。 そのため、場合によってはYAMLの中でJSONを記述してやる必要があります。 OpsWorksLayerCommon : Type : "AWS::OpsWorks::Layer" # ... CustomJson : | { "mackerel-agent" : { "start_on_setup" : false } } まとめ 弊社ではネットワークの構築と管理にAWS CloudFormationとAWS OpsWorksを導入しました。 その結果、手作業で行っていた作業の大部分がなくなり、作業履歴の記録やノウハウの蓄積が可能になりました。 また、CloudFormationとOpsWorksを使ってより効率的に管理するためには幾つかの工夫が必要でした。 本記事が利用を検討している方の一助となれば幸いです。 さいごに 弊社では既存の枠組みにとらわれず、理想の形に向かって挑戦できるインフラエンジニアを大募集しています。 興味をもっていただけましたら、是非Wantedlyからご応募お願いいたします。
アバター