Gunosyデータ分析ブログ

Gunosyで働くデータエンジニアが知見を共有するブログです。

新プロダクトの記事配信ロジックを1から作った話(実装編)

はじめに

こんにちは、Gunosy Tech Lab - Media MLの suchida です。 2020年に入社して1年ちょっと経過しました。 在宅ワークは未だ継続中ですが、たまにWeWorkに出社して気分転換してます✨

本記事では新プロダクト「auサービスToday」の開発において、Media MLチームが関わったことについて紹介します。
また記事ロジックの詳細については、別のブログで今後掲載予定です。

f:id:playTag55:20210823234025p:plain
アプリイメージ

auサービスTodayってなに?

auサービスTodayとは、KDDI株式会社と共同で開発を進めているアプリです。 前身は「auサービスTOP」というアプリで、今回リニューアル開発に携わることになりました。 詳細については、以下の記事をご参考にしていただけると幸いです。

gunosy.co.jp

www.appbank.net

どんな開発に携わったの?

私の所属する Media MLでは、主に各プロダクトのニュース記事まわりのロジックの開発・分析を担当しています。 本プロジェクトでも、記事まわりのロジック開発を中心に、APIやインフラ整備にも携わりました。 開発期間は、2020/12月中旬〜2020/3月中旬 となっています。

バッチ

開発したバッチとしては、以下のようなものがあります。

  • 扇情的な記事を判定するバッチ
  • タブごとの記事の並び順を計算するバッチ
  • 関連記事を選出するバッチ
  • etc.

言語はすべてpythonです。 多くは、別プロダクトでも稼働しているロジックのため、粛々と移植を進めました。

特徴・工夫

1. モノレポ の採用

それぞれのバッチは、ひとつのレポジトリ内で実装するモノレポで開発しています。 レポジトリ構成は以下のようになっています。(一部省略)

.
├── README.md
├── batches            ・・・個々のバッチを配置
│   ├── batch_01
│   ├── ...
│   └── batch_XX
└── shared             ・・・共有ファイルを配置
    ├── build_docker.sh
    ├── docker-compose.yml
    └── setup.cfg

これにより、開発スタイルの統一や参考コードの検索・コピペなどが効率的になっていると感じます。

また、バッチごとにDockerfileやpoetryを管理しており、限りなく依存度の低い構成になっているので、それぞれが独立した状態での開発が可能です。 バッチの構成は以下のようになっています。(一部省略)

.
├── Dockerfile
├── Makefile
├── README.md
├── build_docker.sh -> ../../shared/build_docker.sh
├── docker-compose.yml -> ../../shared/docker-compose.yml
├── poetry.lock
├── pyproject.toml
├── setup.cfg -> ../../shared/setup.cfg
├── skaffold.yaml
├── deploy             ・・・EKSへのdeployまわりの定義フォルダ
├── main               ・・・バッチメイン処理フォルダ
└── tests              ・・・バッチのテストコードフォルダ

共有可能なファイルはsharedを参照しています。 各バッチで「実行コマンドが異なる」や「linterの設定を変えたい」などの要望は自由としており、Makefileで差分の吸収を行っています。Makefile神(笑)
また、EKSへのデプロイ定義やリソース調整も各バッチディレクトリ配下で定義しているので、バッチごとにリソース設定を細かく調整することもできます。

開発スタイルの統一には以下の記事も参考になりました。

data.gunosy.io

2. EKS + Helm + Skaffold の導入

auサービスTodayでは、APIやバッチ、管理画面など多くがEKS上で稼働しています。 また、パッケージマネージャーはHelmで統一されています。 同種のツールであるKustomizeと比較すると、変数のオーバーライドが楽な印象です(笑)。
加えて、自動ビルド・デプロイツールであるSkaffoldを利用することで、労せずにEKS上で実行可能です。 これにより、手元ではpyenvやdocker環境でデバッグしながら実験や開発しつつ、Skaffoldを用いてEKS上の開発環境でもサクッと動作確認ができるので、安全で効率的な開発につながっています。
デプロイ周りもMakefileで管理しているため、開発環境にデプロイする場合は、make skaffold-devでおしまいです。Makefile神(笑)

3. 統合データ基盤 Baikal の利用

Baikalは各プロダクトの統計情報を一元管理しているデータレイクです。 auサービスTodayでは、Baikalから流れてきたデータを利用する点が、他のプロダクトと大きく異なる点の1つです。 これにより、他プロダクトの統計情報を合わせて活用することができ、リリース初期のコールドスタート問題への対応が可能です。

tech.gunosy.io

特にauサービスTodayは、同じくKDDI株式会社と共同で開発しているアプリ「ニュースパス」とユーザー属性が近いことが想定されたため、ニュースパスの統計情報を統合してスコアリングに反映しています。 これにより、ユーザーの興味を引きつける記事の推薦につながっています。 なお、現在はauサービスToday単体で十分な統計情報が確保できているため、A/B テストを経てすでに廃止しています。

4. digdag workflow の活用

各バッチはdigdagでスケジューリング管理しています。 EKS上での実行に必要なマニフェストファイルは各バッチで生成し、CI/CDを通してS3に吐き出しています。 その都度全バッチの出力を行うとハードなので、変更のあったバッチだけ更新することでCI/CDを効率よくまわしています。(開発初期は前者の状態でしたw。)
スケジュールが実行されると、digdagはS3からマニフェストファイルを取得し、EKS上でバッチを実行します。結果はS3やDynamoDBなどに格納しており、その結果をロジックAPIで利用しています。

f:id:playTag55:20210824135336p:plain
アーキテクチャイメージ

大変だった点

1からの開発ということで、個人的に初見のサービスに慣れるまで苦労が多かったです。 初物づくしでした。

  • Helm
  • terraform
  • Makefile
  • Baikal
  • etc.

特に、Baikalのデータを取得するSQLを書く際には、他のプロダクトで利用しているテーブルとは少々勝手が異なっており、普段の感じでSQLを書いてしまい意図した結果が得られないことが当初よくありました。
単にバッチの移植といっても、一筋縄ではいかないなと感じました。

API

開発したAPIとしては、以下のようなものがあります。

  • 各タブの記事リストを返すAPI
  • 関連記事を返すAPI
  • 記事詳細を返すAPI
  • etc.

特徴・工夫

1. Goa v3 の導入

APIの多くはgolangで開発しており、フレームワークにはGoaを採用しています。 他のプロダクトでもすでに導入されている信頼と実績のGoaです。
Goaでは、以下のような design(サービスメソッドとその入力および出力データ構造)を定義してしまえば、goa gen hogehogeで面倒なエンドポイントまわりのコードを自動生成してくれます。 これにより、他のAPIから叩く際に必要なパラメータやレスポンスの形を認識しやすいなど開発の効率化にもつながっています。

var _ = Service("calc", func() {
    Description("The calc service performs operations on numbers")

    // Method はサービスメソッド(エンドポイント)を記述します
    Method("add", func() {
        // Payload はメソッドのペイロードを記述します
        // ここでは、ペイロードは2つのフィールドからなるオブジェクトです
        Payload(func() {
            // Field はフィールドインデックス、フィールド名、型、および説明が与えられたオブジェクトを記述します
            Field(1, "a", Int, "Left operand")
            Field(2, "b", Int, "Right operand")
            // Required は必須となるフィールドの名前を列挙します
            Required("a", "b")
        })

        // Result はメソッドの結果を記述します
        // ここでは、結果は単純に1つの整数値です
        Result(Int)

        〜以下省略〜
    })
})

ref. https://goa.design/ja/design/overview/

2. DDDライクな設計の採用

APIはドメイン駆動設計(DDD)を参考にした構成となっています。 DDD自体は、なるべく役割分担を行い、低依存に開発を進められるような思想となっており、導入することで開発コードの秩序が保たれることを期待しています。 package構成は以下のようになっています。(一部省略)

.
├── cmd                ・・・サーバー初期化・起動まわり
├── controller         ・・・サービスの実装
├── domain
│   ├── model          ・・・ロジックやそれに必要な構造体の定義
│   └── repository     ・・・メソッドのinterfaceの定義
├── infrastructure     ・・・インフラとのアクセスの実装
├── middleware         ・・・ミドルウェアの実装(ログまわりなど)
├── usecase            ・・・ユースケースの実装
├── design             ・・・Goa関連
├── gen                ・・・Goa関連
├── go.mod
└── go.sum

DDDでは、4つの層に役割を分担しており、ざっくり以下のように分担させています。

  • domain層
    • 役割 : アーキテクチャの中心となる層。ロジックや構造体の定義を行います。
    • 対応 : domain配下
  • infrastructure層
    • 役割 : インフラとのアクセスを提供する層。domainの知識を利用してインフラ(S3など)とデータのやりとりを行います。
    • 対応 : infrastructure配下
  • application層
    • 役割 : ユースケースを提供する層。domainの知識を利用して処理の流れを実装していきます。
    • 対応 : usecase配下
  • presentation層
    • 役割 : サービスのエンドポイントを提供する層。ユースケースを組み合わせてAPIの出入口を実装します。
    • 対応 : controller配下

DDDの概要把握には以下の記事も参考になりました。

little-hands.hatenablog.com

3. EKS + Helm + Skaffold の導入

バッチセクションで記載の内容と同様です。

4. サービスメッシュ Istio の導入

Istioはサービス間の通信を管理するサービスメッシュの一つです。 他のプロダクトでは、主にEnvoyを利用しています。 Istioは、Envoyの親玉のようなもので、各APIのEnvoyを一元管理しています。 Envoy単体の場合、各APIごとに通信制御の定義を行う必要があり、各APIで長い長い定義を書く必要があります。 一方で、Istioを導入することで、各APIではシンプルな定義を記述するだけで済み、開発に集中することができます。 加えて、サービス全体での統一感も増しています。

istio.io

大変だった点

他プロダクトとの差分として、以下の変更を行う点は苦労がありました。

  • Kustomize -> Helm
  • Envoy -> Istio

特に、チーム内に有識者がいませんでしたので、他部署の方とコミュニケーションを取りながら形にしていきました。

その他

今回取り上げられませんでしたが、その他に開発したシステムも以下に記載しておきます。

  • 記事クローラー
  • 記事カテゴリ分類システム
  • 記事画像切り抜きシステム
  • 記事プッシュシステム
  • ETL

おわりに

本記事では「auサービスToday」の開発において、Media MLチームが関わったことについて紹介しました。 プロダクトに1から触れる機会はとても希少なので、個人としても良い成長の機会となりましたペコ。 今回の経験を、他のプロダクトにも還元していければと思います。

弊社ではこれからも「auサービスToday」の開発・改善を進めて参りますので、皆様どうぞよろしくお願いいたします。