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,
};
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()을 동기함수로 바꾸고 핸들러 밖으로 꺼냄