コネヒト開発者ブログ

コネヒト開発者ブログ

ママリの WebView を JavaScript + Flow から TypeScript に移行しました

これは コネヒト Advent Calendar 2021 11日目の記事です。

こんにちは! フロントエンドエンジニアのもりやです。 今回はママリのアプリ内で使われている WebView を JavaScript + Flow から TypeScript に移行した事例を紹介します。

WebView の課題

今までママリ内で使われている WebView は JavaScript + Flow で実装されていました。

しかし State of JS 2020 の結果からも分かるように現在は TypeScript の人気が高く、実際コネヒトでも新規プロジェクトでは TypeScript が使われています。 開発体験としても TypeScript の方がよく、ツールチェインやライブラリの型定義の充実度も圧倒的です。現在、新規で何かを作るなら Flow を選ぶ積極的な理由はないと私は思います。

また Flow の問題ではないですが、以前から // @flow の漏れなどで Flow のチェックが上手く機能してなさそうという課題もありました。 ここだけ直すこともできますが、全体を直すなら合わせて TypeScript にしたいとなりました。

これらの理由により、WebView を JavaScript + Flow から TypeScript 化する流れになりました。

TypeScript 化の概要

2021年4月〜12月にかけて実施したプロジェクトです。 ただし @ts-ignoreany などでエラーを抑制している部分もあり、完全に移行が終わったわけではありません。 まだ Storybook など一部 JavaScript ファイルが残っている状況です。

TypeScript 移行中でも、開発は並行して行っていました。 対応する開発者も、メインとなる開発業務とは別に、業務時間の 10% 程度の時間をとって進めていました。

全部で4名の開発者が関わっています。 私はその中でも中心的な役割で、作戦を立てたり初期設定を主導して進め、TypeScript 化の作業も半分以上をやっていました。

TypeScript 化の規模

tokei で計測してます。

TypeScript 化前

言語 ファイル数 行数
JavaScript 387ファイル 20,546行
TypeScript 0ファイル 0行

TypeScript 化後

言語 ファイル数 行数
TypeScript 448ファイル 30,140行
JavaScript 31ファイル 1,175行
  • ファイル数が増えているのは、TypeScript 化の途中でも機能開発していたためです。

TypeScript 化の作戦

一括置換(失敗)

まず最初に、拡張子と変換が必要な特定の型定義パターン(例: ?stringstring | null | undefined に変更するなど)を機械的に変換する方法を試しました。

結論から言うと、これは失敗に終わりました。 原因としては、Flow がきちんと機能していない、という課題に起因しています。

一見定義されて動きそうに見えても @flow の定義漏れなどで any のように扱われてしまっている箇所がいくつもありました。 そのため TypeScript 化し、きちんとチェックが走ることによってエラーが多発してしまうという状況でした。 ある程度機械的に置き換えられるものを置き換えた後でも数百件のエラーがありました。

また型を外すということも考えましたが、Flow の定義が役に立つ場面もあり、これまでの資産がなくなってしまうのもやめたい、という事情もありました。 (Flow を導入していない、純粋な JavaScript であればもっと簡単だったと思います)

そういった事情から、一度に全部を変換することもそれをレビューすることも難しいですし、リリースしても何かが起きれば一気にふりだしに戻ってしまうので、この作戦は諦めました。

一つずつ手動変換(採用)

最終的に開発者が一つずつ JavaScript を TypeScript に変換していく作戦にしました。 数も多いので大変なことは予想していましたが、これが現状取れる手の中で最善と判断しました。

TypeScript 化の対応

ファイルを一つずつ TypeScript 化していく方針を立てたので、次は TypeScript と JavaScript + Flow を共存していくための対応をしました。

WebView はアプリ内で変更が多い画面などによく使われており、長期間開発を止めるのは難しいので、混在した状態でもビルドができるようにしなければならないためです。

Webpack の設定

まずビルドで使っている Webpack の設定を更新します。

依存パッケージに typescriptts-loader を追加し、webpack.config.js に以下の設定を追加します。

+ {
+   test: /\\.tsx?$/,
+   include: path.resolve(__dirname, 'assets'),
+   use: ['babel-loader', 'ts-loader']
+ },

これで .ts .tsx ファイルをビルドできるようになりました。

import 文の対応

JavaScript + Flow ↔ TypeScript 間でファイルを import しようとするとそれぞれ型エラーが出ます。 それぞれ以下の方法でエラーを抑制しました。 (抑制しただけで、それぞれの間で型情報を引き継げるわけではありません)

JavaScript から TypeScript のファイルを参照する場合

この場合は Flow がエラーを出します。

対応としては、まず TSFlowStub.js.flow というスタブ用のファイルを配置して、中身を以下のようにします。

export default {};

そして .flowconfig に以下の指定を追加します。

+ module.name_mapper.extension='ts' -> '<PROJECT_ROOT>/TSFlowStub.js.flow'
+ module.name_mapper.extension='tsx' -> '<PROJECT_ROOT>/TSFlowStub.js.flow'

最後に JavaScript ファイルを読み込む時に、拡張子 .ts または .tsx を参照します。 すると Flow は自動的に TSFlowStub.js.flow の型を参照してくれるので、エラーが出なくなります。

import foo from './foo.ts'

TypeScript から JavaScript のファイルを参照する場合

このパターンは @ts-ignore で抑制しました。 (これで Webpack はエラー無くビルドしてくれました)

// @ts-ignore
import foo from './foo'

自動テストの対応

一旦テスト自体を止める、という判断をしました。

共存する設定も試してみたのですが、大掛かりになって時間がかなり掛かりそうでした。

もともと10ファイル程度しかなく、あまり変更が入らないものが多かったので、共存する設定をするよりも止めておくほうがよいと判断しました。

スクリーンショット比較テストの導入

先に書いたとおり、ママリの WebView のフロントエンドのテストはほとんど無く、開発者による手動テストに頼っていました。 なので、TypeScript 化による意図しない変更に気付けるようなテストを入れておきたいと考えました。

ただテストの導入にコストをかけすぎると TypeScript 化が進まないので、なるべくコスパ的に良いテストを探していました。 検討した結果、主要な画面のスクリーンショットをとって、変更がないかをチェックするものであれば導入のコストが低く、効果も高いと考えました。

具体的には cypresscypress-image-diff-js というライブラリを使って、Chrome で主要な画面をスクリーンショットでの比較をするテストを、Pull Request ごとに GitHub Actions 上で実行するようにしました。 (キャンペーン用ページなど一時的に使って、今は使わない画面などはチェックの時間が増えるだけなので除外しました)

これにより、主要な画面が表示できて変更がないことを自動でチェックできるようになり、一定の安心感ができました。 また、テストが失敗した場合でもスクショと差分をアーティファクトとしてアップロードするようにしたので、どこが失敗したのかも見れるようにしています。

ちなみに WebView なので、当初は TestCafe を使って iOS の環境に近い Safari 上でのテストもしたかったのですが、時々テストが止まってしまうという不具合が起きていたので、一旦諦めました。 毎回ではなく時々止まる、という症状で原因の特定が難しく、あまり時間もかけたくなかったので、詳しい原因までは探れていないです。

ESLint のアップデート

弊社では @connehito/eslint-config という ESLint の設定を OSS として公開していて、Flow 用の v1 系から、TypeScript に対応した v2 系にアップデートしました。

Flow と TypeScript が共存していて書き方が違うものなので、そのあたりを考えてやりました。 1つの Pull Request でやってしまい、かなり Diff が大きくなってしまったのは反省ポイントです・・・。(ほんとすみません。レビューありがとうございました)

https://mryhryki.com/file/bWB6Ftu3-y5cGc2BBpwbLxRvAnhEl-7.png

↓ Pull Request に大まかな変更のポイントを書いていましたので、ついでに載せておきます。

https://mryhryki.com/file/bWB6G3hu0ChMHuS5xdkMMeDTBvhpmf7.png

ちなみに、今までは CI で ESLint によるチェックが行われていなかったので、CI でチェックする対応もしました。

TypeScript 化の作業方針

最初の設定が終わってしまえば、あとは JavaScript + Flow のファイルを TypeScript に変更していくだけです。 ここでは実際に作業する中でできた方針について書いていきます。

依存関係が少ないものから順にやる

他のファイルへの依存がない・少ないファイルの方がやりやすいので、分担してそういったファイルから進めてていきました。

具体例としては、API リクエストやユーティリティ関数などを最初に進めました。

型定義をしながら進める

当初は anyobject などで曖昧になっていた部分を、なるべく型定義を追加しつつ TypeScript 化していきました。

特に API レスポンスは、今まで Markdown でドキュメント化はしていたものの、コード上では何の型定義もできていませんでした。

ここの定義を追加することで、アプリ内で使われるデータのチェックや IDE による補完も効くようになり、かなり開発体験が向上しました。

方針転換:とにかく進める

最初は型定義を拡充していましたが、本丸である UI 系のコードに入ってくると型定義的に不整合が起きる場面が増えてきました。Flow がきちんとチェックされていない影響は、特にこの辺りで大きかったです。

具体例として、例えば配列が渡ってくる場合 JavaScript のプリミティブな配列なのか Immutable.js のリストなのかが合致していない、などがありました。 極端な場合、どちらが来ても動くようになってて、動かしてログに出さないとどちらを期待しているのかがわからないという状況などもありました。

UI のコードは一番ファイル数的に大きいので、あまりコストを掛けてやると終わらない見通しになってきました。 また影響範囲もその画面、その部分でとどまる場合が多いので、一旦は動いているものを正として、 eslint-disable@ts-ignore でのチェック抑制をして進めました。

きちんとした対応をしようとすると、おそらく今年度中には終わっていなかったと思います。

おまけ:ネイティブとの連携箇所でのエラー

WebView から FireBase の機能を呼び出す際は、ネイティブ側と連携して動作しています。 その呼び出し関数を、一度変数への代入を使うとエラーが発生するという不具合がありました。

// iOS の例
const { postMessage } = window.webkit?.messageHandlers?.firebase ?? {}
if (postMessage != null) {
  postMessage(...)
}

推測ですが window.webkit.messageHandlers.firebase.postMessage という関数が呼ばれた時にネイティブ側がハンドリングしているものと思われます。

しかし ?. を使うと、一度変数に入れるようなコードに変換されてしまいます。

https://mryhryki.com/file/bWB6GRucQB0EEZasOO9yMXnP3aCtbYR.png

そのため window.webkit.messageHandlers.firebase.postMessage が呼ばれたとネイティブ側で判断できず、エラーになってしまうのだと考えられます。

解決策としては、以下のように順にチェックをしておくと特にエラーにならず正常に動作するようになりました。

if (
  window.webkit &&
  window.webkit.messageHandlers &&
  window.webkit.messageHandlers.firebase &&
  window.webkit.messageHandlers.firebase.postMessage
) {
  window.webkit.messageHandlers.firebase.postMessage(/* ... */)
}

アプリ内で使う WebView ならではの不具合でした・・・。

ちなみに以下のようにすれば大丈夫そうな気もするんですが、試してはいません。

if (window.?webkit.?messageHandlers.?firebase.?postMessage) {
  window.webkit.messageHandlers.firebase.postMessage(/* ... */)
}

後始末

Flow の除去

Flow 関連のパッケージの除去や設定ファイル、スタブ用ファイル ( TSFlowStub.js.flow ) を削除しました。

これで完全に Flow への依存がなくなりました。

不要な抑制コメントの除去

TypeScript 化が終わったことで不要になった eslint-disable@ts-ignore を除去していきました。

やった人はすぐ分かりますが、あとから見る人は何のためのものなのか判断に困るので、こういうお掃除はなるべく早く対応しておきたいですね。

テストコードの TypeScript 化

一時的に止めていたテストを TypeScript 化し、動かせるようにしました。

また WebView ではテストフレームワークとして ava を使っていたのですが、コネヒトでは基本的に jest を使っていたので、統一して jest に移行しました。

State of JS 2020 でみても Jest の人気が高いというのもあります)

https://mryhryki.com/file/bWB6GgZmC8t7mer1yGoRu5A42Xk9vDJ.png

感想など

長い戦いでした(まだ終わってないけど)が、ようやく終わりが見えてホッとしています。 ちゃんとやりたい部分もありつつ、時間との兼ね合いがあるので、どこまでやってどこはやらないかを判断するのが大変でした。

TypeScript に統一できたことによって、開発体験としてはかなり良くなりました。Flow でも一定サポートはしてくれますが、やはり TypeScript のツールチェインは充実しています。

まだ型定義がちゃんとできていない部分なども残っていますが、今後を開発を進めながら地道に健全な状態にしていきたいと思います。

PR

コネヒトでは、フロントエンド開発のモダン化に挑戦したいエンジニアも募集中です!

hrmos.co

CakePHP3から4へのバージョンアップ時に困ったキャッシュ周りの話

こんにちは。バックエンドエンジニアのTOCです。

このエントリは、 コネヒト Advent Calendar 2021 の10日目のエントリです。 9日目は @otukutunさん による 洋書読み始めるならThe Minimalist Entrepreneurがオススメ でした。

弊社では多くのプロダクトで CakePHP を利用しております。エンジニア組織の取り組みで CakePHP のバージョンアップ対応を行ったのですが、リリース時に起こった困りごとについて、今回は紹介したいと思います。


目次


はじめに

弊社では半期ごとに目標を設定するのですが、各チームが持つ目標とは別にエンジニア組織としての目標も持ちます。この目標は開発組織をよりよくするための取り組みであり、事業に直結しないかもしれないけど、開発組織としてやっていきたいライブラリのアップデートなどをやっていくために置かれているものです。 2021年度の上期では PHP・CakePHP のアップデートや TypeScript 化などが題材として挙げられ、それぞれについて希望した取り組みを目標としてもつことになりました。 やりたいけど、なかなか優先度が上げづらい取り組みを会社として進めようとしてくれるのはありがたいですね🙌

僕はあまりフレームワークのアップデート経験がなかったので PHP・CakePHP のアップデートに取り組むことになりました。

どんなことをやったのか

弊社では PHP を用いたプロジェクトがいくつかありますが、半年で全てのプロジェクトを扱うのは難しかったので、対象を絞ってアップデートを行うことにしました。 その中で、弊社のプロジェクトの中でも規模が大きいリポジトリをアップデート対象にすることに決めました。このリポジトリについて、下記のようにアップデートを行いました。

before after
PHP 7.1.33 7.4.15
CakePHP 3.8.1 4.2.8

アップデート作業はCakePHPドキュメントのアップグレードガイドを参考にアップデートし、非推奨機能を愚直に潰していきました。 困った時はドキュメントを見るだけでなく、実際の CakePHP のコードを見て、ディレクトリ構成や処理の仕方を参考にすることで納得感を持って修正ができました。

余談ではありますが、途中PHPバージョンアップ kickoffというドンピシャなイベントがあり、チームメンバーと参加しました。規模が大きいプロジェクトでは中々骨が折れる作業ではありますが、uzullaさんのPHPバージョンアップけもの道という発表を聞いて、強い気持ちを持ってバージョンアップに取り組もうと決意を改めました。 これからバージョンアップ作業を行う方やバージョンアップに疲弊したときは読んでみてください。

リリースしてどうなった?

このエントリの本題に入っていきたいと思います。 今回規模が大きめのプロジェクトで、修正範囲も大きめだったこともありカナリアリリースを行うことにしました。 インフラチームに協力してもらい、Canary 環境を作成し、いざリリース!

...ん、なんかエラーが出始めた...

Trying to get property of non-object なるエラー通知が起こるようになりました。 一旦カナリアリリースによる影響なのかを調査し、どうやらバージョンアップが原因そうだったのでカナリア環境を一旦停止し、第一弾リリースは撤退することとなりました...orz

原因を調べる

開発環境では起きなかったのに、本番環境で起きたエラーだったので、なんで起きているのかパッとはわかりませんでした。ただエラーが起きている箇所を見てみると、Redis を使用したキャッシュを利用してることがわかりました。そこで一度キャッシュを利用しないで動かしてみるとエラーが発生しなかったので、どうやらキャッシュを利用してる箇所が怪しいぞ、ということになりました。

調査を続けていると、ある仮説が挙がりました。今回カナリアリリースにより本番環境と分けているけど、キャッシュは同じものを利用している。つまり

「CakePHP3と4で扱うデータ構造に互換性がないのではないか...?」

そうです、CakePHP4になったとき、Entity の _properties_fields に変わっていたことにより、下記のような事象が起きていました。

  1. カナリアリリースにより、本番(CakePHP3環境)とカナリア環境(CakePHP4環境)が共存する
  2. CakePHP3環境で値をキャッシュする
  3. キャッシュされた値をCakePHP4環境で取得
  4. _fields にアクセスしようとするが、_propertiesに格納されているため、Trying to get property of non-object が発生

cf. _properties_fields に変わっているcommit

なるほど、開発環境ではCakePHP4環境のみになっていたので、エラーが起きなかったことも頷けます。 バージョンアップにおけるエラー箇所の修正時に知っていたことではありましたが、カナリアリリース時に問題になることは盲点でした。

なんとか原因究明ができました。カナリアリリースのおかげでキャッシュによるエラーを検知しつつ、影響範囲を最小限に抑えることができました...

どう対応したのか

今回は CakePHP3 と CakePHP4 でデータ構造の互換性が問題だったので、CakePHP3と4環境でキャッシュキーを別にして、それぞれの環境でキャッシュした値を利用する形に修正しました。 幸運なことに、組織目標における別の取り組みをしていたチームが Redis の容量を大幅に削減してくれていたこともあり、キャッシュの容量には余裕がありました。 なので、CakePHP4 環境では〇〇_cakephp4-tempといったサフィックスをつけてキャッシュを分けることにより対応を行いました。

リリース再挑戦とその後の対応

再度開発環境での確認や、リリーススケジュールを調整し、2回目のカナリアリリースに挑戦しました! 今度は前回起きたエラーも起きず、その他にもバージョンアップによるエラーが起きていない状態でした。無事カナリア1%リリースが成功し、徐々に比率を上げていき、最終的に本番環境へのリリースが完了しました。

リリース後は一時的につけていたサフィックスを取り外す対応を行い、CakePHP4アップデート作業に終止符を打つことができました。

まとめ

今回はPHP・CakePHPバージョンアップにおいて、特にリリース時に起こった困りごとについて紹介しました。 リリース時に起きうる事象として、今後同じような現象を踏まないように参考になると幸いです。

バージョンアップ作業はかなり愚直な作業が多いかもしれませんが、そのフレームワーク自体の理解も深まったりして非常に学びが多い挑戦でした。 また、バージョンアップに関わったメンバーはもちろん、インフラチームや他のチームメンバーにもテストやリリースに協力いただき、組織的にコラボしながら進めることができて、恵まれているなぁと改めて実感しました。

明日は @mryhryki さんによる、TypeScript 化のお話しです🙌

コネヒトの今回のような活動に少しでも興味をもたれた方は、ぜひ一度お話させてもらえるとうれしいです。

hrmos.co

おまけ

CakePHP3から4へアップデートするにあたり、キャッシュ周りでもう一つ変更があったので、そちらもご紹介いたします。 CakePHPでキャッシュの読み込みをする際に

<?php
Cache::read('my_data');

という形で取得できますが、キャッシュデータがない場合、CakePHP3と4で return する値が変わっていました。CakePHP4の場合 null CakePHP3の場合 false が返ってくるように変更されています。 これにより型の違いでエラーになる可能性があるので気をつけてください。

CakePHP4

<?php
/**
 * Read a key from the cache.
 * .
 * .
 * .
 * @return mixed The cached data, or null if the data doesn't exist, has expired,
 *  or if there was an error fetching it.
 */
public static function read(string $key, string $config = 'default')
{
    return static::pool($config)->get($key);
}

CakePHP3

<?php
/**
 * Read a key from the cache.
 * .
 * .
 * .
 * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it
 */
public static function read($key, $config = 'default')
{
    // TODO In 4.x this needs to change to use pool()
    $engine = static::engine($config);

    return $engine->read($key);
}

CakePHP の Authentication プラグインを使って、複数の Authenticator を有効にする

こんにちは!久々の登場となります @takoba_ です!!

このエントリは、 コネヒト Advent Calendar 2021 の 8日目 のエントリです。 7日目は @shnagai による 異なるデータソース間の同期がしたくてAWS DMSについて調べたのでまとめてみた でした。

tech.connehito.com

CakePHP の Authentication プラグインとは?

CakePHP は CakePHP4 がリリースされるタイミングで、認可をする機能を cakephp/authorization プラグインに、認証をする機能を cakephp/authentication プラグインに分離しました*1。これらのプラグインは、実は PSR-7 に準拠した Middleware を提供する Composer パッケージになっていたりします。つまりは、 PHP 界隈にとっては非常にナウい実装になるわけです!

(加えて、 Middleware 部などに限って言えば、理論上は他のフレームワークでも流用可能!というわけですね〜〜)

???「○○えもん〜〜ログイン画面でメールアドレスと screen_name のどちらかを ID として許容するようにしたいよ〜〜」

さあ、そんな便利でナウい実装を持つプラグインを使いこなしていきたいわけなんですが、たとえばログイン画面で「メールアドレスと screen_name のどちらかを ID として許容するようにしたいよ〜〜😢」ってこと、ありませんか?

(一旦下を向いてコントに入るフェイントをしつつ)ありますよね〜〜〜?*2

この場合に登場するのが cakephp/authentication プラグインになります。 CakePHP Cookbook では、 CMSを実装することをケーススタディにして認証機能を追加するチュートリアル を用意しており、"メールアドレスとパスワード" といった単一の認証要素パターンでの認証機能の実装を説明していますが、先ほど例示した "メールアドレスとパスワード" または "screen_nameとパスワード" というような複数の認証要素パターンを待ち受ける場合に対して、どのように対処するかは説明されてません でした。

本エントリでは、上記のような問題を解決する実装を説明したいと思います!🙋‍♀️

前提: 今回の登場人物

唐突だが、まずは cakephp/authentication プラグインに登場するクラスたちの紹介だ!🎸

cf. CakePHP4 の認証処理 cakephp/authentication のメモ - Qiita

Authenticator

cf. 認証機能 - CakePHP Authentication 2.x Cookbook

Authenticatorsは、リクエストデータを認証に変換する処理を行います。 それらは、 Identifiers を利用して、既知の Identity Objects を見つけます。

書いてある通りに、リクエストデータから認証要素パターンのデータを抽出します。具体的には POSTパラメータ として入力された情報をもとに認証要素パターンだけが含まれるデータオブジェクト(といっても Hash だけど)に変換する、といった部分を担うかんじです。

Identifier

cf. Identifiers - CakePHP Authentication 2.x Cookbook

Identifiersは認証者がリクエストから抽出した情報に基づいてユーザやサービスを識別します。 Identifiersは loadIdentifier メソッドでオプション指定することができます。

認証者 is (上記で出てきた)Authenticator ですねわかります。その他は書いてあるとおりなんですが、具体的には Authenticator が抽出した認証要素パターンをもとにデータベースなどを照合してユーザーやサービスを識別し導出する、といったかんじです。

AuthenticationService

authentication/AuthenticationService.php at 2.x · cakephp/authentication

認証処理に関する Facade の役割を果たします。推奨されている実装では、CakePHP アプリケーションを束ねる class App\ApplicationgetAuthenticationService() を定義した上で Component や Middleware から呼ばれる実装になっています。

AuthenticationMiddleware

cf. Middleware - CakePHP Authentication 2.x Cookbook

AuthenticationMiddleware は認証プラグインの中で成形しています。 これは、アプリケーションへの各リクエストを捕らえ、いずれかの認証証明証でユーザーの認証を試みます。 各認証機能は、ユーザが認証されるまで、あるいはユーザが見つからないまで順番に試行されます。 認証された場合の ID と認証結果オブジェクトを含むリクエストには authenticationidentityauthenticationResult 属性が設定され、認証子によって提供された追加のエラーを含むことができます。

AuthenticationService を使って、認証処理を実行します。実態は AuthenticationService::authenticate() を実行していて、各種 Authenticator を順繰り実行して認証の試行を行うかんじです。

$service->loadAuthenticator() は同じ Authenticator を複数回読み込めない?

さて、本題に戻ります。

とりあえず、前述した「ログイン画面において、メールアドレスと screen_name のどちらかを ID として許容するようにしたい」という要件を満たそうと、以下のような実装をしたとします。

<?php
declare(strict_types=1);

namespace App;

class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
    // いろいろと略

    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        $service = new AuthenticationService();

        // Load the authenticators. Session should be first.
        $service->loadAuthenticator('Authentication.Session');
        $service->loadAuthenticator('Authentication.Form', [
            'fields' => [
                IdentifierInterface::CREDENTIAL_USERNAME => 'email',
                IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
            ],
        ]);
        $service->loadAuthenticator('Authentication.Form', [
            'fields' => [
                IdentifierInterface::CREDENTIAL_USERNAME => 'screen_name',
                IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
            ],
        ]);
        // 以下略
    }

とすると、以下のようなエラーが出てしまい、アプリケーションが起動しなくなりました。

2021-12-0x xx:xx:xx Error: [RuntimeException] The "Form" alias has already been loaded. The `fields` key has a value of `{"username":"screen_name","password":"password"}` but previously had a value of `{"username":"email","password":"password"}` in path/to/app/vendor/cakephp/cakephp/src/Core/ObjectRegistry.php on line 161

これは、 CakePHP で Factory Method パターン を実装する場合に用いられる ObjectRegistry というクラスの仕様なんですが、 ObjectRegistry に登録された object は key-value で管理されているので、 同じ key の object は当然複数持てない、ということになります。困った!

じゃあ、どうする?

でもね、そんなときにもフレームワークの仕様を知ってるとサクッと解決できたりするものです。ここからは上記問題の解決方法を説明していきます。

1. Authenticator 群の設定

まずは Authenticator 群をどのように設定するか考えます。ここでは2つのパターンがあったので、双方説明していきます。

1-1. FormAuthenticator を load する際に alias をつけて回避する

まず、 ObjectRegistry は基本的に key の文字列から Cake\Core\App::className() を用いてクラス名を導出する手筈を採ります。ですが、ObjectRegistry で load する際に className という option を設定すると、keyは自由な文字列を設定することができるようになります。

上記を利用して、実装したのが以下のコードです。

<?php
declare(strict_types=1);

namespace App;

use Authentication\Authenticator\FormAuthenticator;

class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
    // いろいろと略

    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        $service = new AuthenticationService();

        // Load the authenticators. Session should be first.
        $service->loadAuthenticator('Authentication.Session');
        $service->loadAuthenticator('Authentication.FormWithEmail', [
            'fields' => [
                IdentifierInterface::CREDENTIAL_USERNAME => 'email',
                IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
            ],
            'className' => FormAuthenticator::class,
       ]);
        $service->loadAuthenticator('Authentication.FormWithScreenName', [
            'fields' => [
                IdentifierInterface::CREDENTIAL_USERNAME => 'screen_name',
                IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
            ],
            'className' => FormAuthenticator::class,
        ]);
        // 以下略
    }

1-2. src/Authenticator/ にカスタムした Authenticator を実装する

もうひとつの方法としては、class Authentication\Authenticator\AbstractAuthenticator を継承したカスタム Authenticator を定義して、それを利用する方法があります。

今回の場合は、まず class Authentication\Authenticator\FormAuthenticator を継承した class App\Authenticator\FormWithEmailAuthenticator を以下のように定義します。

<?php
declare(strict_types=1);

namespace App\Authenticator;

use Authentication\Authenticator\FormAuthenticator;
use Authentication\Identifier\IdentifierInterface;

/**
 * Form Authenticator with email
 */
class FormWithEmailAuthenticator extends FormAuthenticator
{
    /**
     * @inheritDoc
     */
    public function __construct(IdentifierInterface $identifier, array $config = [])
    {
        parent::__construct($identifier, $config);

        $this->setConfig('fields', [
            IdentifierInterface::CREDENTIAL_USERNAME => 'email',
            IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
        ]);
    }
}

上記に倣って、 class App\Authenticator\FormWithScreenNameAuthenticator も定義しておきます。

そのあとに、 Application::getAuthenticationService() で、以下のように設定します。

<?php
declare(strict_types=1);

namespace App;

class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
    // いろいろと略

    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        $service = new AuthenticationService();

        // Load the authenticators. Session should be first.
        $service->loadAuthenticator('Authentication.Session');
        $service->loadAuthenticator('FormWithEmail');
        $service->loadAuthenticator('FormWithScreenName');
        // 以下略
    }

どうです?こっちの方がシュッとしてるかんじもありますね。

2. PasswordIdentifier は割と柔軟なので、ちょっとconfigを変えればいけちゃう

1.で Authenticator の設定は完了しましたが、続いては Identifier の設定も変更する必要があります。

パスワード認証に用いる class Authentication\Identifier\PasswordIdentifier は割と柔軟に認証要素の拡張ができるので、以下のように ['email', 'screen_name'] として email も screen_name も許容するように設定してあげます。

<?php
declare(strict_types=1);

namespace App;

class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
    // いろいろと略

    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        $service = new AuthenticationService();

        // Load the authenticators. Session should be first.
        $service->loadAuthenticator('Authentication.Session');
        $service->loadAuthenticator('FormWithEmail');
        $service->loadAuthenticator('FormWithScreenName');

        // Load identifiers
        $service->loadIdentifier('Authentication.Password', [
            'fields' => [
                IdentifierInterface::CREDENTIAL_USERNAME => ['email', 'screen_name'],
                IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
            ],
        ]);

        return $service;
    }

これで、求められていた「ログイン画面において、メールアドレスと screen_name のどちらかを ID として許容するようにしたい」という要件が達成できました!助かったよ○○えもん!🐱

おわりに

どうですか?フレームワークやプラグインの仕様を知ってると、ちょっと設定を変えるだけでこんなに柔軟に対応できるようになりました!とてもハッピ〜〜👏

みんなも CakePHP や cakephp/authentication プラグインと仲良くなってハッピーライフを過ごしてください!🙌

コネヒト社では、ぼくらと一緒に CakePHP に詳しくなりながらバリバリ開発を推進していってくれるエンジニアたちを大募集しております!以下よりぜひ応募してくれよな〜〜🤝

明日は @otukutun によるエントリです!おたのしみに〜〜👋

参考

*1:旧来利用されていた AuthComponent は、 CakePHP 4.0.0 がリリースされたタイミングで非推奨となりました

*2:怪奇!YesどんぐりRPGの漫才「財布を拾った」より

異なるデータソース間の同期がしたくてAWS DMSについて調べたのでまとめてみた

こんにちは。インフラエンジニアの永井(shnagai)です。

RDS(MySQL)にあるデータを検索エンジン(今回でいうとOpenSearch)に同期したいというプロジェクトがあり、AWS Database Migration Service(以下、DMS)について調べる機会があったので、これまでAWS SAの方に聞いたりwebで調べたことについてざっとまとめてみました。

この記事はコネヒト Advent Calendar 2021の7日目の記事です。

やりたいことのざっくり要件

最初に今回調査をするトリガとなったプロジェクトの要件を簡単に紹介します。

端的に言うと、RDSにある特定のテーブルをAmazon OpenSearch Service(以下、OpenSearch)に同期したいというもので

  1. RDSにある特定のテーブルのデータをOpenSearchに同期
  2. 将来的にはニアリアルタイム(準同期)でデータを同期したい
  3. データ同期の運用コストは低ければ低いほどよい(もちろん開発すれば同期処理作れるがそれは最後の手段とする)

データ同期手法の候補

AWS SAの方に相談したところいくつかのデータ同期パターンを提案してもらいました。

  • AWS Database Migration Service(DMS)を使うパターン
    • 名前の通りデータベースマイグレーションのためのAWSサービス
    • Bootstrap(バッチ)
    • CDC(継続レプリ)
  • Glue Elastic Views(プレビュー)
  • EC2にLogstashエージェントを使うパターン
    • JDBCドライバでRDSに接続
    • logstashなので、OpenSearchへの接続はお手の物

DMSを使うことで要件を満たせそうだという肌感を得たのでDMSについて、公式ドキュメントを中心にどんなサービスなのか調査してみました。

AWS DMSの特徴

以下は、公式ドキュメントを読み解きながら自分の中で理解したメモになります。

全体像

公式ドキュメントを開くと一番目のつくところに下記の全体像の図が飛び込んできます。

下記の図の登場人物を紹介するとこんな役割になります。

  • Replication Instance: AWS DMSの中心人物。実際のデータ同期処理を実行するEC2インスタンス
  • Source Database: 読んで字のごとくですが同期元のデータソース
  • Source Endpoint: Replication InstanceがSource Databaseに接続するための接続情報
  • Target Database: 読んで字のごとくですが同期先のデータソース
  • Target Endpoint: Replication InstanceがTarget Databaseに接続するための接続情報

f:id:nagais:20211207173346p:plain

一見データマイグレーションするサービスと考えるとまぁそうだよなという登場人物なのですが、ここでうれしいのがターゲットデータソースの多様さです。

AWSマネージドサービスということでデータの同期先に、下記のようなRDB以外のデータソースも指定することが出来、そのアクセスはIAMロールで制御可能なのはうれしいポイントではないでしょうか。

もちろん今回の同期先に要件であるOpenSearchも入っています。

ただ、この異なるデータソース間のデータ同期であればバルクデータローダであるEmbulkや他にもツールは色々ありそうです。

f:id:nagais:20211207173242p:plain

AWS DMSの強みはCDC(継続的なレプリケーション)

先程も書きましたが、異なるデータソース間のデータ同期であればそれが実現出来るツールは世の中にたくさんあります。

では、継続的なレプリケーションで異なるデータソース間のデータ同期となるとどうでしょう?自分の知る限りだと、そのようなツールは多くはありません。

DMSを調べているうちにこの継続的なレプリケーションで、異なるデータソースにデータを同期出来るのが最大の強みだという結論に達しました。

DMSのCDCには下記の仕様があります。

  • CDCを使う場合には、ソースデータストアはレプリケーション出来る必要がある(MySQL系ならbinlog)

これはつまりMySQLの例で示すと、

  • MySQLでいうとbinlogレプリのスレーブにDMSのReplication Instanceを登録
  • DMSはbinlogレプリされた内容を、Replication Instance内でターゲットに即した形に変換して、Target Databaseに同期する
  • Replication Instance内で、特定のテーブルだけを同期するようなフィルタや型変換が出来るから必要なものだけターゲットへは同期することが可能

ソースのデータ同期には、枯れた(信頼性も高い)binlogレプリの仕組みを使い、ターゲットへの同期には、AWSマネージドでデータ変換等にも柔軟に対応してくれるというのは調べている限りかなり筋のいいサービスだなという印象を持ちました。

特に、運用コスト面がほぼかからないのではという点が強力に映ります。

来月には技術検証を終える予定なので、実際の検証でのプロセスはまた別で書こうと思います。

明日は、 @takoba_によるPHPのエントリの予定なのでお楽しみに。

コネヒトではサービスの信頼性向上をミッションに幅広い領域をカバーしながらエンジニアリングの力でサービスをよりよくしていけるエンジニアを募集しています。 少しでも興味もたれた方は、是非気軽にお話出来るとうれしいです。

AWSとDockerでユーザーを支えるインフラエンジニア募集! | コネヒト株式会社

GitHub ActionsのManual Workflowを使ってiOSアプリのバージョンをあげる

これはiOSアドベントカレンダーその2の3日目の記事です。

こんにちは!コネヒトでiOSエンジニアをやっていますyanamuraです。

2021年11月にGitHubActionsにmanual workflowが導入されました。これはなにかというとGitHub Actionsに入力値を指定して手動でトリガーをかけて実行できるようになったのです。

コネヒトではiOSアプリをリリースする際にバージョンやビルド番号をセットするのにこれまではスクリプトを手動で実行してコミットしていましたが、これを使えばこれまで手動でやっていたバージョンアップ作業がGitHub Actionsでできそうなので導入してみました。

設定

.github/workflows 以下にこのymlを配置します

# versionup.yml
name: Manual Version up

# 手動で動かしてアプリのバージョンやビルド番号を変更するアクション
#
# アプリのバージョンを上げる場合はversionを指定するとアプリのバージョンとビルド番号がインクリメントされて設定される
# ビルド番号だけ上げたい場合は、Branchでリリースブランチを指定してversionにはなにも設定しない

on:
  workflow_dispatch:
    inputs:
      version:
        description: new app version x.x.x. if empty only the build number will be incremented
        required: false

jobs:
  versionup:
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v2
    - uses: yanamura/ios-bump-version@v1
      id: version
      with:
        version: ${{ github.event.inputs.version }}
    - name: run
      run: |
        git diff
        git config --global user.name "bot"
        git config --global user.email "bot@example.com"
        git fetch -p
        git checkout -b release-${{ steps.version.outputs.version }}
        git add .
        git commit -m "bump version"
        git push origin HEAD

これで入力でバージョンを指定し、実行するとreleaseブランチをつくってバージョンアップしたコミットをしてpushするworkflowができました。

実行環境にmacOSを使っていますが実行には1分もかからないので無料の範囲内で収まると思います。

実行方法

GitHubのActionsタブのworkflowsの中から追加した Manual Version up を選択します。 f:id:yanamura:20211125143415p:plain

次に Run workflow をクリックし、入力欄にバージョン(例えば1.1.0)を入力し、緑の Run workflow ボタンをクリックすれば実行されます。 f:id:yanamura:20211125143555p:plain

まとめ

このようにmanual workflowを使えば簡単に手動トリガーのworkflowが作成できます。 手動でトリガーをかけたい場面も時々あるかなと思うので活用してみてください!

最後にコネヒトではこういった自動化やSwiftUIに興味のあるエンジニアを募集していますので、少しでもご興味のあるかたはぜひ一度話を聞きに来てください! hrmos.co

Tableauでデータソースの更新に合わせて初期値も更新される日付フィルターの作り方

こんにちは!コネヒトのエンジニアあぼです。この記事はコネヒト Advent Calendar 2021の2日目です。

今回はコネヒトでもデータ分析に活用中のTableauから、Tableauにおいて使う頻度の高い日付フィルターのtipsを紹介します。

f:id:aboy_perry:20211130140426p:plain
日付フィルターの例

データソースが更新されたらフィルターも更新したい

Tableauではデータソースの更新をスケジュール、つまり自動化することができます。データが更新されると新しいデータが増え(例えば月1更新ならば前月分のデータが増えるなど)、ユーザーは鮮度の高いデータをもとに分析ができます。

各種フィルターの値についても同様で、とくに期間を絞り込むものについてはユーザー体験や運用コストの観点からも自動化する効果は高いはずです。Tableauのビューに作用するパラメーターやフィルターなどは、設定によってはデータの更新に自動で追従してくれますが、工夫が必要な場合もあります。今回取り上げるのは、開始日と終了日を両方自由に選択できる形式の日付フィルターです。このフィルターの初期値を、データソース上の直近3ヶ月にしたい、とします。

Tableau 2020.1で動的パラメーターが追加され、ワークブックをひらいたときの値(初期値)を動的な値に設定することが可能です。この動的パラメーターとLOD式を用いて日付フィルターを作成していきます。

作成手順

検証に使ったTableauのバージョンは Tableau Desktop 2020.4 です。

1. フィルターに用いる日付型パラメーターを2つ作成する

f:id:aboy_perry:20211130142221p:plainf:id:aboy_perry:20211130142414p:plain

2. 1で作成した2つの日付型パラメーターをフィルターとして適用させるためのブール型計算フィールドを作成する

f:id:aboy_perry:20211130142748p:plain 絞り込みたい日付のデータは今回は投稿日という名前のパラメーターと仮定します。

3. 2で作成した計算フィールドをフィルター欄にドラッグ&ドロップして、値を真に設定しフィルターを適用させる

f:id:aboy_perry:20211130143006p:plain f:id:aboy_perry:20211130143100p:plain このとき期間内に1つもデータが無ければ真を選べないので注意してください。

4. 1で作成した2つのパラメーターの初期値に使う日付型計算フィールドを2つ作成する

f:id:aboy_perry:20211130143412p:plainf:id:aboy_perry:20211130143445p:plain

この計算フィールドがキモで、データソース上存在する日付の最大値からそれぞれのパラメーターの初期値を算出したいので、LOD式を使います。

それぞれの計算フィールドは以下のようになっています。{}で囲んでいるのがLOD式です。

  • 期間(ここから)の初期値 = {FIXED :DATE(DATETRUNC('month', DATEADD('month', -2, MAX([投稿日]))))}
  • 期間(ここまで)の初期値 = {FIXED :MAX([投稿日])}

期間(ここまで)用の計算フィールドは、データソース上存在する日付の最大値となる日付を返します。期間(ここから)用の計算フィールドは、その最大値から丸3ヶ月前になるような日付を返します。MAX([投稿日])が2021/10/31であれば、期間(ここから)用の計算フィールドは2021/08/01を返します。

ちなみにスコーピングキーワードとしてFIXEDを指定していますが、ディメンションを指定していないので省略が可能です。例えば期間(ここまで)の方は{MAX([投稿日])}と等価です。

なおデータソースによらず単純に今日の日付などから算出したい場合は、TODAY関数などを使うことで動的に設定することが可能です。その場合LOD式は不要です。

5. 4で作成した計算フィールドをそれぞれ、1で作成したパラメータの「ワークブックを開いたときの値」に設定する

f:id:aboy_perry:20211130151055p:plainf:id:aboy_perry:20211130151153p:plain

これで完成です。データソースが更新されるとフィルターの初期値も更新されます。Tableau DesktopだけでなくTableau Serverや、Tableau Serverを使った埋め込み分析でもきちんと動作します。

注意点としては、パラメーターの許容値を「全て」にしているため、データが存在しない日付もカレンダーで選べてしまいます。データ上存在しない日付は選べないようにしたい場合は、許容値を範囲指定する必要がありますが、範囲の最小値/最大値に別々の値を入れたくても固定の値しか設定できないため、自動化できなくなります。この辺は良い解決策があれば知りたいところです。

f:id:aboy_perry:20211130225025p:plain
データ上は10/31までしか扱ってないが今日の日付などが選択できてしまう

おわりに

動的パラメーターとLOD式を用いた、データソースの更新に合わせて初期値も更新される日付フィルターの作り方を紹介しました。Tableauの機能を使い倒すことで、より使いやすい分析ツールにできたり、より運用コストを低くできそうですね。

機械学習プロジェクトにおけるSageMaker Processingの使い所

みなさんこんにちは。機械学習チームのたかぱい(@takapy0210)です。

2021年もあと1ヶ月となりましたね。皆様いかがお過ごしでしょうか。

...さて12月といえば、毎年恒例のアドベントカレンダーの季節ですね!
というわけで、2021年もコネヒト Advent Calendarが始まります!🎉

初日となる本エントリでは、機械学習チームで使用しているSageMaker*1の機能である、Processing*2について、活用事例とともにご紹介しようと思います。


目次


SageMaker Processingとは?

機械学習の前処理、後処理、モデル評価といったワークフローをSageMaker上で簡単に行うためのPython SDKです。
独自のコンテナ上で任意のpythonスクリプトを実行でき、処理が終了するとインスタンスが自動的に停止するというシンプルな機能から、機械学習関連のデータ処理だけでなく、学習や推論にも使うことができるサービスとなっています。

このPython SDKには主に4つのクラスが用意されていて、それぞれで使用方法が異なります。

  • sagemaker.sklearn.processing.SKLearnProcessor
  • sagemaker.processing.PySparkProcessor
  • sagemaker.processing.Processor
  • sagemaker.processing.ScriptProcessor

それぞれについて簡単にご紹介します。
ちなみに、コネヒトでよく用いているのは sagemaker.processing.Processorです。
(以下で紹介するサンプルコードは、notebook上から実行していると思ってください)

SKLearnProcessor / PySparkProcessor

あらかじめ用意されたscikit-learnやPySparkの実行環境(コンテナ)を使用するクラスです。
frameworkのバージョンやインスタンスタイプ、インスタンスの台数を指定し、実行したいpythonスクリプトをrunメソッドの引数に与えて実行します。

from sagemaker.sklearn.processing import SKLearnProcessor
from sagemaker.processing import ProcessingInput, ProcessingOutput

# SKLearnProcessorクラスの実行例
sklearn_processor = SKLearnProcessor(framework_version='0.20.0',
                                     role=role,
                                     instance_count=1,
                                     instance_type='ml.m5.xlarge')

sklearn_processor.run(
    code='processing.py',  # ここで実行したいスクリプトを指定(このスクリプトはSageMaker上に存在する必要がある)
    inputs=[ProcessingInput(
        source='dataset.csv',
        destination='/opt/ml/processing/input')],
    outputs=[ProcessingOutput(source='/opt/ml/processing/output/train'),
        ProcessingOutput(source='/opt/ml/processing/output/validation'),
        ProcessingOutput(source='/opt/ml/processing/output/test')]
)

aws.amazon.com

Processor / ScriptProcessor

独自の実行環境(コンテナ)で処理を行いたい時に使用するクラスです。
Processorクラスはrunメソッドにcode引数(pythonスクリプト)を必要としないクラスで、実行したい処理をDocker image内に内包させる必要があります。
ScriptProcessorクラスはコンテナの作成と処理コードを分けて実行するクラスで、runメソッドにpythonスクリプトを受け渡し、処理を実行することができます。

from sagemaker.processing import Processor

# Processorクラスの実行例
processor = Processor(
    image_uri=image,  # コンテナイメージを指定
    entrypoint=["python3", "/opt/program/processing.py"],  # 実行したいスクリプトを指定(コンテナに内包しておく必要がある)
    env={"PYTHON_ENV": "hoge"},
    role=role,
    instance_count=1,
    instance_type="ml.t3.2xlarge"
)

processor.run()

docs.aws.amazon.com

これまでの課題感

インタラクティブな分析やモデリングなどは以前のブログでご紹介したように、分析用コンテナイメージを用意し、それをローカルマシン上で動かす or SageMaker上(クラウド上)で動かすことで、各人が業務を進めていました。

分析にある程度目処が立ったタイミングで実際のプロダクションコードを書いて、それをプロダクション用のコンテナ環境に乗せてAWS上で動かす基盤を整える、というフローで開発を進めることが多いのですが、このプロダクションコードをどの環境でどのように検証すれば良いのか、という部分に課題を感じていました。

例えば...

  • プロダクション用コンテナ環境を使ってローカルで検証したいが、計算リソースの関係でローカルマシンでは本番相当のデータ量で検証できない
  • クラウド上にある分析用コンテナ環境を使って検証しても良いが、実際に動く環境とは異なるので、プロダクション環境で実行したときに違う挙動をする可能性がある

などなど。

このような課題を解消すべく、現在使っているのがSageMaker Processorです。

SageMaker Processorの活用方法

冒頭で述べたように、弊社でよく利用しているのは sagemaker.processing.Processor クラスです。

下記図は、弊社の機械学習基盤の一部を抽象的に表したものですが、緑色の部分がいわゆる分析環境で、青色の部分が今回紹介するSageMaker Processorを活用したプロダクション動作検証環境になっています。
(図にある分析用コンテナイメージは全プロジェクト共通で、本番用コンテナイメージはプロジェクトごとに異なっています)

f:id:taxa_program:20211129183719p:plain
分析用コンテナと本番用コンテナの使い分けイメージ

実際にSageMaker Processorを利用するのに必要な手順は以下の3つです。

  1. 実行したいpythonスクリプトを内包したコンテナイメージを作成する
  2. 1.のコンテナイメージをECRにPUSHする
  3. SageMaker notebook上からECRにあるコンテナを指定してProcessor Jobを起動する

3.について、コードと共に簡単に解説していきます。

SageMakerからECRにあるコンテナを指定してProcessor Jobを起動する

notebook上からは以下のようにして利用することができます。
コードを見ていただければ分かるように、notebookを起動しているインスタンスとは別のインスタンスを指定して実行できるので、処理内容に合わせて様々なリソース上で実行することができます。

以下の例では processing.py を実行しています。

import boto3
import sagemaker
from sagemaker import get_execution_role
from sagemaker.processing import Processor

role = get_execution_role()
sess = sagemaker.Session()
region = boto3.session.Session().region_name
account = sess.boto_session.client('sts').get_caller_identity()['Account']

# 実行したいコンテナを指定({}の中は任意の文字列に変更してください)
image = f'{account}.dkr.ecr.{region}.amazonaws.com/{image_name}:{tag_name}'

# データ整形JOB
processor = Processor(
    image_uri=image,  # 実行するコンテナイメージ
    entrypoint=["python3", "/opt/program/processing.py"],  # 実行するスクリプト 
    env={"PYTHON_ENV": "hoge"},  # 環境変数
    role=role,  # 実行ロール
    instance_count=1,  # インスタンスの数
    instance_type="ml.t3.2xlarge"  # インスタンスの種類
)

# 処理の実行
processor.run()

例えば、今回実行したprocessing.pyの処理内容が「データを加工して、train.csvというファイル名でS3に保存する」というものだったとします。

この時、生成されたデータが想定通りのデータになっているか確認したい時もあると思います。
そんな時はnotebook上から以下のような処理を実行することでインタラクティブにデータをチェックこともできます。

# 該当データをS3からSageMakerのvolume上にダウンロード
sess.download_data('./', bucket='hoge', key_prefix='dir/train.csv')

# pandasで読み込む
train = pd.read_csv('train.csv')

この辺のことがシームレスに行えるのもSageMakerの良さだと思います。

SageMaker Processorを使うメリット

今までも述べてきましたが、メリットについて改めてまとめると、

  • 本番環境のコンテナ&本番データを使って、(ある程度)お手軽に動作検証ができる。
  • notebookとProcessor Jobで
異なるインスタンスを使えるので、Processer Jobだけに強めのインスタンスを割り当てて実行する、みたいなことができる。
    • Processer Jobで起動したインスタンスは処理終了時に自動的にシャットダウンされるので、「起動しっぱなしでコストが...汗」ということも防ぐことができる。
  • S3を媒介として、データのやりとりも簡単にできる。

という点が挙げられるかな、と思います。

Processorのちょっとイマイチな点

良い点にフォーカスしてお伝えしてきましたが、1点だけイマイチだな〜思っている点があります。

それは、Processor Jobを起動するまでに若干時間がかかってしまうところです。 (こればかりはコンテナのpullやbuildが走るので、一定しょうがないと思いつつ。5分〜ほどかかります。)

とはいえ、この点を考慮しても得られるメリットは大きいと考えているので、これからもしばらくは利用していくと思います。

最後に

本日は、コネヒトの機械学習チームがSageMakerをどのように活用しているか、の一例についてご紹介しました。

もっと話を聞いてみたい方や、少しでも興味が湧いてきた方がいましたら、ぜひ一度話を聞きに来てください! (@takapy0210宛にTwitterDM経由でご連絡いただいてもOKです!)

hrmos.co

hrmos.co