配置存放於環境:Go 應用的配置實踐

配置存放於環境:Go 應用的配置實踐
Photo by Josh Redd / Unsplash

在雲原生的環境中,程式通常採用容器部署,而不同環境間所需要的配置也會不同,像是開發環境的資料需要與生產環境分離;金絲雀部署要分流生產環境的流量,但不會寫資料到生產環境中;開發環境為了除錯,要印出 level 低的 log;開發環境跟生產環境要拿取的 key vault 的 key 跟 version 不同;等等。在雲原生的時代前,開發人員或維運人員通常會各自維護一份執行的程式,開發在開發環境中驗證後,交付維運部署上線,一次性處理好配置;但在雲原生時代,部署變得越來越頻繁,幾乎不太可能手動管理。這時要把問題倒過來想,不是因應開發出來的程式來設定配置,而是有沒有可能,因應部署會遇到的問題來設計開發?

雲原生常見的部署架構

Store config in the environment

Heroku 基於 SaaS(Software-as-a-Service) 實踐,歸納出 12 條雲原生應用的設計原則,稱為 The Twelve-Factor App,其中關於配置,Heroku 要求「代碼與配置分離」,讓一份代碼,可以部署在多個環境。在實踐上,硬編碼當然不是個選擇,但即使用配置檔來管理配置,也還是有一些問題:配置文件要放在哪個路徑呢?它的格式應該採用 JSON 還是 YAML?別忘了微服務有可能用不同語言來實現,不同語言都支援選用的格式嗎?

考量這些問題,Heroku 建議將配置放在環境變數中,這確實更符合雲原生的概念。讓我們假設一個場景,現在是發佈前夕,版本要上到金絲雀部署環境,SRE 需要建立容器後,用命令行進入容器內,修改配置檔,然後重啟容器程序。這個流程聽起來繁複且不太合理。如果好一點,SRE 將這些流程寫成腳本,讓腳本自動修改配置,那當配置檔的路徑或格式變更時,腳本也要跟著異動,這會產生摩擦力,阻止開發者對配置的修改。即使在最好的狀況下,SRE 在啟動容器時指定配置檔,也需要將配置檔事先放入 pod 中。可以看到,單單是路徑跟格式這兩點,就會生出複雜度。

相較下,環境變數是鍵值對結構,更清晰易讀,更重要的是,它更容易標準化,不會受到語言限制。假設 SRE 要在啟動容器時加入配置,他只需要使用 -e 之類的參數;或者更單純,使用 CI/CD Pipeline 管理環境變數,讓不同的 Job 自行取用。

使用 .env

然而從開發者的角度,這勢必關係到開發者體驗的異動。在原本的流程中,開發者可以用配置檔來管理配置,例如在本地環境與開發環境間切換。改成環境變數後,要切換環境需要改用命令行建立環境變數,不容易在專案中管理。

一個折衷的辦法是將環境變數寫成腳本導出,類似

# env.sh

export API_GATEWAY_ENV=dev
export API_GATEWAY_CORS_ALLOW_ORIGINS=https://api.gateway.com
export API_GATEWAY_LOG_LOG_LEVEL=0
export API_GATEWAY_LOG_PRETTY_OUTPUT=false

然後在執行程式前,先執行腳本導出環境變數,例如修改 makefile 為

all: api-gateway

api-gateway:
    source configs/env.sh
    go run main.go

這是從專案著手,解決開發配置問題。相應的,也要求所有專案都採用同樣的慣例,統一 makefile 的寫法。如果想進一步降低複雜度,使用 codebase 來解決開發配置的問題,應該要怎麼做呢?有沒有可能對應配置檔的寫法,讓程式啟動時從檔案讀取環境變數?這裡可以用上 joho/godotenv 這個第三方庫。

用法是執行 func 後載入環境變數,如果沒指定檔案,載入 .env,.env 在這裡扮演了類似配置檔的角色

package main

import (
    "log"
    "os"
    "github.com/joho/godotenv"
)

func main() {
    err := godotenv.Load()
    if err != nil {
    log.Fatal("Error loading .env file")
    }
    s3Bucket := os.Getenv("S3_BUCKET")
    secretKey := os.Getenv("SECRET_KEY")
    // now do something with s3 or whatever
}

godotenv 的原理是包裝了標準庫 Env 相關 func

func loadFile(filename string, overload bool) error {
    envMap, err := readFile(filename)
    // ...
    currentEnv := map[string]bool{}
    rawEnv := os.Environ()
    for _, rawEnvLine := range rawEnv {
        key := strings.Split(rawEnvLine, "=")[0]
        currentEnv[key] = true
    }
    for key, value := range envMap {
        if !currentEnv[key] || overload {
            os.Setenv(key, value)
        }
    }
    return nil
}

而 .env 的內容則是

# .env file

API_GATEWAY_ENV=dev
API_GATEWAY_CORS_ALLOW_ORIGINS=https://api.gateway.com
API_GATEWAY_LOG_LOG_LEVEL=0
API_GATEWAY_LOG_PRETTY_OUTPUT=false

如果環境變數已經存在,使用原本的環境變數,如果環境變數不存在,套用 .env 中的設定。

但這樣畢竟還是需要手動指定,如果我們想套用「約定優於配置」的原則,讓程式自動載入 .env 的話呢?在 godotenv 底下有個 package 可以用來自動載入

import _ "github.com/joho/godotenv/autoload"

利用的是 init() 的機制

func init() {
    godotenv.Load()
}

使用 .env 的另一個優點是,當開發者需要跟 SRE 溝通要設置的環境變數時,他們可以基於同一份檔案來討論。有必要的話,.env 中還可以加上範例跟註解,幫助 SRE 理解環境變數的用途。

Load environment variable as a struct

讓我們來看看在 Go 的應用程式中,要如何使用這些環境變數,當然,既然它已經是環境變數了,我們可以用標準庫的 os.Getenv 來讀取

envName := os.Getenv("API_GATEWAY_ENV");

可是假設,你有超過 10 個以上的環境變數,這會變成是一件苦差事,而且難以維護。如果可以,我們希望用一個 struct 來存放環境變數,讓程式在啟動直接讀進 struct,省下後面轉換的功夫

這裡要用到另一個庫 viper

viper 是設計來處理配置,要從環境變數載入配置,可以用

type LogConfig struct {
    Level        int    `yaml:"level" mapstructure:"level"`
    PrettyOutput string `yaml:"pretty_output" mapstructure:"pretty_output"`
}

type Config struct {
    Env             string    `yaml:"env" mapstructure:"env"`
    CorsAllowOrigin string    `yaml:"cors_allow_origin" mapstructure:"cors_allow_origin"`
    Log             LogConfig `yaml:"log" mapstructure:"log"`
}

func main() {
    b, _ := yaml.Marshal(&Config{})
    defaultConfig := bytes.NewReader(b)
    viper.SetConfigType("yaml")
    viper.MergeConfig(defaultConfig)
    viper.AutomaticEnv()
    viper.SetEnvPrefix("API_GATEWAY")
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    fmt.Println(viper.AllSettings())

    _config := &Config{}
    if err := viper.Unmarshal(&_config); err != nil {
        return
    }
    fmt.Println(_config)
}

坦白說,流程還是有些複雜。因為 viper 是採用覆寫的方式,所以前面要先設定個 yaml 格式的 config 當預設;再用 AutomaticEnv 跟 Unmarshal 把環境變數的值反序列化到 struct 內。為了跟其他環境變數區別,應用相關的環境變數最好加上前綴。

執行結果可以得到

go run main.go
&{dev [https://api.gateway.com](https://api.gateway.com) {0 true}}

viper v2 可能會讓流程更開發友善些,但在有更好的選擇前,可以先用這方式來降低讀取配置的維護成本。

小結

這篇嘗試從「Design for Operation」的角度梳理配置設計。不得不說環境變數是個相當漂亮的提案,小小的改動,大大的效益,既符合配置需求,又能簡化原本流程,讓概念變得更加清晰。我們可以注意到,在雲原生時代,有些責任會從原本的位置左移,而面對這樣的轉變,開發者也需要調整思維與手中的彈藥庫,嘗試從設計,而不是從工具來解決問題。

Reference

Read more

Weekly Issue 第 9 期:Ghost 發布 6.0 版本

Ghost Release 新版了!距離上次大版號更新,已經過了 3 年多,這幾年來,創作者經濟變化得很快,Ghost 也嘗試讓創作者更容易經營自己的內容。 我會等 6.0 發布一陣子,穩定下來後才會更新。很期待他們下一步會是什麼。 🗞️ 熱門新聞 Ghost 6.0 Ghost Release 6.0。 兩個重量級更新:支援 ActivityPub,讓 Ghost 可以 Leverage 社群媒體分發渠道;以及內建 Analytics,支援流量分析。這剛好就是兩個我最想要的功能,Great Work。 常說經營內容的痛點在,不知道如何發佈內容,不知道訪客從哪來。當然這都可以用工具協助,例如設定 GA、或者使用 Postiz 等來經營社群,可是我覺得一個好的平台應該要替創作者處理掉這些事,Ghost

By Ken Chen

Weekly Issue 第 8 期:數位時代的遷徙自由

以前在開發內容平台產品時,常常想,如果有天我們的使用者要離開平台,他們擁有自由嗎?在現代,數位創作者有點像是佃農,替平台生產內容,可是因為數位落差,他們沒有移動的能力。 隨著時代進步,法規應該要與時俱進,這期選了數位部的公告草案,告訴我們科技與制度可以如何相輔相成。 另外,從本期開始,加入了目錄大綱,希望讓讀者閱讀時能更容易在不同議題間切換。 🗞️ 熱門新聞 社交資料可攜權與互通性 在唐鳳那看到這則消息,最近衛城出版編輯的帳號被無預警停權,引發討論,我自己也常常焦慮,當使用這些便利的平台服務時,我們是不是交出一些沒意識到的權利? 身為個人,可行的策略是,在發布內容到平台前,先保留一份在自己手中,但這其中的不平等顯而易見。《數位選擇法案》讓我理解到,創作者有機會在一個更好更平等的環境下創作。 我希望台灣也能有這樣的一天。 I gave the AI arms and legs – then it rejected me 在 HN 上看到的新聞,有名開發者發現自己的函式庫被用在 Claude

By Ken Chen

Weekly Issue 第 7 期:從 GitHub Spark 看 Prompt 工程

近期開始有人建議用 Context Engineering 來取代 Prompt Engineering,的確相較於 Prompt,Context 是更精確的用詞。前一期也提到,當 Duolingo 的 CEO 被問到 AI 是否只是模型套皮時,他也說模型一定有影響,但更多是關乎你的 Context。 那麼,業界現在是如何看待 Prompt 的呢?Github Spark 跟 V0 的例子或許能提供一些參考。 🗞️ 熱門新聞 Using GitHub Spark to reverse engineer GitHub Spark GitHub Spark 最近推出公開預覽,讓你可以用 prompt 直接開發應用。 作者用逆向工程,找出 Spark 的 system

By Ken Chen

Weekly Issue 第 6 期:Duolingo CEO 看 AI 與遊戲化

現在是 AI 時代,大家都在想怎麼讓自己的產品跟 AI 掛勾,但具體要怎麼做呢?背後的思考有哪些?Duolingo 給出他們自己的觀點。 例如,現在的產品是否只是 AI 套皮,你接收使用者的問題,套上自己的提詞後,拿去給 OpenAI,要它回答你?在現在百家爭鳴的情況下,選擇哪個模型會有差嗎?AI 能帶來新用戶與新營收嗎?等等。 另外本週也選了一篇少數派的文章,談 AI 對 RSS 的影響,對 RSS 未來方向有興趣的人不妨看看。 🗞️ 熱門新聞 Duolingo CEO Luis von Ahn wants you addicted to learning Duolingo CEO 專訪,相當紮實,推薦閱讀。 「對我們來說,

By Ken Chen