摸魚心法第一章——和配置文件說拜拜
為了能摸魚我們團隊做了容器化,但是帶來的問題是服務配置文件很麻煩,然后大家在群里進行了“親切友好”的溝通
圖片
圖片
圖片
圖片
對比就對比,簡單對比下獨立配置中心和k8s作為配置中心的區別
獨立配置中心 | k8s作為配置中心 | |
學習成本 | 1.運維要學習搭建、維護 2.研發和研發都需要學習配置中心的工具、系統如何使用 | 1.熟悉yaml/json語法即可 2.研發只需要解析環境變量,無需關注注入細節 |
適配工作量 |
|
|
集群維護成本 | 額外維護成本 | 保證集群etcd穩定即可,無額外成本 |
服務發現 | 支持服務發現 | 支持服務發現 |
云資源費用 | 增加成本 | 無額外成本 |
對比結果出來后,群里的研發也覺得k8s作為配置中心不錯了
圖片
圖片
我這里想問問在看文章的同學:是不是都覺得運維的東西很簡單?還有是不是個鍋都甩給運維?像這樣的研發你身邊多么還是說你也是這樣的研發?
繼續今天的話題,既然服務要在k8s里運行,同時也要把k8s作為配置中心使用,那服務適配需要做些啥? 咱們先列一個清單
圖片
服務要優雅的適配容器化環境,需要解決以下問題
- 避免繁瑣的定義和解析服務環境變量
- 服務在本地調試下和容器環境運行兩種場景下,對于環境變量的解析需要無縫切換
- 服務的dockerfile可根據服務信息自動生成,盡量避免人工操作
1.首先說環境變量的問題
從本地開發和容器運行兩個角度來看,本地開發的時候讀取配文件比讀取環境變量方便,容器運行中讀取環境變量比讀取配置文件方便,我想說你倆擱這卡bug呢?
但是這個問題其實不難,解決邏輯也很簡單。那就是采用覆寫的思路,如果環境變量里讀取到了值就用環境變量的,否則就用代碼里的值。
那按照這個思路是得有一個配置文件,然后服務讀取這個配置文件?可惜這個和我們團隊的一個追求相違背——代碼及文檔
說到文檔,插個題外話,對于寫文檔這事兒。。。
看別人的東西,你TM文檔呢?
做自己的東西,這TM還用寫文檔?
回到環境變量這個問題來,其實在代碼里面定義變量并提供覆寫的能力就足夠了??紤]到在本地開發調試的時候,需要頻繁修改變量的值,無論是修改代碼里的變量值或者修改環境變量還是稍顯麻煩,所以覆寫的信息可以來源于環境變量或一個覆寫的變量文件
流程如下
圖片
針對提供反射機制的編程語言結合一定的規則,環境變量的key可以直接從定義的結構體里獲取無需額外維護。無反射類型的編程語言也可以按照這個思路實現,只是稍顯麻煩。這樣環境變量的問題解決了,然后就是dockerfile的問題
2.dockerfile如何自動生成
我們再看看剛才列的清單
圖片
首先說說核心問題如何編譯,有兩種方式
1.直接在dockerfile里面寫編譯過程
直接手寫dockerfile沒有問題,因為服務的開發人員最清楚自己的服務需要怎么編譯,但是不同的服務總會出現差異化的編譯過程,這樣從代碼自動生成dockerfile的角度來講不可控
2.makefile文件
通過makefile來執行編譯步驟就解決了差異化的問題,在dockerfile里只需要執行類似make build的固定命令便完成了服務編譯全過程。自動化工具按照固定的dockerfile模板生成文件,makefile完成具體的編譯過程,這樣服務編譯與工具完美解耦
核心問題解決了,至于dockerfile如何生成,每種編程語言都可以采用自身語言提供的模板庫進行生成dockerfile了。即便不用模板,拼接字符串也是可以的,條條大路通羅馬。
然后我們再談談為什么會有編譯鏡像和運行鏡像的區別,我們看看下面這個流程
圖片
從流程可以看出,服務在k8s里面啟動時會從鏡像倉庫拉去服務鏡像,這里存在一個網絡傳輸的問題,內網都不說了,如果從公網拉鏡像,并發量高一點,拉的再頻繁點,不管是固定帶寬還是按流量計費,老話說得好,這不就是小刀剌了貔貅腚——拉的都是錢
所以我們期望的是鏡像足夠小,這樣在部署服務的時候更快更省錢,尤其是首次部署的時候(這里涉及到docker 分層的問題不做展開)。編譯鏡像一般都非常龐大并不適合作為運行鏡像使用,只需要提供編譯環境,編譯完成后將編譯后的文件放入一個很小的運行鏡像中即可
以我們團隊采用的是Golang語言為例,編譯鏡像目前采用的1.20.5-buster,AMD64的鏡像大小為275M,ARM的鏡像大小為264M,運行鏡像采用的是gcr.io/distroless/static-debian11,最終運行鏡像大小在20M左右,這不得起飛了啊,拉鏡像就跟玩兒一樣。
好了,今天的文章主要分享了在容器化環境下,通過拋棄服務配置文件而采用環境變量的形式來解決配置注入的問題,自動生成dockerfile需要避免的坑,希望對在走容器化道路的同學有所幫助。如果大家想聽聽其他的可以留言或者私信我們。
接下來就是Golang的福利時間,我們將這個變量注入的庫進行了開源?,F在用gin框架寫個demo來演示環境變量注入和生成dockerfile。(我們在gin框架上加了一點點東西,這樣更好用)
首先創建一個工程目錄
圖片
global/config.go這個文件長這樣
package global
import (
"github.com/kunlun-qilian/conflogger"
"github.com/kunlun-qilian/confserver"
"github.com/kunlun-qilian/confx"
)
func init() {
confx.SetConfX("demo-docker", "..")
confx.ConfP(&Config)
}
var Config = struct {
Logger *conflogger.Log
Server *confserver.Server
TestEnv string `env:""`# 環境變量標記,只要有這個標記則支持注入
}{
Server: &confserver.Server{
Port: 80,
Mode: "debug",
},
TestEnv: "123",
}
github.com/kunlun-qilian/confx
這個庫的作用就是注入環境變量和生成dockerfile,
單獨出來了一個庫,只要是這個工程目錄結構都可以使用
運行之后會生成config/default.yml,這個環境變量文件就是每次啟動服務后根據上述global/config.go文件自動生成的默認配置文件,這個文件是作為后續本地覆寫配置文件的藍本,免得不知道環境變量是啥,環境變量規則是“服務名__環境變量名”
DEMO_DOCKER__Logger_Level: ""
DEMO_DOCKER__Logger_Output: Always
DEMO_DOCKER__Server_Mode: debug
DEMO_DOCKER__Server_UseH2C: "false"
DEMO_DOCKER__TestEnv: "123"
demo里加入了一個接口來返回TestEnv的值
圖片
在本地開發的時候需要覆寫默認值的時候,只需要在config目錄下加入一個叫做 local.yml(這個放gitignore里)的文件并添加想替換的值
圖片
重新運行一下服務,再看接口,變量被local.yml里面的值替換了
圖片
然后我們再通過當前命令行會話中注入一個環境變量,然后啟動
export DEMO_DOCKER__TestEnv=terminal_789 && go run main.go
值又被替換成了環境變量的值,有了這個還要啥自行車?
圖片
再看看生成的dockerfile,下面這個就是自動生成的默認dockerfile
FROM dockerproxy.com/library/golang:1.20-buster AS build-env
FROM build-env AS builder
WORKDIR /go/src
COPY ./ ./
# build
RUN make build WORKSPACE=demo-docker
# runtime
FROM gcr.dockerproxy.com/distroless/static-debian11
COPY --from=builder /go/src/cmd/demo-docker/demo-docker /go/bin/demo-docker
EXPOSE 80
ARG PROJECT_NAME
ARG PROJECT_VERSION
ENV PROJECT_NAME=${PROJECT_NAME} PROJECT_VERSION=${PROJECT_VERSION}
WORKDIR /go/bin
ENTRYPOINT ["/go/bin/demo-docker"]
上文中提到的幾個配置信息,咱們定義了一個結構,包含了編譯鏡像,運行鏡像,GOPROXY代理,openapi文件,奧,差點忘了這篇不涉及到openapi,篇幅有限這個在后續篇章里講,你們懂的
type DockerConfig struct {
BuildImage string
RuntimeImage string
GoProxy GoProxyConfig
Openapi bool
}
type GoProxyConfig struct {
ProxyOn bool
Host string
}
在global/config.go中的init方法中,留了入口
func init() {
confx.SetConfX("demo-docker", "..", confx.DockerConfig{
BuildImage: "private-harbor.xxx.com/xxx/builder:v1.0.0",
RuntimeImage: "private-harbor.xxx.com/xxx/runtime:v1.0.0",
GoProxy: confx.GoProxyConfig{
ProxyOn: true,
Host: "https://goproxy.cn,direct",
},
})
confx.ConfP(&Config)
}
然后我們再重新運行一下看看結果,編譯鏡像、運行鏡像、代理都更新了
FROM private-harbor.xxx.com/xxx/builder:v1.0.0 AS build-env
FROM build-env AS builder
ARG GOPROXY=https://goproxy.cn,direct
WORKDIR /go/src
COPY ./ ./
# build
RUN make build WORKSPACE=demo-docker
# runtime
FROM private-harbor.xxx.com/xxx/runtime:v1.0.0
COPY --from=builder /go/src/cmd/demo-docker/demo-docker /go/bin/demo-docker
EXPOSE 80
ARG PROJECT_NAME
ARG PROJECT_VERSION
ENV PROJECT_NAME=${PROJECT_NAME} PROJECT_VERSION=${PROJECT_VERSION}
WORKDIR /go/bin
ENTRYPOINT ["/go/bin/demo-docker"]
如果服務有多個端口怎么處理?還是從global/config.go中下手,增加一個 TestPort 的變量,tag中加上 `env:"opt,expose"`
var Config = struct {
Logger *conflogger.Log
Server *confserver.Server
TestEnv string `env:""`
TestPort int `env:"opt,expose"` # 看這里,看這里
}{
Server: &confserver.Server{
Port: 80,
Mode: "debug",
},
TestEnv: "123",
TestPort: 9090,
}
然后咱們再運行一次,9090端口暴露出來了,這帶手表了,帶啥手表了?
FROM private-harbor.xxx.com/xxx/builder:v1.0.0 AS build-env
FROM build-env AS builder
ARG GOPROXY=https://goproxy.cn,direct
WORKDIR /go/src
COPY ./ ./
# build
RUN make build WORKSPACE=demo-docker
# runtime
FROM private-harbor.xxx.com/xxx/runtime:v1.0.0
COPY --from=builder /go/src/cmd/demo-docker/demo-docker /go/bin/demo-docker
EXPOSE 9090
EXPOSE 80
ARG PROJECT_NAME
ARG PROJECT_VERSION
ENV PROJECT_NAME=${PROJECT_NAME} PROJECT_VERSION=${PROJECT_VERSION}
WORKDIR /go/bin
ENTRYPOINT ["/go/bin/demo-docker"]
最后貼上全球最大同。。不對,是github鏈接,后續我們還會逐步開源一些工具
工具包confx: https://github.com/kunlun-qilian/confx
本文的demo: https://github.com/kunlun-qilian/gin-demo