要什么样的配置服务
我们的项目以前都是由各开发人员自己写一个config.js
或者app.conf
来管理项目的配置信息,经常出现下面的问题,我需要一个配置服务来解决它
1. 杜绝犯低级错误
如果没犯低级错误的话,一般也不会出现什么问题,什么是低级错误呢,就拿前端来说,以前引入配置一般这样
if( env === 'dev' ){
configs = import('./dev.config')
}else if( env === 'test' ){
configs = import('./test.config')
}else{
configs = import('./prod.config')
}
本地为了解决测试环境的一个bug,本来env
应该为dev
的,结果为了用测试环境的配置,强制把env
设置为test
了,然后问题解决了,测试环境测试也没问题了,发布的时候就直接更新到正式环境了,结果就gg了,一查原来是env
忘记改了。
2. 不希望当前环境看到其它环境的配置值
接着杜绝犯低级错误
,上面那样写前端配置还会有个问题,就是前端按F12
可以看到我们测试服务器的一些信息,比如某个网页的测试前端域名,这些信息我其时是不想外面看到的
3. 配置冗余问题
假如我有一个基础服务A
,B
和C
模块依赖服务A
的域名,那么就要在这两个模块下各写一个域名配置
export default {
A_Domain:"https://xxx.readboy.com"
}
看起来是没问题,其实这里我觉得问题很多,假如我的A
模块域名更新了,我只知道B
模块依赖这个域名,通知了B
模块开发人员修改,然后更新了,旧域名移除了,那么C
模块肯定会有问题,如果能由各开发人员维护自己的配置,依赖项目不需要设置,直接配置依赖,直接引用就好了
4. 建立配置和项目依赖
这个问题是基于配置冗余问题
的,我希望我可以知道这个配置(A
模块域名)有哪些项目依赖了,如果这个配置更新了,我希望依赖的项目能自动拉取最新的配置并部署
总结
基于上面的问题,网上大概找了下相关的解决方案都不太满意(自己能力太水-),
- 我不想要运行时动态拉取配置,总感觉这个可靠性不强(小厂,资源有限)
- 获取配置要足够简单,不要再配环境,还要装什么客户端,我希望一个
http
就能解决,来吧,搞起来
配置服务实现的基本功能
项目管理
新建一个项目,指定这个项目有哪些环境,一般固定prod
,env
,dev
配置管理
项目指定几个环境,相应项目下的配置就有相应的环境,开发人员填入相应的值就行
这基本就完成了项目的配置存储功能,怎么用呢?这个时候就需要定义一个配置描述文件(.config.yml
)了,基本内容就是你要哪些项目的哪个配置,由配置服务接口根据描述文件自动返回配置值
运行效果(gif图,有点大)
配置更新自动部署所有依赖的项目
format: json
itemFormat: 配置key处理规则,看下面
envBranch:
test: test
prod: master
project:
name: readboyconfig
description: 项目描述
id: gitlab项目ID
configs:
projecta:
- configa
- configb
- configc
projectb:
- configb
- configc
- configd
env: test(环境在部署的时候动态写入)
format
表示输出文件格式,值如下:
js
(es6)
json
ini
yml
project
表示工程信息,主要是相关配置更新后用于重启服务用的
id
项目在gitlab中的ID
name
项目名称
description
项目描述信息
envBranch
环境和分支的对应关系,主要是相关配置更新后用于重启服务用的
格式:
env
: branch name
env
表示要输出的环境配置,值对应后台录入的env
itemFormat
表示生成配置项的方式,取值如下
ignore
忽略项目名
prefix
项目名+配置名
dot
项目名+ .
+连接配置
tree
保持层级结构
project_without_prefix
当前项目不加前缀,其它项目同 prefix
project_without_dot
当前项目不用.
连接,其它项目同 dot
project_without_tree
当前项目不保持层级结构,其它项目同 tree
configs
项目配置信息,具体参考后台数据
CI配置
所有配置信息更新后,会触发依赖项目的CI
并携带参数 readboy_trigger=config
,如果某些阶段在自动触发的CI中不要执行,可以在CI
文件中加上条件
only:
variables
- $readboy_trigger == 'config'
或
except:
variables
- $readboy_trigger == 'config'
yml格式说明结束
获取配置
curl "${CONFIG_SERVER}" -fd "`cat .config.yml`" > src/config/index.js
CONFIG_SERVER
这个接口要实现功能主要有:
- 根据环境来获取描述文件的值
- 建立相关配置和
gitlab
项目的依赖关系,当相关配置更新的时候,自己触发相关项目的gitlab CI
,实现自动拉取最新配置,自动部署
配置管理后台的配置值更新的时候,程序会检测哪个环境的值变了,并获取依赖该属性的gitlab
项目信息,利用gitlab
的API
,API链接,自动创建一个pipeline
来拉取配置,部署程序。至此,一个简单的基于gitlab
的配置服务就完成了。
当然要完全把这个配置服务利用起来,还需要跟开发人员做要求,配置使用,依赖项目的配置绝对不要再写一遍,不然配置变了,更新就会出问题。
一些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
服务端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
服务端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的话,会部署一个空的配置文件到服务器上,出现服务不可用的问题,加上-f
,如果你的CONFIG_SERVER
有bug,返回500
了,CI
会中断执行,不会把有问题的程序部署到服务器
为什么有要datetime
如果内容没变,编译出来的镜像名称是一样的,重新部署pod
会失败,所以为了保证每次编译出来的镜像名称不一样,用datetime
来记录时间,保证部署成功