on
[MS-2] React & Nest 기반 애플리케이션 및 Micro Service 통신 설정
[MS-2] React & Nest 기반 애플리케이션 및 Micro Service 통신 설정
React와 Nest를 기반으로 마이크로 서비스를 구축해 본다. 개발 환경은 Nx를 사용한다. Nx 환경구축은 [React HH-2] 글을 참조한다.
Application 생성
4개의 카테고리에 각각 api, web 애플리케이션을 생성한다.
gateway, dashboard, configuration, back-office api, web
Step-1) 사전 준비환경 설치
글로벌 환경 설치를 한다. nvm 설치는 [React HH-2] 글을 참조한다.
$> npm i -g @angular/cli@latest $> npm i -g @nrwl/cli@latest $> npm i -g yarn@latest
node 버전은 nvm을 사용하여 14.17.5 를 사용한다. nvm 버전을 자동으로 설정하고 싶다면
1) .zshrc파일에 다음 내용을 첨부한다.
2) 작업 폴더에 .nvmrc 파일을 만든다.
// .zshrc autoload -U add-zsh-hook load-nvmrc() { local node_version="$(nvm version)" local nvmrc_path="$(nvm_find_nvmrc)" if [ -n "$nvmrc_path" ]; then local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")") if [ "$nvmrc_node_version" = "N/A" ]; then nvm install elif [ "$nvmrc_node_version" != "$node_version" ]; then nvm use fi elif [ "$node_version" != "$(nvm version default)" ]; then echo "Reverting to nvm default version" nvm use default fi } add-zsh-hook chpwd load-nvmrc load-nvmrc // 작업 폴더에 .nvmrc 파일을 생성하고, 버전을 기입힌다. 아래 내용 참조
Step-2) NX 개발환경 생성
create-nx-workspace@latest를 이용하고, .nvmrc 파일을 생성한다. .zshrc가 적용되고, rnm-stack 폴더에 진입하면 자동으로 14.17.5 버전이 적용된다. rnm은 React Nest Micro-service의 약어이다.
$> npx create-nx-workspace@latest ✔ Workspace name (e.g., org name) · rnm-stack ✔ What to create in the new workspace · react ✔ Application name · gateway/web ✔ Default stylesheet format · scss ✔ Use Nx Cloud? (It's free and doesn't require registration.) · No $> cd rnm-stack $> echo "14.17.5" > .nvmrc
다음은 api 애플리케이션을 Nest기반으로 생성한다.
// nest 제너레이터 설치 $> yarn add -D @nrwl/nest@latest // api 생성 $> nx g @nrwl/nest:app gateway/api
다음으로 루트폴더에 있는 workspace.json 파일을 각 api와 web폴더에 project.json 파일을 생성하여 설정을 옮긴다. 이후 생성되는 애플리케이션들은 자동으로 project.json 파일로 생성된다.
// workspace.json { "version": 2, "projects": { "gateway-api": "apps/gateway/api", <== project.json 파일의 위치 지정 "gateway-web": "apps/gateway/web", "gateway/web-e2e": "apps/gateway/web-e2e" }, ... 생략 ... "defaultProject": "gateway-web" }
project.json 파일들
테스트 "nx serve gateway-web" 을 수행하면 정상적으로 dev server가 수행된다.
$> nx serve gateway-web > nx run gateway-web:serve > NX Web Development Server is listening at http://localhost:4200/
나머지 dashboard, configuration, back-office도 생성한다.
// react application $> nx g @nrwl/react:app dashboard/web $> nx g @nrwl/react:app configuration/web $> nx g @nrwl/react:app back-office/web // nest application $> nx g @nrwl/nest:app dashboard/api $> nx g @nrwl/nest:app configuration/api $> nx g @nrwl/nest:app back-office/api
전체 생성 애플리케이션과 workspace.json 내역
Libraries 생성
다음으로 libs 폴더 하위로 라이브러리들을 생성한다. @rnm/page, @rnm/ui, @rnm/domain, @rnm/shared 로 import하여 사용한다.
page
ui
domain
shared
$> nx g @nrwl/react:lib page --importPath=@rnm/page --publishable $> nx g @nrwl/react:lib ui --importPath=@rnm/ui --publishable $> nx g @nrwl/react:lib domain --importPath=@rnm/domain --publishable $> nx g @nrwl/react:lib shared --importPath=@rnm/shared --publishable
libs 폴더밑으로 4개 라이브러리 생성
Micro Service Communication Channel 설정
다음은 애플리케이션간의 연결을 위한 통신부분을 설정한다. NestJS 기반으로 microservice 생성을 위해 패키지를 설치한다. NestJS버전을 7.6.18 버전으로 업데이트 한다. (현재 8.*은 microservice 버그가 있다. 작동불능)
$> yarn add @nestjs/microservices // package.json 에서 nestjs의 모든 패키지의 버전을 업데이트 한다. "dependencies": { "@nestjs/common": "^7.6.18", <== 요기 "@nestjs/core": "^7.6.18", <== 요기 "@nestjs/microservices": "^7.6.18", <== 요기 "@nestjs/platform-express": "^7.6.18", <== 요기 "@nestjs/serve-static": "^2.2.2", "core-js": "^3.6.5", "react": "17.0.2", "react-dom": "17.0.2", "react-router-dom": "5.2.0", "reflect-metadata": "^0.1.13", "regenerator-runtime": "0.13.7", "rxjs": "~6.6.3", "tslib": "^2.0.0" }, "devDependencies": { "@nestjs/schematics": "^7.3.1", <== 요기, 7최신이 7.3.1 "@nestjs/testing": "^7.6.18", <== 요기 ... } // nestjs 모든 패키지의 업데이트 버전을 재설치한다. $> yarn
Step-1) Micro Service 설정
dashboard, configuration, back-office 3가지 micro service http server & static server dashboard: 8001, configuration: 8002, back-office: 8002 HTTP port를 사용한다. api server dashboard: 8100, configuration: 8200, back-office: 8300 TCP port를 사용한다.
3가지 micro service
apps/dashboard/api/src 하위로 public 폴더를 하위로 dashboard 폴더를 추가로 생성한 후 config.json 파일을 생성한다. 그리고 apps/dashboard/api/src/assets 폴더를 public/dashboard 폴더밑으로 옮긴다.
public/dashboard가 context path이다
libs/shared/src/lib 밑에 configuration 폴더를 생성하고, config.service.ts 와 config.model.ts을 생성한다. config.json 파일을 읽기위한 용도이다. index.ts에 export도 설정한다.
// config.service.ts import * as fs from "fs"; import { join } from 'path'; import { GatewayConfiguration, MicroServiceConfiguration } from "./config.model"; export const loadMicroServiceConfiguration = (message = '[LOAD] config.json file'): MicroServiceConfiguration => { console.log(`${message}:`, `${__dirname}/public/config.json`); const jsonFile = fs.readFileSync(join(__dirname, 'public', 'config.json'), 'utf8'); return JSON.parse(jsonFile); } export const loadGatewayConfiguration = (message = '[LOAD] config.json file'): GatewayConfiguration => { console.log(`${message}:`, `${__dirname}/public/config.json`); const jsonFile = fs.readFileSync(join(__dirname, 'public', 'config.json'), 'utf8'); return JSON.parse(jsonFile); } // config.model.ts export interface MicroServiceConfiguration { REVERSE_CONTEXT?: string; REVERSE_ADDRESS?: string; HTTP_PORT?: number, TCP_HOST?: string; TCP_PORT?: number, GLOBAL_API_PREFIX?: string; } export interface GatewayConfiguration { HTTP_PORT?: number, GLOBAL_API_PREFIX?: string; DASHBOARD?: MicroServiceConfiguration; CONFIGURATION?: MicroServiceConfiguration; BACK_OFFICE?: MicroServiceConfiguration; } // index.ts export * from './lib/configuration/config.model'; export * from './lib/configuration/config.service';
apps/dashboard/api/src/main.ts 파일을 수정한다. http server를 위한 listen port를 설정하고, microservice client가 연결토록 TCP로 연결 준비를 한다. loadMicroServiceCofiguration 함수를 "@rnm/shared" 패키지로 부터 위치 투명하게 (마치 node_modules에 설치된 것처럼) import 할 수 있다.
import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { Transport, MicroserviceOptions } from '@nestjs/microservices'; import { loadMicroServiceConfiguration } from '@rnm/shared'; import { AppModule } from './app/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Load config.json file const config = loadMicroServiceConfiguration(); // // Setup tcp server for api const options: MicroserviceOptions = { transport: Transport.TCP, options: { host: config.TCP_HOST, port: config.TCP_PORT || 8100 } }; app.connectMicroservice(options); app.startAllMicroservices(); // // Setup http server for web const httpPort = config.HTTP_PORT || process.env.HTTP_PORT || 8001; const globalPrefix = config.GLOBAL_API_PREFIX || '/api'; app.setGlobalPrefix(globalPrefix); await app.listen(httpPort, () => { Logger.log(`Listening at http://localhost:${httpPort}${globalPrefix}`); }); } bootstrap();
확인 & 테스트
// LISTEN 포트 확인 $> netstat -an |grep 8100 tcp4 0 0 *.8100 *.* LISTEN $> netstat -an |grep 8001 tcp46 0 0 *.8001 *.* LISTEN // gateway 실행 $> nx serve dashboard-api // other console $> curl -X GET "http://localhost:8001/api" {"message":"Welcome to dashboard/api in libs"}
configuration, back-office도 동일하게 적용한다.
Step-2) Micro Service의 HTTP port가 static file 서비스토록 설정
@nestjs/serve-static (v2.2.2) 패키지를 설치한다. (참조)
$> yarn add @nestjs/serve-static
apps/dashboard/api/src/app/app.module.ts 에 static 환경을 설정한다. rootPath는 public 폴더를 만든다. exclude는 global prefix인 api를 제외한다. public 폴더에 index.html 을 만들어 본다. 향후 react기반 SPA 번들파일을 위치할 것이다.
import { Module } from '@nestjs/common'; import { ServeStaticModule } from '@nestjs/serve-static'; import { join } from 'path'; import { AppService } from '@rnm/domain'; import { AppController } from './app.controller'; @Module({ imports: [ ServeStaticModule.forRoot({ rootPath: join(__dirname, 'public'), exclude: ['/api*'], }), ], controllers: [AppController], providers: [AppService], }) export class AppModule { } // index.html dashboard home
재실행 후, dashboard context path로 브라우져에서 확인한다. public/dashboard/assets/images 밑으로 샘플 이미지 복사
// public/dashboard/index.html 내역 welcome to dashboard!
Step-3) Micro Service의 응답을 위한 TCP MessagePattern 데코레이터 생성
apps/dashboard/api/src/app/app.controller.ts 에서 TCP 요청에 대한 응답 코드를 작성한다. TCP는 MessagePattern 데코레이터를 통해 응답하고 반환은 Primitive type, Promise, Observable 다 가능하다. 만일 Observable로 하면 gateway 측에서 Observable로 대응할 수 있다.
{ cmd: 'dashboard-sum' } 은 gateway로부터 오는 Key이다.
import { Controller, Get } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { DashboardApiAppService } from '@rnm/domain'; import { from, Observable } from 'rxjs'; @Controller() export class AppController { constructor(private readonly appService: DashboardApiAppService) { } @Get() getData() { return this.appService.getData(); } @MessagePattern({ cmd: 'dashboard-sum' }) accumulate(data: number[]): Observable { console.log('calling sum from dashboard....'); const sum = data[0] + data[1] + data[2]; return from([sum]); } }
테스트는 Gateway까지 설정후 진행한다.
Step-4) Gateway 설정
gateway http server & static server reverse proxy api client
config.json 파일을 apps/gateway/api/src/public 폴더밑으로 생성하고 하기와 같이 설정한다.
{ "HTTP_PORT": 8000, "DASHBOARD": { "HTTP_PORT": 8001, "TCP_HOST": "0.0.0.0", "TCP_PORT": 8100, "GLOBAL_API_PREFIX": "/api" }, "CONFIGURATION": { "HTTP_PORT": 8002, "TCP_HOST": "0.0.0.0", "TCP_PORT": 8200, "GLOBAL_API_PREFIX": "/api" }, "BACK_OFFICE": { "HTTP_PORT": 8003, "TCP_HOST": "0.0.0.0", "TCP_PORT": 8300, "GLOBAL_API_PREFIX": "/api" } }
apps/gateway/api/src/main.ts는 http server를 설정한다.
global prefix를 설정하지 않고, 각 controller에서 설정한다.
gateway api 호출은 "api/gateway"
dashboard api 호출은 "api/dashboard". 다른 microservice도 동일하다.
// gateway의 main.ts import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { loadGatewayConfiguration } from '@rnm/shared'; import { AppModule } from './app/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Load config.json file const config = loadGatewayConfiguration(); const httpPort = config.HTTP_PORT || process.env.HTTP_PORT || 8000; await app.listen(httpPort, () => { Logger.log(`Listening at http://localhost:${httpPort}`); }); } bootstrap(); // app.controller.ts @Controller('api/gateway') <== 요기 export class AppController { ... } // dashboard.controller.ts @Controller('api/dashboard') <== 요기 export class DashboardController { ... }
apps/gateway/api/src/app/app.module.ts에서는 static file 서비스 폴더를 지정한다. reverse proxy를 위해 아래 3개의 exclude를 추가한다. web context처럼 사용될 것이다.
dashboard
configuration
back-office
import { Module } from '@nestjs/common'; import { ServeStaticModule } from '@nestjs/serve-static'; import { join } from 'path'; import { GatewayApiAppService } from '@rnm/domain'; import { AppController } from './app.controller'; import { DashboardModule } from './dashboard/dashboard.module'; @Module({ imports: [ ServeStaticModule.forRoot({ rootPath: join(__dirname, 'public'), exclude: [ '/api/gateway*', '/api/dashboard*', '/dashboard*', '/configuration*', '/back-office*' ], }), DashboardModule ], controllers: [AppController], providers: [GatewayApiAppService] }) export class AppModule { }
Step-5) Gateway에 Micro Service쪽으로 reverse proxy 설정하기
micro service 인 dashboard, configuration, back-office는 자신의 http web server로써 static file을 서비스하고, tcp를 통해 api를 전송한다. 개발시에는 gateway를 구동하지 않고, micro service만 실행한다면 http를 통해 api 서비스 추가하면 될 것이다.
Gateway에서 reverse proxy되는 3개 context
http-proxy-middleware를 설치한다.
$> yarn add http-proxy-middleware
다음으로 apps/gateway/api/src/app 폴더 밑으로 dashboard 폴더를 생성하고, dashboard용 controller, service, module과 dashboard.proxy.ts 파일들을 생성한다.
NestMiddleware를 상속받아 use를 구현한다.
// dashboard.proxy.ts /* eslint-disable @typescript-eslint/no-explicit-any */ import { NestMiddleware, Logger } from '@nestjs/common'; import { loadGatewayConfiguration } from '@rnm/shared'; import { createProxyMiddleware } from 'http-proxy-middleware'; export class DashboardReverseProxyMiddleware implements NestMiddleware { private config = loadGatewayConfiguration(); private proxyOptions = { target: this.config.DASHBOARD.REVERSE_ADDRESS, secure: false, onProxyReq: (proxyReq: any, req: any, res: any) => { Logger.debug(`[DashboardReverseProxyMiddleware]: Proxying ${req.method} request originally made to '${req.url}'`); }, }; private proxy: any = createProxyMiddleware(this.proxyOptions); use(req: any, res: any, next: () => void) { this.proxy(req, res, next); } }
dashboard.module.ts에 다음과 같이 dashboard에 TCP로 접근하는 Client설정을 한다. 그리고 ProxyMiddleware를 설정한다.
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { loadGatewayConfiguration } from '@rnm/shared'; import { DashboardController } from './dashboard.controller'; import { DashboardReverseProxyMiddleware } from './dashboard.proxy'; import { DashboardService } from './dashboard.service'; const config = loadGatewayConfiguration(); @Module({ imports: [ // dashboard쪽 api 호출을 위한 TCP Client Proxy 등록 ClientsModule.register([ { name: "DASHBOARD", transport: Transport.TCP, options: { host: config.DASHBOARD.TCP_HOST, port: config.DASHBOARD.TCP_PORT } } ]) ], controllers: [DashboardController], providers: [DashboardService], }) export class DashboardModule implements NestModule { // middleware 적용 configure(consumer: MiddlewareConsumer) { consumer .apply(DashboardReverseProxyMiddleware) .forRoutes({ path: config.DASHBOARD.REVERSE_CONTEXT, method: RequestMethod.ALL }); } }
전체 코드 준비는 되었고, 다음 글에서는 MS Code에서 Debugging을 통해 위의 코드를 테스트해 본다.
소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-2
<참조>
- 마이크로 서비스 설정 예제
https://github.com/danmt/microservices-basics
- reverse proxy 설정 예제
https://github.com/baumgarb/reverse-proxy-demo
- http-proxy-middleware 패키지
https://github.com/chimurai/http-proxy-middleware
from http://mobicon.tistory.com/586 by ccl(A) rewrite - 2021-09-23 18:00:41