SSH Tunneling 방법(Nest.js)

히반·2023년 5월 14일
0

Local

tunnel-ssh 설치

yarn add tunnel-ssh @types/tunnel-ssh

폴더 구성

main.ts

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';

import { AppModule } from './app.module';
import { sshTunnel } from '../config/sshTunnel';

async function init(): Promise<void> {
  sshTunnel();
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  await app.listen(3030);
}
init().catch((error) => {
  console.error(error);
});

config/config.ts

export const config = {
  ssh: {
    SSH_USERNAME: 'ssh 접속 id',
    SSH_PASSWORD: 'ssh 접속 password',
    SSH_HOST: '공인 ip',
    SSH_PORT: 'ssh 접속 포트',
    SSH_DESTINATION_HOST: '접속할 목표 ip(사설 ip)',
    SSH_DESTINATION_PORT: '접속할 목표 포트',
		//상단의 목표 ip와 포트를 로컬의 해당 ip와 포트에 연결
    SSH_LOCAL_HOST: 'localhost',
    SSH_LOCAL_PORT: 60000,
  },
  database: {
    DB_HOST: 'localhost',
    DB_PORT: 60000,
    DB_USERNAME: 'root',
    DB_PASSWORD: '0000',
    DB_NAME: 'tunneling',
  },
};

config/sshTunnel.ts

import * as tunnel from 'tunnel-ssh';

import { config } from './config';

export function sshTunnel() {
  tunnel(
    {
      username: config.ssh.SSH_USERNAME, // SSH username
      password: config.ssh.SSH_PASSWORD, // SSH password
      host: config.ssh.SSH_HOST, // SSH host
      port: config.ssh.SSH_PORT, // SSH port
      dstHost: config.ssh.SSH_DESTINATION_HOST, // RDS host
      dstPort: config.ssh.SSH_DESTINATION_PORT, // RDS port
      localHost: config.ssh.SSH_LOCAL_HOST, // Local 매핑 host ( ex) 0.0.0.0 -> 모든 Local host 허용)
      localPort: config.ssh.SSH_LOCAL_PORT, // local 매핑 port
    },
    async (error, server) => {
      if (error) {
        throw error;
      }
    },
  );
}

config/typeorm.config.ts

import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { config } from './config';

export const typeORMConfig: TypeOrmModuleOptions = {
  type: 'mariadb',
  host: config.database.DB_HOST,
  port: config.database.DB_PORT,
  username: config.database.DB_USERNAME,
  password: config.database.DB_PASSWORD,
  database: config.database.DB_NAME,
  synchronize: true,
  autoLoadEntities: true,
};

Lambda(Serverless) 사용시 변경점

tunnel-ssh에 대해 종속성을 가진 패키지들을 optimize에서 제거하고 직접 삽입

#serverless.yml
service: paycoq-boiler-plate

plugins:
  - serverless-plugin-optimize

provider:
  name: aws
  runtime: nodejs14.x
  region: ap-northeast-2
  apiGateway:
    binaryMediaTypes:
      - '*/*'

functions:
  main:
    handler: dist/src/lambda.handler
    events:
      - http:
          method: any
          path: /{any+}

package:
  individually: true

custom:
  optimize:
    external:
      [
        'tunnel-ssh',
        'ssh2',
        'asn1',
        'safer-buffer',
        'bcrypt-pbkdf',
        'tweetnacl',
        'lodash.defaults',
      ]
    includePaths:
      [
        'node_modules/tunnel-ssh',
        'node_modules/ssh2',
        'node_modules/asn1',
        'node_modules/safer-buffer',
        'node_modules/bcrypt-pbkdf',
        'node_modules/tweetnacl',
        'node_modules/lodash.defaults',
      ]

lambda.ts

// lambda.ts
import { Handler, Context } from 'aws-lambda';
import { Server } from 'http';
import { createServer, proxy } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';

import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { sshTunnel } from 'config/sshTunnel';

const express = require('express');

// NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely
// due to a compressed response (e.g. gzip) which has not been handled correctly
// by aws-serverless-express and/or API Gateway. Add the necessary MIME types to
// binaryMimeTypes below
const binaryMimeTypes: string[] = [];

let cachedServer: Server;

async function bootstrapServer(): Promise<Server> {
  if (!cachedServer) {
    const expressApp = express();
    const nestApp = await NestFactory.create(
      AppModule,
      new ExpressAdapter(expressApp),
    );
    nestApp.use(eventContext());

    //swagger config
    const config = new DocumentBuilder()
      .setTitle('[제목]')
      .setDescription('[설명]')
      // .addServer('/dev')
      .setVersion('1.0')
      .build();
    const document = SwaggerModule.createDocument(nestApp, config);
    SwaggerModule.setup('/api-docs', nestApp, document);

    //class validator config
    nestApp.useGlobalPipes(new ValidationPipe({ transform: true }));

    nestApp.use(helmet());
    nestApp.enableCors();

    await nestApp.init();
    cachedServer = createServer(expressApp, undefined, binaryMimeTypes);
  }
  return cachedServer;
}

const insertAt = (str: string | any[], sub: string, pos: number) =>
  `${str.slice(0, pos)}${sub}${str.slice(pos)}`;

export const handler: Handler = async (event: any, context: Context) => {
  cachedServer = await bootstrapServer();

  if (event.path === '/test/api-docs') {
    event.path = '/test/api-docs/';
  }

  event.path = event.path.includes('swagger-ui')
    ? insertAt(`${event.path}`, `/api-docs`, `/test/api-docs`.length)
    : event.path;

  return proxy(cachedServer, event, context, 'PROMISE').promise;
};

sshTunnel(); //여기

핸들러 안에 sshTunnel()을 넣을 경우 이미 열려 있는 포트라는 에러와 함께 인스턴스가 죽고 다시 호출하면 실행되는 성공-실패-성공-실패의 패턴이 나타남

lambda의 경우 호출마다 핸들러를 실행함

sshTunnel()을 동기함수로 바꾸고 핸들러 밖으로 꺼냄

0개의 댓글

관련 채용 정보