구현 내용

이전에 별도의 서버에 도커 컨테이너로 실행시킨 MySQL 서버에 접속해 우리의 네임서버에서 사용하려 합니다.

프라이빗 서버인 DB 서버와 퍼블릭 서버인 네임 서버는 서로 직접적으로 통신할 수 있지만,

데이터베이스 서버가 사용하는 3306 포트를 외부에 아예 개방하지 않으며 보안을 강화해보자는 의도에서 SSH-Tunneling을 활용해 3306 포트는 DB 서버의 로컬에서만 사용하게 했습니다.

멘토님께서 흔치 않은 방식이라고 하시길래 좀 더 찾아보았는데

보안 그룹에서 신경써주는 정도가 흔한 접근이지만, 그래도 보안을 더 신경썼다는 강점을 내세우면 좋을 것 같았습니다.

성능에 대한 문제만 없다면 괜찮지 않을까..

  1. ssh와 mysql 기본 연결 모두 TCP/IP 기반이므로 시간복잡도의 지수적exponential 증가는 없습니다.
  2. ssh의 암호화, 인증, 터널링의 추가 작업도 초기 연결 수립에서만 생기는 오버헤드입니다.

tunnel-ssh 라이브러리 사용

import { createTunnel } from 'tunnel-ssh';
import type { Pool } from 'mysql2/promise';
import mysql from 'mysql2/promise';
import { forwardConfig, poolConfig, sshConfig, tunnelConfig, serverConfig } from './config';
import type { Server } from 'net';

class DatabasePool {
    private static instance: DatabasePool;
    private pool: Pool | null = null;
    private server: Server | null = null;
    
    // ...(생략)...

    private async setupTunnel(): Promise<void> {
        try {
            this.server = (
                await createTunnel(tunnelConfig, serverConfig, sshConfig, forwardConfig)
            )[0];
            console.log('SSH 터널링 성공!');
            this.pool = mysql.createPool(poolConfig);
        } catch (error) {
            console.error('Failed to setup tunnel or create pool:', error);
            await this.cleanup();
            throw error;
        }
    }

    public async getPool(): Promise<Pool> {
        if (!this.pool) {
            await this.setupTunnel();
        }
        return this.pool as Pool;
    }
    // ...(생략)...
}

데이터베이스 설정 변수를 넣어 커넥션 풀을 생성하고 다루는 코드는 다들 아시는 mysql2를 사용한 것과 다를 바가 없습니다.

차이는 createTunnel() 메소드 하나에 있습니다. 매개변수를 확인하면 createTunnel()의 역할에 대한 감이 잡힐 것 같습니다. 네개나 되는 config 매개변수들에 대한 설명은 아래에👇 붙이겠습니다.

config

위에서 createTunnel()에 넘어가는 config 매개변수들을 살펴보면 이해가 조금 더 쉬울 것 같습니다.

export const sshConfig: SshOptions = {
    host: process.env.SSH_HOST,
    port: Number(process.env.SSH_PORT),
    username: process.env.SSH_USERNAME,
    agent: process.env.SSH_AUTH_SOCK,
};

export const tunnelConfig = {
    autoClose: true,
};

export const forwardConfig: ForwardOptions = {
    srcAddr: process.env.LOCAL_HOST,
    srcPort: Number(process.env.LOCAL_PORT),
    dstAddr: process.env.SSH_HOST,
    dstPort: Number(process.env.DB_PORT),
};

export const serverConfig = {
    host: process.env.LOCAL_HOST,
    port: Number(process.env.LOCAL_PORT),
};

export const poolConfig: PoolOptions = {
    host: process.env.LOCAL_HOST,
    port: Number(process.env.LOCAL_PORT),
    user: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
};