プレイドインターン体験記:プロダクトの改善活動から得た学び

はじめに

こんにちは、桂田一輝と申します。2024年8月から10月の3ヶ月間、プレイドのEcosystemチームでソフトウェアエンジニアとしてインターンシップを行いました。インターンを通じて、Ecosystemチームが開発しているKARTE Craftというプロダクトの改善業務に携わってきました。そこで得られた学びについてお伝えします。

EcosystemチームとKARTE Craftについて

私が配属されたEcosystemチームは、KARTEと外部サービスを繋ぐ連携機能のオーナーを務めています。Ecosystemチームがオーナーとなっているプロダクトの一つに、KARTE Craftがあります。

KARTE Craftはアプリケーション開発機能を提供するPaaSです。KARTEの機能拡張や独自のカスタマードリブンなサイト、アプリケーションを作成することができるため、KARTEの特徴であるユーザーの状態変化に応じた適切なアプリケーションの開発が可能になります。

https://craft.developers.karte.io/get-started/about-karte-craft/

今回のインターンで私が担当したのは、KARTE Craftの Craft Functions という機能の改善でした。

Craft Functionsとは

Craft Functionsは、KARTE内外で発生するユーザーの状態変化に関するイベントをトリガーにして、任意のバックエンドプログラムを実行する機能です。外部サービスや自社システムとの連携、シンプルな連携から複雑な作り込みまで、あらゆるカスタマードリブンな独自のアプリケーションを作成できます。

https://craft.developers.karte.io/get-started/about-karte-craft/#craft-functionsの仕組み

Craft Functionsで実行するプログラムをファンクションと呼びます。ファンクションの作成時には、インフラの構築・運用は不要で、AIによるサポートも搭載されているため、手軽に作成することができます。

インターンで行ったタスク

インターン期間中は、大きく3つのタスクに取り組みました。配属後の初めのタスクとして、KARTEのサーバーインフラを理解するための設定変更を行いました。その後は、Craft Functionsの改善として、次の2つのタスクを担当しました。

  • ファンクションが意図せず操作不能となる事象を回避するためのジョブ機能の構築
  • ファンクション一覧をタイプ別に取得するサービス間APIの作成

今回は、上記の2つのタスクを紹介し、そこから得られた学びを説明します。

タスク1: ファンクションが意図せず操作不能となる事象を回避するためのジョブ機能の構築

最初のタスクは、作成したファンクションの状態管理に関する課題を解決するものです。ファンクションの作成処理の流れ、課題、そして解決方法を説明します。

課題

Craft Functionsでファンクションを作成する際のシステム概要図は次のとおりです。

image.png

KARTEは複数のマイクロサービスで構成されており、サービス間通信にはAPIやqueueが使われています。図のwebサービスはKARTE Craftの管理画面を提供しています。workerサービスではファンクション用のコンピューティングリソース作成、更新、削除などの時間のかかる処理を非同期で行っています。

ファンクションの作成順序は次のとおりです。

  1. webサービスはファンクション作成リクエストを受け取り、MongoDBにstatus: IN_PROGRESS でファンクションの設定情報を保存し、workerサービスにファンクション作成メッセージを送信します。
  2. workerサービスはファンクションを実行するためのコンピューティングリソースを作成します。
  3. workerサービスはコンピューティングリソースの作成結果に基づき、ファンクションの設定情報を更新します。
    • リソース作成に成功した場合は設定情報のstatusをSUCCESSに変更します。
    • 何らかのエラーで失敗した場合はFAILEDに変更します。

KARTE Craftのアプリケーション上で予期しない不具合が発生した場合、ファンクションのstatusが IN_PROGRESS のままで操作ができなくなるという課題がありました。ユーザー自身でできる解決手段がないため、問題になっていました。

解決策

この課題の解決策として、ファンクションの設定情報を確認し、「最終更新から1時間以上経過」かつ「status: IN_PROGRESS 」なファンクションを作成失敗したとみなして FAILED に更新する仕組みを考えました。

実装方法として、以下の2つの方法を検討し、それぞれの長所と短所を比較しました。

  1. KubernetesのCronJobリソースを使用する。
    • メリット: 構成がシンプルで直感的に理解しやすい
    • デメリット: 新しいインフラの設定を追加する必要があり構成が複雑化する
  2. フロントエンドからファンクションに対するAPIを投げる
    • メリット: サーバーサイドの計算負荷が少ない
    • デメリット: フロントエンドの計算負荷が増える。APIの露出が増える。

今回はフロントエンドに対する余計な計算負荷やAPIの露出を避けることを優先し、方法1を選択しました。方法1を用いた際の構成図は次のとおりです。

image(1).png

cron-jobというサービスをKubernetesのCronJobとして作成し、定期的にMongoDBにクエリを実行することで、上記で説明した状態のファンクションをFAILEDに更新します。

実装

実装において工夫した点を3点説明します。

  • 汎用的なジョブ実行コンテナの整備
  • ファンクション設定の更新方法
  • テストの実装

汎用的なジョブ実行コンテナの整備

今回の実装時点で、KARTE Craft上でスケジュール実行しているのはこのCronJobリソースのみでした。そのため、プログラムを単純に実装して実行すれば十分でした。しかし、他のKARTEプロダクトのコードを見ると、より汎用的にジョブを起動できる構造になっていたので、それを参考に実装しました。

次のコードは、その実装を抜粋し、説明用に改変したものです。

const argv = minimist(process.argv.slice(2));
const { jobname } = argv;

import(`./path/to/${jobname}`)
  .then(async cronjob => {
    await cronjob.default(argv);
  })
  .catch(err => {
    logger.error(err);
    process.exit(1);
  });

jobnameは引数としてプログラムを実行する際に渡します。今回はCronJobリソースの command 属性でコンテナ起動時に渡すようにしました。これにより、スケジュールの異なる複数のプログラムを1つのコンテナイメージで呼び出すことが可能になりました。

ファンクション設定の更新方法

CronJobで定期的に実行する関数では、「最終更新から1時間以上が経過」かつ「statusがIN_PROGRESS」という条件を満たすファンクションのドキュメントを全件取得し、それらをFAILEDに更新します。

当初はMongooseのupdateMany()メソッドで一括更新しようとしましたが、このメソッドでは更新したドキュメントの内容が確認できないことがわかりました。ジョブでは更新したドキュメントをログ出力したいので、今回は条件を満たすドキュメント一式を find() メソッドで取得し、 findOneAndUpdate()メソッドで1件ずつ更新しました。

テストの実装

作成した処理が正常に動作することを確かめるため、テストコードも実装しました。テストでは、更新日時が異なる複数のドキュメントを作成し、当該処理の結果を想定の値と比較しました。

得られた学び

このタスクでは、KARTE Craft上にジョブシステムを作成しました。タスクを通じて次の学びを得られました。

  • インフラ〜バックエンドを跨いだ機能の実装
    • ジョブ用のプログラムを記述し、それをKubernetesのワークロードとして動かす方法がわかりました。
  • 設計・実装レベルでの取捨選択の方法
    • 構成や実装を検討する際に、候補を挙げてその良し悪しを検討することで、より説得力のある設計・実装になることがわかりました。

タスク2: ファンクション一覧をタイプ別に取得するサービス間APIの作成

次に取り組んだのが、KARTEのHook機能経由でファンクションを起動する設定をする際に必要となる、ファンクション一覧APIの改善タスクでした。マイクロサービス間の通信に必要なAPIを作成し、サービス間通信の実装を行いました。

Hook v2

Hook v2は、KARTE内で発生した特定の変更や追加などの変化をトリガーとして、そのデータをリアルタイムで外部のシステムに送信する機能です。

https://developers.karte.io/reference/about-Hook-v2

Hook v2を使うことで、KARTE内で起きた出来事をトリガーとして、外部システムで処理を行うことができます。例えば、ユーザーからメッセージが送信された際にそれをトリガーとして特定のファンクションを実行させることができ、顧客のリアルタイムな行動に合わせたアクションを提供することができます。参考として、以下の記事では汎用のWebhookリクエストを行う方法を説明しています。

https://solution.karte.io/blog/2023/06/craft-webhook/index.html

課題

次の図はHook v2の設定画面です。この画面では、トリガーとなるイベントと、そのトリガーで起動するファンクションを指定します。

スクリーンショット2024-10-2316.14.09.png

Craft Functionsのファンクションには、2種類のタイプがあります。HTTPタイプとイベント駆動タイプです。

Hook v2において指定できるファンクションはイベント駆動タイプのみです。しかし、管理画面上はHTTPタイプ、イベント駆動タイプの両方が指定できるようになっていました。この相違を解決するため、管理画面上において指定できるファンクションからHTTPタイプを除き、イベント駆動タイプのみを表示する必要がありました。

解決策と実装

Hook v2機能はKARTE Craftとは異なるサービスで動作するため、マイクロサービス間でAPI通信を行いファンクション一覧を取得しています。今回はAPIの改善を行いました。具体的には、ファンクション一覧取得のリクエスト時に、タイプを指定できるAPIを追加し、それを利用するように変更しました。

Hook v2の管理画面においてファンクションの取得を行う際の構成図は次のとおりです。赤字で示すように、タイプを指定できるAPIへの切り替えを行いました。

image(2).png

具体的な実装として、次の作業を行いました。

  • Craft側にサービス間通信用のAPIを追加し、バックエンドを実装しました。
  • クライアントをOpenAPI Specから自動生成する仕組みを使って、サービス間通信用のクライアントを生成しました。
  • Hook v2のバックエンド処理で、新たに作成したサービス間通信用APIを利用するように変更しました。

得られた学び

このタスクでは、マイクロサービス間の通信を行う仕組みを理解し、その改善を実施しました。タスクを通じて、次の学びが得られました。

  • 本番稼働するシステム上で、どのようにマイクロサービスが稼働しているかの全体像が掴めました。
  • インターフェース仕様(OpenAPI Spec)を活用してクライアントを自動生成することで、良い開発体験を提供できることがわかりました。

あとがき

3ヶ月に渡って参加したインターンシップの感想を端的に言えば、非常に充実した経験でした。

今回のインターンシップを通して、次のような学びが得られました。

  • 実際に社員が担当するレベルのタスクを担当することで、本番稼働するプロダクトを開発する際に必要なレベル感を実感しました。
  • タスクを通じて、PaaSのインフラからバックエンドまで、一気通貫の開発を経験できました。
  • システム特性の面でも、Webシステムだけではなく、ジョブシステムを新たに作成する経験を通じて、開発の幅を広げることができました。

これまでの私の開発経験は個人的なプロジェクトに限られており、実際のプロダクトに対する開発は初めてでした。今回のインターンシップで、実際に世の中に出ているシステムの開発に携わることで、多くの刺激を受けることができました。KARTEというアプリケーションの裏側の設計や動作原理を知った時の感動、そこで発生している課題に対して解決策を考え、技術選定をし、実装する楽しさ、そして自分が開発したものが実際にデプロイされる喜びを強く感じました。これらの経験を通じて、エンジニアリングそのものへの好奇心が大いに高まりました。また、この過程でコードを読み解き構造を把握する能力や、未知の技術を調査して組み込む力を養うことができたと考えています。

さらに、インターンシップの環境は非常に学びやすいものでした。メンターの方はいつ質問しても丁寧に答えてくださり、質問しやすい雰囲気が整っていました。加えて、インターンシップ期間中は他のチームの方々も交えたランチに頻繁に誘っていただき、様々なチームや職種の方々の話を聞くことで、多様な視点を知ることができました。

お世話になった社員の皆様に心から感謝申し上げます。本当にありがとうございました。