.envから設定パッケージを自動生成するツールを作った

author potpro(ぼとぷろ)
2021/11/30

.envから設定パッケージを自動生成するツールを作った

本記事は Go 2 Advent Calendar 2021 の1日目の記事です。

ついに断念してQiitaのアカウントを取ってAdvent Calendarを書くことにしました。よろしくお願いいたします。

作ったもの

https://github.com/potproject/goenvgen

概要

自分はGo言語でWebアプリケーションをよく開発しています。

最近は、GraphQL Schemaから自動的にサーバーサイドGoのコードを自動生成してくれるgqlgenや、クライアントサイドのTypeScriptの型を生成してくれるgraphql-code-generatorなどを使ってきてよく感じることなのですが、

コードを自動生成して使うのは生産性も高くなるし非常に開発効率が高まるなあと感じています。

先ほど例に挙げたOSSの素晴らしいところは、自動生成で型安全なコードを生成してくれる所にあります。自動生成できる部分は生成してくれれば、それだけビジネスロジックに集中できます。開発効率もかなり上がります。

やっぱり気持ち的にも、サーバにもクライアントにも型定義ファイルを書きまくるのも結構労力が要りますよね。

それが大量に存在するとなるとすごく単純作業に感じてしまいまして、自分は開発時の体験を悪くしている部分と感じているわけです。そういうことは勝手にやってくれるに越したことはありません。

つまり、すごく簡単に言うと生成できそうなめんどくさい部分のコードは自動生成して楽したいという話です。

なので自分もその手のOSSを何か作るかあみたいな感じで作ったのがこれです。

今まで、割とプロダクト寄りのものはOSSとして出しているんだけどこういうCLIツール的なものはちゃんと出したことが無かったです。

解決したい問題

Go言語でWeb APIなどを作る場合、jsonやyamlから設定を取得したりと昔はしてましたが、コンテナでの本番運用が一般化した今、環境の設定情報は環境変数から取得するのが一般的かな、と思っています(だよね?)。

そのため、環境設定に値をセットして、アプリケーション側で取得するようなコードを書くと思います。

Go言語には環境変数から取得する関数としてos.Getenv関数が存在します。

$ export BASE_HOST=localhost
func main() {
    host := os.Getenv("BASE_HOST")
    fmt.Println(host) // localhost
}

これが一般的な使い方だと思います。しかし、実際にアプリケーションを開発する時は考えることがまだまだあります。

os.GetEnvの返り値は全てstring

os.GetEnvの返り値は全てstringです。環境変数が文字列なので当然ではあるのですが、数値として扱いたいのであれば、Go言語は静的型付けなのでInt型などに変換が必要になります。

func main() {
    portStr := os.Getenv("BASE_PORT")
    var port int
    port, err := strconv.Atoi(portStr)
    if err != nil {
       log.Fatal(err)
    }
    fmt.Println(port) // 8080
}

これでint型に変換できました。しかし、変換によるエラー処理も必要になり、一気にコードが増えてしまいました。

なので、環境変数にたくさん保持するようになると、こういったコードがどんどん増えてきます。 少ない状態であれば問題ありませんが、大量に変数を管理しようとすると大変です。実際、アプリケーション開発ではこれが30個とか50個になってもおかしくありません。

.envを使用して環境変数をファイル管理をしたい

開発環境などで環境変数を設定するのは、値を毎回exportする必要があったりと開発しづらいと思います。

なのでgodotenvというライブラリを使用します。 dotenvを使用することで、.envというファイル(ファイル名は変更可)から環境変数を読み込みことができます。

.envファイル

BASE_HOST=localhost
BASE_PORT=8080
func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal(err)
    }
    host := os.Getenv("BASE_HOST")
    portStr := os.Getenv("BASE_PORT")
    var port int
    port, err = strconv.Atoi(portStr)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s:%d", host, port) // localhost:8080
}

これで開発環境と本番環境でファイルで切り分けしたりと使いやすくなりました。

設定をstructで管理したくなる

毎回os.GetEnv関数を使って取得するのもコードの見通しが良くなくなってきます。

文字列で指定するのでエディタの入力補完機能も効きませんし、テストコードを書く時なども扱いづらいです。

なので、どこかにstructやパッケージで保持しておいてそこから取得したい、という形になってくると思います。

type ENV struct {
    Host string
    Port int
}

func main() 
    err := godotenv.Load()
    if err != nil {
        log.Fatal(err)
    }
    host := os.Getenv("BASE_HOST")
    portStr := os.Getenv("BASE_PORT")
    var port int
    port, err := strconv.Atoi(portStr)
    if err != nil {
        log.Fatal(err)
    }
    e := ENV{
        Host: host,
        Port: port,
    }
    fmt.Printf("%s:%d", e.Host, e.Port) // localhost:8080
}

で、大体こんな感じで落ち着く気がします。どうでしょうか?コードがどんどん増えてきますね。

そう、自分はこの作業が非常にめんどくさいなあと感じていました。特に毎回型を定義はめんどくさいですし、でもやらないとコードの見通しが悪い。テストも書きづらい・・・。けど書きたくない・・・。

そうだ、自動生成しよう

そして、作ったのがpotproject/goenvgen というツールです。

このOSSはCLIとして、解決したい問題で語った部分のコードを型定義部分も含めて全部自動生成してくれます。

扱い方は通常のGo製のCLIと同じように、

go get github.com/potproject/goenvgen

でインストールした後に.envファイルを用意して、

goenvgen

でenvgenというパッケージが生成されます(名前は変更可能)。

このパッケージを使うと、こんな感じで取得できます。

func main() {
    // .envファイルを環境変数にセットする
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    // 環境変数をenvgenパッケージにセットする
    err = envgen.Load()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s:%d", envgen.Get().BASE_HOST(), envgen.Get().BASE_PORT()) // localhost:8080
}

どうでしょう。かなり楽になったんじゃないでしょうか。

型も.envファイルから自動的に判定していて、型変換にエラーがある場合はenvgen.Load()時にエラーを吐くようにしています。

使い方としても環境変数名がそのまま変数になっているので、直感的に使えるのではないでしょうか。

これで長いコードを書く必要がなくなりました。楽しく開発が出来ますね!

その他の機能

本筋の機能はこれらですが、いろんな場合に対応できるように色々な機能を入れています。

変数の値がjson形式の場合、jsonにも対応して取得が可能であったり、Alice,Bobといったカンマ付きの文字列であれば、自動的にslice付きの型が定義されます。

こんな感じです。

ADMIN_IDS=123,234,345,456
USER_JSON={"Alice": {"ID": 100}, "Bob": {"ID": 200}}
func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    err = envgen.Load()
    if err != nil {
        log.Fatal(err)
    }

    // Slice Type
    ids := envgen.Get().ADMIN_IDS()
    for _, id := range ids {
        fmt.Printf("ID: %d ", id) // ID: 123 ID: 234 ID: 345 ID: 456
    }

    // JSON Type
    user := envgen.Get().USER_JSON()
    fmt.Printf("Bod ID: %d \r\n", user.Bob.ID) // Bob ID: 200
}

めっちゃ良い(自画自賛)。

また、オプションで必須にしたり、型の設定を自動ではなく特定の型を設定したい場合は手動で設定することも可能にしています。

詳しくは README.md を見ていただけるとありがたいです。

今回、ちゃんとしようということでReadmeもちゃんと書きました。英語はおかしいかもしれないけど・・・。

今後について

とりあえず自分は自分でも使っていくつもりで作ったので、いいと思ったらスターをくれると開発意欲が上がります。

ちなみに完成させたのは2日前くらいです。もっと余裕をもって作れよと言う話ですが作れてよかったです。