다음을 통해 공유


빠른 시작: TypeSpec 및 TypeScript를 사용하여 새 API 프로젝트 만들기

이 빠른 시작에서는 TypeSpec을 사용하여 RESTful TypeScript API 애플리케이션을 디자인, 생성 및 구현하는 방법을 알아봅니다. TypeSpec은 클라우드 서비스 API를 설명하기 위한 오픈 소스 언어이며 여러 플랫폼에 대한 클라이언트 및 서버 코드를 생성합니다. 이 빠른 시작에 따라 API 계약을 한 번 정의하고 일관된 구현을 생성하여 유지 관리가 가능하고 잘 문서화된 API 서비스를 빌드하는 방법을 알아봅니다.

이 빠른 시작에서 당신은 다음을 수행합니다.

  • TypeSpec을 사용하여 API 정의
  • API 서버 애플리케이션 만들기
  • 영구 스토리지에 Azure Cosmos DB 통합
  • Azure에 배포
  • API 실행 및 테스트

Important

@typespec/http-server-js 이미터는 현재 미리 보기로 제공됩니다. 이 정보는 릴리스되기 전에 상당히 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보와 관련하여 명시적이거나 묵시적인 보증을 하지 않습니다.

Prerequisites

TypeSpec을 사용하여 개발

TypeSpec은 언어에 구애받지 않은 방식으로 API를 정의하고 여러 플랫폼에 대한 API 서버 및 클라이언트 라이브러리를 생성합니다. 이 기능을 통해 다음을 수행할 수 있습니다.

  • API 계약을 한 번 정의
  • 일관된 서버 및 클라이언트 코드 생성
  • 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 문서
      • JavaScript 서버 스텁

    TypeSpec 발생기는 다양한 TypeSpec 컴파일러 API를 활용하여 TypeSpec 컴파일 프로세스를 고찰하고 아티팩트를 생성하는 라이브러리입니다.

  6. 계속하기 전에 초기화가 완료되기를 기다립니다.

  7. 프로젝트를 컴파일합니다.

    tsp compile .
    
  8. TypeSpec은 두 개의 개별 폴더를 만드는 기본 ./tsp-output프로젝트를 생성합니다.

    • 스키마 는 OpenApi 3 사양입니다. ./main.tsp의 몇 줄이 여러분을 위해 200줄이 넘는 OpenApi 사양을 생성한 것을 주목하세요.
    • 서버 는 생성된 미들웨어입니다. 이 미들웨어는 Node.js 서버 프로젝트에 통합할 수 있습니다.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts 는 위젯 API에 대한 인터페이스를 정의합니다.
      • ./tsp-output/js/src/generated/http/openapi3.ts 는 Open API 사양을 TypeScript 파일로 정의하고 TypeSpec 프로젝트를 컴파일할 때마다 다시 생성됩니다.

TypeSpec 방출기 구성

TypeSpec 파일을 사용하여 전체 Express.js 서버를 스캐폴드하도록 API 서버 생성을 구성합니다.

  1. 기존 구성을 ./tsconfig.yaml 열고 다음 YAML로 바꿉다.

    emit:
      - "@typespec/openapi3"
      - "@typespec/http-server-js"
    options:
      "@typespec/openapi3":
        emitter-output-dir: "{output-dir}/server/schema"
        openapi-versions:
          - 3.1.0
      "@typespec/http-server-js":
        emitter-output-dir: "{output-dir}/server"
        express: true
    

    이 구성은 전체 Express.js API 서버를 만듭니다.

    • express: Swagger UI를 포함하여 Express.js API 서버를 생성합니다.
    • emitter-output-dir: 모든 항목을 ./server 디렉터리에 생성합니다.
  2. 기존 ./tsp-output를 삭제하십시오. 걱정하지 마세요. 다음 단계에서 서버를 생성합니다.

  3. TypeSpec JavaScript 방출기를 사용하여 Express.js 서버를 만듭니다.

    npx hsjs-scaffold
    
  4. ./tsp-output/server 디렉터리로 변경합니다.

    cd ./tsp-output/server
    
  5. TypeScript를 JavaScript로 컴파일합니다.

    tsc
    
  6. 프로젝트를 실행합니다.

    npm start
    

    브라우저에서 알림이 열릴 때까지 기다립니다.

  7. 브라우저를 열고 http://localhost:3000/.api-docs 웹 사이트로 이동합니다.

    위젯 API용 Swagger UI를 표시하는 브라우저의 스크린샷

  8. 기본 TypeSpec API와 서버가 모두 작동합니다. 이 API 서버를 완성하려면 비즈니스 논리를 추가하여 위젯 API ./tsp-output/server/src/controllers/widgets.ts를 지원해야 합니다. UI는 하드 코딩된 가짜 데이터를 반환하는 API에 연결됩니다.

애플리케이션 파일 구조 이해

Express.js 프로젝트 구조 tsp-output/server/ 에는 생성된 서버, package.json및 Azure 통합을 위한 미들웨어가 포함됩니다.

server
├── package.json
├── package-lock.json
├── src
│   ├── controllers
│   │   └── widgets.ts
│   ├── generated
│   │   ├── helpers
│   │   │   ├── datetime.ts
│   │   │   ├── header.ts
│   │   │   ├── http.ts
│   │   │   ├── multipart.ts
│   │   │   ├── router.ts
│   │   │   └── temporal
│   │   │       ├── native.ts
│   │   │       └── polyfill.ts
│   │   ├── http
│   │   │   ├── openapi3.ts
│   │   │   ├── operations
│   │   │   │   └── server-raw.ts
│   │   │   └── router.ts
│   │   └── models
│   │       └── all
│   │           ├── demo-service.ts
│   │           └── typespec.ts
│   ├── index.ts
│   └── swagger-ui.ts

부모 TypeSpec 프로젝트의 파일 구조에는 다음 Express.js 프로젝트가 포함됩니다.tsp-output

├── tsp-output
├── .gitignore
├── main.tsp
├── package-lock.json
├── package.json
├── tspconfig.yaml

Azure Cosmos DB no-sql로 지속성 변경

이제 기본 Express.js API 서버가 작동하므로 영구 데이터 저장소에 대한 Azure Cosmos DB 와 함께 작동하도록 Express.js 서버를 업데이트합니다. 여기에는 미들웨어에서 index.ts Cosmos DB 통합을 사용하도록 변경된 내용이 포함됩니다. 모든 변경 내용은 ./tsp-output/server/src/generated 디렉터리 외부에서 일어나야 합니다.

  1. ./tsp-output/server 디렉터리에서 프로젝트에 Azure Cosmos DB를 추가합니다.

    npm install @azure/cosmos
    
  2. Azure ID 라이브러리를 추가하여 Azure에 인증합니다.

    npm install @azure/identity
    
  3. Azure와 ./tsp-output/server/src/azure 관련된 소스 코드를 저장할 디렉터리를 만듭니다.

  4. cosmosClient.ts 해당 디렉터리에 파일을 만들어 Cosmos DB 클라이언트 개체를 만들고 다음 코드에 붙여넣습니다.

    import { CosmosClient, Database, Container } from "@azure/cosmos";
    import { DefaultAzureCredential } from "@azure/identity";
    
    /**
     * Interface for CosmosDB configuration settings
     */
    export interface CosmosConfig {
      endpoint: string;
      databaseId: string;
      containerId: string;
      partitionKey: string;
    } 
    
    /**
     * Singleton class for managing CosmosDB connections
     */
    export class CosmosClientManager {
      private static instance: CosmosClientManager;
      private client: CosmosClient | null = null;
      private config: CosmosConfig | null = null;
    
      private constructor() {}
    
      /**
       * Get the singleton instance of CosmosClientManager
       */
      public static getInstance(): CosmosClientManager {
        if (!CosmosClientManager.instance) {
          CosmosClientManager.instance = new CosmosClientManager();
        }
        return CosmosClientManager.instance;
      }
    
      /**
       * Initialize the CosmosDB client with configuration if not already initialized
       * @param config CosmosDB configuration
       */
      private ensureInitialized(config: CosmosConfig): void {
        if (!this.client || !this.config) {
          this.config = config;
          this.client = new CosmosClient({
            endpoint: config.endpoint,
            aadCredentials: new DefaultAzureCredential(),
          });
        }
      }
    
      /**
       * Get a database instance, creating it if it doesn't exist
       * @param config CosmosDB configuration
       * @returns Database instance
       */
      private async getDatabase(config: CosmosConfig): Promise<Database> {
        this.ensureInitialized(config);
        const { database } = await this.client!.databases.createIfNotExists({ id: config.databaseId });
        return database;
      }
    
      /**
       * Get a container instance, creating it if it doesn't exist
       * @param config CosmosDB configuration
       * @returns Container instance
       */
      public async getContainer(config: CosmosConfig): Promise<Container> {
        const database = await this.getDatabase(config);
        const { container } = await database.containers.createIfNotExists({
          id: config.containerId,
          partitionKey: { paths: [config.partitionKey] }
        });
        return container;
      }
    
      /**
       * Clean up resources and close connections
       */
      public dispose(): void {
        this.client = null;
        this.config = null;
      }
    }
    
    export const buildError = (error: any, message: string) => {
      const statusCode = error?.statusCode || 500;
      return {
        code: statusCode,
        message: `${message}: ${error?.message || 'Unknown error'}`
      };
    };
    

    파일은 엔드포인트, 데이터베이스 및 컨테이너를 사용합니다. Azure ID 자격 증명 DefaultAzureCredential을 사용하므로 연결 문자열이나 키가 필요하지 않습니다. 로컬 및 프로덕션 환경 모두에 대해 이 보안 인증 방법에 대해 자세히 알아봅니다.

  5. 새 위젯 컨트롤러 ./tsp-output/server/src/controllers/WidgetsCosmos.ts를 만들고 Azure Cosmos DB에 대한 다음 통합 코드를 붙여넣습니다.

    import { Widgets, Widget, WidgetList,   AnalyzeResult,Error } from "../generated/models/all/demo-service.js";
    import { WidgetMergePatchUpdate } from "../generated/models/all/typespec/http.js";
    import { CosmosClientManager, CosmosConfig, buildError } from "../azure/cosmosClient.js";
    import { HttpContext } from "../generated/helpers/router.js";
    import { Container } from "@azure/cosmos";
    
    export interface WidgetDocument extends Widget {
      _ts?: number;
      _etag?: string;
    }
    
    /**
     * Implementation of the Widgets API using Azure Cosmos DB for storage
     */
    export class WidgetsCosmosController implements Widgets<HttpContext>  {
      private readonly cosmosConfig: CosmosConfig;
      private readonly cosmosManager: CosmosClientManager;
      private container: Container | null = null;
    
      /**
       * Creates a new instance of WidgetsCosmosController
       * @param azureCosmosEndpoint Cosmos DB endpoint URL
       * @param databaseId The Cosmos DB database ID
       * @param containerId The Cosmos DB container ID
       * @param partitionKey The partition key path
       */
      constructor(azureCosmosEndpoint: string, databaseId: string, containerId: string, partitionKey: string) {
        if (!azureCosmosEndpoint) throw new Error("azureCosmosEndpoint is required");
        if (!databaseId) throw new Error("databaseId is required");
        if (!containerId) throw new Error("containerId is required");
        if (!partitionKey) throw new Error("partitionKey is required");
    
        this.cosmosConfig = {
          endpoint: azureCosmosEndpoint,
          databaseId: databaseId,
          containerId: containerId,
          partitionKey: partitionKey
        };
    
        this.cosmosManager = CosmosClientManager.getInstance();
      }
    
      /**
       * Get the container reference, with caching
       * @returns The Cosmos container instance
       */
      private async getContainer(): Promise<Container | null> {
        if (!this.container) {
          try {
            this.container = await this.cosmosManager.getContainer(this.cosmosConfig);
            return this.container;
          } catch (error: any) {
            console.error("Container initialization error:", error);
            throw buildError(error, `Failed to access container ${this.cosmosConfig.containerId}`);
          }
        }
        return this.container;
      }
    
      /**
       * Create a new widget
       * @param widget The widget to create
       * @returns The created widget with assigned ID
       */
      async create(ctx: HttpContext,
        body: Widget
      ): Promise<Widget | Error> {
    
        const id = body.id;
    
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          if (!body.id) {
            return buildError({statusCode:400}, "Widget ID is required");
          }
    
          const response = await container.items.create<Widget>(body, { 
            disableAutomaticIdGeneration: true 
          });
    
          if (!response.resource) {
            return buildError({statusCode:500}, `Failed to create widget ${body.id}: No resource returned`);
          }
    
          return this.documentToWidget(response.resource);
        } catch (error: any) {
          if (error?.statusCode === 409) {
            return buildError({statusCode:409}, `Widget with id ${id} already exists`);
          }
          return buildError(error, `Failed to create widget ${id}`);
        }
      }
    
      /**
       * Delete a widget by ID
       * @param id The ID of the widget to delete
       */
      async delete(ctx: HttpContext, id: string): Promise<void | Error> {
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          await container.item(id, id).delete();
        } catch (error: any) {
          if (error?.statusCode === 404) {
            return buildError({statusCode:404}, `Widget with id ${id} not found`);
          }
          return buildError(error, `Failed to delete widget ${id}`);
        }
      }
    
      /**
       * Get a widget by ID
       * @param id The ID of the widget to retrieve
       * @returns The widget if found
       */
      async read(ctx: HttpContext, id: string): Promise<Widget | Error> {
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          const { resource } = await container.item(id, id).read<WidgetDocument>();
    
          if (!resource) {
            return buildError({statusCode:404}, `Widget with id ${id} not found`);
          }
    
          return this.documentToWidget(resource);
        } catch (error: any) {
          return buildError(error, `Failed to read widget ${id}`);
        }
      }
    
      /**
       * List all widgets with optional paging
       * @returns List of widgets
       */
      async list(ctx: HttpContext): Promise<WidgetList | Error> {
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          const { resources } = await container.items
            .query({ query: "SELECT * FROM c" })
            .fetchAll();
    
          return { items: resources.map(this.documentToWidget) };
        } catch (error: any) {
          return buildError(error, "Failed to list widgets");
        }
      }
    
      /**
       * Update an existing widget
       * @param id The ID of the widget to update
       * @param body The partial widget data to update
       * @returns The updated widget
       */
      async update(
        ctx: HttpContext,
        id: string,
        body: WidgetMergePatchUpdate,
      ): Promise<Widget | Error> {
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          // First check if the widget exists
          const { resource: item } = await container.item(id).read<WidgetDocument>();
          if (!item) {
            return buildError({statusCode:404}, `Widget with id ${id} not found`);
          }
    
          // Apply patch updates to the existing widget
          const updatedWidget: Widget = {
            ...item,
            ...body,
            id
          };
    
          // Replace the document in Cosmos DB
          const { resource } = await container.item(id).replace(updatedWidget);
    
          if (!resource) {
            return buildError({statusCode:500}, `Failed to update widget ${id}: No resource returned`);
          }
    
          return this.documentToWidget(resource);
        } catch (error: any) {
          return buildError(error, `Failed to update widget ${id}`);
        }
      }
    
      async analyze(ctx: HttpContext, id: string): Promise<AnalyzeResult | Error> {
        return {
          id: "mock-string",
          analysis: "mock-string",
        };
      }
    
      /**
       * Convert a Cosmos DB document to a Widget
       */
      private documentToWidget(doc: WidgetDocument): Widget {
        return Object.fromEntries(
          Object.entries(doc).filter(([key]) => !key.startsWith('_'))
        ) as Widget;
      }
    }
    
  6. ./tsp-output/server/src/index.ts 새 컨트롤러를 가져오도록 업데이트하고, Azure Cosmos DB 환경 설정을 가져온 다음, WidgetsCosmosController를 만들고 라우터에 전달합니다.

    // Generated by Microsoft TypeSpec
    
    import { WidgetsCosmosController } from "./controllers/WidgetsCosmos.js";
    
    import { createDemoServiceRouter } from "./generated/http/router.js";
    
    import express from "express";
    
    import morgan from "morgan";
    
    import { addSwaggerUi } from "./swagger-ui.js";
    
    const azureCosmosEndpoint = process.env.AZURE_COSMOS_ENDPOINT!;
    const azureCosmosDatabase = "WidgetDb";
    const azureCosmosContainer = "Widgets";
    const azureCosmosPartitionKey = "/Id";
    
    const router = createDemoServiceRouter(
      new WidgetsCosmosController(
        azureCosmosEndpoint, 
        azureCosmosDatabase, 
        azureCosmosContainer, 
        azureCosmosPartitionKey)
    );
    const PORT = process.env.PORT || 3000;
    
    const app = express();
    
    app.use(morgan("dev"));
    
    const SWAGGER_UI_PATH = process.env.SWAGGER_UI_PATH || "/.api-docs";
    
    addSwaggerUi(SWAGGER_UI_PATH, app);
    
    app.use(router.expressMiddleware);
    
    app.listen(PORT, () => {
      console.log(`Server is running at http://localhost:${PORT}`);
      console.log(
        `API documentation is available at http://localhost:${PORT}${SWAGGER_UI_PATH}`,
      );
    });
    
  7. 사용 중인 터미널에서 ./tsp-output/server의 TypeScript를 JavaScript로 컴파일합니다.

    tsc
    

    이제 프로젝트는 Cosmos DB 통합을 사용하여 빌드됩니다. Azure 리소스를 만들고 프로젝트를 배포하는 배포 스크립트를 만들어 보겠습니다.

배포 인프라 만들기

Azure Developer CLIBicep 템플릿을 사용하여 반복 가능한 배포에 필요한 파일을 만듭니다.

  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-js
    metadata:
        template: azd-init@1.14.0
    services:
        api:
            project: ./
            host: containerapp
            language: js
            docker:
                path: Dockerfile
    pipeline:
      provider: github
    hooks:
      postprovision:
        windows:
          shell: pwsh
          run: |
            # Set environment variables for the Container App
            azd env set AZURE_COSMOS_ENDPOINT "$env:AZURE_COSMOS_ENDPOINT"
          continueOnError: false
          interactive: true
        posix:
          shell: sh
          run: |
            # Set environment variables for the Container App
            azd env set AZURE_COSMOS_ENDPOINT "$AZURE_COSMOS_ENDPOINT"
          continueOnError: false
          interactive: true
    

    이 구성은 전체 TypeSpec 프로젝트를 참조합니다.

  2. TypeSpec 프로젝트의 루트에서 Azure Container Apps에 사용할 컨테이너를 구성하는 ./Dockerfile를 만듭니다.

    # Stage 1: Build stage
    FROM node:20-alpine AS builder
    
    WORKDIR /app
    
    # Install TypeScript globally
    RUN npm install -g typescript
    
    # Copy package files first to leverage Docker layer caching
    COPY package*.json ./
    
    # Create the tsp-output/server directory structure
    RUN mkdir -p tsp-output/server
    
    # Copy server package.json 
    COPY tsp-output/server/package.json ./tsp-output/server/
    
    # Install build and dev dependencies
    RUN npm i --force --no-package-lock
    RUN cd tsp-output/server && npm install
    
    # Copy the rest of the application code
    COPY . .
    
    # Build the TypeScript code
    RUN cd tsp-output/server && tsc
    
    #---------------------------------------------------------------
    
    # Stage 2: Runtime stage
    FROM node:20-alpine AS runtime
    
    # Set NODE_ENV to production for better performance
    ENV NODE_ENV=production
    
    WORKDIR /app
    
    # Copy only the server package files
    COPY tsp-output/server/package.json ./
    
    # Install only production dependencies
    RUN npm install
    
    # Copy all necessary files from the builder stage
    # This includes the compiled JavaScript, any static assets, etc.
    COPY --from=builder /app/tsp-output/server/dist ./dist
    
    # Set default port and expose it
    ENV PORT=3000
    EXPOSE 3000
    
    # Run the application
    CMD ["node", "./dist/src/index.js"]
    
  3. TypeSpec 프로젝트의 루트에서 ./infra 디렉터리를 만듭니다.

  4. ./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', '')
    

    이 매개 변수 목록은 이 배포에 필요한 최소 매개 변수를 제공합니다.

  5. ./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'
    var partitionKey = '/id'
    
    @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: [
                  partitionKey
                ]
              }
            ]
          }
        ]
      }
    }
    
    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: 3000
        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/devcontainers/typescript-node'
            name: serviceName
            resources: {
              cpu: '0.25'
              memory: '.5Gi'
            }
            env: [
              {
                name: 'AZURE_COSMOS_ENDPOINT'
                secretRef: 'azure-cosmos-db-nosql-endpoint'
              }
              {
                name: 'AZURE_CLIENT_ID'
                secretRef: 'user-assigned-managed-identity-client-id'
              }
            ]
          }
        ]
      }
    }
    
    output AZURE_COSMOS_ENDPOINT string = cosmosDb.outputs.endpoint
    output AZURE_COSMOS_DATABASE string = databaseName
    output AZURE_COSMOS_CONTAINER string = containerName
    output AZURE_COSMOS_PARTITION_KEY string = partitionKey
    
    output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
    output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name
    

    OUTPUT 변수를 사용하면 로컬 개발과 함께 프로비전된 클라우드 리소스를 사용할 수 있습니다.

Azure에 애플리케이션 배포

Azure Container Apps를 사용하여 Azure에 이 애플리케이션을 배포할 수 있습니다.

  1. 프로젝트의 루트에 있는 터미널에서 Azure 개발자 CLI에 인증합니다.

    azd auth login
    
  2. Azure 개발자 CLI를 사용하여 Azure Container Apps에 배포:

    azd up
    
  3. 제공된 답변으로 다음 프롬프트에 응답합니다.

    • 고유한 환경 이름을 입력합니다. tsp-server-js
    • 사용할 Azure 구독 선택: 구독 선택
    • 사용할 Azure 위치 선택: 가까운 위치 선택
    • 사용할 리소스 그룹 선택: 새 리소스 그룹 만들기 선택
    • 새 리소스 그룹의 이름을 입력합니다. 제공된 기본값을 적용합니다.
  4. 배포가 완료될 때까지 기다립니다. 응답에는 다음과 유사한 정보가 포함됩니다.

    Deploying services (azd deploy)
    
      (✓) Done: Deploying service api
      - Endpoint: https://container-app-123.ambitiouscliff-456.centralus.azurecontainerapps.io/
    
    
    SUCCESS: Your up workflow to provision and deploy to Azure completed in 6 minutes 32 seconds.
    

브라우저에서 애플리케이션 사용

배포되면 다음을 수행할 수 있습니다.

  1. 콘솔에서 URL을 Endpoint 선택하여 브라우저에서 엽니다.
  2. Swagger UI를 사용하기 위해 엔드포인트에 경로를 /.api-docs추가합니다.
  3. 이제 각 메서드에서 Try it 기능을 사용하여 API를 통해 위젯을 만들고, 읽고, 업데이트하고, 삭제합니다.

애플리케이션 확장

이제 전체 엔드-엔드 프로세스가 작동했으므로 API를 계속 빌드합니다.

  • 에 API 및 API 계층 기능을 더 추가하려면 TypeSpec 언어 에 대해 자세히 알아봅니다 ./main.tsp.
  • 방출기를 더 추가하고 해당 매개변수를 ./tspconfig.yaml에서 구성합니다.
  • TypeSpec 파일에 추가 기능을 추가하면 서버 프로젝트의 소스 코드를 사용하여 이러한 변경 내용을 지원합니다.
  • Azure ID에서 암호 없는 인증 을 계속 사용합니다.

자원을 정리하세요

이 빠른 시작을 완료하면 Azure 리소스를 제거할 수 있습니다.

azd down

또는 Azure Portal에서 직접 리소스 그룹을 삭제합니다.

다음 단계