-
[Node.js] SSH Tunneling 을 통한 ORM 생성하기Backend 2021. 9. 29. 16:47
필자는 Ubuntu 내부 MySQL 서버에 접속해서 작업이 필요할 때 방화벽 DB port(3306) 를 열고 외부 접속을 했습니다. 당연히 보안에 취약할 것입니다. 귀찮음으로 인해서 필자의 local ip 에 대해서 접속을 허용하는 작업을 계속하게 되면 나중에 더 힘들 것 같아서 모듈을 만들고자 했습니다.
해당 Thread 를 읽으며 Web Application Server에서 ssh 터널링을 통해 MySQL 이 존재하는 Database server에 접속하여 작업할 수 있도록 설정하고자 했습니다. 하지만 자료가 많이 있지 않아서 돌아다니며 조합하여 퍼즐을 맞춰갔습니다.
SSH Tunneling
우선 SSH Tunneling 을 위해 node.js 3가지의 모듈을 두고 고민을 했습니다.
해당 Stackoverflow Thread 를 읽으며 tunnel-ssh 모듈을 선택했습니다.
우선 필요한 모듈을 설치합니다.
npm i tunnel-ssh npm i dotenv
dotenv 를 통해 사용할 .env 파일에 기본 config 를 설정합니다.
SSH_HOST=[DB 가 설치된 Server 접속 IP] SSH_USER=[DB 가 설치된 Server 접속 Username] SSH_PORT=[DB 가 설치된 Server 접속 Port] // ssh key 를 사용한다면 없어도 됩니다! SSH_PASSWORD=[DB 가 설치된 Server 접속 Password] DB_HOST=[DB Host(localhost)] DB_PORT=[DB Port] DB_USERNAME=[DB username] DB_PASSWORD=[DB password] DB_DATABASE=[사용할 database name]
이제 tunnel-ssh 모듈을 사용하여 SSH 접속을 테스트 해보겠습니다.
const tunnel = require('tunnel-ssh'); require('dotenv').config(); const ssh_config = { username: process.env.SSH_USER, password: process.env.SSH_PASSWORD, host: process.env.SSH_HOST, port: process.env.SSH_PORT, dstHost: process.env.DB_HOST, // 서버 내부에서 사용할 HOST(Localhost) dstPort: process.env.DB_PORT, // 서버 내부에서 사용할 Port(DB Port) }; tunnel(ssh_config, (error, server) => { if (error) { throw error.toString(); } else if (server !== null) { console.log('Connection!'); //Connection! } });
제대로 유효한 Config 값을 .env 파일에 작성했다면 정상적으로 Connection! 이 출력되는 것을 확인할 수 있습니다.
MySQL Connection
ssh 접속을 확인했으니 이제 Database 에 접속할 차례입니다.
create database test; create table test_table ( id int auto_increment primary key );
우선 mysql 에 test database 와 test databse 내부 test_table 를 생성했습니다.
npm i mysql2
mysql2 모듈을 사용하여 connection 을 생성하겠습니다.
tunnel(ssh_config, (error, server) => { if (error) { throw error; } else if (server !== null) { const mysql = require('mysql2'); mysql .createConnection({ host: process.env.DB_HOST, user: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, }) .execute('SHOW TABLES from test;', (err, result, fields) => { if (err) throw err; console.log(result); }); } });
정상적으로 Connection 이 생성되었다면 이전 생성한 test database 내부 table list 로 test_table 이 출력되어야 할 것입니다.
[ BinaryRow { Tables_in_test: 'test_table' } ]
정상적으로 MySQL Connection 이 생성됐습니다.
ORM(Sequelize)
정상적으로 MySQL Connection 까지 생성된 것을 확인했습니다. 이제 ORM 을 생성할 차례입니다.
npm i sequelize
mysql2 모듈이 설치 되어 있지 않다면 필수로 설치해야 됩니다.
npm i sequelize mysql2
sequelize-init 을 설치하여 sequelize init 명령어를 통해 각 entity 에 대해서 models 폴더 내부에 파일을 생성할 수 있습니다.
하지만 각 Dataabase 의 설계는 ERD 를 통해 많이 설정합니다. 즉, 이미 table 들이 생성되어 있으며 table 정보들을 자동으로 생성되는 것이 편할 것 입니다. 해당 작업에 대한 모듈로 sequelize-auto 가 존재해서 설치하게 되었습니다.
npm i sequelize-auto
이제 코드를 수정해보겠습니다.
const tunnel = require('tunnel-ssh'); require('dotenv').config(); const ssh_config = { username: process.env.SSH_USER, password: process.env.SSH_PASSWORD, host: process.env.SSH_HOST, port: process.env.SSH_PORT, dstHost: process.env.DB_HOST, dstPort: process.env.DB_PORT, }; const db_config = { database: process.env.DB_DATABASE, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, host: process.env.DB_HOST, dialect: 'mysql', }; tunnel(ssh_config, (error, server) => { if (error) { throw error; } else if (server !== null) { const SequelizeAuto = require('sequelize-auto'); new SequelizeAuto( db_config.database, db_config.username, db_config.password, { host: ssh_config.dstHost, port: ssh_config.dstPort, dialect: db_config.dialect, } ).run(); } });
SequelizeAuto 의 run 함수를 통해 실제 Database 의 ERD 가 models 폴더 내부에 생성되는 것을 확인할 수 있습니다.
├── models │ ├── init-models.js │ └── test_table.js
현재 database 에 존재하는 test_table 이 생성되었습니다. 추가로 생성된 init-models 모듈을 통해 각 Database 의 Entity 를 Object 형태로 얻을 수 있습니다.
const sequelize = new Sequelize( db_config.database, db_config.username, db_config.password, db_config ); tunnel(ssh_config, async (error, server) => { if (error) { throw error; } else if (server !== null) { const SequelizeAuto = require('sequelize-auto'); await new SequelizeAuto( db_config.database, db_config.username, db_config.password, { host: ssh_config.dstHost, port: ssh_config.dstPort, dialect: db_config.dialect, } ).run(); const init_models = require('./models/init-models'); const orm = init_models(sequelize); console.log(orm); } });
{ test_table: test_table }
이제 orm 까지 생성되었습니다. 하지만 문제점이 하나 있습니다. ssh tunneling 은 Timeout 이 존재하므로 ssh 연결이 끊기게 되며 orm 을 통해 db 작업을 할 경우 "SequelizeConnectionRefusedError"가 발생합니다.
SSH Connection 유지
처음에는 막연히 ssh timeout 을 무제한으로 늘릴까도 생각했지만 알량한 자존심.. 때문에 다른 방식을 고민했습니다.
선택한 방식은 orm 을 사용할 때 ssh connection 오류가 발생한다면 새로 ssh tunneling 을 실행하는 방식입니다. 그렇게 하기 위해서는 ssh tunneling 함수와 sequelize 기능을 분리해야 합니다.
const ssh_config = { username: process.env.SSH_USER, password: process.env.SSH_PASSWORD, host: process.env.SSH_HOST, port: process.env.SSH_PORT, dstHost: process.env.DB_HOST, dstPort: process.env.DB_PORT, }; const db_config = { database: process.env.DB_DATABASE, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, host: process.env.DB_HOST, dialect: 'mysql', }; const sequelize = new Sequelize( db_config.database, db_config.username, db_config.password, db_config ); function sshTunneling() { return new Promise((resolve, reject) => { tunnel(ssh_config, (error, server) => { if (error) { reject(error); } else if (server !== null) { resolve(server); } }); }); } function getORM() { return new Promise((resolve, reject) => { sequelize .authenticate() .then(() => { const init_models = require('./init-models'); const orm = init_models(sequelize); resolve(orm); }) .catch(async (exception) => { if (exception.name === 'SequelizeConnectionRefusedError') { await sshTunneling(); const orm = await getORM(); resolve(orm); } else reject(exception); }); }); }
핵심은 getORM 함수입니다.
sequelize 의 authenticate() 함수를 통해 connection 을 검사할 수 있습니다. 만약 ssh connection 이 정상적으로 유지 되어있다면 정상적으로 then 함수의 콜백이 실행될 것입니다. 하지만 ssh connection timeout 으로 인해서 "SequelizeConnectionRefusedError" 가 발생한다면 sshTunneling 함수를 통해 ssh tunneling 을 진행하고 다시 getORM 을 실행합니다.
모듈
https://github.com/dmin0211/web-server-was/blob/main/orm.js
이와같은 작업에 대한 자료가 많이 없어서 부족하거나 생각하지 못한 예외가 생길 수 있습니다. 만약 예외가 발생하거나 부족한 점이 있다면 지적해주시면 감사할 것 같습니다.