LoginSignup
6
2

Next.js + ASP.NET Core を .NET Aspire で構成する(with YARP)

Last updated at Posted at 2024-01-04

はじめに

この記事は .NET Aspire に関する一連の記事の一部です。

.NET Aspire + Dapr についてはこちらをご覧ください。メインは Dapr についてですが、.NET Aspire を使用する場合についても記載があります。


この記事は .NET Aspire でフロントエンドフレームワークと他サービスの構成を構築してみよう、という内容です。サンプルとして Next.js を扱いますが、React、 Vue.js を使用する場合でも参考になるかと思います。

Node アプリケーション用の拡張メソッド AddNodeApp, AddNpmApp

 2023/12 にリリースされた.NET Aspire の Preview 2 は、IDistributedApplicationBuilder の拡張メソッドに AddNodeApp 拡張メソッドと AddNpmApp 拡張メソッドが追加されました。これによって Node アプリケーションを .NET Aspire 管理下で Executable として簡単に扱えるようになりました。

例えば Vite を使って React アプリケーションを作成したり、Next.js アプリケーションを作成した場合、

npm run dev

という npm コマンドを使って Hot Reload させながら開発することになりますので、AddNpmApp 拡張メソッドを使います。App Host プロジェクトの Program.cs には次のように実装します。

Program.cs
builder.AddNpmApp(name: "ReactApp", workingDirectory: "../frontend", scriptName: "dev");

name には好きな名前をつけます。workingDirectoryは AppHost プロジェクトルートから見た React/Next.js アプリケーション のルートフォルダへの相対パスです。scriptName には package.json の scripts に記載されたコマンド名を設定します。この実装例 dev は、npm run dev の dev です(package.json の scripts に記載された dev コマンドです)。

Visual Studio の Node.js アプリケーション用のテンプレート使えばいいんじゃないの?

Visual Studio には以前より Node.js アプリケーション用のプロジェクトテンプレートがあります。React, Vue.js, Augularのフレームワークに対応しています。Visual Studio 用にプロジェクトファイルも作成されて、デバッグ実行もできます。.NET Aspire 管理下にするのであれば、そのようにして作成した Node.js プロジェクトに対して AddProject 拡張メソッドを使用すれば良さそうなものです。

しかし、AddProject 拡張メソッドは .NET プロジェクト専用なので Node.js アプリケーションに対して AddProject 拡張メソッドは使用できないのです。そのため、Node.js アプリケーションを .NET Aspire で管理するためには AddResource、または AddExecutable メソッドを使って自分でセットアップする必要があったのですが、簡単にセットアップできるように Preview 2のリリース時に新たに AddNodeApp、AddNpmApp拡張メソッドが追加されました.

.NET Aspire を使う必要ってある?

単体の Node.js アプリケーションを .NET Aspire 管理下にするのは AddNodeApp, AddNpmApp 拡張メソッドを使えば簡単に実現できるのですが、それだけでは全く意味がありません。でも CSR (Client Side Rendering)として Node.js アプリケーションを稼働させる箇所については WebAPI との通信で CORS への対処が必要です。コンテナベースの場合は次のような構成を取ることになるでしょう。

image.png

Next.js の API Routes や Nuxt.js の serverMiddleware を使う場合はこういう構成じゃない!と言う意見があると思いますが、WebAPI は分離して管理・スケールしたいシナリオだと考えていただければと思います。

そして SSR (Server Side Rendering)をする場合や最近では Next.js の Server Components のようにサーバーサイドで Database や 他サービスにアクセスする場合もあるでしょう。

image.png

この構成を構築する場合、ローカルと本番の環境による違いを考慮しなければならないのはもちろんですが、開発時のローカルマシンではアクセス先のサービスを立ち上げながら開発する必要があります。そんな環境を整えるのは結構大変です。

これらの課題を .NET Aspire を使ってうまく解決していきましょう。

余談:.NET Aspire の内部実装
.NET Aspire を使う場合、次のように IDistributedApplicationBuilder インターフェースのインスタンスである builder オブジェクトに対して AddProject や AddContainer 拡張メソッドなどを使ってセットアップします。

var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.SomeProj>(name: "SomeProj");
builder.AddContainer(name: "containerA", image:"imageName");

この Add〜〜 拡張メソッドは内部で最終的に AddResouce メソッドを使っています。AddResource メソッドの引数に渡すオブジェクトは実行に関する情報を持つのですが、.NET プロジェクト、Container、Executableの3つの形式それぞれごとに異なるクラスのオブジェクトを渡します。

Node アプリケーション用の情報をもつクラスは NodeAppResource というクラスで、これは ExecutableResource クラスを継承しています。そのため、AddNodeApp, AddNpmApp拡張メソッドを使って管理下にした Node アプリケーションは、Executableとして .NET Aspire が認識します。

今回の構成

image.png

Reverse Proxy

CSR の場合に発生する CORS を解決するために Reverse Proxy を前段に立てることにします。Reverse Proxy は /api/* へのアクセスの場合だけ WebAPI の方にトラフィックを流し、それ以外は全て Node.js アプリケーションを呼び出すように設定します。

.NET で Reverse Proxy の実装は YARP を使うととても簡単に実装できます。

YARP: Yet Another Reverse Proxy

YARP は Reverse Proxy を C# で簡単に実装できるので .NET エンジニアにはとてもおすすめです。今回の Reverse Proxy にはこの YARP を使います。

YARP の概要とデモは 2023/11 に開催された .NET Conf 2023 のセッションで紹介されています。英語ではありますが、英語が苦手な方でも自動翻訳を使うとなんとかなると思いますので是非ご覧ください。

Reverse proxying is easy with YARP | .NET Conf 2023

Node.js アプリケーション

フロントエンドフレームワークに Next.js を使用します。WebAPI へのアクセスは CSR によるブラウザからのアクセスと Server Compoments による Node.jsからのアクセスの2パターン用意します。

WebAPI アプリケーション

ASP.NET Core で WebAPI を立てます。上記の通り /api/* へのアクセスは全てこの WebAPI にトラフィックを流すように Reverse Proxy を設定します。

開発環境

  • Windows 11
  • Visual Studio 2022 17.10.0 Preview 1
  • Docker Desktop 4.26.1 (131620)
  • PowerShell 7.4.0
  • Node.js 20.10.0

.NET Aspire は VSCode でも扱うことができますが、本記事では Visual Studio 2022 Preview 版を使用します。

1. WebAPI を ASP.NET Core で作る

空のソリューションを作る

新規にプロジェクトを作ると、ソリューション名とプロジェクト名が同じになってしまいますので、先に空のソリューションファイルだけを作って、そこに追加していくことにします。

新規プロジェクト作成のダイアログの検索条件に「ソリューション」と入力すると、空のソリューションが出てきますので、選択します。

image.png

ソリューション名を ReactAspire にしました。

image.png

ASP.NET Core を作成

WebAPI 用に ASP.NET Core を作成します。ソリューションエクスプローラで先ほど作成したソリューションを右クリック → 追加 → 新しいプロジェクトを選びます。ダイアログが表示されるので、フィルターで API を選ぶと ASP.NET Core Web API プロジェクトが表示されるので、選択します。

image.png

WebApi という名前のプロジェクトにします。

image.png

「HTTPS 用の構成」は忘れずにチェックを外しておきます。「コントローラーを使用する」にチェックは入れておいた方が実装しやすいと思います。また、一番下の「.NET Aspire オーケストレーションへの参加」にチェックを入れましょう。

image.png

3つプロジェクトが作成されたと思います。では実行してみます。.NET Aspire のダッシュボードが表示されます。Endpoints のリンクをクリックすると

image.png

天気予報データが json 形式で取得できることがわかります。

image.png

2. フロントエンドを Next.js で作る

ではここでいったん Visual Studio から離れます。ソリューションファイルを作った階層を PowerShell で開き、次のコマンドを入力します。

npx create-next-app@latest

アプリケーション名は frontend としました。それ以外のオプションは以下の通りです。

image.png

作成終了したら、実行してみましょう。

cd frontend && npm run dev

http://locahost:3000 をブラウザで開くと、既定の画面が表示されます。

image.png

frontend フォルダを VSCode で開きます。

cd frontend && code .

app フォルダの中に server と client というフォルダを作り、それぞれのフォルダの中に page.tsx ファイルを作成します。

image.png

Server フォルダの中の page.tsx は Server Compomentとして、つまり サーバーサイドから WebAPI を叩きます。 client フォルダの中の page.tsx は CSR として、つまりブラウザから WebAPI を叩くようにしていく想定です。

いきなり WebAPI を叩くのではなく、まずダミー実装をして間違いなく それぞれがサーバーとブラウザで動作することを確認します。

まずはブラウザで動作する場合です。

app/client/page.tsx
'use client'

const Page = () => {
    console.log('running in client')

    return (
        <main>
            <div>Client Page</div>
        </main>
    )
}

export default Page

先頭に 'use client' と書けば、ブラウザで動きます。簡単ですね。では次に Server Component です。

app/server/page.tsx
const Page = async () => {
    console.log('running in server')

    return (
        <main>
            <div>Server Page</div>
        </main>
    )
}

export default Page

既定で Server Component として動作しますので、特別な実装はありません。では実行して http://localhost:3000/client にアクセスしてみます。

image.png

開発者ツールでコンソールを見ると、ログが出力されているので、ブラウザで動作していることがわかります。

では http://localhost:3000/server にアクセスします。

image.png

開発者ツールでコンソールを見ても何も出力されていないのでサーバー側で動いていることが確認できます。コンソールにはログが出ていますね。想定通りです。

image.png

Next.js アプリケーションを .NET Aspire 管理にする

では Nex.js アプリケーションを .NET Aspire 管理にします。その前にフォルダ構成を確認します。次のようになっているはずです。

image.png

AppHost プロジェクトの Program.cs を開いて、次のように実装します。

Program.cs
var builder = DistributedApplication.CreateBuilder(args);

- builder.AddProject<Projects.WebApi>("webapi");
+ var api = builder.AddProject<Projects.WebApi>("webapi");

+ builder.AddNpmApp(name: "frontend", workingDirectory: "../frontend", scriptName: "dev")
+     .WithEndpoint(hostPort: 3000, scheme: "http", env: "PORT")
+     .WithReference(api);

builder.Build().Run();

AddNpmApp 拡張メソッドで上で説明した通りに実装しました。ですが、実は Preview 3 の段階ではこの拡張メソッドは少し機能が足りません。細かい説明は省きますが、WithEndpoint 拡張メソッドを使って Next.js の開発サーバーの情報を渡す必要があります。そして、WithReference 拡張メソッドを使って参照先である webapi オブジェクトを渡しました。

Next.js アプリケーション から WebAPI を叩く: Server Component

ではまずは Node.js のサーバーから WebAPI を叩いてみます。Next.js は Server Component を使うとこれを簡単に実装できます。

Next.js の実装をする前に、少しだけ WebAPI を修正します。WebAPI のエンドポイントは必ず /api の下になるようにします。WebAPI プロジェクトの WeatherForecastController を次のように修正します。

Controllers/WeatherForecastController
using Microsoft.AspNetCore.Mvc;

namespace WebApi.Controllers;

[ApiController]
+ [Route("api/[controller]")]
- [Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

では Next.js の Server Component を次のコードに全て入れ替えます。

app/server/page.tsx
const getData = async () => {

    const apiServer = process.env['services__webapi__1'];
    const weatherData: Response = await fetch(`${apiServer}/api/weatherforecast`, { cache: 'no-cache' });

    if (!weatherData.ok) {
        throw new Error('Failed to fetch data.');
    }

    const data = await weatherData.json();

    return data
}

const Page = async () => {
    const data = await getData()

    return <main>{JSON.stringify(data)}</main>
}

export default Page

getData 関数で取得した天気予報データをそのまま画面に表示しているだけです。ポイントは WebAPI のエンドポイントを環境変数で受け取っている箇所です。services__webapi__1 という謎の環境変数名は一体どこからきたのでしょうか。

それを確認するためにもまずは実行してみましょう。ダッシュボードが立ち上がります。

image.png

frontend 行の Details の View リンクをクリックします。下にスクロールしていくと環境変数が表示されます。

image.png

一番下にスクロールすると services__webapi__1 環境変数の Value に WebAPI のホスト名が格納されていることがわかります。

.NET アプリケーションであれば .NET Aspire の ServiceDiscovery によって環境変数を使わず http://webapi としてアクセスすることができるのですが、違う言語を使う場合はこのように環境変数を使う必要があります。

では http://localhost:3000/server にアクセスしてみます。

image.png

既定の CSS が当たっているので見た目がおかしいですが、天気予報データが無事に取れました。

Next.js アプリケーション から WebAPI を叩く: CSR

では今度はブラウザ側から WebAPI にアクセスしてみましょう。わかっていることですが、実装しても WebAPI にアクセスした時点で CORS でエラーとなります。言い換えると CORS エラーになれば WebAPI にアクセスする箇所の実装は正しいということになります。

CSR 用のコードを全て次のコードに入れ替えます。

app/client/page.tsx
'use client'

import { useEffect, useState } from "react";

const getData = async () => {

    const weatherData: Response = await fetch('http://localhost:5031/api/weatherforecast', { cache: 'no-cache' });

    if (!weatherData.ok) {
        throw new Error('Failed to fetch data.');
    }

    const data = await weatherData.json();

    return data
}

const Page = () => {

    const [data, setData] = useState([])

    useEffect(() => {
        getData().then(setData)
    }, [])

    return <main>{JSON.stringify(data)}</main>
}

export default Page

Server Component の時とは異なり、fetch 先のホスト名に http://localhost:5031 と直接 WebAPI のホストを入力していますが、これは Reverse Proxy を導入した後で削除します。今は CORS になることを確認するためにわざと入力しています。

http://localhost:3000/client を開くと、次のようになります。

image.png

開発者ツールのコンソールを見ると CORS が発生していることがわかります。

3. YARP で Reverse Proxy を実装する

では CORS を解決するための Reverse Proxy を実装しましょう。

空の ASP.NET Core アプリケーションを追加する。

YARP はライブラリなので Web プロジェクトを新規に作成します。ソリューションファイルを右クリック → 追加 → 新しいプロジェクトを選択します。今回は ASP.NET Core(空)というプロジェクトを選びます。プロジェクトの種類で Web を選択すると見つけやすいです。

image.png

プロジェクト名は ReverseProxy としました。

image.png

.NET Aspire オーケストレーションにチェックが入っていることを確認して、作成します。

image.png

AppHost プロジェクトで参照先を実装する

AppHost プロジェクトの Program.cs を開くと、既に ReverseProxy プロジェクトが AddProject メソッドで追加されているはずです。
Reverse Proxy は Next.js, WebAPI の前に立たせますので、Next.js アプリケーションと WebAPI アプリケーションを参照するように実装します。

Program.cs
var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.WebApi>("webapi");

+ var frontend = builder.AddNpmApp(name: "frontend", workingDirectory: "../frontend", scriptName: "dev")
- builder.AddNpmApp(name: "frontend", workingDirectory: "../frontend", scriptName: "dev")
    .WithServiceBinding(hostPort: 3000, scheme: "http", env: "PORT")
    .WithReference(api);

+ builder.AddProject<Projects.ReverseProxy>("reverseproxy")
+     .WithReference(frontend)
+     .WithReference(api);

builder.Build().Run();

AddNpmApp 拡張メソッドの戻り値を frontend 変数で受け取り、それを Reverse Proxy プロジェクトの WithReference 拡張メソッドに渡すことで、参照を構成します。同様に WebAPI も構成します。

Microsoft.Extensions.ServiceDiscovery.Yarp をインストールする

では Yarp を使って実装していきます。Reverse Proxy プロジェクトの Nuget パッケージの管理画面を開きます。

image.png

検索ボックスに ServiceDiscovery.Yarp と入力します。すると Microsoft.Extensions.ServiceDiscovery.Yarp というパッケージが見つかるので、それをインストールします。

image.png

Yarp を使う場合、通常は Yarp.ReverseProxy というパッケージをインストールします。
image.png

しかし、今回はそれを使用せずに Yarp.ReverseProxy をラップした Microsoft.Extensions.ServiceDiscovery.Yarp をインストールしました。これは .NET Aspire 用に作成された ServiceDiscovery を可能にするパッケージです。

Reverse Proxy を実装する

では、ReverseProxy プロジェクトの Program.cs を次のコードで完全に置き換えます。

Program.cs
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddReverseProxy()
    .LoadFromMemory(GetRoutes(), GetClusters())
    .AddServiceDiscoveryDestinationResolver();

var app = builder.Build();

app.MapDefaultEndpoints();

app.MapReverseProxy();

app.Run();

RouteConfig[] GetRoutes()
{
    return
    [
        new RouteConfig
        {
            RouteId = "Route1",
            ClusterId = "default",
            Match = new RouteMatch { Path = "{**catch-all}" }
        },
        new RouteConfig
        {
            RouteId = "Route2",
            ClusterId = "api",
            Match = new RouteMatch { Path = "/api/{*any}" }
        },
    ];
}

ClusterConfig[] GetClusters()
{
    return
    [
        new ClusterConfig
        {
            ClusterId = "default",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                { "destination1", new DestinationConfig { Address = "http://frontend" } },
            }
        },
        new ClusterConfig
        {
            ClusterId = "api",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                { "destination2", new DestinationConfig { Address =  "http://webapi" } },
            }
        },
    ];
}

ポイントがいくつかあるので少しずつ解説します。1つ目は AddServiceDefaults メソッドの使用です。

Program.csの一部
builder.AddServiceDefaults();

このメソッドを使うだけで通常は ServiceDiscovery が有効になります。 今回の場合、AppHost プロジェクトで実装した Next.js と WebAPI プロジェクトへの参照を SerivceDiscovery で解決できるようになるはずなのですが、YARP の場合はこれだけでは ServiceDiscovery できません。これについては、次で説明します。

次のポイントは AddReverseProxy メソッドです。

Program.csの一部
builder.Services.AddReverseProxy()
    .LoadFromMemory(GetRoutes(), GetClusters())
    .AddServiceDiscoveryDestinationResolver();

AddReverseProxy は Yarp.ReverseProxy パッケージに含まれている YARP を使用することをパイプラインに適用するためのメソッドです。 Add したら Use するのがパイプラインの基本なので、Use もしています。もう少し下を見ると次の実装があります。なぜか Use ではなく Map〜 となっていますけど、同じ意味です。

Program.csの一部
app.MapReverseProxy();

AddReverseProxy メソッドの話に戻しましょう。

Program.csの一部
builder.Services.AddReverseProxy()
    .LoadFromMemory(GetRoutes(), GetClusters())
    .AddServiceDiscoveryDestinationResolver();

LoadFromMemory メソッドは YARP による ReverseProxy の定義を行うメソッドです。GetRoutes メソッドで振り分けのルールを定義し、GetClusters メソッドで振り分け先のパスを定義しています。別にメソッドを作らなくても良いのですが、見やすさのためにメソッドに切り出しています。YARP は appSettings.json に振り分け定義を書くことも多いのですが、このようにコードで定義することもできます。.NET Aspire ではコードで全て定義できるのですから、それに合わせてコードで実装してみました。

そして AddServiceDiscoveryDestinationResolver メソッドですが、これが .NET Aspire によって追加されたメソッドです。このメソッドを叩くことで YARP で ServiceDiscovery が機能します。 AddServiceDefaults メソッドを叩くだけではダメなので、注意が必要です。

最後のポイントは GetClusters メソッドの中です。

Program.csの一部
ClusterConfig[] GetClusters()
{
    return
    [
        new ClusterConfig
        {
            ClusterId = "default",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                { "destination1", new DestinationConfig { Address = "http://frontend" } },
            }
        },
        new ClusterConfig
        {
            ClusterId = "api",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                { "destination2", new DestinationConfig { Address =  "http://webapi" } },
            }
        },
    ];
}

http://frontend, http://webapi と AppHost プロジェクトの Program.cs で定義した名称を使って振り分け先を定義しています。.NET Aspire っぽいところですね!

Next.js の CSR から WebAPI の呼び出し実装を修正

忘れちゃいけない修正がありました。 Next.js の CSR の実装では WebAPI のアクセス先のホスト名を直書きしていましたが、ReverseProxy によって /api へアクセスすれば WebAPI にトラフィックが振り分けられるようになったので、ホスト名を削除します。

app\client\page.tsx
'use client'

import { useEffect, useState } from "react";

const getData = async () => {

+     const weatherData: Response = await fetch('/api/weatherforecast', { cache: 'no-cache' });
-     const weatherData: Response = await fetch('http://localhost:5031/api/weatherforecast', { cache: 'no-cache' });

    if (!weatherData.ok) {
        throw new Error('Failed to fetch data.');
    }

    const data = await weatherData.json();

    return data
}

const Page = () => {

    const [data, setData] = useState([])

    useEffect(() => {
        getData().then(setData)
    }, [])

    return <main>{JSON.stringify(data)}</main>
}

export default Page

では実行してみましょう。ダッシュボードには ReverseProxy のリソースが増えています。Endpointのリンクをクリックして初期画面を表示後、 /client にアクセスしてみましょう。

image.png

ReverseProxy が動いて、WebAPI から取得したデータが表示されました。

image.png

まとめ

一度この構成を組み上げてしまえば、AppHost プロジェクトは起動したまま、つまりダッシュボードも立ち上げっぱなしで Next.js の開発を行うことができます。
さらに参照先のサービスが増えていくと、それらも .NET Aspire で分散アプリケーションの管理することでローカルでの開発がやりやすくなるのではないでしょうか。

AddNodeApp, AddNpmApp 拡張メソッドはまだ初回リリースです(Preview 2)。Issue を見ると、これはまだ単に薄いラッパーを用意しただけなので課題があることは認識しており、追加で修正が行われる予定のようです。きっともっと使いやすくなるでしょう。

Added support for node and npm based projects

そして今回は YARP 用にプロジェクトを作成してセットアップする必要がありましたが、2024/1現在、YARP による Ingress をもっと簡単にセットアップする機能が Preview 3 のロードマップに入りました。

Support for YARP-based reverse proxy to use as ingress

この機能がリリースされると、もっと簡単に構築できるようになると思います。期待したいですね。

6
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2