おそらくはそれさえも平凡な日々

database/sqlのDB接続パラメータをアプリケーション内で明に指定する

tl;dr

Goのdatabase/sqlのDSN内のsql_modeやLocation等、固定したほうが良いパラメータ設定は、設定値に持たせるのではなく、アプリケーション内部で決め打ちしたほうが安全です。

本論

社内でMySQLを使っているので、それを例にとって書きます。

いわゆる、DSN(dataSourceName)呼ばれる、sql.Open に渡すDB接続文字列があります。これは、環境変数 DATABASE_URL 等に入れてアプリケーション内で読み出してDBに接続するでしょう。

DSNにはホスト名、ユーザー名、パスワードなどの接続先情報の他に、様々なオプションパラメータを記述することができます。以下にDSNの例を出しますが、この中の?以降がオプションパラメータです。

user:pass@tcp(myhost:3306)/dbname?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&sql_mode=%27TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY%27

しかしサービス開発において、それらのパラメータの全てを環境変数に直に設定することはおすすめしません。柔軟に色々設定できすぎてしまうことが逆に危険だからです。

DB Driverのライブラリレベルでは利用者の環境に合わせて柔軟に設定できて然るべきですが、Driverを利用する側のアプリケーションレベルでは決め打ちにした方が安全な項目も多いため、全てを設定可能にしておく必要はありません。

例えば、parsetime=trueが期待されているアプリケーションはそのパラメータが無いと動かないのでそもそも設定させる必要がないとか、sql_modeでkamipo TRADITIONALを強制したい、loc(Location)は決め打ちにしておきたい、などの要件があります。

この辺りは、開発環境、CI環境、ステージング、本番環境で揃っていたほうが変な事故も少なくなるため、設定させる余地を与えないほうが安全です。それらを設定可能にしておいて、ひょんなタイミングでずれたら怖い。特に本番運用中のDBにつなぐLocation設定が突然変わってDBの時刻がずれたら大事故です。

ですので、アプリケーションレベルでそれらを揃えてしまうのが良いでしょう。例えば以下のような具合です。

func DatabaseURL() (string, error) {
    databaseURL := os.Getenv("DATABASE_URL")
    c, err := mysql.ParseDSN(databaseURL)
    if err != nil {
        return "", err
    }
    c.Loc = time.UTC
    c.ParseTime = true
    c.Collation = "utf8mb4_general_ci"
    if c.Params == nil {
        c.Params = map[string]string{}
    }
    c.InterpolateParams = true
    // enforce kamipo TRADITIONAL!
    c.Params["sql_mode"] = "'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'"
    return c.FormatDSN(), nil
}

値を決め打ちにするのではなく、おかしな値が設定されていたらエラーを返す手もあるでしょうし、os.Getenv を内部で呼び出すのではなくパラメーター渡しにしたほう良いという指摘も出てきそうですが、その辺りはお好みで。

これを以下のようにして使えばよいだけです。

dsn, err := DatabaseURL()
if err != nil {
    // エラー処理
}
db, err := sql.Open("mysql", dsn)
...

コードにしておけば以下のようなテストコードを書くことができ、設定されて欲しいオプションが間違いなく設定されていることが担保できるため、その点でも安心です。

func TestDatabaseURL(t *testing.T) {
   orig := os.Getenv("DATABASE_URL")
   defer os.Setenv("DATABASE_URL", orig)

   os.Setenv("DATABASE_URL", "root@tcp(localhost:3306)/testdb")
   expect := "root@tcp(localhost:3306)/testdb?parseTime=true&interpolateParams=true&sql_mode=%27TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY%27"
   dsn, _ := DatabaseURL()
   if e, g := expect, dsn; e != g {
       t.Errorf("got %s, expect: %s", g, e)
   }
}

ところで最近は、秘匿情報を保管場所を分けて管理することが一般的になり、それ自体は良いことだと思いますが、逆にそれらの値がvalidかどうかのテストが難しくなっているようにも感じています。昔は各環境の設定ファイルをテストコード内で読み込んで正しいかどうかの確認などをしていましたが、それが難しくなったと感じています。その辺り何か工夫している方がいれば教えて下さい。

閑話休題。この方法で嬉しい副作用として設定値が見やすくなることがあります。オプションパラメーターを環境変数に長々と書く必要がなくなるため、例えば以下のようにDATABASE_URLを設定するだけで良くなります。

DATABASE_URL=user:pass@tcp(myhost:3306)/dbname

DSNが長くなりすぎて何を設定しているかわかりづらくなるとか、実はtypoしてパラメーター指定が誤っていたなどの事故も防げるので安心です。

実はその昔、Perlで似たような話を書いていましたが、今回はGoでのお話でした。

採用情報

Natureでは、バックエンドエンジニア、スマートフォンアプリエンジニア、その他職種を絶賛募集中ですので、ご応募お待ちしています!

https://nature.global/jp/careers

created at
last modified at

2021-01-27T00:35:43+0900

comments powered by Disqus