Feature toggles を複数の環境へ即時反映する仕組みを開発しました

f:id:hamakou108:20210528145639p:plain

こんにちは。エンジニアの濱田( @hamakou108 )です。

数ヶ月前にM&Aクラウドの feature toggles の仕組みを刷新したので、今回はその内容について紹介したいと思います。

Feature toggles とは?

Feature toggles はアプリケーションのビルド時または実行中に機能の切り替えを行う仕組みです。 Feature toggles はいわゆる feature branches の戦略とは対照的に、メインのブランチに頻繁にコードを統合します。 こうすることで CI/CD による早期のフィードバックと開発中の個別機能へのアクセスを両立します。

詳細については以下のページが参考になるかと思います。

刷新前の feature toggles の運用方法と問題点

M&Aクラウドフレームワークとして Laravel と Nuxt.js を利用しており、それぞれが別の環境で稼働しています。 各環境用の環境変数はファイルで管理しており、そこに feature toggles の値を追加するような形で運用を行っていました。

しかしこの運用方法には幾つか問題がありました。

環境変数の更新からリリースまでのタイムラグ

各環境に環境変数を反映するために、わざわざファイルを更新して CircleCI のワークフローを実行する必要がありました。 この一連の作業に30分から1時間程度の時間が掛かり、リリースのタイミングのコントロールが難しい状況になっていました。 バグが混入していた場合の切り戻しでも同様の作業を行う必要があるため、 MTTR の悪化の要因の一つにもなっていました。

各環境の切り替えタイミングの差異

Laravel と Nuxt.js の動作環境が異なることから、デプロイフローもそれぞれ分かれています。 このためフロントエンドとバックエンドで feature toggle の切り替えタイミングに差異が生じてしまい、それに起因してエラーが発生することが何度かありました。

Feature toggles の機能の刷新

上述した問題点を解消するべく、以下の課題を克服する新たな feature toggles 機能を開発するミニプロジェクトが立ち上がりました。

  1. Feature toggles の切り替えタイミングをコントロール可能であること
  2. フロントエンドとバックエンドで同じタイミングで feature toggle を切り替えられること

ここからは設計上のポイントについて3つ紹介します。

コード中にマスタを定義し、 RDB で on/off を管理

環境変数による feature toggles の管理は廃止し、次のような設計を行いました。

まず RDB に feature toggles 用のテーブルを作成し、 toggle の on/off をレコードとして管理できるようにします。 レコードを追加・更新すれば toggle の on/off が即時に切り替わるようにする算段です。

もう一つの工夫として FeatureToggleName という Enum 型のクラスを用意し、プロパティとして各 toggle の名前を持たせています。

<?php

class FeatureToggleName extends BaseEnum
{
    // 新規作成する際に定数を追加
    const FOO = '2021_05_27_foo';
    const BAR = '2021_05_28_bar';
    const BAZ = '2021_05_29_baz';

    public function __construct($value)
    {
        parent::__construct($value);
        if (!$this->isValidValue()) {
            throw new InvalidArgumentException('引数の形式は YYYY_MM_DD_name でなければなりません');
        }
    }

    private function isValidValue(): bool
    {
        return (bool) preg_match('/\d{4}_\d{2}_\d{2}_.+$/', $this->rawValue());
    }
}

アプリケーション内では FeatureToggleName のプロパティを feature toggles のマスタとし、ここに定義されていない toggle は存在しないものとして扱うように設計しています。 こうすることで不要になったレコードがテーブル内に残存していても、プロパティを削除しておけば UI から不要な toggle を排除できます。

また新たに追加する toggle の名前が過去に使っていたフラグの名前とたまたま重複していた場合、残存していた過去のレコードによって意図せずにフラグが on になってしまう危険があります。 これを避けるため、(現実的には)重複しないようなフラグ名を設定することをコンストラクタで強制しています。

管理画面から GUI でフラグ切り替え

M&Aクラウド社内のメンバーがアクセスできる管理画面に feature toggles 管理用のページを追加し、フラグの一覧表示および更新ができるようにしました 1

管理画面のフラグ管理ページの UI
コード中で定義されている feature toggles と ON/OFF の状態が表示され、個別に編集できる

即時に反映されるのは勿論のこと、アプリケーション経由で更新することで監査ログも残るようになり、ガバナンスの面でも強固な仕組みとなりました。 監査ログの機能に関しては別の記事としてまとめられていますので、興味のある方はこちらもご覧ください。

tech.macloud.jp

フロントエンド向け API

フロントエンド向けの feature toggles 取得 API を作成しました。 Nuxt.js の Router の middleware として設定することで各ページを開いた際に API が呼び出され、 Vuex 経由で feature toggles が取得できるようになりました。

const nuxtConfig: Configuration = {
  // ...

  router: {
    middleware: 'fetch-feature-toggle'
  },

  // ...
}

Feature toggles の刷新後

上述した課題を克服し、先日リリースした「かんたんM&A診断」では新しい feature toggles の仕組みを使って即時リリースすることができました!

macloud.jp

その後、開発時に想定していませんでしたが、この仕組みにはデータとロジックを同時に移行する際にメリットが得られることに気づきました。 Feature toggles も RDB で管理されているため、同一トランザクション内で移行したいデータと feature toggles のレコードを更新することで、データ移行の間にユーザーの操作が発生してデータに不整合が発生するといったケースを防止できます 2

一方で新たな課題も浮かび上がってきました。 機能のリリース後に不要なコードを削除する際、 FeatureToggleName のプロパティを先に消してしまうとそのフラグが off として扱われるようになるため、リリースした機能が巻き戻ってしまいます。 そのため削除の際は

  1. フラグを使って条件分岐を行うコードを削除する
  2. FeatureToggleName のプロパティを削除する

という手順を踏まなくてはなりません。 この順序を気にしなくても良いように改善することが今後の課題です。

最後に

M&Aクラウドではデザイナーやエンジニアを募集しています! 是非お気軽にご連絡ください!

www.wantedly.com

www.wantedly.com

www.wantedly.com


  1. 開発チーム内では feature toggles のことを feature flags や機能フラグのように呼んでいます。

  2. トランザクション数や移行するデータの量が多い場合はロックの影響を無視できなくなるため、この仕組みが万能というわけではありません。

生産性を落とす原因を可視化するためのサイクルタイム計測と施策について

はじめに

こんにちは。エンジニアの鈴木(@yamotuki)です。 今回はエンジニアの生産性向上の施策として、Jiraを用いた計測手法と、やってみた施策について書こうと思います。

背景

ストーリーポイントの計測

”ストーリーポイントとは?”についての詳細は他の記事にお任せします。

弊社ではスプリントを採用しており、毎スプリントで消化しているストーリーポイントを計測しています。 エンジニアが特定の一つのタスクに詰まってしまって、なかなか終わらせることができなければストーリーポイントの合計は当然低くなってしまいます。 スプリントで消化したストーリーポイントが高いからといって、優先度の低かったり不要なタスクをやっていたら生産性というのは向上しないものですが、消化したストーリーポイントが低い場合には何らかの問題が隠れているものです。

ただ、スプリントが終わった時の消化ストーリーポイントの合計というのは、結果の値でしかなく、細かくどこでエンジニアが詰まったのか?というのは一見して分析することができません。

サイクルタイムの計測

そこで、”サイクルタイム”を計測することにしました。 サイクルタイムは Jira ですと管理チャートで確認することができます。

サイクルタイムとは何か、については以下にGoogle翻訳による翻訳を引用します。

サイクルタイムは、問題の作業に費やされた時間です。通常、問題の作業が開始されてから作業が完了するまでにかかる時間ですが、問題の作業に費やされたその他の時間も含まれます。たとえば、問題が再開され、作業が行われ、再度完了した場合、この余分な作業の時間がサイクルタイムに追加されます。

例えば、「仕様決めに時間がかかったのか?」「作業に時間がかかったのか?」「レビューで時間がかかったのか」などを細かく追っていくことができます。

施策を始める前

Github IssueからJiraに移行してから測定は勝手にされていたので、遡って各種数値を確認することができました。 測定と生産性向上施策を始める前の状態は以下の図の通りでした。 一つのタスクを着手から完了まで3日近くかかっています。赤い線が表示期間の中での平均で、青い線が前後のタスクとの移動平均です。

f:id:yamotuki:20210518131944p:plain
施策をやる前の状態

タスク完了まで時間がかかるのには以下のような問題点があると考えました。

  • チェック待ち状態が長くなる要因
    • プルリクを出した後にレビュアが二人チェック。議論が全てGithub上で行われるので、ラリーが多い
    • 仕様がふわっとした状態で実装を始めて、レビューの段階で「ここPdMと相談しておいた方がいいんじゃない?」みたいな話が頻繁に行われ、手戻りが多い
  • 作業中状態が長くなる要因
    • 一人が複数タスクを際限なく取ることができるので、複数タスクを平行で進めることによって結果として全てのタスクに時間がかかる。3ポイントのタスクを二つ持つとどちらもなかなか終わらない。(補足: 現在は適当なタスクからの相対的な値としてストーリーポイントを見積もっていますが、過去には3ポイントは ”2日以内に終わる” という風に見積もっていました。弊社における3ポイントはこれくらいの大きさです。
    • 各エンジニアがハマっても、他の人が適切に助けることができない
    • 一つの大きなタスクを分割せずやろうとする。ざっくり8ポイントとかだと、いつ終わるかの見積もりも立てづらい

施策

DevOpsハンドブックには「それぞれのプロセスを短くしてフィードバックサイクルを早くすることで生産性が上がる」(と私は解釈)と書かれていました。そこで、できるだけそれぞれのプロセスを小さなバッチサイズにするような施策を試してみることにしました。

試した施策

以下のような施策を試してみました。特に改善効果があった施策は太文字にしておきます。

  • チェック待ちが長くなる原因の対処
    • レビューを一回やって修正項目が出たらレビュアとペアプロをやって一気にマージまで持っていく。文章でのラリーを避ける
    • Jiraのステータスで、"作業中"の他に”仕様受け入れ中”を作る。PMとして仕様が固まったら”仕様策定済み”になり、エンジニアが細かい仕様を詰めているステータスとして”仕様受け入れ中”を使用。エンジニアとして明確になったと思ったら"作業中"に移動する。 
    • 実装者とレビュアを三人チームとして固定。タスクを取る人はレビュアになる人にもできるだけ共有しておく。実装前に不明点をできるだけ潰して手戻りを防ぐ
  • 作業中状態が長くなる原因の対処
    • 並列で取れるタスクの量をストーリーポイント4ポイント以内にする。3ポイントのタスクを取ったら1ポイントの小粒なタスクしか取ってはいけない。
    • Slackの#developers_chattingチャンネルに今やっていること、困っていることを投稿する。他の人は余裕があれば支援(口出しともいう)をする
    • 詰まったら積極的にレビュアになる人に通話を繋いでペアプロ作業をする。
    • タスクを1~3ポイントのタスクに積極的に分割して、順次終わらせる

一番効果があった施策

一番効果があったのは、”タスクを一人で抱え込まず、チームで一気に終わらせる” ためのペアプロでした。ペアプロを通して、タスクは一人のものではなくてチームで解決するものなのだという意識が浸透したのも良かった点かなと個人的には考えています。
ペアプロの方法としては、Aroundというミーティングツールを使ってslack上でミーティングルームを立ち上げて、他の人が入るという形をよく使っています。

f:id:yamotuki:20210518173221p:plain
Aroundを使っている風景

他にもPhpStormでリアルタイム共同編集できる機能なども出てきているので、活用していきたいと思っています。

施策の結果

サイクルタイムを見ながら施策を投入していくことで、以下のように一つのタスクが終わるまでの平均時間が改善しました。タスクを小さく分割する施策も入れているので平均時間が減るのは当たり前と言えば当たり前ですが、一人でずっと抱えて巨大プルリクを作って大きく手戻り、というのは減ったと思います。

👇 1ヶ月以上かかるプロジェクトをみんなでやっていたパターン。コミュニケーションコストが低く、平均として低く抑えられている。

f:id:yamotuki:20210518163833p:plain
1ヶ月以上かかるプロジェクトをやっていた場合の管理チャート

👇 細かい改善タスクをバラバラやっていた期間のパターン。コミュニケーションコストが高いが、beforeの2.9日からafterとして2.2日くらいまでよくなっています。 期間の最後ではちょっと上がってしまっていますが、時間のかかるインフラタスクをやっていたり、他部署の人にデータ渡す系のタスクなど、完了まである程度時間がかかる原因が分かりやすいタスクが多かったです。

f:id:yamotuki:20210518164151p:plain
細かいタスクをやっていた時期

終わりに

もっと早く、もっと楽に、ユーザに価値を届けられる組織をこれからも目指していきます。 生産性の高いエンジニア組織作りに興味がある方などいらっしゃれば声をかけていただけるとありがたいです。

www.wantedly.com

コード自動生成とDIを活用したレバレッジ指向なメールプレビュー機能を実装しました

はじめに

こんにちは、エンジニアの津崎(@820zacky)です。 今回は、先日実装したLaravelでのメールのプレビュー表示、テスト送信機能についてご紹介します。

「メールプレビューはあると便利そうだけど、 メールごとにプレビューのための処理を追加する必要があるんじゃない? すでにたくさんの種類のメールがあるし、メールを追加するのも大変になるからやりたくないよ」 と思ったエンジニアのあなた。

まさにその通りです。私たちも同じ気持ちだったので、最小限の労力でメールプレビューができる仕組みを目指して機能を作りました。

私たちが開発・運用しているマッチングプラットフォームでは、ユーザへの通知やメールマガジンなど様々な場面でメールを送信しており、現在110種類ものメールが存在します。

これらのメールは、開発時にPdMへ内容の確認を行ったり、営業などの他部署から内容の問い合わせを受けることがあります。 プレビュー機能を実装する前は、メールの内容を確認するには、開発環境で実際にメール送信の条件を作り、メール送信を行うことで確認を行っていました。

開発環境ではMailtrapというメールのサンドボックスサービスを利用しており、開発環境から送信されたメールはMailtrap上のメールボックスにて表示の確認ができるようになっております。

Mailtrap - Email Testing Tool #1

f:id:zacky2:20210514180659p:plain
Mailtrap上の受信箱にて受信したメールを開いているスクリーンショット

Mailtrapにより、送信されたメールについては簡単に確認と共有ができるのですが、メールによってはバッチを実行しないと送信されなかったり、メール送信の条件が難しいものがあり、メール送信することが手間のかかる作業となっていました。 そこで、マッチングプラットフォームの管理画面に、メールの内容を簡単に確認するための機能としてメールプレビュー機能を実装しました。 また、メールクライアントによる表示の崩れがないか確認できるよう、メールの送信機能も併せて実装しています。 スマートフォンの実機でのメール表示や、Outlookなど癖の強いメールクライアントなどもあるため、凝ったデザインのメールを作る際は必須の機能です。

どんな機能か?

社内のメンバのみアクセスできる管理画面から利用できる機能です。 メールの一覧が並んでおり、「プレビュー」と「任意のメールアドレスに向けたメール送信」ができます。

f:id:zacky2:20210514184109p:plain
メールプレビュー機能のメール一覧画面

f:id:zacky2:20210514204030p:plain
メールのプレビュー画面

実装の詳細

実装の詳細についてはQiitaの記事をご覧ください。

qiita.com

メール一覧の取得

メールプレビュー機能を作るにあたって、プロダクトで使われているメールクラスの一覧が必要です。 しかし、新しいメールを追加するたびに、メールクラスのパスをリストに追加するのは億劫な作業であり、追加の漏れも懸念されます。 メールプレビューのためにメール実装の手間が増えるのは避けたいところです。 そこで、実行するだけでメールクラスの一覧を持つクラスを自動生成するコマンドを作りました。

f:id:zacky2:20210514201820p:plain
メールクラスリスト生成コマンドの実行例

メールクラスの一覧の取得する方法として、特定のディレクトリを探索しメールのクラスが記述されたphpファイルの抽出を行っています。 メールクラスはMailableを継承したクラスであるため、ReflectionClassを使い、再起的にIlluminate\Mail\Mailableを継承しているかチェックすることで判別できます。

ファイル探索と継承のチェック処理はページ表示のたびに行うと処理に時間がかかってしまうと想定できたため、コマンド化し手動実行するようにしました。

DIを活用したメールプレビューの仕組み

LaravelのControllerクラスは、メール(Mailable)のインスタンスを返却するとHTMLとしてレンダリングしてくれます。 そのため、Mailableクラスのインスタンスを作ることができれば簡単にプレビューを行うことができます。

しかし、メールのインスタンスは簡単に作ることはできません。 なぜなら、メールは送信先が可変であったり、本文が可変であるものがほとんどであり、可変な部分はインスタンス生成の引数として渡してあげる必要があるためです。

<?php
use Illuminate\Mail\Mailable;

class HogeMail extends Mailable
{
    private User $user;

    // 例として引数にUserを渡している。インスタンス化するにはUserのインスタンスが必要。
    public function __construct(User $user)
    {
        $this->user = $user;
    }
    
    public function build()
    {
        return $this
            ->subject( $user->getName() . "さんこんにちは!") // 対象のユーザによってタイトルを変える
            ->to($user->getEmail())  // 対象のユーザによって送信先を変える
            ->markdown('emails.hoge_mail');  // 対象のユーザによって本文を変える (Markdownの外部ファイルにて記述)
    }
}

実際のプロダクトでは100種類以上のメールが存在するため、それぞれのメールについて引数に渡す値を作り、Mailableクラスのインスタンスを生成するのは手間のかかる作業です。 メールを増やすたびに毎度メールプレビューのための処理を追加しなければならないのは、開発チームのバリュー「レバレッジ指向」と乖離します。

tech.macloud.jp

そこで、LaravelのDI機能を使って、メールに渡す引数についてDIするように実装を行いました。DIを活用すれば、同じ種類の引数を使っているメールであれば、処理を追加しなくてもメールのインスタンス化ができ、プレビューを表示することができます。

メールの引数のインスタンス化(ダミーの作成)については、すでにUnitテスト用にダミーのインスタンスを生成する実装が存在していたため簡単に作ることができました。

しかしながら、全てのメールについて引数を作ることは大変です。そこで、最初の実装ではいくつかのメールだけがプレビューできれば良く、段階的にプレビューできる対象を増やしていくことにしました。 メールで使われる引数はたくさんの種類がありますが、共通している引数も多いです。 よく使われるいくつかの引数についてDIを行うことで 30% 程度のメールについてプレビューできるようになりました。

f:id:zacky2:20210514192654p:plain
DIできてないメールはインスタンス化できないためプレビューできない

プレビュー未対応のメールについては、要望があった時に対応予定です。DIを追加するだけなので大きな手間はかかりません。

まとめ

今回、最小限の労力でメールのプレビューが実装できる仕組みを作りました。 一つはファイル探索とRefrectionを利用したメールクラスリストの自動生成、もう一つは、DIを利用したメールインスタンスの生成です。 「最小限の労力で大きな成果を出す」という私たちのバリューの一つである「レバレッジ指向」が発揮されたいい機能開発ができたと思います。

最後に

M&Aクラウドではエンジニアを絶賛募集中ですので、興味がありましたら是非以下からご応募ください!

www.wantedly.com

www.wantedly.com

最適なメンテナンス運用方法に辿り着くまでの試行錯誤

f:id:tsukahara1991:20210428181312j:plain こんにちは。エンジニアの塚原(@AkitoTsukahara)です

ECサイトや自社サービスを運用していると、サービスを一時停止しなければならないケースがあります。そのためにはサービス形態やインフラ構成に適したメンテナンス運用方法を事前に用意しておく必要があります。ほとんど利用することはありませんが、サービス信頼に関わる仕組みになります。
M&Aクラウドでは2通りの方法でメンテナンスページを表示する仕組みを準備しております。
今回の記事では運用方法とその運用に至った経緯をご紹介させていただきます。

メンテナンスに切り替える方法

切り替え手法 操作ページ メリット デメリット
1 管理ページからメンテナンスに切り替え 管理ページ(プラットフォーム独自のもの) 切り替えが簡単 バックエンドサーバーが停止したら利用できない
2 メンテナンス用のCloudFrontに切り替え AWSコンソール サーバー稼働状態に依存しない 切り替えに必要な操作が多い

1の「管理ページからメンテナンスに切り替え」は、フラグを切り替えるだけでメンテナンスページに切り替えられるようになっています。 プラットフォームの管理ページからフラグの切り替え機能を用意しているため、運用コストを低く抑えることができています。 通常の切り替えではこちらを採用しています。

もしもプラットフォームサーバーが停止してしまったら

1の方法は操作が簡単なのですが、万が一プラットフォームが稼働しているサーバーが停止してしまうと利用することができません。この不測の事態に備えて、稼働しているサーバよりも手前でメンテナンスに切り替える必要がありました。

f:id:tsukahara1991:20210428171103p:plain
インフラ構成

上の図はプラットフォームのインフラ構成になっています。この構成を考慮してCloudFrontを活用してメンテナンスに切り替える方法を模索しました。

CloudFrontを活用したメンテナンス切り替えを模索する

バックエンドサーバーを必要としないメンテナンスページの運用方法については、AWSサポートセンターにご質問させていただきました。M&Aクラウドのインフラ構成を考慮して、4つも運用方法をご提案いただきました。いつも丁寧な対応ありがとうございます!🙇‍♂️

4つの運用方法

  • CloudFrontのカスタムエラーレスポンス機能を利用する
  • メンテナンス用のCloudFrontディストリビューションへの切り替え
  • Lambda@Edge を使った静的ウェブサイトコンテンツの提供
  • CloudFront 以外のリソースへの切り替え

4つの運用方法と実際に試して得られた知見をメリットとデメリットに分けて簡単にまとめさせていただきます。

CloudFrontのカスタムエラーレスポンス機能を利用する

  • 運用方法
    CloudFrontのカスタムエラーレスポンス機能でHTTP Error Codeを受け取った際に、指定したページにレスポンスさせる

  • メリット

    • CloudFrontの更新だけで対応することができる
    • レスポンスを上書きすることができる(404エラーを503エラーにしたり)
  • デメリット
    • システムからのHTTPレスポンスを受け取らないとカスタムエラーレスポンスが働かないため、システムからのレスポンスが遅れた場合に何も表示されない時間が発生する
    • メンテナンス切り替えの度にカスタムエラーレスポンスを設定する必要がある

参考
カスタムエラーレスポンスの生成

メンテナンス用のCloudFrontディストリビューションへの切り替え

  • 運用方法
    メンテナンス用のCloudFrontディストリビューションを作成しておき、Route53のレコードを変更する

  • メリット

    • 稼働中のCloudFrontの設定を変更しないため、メンテナンス後の復旧は容易
  • デメリット
    • CNAMEの削除・登録(CloudFront)、レコード変更(Route53)と複数のアプリの設定変更が必要
    • CNAME切り替えが反映されるまでに少々時間がかかる可能性がある

Lambda@Edge を使った静的ウェブサイトコンテンツの提供

  • 運用方法
    Lambda@Edgeを適用させることで、メンテナンスページにリダイレクトさせる。

  • メリット

    • 任意のレスポンスにすることができる(メンテナンスページを表示して、503エラーを返す)
  • デメリット
    • CloudFrontのビヘイビア1つ1つに「Lambda Function Associations」を適用する必要がある(ビヘイビアが複数あるとその数だけ変更が大変に)
    • 新しくビヘイビアを追加するたびにLambda@Edgeの設定が必要になる

参考
チュートリアル: シンプルな Lambda@Edge 関数の作成 レスポンスの生成 - 例: 静的コンテンツの提供 (生成されたレスポンス)

「CloudFront 以外のリソースへの切り替え」

  • 運用方法
    切り替え先のリソースにCloudFront以外のサービス (例: ロードバランサー、EC2、ドメイン名と同じ名前を持つ S3バケット 等) を使用し、Route53のレコードを変更する

  • メリット

    • 稼働中のCloudFrontの設定を変更しないため、メンテナンス後の復旧は容易
  • デメリット

結果的に採用した運用方法

4つの運用方法を試した結果、「カスタムエラーレスポンス機能」と「メンテナンス用ディストリビューションへの切り替え」の良いとこ取りしたハイブリットな運用方法となりました。 メンテナンス時の処理フローは以下の図のようになっております。

f:id:tsukahara1991:20210430124618p:plain
メンテナンス表示までの処理フロー

ポイント

  • CloudFrontはどのようなURLを受けても、S3のindex.htmlをリクエストする
  • S3にはindex.htmlを用意しない(必ず404を返却する)
  • カスタムエラーレスポンスでレスポンスを503にする(SEOを考慮)

懸念していたディストリビューション切り替え時のダウンタイムですが、2の運用を実施するのはサーバー停止してしまっている時なので、多少のダウンタイムは考慮しないことになりました。

色々と試行錯誤しましたが、プラットフォームに適したメンテナンス運用方法をまとめることができました。 「もっと良い方法があるよ!」、「もっと詳しく知りたい!」という方はお気軽にご意見いただけると嬉しいです。
@AkitoTsukahara

最後に

M&Aクラウドではエンジニアを絶賛募集中ですので、興味がありましたら是非以下からご応募ください!

www.wantedly.com

www.wantedly.com

Nuxt.js化計画vol.6

f:id:kubotak:20200317182842j:plain
Nuxt.js化計画vol.6

第6弾です。

Nuxt.js化計画の概要についてはvol.1を参照ください。
また、過去のシリーズも通してリンクしているのでぜひ御覧ください。

tech.macloud.jp

今回はリプレースではなく、新規機能に伴う新しいページをNuxt.jsで構築しました。
かんたんM&A診断という機能です。

macloud.jp

7つの設問に答えることで売却したい会社・事業に対してマッチする買い手企業をレコメンドするという機能です。

今回はNuxt.js側では初の試みとしてABテストを行っています。
ABテストについては次の記事で紹介しています。

tech.macloud.jp

かんたんM&A診断のLPでデザインを異なるパターンでABテストを行います。
ちなみにこのLPはジョインして日の浅い塚原さんがコーディング・実装してくれました。即戦力!

f:id:kubotak:20210422110220p:plain
LPパターン

設問は以前売却・調達情報の案件登録ページでも実装したようにSPAとしてクライアント側でスムーズに遷移します。

f:id:kubotak:20210422174615g:plain
設問の遷移

f:id:kubotak:20210422174631g:plain
キーワードサジェスト

設問2で表示されているキーワード登録ではサジェスト機能が働いてるのがわかるかと思います。
データベースに登録されているキーワードから検索してGoogle検索窓のようにサジェストでキーワード候補を表示します。

サジェストの機能はvue-simple-suggestを利用しています。
シンプルの名の通り必要な機能(API)が一通り揃っていて使いやすく、ポリフィルも用意されているのでIE11+でも動作します。
弊社プラットフォームの利用者の性質上、まだまだIEは切り離せないのでありがたいです。

f:id:kubotak:20210422151204p:plain
結果のOGP

設問を完了すると結果ページが表示されます。
このページはSNSシェアされることを想定していますので、それぞれ結果に応じてOGPで利用される画像が異なります。

Nuxt.jsはユニバーサルモードで運用しているのでこのように動的なSSRが必要な機能も作れるので強力です。

最後に

M&Aクラウドではエンジニアを絶賛募集中ですので、興味がありましたら是非以下からご応募ください!
Nuxt.jsで開発したい方は是非どうぞ

www.wantedly.com

www.wantedly.com

Laravelで監査ログを実装する

f:id:kazuhei0108:20210418191338j:plain
ログハウス

こんにちは、かずへいです。

M&Aクラウドサービスで管理画面に監査ログの記録機能を追加しましたので、その実装方法について紹介致します。

監査ログとは

f:id:kazuhei0108:20210418191637j:plain
実際の監査ログ画面

監査ログは監査証跡として、システム監査人が追跡するために、操作内容やそれに伴うデータの移り変わりが時系列に沿って記録されているログのことです。

システム監査人とは、システムが正しく動いているか、問題が合っても追跡できるか等を確認する人で、公民情報システム監査人という資格もあるようです。

cisa.jp.net

Laravel Auditingの活用

Laravelでの監査ログの記録にはLaravel Auditingというライブラリが便利です。Githubのスター数も2,000件を超えており、実質的にデファクトみたいになっているようです。

github.com

ドキュメントもしっかりしています。

www.laravel-auditing.com

Laravel Auditingの導入

以下でLaravel Auditingをインストール出来ます。

composer require owen-it/laravel-auditing

その上で、設定ファイルをライブラリ側からコピーして自分のプロジェクトに持ってきます。

php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="config"
php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="migrations"

必要に応じて設定は変更してください。

あとはOwenIt\Auditing\Auditable.phpというトレイトが用意されているので、監査ログを入れたいEloquentを継承しているモデルに use Auditable すれば終わりです。簡単ですね。

Eloquentを使っていない部分の対応

弊社のシステムではEloquentを使わずにQuery Builderを使っている箇所もあり、そちらはTraitをuseするだけで実装することが出来ませんでした。

Laravel AuditableにはAuditorという監査ログを記録するクラスがあり、それを直接呼び出す事もできるので、クエリビルダーを継承し、insertやupdateが呼び出される度に一緒にAuditorも呼ばれるように実装を変更しました。

<?php
declare(strict_types=1);

use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Query\Processors\Processor;
use OwenIt\Auditing\Contracts\Auditor;

class QueryBuilderWithAudit extends Builder
{
    /**
     * @var Auditor
     */
    private Auditor $auditor;

    public function __construct(Auditor $auditor, ConnectionInterface $connection, Grammar $grammar = null, Processor $processor = null)
    {
        parent::__construct($connection, $grammar, $processor);
        $this->auditor = $auditor;
    }

    public function newQuery(): self
    {
        return new static($this->auditor, $this->connection, $this->grammar, $this->processor);
    }

    public function insert(array $values)
    {
        $result =  parent::insert($values);
        $this->auditor->execute(new CreatedAudible(null, $this->from, $values));

        return $result;
    }

    public function insertGetId(array $values, $sequence = null)
    {
        $result = parent::insertGetId($values, $sequence);
        $this->auditor->execute(new CreatedAudible($result, $this->from, $values));

        return $result;
    }

    …省略…

このQuery Builderを継承したクラスを使いさえすれば、後の実装はいつものとおりにするだけで、監査ログが記録されるようになります。

まとめ

LaravelではLaravel Auditingを活用することで簡単に監査ログを導入することが出来ます。 これによりシステムが正常に動いているか、問題が起きたときに、システム上で誰が何を変更していたのか等を追跡できるようになりました。

Laravel Auditingはドキュメントもしっかりしていて、簡単に導入できるのでぜひ使ってみて下さい。

おわりに

M&Aクラウドではデザイナーやエンジニアを募集中です! 少しでも興味を持っていただけましたら、是非お気軽にご連絡ください。

www.wantedly.com

www.wantedly.com

www.wantedly.com

「ナナメウエをいく」という開発バリューについて

f:id:hamakou108:20210405123141j:plain

こんにちは。エンジニアの濱田( @hamakou108 )です。

今回は開発チームのバリューの一つ「ナナメウエをいく」について紹介したいと思います。

開発チームのバリュー

M&Aクラウドの開発チームでは行動指針として以下の三つのバリューを制定しています。

レバレッジ指向」と「全員インフルエンサー」の二つについては過去の記事で紹介していますので、是非そちらも読んでいただければと思います。

tech.macloud.jp

tech.macloud.jp

今回は最後の一つ、「ナナメウエをいく」について紹介します。

「ナナメウエをいく」とは?

M&Aクラウドは「2nd Priority」というコーポレートバリューを掲げています。

corp.macloud.jp

2nd Priority

顧客第一になろう。
それ以外の都合は2番目に置いておいて、まずはなによりユーザーを大事にすること。ユーザーが求めることに素直に応えること。
長期の視点ではユーザーにもっとも価値を提供できる会社が、必ず流通革命を起こす。自分たちの作ったサービスで、ユーザーが喜ぶ瞬間は最高の時間だ。
とにかく迷ったら自分達のことより、ユーザーのことを考えよう。

営業や M&A アドバイザーのようにお客様と対面で直接関わるメンバーにとっては比較的意識しやすいバリューかもしれません。 一方で開発者の場合、ユーザーと直接関わる機会は少なく、ややもすると画面の向こう側のユーザーよりも目の前の機能開発やバグ修正に目を奪われてしまします。

開発チームとしてどうすれば 2nd Priority を実践できるのか話し合った末、生まれたバリューがこの「ナナメウエをいく」です。

ユーザーにとって使いやすいもの、必要なものを作ることでユーザーのニーズを満たすことはできるかもしれない。 しかし我々は卓越したアイデアを出し、それをプロダクトとして具現化することで、ユーザーの想像のナナメウエをいく価値を届けたい。 そのためには顧客ヒアリングやビジネス・テクノロジーの理解を積極的に進めていこう。

このような意味がバリューに込められています。

バリュー体現に向けた取り組み

ユーザーを理解する

先ほど「顧客ヒアリング」という言葉が出てきましたが、開発チームのメンバーは定期的に商談やヒアリングなどに同行させていただいています。

同行を通して実感したことの一つは、各売り手・買い手企業様の M&A に対するスタンスの幅広さです。 最初から能動的に M&A を進めたいと思って仲介業者を探したりプラットフォームを訪れる方もいれば、そもそも自分の会社を売却するという選択肢に気づいていない方もいらっしゃいます。

ユーザーの属性が画一的でないことを理解するにつれて、日々の開発で意識することも少しずつ変化してきたように思います。 自分が開発している機能はどんなペルソナに向けて提供するのか、どんな価値を感じてもらえれば成功なのか、というようにプロダクトの意義を意識する重要性を感じています。

ナナメウエの UX を提供する

M&Aクラウドで売り手として本登録する際には会社や M&A に関する情報を幾つか入力していただいています。 そのうち会社 URL や従業員規模といった項目はその場ですぐに入力できないケースが多いのか、離脱が発生しやすい箇所となっていました。

この問題への対策として ST&E というツールを導入しました。 このツールの導入により、会社名の最初の数文字を入力すると会社名の候補が一覧表示され、自分の会社を選択すると会社 URL や従業員規模などの情報が自動で補完されるようになりました。

会社名の一部を入力し、候補一覧が表示された際のスクリーンショット
会社名の一部を入力すると、候補一覧が表示される

この効果はデータとしても現れており、会社 URL の離脱率が導入前後で 57% も減少しました! 「ナナメウエ」の UX を提供し、データからも分かるように明確に価値を向上させることができたと考えています。

カスタマーサポートの業務効率化への貢献

先ほどの自動補完機能の話には続きがあります。

自動補完の機能のリリースを全社に向けて発表したところ、カスタマーサポート(以下 CS と書きます)チームから法人番号などのデータも一緒に取得してほしいと要望がありました。 CS チームでは売り手企業様のデータを調査する際に法人番号を検索しており、この作業のコスト削減を狙っての起案でした。 会社の検索履歴のデータは(フォーム中では使用されなかった情報も含め)管理ツールから CSV 形式でエクスポートすることができるため、このデータ取得の方法を CS チームに情報共有しています。

2nd Priority の指す「ユーザー」は社外の関係者のみではなく、社内のメンバーも含んでいます。 結果論ではありますが、社内メンバーに対しても「ナナメウエ」の価値を届けることができた好例だったと思います。

まとめ

「ナナメウエをいく」という開発バリューとそれを体現するための取り組みについて紹介しました。 バリューが制定されてから日が浅く、事例はまだ少ないのですが、今回紹介したような活動を継続してより多くの「ナナメウエ」なユーザー価値を提供していきたいと思います。

最後に

M&Aクラウドでは「ナナメウエをいく」のバリューを体現したいデザイナーやエンジニアを募集中です! 少しでも興味を持っていただけましたら、是非お気軽にご連絡ください。

www.wantedly.com

www.wantedly.com

www.wantedly.com

Repositoryパターンの設計方針を維持しながらN+1問題を起こさないようにする方法論について

こんにちは。エンジニアの鈴木(@yamotuki)です。
今回はRepositoryパターンの設計方針を維持しながらSQLのN+1問題を起こさないようにする方法論について書いていきます。

前提

レイヤードアーキテクチャについて

弊社では、DDDの考え方を取り入れたレイヤードアーキテクチャが使われています。

GET処理に関わるコードですと、具体的には以下のようなレイヤに別れています。特に今回のお話に関わる部分は太文字にしておきます。

  • Controlller
  • Scenario Service
  • Query Service
    • 画面やコマンド出力に必要な Data Transfer Object (以下DTOと呼ぶ)を返すのが責務
    • Domain Entity (以下Entityと呼ぶ)そのまま返すので事足りるのであれば強いて新しい DTO を作らず Entity をそのまま返すこともある
    • Repository や Domain Service を呼び出す
  • Domain Service
  • Domain Repository
    • InterfaceとしてDomain層に配置される
    • Entity もしくは Entity のリストだけを返しても良い制約がある。DTO はアプリケーション層のものなので DTO は返さない
    • Repository には参照と更新の責務があるが、この記事では参照だけに焦点を当てる
  • Infra Repository
    • 実装としてInfra層に配置される

例とする Entity の切り方

Entity の切り方として、例えば以下のようにしたとします(弊社事例を使って話そうかと思ったのですが、前提知識が多くなって分かりづらくなりそうだったので汎用的な例を用います)

  • User は Entity として存在する
  • User が持っている Watch(腕時計)も User とは独立した Entity として存在する
    • Watch の中に所有者情報として User Id を持っている

例とする DTO の構造

<?php
class UserWithWatch
{
    private User $user;
    private ?Watch $watch;

    public function __construct(User $user, ?Watch $watch)
    {
        $this->user = $user;
        $this->watch = $watch;
    }

    // 略: 必要な getter など
}

これまでの実装方針

Repository が Entity か Entity List しか返せない制約により、DTO として複数の Entity にまたがる情報をまとめる場合には、QueryService において複数の Repository を呼ばなければなりません。

仮に、管理ツールのユーザ一覧で「つけている腕時計の情報も一緒にみたいよ」というニーズがあったとします。愚直にやると以下のような実装になります。この実装手法は、過去に(現在も一部?)弊社で使われていました。

  • User Repository の getUserList() から User List を返す
    • SQL発行1回
  • Query Service において、N件のUserを含む User List をループで回し、その中で Watch Repository の get() を叩く
    • SQL発行N回

この実装の問題点は、get() の内部で1回SQL実行されるため、User 件数が多くなると、Watch Repository の get() 回数もそのまま増えることです。つまり"N+1 問題"を起こしています。サービスリリース当初やlocal開発環境では数十件しかなくてSQL発行数もたかだか100件程度だったものが、仮にユーザが10000人になりそのリストを表示させたいとなると、10000回以上のSQLクエリが発行されることになります。つまりページ表示がとても重くなります。

解決方法案は何があるか?

一般的に用いられる解決策は以下のようなものがあるかと思います(※私の観測範囲のみ)。

  • キャッシュをする
  • 一覧にページネーションを追加する
  • CQRS を使う

弊社ではどれも使っています。ただし、メリットデメリットあるため、必ずしも全てのケースでぴったりの解決策とはなりませんでした。

  • キャッシュをする
    • Watch Repository の get() でキャッシュを差し込み、N件取得を早くする方法
    • プロダクト立ち上げ時点ではこの手法が採用されていました
    • メリット
      • コードを大きく変更しなくて良い。ServiceProvider により差し込む Infra Repository を Cache を使う Repository に差し替えれば良いので導入が簡単
    • デメリット
      • SQLより早いかもしれないが、キャッシュにN回問い合わせされるのは変わらないので、件数が増えるとやっぱり重い
  • 一覧にページネーションを追加する
    • メリット
      • 一回で取得するユーザ数が少なければ N+1問題も大した問題となりません
      • 件数が多いとブラウザ側でDOM表示に時間がかかるのでページネーションは入れることで、ページは軽くなる
    • デメリット
      • 用途によりますが、管理ツールで一度にたくさん見たいとなると1ページあたりの件数を強く制限するのは社内UXが下がる。弊社の管理ツールのケースだと、件数はそこまで小さくしていないところがあります
  • CQRS を使う
    • Repository を通さず、アプリケーション層で DTO を返してくれる Interface を定義し、Infra 層で実装する
    • メリット
      • 部分的に導入できる
      • Infra層実装次第だが、join を使用してSQLを1回にすることができるので高速
    • デメリット
      • 過去作ってきた Repository の実装を使用できず、新たに Infra 層を記述しなければいけず、join してフィールド全て列挙するなどの手間が少なくない。また、infra から Domain や DTO にコンバートするロジックが散らばり、保守性が悪い
      • 今まで、ドメインロジックは Domain Service や Domain オブジェクトに書いてきたが、それらを経由しないことによりロジックの置き場所がない
    • 詳細は以下の記事が詳しいです

第4の選択肢としてのHashMapAttachment法

HashMapAttachment法は広く知られている単語ではなくて、私が社内で啓蒙のために名前が欲しかったので適当につけているものです。ググっても他の記事は出ません(実は正式名称ある、などあれば連絡ください)。
Repository から取得したものを Query Service で DTO に変換する時にN+1問題とO(N2)に陥らないようにするテクニック。

この方法の詳細は以下の通りです

  1. Repository から Entity List(①とする)を取得する
  2. DTOの中に含めたい Entity List(②とする) を別にリストとして取得する(ループを回してそれぞれ取得するとN+1)。取得したリストを①に含まれるIDをKeyとするHashMapに変換する。
  3. ①をループで回して、HashMapのキーにより②から該当のものを探してDTOを作る
    • HashMap の探索のオーダはO(1)
    • 補足: HashMapを用いず、①をループで回して②を全件走査するのだと、N+1問題にはならないが、O(N2)のコードになり、将来性能の問題が起こり得るので避けた方が良い

実装イメージを掴んでもらうため、実装の一部を紹介します。

<?php
class UserQueryService
{
    // プロパティなど省略

    public function getDtoList(): UserWithWatchList
    {
        // ①のリスト取得
        $userList = $this->repository->getList();

        return $this->attachWatch($userList);
    }

    private function attachWatch(UserList $userList): UserWithWatchList
    {
        // ②のリスト取得
        $watchList = $this->watchRepository->getListByUserIds($userList->toUserIdList());

        // ②のリストをHashMapに変換
        $watchHashMap = $watchList->toUserIdHashMap();

        $resultArray = array_map(function (User $user) use ($watchHashMap) {
            // HashMap からの取得で軽量
            $watch = $watchHashMap->get($user->getId());

            return new UserWithWatch(
                $user,
                is_null($watch) ? null : $watch
            );
        }, $userList->toArray());

        return new UserWithWatchList($resultArray);
    }
}

この方法を使用すると、Entity List などにその List に対してのロジックを持たせて使用することができます。
また、今までの Repositoryパターンに沿った実装方針の資産をフル活用することができます。

終わりに

サービス成長に伴い、今までの設計が限界を迎えることがよくあります。弊社では、設計をアップデートし、ユーザに価値を届けられるコードを一緒に作ってくれるエンジニアを募集しています。

www.wantedly.com

Laravel Meetup Tokyo Vol.13 Onlineで津崎がLT登壇しました

こんにちは、エンジニアの津崎( https://twitter.com/820zacky )です。

最近は燻製作りにハマっていて、よくベーコンを作っているのですが、 ベーコンの食べ過ぎでいよいよ自分が🐷ちゃんになりそうなフェーズに突入しております。

f:id:zacky2:20210308211814p:plain
ぶたちゃん

Laravel Meetup Tokyo Vol.13 Online

先日「Laravel Meetup Tokyo Vol.13 Online」というイベントに参加させていただきました。

「Laravel Meetup Tokyo」は、PHPフレームワークLaravelで開発を行うエンジニアを対象としたmeetupです。 今回およそ1年半ぶりの開催で、初のオンライン開催とのことです。

こちらのイベント詳細はこちら。 laravel-meetup-tokyo.connpass.com

今回は、以下のタイムテーブルで行われました。

f:id:zacky2:20210308212618p:plain

ytake ( https://twitter.com/ex_takezawa )さんのDDDのお話がとても白熱していたのと、 Hiroki Matsuo (https://twitter.com/handm871 )さんのLaravelあるあるネタがオーディエンスを沸かしていたが印象的でした。

Laravel Meetup Tokyoのおもひで

2019年の5月に行われた前回の「Laravel Meetup Tokyo」にも参加させていただきました。 当時は弊社に入社したばかりで、LaravelもPHPもど素人で、勉強会に参加するのもほとんど初めてという感じだったのを懐かしく思います。

スピーカーは、「吉田あひる」さん、「くまモンエンジニア」さん、「suthio」さん、「おかしょい」さん、「カンボ」さんで、 今でもLaravel界隈のイベントやTwitterでよく目にする有名な方達が登壇されていました。 今回はLT参加でしたが、同じように登壇できたことを嬉しく思います。

当時はLaravelに苦戦していた時期だったので、同じくLaravelを使っている方々とピザをつまみながら直接お話しできたのがとても楽しかったです。 コロナになってしまい、対面でお酒を飲みながら話すことができないのが、やはりちょっと寂しいですね。

発表内容

今回は「アクセス制御ライブラリ Casbinを使ってみた」というテーマでLTさせていただきました。 ご覧いただいた皆様ありがとうございました。

speakerdeck.com

最後に

エンジニア募集中です! 一緒に燻製やりましょう!

www.wantedly.com

www.wantedly.com

【AkitoTsukahara】中途入社しました。

f:id:tsukahara1991:20210226173143j:plain
 

みなさん、こんにちは!

今月よりM&Aクラウドにジョインしました塚原です。 ネット上では以下のアカウントで活動しております。よろしくお願いします!

AkitoTsukahara (akito) · GitHub
Akito.Tsukahara (@AkitoTsukahara) | Twitter

自分が入社するまでの経緯と、入社して感じたM&Aクラウドの魅力をご紹介させていただきます。

入社までの経緯

前職ではWebの受託開発の会社でWebエンジニアをしていました。
担当していたプロジェクトでは、設計・開発・保守、顧客折衝にまで及び、プロジェクトに関することは満遍なく携わってきました。
お仕事する上で、エンジニアとしてシステムを開発するだけではなく、クライアントの期待値を意識したコミュニケーションを心がけていました。

今回はCTOの荒井さんからお誘いいただき入社する運びとなりました。
お声がけいただいたきっかけは昨年のPHPerKaigi2020での登壇経験を評価いただいてのことでした。(外部に向けて発信するって大切ですね!)

入社して感じたM&Aクラウドの魅力

日々アップデートされるアーキテクチャとそれを支える環境

M&Aクラウドの魅力の1つは、入社を決めた理由にもなったM&Aクラウドプラットフォームのアーキテクチャです。前職の受託開発会社でもシステム設計・開発をしていましたが、スケジュールや予算の兼ね合いで、アーキテクチャを意識した開発や技術的負債を解消する機会作ることが難しく、歯痒い思いをしておりました。。。

一方で、M&AクラウドではDDD や CQRS などの考え方をシステムに取り入れて開発されており、ビジネスの複雑性と技術的な複雑性に応えられる設計になっています。また、現在進行中のプロジェクトでも実装方針の意見交換が活発に行われており、メンバー全員がシステムの設計・開発に高い関心を持って、開発を進めていると感じました。 システムの完成度の高さはもちろんのこと、アーキテクチャに高い関心を持つエンジニアメンバーがいることも素敵だと感じました。

エンジニアが主導する「リリース⇆計測&ヒアリング」のサイクルがある

開発チームでは毎日サービスのKPIを確認するMTGがあります。直近のリリースした機能がユーザに使われてるのか、さらに改善できるところは無いかと議論しています。
また、営業の方に同行してクライアントのアポイントメントに参加する機会があります。数値データだけでなくサービスを利用するクライアントの意見に触れる中で、サービス改善の気付きや学びを得る為の取り組みが行われています。
より良いサービスをリリースし続けていく仕組みが根付いていることもM&Aクラウドの魅力だと感じました。

これからは

M&Aは専門知識が多く、この会社の事業ドメインを理解するまでにやらなければいけないことが盛り沢山で大変ですが、新しい見識が得られる機会を楽しんでいこうと思っています。

開発チームの行動指針「全員インフルエンサー」を自分も体現できるように情報発信を増やしていきます! (自分も含めて、開発チームは全員PHPerKaigi2021に登壇しますので、ぜひご覧ください。)

引き続き、技術力をさらに磨き、事業ドメインの知識を身につけて、1日でも早くみなさまにM&Aクラウドのさらなる活躍、素敵なサービスをお届けできるように頑張って参ります!

最後に

M&Aクラウドでは、エンジニアを募集中です!!興味がありましたら、是非以下からご応募ください!

www.wantedly.com

www.wantedly.com