Skip to content

[BE] ssh터널링으로 db연결

SeongHyeon edited this page Nov 8, 2024 · 4 revisions

로컬환경에서 ssh 터널링을 통해 private DB서버에 연결하기

image

로컬  웹서버  db서버

db서버는 private subnet으로 외부에서 접속이 불가능하기때문에 로컬에서 db서버에 접속을 하기 위해서는 ssh터널링을 통해 접속해야 한다.

왜 터널링을 해야하나요????

  • SSH 터널링을 사용하여 로컬 호스트를 통해 원격 서버의 데이터베이스에 연결하면, 로컬 시스템이 마치 원격 서버처럼 작동하는 것처럼 취급됩니다.
  • 이 방법은 개발 환경에서 원격 데이터베이스에 접근할 때 보안과 편의성을 동시에 제공하며, 데이터베이스 연결을 단순화합니다.

터널링이-모에여

//ssh-server에 들어가서 원격 서버 명령어가 실행이 가능합니다.
//주의할 점은 3306포트를 db서버에서 이미 사용중이라 앞 뒤를 똑같이 3306을 사용하게 될 경우 충돌이 발생 할 수 있습니다.
ssh -L 3307:remote-db-host:3306 user@ssh-server

//ssh-server에 터널링만 진행하고 원격 서버 명령어를 실행하지 않도록 해서 아무 반응이 없습니다.
ssh -L 3307:remote-db-host:3306 -N user@ssh-server

//이제 터널링을 했으면 접속을 해봅시다.
//mysql -h (로컬 호스트) -P (접속 포트) -u (사용자 이름) -p
//SSH 터널링을 통해 로컬에서 포트 3307으로 원격 MySQL 서버에 접근하고, corinee 사용자로 로그인하려는 명령어입니다.
mysql -h 127.0.0.1 -P 3307 -u corinee -p
const connection = mysql.createConnection({
  host: '127.0.0.1', // 로컬 호스트 사용
  port: 3307,        // 로컬 포트
  user: 'db_user',
  password: 'db_password',
  database: 'db_name'
});
  • localhost:3307는 ssh-server를 터널링해서 db server에 연결한다.

SSH 터널링 자동화

SSH 터널링을 사용하여 로컬 머신에서 원격 네트워크에 있는 MySQL 서버에 접근할 수 있도록 설정 후 private db에 연결할 수 있다.

하지만 매번 개발때마다 여러 터미널을 열고 수동으로 터널링을 하기는 귀찮다..

그래서 node에서 제공하는 여러 모듈을 사용하여 자동화를 해보기로 했다.

처음에는 ssh2모듈을 사용하여 시작해 인터넷과 gpt를 찾아보며 여러 레퍼런스를 시도하였지만 알 수 없는 에러로인해 계속 실패하였다.

터널링하는 포트가 3306으로 mysql 포트와 겹쳐서 충돌이 난다는 글을 보고 3307로 바꾼뒤 연결해봤지만 여전히 실패…

@Module({
  imports: [
    AuthModule,
    TypeOrmModule.forRootAsync({
      useFactory: async () => {
        return await getTypeOrmConfig(); // SSH 터널링이 완료된 후 TypeORM 설정 로드
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

다음은 터널링 되기전에 db연결을 먼저시도하는 문제인 줄 알고 await로 먼저 터널링 후 db연결을 시도했다. → 실패

gpt o1-preview한테 물어봤다.

현재 사용하신 ssh2 라이브러리의 forwardOut 메서드는 단일 연결에 대한 포워딩을 처리합니다. 하지만 ssh -L 명령어처럼 로컬 포트를 열어 여러 연결을 포워딩하려면, net 모듈과 함께 사용하거나, 더 간단하게는 tunnel-ssh 라이브러리를 사용하는 것이 좋습니다.

바로 tunnel-ssh 라이브러리를 사용했지만 여전히 발생하는 문제들

구글링 레퍼런스를 보며 구현을 해봤지만 tunnul이라는 함수를 못불러오는 문제가 계속 발생했다.

commonJS 문제인지 뭔지 계속 방법을 찾아보았지만 실패..

거지같은 tunnel is not a function을 해결하지 못했다.

그러던 중 tunnel 라이브러리를 다운그레이드해서 설치해서 되었다는 글을 보고 공식문서를 찾아보기로 했다.

최신버젼에서는 createTunnel로 변경되고 구조가 많이 변경된 것을 볼 수 있었다.

결국 공식문서 레퍼런스를 보고 해결 !

import {createTunnel} from 'tunnel-ssh';
const sshOptions = {
	host: '192.168.100.100',
	port: 22,
	username: 'frylock',
	password: 'nodejsrules'
};

function mySimpleTunnel(sshOptions, port, autoClose = true){
    let forwardOptions = {
        srcAddr:'127.0.0.1',
        srcPort:port,
        dstAddr:'127.0.0.1',
        dstPort:port
    }

    let tunnelOptions = {
        autoClose:autoClose
    }
    
    let serverOptions = {
        port: port
    }

    return createTunnel(tunnelOptions, serverOptions, sshOptions, forwardOptions);
}

await mySimpleTunnel(sshOptions, 27017);

https://www.npmjs.com/package/tunnel-ssh

하지만 터널링은 로컬 개발환경에서만 사용하고 배포서버에서는 같은 서브넷인 db에 바로 연결해야 한다.

const env = process.env.NODE_ENV || 'development';

export default async function getTypeOrmConfig(): Promise<TypeOrmModuleOptions> {
	if(env === 'development') await setupSshTunnel();
	return {
		type: process.env.DB_TYPE as 'mysql',
		host: process.env.DB_HOST,
		port: Number(process.env.DB_PORT),
		username: process.env.DB_USERNAME,
		password: process.env.DB_PASSWORD,
		database: process.env.DB_DATABASE,
		entities: [__dirname + '/../**/*.entity.{js,ts}'],
		synchronize: Boolean(process.env.DB_SYNCHRONIZE), 
	};
}

Node_ENV 환경설정에 따라 분기를 나누도록 했다.

이제 배포시 db에 제대로 연결하는지만 확인하면 해결…

최종 결과

import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { config } from 'dotenv';
import { createTunnel } from 'tunnel-ssh'; 

config();
//여기서 process.env.NODE_ENV는 실행할 때 NODE_ENV에 대한 정보를 넣어주지 않으면 NULL값입니다.
//그래서 default값으로 development를 넣어줬습니다.
const env = process.env.NODE_ENV || 'development';
config();

const env = process.env.NODE_ENV || 'development';

export default async function getTypeOrmConfig(): Promise<TypeOrmModuleOptions> {
	if(env === 'development') await setupSshTunnel();
	return {
		type: process.env.DB_TYPE as 'mysql',
		host: process.env.DB_HOST,
		port: Number(process.env.DB_PORT),
		username: process.env.DB_USERNAME,
		password: process.env.DB_PASSWORD,
		database: process.env.DB_DATABASE,
		entities: [__dirname + '/../**/*.entity.{js,ts}'],
		synchronize: Boolean(process.env.DB_SYNCHRONIZE), 
	};
}

async function setupSshTunnel(): Promise<void> {
	return new Promise((resolve, reject) => {
		const sshOptions = {
			host: process.env.SSH_HOST,
			port: Number(process.env.SSH_PORT),
			username: process.env.SSH_USER,
			password: process.env.SSH_PASSWORD,
		};

		tunnel(sshOptions, process.env.DB_PORT)
			.then(() => {
				resolve();
			})
			.catch((error) => {
				console.error('SSH 터널링 설정 중 오류 발생:', error);
				reject(error);
			});
	});
}

function tunnel(sshOptions, port, autoClose = true): Promise<void> {
	return new Promise((resolve, reject) => {
		const forwardOptions = {
			srcAddr: process.env.DB_HOST,
			srcPort: port,
			dstAddr: process.env.SSH_DB_HOST, // 원격 DB 서버 IP
			dstPort: Number(process.env.SSH_DB_TUNNUL_PORT), // 원격 DB 포트
		};

		const tunnelOptions = {
			autoClose: autoClose,
		};

		const serverOptions = {
			port: port,
		};

		createTunnel(tunnelOptions, serverOptions, sshOptions, forwardOptions)
			.then((server) => {
				console.log('SSH 터널링이 성공적으로 설정되었습니다.');
				resolve();
			})
			.catch((error) => {
				console.error('터널 생성 중 오류 발생:', error);
				reject(error);
			});
	});
}

TypeOrmModuleOptions

💻 개발 일지

💻 공통

💻 FE

💻 BE

🙋‍♂️ 소개

📒 문서

☀️ 데일리 스크럼

🤝🏼 회의록

Clone this wiki locally