🍆

Akebiを拡張してマルチサービスのローカル開発を楽にする

2024/10/05に公開

私たちはスコラボ(https://www.sucolab.jp)やスコマド(https://www.sucomado.jp)、tobaso(https://www.tobaso.jp)といったいくつかのサービスを開発・運用しています。
同時にいくつかのサービスを開発し、それぞれがある程度つながっているため同時に開発環境を起動したい状況が存在しています。
この記事では、その状況で発生した問題とAkebi (https://github.com/tsukumijima/Akebi)を拡張し用いた弊社の対応策を紹介したいと思います。

前提とサービスのつながり

弊社のサービスは、スコラボという配信画面がデザインできるサービスをに対して、スコマドという配信素材ストアで購入した素材が利用できたり、tobasoというおたよりサービスで募集したおたよりをテンプレートを組み合わせて表示出来たりとサービスがある程度密接に関わっている状況です。
また、初期はスコラボを軸に開発していたことから、アカウントシステムは基本的にスコラボ側に存在しており、スコラボで作成されたアカウントを軸に関連サービスの認証・認可を行うという構成を取っています。

発生した問題達

サービス間がある程度密接に関わっており、認証にスコラボで作成されたアカウントを利用しているため、開発時には基本、スコラボは必ず起動する必要があり、それと合わせて開発する関連サービスもローカルで起動する必要があります。
また、弊社のサービスは/api以下のパスへのトラフィックをすべてバックエンドに流すこととしており、クロスオリジンでのリクエストを行わないという方針にしているため、開発環境であっても/api以下のパスへのトラフィックをすべてバックエンドに流す必要が出てきます。

問題① /api以下のパスへのトラフィックだけをどうやってバックエンドに流すか

これに関してはフロントエンドがNext.jsを利用しているため簡単な解決策が存在しています。
Next.js側で提供されているrewritesの仕組みを利用することです。
開発サービスがスコラボのみだった頃はこの問題しか発生していなかったためこの仕組みを利用して解消していました。実際には以下のような設定をnext.config.jsに入れることで解決できます。

const apiBase = process.env.API_SSR_BASE_URL || 'http://localhost:8000';

/** @type {import('next').NextConfig} */
const nextConfig = {
  // その他の設定は省略
  rewrites: async () => {
    return [
      {
        source: '/api/:path*',
        destination: new URL('/api/:path*', apiBase).href,
      },
    ];
  }
}

export default nextConfig;

この方法で注意が必要な点は、サーバーサイドからのリクエストにはrewritesルールが適用されないことです。
そのため、サーバーサイドからのリクエストでは直接バックエンドにリクエストを行い、クライアントサイドからのリクエストではフロントエンドのサーバ(ここで言うとNext.jsの開発サーバ)を経由してリクエストを行う必要があることに注意が必要です。(クライアントからのリクエスト時にOriginを記述せずにリクエストするだけで、同一オリジンへのリクエストになるためこれでも大きな問題はないかと思います)

問題② フロントエンドの開発時にバックエンドのサーバをローカルで立ち上げなければいけない

何を当たり前のことを言っているのかという感じかもしれませんが、一つのサービスであれば良いですが、複数サービスを開発するとなるとどうでしょう。
フロントエンドとバックエンドのリポジトリをすべてclone/pullしてきて、ローカルの環境を構築して全部起動するとなると、PCへの負荷も気になりますしなにより面倒くさいですね。DBなども立ち上げるかつなげられるようにするのも若干面倒です。(バックエンドとフロントエンドを同時進行で開発するのでAPIができあがるたびにupstreamからpullし直すと考えるとつらい...)

これに関しては対応策がいくつかあります。

1つ目はdevcontainerやdocker composeを用意して環境構築を楽にするという方法です。
この場合はPCへの負荷という面、変更差分を毎回取得しないといけないという面はどうにもなりませんが、環境構築の手間だけはかなり抑えられるかと思います。
docker imageをGitHub Actionsでビルドしどこか(Artifact RegistryやECRなど)にpushするようにすればバックエンドのリポジトリをpullしてくるという手間も若干なくなるのではないでしょうか。

2つ目はローカル開発用のAPIサーバーを用意するという方法です。
弊社ではこの方式を取っており、ローカルでバックエンドのサーバを立ち上げる必要がなくなるため、問題になっていたことをすべて解決することが出来ます。
ローカル開発用のAPIサーバを用意し、問題①でも利用したrewritesの仕組みを使って同一オリジンへのリクエストとして利用できるようにすれば、ローカルで立ち上げる必要のあるサーバはフロントエンドの開発サーバのみになります。

問題③-1 localhostだとCookieのドメイン空間が衝突する

弊社のサービスはすべてCookieを用いた認証とCSRF対策をしているため、これが最も重大な問題です。
複数サービスをすべてlocalhostで立ち上げるために、ポート番号を3000,3001,3002...のように衝突しないように設定しますが、Cookieは同一ホストの異なるポートでは共有されるため、ローカルでは全サービスのCookieに付いているドメインがlocalhostとなり衝突します。
このとき、CSRF Tokenがサービス間で衝突し上書きされてしまうため、Token Mismatchが発生したり、セッションのCookieの名前が一緒なのであればそれも衝突して上書きされてしまうため、認証関連もおかしなことになるでしょう。(セッションのCookieの名前はサービスごとに違うため弊社で問題になったのはCSRF Tokenの方です)

これに関しても対応策としてはいくつかあります。

1つ目はCookieの名前をサービスごとに変更する方法です。
セッションに関しては変えていること・変えることが多いため問題ないかと思いますが、CSRF Tokenが問題になってきます。

2つ目はlocalhostを利用せずに別なドメインを用意する方法です。
RFC6761にて特定用途用の予約済みドメインとして、.example.localhost.testなどがあるためこれらを利用して別なドメインを用意する方法や、すでに会社として保有しているドメインのサブドメインをこの用途のために利用するという方法が想定されるかと思います。
/etc/hostsを用いてlocal.sucolab.testlocal.sucomado.testをループバックアドレスに向け、各サービスへのアクセス時にそれぞれのドメインを使うことでドメインが重複しなくなり、この問題へは対処できるようになります。
しかし、この方法では異なる問題が発生します...

問題③-2 localhost以外のドメインにするとSecure Contextを求められる状況で困る

問題③-1の2つ目の解決策であるlocalhost以外の別なドメインを用意する方式をとると、httpsでないとSecure Contextではなくなってしまいます。

最近知ったのですが.localhostに関してはlocalhost同様にPotentially Trustworthyになるらしいのでこの問題は.localhostを使うことでもどうにかなります。
また、httpsでアクセスするようにする術自体はいくつかあるため、対応方法がないわけではありませんがかなり面倒です。

問題③-3 localhost以外のドメインにするとGoogle OAuthのCallback先として利用できない

問題③-1の2つ目の解決策であるlocalhost以外の別なドメインを用意する方式をとると、Google OAuthのCallback先として設定できなくなります。
Twitterログインとかを利用するという術はありますが、YouTube連携機能を提供しているためローカルでGoogle ログインのテストができなくなると動作確認・検証が出来なくなってしまいます。

これは、Google Cloudのコンソールで弾かれてしまうため、どうしようもありません...

解決策

これらの問題に対して、Akebiを拡張することによって弊社は解決を図っています。

Akebiとは?

ncrucesさん開発のkeylessをベースにtsukumijimaさんが開発されたHTTPSリバースプロキシサーバです。
これを用いることで、インターネットに公開されていないWebサイト・Webアプリケーションを正規のLet's Encryptの証明書でHTTPS化することができ、Akebiをそのまま利用するだけでも上記であげた問題の一部を解決することもできます。

Akebiのベースとなったkeylessを開発されたncrucesさん、Akebiを開発されたtsukumijimaさんに深く感謝申し上げます。

Akebiのアプローチに至った理由など、とても面白いですので是非Akebi自体のReadmeもお読みください

https://github.com/tsukumijima/Akebi

『Akebiを拡張する』とは

Akebiをそのまま利用するだけでも、Secure Contextに関する問題は解決することが可能ですが、複数のサービス名のサブドメインでのリバースプロキシを提供するのは難しいことと、/apiへのリクエストを可能であればリバースプロキシでバックエンドに向けたいことから、Akebiをforkして一部拡張しています。
また、keyless-serverについては会社側で環境を構築し、提供するようにしているため、開発者が意識するのはローカルでのリバースプロキシであるhttps-serverのみにしています。

弊社で拡張・変更しているのは以下のような内容です
(以下で出てくるexample.comは実際には別な実在するドメインです。)

各サービス名のサブドメインを利用できるように

問題③-1の解決のためサービスごとにlab.local.example.commado.local.example.comtobaso.local.example.comのようにサブドメインを分けたいため、keyless-server側で利用するサービス名のサブドメインをループバックアドレスに解決するようにすると共に、https-server側でもサブドメインごとに向き先を変更できるようにしています。

/apiのみを別な向き先にできるように

問題①の解決はNext.jsの機能でももちろんできるのですが、本番環境においてLoadBalancerでトラフィックの向き先を変えているため、責務としてはリバースプロキシに持たせてあげたいところでした。
そのため、各サービス名のサブドメインに対するリクエストについては/apiのみ別な向き先に設定することもできるようにしています。

デフォルト値を基本的に扱いやすい値に

社内向けの開発ツールなのである程度設定する値は共通になります(labは3000ポート、madoは3001ポートみたいな)
そのため、初期値としてそれらの基本的な値に設定することで社内向けに配布されているAkebiを起動するだけで利用できるようにしています。

また、問題②の解決のためにフロントエンドの開発時はローカル開発用のAPIサーバに接続したいため、それらを一括で設定できる(/apiの向き先をローカル開発用に用意されているAPIサーバに向ける)フラグを用意しています。

IAPの認証を通過できるように

弊社の開発環境はすべてIdentity-Aware Proxyを用いて保護しています。
そのため、ローカル開発用のAPIサーバに到達するためにはブラウザでIAPの認証がなされていてCookieが存在するか、AuthorizationヘッダもしくはProxy-Authorizationヘッダで認証情報を載せる必要があります。

そこで、https-server側で各開発者がそれぞれログインしているのgcloud CLIの Application Default Credentialsを利用し、アクセス許可がなされている専用のサービスアカウントの権限を借用してTokenを生成し、Proxy-Authorizationを自動付与するように変更しています。

これによってgcloud auth application-default loginをしておくだけでIAPの認証を意識することなく開発ができるようになるため、開発環境の保護と開発体験の両立ができるようにしています。

最後に

それぞれの会社でこんな問題に対応するためにいろいろな社内ツールや対応策が考えられているとは思いますが、弊社ではこんな風に対応してるよ!というご紹介でした。
まだサービスログインにスコラボが必要なのは変わりなく、関連サービスの開発時にスコラボを起動する必要があるという状況をどうにかするところまでは至っていませんが、今後解決できたらと思っています。

Akebiをforkして利用させていただいて居ますが、社内向けのカスタマイズが多く、公開できるようなものではないためOSSにできていません。
改めてになりますが、ncrucesさん、tsukumijimaさんに深く感謝申し上げます。

スコテック

Discussion