配置存放於環境: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 第 24 期:網路的精神高地

前陣子去了雪梨一趟,跟布里斯本或台北都形成有趣的對比,旅行中也不斷在想,一座城市如何發展出自己的文化?這有點像是網路平台如何形成聚落,而又如何消亡。 很喜歡本期談知乎的一篇文章,理想主義的光輝是最吸引人的,我常在想,有沒有辦法將那座「看不見的城市」帶到真實世界中。 🗞️ 熱門新聞 A ChatGPT prompt equals about 5.1 seconds of Netflix 看到 Simon Willison 提到,如果 Sam Altman 的資訊是對的,每個 LLM 提問相當於 5.1s 的 Netflix 影片耗能。 計算的需求讓輝達跟台積電挖到金礦,那電力需求又會讓誰挖到金礦呢? ✨ 科技觀點 我们失去的不只是知乎,而是中文互联网的精神高地 「那时的知乎,更像“思想沙龙”,而非“内容平台”。」 昨天跟朋友聊天,

By Ken Chen

Weekly Issue 第 23 期:Mastodon CEO 離職感言

電子報本質是種自媒體,儘管我發文前都會確認,還因為能力所限,偶爾還是有沒做好的地方。每次遇到時我都會想,不知道其他自媒體是如何查證的呢? 現代的訊息越來越快,不只是自媒體,很多專業媒體也不見得有完備的查證能力,我猜當內容氾濫,「真實」會變得越來越有價值,最終變成一門生意。 🗞️ 熱門新聞 Explore the independent web Ghost 最新一期的電子報談到他們如何處理「內容發現」的問題。 簡單來說,他們有個內容發現工具 Ghost Explore,如果創作者願意提交自己的網站數據,他們能依照這些網站數據來推薦。再來,他們還會參考 ahrefs 的資料,判斷該網域是否具有高品質。 這比 Substack 發展社群工具,更貼近我對產品的想像。現代內容網站基本都需要演算法,這已經不是要不要,是怎麼設計的問題。 My next chapter with Mastodon Mastodon 的 CEO 即將卸任,他發了篇談談這段時間的心路歷程。

By Ken Chen

Weekly Issue 第 22 期:Google 發布 Nano Banana Pro

最近大新聞要算 Cloudflare 出問題,以及 Google 發布新的 AI 模型。新的 Nano Banana Pro 不管在一致性還是文字呈現,都出乎意料地好。如果 Google 真的能在這場 AI 大戰中笑到最後,這一定會成為商業競爭的經典案例。 🗞️ 熱門新聞 How we’re bringing AI image verification to the Gemini app Google 幾天前發布的 AI 模型太強了,各種錦上添花的稱讚就不說了,在 Simon Willison 的 Blog 看到,Google 設計出防偽機制,避免假圖到處跑。 機制有兩種,一種是在生成的內容中,插入人眼不可辨識的 SynthID,

By Ken Chen

Weekly Issue 第 21 期:JetBrains 發表 2025 Go 生態系調查

最近在讀 Tony Fadell 的 "Build",作者曾經參與過 iPhone 的開發,各種經驗談讓人嘆為觀止,例如這段:「如果故事有某個部分銜接不上,那麼產品本身也會有某個地方行不通…這便是為什麼最後 iPhone 的表面是玻璃,而不是塑膠,以及為什麼 iPhone 沒有硬體鍵盤。」 好在哪呢?好在如果能掌握這個觀念,就能知道如何「閱讀」產品,看見一個產品,就像閱讀一則故事一樣,知道它的抑揚頓挫,知道它想表現的東西。我相信每個經歷過產品開發的人,看這本書都會很有感覺。   🗞️ 熱門新聞 The Go Ecosystem in 2025: Key Trends in Frameworks, Tools, and Developer Practices JetBrains 前陣子公布 Go 生態系的調查結果。

By Ken Chen