Edited at

Go言語で踏み台サーバー越しにDBからデータを取得する


なにこれ

表題の通りsshした踏み台サーバーからprivateなネットワークにあるDBに対し、クエリを投げたい という要件をgoで実践してみたのでまとめます。


あらまし


  • 「DBから情報を集計して統計取ったり活用したい!」

  • 「けどre:dashみたいなイカした集計ツールも入れてないんだよね・・・。」

  • 「でも会計の関係ですぐにでも情報が欲しいんだ!」

  • 「いつものようにパパッと頼むね☆」

・・・なんて言われた日には「ほう?」ぐらいしか返す言葉が思いつかないです。

でもやらないとね。仕方ないね。

で、


  • 特に制約さえなければre:dash等の集計ツールを入れてしまえばOK

  • 目的を達成するだけなら踏み台から手作業をすればOK

なのですが、折角なので既にある構成から試してみたかったことをやってみました。

決して踏み台入って手作業とか失敗する未来しか見えなかったので嫌だったし、新しくツールを導入するのに諸々制約があってダルかったとかではないです。


ネットワーク設定

本ケースでのネットワーク設定は以下のような感じです。

詳細としては


  • アプリケーションが動いているサブネットに踏み台がある

  • 踏み台からRDSへのリクエストはできる

  • 踏み台への接続は社内IPからのみ

となっています。

今回のゴールは bastion server となっている踏み台からRDSへリクエストを投げて結果を取得することです。


プログラム

今回利用したプログラムは以下のような形です。

厳密には参照するカラムなどは別です。

package main

import (
"database/sql"
"fmt"
"io/ioutil"
"net"

"github.com/go-sql-driver/mysql"
"golang.org/x/crypto/ssh"
)

type DB struct {
Host string
Port string
User string
Password string
DBName string
}

type SSH struct {
Key string
Host string
Port string
User string
}

// newSSH はsshクライアントを生成する
func newSSH(conf *SSH) (*ssh.Client, error) {
// ssh_keyファイルを取得
sshKey, err := ioutil.ReadFile(conf.Key)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(sshKey)
if err != nil {
return nil, err
}
hostKeyCallbackFunc := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
}
sshConf := &ssh.ClientConfig{
User: conf.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: hostKeyCallbackFunc,
}
return ssh.Dial("tcp", conf.Host+conf.Port, sshConf)
}

// newDB はmysqlクライアントを生成する
func newDB(conf *DB, sshc *ssh.Client) (*sql.DB, error) {
// MySQLで使うプロトコルは一旦tcpで設定
mysqlNet := "tcp"
if sshc != nil {
// MySQLはプロトコルを更新
mysqlNet = "mysql+tcp"
dialFunc := func(addr string) (net.Conn, error) {
// dialはtcpでやりとり
return sshc.Dial("tcp", addr)
}
mysql.RegisterDial(mysqlNet, dialFunc)
}
dbConf := &mysql.Config{
User: conf.User,
Passwd: conf.Password,
Addr: conf.Host + conf.Port,
Net: mysqlNet,
DBName: conf.DBName,
ParseTime: true,
AllowNativePasswords: true,
}
return sql.Open("mysql", dbConf.FormatDSN())
}

func main() {
// init config
// 本当はtomlとかで取得した方が良い
dbConf := &DB{
Host: "your.mysql.host.domain",
Port: ":3306",
User: "user_name",
Password: "p@ssW0rd",
DBName: "db_name",
}
sshConf := &SSH{
Key: "/Users/user-name/.ssh/ssh_key",
Host: "bastion_host_ip",
Port: ":22",
User: "user_name",
}

// init ssh client
sshClient, err := newSSH(sshConf)
if err != nil {
fmt.Println("err in new ssh client. reason : " + err.Error())
} else {
defer sshClient.Close()
}
// init db client
dbClient, err := newDB(dbConf, sshClient)
if err != nil {
fmt.Printf("erro in new db client. reason : %v\n", err)
panic(err)
}
defer dbClient.Close()

// select from mysql
rows, err := dbClient.Query("SELECT * FROM stores")
if err != nil {
panic(err.Error())
}
// 結果が見たい
columns, err := rows.Columns()
if err != nil {
panic(err.Error())
}
values := make([]sql.RawBytes, len(columns))
args := make([]interface{}, len(values))
for i := range values {
args[i] = &values[i]
}
for rows.Next() {
err = rows.Scan(args...)
if err != nil {
panic(err.Error())
}
var value string
for i, val := range values {
if val == nil {
value = "NULL"
} else {
value = string(val)
}
fmt.Println(columns[i], ": ", value)
}
fmt.Println("-----------------------------------")
}
}

仮置きで店舗情報が入っている stores テーブルがあるものとしています。

ファイル一個で済ませるためベタ書きになっているのが気がかり。


ポイント



  1. ssh.Dialでssh Clientを生成


  2. mysql.RegisterDial を利用し踏み台経由でリクエストを送るように設定


    • この時mysql側で指定するプロトコルは"mysql+tcp"

    • 指定するdialFuncは func(addr string) (net.Conn, error) を満たすもの

    • このサンプルではssh Clientに対し Dial("tcp", addr) を指定



これだけでOKなので楽チン。


余談

一個のファイルにベタ書きとかではなく、真面目にパッケージ分離した場合はこんな感じ


まずないとは思うけど、こんな風にやれば 連携チームの踏み台にsshで入ってSQLを叩き、結果を利用するAPI とか作れます。まぁそんな要件自体がそもそもあまりないとは思うけど。