🐳项目容器化改造与devops实践
本文记录了笔者在项目中使用docker和jenkins的实践,涉及的代码均为简单示例,不作为教程参考。
🐋容器化改造
云原生时代,为了提高应用的拓展性和伸缩性、简化部署流程,对传统项目进行容器化改造已经成为了大势所趋。而在容器领域,除了Docker,其实还有Podman、Containerd、LXD等值得考虑的选择,但作为容器领域的先行者,Docker目前已经构建起了相当丰富的生态。在此情况下,我打算在我们的项目中接入Docker。
📷镜像打包
网上关于go应用的docker部署方案有很多种,但大体上可以分为两类:
- 在容器内对代码进行编译,然后运行应用。
- 在容器外对代码进行编译,仅在容器内运行编译好的可执行文件。
前者在进行镜像打包的时候,需要以包含go运行时的镜像为基础,进行构建,打包出来的镜像相对会比较大。而后者在进行镜像打包的时候,只需要选择一个尽可能轻量的linux镜像即可,比如alpine。
以下是一份简单的dockerfile示例,beta是我打包好的可执行文件的名称。
bash
FROM alpine:latest
ENV TZ Asia/Shanghai
WORKDIR /app
COPY ./config .
COPY ./log .
RUN chmod +x /app/beta
VOLUME ["/app/config/","/app/log/"]
EXPOSE 8080
CMD ["/app/beta"]
在这个环节中有一个需要注意的点:确定好应用在运行时所依赖的目录结构与相关文件,比如此处的config和log目录,在镜像打包阶段,需要将这些目录与相关文件一并复制到镜像中。
然后,使用docker build指令进行进行镜像的构建。
erlang
docker build -t myapp1:v1 .
🎹容器编排
在评估了项目目前的业务模块数量和应用发布需求后,我发现我们暂时还用不上K8S等高级的容器编排工具。为了保证应用能够简单且高效地进行发布,降低维护成本,我们决定使用docker-compose。
以下是一份简单的docker-compose.yml示例。
yaml
version: '3'
services:
# 数据库
mariadb:
image: circleci/mariadb
container_name: mariadb
command: --default-authentication-plugin=mysql_native_password
environment:
MARIADB_ROOT_PASSWORD: 456123
MYSQL_ROOT_HOST: '%'
MYSQL_USER: test
MYSQL_PASSWORD: 456123
TIME_ZONE: Asia/Shanghai
privileged: true
volumes:
- ./db/data:/var/lib/mysql
- ./db/log:/var/log/mysql
- ./db/conf:/etc/mysql
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro # 时区配置
restart: always
networks:
- my-net
ports:
- 3306:3306
# 项目的各个应用模块
myapp1:
image: myapp1:v1
container_name: myapp1
restart: always
ports:
- "8000:8000"
environment:
- TZ=Asia/Shanghai
volumes:
- /usr/share/zoneinfo:/usr/share/zoneinfo
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- mariadb
networks:
- my-net
myapp2:
image: myapp2:v1
container_name: myapp2
restart: always
ports:
- "9000:9000"
environment:
- TZ=Asia/Shanghai
volumes:
- /usr/share/zoneinfo:/usr/share/zoneinfo
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- mariadb
networks:
- my-net
networks:
my-net:
driver: bridge
这里我们需要注意几个点:
- 根据实际的场景需要,选择好容器间的网络连接类型。这里我们选择了桥接网络,并配置了extra_hosts确保容器内部可以正常地与宿主机进行连通。
- 通过挂载宿主机的/usr/share/zoneinfo目录,确保go应用在alpine镜像中运行时,不会出现时区问题。具体可参考这篇文章: 容器化Go应用--基础镜像的未知时区问题 (zhihu.com)
- 使用environment和command指令,在运行容器的时候,进行一些账户初始化、配置初始化等操作。
- 在进行目录挂载的时候,要根据dockerfile中通过volume指令定义好的路径进行配置,避免因挂载路径不存在导致的容器启动错误。
💾存储改造
随着项目接入容器,伴随而来的一个问题是:由于各模块已经通过容器进行了隔离,原来将文件上传到指定文件夹的存储方案已经失效。在此情况下,我参考了一些go相关的文件存储方案,MINIO、seaweedfs、caddy,发现比较符合业务需求且配备了官方sdk的就只有MINIO。它是一个高性能的分布式对象存储解决方案,而且自带了文件的版本管理功能。
🚴devops
至此,我们便可以通过 docker-compose up -d 指令轻松地部署起我们的应用以及项目所依赖的数据库、中间件。
对于上线部署来说,做到这里已经是蛮不错的了。但考虑到我们日常开发过程中,需要快速迭代,进行效果展示,仅仅接入容器,我感觉仍然没办法很好地提高我们的开发效率。于是我打算更进一步,接入devops工具,打通开发到部署的"最后一公里"。
在工具选择中,选择了比较成熟的jenkins,但由于它是使用java编写的,不管是裸机安装还是通过容器进行部署,都需要搭配JDK,比较占用内存资源。
📧配置代码仓库的webhook
通过在代码仓库中设置webhook,即可实现,每当仓库中出现代码更新,就会提醒jenkins进行应用构建。不过在实际生产过程中,正式环境需要确保应用的稳定性,且需要对应用进行版本管理,所以仅仅建议在测试环境中接入webhook功能。
🚝编写shell脚本
以下是一个简单的在jenkins中的shell示例。
bash
# 切换到指定路径
cd /home/myapp
#清除本地改动
git checkout .
# 拉取最新代码
git pull origin master
# 配置go参数并编译
export GO111MODULE=on
# 配置go代理
export GOPROXY=https://goproxy.cn
go env -w GOOS=linux
go build -o beta .
# 停止并删除旧容器
docker stop myapp1 && docker rm myapp1
# 删除旧镜像
docker rmi myapp1:v1
# 构建新镜像
docker build -t myapp1:v1 .
# 通过docker-compose启动新容器
docker-compose up -d
在这里,我们通过shell脚本来控制容器的构建。
至此,我们实现了docker+jenkins的部署方案。
💡关于容器的思考
其实刚开始的时候,团队关于接入容器始终是保持一个试探性的态度。为什么呢?就拿最简单的数据库来说,比如我运行一个mysql,如果在运行期间出了问题,我们没有办法立刻定位到相关的文件路径去进行错误排查或修复。虽然我们可以进行目录挂载,但大多的时候还是只能"docker exec"进入容器内,然后再进行排查(且容器只有在正常运行的状态下才能进入,否则只能"docker inspect"去定位容器目录在宿主机中的具体位置)。所以对于运维人员来说,相比传统的部署方式,使用容器只能说在某些方面是方便的,但又会在另一些方面带来不必要的麻烦。
但就像文章开头所说的,容器化始终是大势所趋,谈到云原生,基本也离不开容器。作为开发者,我认为我们还是需要拥抱容器,在实践中找到比较适合自己项目的部署流程。另外,对于个人开发者而言,我始终相信,容器是学习各类新工具的不二选择。