SSHポートフォワーディングを使ってリモートのMySQLに接続

2017/12/7 · 8 mins read
※ この記事は公開されてから1年以上経過しているため、情報が古い可能性があります。
SSHポートフォワーディングを使ってリモートのMySQLに接続

久々の更新です(*・ω・)ノ

さて、今回はタイトルの通りです(`・ω・´)
家のラズパイは各種監視やエアコン等の制御用に Springboot で作られた API が走っています。
なのでスマホから色々操作できるようになっています♪
この話はまたいつか…

で、こいつの接続先をローカル DB ではなく、別のサーバー(このブログの動いてるサーバー)の DB に接続するようにします!
経緯としては結構前になってしまうのですが、実はラズパイの SD カードがアボ〜ンしました(T_T)
サーバー自体の再構築は Ansible さんがいらっしゃったのでそこまで大変でもなかったのですが、DB のデータはもう帰ってこない…
せめて DB はレン鯖にしとくべきだったと反省しております_| ̄|○

ということでその作業の一角、Springboot の DB 接続は SSH ポートフォワーディングを使ってリモートの MySQL に接続するようにする手順の備忘録的なものです。
作業の手順としては

  • JSch を導入
  • コネクション用のクラスを作成
  • リスナーに登録処理追加
  • ポートフォワーディングに合わせて SQL の接続先を修正
  • テストの修正
  • となります。
  • では行ってみましょう!!

JSch を導入

まぁこれは大した内容ではないですね。
gradle に追加します。

diff --git a/build.gradle b/build.gradle
index 163dc68..e8e1c89 100644
--- a/build.gradle
+++ b/build.gradle
@@ -49,6 +49,7 @@ dependencies {
        compile('org.projectlombok:lombok')
+       compile('com.jcraft:jsch:0.1.54')
}

コネクション用のクラスを作成

JSch を使ってポートフォワーディングを行います。
rsaKeyPath と knownHostsPath を引数にしているのはテストや本番用です。

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

public class SSHConnection {
    private static final String S_PASS_PHRASE = "公開鍵のパスフレーズ";
    private static final int LOCAl_PORT = 3307;
    private static final int REMOTE_PORT = 3306;
    private static final int SSH_REMOTE_PORT = 10022;
    private static final String SSH_USER = "SSH接続ユーザー";
    private static final String SSH_REMOTE_SERVER = "SSH接続先サーバ";
    private static final String MYSQL_REMOTE_SERVER = "MySQLのサーバ";

    private Session session;

    public void closeSSH() {
        session.disconnect();
    }

    public SSHConnection(String rsaKeyPath, String knownHostsPath) throws JSchException {
        JSch jsch = new JSch();
        jsch.setKnownHosts(knownHostsPath);
        jsch.addIdentity(rsaKeyPath, S_PASS_PHRASE.getBytes());

        session = jsch.getSession(SSH_USER, SSH_REMOTE_SERVER, SSH_REMOTE_PORT);
        session.connect();

        session.setPortForwardingL(LOCAl_PORT, MYSQL_REMOTE_SERVER, REMOTE_PORT);
    }
}

リスナーに登録処理追加

ServletContextListener を継承したクラスを作ります。
これで登録することで、起動と同時にポートフォワーディング処理が走ります。

import com.jcraft.jsch.JSchException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.java.Log;
import org.springframework.stereotype.Component;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.util.logging.Level;

@Log
@Component
@WebListener
@RequiredArgsConstructor
public class ContextListener implements ServletContextListener {

    @NonNull
    private final AppParams appParams;

    private SSHConnection sshConnection;

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("Context Initialized");
        try {
            sshConnection = new SSHConnection(appParams.getRsaKeyPath(), appParams.getKnownHostsPath());
        } catch (JSchException e) {
            log.log(Level.WARNING, "SSHConnection Error", e);
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        if (sshConnection != null) {
            sshConnection.closeSSH();
        }
        log.info("Context Destroyed");
    }
}

ポートフォワーディングに合わせてSQLの接続先を修正

コネクション用のクラスで定義したポートに変更してやります。

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3307/scheme_name?useSSL=false
    username: MySQLのユーザー名
    password: MySQLのパスワード
    driverClassName: com.mysql.jdbc.Driver
    testonborrow: true
    validationQuery: SELECT 1 FROM DUAL

テストの修正

実はこれが地味にハマりました。。。
テスト時も DB 接続テストが発生するため、リスナーに登録したのと同様に実行前に接続し、実行後に破棄してやる必要があったのです。

方法は色々あるかと思いますが、今回は ClassRule アノテーションを使い、TestRule を実装したクラスを呼び出す方法をとりました。

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class ConnectionTestRule implements TestRule {
    private static final String ID_RSA_DIR = "公開鍵のファイルパス";
    private static final String KNOWN_HOSTS_DIR = "known_hostsのファイルパス";

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                SSHConnection sshConnection = new SSHConnection(ID_RSA_DIR, KNOWN_HOSTS_DIR);
                base.evaluate();
                sshConnection.closeSSH();
            }
        };
    }
}

上記のクラスを以下のように設定してあげます。

import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class BvlionBatchApplicationTests {

    @ClassRule
    public static ConnectionTestRule rule = new ConnectionTestRule();

    @Test
    public void contextLoads() {
    }
}

これで無事に gradle build も通り、DB にも繋がるようになりました!
今まで何度か自宅鯖をアボ〜ンさせた経験はあったのですが、ここ最近はずっと PaaS 系を使っていたためすっかりローカル DB が信頼ならないことを失念していました(>_<)
いつか Firebase にしたい…