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

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

突然ですが、これまでコードはGistにアップして、スクリプトを貼り付けていました。
でも、今回みたいにたくさんコードがあると結構困るんですよね(´・ω・`)
そこで今回からhighlight.jsを導入してみました♪
diffも書けるのが便利です(・∀・)

さて、今回はタイトルの通りです(`・ω・´)
家のラズパイは各種監視やエアコン等の制御用に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にしたい…