4
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-19

なにこれ

表題の通り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 とか作れます。まぁそんな要件自体がそもそもあまりないとは思うけど。

4
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?