ABOUT ME

-

My Email
  • kdmin0211@gmail.com
  • Today
    -
    Yesterday
    -
    Total
    -

    • [Node.js] SSH Tunneling 을 통한 ORM 생성하기
      Backend 2021. 9. 29. 16:47

      필자는 Ubuntu 내부 MySQL 서버에 접속해서 작업이 필요할 때 방화벽 DB port(3306) 를 열고 외부 접속을 했습니다. 당연히 보안에 취약할 것입니다. 귀찮음으로 인해서 필자의 local ip 에 대해서 접속을 허용하는 작업을 계속하게 되면 나중에 더 힘들 것 같아서 모듈을 만들고자 했습니다.

       

      Is it safe to open port 3306 in Firewall for external MySQL Connection

      I want to connect to a MySQL DB that is hosted with an ISP using something like TOAD, Navicat or HeidiSQL. I was told by the ISP that MySQL is listening on port 3306 but the hardware firewall is not

      serverfault.com

      해당 Thread 를 읽으며 Web Application Server에서 ssh 터널링을 통해 MySQL 이 존재하는 Database server에 접속하여 작업할 수 있도록 설정하고자 했습니다. 하지만 자료가 많이 있지 않아서 돌아다니며 조합하여 퍼즐을 맞춰갔습니다.

      SSH Tunneling

      우선 SSH Tunneling 을 위해 node.js 3가지의 모듈을 두고 고민을 했습니다.

       

       

      How to use Node.js to make a SSH tunneling connection to a MongoDB database

      My credentials work perfectly with Robomongo but I can't make the connection with node.js I have tried to make the connection using ssh2 and tunnel-ssh npm module and failed both times. -The mongo

      stackoverflow.com

      해당 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

       

      GitHub - dmin0211/web-server-was

      Contribute to dmin0211/web-server-was development by creating an account on GitHub.

      github.com

       

      이와같은 작업에 대한 자료가 많이 없어서 부족하거나 생각하지 못한 예외가 생길 수 있습니다. 만약 예외가 발생하거나 부족한 점이 있다면 지적해주시면 감사할 것 같습니다.

       

       

       

       

       

       

       

       

       

      댓글

    Designed by Tistory.