# 在monorepo裡進行config管理 --- ## About Speaker 葉家郡 / xnum@GitHub 天鏡科技 - Vice President of Engineering COBINHOOD - Backend developer jun.yeh@skymirror.com.tw --- ## 什麼是monorepo ![image alt](https://assets.toptal.io/images?url=https%3A%2F%2Fuploads.toptal.io%2Fblog%2Fimage%2F129133%2Ftoptal-blog-image-1550062710292-1db9f4f6ffc00e30acb3a43b3504c4a9.png) https://www.toptal.com/front-end/guide-to-monorepos --- ## single-repo, multi-repo, monorepo - 有很多有趣的挑戰 - config - deploy - dependency - unittest - lint, vet style --- ## 今天只討論config management 如果你不假思索的這樣做了,在single-repo沒什麼問題,但在monorepo會逐漸變成天坑 `addr := os.GetEnv("XXX_ADDR")` 執行到一半才發現這個參數忘了填,Panic! --- 或是專門做了一個package來統一管理,讓所有需要config的code自己過來抓: ```go package config // or env func (c *config) GetXXXAddr() string {} func (c *config) GetZZZToken() string {} ``` 接著會慢慢發現,怎麼某個執行檔不需要`GetZZZToken`,卻還是需要餵給他。 結果所有執行檔都餵了一堆參數進去。 歡迎來到deploy地獄。 --- root cause在於:沒有考慮package依賴 ###### 如果你三不五時遇到import cycle的編譯失敗,那很有可能是package層級沒有經過仔細規劃。 --- 在clean arch實作篇chapter 9提到 - config component應該獨立於架構外 - 它負責初始化所有比他內圈的物件 - 它也負責組裝這些物件 --- ### 所以: 如果以clean arch來說,config應該被擺在哪圈呢? --- 最外圈 (永遠是外圈會import內圈) ![](https://github.com/mattia-battiston/clean-architecture-example/raw/master/docs/images/clean-architecture-diagram-2.png) --- ### 在Golang裡面可以怎麼實作? (不借助reflection, codegen, generic各派黑魔法) --- ### 先來看看怎麼要求使用者提供config ###### 當你想設計一個函式庫,需要提供使用者填寫設定,可能會這樣做: ```go // github.com/redis/go-redis/v9 rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB }) ``` ```go // gorm.io/gorm dsn := "user:pass@tcp(127.0.0.1:3306)/dbname" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) ``` --- 如果你自己的函式庫設計良好,應該也會用類似的作法:叫他傳進來就對了 --- #### 當我們要使用時,在single-repo,這件事或許不會太複雜。 ```go type Config struct { DBPath string RedisURL string } func main() { viper.SetConfigName("config") viper.AddConfigPath(".") viper.ReadInConfig() var config Config viper.Unmarshal(&config) db, err := gorm.Open("mysql", config.DBPath) defer db.Close() redisClient := redis.NewClient(&redis.Options{ Addr: config.RedisURL, }) // 手動注入相依性 service.Start(db, redisClient) } ``` --- 但如果你在monorepo這樣做,會面臨幾個問題: - code base越來越龐大,可能你需要import 10個package,初始化7個service - 手寫一大堆組裝code和config struct - 只要有config,就無法直接初始化物件,需要外部注入object或傳入config - DI框架是一種解法 --- 我的解法是,設計一個中立的package負責讓其他package註冊參數: ```go package cache type config struct { Addr string Password string } var defaultConfig config func init() { boot.Register(&defaultConfig) } func (cfg *config) CliFlags() []cli.Flag { var flags []cli.Flag flags = append(flags, &cli.StringFlag{ Name: "redis-addr", Value: "redis:6379", EnvVars: []string{"REDIS_ADDR"}, Destination: &cfg.Addr, }) // ... return flags } ``` --- - 藉由init的特性來自動註冊用到的package參數 - 藉由消除依賴,重構原本不好的架構,讓api變得更完善 - 談架構或許很抽象,但多餘的config是肉眼可見的煩 --- usecase1 - 把直接import轉成interface (bad) ```go package user import ( "gobe/email/smtp" ) func SendResetPasswordEmail(email string) error { sender := smtp.NewSender() } ``` --- (good) ```go package user type EmailSender interface { SendEmail(email, subject, body string) error } func SendResetPasswordEmail( sender EmailSender, email string) error {} ``` --- usecase2 - 把外部函式庫struct當成依賴的目標 (bad) ```go func GetLatestExchangeRate() (*ExchangeRate, error) { redis := cache.GetRedis() // ... } ``` --- (good) ```go func GetLatestExchangeRate( c *redis.Client) (*ExchangeRate, error) { ... } ``` --- ## 如何實作 基於 `github.com/urfave/cli/v2` 實現 read from {args, env, file}, default-value, usage, required ``` package boot // import "github.com/sky-mirror/boot" func Finalize(c *cli.Context) error func Flags() []cli.Flag func Initialize(c *cli.Context) error func Register(f CliFlager) type CliFlager interface { CliFlags() []cli.Flag } ``` --- 除此之外還加上了 `Before()` `After()` hook 方便在程式正式啟動前先檢查資料庫連線或參數設定 ``` func IsAfterer(Afterer) func IsBeforer(Beforer) type Afterer interface{ ... } type Beforer interface{ ... } ``` --- ```go func (cfg *config) Before(c *cli.Context) error { if cfg.Disable { return nil } if len(cfg.MasterName) > 0 { InitializeSentinel( cfg.Addr, cfg.Password, cfg.MasterName, cfg.SentinelPassword, ) } else { Initialize(cfg.Addr, cfg.Password) } return nil } ``` --- ## 工商 - 下禮拜的Talk CNTUG 2023/09 meetup (線上活動) 9/21 (四) 19:00 ~ 21:00 ##### 在地端自建 Kubernetes 叢集之旅 - 上線前的那些事 ###### 在 2021 年我們將程式交易系統從實體機 / VM 架構轉換到 Kubernetes 上運作,一路歷經規劃、架構設計、上線到這兩年間的維護,累積了許多經驗。這次議程分享的內容涵蓋了當初決定轉換的因素,如何建構一套在地端運行的高可用系統,以及和雲端服務比起來實際需要解決的挑戰。 --- ## 工商 - 公司簡介 天鏡科技 (大神徵求中) - 程式交易系統客製化服務 - 研發期貨交易的風控中台 - 程式交易雲端服務 可以直接email給我 (jun.yeh@skymirror.com.tw) --- # QA ![](https://media.discordapp.net/attachments/1151020746802462811/1151020938754797699/QA9F5gACJztcciR6gBiBDOUBAiRD3U5eDD1AD9AD0XmAAInO1xyJHqAH6IEM5QECJEPdTl4MPUAP0APReYAAic7XHIkeoAfogQzlgf8HUTOxWijY7T0AAAAASUVORK5CYII.png)
{"title":"在monorepo裡進行config管理","breaks":true,"description":"View the slide with \"Slide Mode\".","contributors":"[{\"id\":\"27ff3d0c-72ef-430c-805f-9efc1581d8cf\",\"add\":7423,\"del\":4387}]"}
    723 views