Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

なにこれ

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

mochisuna
主にサーバーサイドエンジニア。 goとかrubyとかでお仕事中。 自動化とかすき。手作業きらい。
https://github.com/mochisuna/portfolio
giftee
giftee (株式会社ギフティ) は、ソーシャルギフトサービス 「giftee」、法人向けデジタルギフトチケット販売画面の提供、その他O2Oソリューションなどを展開する五反田のスタートアップです。(onlab第1期, KDDI ∞ LABO 第1期)
https://giftee.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away