はじめに
みなさんこんにちは。
AmebaLIFE事業本部でAmebaブログのバックエンド開発を担当している宮野 奎太朗(@38ke1)です。
私たちのチームでは現在、約20年以上にわたって運用を続けているJavaベースのバックエンドシステムを
Goで実装したシステムへ移行するプロジェクトを機能開発と並行して進めています。
本記事では移行に踏み切った背景、そして最近導入したTypeSpecによるスキーマ駆動開発の具体的な取り組みについてご紹介します。
移行の背景
AmebaブログのバックエンドシステムはJava言語をベースとしたシステムを構築&運用していましたが、
大きく以下3つの課題に直面しており継続した開発が難しい状況にありました。
- 複雑化したシステムのメンテナンス性
- Javaエンジニア人材の縮小
- コンテナ化への移行推進
Amebaが掲げる「100年愛されるメディアを創る」ビジョンの実現に向けてこれらの課題を解決するため、
モダンな技術スタックへの移行を進めています。
段階的移行について
20年以上稼働する巨大なAmebaブログのシステムをサービスを止めることなく刷新するため、「段階的移行」を行なっています。
具体的には既存のJava APIの前段にGoで実装したAPIを配置し、すべてのリクエストを最初にGoアプリケーションで受け取るような構成です。
移行のプロセスは、以下の図に示す「既存のAPIリクエスト」と「Go での新実装」の2つのリクエストフローで構成されています。
1. 未移行のエンドポイント(プロキシ経由)
まだGoへの移行が完了していないエンドポイントへのリクエストは、
Goアプリケーションが受け取った後、そのまま後ろに控える既存のJava APIへプロキシします。
クライアントから見れば、これまで通りAPIが機能しているように見えます。
2. 移行済みのエンドポイント(Goでの新実装)
Goで再実装が完了したエンドポイントはGoのアプリケーションが直接リクエストを処理し、
データベースアクセスやレスポンス生成を行います。
これにより、エンドポイント単位で段階的にGoで実装したシステムへと切り替えていくことが可能です。
この仕組みを構築することにより、既存のAPIを機能単位で移行しつつ並行して新しい機能開発を
Goで実装する、といった柔軟な開発を実現することが出来ています。
TypeSpecによるスキーマ開発の取り組み
TypeSpecとは?
TypeSpecはMicrosoft社が開発を進めているAPI仕様定義のための独自言語です。
TypeScriptにインスパイアされた言語で、開発者はTypeScriptに似た構文を使用して
OpenAPIのスキーマを効率的に定義することが出来ます。
そのため、OpenAPI単体では冗長になりがちなジェネリクスやインターセクション、ユニオン型が
サポートされており、より柔軟なAPI定義が可能となっているのが特徴です。
また提供されているデコレーターを利用することで、OpenAPIのメタデータ定義やバリデーション情報を
簡単に追加することが出来ます。
なぜOpenAPIではなくTypeSpecを導入したか?
Amebaブログでは元々、OpenAPIを使ったAPI仕様を定義していました。
しかし、Amebaブログ程の規模になるとAPIエンドポイントの数は800を超え、
単一のYAMLファイルが肥大化し、ファイルの視認性が低い状態にありました。
これらの課題を解決する上でTypeSpecが持つ以下の特徴が有効であると判断し、導入を決定しました。
- TypeScriptのようにimportを使ってファイルを分割し、モデルやインターフェースをモジュール化することが可能な点。
- 認証情報や共通のレスポンス形式などを「ベースモデル」として定義し、 各エンドポイントで再利用出来る点。
- YAMLでの記述に比べてはるかに記述量が少なく、APIの親子関係や構造が直感的に理解しやすい点。
私たちは既存のOpenAPI定義を活かしつつ、段階的にTypeSpecへの移行を進めており
その際TypeSpec公式のCLIツールである「tsp-openapi3
」が非常に役立ちました。
Converting OpenAPI 3 into TypeSpec
手順としては
- OpenAPI定義からtspファイルを生成する
- 生成されたファイルをもとに1エンドポイントごとに別ファイルでモデルとインターフェースの定義をする
- 定義が完了したタイミングで生成ファイルから対応箇所のコードを消す
上記の運用を取ることで、現状どのくらい移行が進んでいるかが可視化された状態で実装することが出来ます。
TypeSpecはあくまでOpenAPI定義を生成するためのツールです。
万が一、TypeSpec自体の開発が停止するようなことがあっても、
最終的に生成されたopenapi.yaml
を直接メンテナンスするという道が残されています。
特定のツールにロックインされるリスクが低く柔軟である点は、導入の大きな安心材料となりました。
既存のOpenAPI定義を使ったTypeSpecへの移行を考えている方がいらっしゃれば、是非参考にして頂けると嬉しいです。
TypeSpecの実装例
それでは実際のコードを例に実装内容を見ていきましょう。(セキュリティの都合上一部コードを改変しております。)
以下はブログのテーマを取得するエンドポイントの実装例です。
// routes/theme.tsp
import "@typespec/http";
import "../models/theme.tsp";
using Http;
namespace BlogAPI;
@route("/example/blog")
@tag("theme-controller")
interface ThemeController {
/** ブログテーマ単体取得 */
@summary("ブログテーマ単体取得")
@route("/themes/{theme_id}")
@get
getEntryTheme(@path theme_id: int64, ...AuthHeader):
| {
@header contentType: "application/json";
@body body: GetEntryThemeResponse;
}
| UnauthorizedResponse
| ForbiddenResponse
| NotFoundResponse;
}
上記のファイルでは、APIの具体的なエンドポイントを定義しています。
@route
デコレーターでURLのパスを指定し、@get
でHTTPメソッドを定義します。
ここでは、GET /example/blog/themes/{theme_id}
というエンドポイントを定義しており、成功時のレスポンス(GetEntryThemeResponse
)や、
認証エラーなどの異常系レスポンスもパイプでつなげて網羅的に記述できるのが特徴です。
// models/theme.tsp
import "@typespec/http";
import "./common.tsp";
namespace BlogAPI;
model EntryTheme {
blog_id: int64;
...
}
model EntryThemePaging {
next_theme: EntryTheme;
prev_theme: EntryTheme;
}
model GetEntryThemeResponse {
data: EntryTheme;
paging: EntryThemePaging;
session_user: SessionUser;
}
こちらは、APIが返すJSONなどのデータ構造(モデル)を定義するファイルです。model
キーワードを使って、TypeScriptのinterface
のようにデータの型を定義します。
先ほどのroutes/theme.tsp
で定義したAPIが返すGetEntryThemeResponse
の中身が、data
やpaging
といった複数のモデルで構成されていることが分かります。
// models/auth.tsp
import "@typespec/http";
using TypeSpec.Http;
namespace BlogAPI;
// External API 用の認証ヘッダー
model AuthHeader {
@header("Authorization")
@doc("ドキュメントをここに記載")
authorization?: string;
}
// 以下のように他の認証も含めたmodelを作ることも可能
model AnotherAuthHeader {
@header("X-AnotherAuth")
@doc("another-auth")
xAnotherAuth: string;
}
model AuthHeaders {
...AuthHeader;
...AnotherAuthHeader;
}
複数のAPIで共通して利用するパーツは、別ファイルに切り出して管理できます。
この例では認証ヘッダーをAuthHeader
というモデルとして定義しています。
スプレッド構文を使うことで、AuthHeaders
のように複数のモデルを合成して、新しいモデルを簡単に作成することも可能です。
上記の例から分かるように、TypeSpecではAPIの「エンドポイント」、「データモデル」、「共通パーツ」といった責務を明確に分離出来ることが強みです。
YAMLを直接記述するよりも遥かに簡潔かつ再利用性の高いスキーマを構築できることが、お分かりいただけたかと思います。
まとめ
JavaからGoへの大規模な言語移行は、技術的なチャレンジが多い取り組みです。
しかし、段階的な移行アプローチとTypeSpecのようなモダンなツールの活用により、
サービスを止めることなく小さく適切な粒度で着実に進めることができています。
これから数十年先を見据えた開発基盤を整えることは、将来の技術的負債を減らすためにも不可欠です。
この取り組みを今後もチームとして積極的に推進していきます。
こちらの記事が少しでもみなさんの参考になれば嬉しいです!
最後まで読んでいただき、ありがとうございました!