本文へスキップ
.NET によるクラウドネイティブ開発の実践ガイドのアイキャッチ画像
Architecture

.NET によるクラウドネイティブ開発の実践ガイド

公開: 更新: 約11分で読めます

はじめに

クラウドネイティブ開発とは、コンテナ・オーケストレーション・マネージドサービスを前提に、スケールと回復性、継続的デリバリを設計の中心へ据える取り組みを指します。.NET は近年のリリースで、小さなコンテナイメージ、ネイティブな OpenTelemetry 対応、そして開発体験を束ねる .NET Aspire を揃え、この領域で扱いやすい基盤になりました。

本記事では、コンテナ化からオーケストレーション、マイクロサービスの回復性、構成とシークレット、ヘルスチェック、可観測性、CI/CD までを一つの流れとして整理します。個々の技術を断片的に紹介するのではなく、実運用に乗せるまでの全体像を体系立てて解説することを目的とします。コード例は .NET 10 系を前提としていますが、考え方の多くは .NET 8 以降で共通です。

コンテナ化からオーケストレーション、回復性、構成とシークレット、ヘルスチェック、可観測性、CI/CD までを並べ、下段の .NET Aspire が開発体験を束ねる .NET クラウドネイティブ構成要素の流れ図
コンテナ化からCI/CDまでの各構成要素を .NET Aspire が開発時に束ねる全体像を示した図

コンテナ化 — 小さく安全なイメージを作る

クラウドネイティブの出発点はコンテナ化です。イメージが小さいほど起動が速く、攻撃面が狭く、レジストリの転送コストも下がります。.NET では大きく二つの作り方があります。マルチステージの Dockerfile を書く方法と、SDK に組み込まれたコンテナビルド機能を使う方法です。

マルチステージ Dockerfile と chiseled イメージ

ビルド環境とランタイム環境を分離するマルチステージ構成が基本です。ランタイムには、Microsoft が提供する chiseled イメージ(Ubuntu ベースから不要なパッケージを削ぎ落とした distroless 相当のイメージ)を選ぶと、シェルやパッケージマネージャを含まない最小構成になります。非 root 実行も既定で有効です。

# build ステージ
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["OrderService.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish "OrderService.csproj" -c Release -o /app/publish

# runtime ステージ(chiseled + 非 root)
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS final
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
ENTRYPOINT ["dotnet", "OrderService.dll"]

より徹底して起動時間とメモリを削るなら Native AOT を検討します。AOT はネイティブコードへ事前コンパイルするため、リフレクションや動的コード生成に依存するライブラリと相性が悪い場合があります。ミニマル API やバックグラウンドワーカーのように依存が明確なサービスから適用するのが現実的です。

SDK コンテナビルドで Dockerfile を書かない

Dockerfile を保守したくない場合は、SDK に組み込まれたコンテナ発行機能が使えます。プロジェクトのプロパティでベースイメージや実行ユーザーを指定し、コマンド一つでイメージを生成できます。

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled</ContainerBaseImage>
  <ContainerUser>$APP_UID</ContainerUser>
  <ContainerRepository>order-service</ContainerRepository>
</PropertyGroup>
dotnet publish -c Release /t:PublishContainer

この方式は依存パッケージのレイヤリングを SDK が最適化するため、キャッシュ効率の良いイメージが得られます。Dockerfile 方式と SDK 方式のどちらを選ぶかは、既存のビルドパイプラインや細かなレイヤ制御が必要かどうかで判断します。

オーケストレーション — Kubernetes と Azure Container Apps

コンテナを本番で動かすには、配置・スケール・ローリング更新・ヘルス監視を担うオーケストレーション層が必要です。選択肢は運用の自由度と管理コストのトレードオフで決まります。

Kubernetes は最も柔軟で、ネットワークポリシーやカスタムリソースまで細かく制御できます。その反面、クラスタ運用の負担は小さくありません。次のマニフェストは、レプリカ数とヘルスプローブ、リソース要求を定義する最小構成です。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels: { app: order-service }
  template:
    metadata:
      labels: { app: order-service }
    spec:
      containers:
        - name: order-service
          image: myregistry.azurecr.io/order-service:1.4.0
          ports: [{ containerPort: 8080 }]
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
            limits: { cpu: "500m", memory: "256Mi" }
          readinessProbe:
            httpGet: { path: /health/ready, port: 8080 }
          livenessProbe:
            httpGet: { path: /health/live, port: 8080 }

一方の Azure Container Apps は、Kubernetes(内部的には KEDA と Dapr)をマネージドに包んだサービスです。クラスタ管理を意識せずにコンテナを配置でき、HTTP トラフィックやキュー長に応じたゼロスケールを含む自動スケールを設定だけで得られます。マイクロサービス群を素早く立ち上げたい場合や、インフラ専任チームを置けない組織では有力な選択肢です。細かなカスタムコントローラや特殊なネットワーク要件がある場合は Kubernetes を、標準的な Web/API とワーカーが中心なら Container Apps を、という切り分けが目安になります。

マイクロサービス設計と回復性

サービスを分割すると、プロセス間通信は必ずネットワークをまたぎます。ネットワークは遅延し、ときに失敗します。したがって回復性の作り込みは付加機能ではなく必須要件です。.NET では標準ライブラリに統合された Polly(Microsoft.Extensions.Http.Resilience)で、リトライ・サーキットブレーカー・タイムアウトを宣言的に構成できます。

builder.Services.AddHttpClient<InventoryClient>(client =>
{
    client.BaseAddress = new Uri("https://inventory-service");
})
.AddStandardResilienceHandler(options =>
{
    // 一時的な失敗を指数バックオフで再試行
    options.Retry.MaxRetryAttempts = 3;
    options.Retry.BackoffType = DelayBackoffType.Exponential;
    options.Retry.UseJitter = true;

    // 連続失敗が続いたら回路を開いて呼び出しを遮断
    options.CircuitBreaker.FailureRatio = 0.5;
    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);

    // 個々の試行に上限を設ける
    options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
});

各パターンの役割を整理します。

  • リトライ — 一時的なネットワーク断や瞬間的な過負荷から自動で回復する。ジッターを加えて再試行が同時に集中する事態を避ける。
  • サーキットブレーカー — 障害中のサービスへ呼び出しを送り続けて悪化させないよう、一定期間だけ回路を開いて即座に失敗を返す。
  • タイムアウト — 応答しない相手に呼び出し側のスレッドやコネクションを占有させない。

リトライは冪等な操作にのみ安全に適用できる点に注意します。注文確定や決済のように副作用を伴う処理では、重複を防ぐために冪等キーを設計へ組み込みます。また、サービス間の同期呼び出しが連鎖すると一箇所の遅延が全体へ波及するため、可能な範囲でメッセージキューによる非同期連携へ寄せると全体の回復性が高まります。

構成・シークレット・ヘルスチェック

クラウドネイティブでは、同じイメージを環境ごとに異なる設定で動かします。設定値は環境変数や構成プロバイダから注入し、機微情報はコードにもイメージにも埋め込みません。.NET の構成システムは複数のソースを重ね合わせられるため、ローカルでは user-secrets、本番では Key Vault やシークレットストアという使い分けが自然に書けます。

var builder = WebApplication.CreateBuilder(args);

// 本番のみ Azure Key Vault を構成に追加
if (builder.Environment.IsProduction())
{
    var vaultUri = new Uri(builder.Configuration["KeyVault:Uri"]!);
    builder.Configuration.AddAzureKeyVault(vaultUri, new DefaultAzureCredential());
}

認証情報のハードコードを避ける鍵は、パスワードではなくワークロード ID を使うことです。Azure ではマネージド ID、AWS では IRSA(IAM Roles for Service Accounts)を用いると、資格情報を配布・ローテーションする手間そのものがなくなります。上のコード例の DefaultAzureCredential は、ローカルでは開発者の認証情報を、クラスタ上ではマネージド ID を自動的に選択します。

ヘルスチェックは、オーケストレータがコンテナの生死とトラフィック受け入れ可否を判断するための情報源です。liveness(プロセスが生きているか)と readiness(依存先を含めて要求を処理できるか)を分けて公開するのが要点です。

builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "db", tags: ["ready"])
    .AddAzureServiceBusQueue(sbConnection, "orders", tags: ["ready"]);

var app = builder.Build();

// 依存先を確認せず、プロセスの生存だけを返す
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
// "ready" タグの依存先をすべて確認する
app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Contains("ready") });

readiness を依存先の確認と結び付けておくと、データベース接続が確立するまでオーケストレータはそのインスタンスへトラフィックを流しません。起動直後の失敗を利用者に見せずに済みます。

可観測性 — OpenTelemetry によるトレース・メトリクス・ログ

分散したサービスでは、一つのリクエストが複数のサービスとキューを渡り歩きます。障害の原因を追うには、ログを個別に眺めるだけでは足りず、トレース・メトリクス・ログを相関させて全体を俯瞰する必要があります。.NET は OpenTelemetry を一級市民として扱い、ベンダーに依存しない形で計装できます。

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("order-service"))
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter())
    .WithMetrics(m => m
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddOtlpExporter());

// ログも OpenTelemetry へ流し、トレースと相関させる
builder.Logging.AddOpenTelemetry(o =>
{
    o.IncludeScopes = true;
    o.AddOtlpExporter();
});

三つのシグナルの役割は補完的です。トレースはリクエストがサービス間をどう流れ、どこで時間を消費したかを示します。メトリクスはスループットやエラー率、リソース使用量の傾向をダッシュボードで捉えます。ログは特定の事象の詳細を伝えます。OTLP でエクスポートしておけば、送り先は Azure Monitor、Grafana、あるいは Jaeger と Prometheus の組み合わせなど、後から差し替えられます。ビジネス上重要な指標は、次のように独自メーターとして計装しておくと運用の見通しが良くなります。

public sealed class OrderMetrics
{
    private readonly Counter<long> _ordersPlaced;

    public OrderMetrics(IMeterFactory factory)
    {
        var meter = factory.Create("OrderService.Orders");
        _ordersPlaced = meter.CreateCounter<long>("orders.placed");
    }

    public void OrderPlaced(string channel) =>
        _ordersPlaced.Add(1, new KeyValuePair<string, object?>("channel", channel));
}

CI/CD — ビルドから配置までを自動化する

クラウドネイティブの価値は、小さな変更を頻繁かつ安全に届けられる点にあります。そのためにはビルド・テスト・イメージ発行・配置を自動化したパイプラインが欠かせません。次の GitHub Actions は、テストを通し、SDK コンテナビルドでイメージを発行し、レジストリへ押し出すまでの骨格です。

name: build-and-deploy
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # OIDC でクラウドへ認証
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: "10.0.x" }
      - run: dotnet test --configuration Release
      - name: Publish container
        run: dotnet publish -c Release /t:PublishContainer
      - name: Push image
        run: docker push myregistry.azurecr.io/order-service:${{ github.sha }}

実運用では、コミット SHA をイメージタグに使ってどのコードが動いているかを追跡可能にし、レジストリへの認証は長期キーではなく OIDC による短命トークンで行います。配置戦略は、ローリング更新を基本としつつ、影響の大きな変更では新旧を並行稼働させて一部トラフィックだけ新版へ流すカナリアリリースを組み合わせると、問題を早期に検知して切り戻せます。マイグレーションを伴うデータベース変更は、旧版と新版が同居できる後方互換な手順に分けて進めるのが安全です。

.NET Aspire による開発体験の統合

ここまでの構成要素は、それぞれ設定ファイルや接続文字列、起動順序の管理を必要とします。サービスが増えるほど、ローカルで全体を立ち上げるだけでも手間がかかります。.NET Aspire は、この複雑さを開発時に束ねるための仕組みです。アプリケーションの構成を C# のコードとして記述し、依存サービスの起動・接続・可観測性の既定設定をまとめて面倒を見ます。

// AppHost/Program.cs — アプリ全体の構成をコードで宣言
var builder = DistributedApplication.CreateBuilder(args);

var db = builder.AddPostgres("pg").AddDatabase("orders");
var cache = builder.AddRedis("cache");

var api = builder.AddProject<Projects.OrderService>("order-service")
    .WithReference(db)
    .WithReference(cache);

builder.AddProject<Projects.WebFrontend>("web")
    .WithReference(api);

builder.Build().Run();

この宣言から、Aspire はローカルで PostgreSQL と Redis をコンテナとして起動し、接続情報をサービスへ自動注入します。開発中はダッシュボードでトレース・メトリクス・ログを一望でき、先に述べた OpenTelemetry の計装がそのまま活きます。サービスディスカバリと回復性の既定も共有プロジェクト経由で一括適用できるため、各サービスに同じ設定を書き写す必要がありません。

Aspire はあくまで開発時とデプロイ記述のためのモデルであり、本番のオーケストレーションを置き換えるものではありません。同じ構成記述から Kubernetes マニフェストや Azure Container Apps 向けのデプロイを生成できるため、ローカルで確認した構成と本番構成の乖離を小さく保てる点に価値があります。

まとめ

クラウドネイティブな .NET 開発は、小さく安全なコンテナイメージを起点に、オーケストレーションで配置とスケールを任せ、回復性・構成管理・ヘルスチェックでサービス間の不確実性に備え、OpenTelemetry で全体を観測し、CI/CD で変更を安全に届けるという流れで組み立てます。個々の技術は独立して導入できますが、.NET Aspire を軸に据えると、これらを一貫した開発体験としてまとめやすくなります。まずは一つのサービスをコンテナ化してヘルスチェックと可観測性を通し、そこから回復性や自動デプロイへ広げていく段階的な進め方が現実的です。

エンハンスド株式会社では、既存の .NET アプリケーションのコンテナ化やマイクロサービスへの分割、Kubernetes / Azure Container Apps への移行、そして CI/CD と可観測性を含めた運用基盤づくりまでを一貫して支援しています。あわせて、社内チームが自走できるよう .NET Aspire を用いた開発体験の整備やハンズオンを通じた内製化支援にも取り組んでいます。クラウドネイティブ移行の進め方でお悩みの際は、現状の構成を伺ったうえで無理のないロードマップをご提案しますので、お気軽にご相談ください。

この記事をシェア

コピーしました

関連記事