Pj-ortega を振り返る―「トイサブ!」マイページのスタートダッシュ

マイページ開発プロジェクト「オルテガ」に関して、マイページの機能開発は今後も進んでいきますが、2020年6月頃から見えていた「構想」から2022年4月に迎えた「リリース」までを振り返ってみましょう。

参加者

Zoomで振り返ってみてもらいました

抹茶氏:フロントエンドエンジニア、プロダクト開発部テックリード。2022年4月より、プロダクト開発部ユーザーサービス開発チーム チームヘッド。2021年1月入社。

けんご:プロダクトマネージャー。プロダクト企画部所属。2021年10月入社。

きたい:バックエンドエンジニア。2022年4月より、プロダクト開発部プラットフォーム開発チーム。2021年10月入社。

ゆーき(聞き手):コミュニケーション室。広報担当。2020年1月入社。

そもそも、オルテガの始まりは

ゆーき:今回、年表みたいな感じで振り返ってみたいなと思っていまして。まず、オルテガの始まりはいつなんでしょう。

抹茶氏オルテガの始まり……始まりの定義ってなんですかね。

けんご:(笑)

ゆーき:確かに……。オルテガの構想とか、まぁなんでもいいんですけど、始まりの地点を探りたいんですよ。

抹茶氏:ちょっとesa*1で見てみると、2020年の6月に、「トイサブ!でマイページをつくる」ことについて触れていますね。コードとしては、イニシャルコミットがフロントだと2021年10月。バックだと2021年9月だ。

さらにたどってみると、2021年の3月に「オルテガ」っていうプロジェクト名が出てきてる。実際に準備のミーティングがあったのが2021年の7月かな? で、2021年の8月にA社*2と組み始めて、しださんとめもりーさんとやりとりをしているらしい。

けんご:自分が入った時、2021年の10月にはもう始まっていましたもんね。

抹茶氏:2021年の12月に、ドメインの話をしてた。Slackによれば。

Zoomのホワイトボード機能で作るのを断念した年表

振り返ってみて、オルテガに関わっていた中で印象深い時期は?

ゆーき:入社してからすぐPdM業が始まったわけですが、けんごさんにとって印象深い時期ってどこですかね。

けんご:そもそも、自分が入社して議論に参加しても、マイページの何について話し合っているのか分からない状態だったんですよね。自分の役割もそうですが、だれが何をするのか、マイページに何を期待すべきなのかが整理されていなかった状態でした。

とにかく、このまま行くとだらだらプロジェクトが進行してしまいそうなので、スコープ切りから始めて、フェーズを定義して。

この時期は本当に、入ってすぐだったので現場での業務のような「トイサブ!」オペレーションがつかめず、キャッチアップが想像以上に大変だったなと思います。何より、方向性を決める時や検討するときに、自信を持つまでが大変でした。

全業務体験をしたからこそ言えるんですけど、入社前にはオペレーションの煩雑さがいまいちイメージ出来ていなかったので、「こんなオペレーションがあるんだ」という驚きに溢れていました。

きたい:けんごさんとかぶるんですけれど、僕が入社した時(2021年10月)、そもそもエンジニア側にもどういうプロダクトになるかが見えていなかった印象があります。

なので、まずはバックエンド側でも設計指針などを議論し、自分の中では年末ぐらいに設計のイメージが明確になってきましたね。議論しながら始められたこともあって、プロダクトリリース自体無茶な感じではなく、みんなでやればなんとかなりそうな雰囲気でした。悲観的な始まりでもなかったです。

抹茶氏オルテガという新規プロダクトでは、技術構成も進め方も新しくきれいにできる、というのがはじまり時点での印象深い感覚ですかね。 DeveloperExperienceを良くしたいなという思いもあって、Componentから始めて、Schema,View,Logicと開発の進行を分けていきました。

フロントエンドエンジニアの皆のスキルもあげつつ、誰かに聞かないと指針が分からない、というような属人性を排除することができていってよかったかな。

Issueとのトランスファーが相互にできる「GitHub Discussions」も活用していけた、というのも新規プロダクト「オルテガ」で試せたことですね。

2022年4月に迎えたリリースに関して振り返り

ゆーき:新規プロダクトならではの進め方や、そもそものプロダクト開発進行について印象深く思われていたのがわかりました。ところで、先日迎えたばかりのマイページリリース、どうでしたか? どうでしたか、というのもかなり抽象的な質問ですが。

トイサブ!といえば箱!のログイン後画面

けんご:段階的にリリースをしていて、ユーザーのリアクションを定量的に確認しています。マイページ登録率や、お知らせの開封率等、比較的ポジティブな滑り出しですね。トイサブ!ユーザーの寛容度が高いと改めて感じました。

マイページ登録率が上がらない可能性も考えて、登録推進施策もいくつか検討していたのですが、現時点ではそれも投下しなくていいくらい。トイサブ!おもちゃを交換するタイミングでマイページ登録を促せているという、時機の効果もあると思っています。

登録時の手順についても、重大なトラブルは発生しておらず、良好な状態ですね。

ゆーき:トラーナのオペレーションチーム、特に顧客対応をダイレクトに扱うカスタマーサポートグループへの説明は苦労しましたか?

けんご:オペレーションチームにはマイページの説明会を行いました。事前にFAQを用意し、リリース前にカスタマーサポートグループへインストラクションも行いました。段階的なリリースと共に起こるであろう事前に想定できなかったお問い合わせについては、チームで学習していきましょう、と。

カスタマーサポートグループからの反応としては、むしろ、月に数百件ある住所変更のような手動オペが減ることもあり、好意的に受け入れてもらえました。

抹茶氏:フロントでも、リリースでのバグ報告はありませんでしたね。バグは出るものだと想定して、備えはおこなっていましたが、結果的に出なかったのは喜ばしい。

バグが出ていない原因として、一番大きいのはQAを企画部でやってくれたので、不具合を事前に潰すことができたという点があるでしょうね。

さらに、クリーンなコンポーネントの責務というものを考えてディレクトリを設計したり、ビューとロジックがいりまじってしまわないよう注意を払ったり。カバレッジを計測して高い水準を維持しつつ、高速化への取り組みを推進できていたんじゃないでしょうか。

ゆーき:それがもしや「治安維持」ですか?

抹茶氏:そうです。治安維持をオルテガ開発の当初から心がけていました。

GitHub Actions を用いたあらゆることの自動化をオルテガでは積極的に導入ています。例えば、Linter や test,deploy といった基本的なことから、branch の merge や CHANGELOG 生成、ラベル付け、Project Board の status 変更等を自動化しています。

きたいオルテガで導入しているLinterは多いと思う。前職の倍くらいあるんじゃないかな。

抹茶氏:人間は間違いを犯しがちなので、いない方がいいんです。

あくまで和やかな雰囲気

きたい:リリース後はプラットフォーム開発チームに異動しましたが、リリース直後もめちゃくちゃやばいエラーは出ていないですね。重大なバグのような。

ゆーき:オルテガのフロントエンド開発は抹茶氏がリードしていたと聞いたのですが、バックエンドは誰がやっていたんです?

きたい:……。

同じポーズをしていますね

けんご:きたいさんシャイだから言わなそうなんですけど、リリース前は全体的な設計とか結構リードしていましたよ。きたいさん。

きたい:さっきも出た話なんですが、みんなプロダクトのゴールがふわふわしていたので、実装もふわふわしてしまわないよう、設計を自分から提案していってますね。インフラとかには僕は積極的に意見していないと思うけれど……。

けんご:どういうアーキテクチャにするか、権限をどうするか、とか積極的に巻き込んで議論していってましたよ。きたいさん。

きたい:大変だったかどうかでいうと、やっぱり、リリース直前期より、はじまりの頃のほうが、プラットフォームでもあるMADRASの理解も必要で大変でしたね。構成がわからないと開発が困難になってしまうテーブルに関しての知識とか。

リリース直前は企画部メンバーも増えたし、問題なく進められていた印象があります。

とりくめて良かったな、と思うことなど

2022年1月に使用された、全社ミーティング資料より抜粋

けんごオルテガというプロダクト開発を進める上で、様々なプロセスの共通認識を揃える取り組みを実践できたことですね。

「ゴールが見えない」というと一言になってしまいますが、その裏側には、プロダクト全体のゴールが見えない、ローンチの先が見えない、等々いろんな「見えない」があると思うんです。今回は、テストシナリオを定義するというような、オルテガそもそもの仕様書を元に開発を進める取り組みができてよかったと自分は思っていますけど……エンジニアにとってはどうだったんだろう?(笑)

抹茶氏:けんごさんの言う通り、良かったと思いますね。

きたい:トラーナドメインを熟知していないながらに入社歴の浅いバックエンドエンジニアがオルテガを開発できたのは、企画部が主導して出してくれていた仕様書のおかげだと思いますよ。

けんご:まず型をいれて、型をもとに議論しながら進めていくことは、トラーナとしては初めてのことだったと思うので、エンジニアにとってもやりやすいのであれば、よかったです。(笑)

フロントエンドはとにかく、抹茶氏がクリーンに作っていっていることがわかったから、安心材料になりました。バックエンドもかなり苦労してたと思いますけどね。権限周りの設計とか。いずれにしても、苦労しているとか、やばそうとかネガティブな感覚を遠慮なくアラートとして出してくれていたので、議論できていたな、と開発部の様子を見ていて感じました。知らぬ間におかしいことにならないなという安心材料になりました。

いつでも、こうあるべき、がちゃんと話せていることで、技術力の高さが自分には伝わってきたんじゃないかなと。

ゆーき:バックエンドは同期入社も多かったと思うのですが、それで議論しやすいというようなことはありましたか?

きたい:人間として同じ頃の入社だから会話しやすい、とかそういう感じではないんですが、入社が同じくらいなので皆トラーナドメインに対してフレッシュで、だからこそものを言えた、という環境だったかもしれないです。ピヨピヨできた。

抹茶氏:フロントエンドは皆、得意・不得意の分野が違っていたんですよね。

小さくフェーズを区切り、まずフェーズ毎のテーマにおいて、それが得意な人にファーストペンギンをやってもらって、レビューして、次は自分で書いて、というのをとにかく繰り返していました。小さなフェーズが終わるたびにディスカッションをやって、リファクタして、というふうに。そういうのができるチームでよかったし、プロダクトもよく出来上がったと思ってます。

今後のトラーナでの野望や展望、ありますか?

抹茶氏:意思決定では何よりデファクトを意識する。デファクトを変更するときはそれなりの理由を用意してから変更していく。「オレオレ」的な進め方を避けることは続けていきたいです。

「オレオレ」という誰かの基準や技術構成でやっていってしまうと、属人性が強く出ていっちゃいますよね。属人性を廃していかないと、会社の開発チームとしてはよくないんじゃないかなと。新しく入った人にもすっと理解できるような意思決定を進めていきたいです。

あと、候補者にトラーナをおすすめする記事を書くのであれば、会社で使っている技術以外の話題が多いというのはいい環境だから維持したいかな。

けんご:(笑) 自分は、「ギークな人が多い」とよく説明してますよ。

ギークな人の昼下がりの会話

抹茶氏:(笑) 技術に好奇心を抱く人が多いですね。技術好きが多い環境で過ごしやすい人にとっては楽しいと思いますよ。

きたい:エンジニアがもっと増えたときには、ビジネス・オペレーションサイドとコミュニケーションを取れるような体制を取りたいですね。

今は、「何に困っているのか」ということを聞いているけれど、そもそも「困っていること」はエンジニアにも共有済みの状況になっているといいなと。エンジニアとして、課題解決のための議論の時間がしっかりとれるような体制が出来たらいいなという思いがあります。

抹茶氏:そもそも、エンジニアって言っているのは、ただコーディングしていくだけじゃなくて、ちゃんと「エンジニアリング」していく役割というのがあるからね。課題解決していける組織がいいですね。

きたい:そうです。前職ではビズオペの人と話す時間があって、悩みが共有されてたんですよね。エンジニア目線での解決方法を提案できる環境が自分にとってはよかったので、トラーナでも試したい、という野望というか。(笑)

けんご:きたいさんが言ってくれているようなことはいいですよね。新しいチーム分けでエンジニアが直接ヒアリングの時間をとることも進めていきたい。

やっぱりプロダクト企画もプロダクト開発も「ものをつくる」ことなんですよね。この、「ものをつくる」というときに、エンジニアが作ってて気持ちよくなるくらい尖ったものをやって欲しいです。

オルテガだと治安の維持が徹底的にやられているじゃないですか。バックエンドも納得行かなかったら一回壊してやり直していく。不要な妥協をして仕方なく開発するんじゃなくて、エンジニアのやりたいことをできる進め方を模索していきたい。

尖ったプロダクトかつオペレーションも効率化されているようなものがPdMとして理想ですかね。一個プロジェクトが終わったときに、お互いめちゃくちゃ気持ちよくなる、その両立を企画部で目指していきたいです。野望としては。(笑)

お互いめちゃくちゃ気持ちよくなるイメージ

トイサブ!マイページは、段階的にユーザー様へご案内を進めている状況です。今後、トイサブ!マイページがユーザー様にとって幸せな親子時間を増やすサポーターとなれるよう、オルテガプロジェクトは進行中。野望を胸に燃やしつつ、クールにプロダクトに向き合うトラーナ開発部に関心のある方はぜひ、トラーナキャリアサイトもご覧くださいね。

*1:社内ドキュメントとして利用している

*2:マイページデザインを依頼した会社

PHPer チャレンジトークンの答え合わせ

みなさま、こんにちは!CTO のめもりーです。 本年開催された PHPer チャレンジトークンの答え合わせ、解き方を解説します。

toranabox.com

(1) の答え合わせ

出題クイズ

<?php

$bytes = [0x61, 0x4c, 0x45, 0x45, 0x46, 0x79, 0x61, 0x79, 0x4c, 0x5b, 0x62, 0x48, 0x40, 0x4e, 0x40, 0x7e, 0x4c, 0x68, 0x5b, 0x4c, 0x7d, 0x46, 0x5b, 0x48, 0x47, 0x48];

$string = '';

for ($i = 0; $i < count($bytes); $i++) {
    $string .= chr($bytes[$i] ^ <1>);
}

if (md5($string) !== 'a8f101dec277521c969386effb2c8397') {
    echo "ハズレです!";
    return;
}

echo "#{$string}";
  • <1> … ここに入る 0x00 〜 0xff までを探してください

答え

  • <1> … 0x29 (PHP エンジニア界隈といえば肉ですよね。)
  • トークンの答えは「#HelloPHPerKaigiWeAreTorana

解き方

0x00 から 0xff までと定められるので、計算量 O(n) (n=255) からもブルートフォースで求められます。 以下のようにコードを書き直すとよいでしょう。

<?php

$bytes = [0x61, 0x4c, 0x45, 0x45, 0x46, 0x79, 0x61, 0x79, 0x4c, 0x5b, 0x62, 0x48, 0x40, 0x4e, 0x40, 0x7e, 0x4c, 0x68, 0x5b, 0x4c, 0x7d, 0x46, 0x5b, 0x48, 0x47, 0x48];

for ($k = 0x00; $k <= 0xff; $k++) {
    $string = '';

    for ($i = 0; $i < count($bytes); $i++) {
        $string .= chr($bytes[$i] ^ $k);
    }

    if (md5($string) === 'a8f101dec277521c969386effb2c8397') {
        echo "#{$string}";
        return;
    }
}

(2) の答え合わせ

出題クイズ

<?php

$string = '<1>';

$sum = array_sum(
    array_map(
        static fn (string $char) => ord($char),
        <2>($string)
    )
);

$additional = implode(
    array_map(
        static fn (string $numberValue) => chr(0x63 + ((int) $numberValue)),
        <2>((string) $sum)
    )
);

$string = "{$string}-{$additional}";

if (md5($string) !== '2fa885e32bc24b26f9dff3a47efb0d08') {
    echo "ハズレです!";
    return;
}

echo "#{$string}";
  • <1> … シャープを除いた (1) のPHPer チャレンジトーク
  • <2> … 適切な関数名を埋めてください

答え

  • <1> … HelloPHPerKaigiWeAreTorana
  • <2> … str_split
  • トークンの答えは「#HelloPHPerKaigiWeAreTorana-ehdf

解き方

文字列を array_map にかけようとしている点から、文字列を分割する関数であることの予測が付くでしょう。 そうすると、自然に str_splitpreg_split などがパッと思い浮かぶかなと思いますが、第 1 引数のみで対応していることから str_split であることがわかります。

(3) の答え合わせ

出題クイズ

<?php

$string = '<1>-<2>';

[$part1, $part2, $part3] = <3>('-', $string);

$calculator = fn (string $stringValue) => array_sum(
    array_reverse(
        array_map(
            fn (string $numberValue) => crc32($numberValue),
            str_split($stringValue)
        )
    )
);

$string = substr(md5($calculator($part1) + $calculator($part2) + $calculator($part3)), 0, 10);

if (md5($string) !== '8037d28b5a754eeacd1ee90fb1246610') {
    echo "ハズレです!";
    return;
}

echo "#{$string}";
  • <1> … シャープを除いた (2) の PHPer チャレンジトーク
  • <2> … 弊社 CTO が登壇するプロポーザルに出現する PHP 以外の小文字の英字 3 文字
  • <3> … 適切な関数名を埋めてください

答え

  • <1> … HelloPHPerKaigiWeAreTorana-ehdf
  • <2> … nfc
  • <3> … explode
  • トークンの答えは「#11e7eb18b2

解き方

HelloPHPerKaigiWeAreTorana-ehdf-nfc としたときにこれらを [$part1, $part2, $part3] のように配列から取り出しているという点から、指定した文字列で分割されているように予測できます。したがって explode であることがわかります。

(4) の答え合わせ

出題クイズ

<?php

$string = '<1>';

$string = substr(md5(str_replace(<3>('a', 'f'), '', $string)), 0, 10);

if (md5($string) !== 'b310309130966447075369fb9d56b437') {
    echo "ハズレです!";
    return;
}

echo "#{$string}";
  • <1> … シャープを除いた (3) の PHPer チャレンジトーク
  • <2> … 適切な関数名を埋めてください

答え

  • <1> … 11e7eb18b2
  • <2> … range
  • トークンの答えは「#6458e00ac0

解き方

str_replace の第 2 引数が空を指定していることから、特定の文字列を削除するようなコードであることが推測できます。 また a から f までの値を削除しているであろうと考えられ、str_replace の第 1 引数は配列を受け付けることから a から f までの配列を作成しているということも同様に推測できます。 したがって range を使って生成することが可能であることがわかります。

(5) の答え合わせ

出題クイズ

<?php

$string = '<1>';

$string .= implode(array_reverse(str_split($string))) . '<2>';

$map = [
    'e971773f' => 0x7d,
    'cbea68d7' => 0x46,
    '7f8abdb1' => 0x5b,
    'a42a75c2' => 0x48,
    '29d01a37' => 0x47,
    '4a2414ee' => 0x48,
    '1be678b5' => 0x61,
    'a347b1db' => 0x40,
    '00f56a27' => 0x5b,
    '35497491' => 0x40,
    '23a0dee4' => 0x47,
    'baa98f5e' => 0x4e,
    'cdaebfc8' => 0x08,
];

$split = str_split($string, 2);

$result = '';
for ($i = 0; $i < count($split); $i++) {
    $result .= chr($map[<3>(<4>, crc32($split[$i]))] ^ <5>);
}


if (md5($result) !== 'd5ff5cec18c54a4fdc90f8ce1462e6b4') {
    echo "ハズレです!";
    return;
}


echo "#{$result}";
  • <1> … シャープを除いた (4) の PHPer チャレンジトーク
  • <2> … 適切な 0-9a-f で作成された文字列を埋めてください (ヒント: $map で生成されている値の規則性)
  • <3> … 適切な関数名を埋めてください
  • <4> … 適切な関数のパラメータを埋めてください
  • <5> … ここに入る 0x00 〜 0xff までを探してください

答え

  • <1> … 6458e00ac0
  • <2> … f1f2f3 または c0f2f3
  • <3> … sprintf
  • <4> … %08x
  • <5> … 0x29
  • トークンの答えは「#ToranaHiring!

解き方

<2> で苦戦している方が結構多かったのではないかなと思います。したがって、まず <2> の解答を飛ばし <3>, <4>, <5> から解いていきます。 $map を見るとインデックスが 8 番目のものが 00 となっていること、16 進数になっていっること、そして全体的に 8 桁になっていることに着目をします。 crc32 の出力結果は必ずしも、8 桁目に値があるとは限らない中で、先頭がゼロフィルされているということは <3><4> はゼロフィル 16 進数の変換をするための関数とパラメータであることが推測できます。したがって sprintf%08x であることがわかります。

次に <5> ですが、これは (1) で解いたものを応用し、かつ、$split = str_split($string, 2) のように分解されており、また count($split) 回繰り返すということから、以下のようにコードを書き換えられます。

<?php

$string = '6458e00ac0';

$string .= implode(array_reverse(str_split($string))) . '<2>';

$map = [
    'e971773f' => 0x7d,
    'cbea68d7' => 0x46,
    '7f8abdb1' => 0x5b,
    'a42a75c2' => 0x48,
    '29d01a37' => 0x47,
    '4a2414ee' => 0x48,
    '1be678b5' => 0x61,
    'a347b1db' => 0x40,
    '00f56a27' => 0x5b,
    '35497491' => 0x40,
    '23a0dee4' => 0x47,
    'baa98f5e' => 0x4e,
    'cdaebfc8' => 0x08,
];

$split = str_split($string, 2);

for ($k = 0x00; $k <= 0xff; $k++) {
    $result = '';
    for ($i = 0; $i < count($split); $i++) {
        $result .= chr($map[sprintf('%08x', crc32($split[$i]))] ^ $k);
    }

    if (md5($result) === 'd5ff5cec18c54a4fdc90f8ce1462e6b4') {
        echo "#{$result}";
        return;
    }
}

これだけだと <2> が解けていないので、<2> を解きます。$map[sprintf('%08x', crc32($split[$i]))] となっており、 for 内において $map に split の分割後の要素数の最大までをループしています。

仮に $map にダミーの値が入っていると仮定したとしても $map の要素数以下がトークンの長さの最大値が含まれている(0 < トークンの長さ <= $map の要素数)ということが推測できますね。

また、 $map の要素数は、13 であることがわかります。次に 6458e00ac0 とこれを逆にした 0ca00e8546 を合わせると 20 文字、 str_split($string, 2) で 2 文字ずつ分解していることから $map にダミーが含まれていることまで考慮すると 06 文字不足しているということがわかります。 したがって <2>0 <= 2n <= 6 文字だということから、 0x000xff の範囲のものが 0 <= 2n <= 6 個入ることも推測できます。

さらに 2 文字ずつ分解し $map[sprintf('%08x', crc32($split[$i]))] としている点から $map のキーは 0x000xff の範囲の分割された文字 2 つを crc32 しているということもわかります。そのため以下のように解くことができます。


さっそく以下のように $map から使われている値を探索します。

<?php

$map = [
    'e971773f' => 0x7d,
    'cbea68d7' => 0x46,
    '7f8abdb1' => 0x5b,
    'a42a75c2' => 0x48,
    '29d01a37' => 0x47,
    '4a2414ee' => 0x48,
    '1be678b5' => 0x61,
    'a347b1db' => 0x40,
    '00f56a27' => 0x5b,
    '35497491' => 0x40,
    '23a0dee4' => 0x47,
    'baa98f5e' => 0x4e,
    'cdaebfc8' => 0x08,
];


$targets = [];

for ($l = 0x00; $l <= 0xff; $l++) {
    for ($m = 0x00; $m <= 0xff; $m++) {
        $key = sprintf('%08x', crc32(chr($l) . chr($m)));
        if (!isset($map[$key])) {
            continue;
        }
        $targets[] = chr($l) . chr($m);
    }
}

print_r($targets);

以下のような値が表示されることがわかります。

Array
(
    [0] => 0a
    [1] => 0c
    [2] => 0e
    [3] => 46
    [4] => 58
    [5] => 64
    [6] => 85
    [7] => a0
    [8] => c0
    [9] => e0
    [10] => f1
    [11] => f2
    [12] => f3
)

つまり <2> に入る値はこれらのいずれかの組み合わせであるため、これをブルートフォースでトライしてみます。

<?php

$map = [
    'e971773f' => 0x7d,
    'cbea68d7' => 0x46,
    '7f8abdb1' => 0x5b,
    'a42a75c2' => 0x48,
    '29d01a37' => 0x47,
    '4a2414ee' => 0x48,
    '1be678b5' => 0x61,
    'a347b1db' => 0x40,
    '00f56a27' => 0x5b,
    '35497491' => 0x40,
    '23a0dee4' => 0x47,
    'baa98f5e' => 0x4e,
    'cdaebfc8' => 0x08,
];


$targets = [
    // 空文字列で $map にダミーが含まれていても問題ないようにする
    '',
];

for ($l = 0x00; $l <= 0xff; $l++) {
    for ($m = 0x00; $m <= 0xff; $m++) {
        $key = sprintf('%08x', crc32(chr($l) . chr($m)));
        if (!isset($map[$key])) {
            continue;
        }
        $targets[] = chr($l) . chr($m);
    }
}



foreach ($targets as $a) {
    foreach ($targets as $b) {
        foreach ($targets as $c) {
            $string = '6458e00ac0';

            $string .= implode(array_reverse(str_split($string))) . $a . $b . $c;

            $split = str_split($string, 2);

            for ($k = 0x00; $k <= 0xff; $k++) {
                $result = '';
                for ($i = 0; $i < count($split); $i++) {
                    $key = sprintf('%08x', crc32($split[$i]));
                    if (!isset($map[$key])) {
                        continue;
                    }
                    $result .= chr($map[$key] ^ $k);
                }

                if (md5($result) === 'd5ff5cec18c54a4fdc90f8ce1462e6b4') {
                    echo "#{$result}";
                    return;
                }
            }
        }
    }
}

これにより、出力結果が #ToranaHiring! であることがわかります。計算量は O(n^3)n=14です。(最大 2,744 回の探索) もしくは、愚直に 6 文字分全てのパターンを網羅するでも良いかなとは思いますが、出力にはおそらく時間がかかるであろうと思われます。 (計算量が純粋に O(n^6) であり n=256 なので 最大 281,474,976,710,656 回の探索が必要になります)

いかがでしたでしょうか?

md5トークンの結果を検証しているので、結果が正しいか検証しやすかったのではないかなと思います。

出題後に、デジタルサーカスさんの PHPer チャレンジトークンを見て、もうちょっと捻れば良かったなぁと、思ったのは内緒です。 また次の PHPerKaigi で同様の取り組みがありましたら、その時を是非お楽しみに!

PHPerKaigi 2022 にスポンサード &amp;&amp; 登壇をします &amp;&amp; PHPer チャレンジトークン

スポンサード && 登壇

みなさま、こんにちは!CTO のめもりーです。 本年開催の PHPerKaigi 2022 にスポンサード、また私が登壇いたします。

phperkaigi.jp

YAPC と同様に「PHP で NFC リーダーを実装する」で登壇させていただく予定ですが、YAPC とは異なり PHP ユーザー向けの内容に資料をアップデートしておりますので、ぜひご覧ください。

speakerdeck.com

また、事前収録したトーク映像は資料で書いていないようなこともお話しておりますので、お楽しみに!

PHPer チャレンジトーク

CTO からの挑戦と題し、PHPer チャレンジトークンの入手を楽しんでいただける PHP コードをご用意しました。

各コードの実行結果がトークンとなります。

(1) 以下の空白を埋めてください。

<?php

$bytes = [0x61, 0x4c, 0x45, 0x45, 0x46, 0x79, 0x61, 0x79, 0x4c, 0x5b, 0x62, 0x48, 0x40, 0x4e, 0x40, 0x7e, 0x4c, 0x68, 0x5b, 0x4c, 0x7d, 0x46, 0x5b, 0x48, 0x47, 0x48];

$string = '';

for ($i = 0; $i < count($bytes); $i++) {
    $string .= chr($bytes[$i] ^ <1>);
}

if (md5($string) !== 'a8f101dec277521c969386effb2c8397') {
    echo "ハズレです!";
    return;
}

echo "#{$string}";
  • <1> … ここに入る 0x00 〜 0xff までを探してください

(2) 以下の空白を埋めてください。

<?php

$string = '<1>';

$sum = array_sum(
    array_map(
        static fn (string $char) => ord($char),
        <2>($string)
    )
);

$additional = implode(
    array_map(
        static fn (string $numberValue) => chr(0x63 + ((int) $numberValue)),
        <2>((string) $sum)
    )
);

$string = "{$string}-{$additional}";

if (md5($string) !== '2fa885e32bc24b26f9dff3a47efb0d08') {
    echo "ハズレです!";
    return;
}

echo "#{$string}";
  • <1> … シャープを除いた (1) のPHPer チャレンジトーク
  • <2> … 適切な関数名を埋めてください

(3) 以下の空白を埋めてください。

<?php

$string = '<1>-<2>';

[$part1, $part2, $part3] = <3>('-', $string);

$calculator = fn (string $stringValue) => array_sum(
    array_reverse(
        array_map(
            fn (string $numberValue) => crc32($numberValue),
            str_split($stringValue)
        )
    )
);

$string = substr(md5($calculator($part1) + $calculator($part2) + $calculator($part3)), 0, 10);

if (md5($string) !== '8037d28b5a754eeacd1ee90fb1246610') {
    echo "ハズレです!";
    return;
}

echo "#{$string}";
  • <1> … シャープを除いた (2) の PHPer チャレンジトーク
  • <2> … 弊社 CTO が登壇するプロポーザルに出現する PHP 以外の小文字の英字 3 文字
  • <3> … 適切な関数名を埋めてください

(4) 以下の空白を埋めてください。

<?php

$string = '<1>';

$string = substr(md5(str_replace(<3>('a', 'f'), '', $string)), 0, 10);

if (md5($string) !== 'b310309130966447075369fb9d56b437') {
    echo "ハズレです!";
    return;
}

echo "#{$string}";
  • <1> … シャープを除いた (3) の PHPer チャレンジトーク
  • <2> … 適切な関数名を埋めてください

(5) 以下の空白を埋めてください。

<?php

$string = '<1>';

$string .= implode(array_reverse(str_split($string))) . '<2>';

$map = [
    'e971773f' => 0x7d,
    'cbea68d7' => 0x46,
    '7f8abdb1' => 0x5b,
    'a42a75c2' => 0x48,
    '29d01a37' => 0x47,
    '4a2414ee' => 0x48,
    '1be678b5' => 0x61,
    'a347b1db' => 0x40,
    '00f56a27' => 0x5b,
    '35497491' => 0x40,
    '23a0dee4' => 0x47,
    'baa98f5e' => 0x4e,
    'cdaebfc8' => 0x08,
];

$split = str_split($string, 2);

$result = '';
for ($i = 0; $i < count($split); $i++) {
    $result .= chr($map[<3>(<4>, crc32($split[$i]))] ^ <5>);
}


if (md5($result) !== 'd5ff5cec18c54a4fdc90f8ce1462e6b4') {
    echo "ハズレです!";
    return;
}


echo "#{$result}";
  • <1> … シャープを除いた (4) の PHPer チャレンジトーク
  • <2> … 適切な 0-9a-f で作成された文字列を埋めてください (ヒント: $map で生成されている値の規則性)
  • <3> … 適切な関数名を埋めてください
  • <4> … 適切な関数のパラメータを埋めてください
  • <5> … ここに入る 0x00 〜 0xff までを探してください

YAPC::Japan::Online 2022 にスポンサード &amp;&amp; 登壇をしました。

みなさん、こんにちは!CTO のめもりー (@m3m0r7) です。

本年開催された YAPC::Japan::Online 2022 にスポンサード && 登壇をしました。

yapcjapan.org

Yet Another Perl Conference ということで、Perl を主軸としたカンファレンスだったのですが全く Perl の話をしない 「PHP で NFC リーダーを実装する」で登壇をしました。

また、長く続いている YAPC というイベントに貢献したいという気持ちから、今回スポンサードもさせていただいております。

torana.co.jp

登壇に使った資料は以下です。

speakerdeck.com

実は、2022 年に開催される PHPerKaigi 2022 でも同じようなタイトルで登壇させていただく予定なのですが、今回のこの資料は Perl を普段使いしている方でも、NFC リーダーを作るという面白さが伝わりやすいように PHP を全面に出さないようにしました。 Perl でもできそうだということを念頭に伝えるようなトークができたかなと思います。 また、当日の Discord の裏トークでも、NFC ではなくもっと他のデバイスでもできないかという話から、バーコードリーダーの話であったり、よりニッチな会話を展開できました。


YAPC そのものにスピーカーとして登壇することは、私自身の目標の 1 つでもありました。

ただ、Perl そのものを触る機会というのが私自身のキャリアの中であまりなく、登壇の機会は巡ってこないだろうなと感じておりました。 実は YAPC 2020 でも「Perl 初心者の私が YAPC のために Perl で JVM を実装してみた」がプロポーザルで採択されていたものの、昨今の新型コロナウィルスの影響で、開催が見送りとなっていました。

本当はこのタイミングで Perl を使って登壇する予定だったのですが、 日が経っているということもあり JVM とはまた違うアプローチで行こうと思い、 YAPC 2022 では Perl とはまったく異なる言語である PHP でプロポーザルを出しました。 まさか、PHP ネタで採択されるとは思ってもみなかったので、採択のメールが届いて二度見しつつも「YAPC で登壇できるのか…」と、感慨深くなりました。 PerlJVM を実装してみるというトークも、もちろんしたかったのですが、私自身の得意領域のプログラミング言語で、かつ目標の 1 つであったイベントで登壇できたのは 1 つのステップを乗り越えたなという気持ちになります。

こうして、 YAPC にスピーカーとして登壇できたのも、盛り上げてくださったスタッフの皆様、オーディエンスの皆様のおかげです。 本当にありがとうございました!

プロダクト開発部で社内 LT を実施しました!

みなさん、こんにちは!CTO のめもりー (@m3m0r7) です。 2022 年 1 月より、CTO に就任しました。詳しくはこちらのプレスリリースをご覧ください。


はじめに

エンジニア組織がある程度成長したら、社内 LT はやりたいことの 1 つでした。それが、ようやく、そのタイミングが来まして 2 月 4 日に無事、弊社初のエンジニア向け社内 LT を実施しました。 社内 LT ができるくらいにはエンジニアの組織規模が大きくなったことにも、感慨深いなという気持ちがあります。

どうやったのか

オープンに実施するか、クローズドに実施するか悩んでいたのですが、まずは初開催ということもあり社内で完結する形としました。 Lightning Talk で 5 分という枠の中で、社内から計 4 名トークいただきました。

ドラはなかったので、レストランとかによくある呼び出しベルをドラの代替として使いました。

毎週金曜日に KPT の時間を設けているのですが、その時間を使って実施しました。

そして、LT の前に話す側も、聞く側も楽しくワイワイできるように、諸注意事項などを私の方から皆さんに共有をして、開始しました。

f:id:m3m0r7:20220209175641p:plain


トーク内容

ゆびき♥

f:id:m3m0r7:20220209172232p:plain

弊社フロントエンドテックリードの YubiKey がいかに素晴らしいかを説いたトークでした。 私自身も、YubiKey は使っていて、GitHub のコミットに Verified を付けられるのですが、YubiKey でこれができると少し笑顔になります。

TCP/IPをしゃべる

f:id:m3m0r7:20220209172726p:plain

弊社バックエンドテックリードは、Rust で TCP/IP を喋らせるということをトークしてくれました。 PHP 話者でもあり、Rust 話者でもあり Swift 話者でもあり、一体何者なんだ…。最近はキーボードと通信をして、遊んでおられるようです。

RDBを取り巻くアレコレ(AWSを中心に)

f:id:m3m0r7:20220209173134p:plain

弊社エンジニアリングマネージャーによる、RDB を取り巻く環境についてのトークでした。 Oracle や DynamoDB とか MySQL だとか PostgreSQL とかは割と聞く名前なのですが、IBM も作ってたとは。 (いや、でも SQL 文自体は IBM 発祥(要出典)だしそれはそうなのか)

MBBから理解する私のモバイル設定のお勧め

f:id:m3m0r7:20220209173641p:plain

弊社代表による、モバイルネットワーク周りのトーク。時々いろんなカンファレンスのスポンサートークで登場していますが、弊社代表もエンジニア出身です。 Web アプリケーションエンジニアをやっていると、モバイルネットワーク周りと絡むことはあまりないので、刺激的な内容です。 その昔は地下鉄に乗ると電波が通じないみたいなのが良くあったなぁと思い出しました。

トークを終えて

初開催にしては、概ね成功だったんじゃないかなと思っています。

f:id:m3m0r7:20220209174537p:plain

5 分きっちりで、やろうかなと思っていたのですが、初開催ということもあって、きっちりやるのは難しいなと主催側として反省がありました。 終了後、アンケートを取ったのですが、やはり社内ということもあって、時間をきっちり決めるよりかは、時間を選べるようにしたほうが良かったなと思います。 (登壇者 4 人のはずなのに解答者数が 1 人多いのはなんでだろう…)

f:id:m3m0r7:20220209174909p:plain

(今回登壇した人も含めて)登壇してみたいか、という質問に 7 割以上が登壇してみたいと書いてくれたのは、 すごく嬉しい気持ちになりました。もちろん強制ではないですし、オーディエンスとして参加してくださったことにも感謝です。


エンジニア絶賛募集中です!

直近までは、バックエンドエンジニアとフロントエンドエンジニアの募集だけだったのですが、多くのポジションをオープンしています。 ぜひご興味があるかたは私への DM でも、お気軽にカジュアル面談スタートからでも!

herp.careers

AuroraとOpenSearchとElastiCacheをGraviton2移行!...に失敗しました(切り戻し済み)

@watarukuraです。

弊社、AWSさんからスタートアップ支援としてアドバイスを受ける機会を定期的に作っていただいており、直近では主にコスト面やセキュリティ面について、アーキテクチャ図を見ながらご提案をいただいています。 使っていないときは止める、SavingsPlansやリザーブインスタンスで前払い、などのご提案とともに、Graviton2インスタンスへの切り替えも提案いただきました。

Graviton2移行、いつかやりたい!と思っていたのですが、折よくMySQL8.0互換のAurora MySQL v3が登場しまして。さらに、AWSさんからAuroraのパッチバージョンを上げるよう通知もとんできました。ついでだし、この機会にやってやろう!と年末からstg環境に適用して検証を開始しました。

事前検証

terraformでengine_version書き換えてapplyすればOKでしょ!と軽く考えておりましたが、さすがにメジャーバージョンアップなのでクラスタから作り直さないとダメでした...。 インスタンス->クラスタの順に削除して、再度terraform applyし、事前にmysqldumpで取得しておいたSQLを流し込んで完了です。 さらに、パラメータグループもv2とは別になるため、新規に作成する必要があります。当然ながらMySQL 8.0用の設定に変更しなければなりません。ただ、弊社はローカル開発用およびCI用のdocker環境でMySQL8.0を使用してきており、特段の不具合が発生していなかったため、v2時のパラメータグループのデフォルトからの変更内容そのまま引き継いでおります。

作業検証の結果、サービスダウンが避けられないことがわかり、業務部門と相談して移行日に決定しました。

さらに、どうせサービス停止するなら、と以下もまとめて実施することにしました。

いずれも、stg環境で作業内容を検証、移行手順書を作成します。 弊社、本番DBでDDLやUPDATE/INSERT/DELETEなどを実行する場合はSQLレビューというGitHub Issueを立てて承認を得るようにしていたのですが、手順書レビューというIssueテンプレートを作って同じ運用に乗せるようにしました。

f:id:watarukura:20220129082359p:plain
runbook review

移行当日

まずはOpenSearchから作業を開始します。 と言ってもterraform applyするだけでしょ、とのんきに構えていた@watarukuraを待ち受けていたのは、applyエラーでした。

│ Error: InvalidTypeException: Invalid instance type: m6g.large.elasticsearch
│ 
│   with module.prd.module.base.module.search.aws_elasticsearch_domain.search,
│   on modules/search/aws.tf line 10, in resource "aws_elasticsearch_domain" "search":
│   10: resource "aws_elasticsearch_domain" "search" {

OpenSearchを運用されてる方ならお気づきかもしれません。Elasticsearchバージョンを上げてからでないとインスタンスタイプは変更できないのでした...。ということで2回に分けて実行します。 OpenSearch、とても良いサービスなのですがBlue/Greenデプロイメントのたびに結構な待ち時間が発生します...。1時間の想定だった作業は2時間かかりました。

ElastiCacheは流石になにもないでしょ、と想定しておりましたが、terraform applyが成功したのにノードのインスタンスタイプが変わりません。 マネジメントコンソールから変更したのと同様に、「変更の即時反映」が必要でした...。

最後に本命のAuroraです。 本番からまずはデータをExportします。更新が入らないようにapiサーバもbatchサーバも停止済みです。 ところが、全くExportが終わる気配がありません。JSON型で変更履歴を保持しているテーブルのサイズが大きく、Export完了に40時間くらいかかりそうと判明。 まだ諦めるには早いので、スナップショットを指定してインスタンスを作成するようにterraformを修正します。 最新のスナップショットを指定して再度plan、通ったのでapply。よしよし、と25分ほど待ったところでエラーが出ました。

│ Error: error creating RDS Cluster (madras-db-prd) Instance: InvalidParameterCombination: You can't create a DB instance using the instance class db.r6g.large in the DB cluster madras-db-prd. For the cluster's initial instance, use an instance class other than the AWS Graviton2 instance classes.

??? しゃーねーなーとマネジメントコンソールから作成しようと試みますが、同じエラーメッセージが出ます。 Aurora MySQL v3のクラスタ自体は作成できているのですが、r6gインスタンスを追加しようとするとエラーになる様子。 公式ドキュメントを読んでも挙動がよくわからなかったので、AWSサポートに投げてみます。 エラーメッセージを読んで、クラスタの最初のインスタンスがGraviton2だとダメよ、って話なのかと思い、r5インスタンスクラスタを再作成したあとでインスタンスを削除してr6gインスタンスを追加してもやはりダメ。

はてさて...。 業務部門と約束したサービス停止時間はとうに過ぎており、何度か「もうちょっとかかります」とSlackで伝えています。 諦めて、今まで使っていたr5インスタンスを使うことにしました。 無事に起動を確認、止めていたapiサーバとbatchサーバを稼働させて、サービス再開を確認し、業務部門へ通知します。

その後

OpenSearch、ElastiCacheについては、特段の問題なく動作しています。コストも削減できました。 Auroraについては、なぜか一部のバッチ処理でINSERTの動作が遅くなり、処理時間が長くなるという影響が出たため調査中です。 個人的に、AuroraでCTEやウィンドウ関数が使えるようになったのでホクホクです。

AWSサポートからの回答では、AWS CLI の restore-db-cluster-from-snapshot および create-db-instance を試してほしいとのこと。ただ、これでは再度サービス停止してしまいます。 ↓こちらを読む限り、Graviton2インスタンスを後から追加してフェイルオーバーすればイケそう?1分位サービス断が発生しますが、近日中に試して見る予定です。

aws.amazon.com

まとめに代えて教訓

  • 一度にまとめて色々やっつけようとしないようにしましょう
  • サービス停止時間は事前に検証環境で十分検証の上、余裕を持って見積もりましょう

フラフープとボードゲームで、チームビルディングワークショップをやってみました!

f:id:toranacom:20220121122341p:plain

こんにちは!2021年10月からPdM(プロダクト企画部)としてトラーナにジョインした、けんご( @kengoro )です。 コロナが落ち着いてるうちに!ということでプロダクト開発部と一緒にアクティビティと座学、ゲームを織り交ぜたチームビルディングのワークショップを行いました。 学べて盛り上がるワークショップになったので、普段リモートだけどチームメンバーで集まって何かやりたい!とお考えの方に少しでも参考になれば幸いです。

エンジニアの働き方

エンジニアのみんなはリモートでの業務となっています。 ほとんどのメンバーがコロナ禍に入社したため、これまでもリアルなコミュニケーションを行う機会がほとんどありませんでした。それでも毎日Zoomで集まったり、Slackのハドルミーティングを使ったりしながら共通認識を持ってプロジェクトを進めています。ハドルミーティングはトラーナに入って初めて使いましたが、気楽にできてマジでオススメです!

自分たちはこのままで良いんだっけ?

今後もリモート中心であることは変わりないと思いますが、リアルに会わないと得られない情報や、発生しないコミュニケーションがあり、相互理解を進めるにはリアルで会うことも必要ではないだろうか?業務上で必要なコミュニケーションは取ってるけど、チームと単位でお互いを知るための活動ってできてないよね?ということで、コロナが落ち着いてる間にみんなでチームビルディングのワークショップをやろうという運びになりました。

ワークショップ当日

当日のアジェンダ

* ヘリウムリング(フラフープを使ったゲーム) ※ 当日まで内緒にしてました
* チームビルディング座学(タックマンモデルを学ぶ)
* ボードゲームを使ってコミュニケーション!

集合していきなり微妙な空気を感じました。みんなZoomやSlackだとあれだけコミュニケーションしているのに、集まったら会話があまりありません(笑)。リアルとオンラインの差がいきなり出ました。

ヘリウムリング

今回はヘリウムリングというフラフープを使ったゲームを行いました。 ルールはいたってシンプルです。

  • 1チーム6人以上
  • 人差し指の第一関節の上にフラフープを載せる(指の腹の上に載せない)
  • 目線くらいの高さから降ろしていきフラフープを地面におく
  • 誰かの指が少しでもフラフープから離れたら最初からやり直し

実はこれがなかなか難しく、チーム内でどうすればよいか議論しながら進めていきます。 最初は7分以上かかってもクリアできなかったのが、2度、3度と繰り返すうちに、最終的にはどのチームも20秒以内でクリアできるようになりました!

※途中でスマホアプリを使っての攻略などゲームをハックするアイディア出てきましたが、今回は禁止としました(笑)

f:id:toranacom:20220121122736p:plain
一番最初は下がるどころか上がります(笑)すぐに話し合いが始まります。

タックマンモデルを学ぶ

ヘリウムリングで起きていたことをベースにチームの発展段階として有名なタックマンモデルをみんなで学びました。 何かを伝えることはコミュニケーションの入り口に過ぎません。お互いの考えを伝え合い方向性を決めてそこに向かっていく合意を生むことがコミュニケーションの目的です。 これは少なくとも混乱を生みますが、この混乱期を乗り越えないと良いチームにはなれないという考え方です。(メンバーの変更や状況の変更でも混乱期は訪れる) いま自分たちのチームがどのような状況かを客観的に理解することで、チームが健全な状態か、これからどうするべきかを考えることができます。

f:id:toranacom:20220121122822p:plain
タックマンモデルは調べるとたくさん情報が出てきます。混乱期は大事なのです。

ボードゲームを使ってコミュニケーション!

最後は純粋にコミュニケーションを深めることを目的にチーム戦によるボードゲームを行いました。 トラーナのボードゲームマスターが、テレストレーション、クイズいいセン行きまSHOW、ワードウルフを選んでくれました。 その中でもお題を絵で書いて伝言ゲームする「テレストレーション」は、チームでコミュニケーションが自然と生まれる今回にピッタリのゲームでした!最後の感想戦では、相手が何を伝えたくてその絵を書いたのか、チームメンバーが自分の書いた絵を観てどう解釈をしたのかがわかり「なるほどねぇ!」が連発でした。

f:id:toranacom:20220121122851p:plain
言葉、絵、言葉、絵と伝言され、最後にお題を当てます。マジで盛り上がりました。

良いチームになるんだという意思が大事

お互いを認め合った上で、お互いをよく知り、よく議論し、良い解決方法を導き出す。それぞれ強みや弱みを知り補完しあえる。 そのために必要なのは、お互いの考えを伝え合い、方向性を決めてそこに向かっていくという合意を生むコミュニケーションです。 そんなチームづくりを改めて意識して、みんながパフォーマンスを最大限に発揮できるチームづくりを目指していきたいと思います!

トラーナではエンジニアを始め、バイヤー、法務担当など最高のチームを作るために色々な方を募集しています。

herp.careers

ご興味ある方はぜひご連絡ください。

トラーナでのPdM業務は始めたばかりですが、他の会社のPdMの方がどんな仕事をしているのかなど聞いてみたいので情報交換してもいいよ!という方がいらっしゃれば、ぜひ @kengoro までDMでご連絡いただければと思います。

最後までお読みいただきありがとうございます!