# 在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}]"}