要什么樣的配置服務(wù)
我們的項(xiàng)目以前都是由各開發(fā)人員自己寫一個(gè)config.js
或者app.conf
來(lái)管理項(xiàng)目的配置信息某饰,經(jīng)常出現(xiàn)下面的問(wèn)題,我需要一個(gè)配置服務(wù)來(lái)解決它
1. 杜絕犯低級(jí)錯(cuò)誤
如果沒(méi)犯低級(jí)錯(cuò)誤的話渴逻,一般也不會(huì)出現(xiàn)什么問(wèn)題猿妈,什么是低級(jí)錯(cuò)誤呢精钮,就拿前端來(lái)說(shuō)袍啡,以前引入配置一般這樣
if( env === 'dev' ){
configs = import('./dev.config')
}else if( env === 'test' ){
configs = import('./test.config')
}else{
configs = import('./prod.config')
}
本地為了解決測(cè)試環(huán)境的一個(gè)bug,本來(lái)env
應(yīng)該為dev
的珠增,結(jié)果為了用測(cè)試環(huán)境的配置超歌,強(qiáng)制把env
設(shè)置為test
了,然后問(wèn)題解決了蒂教,測(cè)試環(huán)境測(cè)試也沒(méi)問(wèn)題了巍举,發(fā)布的時(shí)候就直接更新到正式環(huán)境了,結(jié)果就gg了凝垛,一查原來(lái)是env
忘記改了懊悯。
2. 不希望當(dāng)前環(huán)境看到其它環(huán)境的配置值
接著杜絕犯低級(jí)錯(cuò)誤
,上面那樣寫前端配置還會(huì)有個(gè)問(wèn)題梦皮,就是前端按F12
可以看到我們測(cè)試服務(wù)器的一些信息炭分,比如某個(gè)網(wǎng)頁(yè)的測(cè)試前端域名,這些信息我其時(shí)是不想外面看到的
3. 配置冗余問(wèn)題
假如我有一個(gè)基礎(chǔ)服務(wù)A
剑肯,B
和C
模塊依賴服務(wù)A
的域名捧毛,那么就要在這兩個(gè)模塊下各寫一個(gè)域名配置
export default {
A_Domain:"https://xxx.readboy.com"
}
看起來(lái)是沒(méi)問(wèn)題,其實(shí)這里我覺得問(wèn)題很多让网,假如我的A
模塊域名更新了呀忧,我只知道B
模塊依賴這個(gè)域名,通知了B
模塊開發(fā)人員修改溃睹,然后更新了而账,舊域名移除了,那么C
模塊肯定會(huì)有問(wèn)題因篇,如果能由各開發(fā)人員維護(hù)自己的配置泞辐,依賴項(xiàng)目不需要設(shè)置,直接配置依賴竞滓,直接引用就好了
4. 建立配置和項(xiàng)目依賴
這個(gè)問(wèn)題是基于配置冗余問(wèn)題
的咐吼,我希望我可以知道這個(gè)配置(A
模塊域名)有哪些項(xiàng)目依賴了,如果這個(gè)配置更新了商佑,我希望依賴的項(xiàng)目能自動(dòng)拉取最新的配置并部署
總結(jié)
基于上面的問(wèn)題锯茄,網(wǎng)上大概找了下相關(guān)的解決方案都不太滿意(自己能力太水-),
- 我不想要運(yùn)行時(shí)動(dòng)態(tài)拉取配置莉御,總感覺這個(gè)可靠性不強(qiáng)(小廠撇吞,資源有限)
- 獲取配置要足夠簡(jiǎn)單,不要再配環(huán)境礁叔,還要裝什么客戶端牍颈,我希望一個(gè)
http
就能解決,來(lái)吧琅关,搞起來(lái)
配置服務(wù)實(shí)現(xiàn)的基本功能
項(xiàng)目管理
新建一個(gè)項(xiàng)目煮岁,指定這個(gè)項(xiàng)目有哪些環(huán)境,一般固定prod
,env
,dev
配置管理
項(xiàng)目指定幾個(gè)環(huán)境涣易,相應(yīng)項(xiàng)目下的配置就有相應(yīng)的環(huán)境画机,開發(fā)人員填入相應(yīng)的值就行
這基本就完成了項(xiàng)目的配置存儲(chǔ)功能,怎么用呢新症?這個(gè)時(shí)候就需要定義一個(gè)配置描述文件(.config.yml
)了步氏,基本內(nèi)容就是你要哪些項(xiàng)目的哪個(gè)配置,由配置服務(wù)接口根據(jù)描述文件自動(dòng)返回配置值
運(yùn)行效果(gif圖徒爹,有點(diǎn)大)
配置更新自動(dòng)部署所有依賴的項(xiàng)目
format: json
itemFormat: 配置key處理規(guī)則荚醒,看下面
envBranch:
test: test
prod: master
project:
name: readboyconfig
description: 項(xiàng)目描述
id: gitlab項(xiàng)目ID
configs:
projecta:
- configa
- configb
- configc
projectb:
- configb
- configc
- configd
env: test(環(huán)境在部署的時(shí)候動(dòng)態(tài)寫入)
format
表示輸出文件格式,值如下:
js
(es6)
json
ini
yml
project
表示工程信息隆嗅,主要是相關(guān)配置更新后用于重啟服務(wù)用的
id
項(xiàng)目在gitlab中的ID
name
項(xiàng)目名稱
description
項(xiàng)目描述信息
envBranch
環(huán)境和分支的對(duì)應(yīng)關(guān)系界阁,主要是相關(guān)配置更新后用于重啟服務(wù)用的
格式:
env
: branch name
env
表示要輸出的環(huán)境配置,值對(duì)應(yīng)后臺(tái)錄入的env
itemFormat
表示生成配置項(xiàng)的方式胖喳,取值如下
ignore
忽略項(xiàng)目名
prefix
項(xiàng)目名+配置名
dot
項(xiàng)目名+ .
+連接配置
tree
保持層級(jí)結(jié)構(gòu)
project_without_prefix
當(dāng)前項(xiàng)目不加前綴泡躯,其它項(xiàng)目同 prefix
project_without_dot
當(dāng)前項(xiàng)目不用.
連接,其它項(xiàng)目同 dot
project_without_tree
當(dāng)前項(xiàng)目不保持層級(jí)結(jié)構(gòu)丽焊,其它項(xiàng)目同 tree
configs
項(xiàng)目配置信息较剃,具體參考后臺(tái)數(shù)據(jù)
CI配置
所有配置信息更新后,會(huì)觸發(fā)依賴項(xiàng)目的CI
并攜帶參數(shù) readboy_trigger=config
粹懒,如果某些階段在自動(dòng)觸發(fā)的CI中不要執(zhí)行重付,可以在CI
文件中加上條件
only:
variables
- $readboy_trigger == 'config'
或
except:
variables
- $readboy_trigger == 'config'
yml格式說(shuō)明結(jié)束
獲取配置
curl "${CONFIG_SERVER}" -fd "`cat .config.yml`" > src/config/index.js
CONFIG_SERVER
這個(gè)接口要實(shí)現(xiàn)功能主要有:
- 根據(jù)環(huán)境來(lái)獲取描述文件的值
- 建立相關(guān)配置和
gitlab
項(xiàng)目的依賴關(guān)系,當(dāng)相關(guān)配置更新的時(shí)候凫乖,自己觸發(fā)相關(guān)項(xiàng)目的gitlab CI
确垫,實(shí)現(xiàn)自動(dòng)拉取最新配置,自動(dòng)部署
配置管理后臺(tái)的配置值更新的時(shí)候帽芽,程序會(huì)檢測(cè)哪個(gè)環(huán)境的值變了删掀,并獲取依賴該屬性的gitlab
項(xiàng)目信息,利用gitlab
的API
导街,API鏈接披泪,自動(dòng)創(chuàng)建一個(gè)pipeline
來(lái)拉取配置,部署程序搬瑰。至此款票,一個(gè)簡(jiǎn)單的基于gitlab
的配置服務(wù)就完成了控硼。
當(dāng)然要完全把這個(gè)配置服務(wù)利用起來(lái),還需要跟開發(fā)人員做要求艾少,配置使用卡乾,依賴項(xiàng)目的配置絕對(duì)不要再寫一遍,不然配置變了缚够,更新就會(huì)出問(wèn)題幔妨。
一些CI配置
前端CI文件
image: node:8.9.3
stages:
- build
- buildImage
- deploy
variables:
ALI_REGISTRY_HOST: ""
ALI_REGISTRY_IMAGE: ""
ALI_SERVICE_NAME: ""
PROD_NAMESPACE: ebag-prod
BETA_IMAGE: beta-$CI_COMMIT_SHA
BETA_LATEST: beta-latest
masterbuild:
stage: build
script:
- printf "\nenv:" >> .config.yml
- printf " prod" >> .config.yml
- curl "${CONFIG_SERVER}" -fd "`cat .config.yml`" > src/config/index.js
- npm install --no-optional
- npm run build
- node ./tools/generate.config.js
- qshell='./tools/qshell-linux-x64'
- chmod a+x "${qshell}"
- ${qshell} account "${QINIU_AK}" "${QINIU_SK}"
- ${qshell} qupload 8 ./qiniuconfig
only:
- master
artifacts:
expire_in: 1 week
paths:
- dist
imagebuild:
stage: buildImage
image: docker:latest
script:
- docker login -u $ALI_REGISTRY_USER -p $ALI_REGISTRY_PASSWORD $ALI_REGISTRY_HOST
- docker build -t $ALI_REGISTRY_IMAGE:$BETA_IMAGE -t $ALI_REGISTRY_IMAGE:$BETA_LATEST -f docker/Dockerfile .
- docker push $ALI_REGISTRY_IMAGE:$BETA_IMAGE
- docker push $ALI_REGISTRY_IMAGE:$BETA_LATEST
only:
- master
前端Dockerfile
FROM nginx:latest
COPY dist /usr/share/nginx/html
COPY docker/default.conf /etc/nginx/conf.d
服務(wù)端CI文件
image: docker:git
stages:
- build
- deploy
variables:
ALI_REGISTRY_IMAGE: "..."
ALI_SERVICE_NAME: "readboyconfig"
build_test:
stage: build
script:
- echo `date "+%Y%m%d%H%M%S"` > ./datetime
- docker build --build-arg APP_ROOT=/go/src/$CI_PROJECT_NAME --build-arg EXPOSE_PORT=6381 --build-arg DREAM_ENV=test --build-arg TAG_NAME=${CI_COMMIT_SHA} --build-arg CONFIG_SERVER=${CONFIG_SERVER} -t ${ALI_REGISTRY_IMAGE}:latest -t ${ALI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}_`cat ./datetime` -f docker/Dockerfile .
- docker login -u $ALI_REGISTRY_USER -p $ALI_REGISTRY_PASSWORD $ALI_REGISTRY_HOST
- docker push ${ALI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}_`cat ./datetime`
- docker push ${ALI_REGISTRY_IMAGE}:latest
artifacts:
expire_in: 2 days
paths:
- datetime
only:
- test
deploy_test:
stage: deploy
variables:
IMAGE_NAME: ${ALI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
image: ...
before_script:
- mkdir -p ~/.kube
- echo "$TEST_KUBERNETES_CONFIG" > ~/.kube/config
- echo "$TEST_KUBERNETES_CA" > ~/.kube/ca.crt
script:
- kubectl -n ebag-test set image deployment/${ALI_SERVICE_NAME} ${ALI_SERVICE_NAME}=${IMAGE_NAME}_`cat ./datetime`
only:
- test
服務(wù)端Dockerfile文件(去掉一些非必要信息)
FROM golang:1.9.2 AS gobuild
WORKDIR ${APP_ROOT}
RUN curl "${CONFIG_SERVER}" -fd "`cat .comfig.yml`" > ./conf/dev/app.conf
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./main.go
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o migrate ./migrate.go
FROM alpine:latest
ARG APP_ROOT
ARG DREAM_ENV
ARG EXPOSE_PORT
WORKDIR /app
EXPOSE ${EXPOSE_PORT}
USER root
RUN mkdir -p ./conf/dev && touch ./conf/dev/app.conf
RUN mkdir -p ./log
VOLUME ["/app/log"]
COPY --from=gobuild ${APP_ROOT}/migrate ./migrate
COPY --from=gobuild ${APP_ROOT}/main ./main
COPY --from=gobuild ${APP_ROOT}/conf/ ./conf/
COPY --from=gobuild ${APP_ROOT}/migration/ ./migration/
COPY --from=gobuild ${APP_ROOT}/docker/start.sh ./start.sh
COPY --from=gobuild ${APP_ROOT}/bin ./bin
ENTRYPOINT ["/app/start.sh"]
注意
curl
一定要把-f
加上,不然你的CONFIG_SERVER
有bug的話谍椅,會(huì)部署一個(gè)空的配置文件到服務(wù)器上误堡,出現(xiàn)服務(wù)不可用的問(wèn)題,加上-f
雏吭,如果你的CONFIG_SERVER
有bug锁施,返回500
了,CI
會(huì)中斷執(zhí)行思恐,不會(huì)把有問(wèn)題的程序部署到服務(wù)器
為什么有要datetime
如果內(nèi)容沒(méi)變沾谜,編譯出來(lái)的鏡像名稱是一樣的,重新部署pod
會(huì)失敗胀莹,所以為了保證每次編譯出來(lái)的鏡像名稱不一樣基跑,用datetime
來(lái)記錄時(shí)間,保證部署成功