新ヘルプセンターシステム#3 バックエンドチームがアプリ開発に集中できた秘訣

サムネイル

はじめに

プラットフォーム事業本部 CSプラットフォームグループの佐々木です。
私はスキルアップおよび過去の別プロジェクトにおけるリプレイス経験を活かしたいと考え、本プロジェクトに社内公募で参加しました。

本記事では我々のチームがどのように横断組織をうまく活用しながら開発を進めていったのかを共有したいと思います。

開発初期の状況

本プロジェクトでは既存のPHP(Laravel)で運用していたバックエンドAPIを、Go言語でほぼフルスクラッチで再開発する必要がありました。
またオンプレミス上で稼働していたAPIおよびDBをクラウドに移管するため、APIの設計だけではなくインフラ周りも考慮する必要がありました。

開発当初は立ち上げ段階ということもあり、バックエンドAPI開発メンバーはたったの3人でした。
また、下記のように、リプレイスに必要な知識がメンバー全員に不足していました。

  • クラウド移行先のAWSの経験があるメンバーがいませんでした。
  • 開発に利用するGo言語のスキルも、本プロジェクトから初めて利用するメンバーや長くても1年程度のメンバーしかいませんでした。
  • 最低限のアプリケーションアーキテクチャの知識はあったものの、レビューできるほどの知識があるわけではありませんでした。

上記の状況から短期間でチーム内のみで品質を保ったAPIを開発することは困難と考え、MicroservicesArchitect(以下MSA)グループの支援を受けました。

MSAグループについて

DMM内のプラットフォーム開発において技術的支援を行なっているチームです。
社内で非常に高い技術力や思考力を持ったエンジニアが集まるスペシャリスト集団です。
気になる方は、下記の記事でMSAグループについて詳しく知ることができます。
https://inside.dmm.com/articles/microservices-architect-in-dmm-platform/

設計の進め方

プロジェクト発足当初からチームの方針として、設計時にPRD(プロダクト要求仕様書)と、それを元にphaseを分けてDesignDocを作成する流れになっています。
作成したDesignDocは、チーム内にとどまらずMSAのレビューを受けることで、できる限り考慮漏れなどを防いでいました。
BEチームでは、主に下記の内容で設計を行なっていました。

  • アプリケーションアーキテクチャ
  • データベース設計
  • リリースおよびテスト方針の策定
  • リプレイス前の古いデータベースから新しいデータベースへのデータ移行方法

基本的にはMTGを行いながらレビューを進めて、抽象的で曖昧な部分に対して都度コメントを受けていました。

社内にDesignDocのテンプレートは用意されているものの、不慣れな箇所で非常に多くの指摘を受けました。
特に、チーム内の用語を使ったり、設計の具体性が抜けていたりする部分が多くあり、レビュワーにうまく意図が伝えらないことに苦労しました。
当たり前のことですが、設計書は自分が見るメモではなく、周囲に仕様を共有するための非常に重要なものであると、あらためて認識しました。

ただ、DesignDocのレビューを通して、開発を進める前にアンチパターンや現実的ではないタスクの進め方などを洗い出せていたため、大きな手戻りもなく開発を進められた点はよかったです。

社内マイクロサービスプラットフォームの活用

設計段階でAPI開発をするにあたって、アプリケーションをどこでどのように動かすかを考える必要がありました。
しかし、クラウド基盤の選定だけでも「AWS」「GCP」「Azure」などの選択肢が多くあり、膨大な時間を費やすことになります。
そこでインフラ構築コストを低減させるために、社内マイクロサービスプラットフォームを利用することにしました。

マイクロサービスプラットフォームはMSAグループで構築しているkubernetesとその周辺のエコシステムを利用した社内アプリケーションプラットフォームです。
推奨されるテンプレート通りに環境を構築すれば、簡単にアプリケーションをデプロイして動かすことができます。

BEチームではAPIのコードを管理しているGithubのリポジトリ上で下記のようなGithubActionsのワークフローを準備しました。

name: Push Docker Image

on:
    push:
    branches:
        - main

env:
    IMAGE: "DockcerRegistoryにプッシュするDockerImageName"

jobs:
    build:
    runs-on: ubuntu-latest

    steps:
        - name: Checkout the repository
        uses: actions/checkout@v2

        - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v0
        with:
            project_id: "イメージプッシュ先のプロジェクトID"
            service_account_key: "イメージプッシュ先のサービスアカウントキー"
            export_default_credentials: true

        - name: Configure docker to use the gcloud cli
        run: gcloud auth configure-docker asia-docker.pkg.dev

        - name: Build a docker image
        run: docker build -t $IMAGE -f ./docker/app/deploy.dockerfile ./

        - name: Push the docker image
        run: docker push $IMAGE

mainブランチにPRがマージされるとワークフローが実行され、Docker ImageをビルドしDocker Registry(GAR)にpushしています。
push後はマイクロサービスに組み込まれているデプロイツール(ArgoCD)によって変更を検知して、kubernates環境上にアプリケーションがデプロイされる仕組みになってます。
もちろんデプロイ先のkubernatesの環境設定を行うためのマニフェストファイルなど、プロジェクトに合わせた細かい設定は必要になりますが、ドキュメントも手厚く整備されており環境構築にはそれほど時間はかかりませんでした。

この基盤を利用することでBEアプリ開発のインフラにかかる運用コストをほぼ0に、またAPI開発にも集中することができました。

レビューの遅延とその改善

プロジェクト中期になり設計も概ねFIXしてきて必要なAPIの量も多かったことから、開発スピード向上のため、主にコーディング要員としてGo言語の経験者メンバーを増やすことにしました。
一時期、開発メンバーを最大8人に増やした時期もありました。

当初はコーディングメンバーが増えれば開発効率も上がるに違いないと信じていましたが、現実はそう甘くありませんでした。

確かにメンバーが増えたことで作られるプルリクエストの数は多くなったものの、レビューが追いつかず最大20件のプルリクエストがたまってしまいました。

またメンバーが増えたことで、仕様に詳しいレビュワーがタスク管理や新規メンバーに一から仕様説明する必要があり、レビューに時間が割けなくなるという悪循環に陥っていました。

そこでMSAグループのレビューチームと協力してこの状況を改善することになりました。

まずはスプレットシートでプルリクエスト毎にどれくらいマージに時間がかかったか、なぜ時間がかかったかの理由を洗い出すようにしました。

これによってプルリクエストが遅れる原因の可視化ができるようになり、改善策が考えやすくなりました。
また、改善途中からはFindyでプルリクエストのサイクルタイム(PRがオープンしてからマージされるまでの時間)を可視化できるようになり、そちらを確認しながら問題点を共有して改善していきました。

エンジニア組織支援クラウド『Findy Team+』によるプルリクエストのサイクルタイム可視化イメージ1

その結果、200時間を超えることもあったサイクルタイムが半分以下になりました。

エンジニア組織支援クラウド『Findy Team+』によるプルリクエストのサイクルタイム可視化イメージ2

具体的にどういった分析をしてどのように改善していったのかはこちらが参考になります。
https://blog.findy-team.io/posts/interview_dmm_01/

以降、チーム内でもFindyを活用して遅れているプルリクエストを分析し、改善していく習慣ができました。
現在までチーム内でプルリクエストが長期間たまることはほぼなくなりました。

負荷試験

APIがリリースされる前に非機能要件の検証もする必要がありました。

ヘルプページ関連で利用するAPIにもそれなりのトラフィックがあります。ピーク時間帯だと秒間100リクエストを超えることも少なくありません。
過去の問い合わせも含めると600万件を超えるデータに対して検索をかける場合もあり、ユーザー影響がない範囲で負荷に耐え得るかを検証する必要がありました。

しかし、負荷試験を実施するとしても負荷試験ツールの選定や学習、負荷試験環境の構築などに工数がかかってしまいます。
そこで社内マイクロサービスに組み込まれている負荷試験環境を利用することにしました。

Locust + Boomerで負荷試験を行うのですが、以下のようなテンプレートがあらかじめ準備されており、試験に合わせてカスタマイズすることでフレームワーク特有の細かい設定を気にせずに負荷試験を実施できました。

// 試験実施時に並列で実行される関数を定義する。
func sampleAPIGetRequest(client *http.Client) taskFunc {
    return func(ctx context.Context, l *zap.Logger, b *boomer.Boomer) {
        req, err := http.NewRequestWithContext(
            ctx,
            http.MethodGet,
            // ローカルでサンプルを実行するために用意しているwebサーバーのURLです。
            // 実際の試験では、負荷試験計測対象のURLをセットしてください。
            "http://nginx:80",
            nil)
        if err != nil {
            errMsg := "リクエストの生成に失敗しました"
            l.Error(errMsg, zap.Error(err))
            // HTTPリクエストの成功/失敗を明示的に記録する必要がある。
            // HTTPリクエストに成功した場合、b.RecordSuccessを実行する。
            // HTTPリクエストに失敗した場合、b.RecordFailureを実行する。
            b.RecordFailure(sampleAPIGetRequestType, sampleAPIGetRequestName, 0, errMsg)
            return
        }

        ...

        b.RecordSuccess(sampleAPIGetRequestType, sampleAPIGetRequestName, duration, res.ContentLength)
        return
    }
}
func main() {
    // 試験実施時に並列で実行されるタスクを宣言する。
    sampleAPIGetRequestTask := &boomer.Task{
        // タスクの並列数の割合を指定する。
        // このサンプルコードでは、sampleAPIGetRequestTaskとsampleAPIPostRequestTaskが1:2の割合で実行される。
        Weight: 1,
        // タスクとして実行する関数を指定する。
        Fn: taskWithSpan(sampleAPIGetRequest(c), logger, b, "sampleAPIGetRequest"),
        // タスク名を指定する。
        Name: "Sample APIへのGETリクエスト",
    }

    sampleAPIPostRequestTask := &boomer.Task{
        Weight: 2,
        Fn:     taskWithSpan(sampleAPIPostRequest(c), logger, b, "sampleAPIPostRequest"),
        Name:   "Sample APIへのPOSTリクエスト",
    }

    // 試験を実行する。
    b.Run(sampleAPIGetRequestTask, sampleAPIPostRequestTask)
    lib.WaitAndQuit(b)
}

ファイルの準備ができたら、あとはGithubActionsのワークフローから負荷設定をしてテストを実施するだけです。

実行結果は以下のようにSlackに通知され、WEBUI上から簡単に確認できます。

負荷試験を実施したことで潜在的に重くなっている処理を発見できました。
負荷試験環境の準備も含めると1カ月以上かかるであろう工数を数日に短縮できました。

負荷試験環境について詳しく知りたい方はこちらの記事が参考になります。
https://inside.dmm.com/articles/dmm-go-4-load-testing/

モニタリング

テスト中や各種テストが完了しAPIをリリースした後のアプリケーションの監視が必要になると思います。

PF事業本部共通でモニタリングツールにDatadogを採用しています。
しかし、部全体としてどのようにDatadogを運用していくかの方針が決まっておらず、BEチームも活用しきれていませんでした。
そこでMSAグループが定めた運用方針を参考にしながらモニタリング環境を整備することにしました。

もしアプリケーションでエラーが発生した場合は、アラートを発火させてslackに通知が飛ぶようにしました。

もし通知が来たら即座にDataDog Logsを確認します。

ログだけでは原因がわからない場合はTraceからネットワーク経路上の他チームのAPIやDBで実行したクエリを1つのログから追うことができます。

モニタリングとは直接は関係ありませんが、エラー内容がユーザーに大きく影響する場合は、ポストモーテムを作り再発防止のための振り返りも実施しています。

まだまだSLOのバーンレートアラートの設定など改善の余地はありますが、最低限の障害発生時の運用はできていると思います。
リリース後に何度かBEアプリケーション起因の障害が発生してしまいましたが、即座に検知や対応をしてほぼその日のうちに復旧できています。
現在までサービスを止めるまでに至ってないのは、こうしたモニタリング環境を地道に整備し対策していたからだと思います。

まとめ

当初はリプレイス経験が浅いメンバーが多かったものの、MSAグループの協力によって設計から運用まで効率的に開発を進めることができました。
PRや設計書に対してレベルの高いフィードバックを受けたり、各種指針に従い開発環境を整備したりしていく中で、チームとしてスキルや知識を深めることができ成長できました。

本記事では紹介しなかったですが、QA部のテストよる品質の担保、セキュリティ部の脆弱性診断によるセキュリティリスクの把握、基盤開発チームによるAPI設計のレビューなど、MSAグループ以外にも多くのチームから支援を受けました。支援を通して高い技術力を持った方々と関われたことで、私たちもそこから多くの学びが得られ、本当に感謝しています。

このようにDMMではさまざまな領域のスペシャリストが揃っており、プロジェクトを通して関ったり学んだりすることができます。
この環境で共にプロジェクトを進めてくださる仲間を募集しています。少しでも興味がある方はぜひとも下記採用ページをご確認ください。

dmm-corp.com