次の方法で共有


クイック スタート: TypeSpec と .NET を使用して新しい API プロジェクトを作成する

このクイック スタートでは、TypeSpec を使用して RESTful API アプリケーションを設計、生成、実装する方法について説明します。 TypeSpec は、クラウド サービス API を記述するためのオープンソース言語であり、複数のプラットフォーム用のクライアントコードとサーバー コードを生成します。 このクイック スタートに従って、API コントラクトを 1 回定義し、一貫した実装を生成する方法について説明します。これにより、保守性が高く、文書化された API サービスを構築できます。

このクイック スタートでは次の作業を行います。

  • TypeSpec を使用して API を定義する
  • API サーバー アプリケーションを作成する
  • Azure Cosmos DB を永続ストレージに統合する
  • API をローカルで実行してテストする
  • Azure Container Apps へのデプロイ

Prerequisites

TypeSpec を使用した開発

TypeSpec は、言語に依存しない方法で API を定義し、複数のプラットフォーム用の API サーバーとクライアント ライブラリを生成します。 この機能を利用すると、次のことが可能になります。

  • API コントラクトを 1 回定義する
  • 一貫性のあるサーバーとクライアント のコードを生成する
  • API インフラストラクチャではなくビジネス ロジックの実装に重点を置く

TypeSpec は API サービス管理を提供します。

  • API 定義言語
  • API 用のサーバー側ルーティング ミドルウェア
  • API を使用するためのクライアント ライブラリ

クライアント要求とサーバー統合を指定します。

  • データベース、ストレージ、メッセージング用の Azure サービスなどのミドルウェアにビジネス ロジックを実装する
  • API のホスティング サーバー (ローカルまたは Azure)
  • 繰り返し可能なプロビジョニングとデプロイのためのスクリプト

新しい TypeSpec アプリケーションを作成する

  1. API サーバーと TypeSpec ファイルを保持する新しいフォルダーを作成します。

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. TypeSpec コンパイラをグローバルにインストールします。

    npm install -g @typespec/compiler
    
  3. TypeSpec が正しくインストールされていることを確認します。

    tsp --version
    
  4. TypeSpec プロジェクトを初期化します。

    tsp init
    
  5. 次のプロンプトには提供された答えで回答してください。

    • ここで新しいプロジェクトを初期化しますか? Y
    • プロジェクト テンプレートを選択しますか? 汎用 REST API
    • プロジェクト名を入力します:ウィジェット
    • どのエミッターを使用しますか?
      • OpenAPI 3.1 ドキュメント
      • C# サーバースタブ

    TypeSpec エミッタ ーは、さまざまな TypeSpec コンパイラ API を利用して TypeSpec コンパイル プロセスを反映し、成果物を生成するライブラリです。

  6. 初期化が完了するまで待ってから続行します。

    Run tsp compile . to build the project.
    
    Please review the following messages from emitters:
      @typespec/http-server-csharp: 
    
            Generated ASP.Net services require dotnet 9:
            https://dotnet.microsoft.com/download 
    
            Create an ASP.Net service project for your TypeSpec:
            > npx hscs-scaffold . --use-swaggerui --overwrite
    
            More information on getting started:
            https://aka.ms/tsp/hscs/start
    
  7. プロジェクトをコンパイルします。

    tsp compile .
    
  8. TypeSpec は、 ./tsp-outputで既定のプロジェクトを生成し、2 つの個別のフォルダーを作成します。

    • スキーマ
    • サーバー
  9. ./tsp-output/schema/openapi.yaml ファイルを開きます。 ./main.tspの数行が、あなたのために200行を超えるOpenApi仕様を生成していることに注意してください。

  10. ./tsp-output/server/aspnet フォルダーを開きます。 スキャフォールディングされた .NET ファイルには次のものが含まれます。

    • ./generated/operations/IWidgets.cs は Widgets メソッドのインターフェイスを定義します。
    • ./generated/controllers/WidgetsController.cs は Widgets メソッドへの統合を実装します。 ここで、ビジネス ロジックを配置します。
    • ./generated/models は、Widget API のモデルを定義します。

TypeSpec エミッタを構成する

TypeSpec ファイルを使用して、API サーバーの生成を構成します。

  1. tsconfig.yamlを開き、既存の構成を次の YAML に置き換えます。

    emit:
      - "@typespec/openapi3"
      - "@typespec/http-server-csharp"
    options:
      "@typespec/openapi3":
        emitter-output-dir: "{cwd}/server/wwwroot"
        openapi-versions:
          - 3.1.0
      "@typespec/http-server-csharp":
        emitter-output-dir: "{cwd}/server/"
        use-swaggerui: true
        overwrite: true
        emit-mocks: "mocks-and-project-files"
    

    この構成では、完全に生成された .NET API サーバーに必要ないくつかの変更が投影されます。

    • emit-mocks: サーバーに必要なすべてのプロジェクト ファイルを作成します。
    • use-swaggerui: Swagger UI を統合して、ブラウザーに優しい方法で API を使用できるようにします。
    • emitter-output-dir: サーバー生成と OpenApi 仕様生成の両方の出力ディレクトリを設定します。
    • すべてを ./serverに生成します。
  2. プロジェクトを再コンパイルします。

    tsp compile .
    
  3. 新しい /server ディレクトリに移動します。

    cd server
    
  4. 既定の開発者証明書がまだない場合は作成します。

    dotnet dev-certs https
    
  5. プロジェクトを実行します。

    dotnet run
    

    通知が ブラウザーで開くのを待ちます

  6. ブラウザーを開き、Swagger UI ルート ( /swagger) を追加します。

    Widgets API を使用した Swagger UI を示すスクリーンショット。

  7. 既定の TypeSpec API とサーバーはどちらも機能します。

アプリケーション ファイルの構造を理解する

生成されたサーバーのプロジェクト構造には、.NET コントローラー ベースの API サーバー、プロジェクトをビルドするための .NET ファイル、および Azure 統合用のミドルウェアが含まれます。

├── appsettings.Development.json
├── appsettings.json
├── docs
├── generated
├── mocks
├── Program.cs
├── Properties
├── README.md
├── ServiceProject.csproj
└── wwwroot
  • ビジネス ロジックを追加します。この例では、 ./server/mocks/Widget.cs ファイルから始めます。 生成された Widget.cs は定型メソッドを提供します。
  • サーバーを更新します。特定のサーバー構成を ./program.csに追加します。
  • OpenApi 仕様を使用します。TypeSpec は、OpenApi3.json ファイルを ./server/wwwroot ファイルに生成し、開発中に Swagger UI で使用できるようにしました。 これにより、仕様の UI が提供されます。 REST クライアントや Web フロントエンドなどの要求メカニズムを提供しなくても、API を操作できます。

永続化を Azure Cosmos DB no-sql に変更する

基本的な Widget API サーバーが動作するように、永続的なデータ ストア用に Azure Cosmos DB と連携するようにサーバーを更新します。

  1. ./server ディレクトリで、Azure Cosmos DB をプロジェクトに追加します。

    dotnet add package Microsoft.Azure.Cosmos
    
  2. Azure に対して認証する Azure ID ライブラリを追加します。

    dotnet add package Azure.Identity
    
  3. Cosmos DB 統合設定の ./server/ServiceProject.csproj を更新します。

    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
        ... existing settings ...
        <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
      </PropertyGroup>
      <ItemGroup>
        ... existing settings ...
        <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
      </ItemGroup>
    </Project>
    
    • EnableSdkContainerSupport を使用すると、Dockerfile を記述することなく、.NET SDK の組み込みのコンテナー ビルド サポート (dotnet publish –-container) を使用できます。
    • Newtonsoft.Json は、Cosmos DB SDK が JSON との間で .NET オブジェクトを変換するために使用する Json .NET シリアライザーを追加します。
  4. 新しい登録ファイルを作成 ./azure/CosmosDbRegistration 、Cosmos DB の登録を管理します。

    using Microsoft.Azure.Cosmos;
    using Microsoft.Extensions.Configuration;
    using System;
    using System.Threading.Tasks;
    using Azure.Identity;
    using DemoService;
    
    namespace WidgetService.Service
    {
        /// <summary>
        /// Registration class for Azure Cosmos DB services and implementations
        /// </summary>
        public static class CosmosDbRegistration
        {
            /// <summary>
            /// Registers the Cosmos DB client and related services for dependency injection
            /// </summary>
            /// <param name="builder">The web application builder</param>
            public static void RegisterCosmosServices(this WebApplicationBuilder builder)
            {
                // Register the HttpContextAccessor for accessing the HTTP context
                builder.Services.AddHttpContextAccessor();
    
                // Get configuration settings
                var cosmosEndpoint = builder.Configuration["Configuration:AzureCosmosDb:Endpoint"];
    
                // Validate configuration
                ValidateCosmosDbConfiguration(cosmosEndpoint);
    
                // Configure Cosmos DB client options
                var cosmosClientOptions = new CosmosClientOptions
                {
                    SerializerOptions = new CosmosSerializationOptions
                    {
                        PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
                    },
                    ConnectionMode = ConnectionMode.Direct
                };
    
                builder.Services.AddSingleton(serviceProvider =>
                {
                    var credential = new DefaultAzureCredential();
    
                    // Create Cosmos client with token credential authentication
                    return new CosmosClient(cosmosEndpoint, credential, cosmosClientOptions);
                });
    
                // Initialize Cosmos DB if needed
                builder.Services.AddHostedService<CosmosDbInitializer>();
    
                // Register WidgetsCosmos implementation of IWidgets
                builder.Services.AddScoped<IWidgets, WidgetsCosmos>();
            }
    
            /// <summary>
            /// Validates the Cosmos DB configuration settings
            /// </summary>
            /// <param name="cosmosEndpoint">The Cosmos DB endpoint</param>
            /// <exception cref="ArgumentException">Thrown when configuration is invalid</exception>
            private static void ValidateCosmosDbConfiguration(string cosmosEndpoint)
            {
                if (string.IsNullOrEmpty(cosmosEndpoint))
                {
                    throw new ArgumentException("Cosmos DB Endpoint must be specified in configuration");
                }
            }
        }
    }
    

    エンドポイントの環境変数に注目してください。

    var cosmosEndpoint = builder.Configuration["Configuration:AzureCosmosDb:Endpoint"];
    
  5. 永続的なストア用に Azure Cosmos DB と統合するビジネス ロジックを提供する ./azure/WidgetsCosmos.cs 、新しい Widget クラスを作成します。

    using System;
    using System.Net;
    using System.Threading.Tasks;
    using Microsoft.Azure.Cosmos;
    using Microsoft.Extensions.Logging;
    using System.Collections.Generic;
    using System.Linq;
    
    // Use generated models and operations
    using DemoService;
    
    namespace WidgetService.Service
    {
        /// <summary>
        /// Implementation of the IWidgets interface that uses Azure Cosmos DB for persistence
        /// </summary>
        public class WidgetsCosmos : IWidgets
        {
            private readonly CosmosClient _cosmosClient;
            private readonly ILogger<WidgetsCosmos> _logger;
            private readonly IHttpContextAccessor _httpContextAccessor;
            private readonly string _databaseName = "WidgetDb";
            private readonly string _containerName = "Widgets";
    
            /// <summary>
            /// Initializes a new instance of the WidgetsCosmos class.
            /// </summary>
            /// <param name="cosmosClient">The Cosmos DB client instance</param>
            /// <param name="logger">Logger for diagnostic information</param>
            /// <param name="httpContextAccessor">Accessor for the HTTP context</param>
            public WidgetsCosmos(
                CosmosClient cosmosClient,
                ILogger<WidgetsCosmos> logger,
                IHttpContextAccessor httpContextAccessor)
            {
                _cosmosClient = cosmosClient;
                _logger = logger;
                _httpContextAccessor = httpContextAccessor;
            }
    
            /// <summary>
            /// Gets a reference to the Cosmos DB container for widgets
            /// </summary>
            private Container WidgetsContainer => _cosmosClient.GetContainer(_databaseName, _containerName);
    
            /// <summary>
            /// Lists all widgets in the database
            /// </summary>
            /// <returns>Array of Widget objects</returns>
            public async Task<WidgetList> ListAsync()
            {
                try
                {
                    var queryDefinition = new QueryDefinition("SELECT * FROM c");
                    var widgets = new List<Widget>();
    
                    using var iterator = WidgetsContainer.GetItemQueryIterator<Widget>(queryDefinition);
                    while (iterator.HasMoreResults)
                    {
                        var response = await iterator.ReadNextAsync();
                        widgets.AddRange(response.ToList());
                    }
    
                    // Create and return a WidgetList instead of Widget[]
                    return new WidgetList
                    {
                        Items = widgets.ToArray()
                    };
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error listing widgets from Cosmos DB");
                    throw new Error(500, "Failed to retrieve widgets from database");
                }
            }
    
            /// <summary>
            /// Retrieves a specific widget by ID
            /// </summary>
            /// <param name="id">The ID of the widget to retrieve</param>
            /// <returns>The retrieved Widget</returns>
            public async Task<Widget> ReadAsync(string id)
            {
                try
                {
                    var response = await WidgetsContainer.ReadItemAsync<Widget>(
                        id, new PartitionKey(id));
    
                    return response.Resource;
                }
                catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                {
                    _logger.LogWarning("Widget with ID {WidgetId} not found", id);
                    throw new Error(404, $"Widget with ID '{id}' not found");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error reading widget {WidgetId} from Cosmos DB", id);
                    throw new Error(500, "Failed to retrieve widget from database");
                }
            }
            /// <summary>
            /// Creates a new widget from the provided Widget object
            /// </summary>
            /// <param name="body">The Widget object to store in the database</param>
            /// <returns>The created Widget</returns>
            public async Task<Widget> CreateAsync(Widget body)
            {
                try
                {
                    // Validate the Widget
                    if (body == null)
                    {
                        throw new Error(400, "Widget data cannot be null");
                    }
    
                    if (string.IsNullOrEmpty(body.Id))
                    {
                        throw new Error(400, "Widget must have an Id");
                    }
    
                    if (body.Color != "red" && body.Color != "blue")
                    {
                        throw new Error(400, "Color must be 'red' or 'blue'");
                    }
    
                    // Save the widget to Cosmos DB
                    var response = await WidgetsContainer.CreateItemAsync(
                        body, new PartitionKey(body.Id));
    
                    _logger.LogInformation("Created widget with ID {WidgetId}", body.Id);
                    return response.Resource;
                }
                catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
                {
                    _logger.LogError(ex, "Widget with ID {WidgetId} already exists", body.Id);
                    throw new Error(409, $"Widget with ID '{body.Id}' already exists");
                }
                catch (Exception ex) when (!(ex is Error))
                {
                    _logger.LogError(ex, "Error creating widget in Cosmos DB");
                    throw new Error(500, "Failed to create widget in database");
                }
            }
    
            /// <summary>
            /// Updates an existing widget with properties specified in the patch document
            /// </summary>
            /// <param name="id">The ID of the widget to update</param>
            /// <param name="body">The WidgetMergePatchUpdate object containing properties to update</param>
            /// <returns>The updated Widget</returns>
            public async Task<Widget> UpdateAsync(string id, TypeSpec.Http.WidgetMergePatchUpdate body)
            {
                try
                {
                    // Validate input parameters
                    if (body == null)
                    {
                        throw new Error(400, "Update data cannot be null");
                    }
    
                    if (body.Color != null && body.Color != "red" && body.Color != "blue")
                    {
                        throw new Error(400, "Color must be 'red' or 'blue'");
                    }
    
                    // First check if the item exists
                    Widget existingWidget;
                    try
                    {
                        var response = await WidgetsContainer.ReadItemAsync<Widget>(
                            id, new PartitionKey(id));
                        existingWidget = response.Resource;
                    }
                    catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                    {
                        _logger.LogWarning("Widget with ID {WidgetId} not found for update", id);
                        throw new Error(404, $"Widget with ID '{id}' not found");
                    }
    
                    // Apply the patch updates only where properties are provided
                    bool hasChanges = false;
    
                    if (body.Weight.HasValue)
                    {
                        existingWidget.Weight = body.Weight.Value;
                        hasChanges = true;
                    }
    
                    if (body.Color != null)
                    {
                        existingWidget.Color = body.Color;
                        hasChanges = true;
                    }
    
                    // Only perform the update if changes were made
                    if (hasChanges)
                    {
                        // Use ReplaceItemAsync for the update
                        var updateResponse = await WidgetsContainer.ReplaceItemAsync(
                            existingWidget, id, new PartitionKey(id));
    
                        _logger.LogInformation("Updated widget with ID {WidgetId}", id);
                        return updateResponse.Resource;
                    }
    
                    // If no changes, return the existing widget
                    _logger.LogInformation("No changes to apply for widget with ID {WidgetId}", id);
                    return existingWidget;
                }
                catch (Error)
                {
                    // Rethrow Error exceptions
                    throw;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error updating widget {WidgetId} in Cosmos DB", id);
                    throw new Error(500, "Failed to update widget in database");
                }
            }
    
            /// <summary>
            /// Deletes a widget by its ID
            /// </summary>
            /// <param name="id">The ID of the widget to delete</param>
            public async Task DeleteAsync(string id)
            {
                try
                {
                    await WidgetsContainer.DeleteItemAsync<Widget>(id, new PartitionKey(id));
                    _logger.LogInformation("Deleted widget with ID {WidgetId}", id);
                }
                catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                {
                    _logger.LogWarning("Widget with ID {WidgetId} not found for deletion", id);
                    throw new Error(404, $"Widget with ID '{id}' not found");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error deleting widget {WidgetId} from Cosmos DB", id);
                    throw new Error(500, "Failed to delete widget from database");
                }
            }
    
            /// <summary>
            /// Analyzes a widget by ID and returns a simplified analysis result
            /// </summary>
            /// <param name="id">The ID of the widget to analyze</param>
            /// <returns>An AnalyzeResult containing the analysis of the widget</returns>
            public async Task<AnalyzeResult> AnalyzeAsync(string id)
            {
                try
                {
                    // First retrieve the widget from the database
                    Widget widget;
                    try
                    {
                        var response = await WidgetsContainer.ReadItemAsync<Widget>(
                            id, new PartitionKey(id));
                        widget = response.Resource;
                    }
                    catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                    {
                        _logger.LogWarning("Widget with ID {WidgetId} not found for analysis", id);
                        throw new Error(404, $"Widget with ID '{id}' not found");
                    }
    
                    // Create the analysis result
                    var result = new AnalyzeResult
                    {
                        Id = widget.Id,
                        Analysis = $"Weight: {widget.Weight}, Color: {widget.Color}"
                    };
    
                    _logger.LogInformation("Analyzed widget with ID {WidgetId}", id);
                    return result;
                }
                catch (Error)
                {
                    // Rethrow Error exceptions
                    throw;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error analyzing widget {WidgetId} from Cosmos DB", id);
                    throw new Error(500, "Failed to analyze widget from database");
                }
            }
        }
    }
    
  6. Azure に対して認証する ./server/services/CosmosDbInitializer.cs ファイルを作成します。

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Azure.Cosmos;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    
    namespace WidgetService.Service
    {
        /// <summary>
        /// Hosted service that initializes Cosmos DB resources on application startup
        /// </summary>
        public class CosmosDbInitializer : IHostedService
        {
            private readonly CosmosClient _cosmosClient;
            private readonly ILogger<CosmosDbInitializer> _logger;
            private readonly IConfiguration _configuration;
            private readonly string _databaseName;
            private readonly string _containerName = "Widgets";
    
            public CosmosDbInitializer(CosmosClient cosmosClient, ILogger<CosmosDbInitializer> logger, IConfiguration configuration)
            {
                _cosmosClient = cosmosClient;
                _logger = logger;
                _configuration = configuration;
                _databaseName = _configuration["CosmosDb:DatabaseName"] ?? "WidgetDb";
            }
    
            public async Task StartAsync(CancellationToken cancellationToken)
            {
                _logger.LogInformation("Ensuring Cosmos DB database and container exist...");
    
                try
                {
                    // Create database if it doesn't exist
                    var databaseResponse = await _cosmosClient.CreateDatabaseIfNotExistsAsync(
                        _databaseName,
                        cancellationToken: cancellationToken);
    
                    _logger.LogInformation("Database {DatabaseName} status: {Status}", _databaseName,
                        databaseResponse.StatusCode == System.Net.HttpStatusCode.Created ? "Created" : "Already exists");
    
                    // Create container if it doesn't exist (using id as partition key)
                    var containerResponse = await databaseResponse.Database.CreateContainerIfNotExistsAsync(
                        new ContainerProperties
                        {
                            Id = _containerName,
                            PartitionKeyPath = "/id"
                        },
                        throughput: 400, // Minimum RU/s
                        cancellationToken: cancellationToken);
    
                    _logger.LogInformation("Container {ContainerName} status: {Status}", _containerName,
                        containerResponse.StatusCode == System.Net.HttpStatusCode.Created ? "Created" : "Already exists");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error initializing Cosmos DB");
                    throw;
                }
            }
    
            public Task StopAsync(CancellationToken cancellationToken)
            {
                return Task.CompletedTask;
            }
        }
    }
    
  7. Cosmos DB を使用するように ./server/program.cs を更新し、運用環境のデプロイで Swagger UI を使用できるようにします。 ファイル全体をコピーします。

    // Generated by @typespec/http-server-csharp
    // <auto-generated />
    #nullable enable
    
    using TypeSpec.Helpers;
    using WidgetService.Service;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddControllersWithViews(options =>
    {
        options.Filters.Add<HttpServiceExceptionFilter>();
    });
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    // Replace original registration with the Cosmos DB one
    CosmosDbRegistration.RegisterCosmosServices(builder);
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    // Swagger UI is always available
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.DocumentTitle = "TypeSpec Generated OpenAPI Viewer";
        c.SwaggerEndpoint("/openapi.yaml", "TypeSpec Generated OpenAPI Docs");
        c.RoutePrefix = "swagger";
    });
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.Use(async (context, next) =>
    {
        context.Request.EnableBuffering();
        await next();
    });
    
    app.MapGet("/openapi.yaml", async (HttpContext context) =>
    {
        var externalFilePath = "wwwroot/openapi.yaml"; 
        if (!File.Exists(externalFilePath))
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            await context.Response.WriteAsync("OpenAPI spec not found.");
            return;
        }
        context.Response.ContentType = "application/json";
        await context.Response.SendFileAsync(externalFilePath);
    });
    
    app.UseRouting();
    app.UseAuthorization();
    
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    
    app.Run();
    
  8. プロジェクトをビルドします。

    dotnet build
    

    これで、プロジェクトは Cosmos DB 統合でビルドされます。 Azure リソースを作成してプロジェクトをデプロイするデプロイ スクリプトを作成しましょう。

デプロイ インフラストラクチャを作成する

Azure Developer CLI と Bicep テンプレートを使用して、繰り返し可能なデプロイを行うために必要なファイルを作成します。

  1. TypeSpec プロジェクトのルートで、 azure.yaml 配置定義ファイルを作成し、次のソースに貼り付けます。

    # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
    
    name: azure-typespec-scaffold-dotnet
    metadata:
        template: azd-init@1.14.0
    services:
        api:
            project: ./server
            host: containerapp
            language: dotnet
    pipeline:
      provider: github
    

    この構成は、生成されたプロジェクトの場所 (./server) を参照していることに注意してください。 ./tspconfig.yamlが、./azure.yamlで指定された場所と一致していることを確認します。

  2. TypeSpec プロジェクトのルートで、 ./infra ディレクトリを作成します。

  3. ./infra/main.bicepparam ファイルを作成し、次のようにコピーして、デプロイに必要なパラメーターを定義します。

    using './main.bicep'
    
    param environmentName = readEnvironmentVariable('AZURE_ENV_NAME', 'dev')
    param location = readEnvironmentVariable('AZURE_LOCATION', 'eastus2')
    param deploymentUserPrincipalId = readEnvironmentVariable('AZURE_PRINCIPAL_ID', '')
    

    このパラメーター リストには、このデプロイに必要な最小パラメーターが用意されています。

  4. ./infra/main.bicep ファイルを作成し、次の内容をコピーして、プロビジョニングとデプロイ用の Azure リソースを定義します。

    metadata description = 'Bicep template for deploying a GitHub App using Azure Container Apps and Azure Container Registry.'
    
    targetScope = 'resourceGroup'
    param serviceName string = 'api'
    var databaseName = 'WidgetDb'
    var containerName = 'Widgets'
    
    @minLength(1)
    @maxLength(64)
    @description('Name of the environment that can be used as part of naming resource convention')
    param environmentName string
    
    @minLength(1)
    @description('Primary location for all resources')
    param location string
    
    @description('Id of the principal to assign database and application roles.')
    param deploymentUserPrincipalId string = ''
    
    var resourceToken = toLower(uniqueString(resourceGroup().id, environmentName, location))
    
    var tags = {
      'azd-env-name': environmentName
      repo: 'https://github.com/typespec'
    }
    
    module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
      name: 'user-assigned-identity'
      params: {
        name: 'identity-${resourceToken}'
        location: location
        tags: tags
      }
    }
    
    module cosmosDb 'br/public:avm/res/document-db/database-account:0.8.1' = {
      name: 'cosmos-db-account'
      params: {
        name: 'cosmos-db-nosql-${resourceToken}'
        location: location
        locations: [
          {
            failoverPriority: 0
            locationName: location
            isZoneRedundant: false
          }
        ]
        tags: tags
        disableKeyBasedMetadataWriteAccess: true
        disableLocalAuth: true
        networkRestrictions: {
          publicNetworkAccess: 'Enabled'
          ipRules: []
          virtualNetworkRules: []
        }
        capabilitiesToAdd: [
          'EnableServerless'
        ]
        sqlRoleDefinitions: [
          {
            name: 'nosql-data-plane-contributor'
            dataAction: [
              'Microsoft.DocumentDB/databaseAccounts/readMetadata'
              'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'
              'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*'
            ]
          }
        ]
        sqlRoleAssignmentsPrincipalIds: union(
          [
            managedIdentity.outputs.principalId
          ],
          !empty(deploymentUserPrincipalId) ? [deploymentUserPrincipalId] : []
        )
        sqlDatabases: [
          {
            name: databaseName
            containers: [
              {
                name: containerName
                paths: [
                  '/id'
                ]
              }
            ]
          }
        ]
      }
    }
    
    module containerRegistry 'br/public:avm/res/container-registry/registry:0.5.1' = {
      name: 'container-registry'
      params: {
        name: 'containerreg${resourceToken}'
        location: location
        tags: tags
        acrAdminUserEnabled: false
        anonymousPullEnabled: true
        publicNetworkAccess: 'Enabled'
        acrSku: 'Standard'
      }
    }
    
    var containerRegistryRole = subscriptionResourceId(
      'Microsoft.Authorization/roleDefinitions',
      '8311e382-0749-4cb8-b61a-304f252e45ec'
    ) 
    
    module registryUserAssignment 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.1' = if (!empty(deploymentUserPrincipalId)) {
      name: 'container-registry-role-assignment-push-user'
      params: {
        principalId: deploymentUserPrincipalId
        resourceId: containerRegistry.outputs.resourceId
        roleDefinitionId: containerRegistryRole
      }
    }
    
    module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = {
      name: 'log-analytics-workspace'
      params: {
        name: 'log-analytics-${resourceToken}'
        location: location
        tags: tags
      }
    }
    
    module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.8.0' = {
      name: 'container-apps-env'
      params: {
        name: 'container-env-${resourceToken}'
        location: location
        tags: tags
        logAnalyticsWorkspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
        zoneRedundant: false
      }
    }
    
    module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = {
      name: 'container-apps-app'
      params: {
        name: 'container-app-${resourceToken}'
        environmentResourceId: containerAppsEnvironment.outputs.resourceId
        location: location
        tags: union(tags, { 'azd-service-name': serviceName })
        ingressTargetPort: 8080
        ingressExternal: true
        ingressTransport: 'auto'
        stickySessionsAffinity: 'sticky'
        scaleMaxReplicas: 1
        scaleMinReplicas: 1
        corsPolicy: {
          allowCredentials: true
          allowedOrigins: [
            '*'
          ]
        }
        managedIdentities: {
          systemAssigned: false
          userAssignedResourceIds: [
            managedIdentity.outputs.resourceId
          ]
        }
        secrets: {
          secureList: [
            {
              name: 'azure-cosmos-db-nosql-endpoint'
              value: cosmosDb.outputs.endpoint
            }
            {
              name: 'user-assigned-managed-identity-client-id'
              value: managedIdentity.outputs.clientId
            }
          ]
        }
        containers: [
          {
            image: 'mcr.microsoft.com/dotnet/samples:aspnetapp-9.0'
            name: serviceName
            resources: {
              cpu: '0.25'
              memory: '.5Gi'
            }
            env: [
              {
                name: 'CONFIGURATION__AZURECOSMOSDB__ENDPOINT'
                secretRef: 'azure-cosmos-db-nosql-endpoint'
              }
              {
                name: 'AZURE_CLIENT_ID'
                secretRef: 'user-assigned-managed-identity-client-id'
              }
            ]
          }
        ]
      }
    }
    
    output CONFIGURATION__AZURECOSMOSDB__ENDPOINT string = cosmosDb.outputs.endpoint
    output CONFIGURATION__AZURECOSMOSDB__DATABASENAME string = databaseName
    output CONFIGURATION__AZURECOSMOSDB__CONTAINERNAME string = containerName
    
    output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
    

    出力変数を使用すると、プロビジョニングされたクラウド リソースをローカル開発で使用できます。

  5. containerAppsApp タグは、serviceName 変数 (ファイルの先頭でapiに設定) と、apiで指定された./azure.yamlを使用します。 この接続により、.NET プロジェクトを Azure Container Apps ホスティング リソースにデプロイする場所が Azure Developer CLI に指示されます。

    ...bicep...
    
    module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = {
      name: 'container-apps-app'
      params: {
        name: 'container-app-${resourceToken}'
        environmentResourceId: containerAppsEnvironment.outputs.resourceId
        location: location
        tags: union(tags, { 'azd-service-name': serviceName })                    <--------- `API`
    
    ...bicep..
    

Project structure

最終的なプロジェクト構造には、TypeSpec API ファイル、Express.js サーバー、Azure デプロイ ファイルが含まれます。

├── infra
├── tsp-output
├── .gitignore
├── .azure.yaml
├── Dockerfile
├── main.tsp
├── package-lock.json
├── package.json
├── tspconfig.yaml
Area Files/Directories
TypeSpec main.tsptspconfig.yaml
Express.js server ./tsp-output/server/ ( controllers/models/ServiceProject.csprojなどの生成されたファイルが含まれます)
Azure Developer CLI のデプロイ ./azure.yaml./infra/

Azure にアプリケーションをデプロイする

このアプリケーションは、Azure Container Apps を使用して Azure にデプロイできます。

  1. Azure Developer CLI に対する認証:

    azd auth login
    
  2. Azure Developer CLI を使用して Azure Container Apps にデプロイします。

    azd up
    

ブラウザーでアプリケーションを使用する

デプロイ後、次のことができます。

  1. Swagger UI にアクセスして、 /swaggerで API をテストします。
  2. API を使用してウィジェットを作成、読み取り、更新、削除するには、各 API で 今すぐ試 す機能を使用します。

アプリケーションを拡張する

エンドツーエンドのプロセス全体が機能したら、引き続き API をビルドします。

  • typeSpec 言語の詳細を確認し、./main.tspに API と API レイヤーの機能を追加します。
  • エミッタを追加し、./tspconfig.yamlでそのパラメータを設定します。
  • TypeSpec ファイルにさらに機能を追加する場合は、サーバー プロジェクトのソース コードでそれらの変更をサポートします。
  • Azure ID で パスワードレス認証 を引き続き使用します。

リソースをクリーンアップする

このクイック スタートが完了したら、Azure リソースを削除できます。

azd down

または、Azure portal から直接リソース グループを削除します。

Next steps