为了能摸鱼我们团队做了容器化,但是带来的问题是服务配置文件很麻烦,然后大家在群里进行了“亲切友好”的沟通
图片
图片
图片
图片
对比就对比,简单对比下独立配置中心和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的福利时间,我们将这个变量注入的库进行了开源。现在用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