Cover image
Hero image

托码特人

分享科技与人文

一个关注互联网的技术博客

关于Docker镜像定制和前端多项目Gitlab CI/CD的整理

一、缘起

由一次内部系统小改引发的小故事:旧系统迁移和两个系统的整合部署,目标是一个容器内跑新旧两套系统,并用一个域名承载!

二、故事开始

1、镜像升级(定制镜像)

发现新系统在 Node 版本方面,有不一致的地方,比如编译环境是node-v12.x,而实际容器的运行版本则是node-v10.x,这还没算部分项目有依赖安装的场景…对于 Web 项目本身可能影响不大(已知 node-sass 对版本有依赖,使用 Sass 时需要注意),但是如果跑 Node 服务就会有影响(js 引擎在不同版本之间往往会有差异),更何况不同的版本之间默认 NPM 也不一致,也会造成安装的依赖存在略微差异!

除了 Node 版本,nginx 也一样。理论上讲,最新的稳定版本应该也会比老舅的性能更好,特别是版本差的太多的!

本来是想在之前的镜像上直接升级,后来觉得 OS 是不是也能折腾一下(主要是我米 SRE 提供的标准镜像都太老舅),于是就去 Docker 官方找了标准镜像

系统版本说明

  • alpine:Alpine Linux 操作系统。占用空间最少(工具和基础软件包没有),但出现问题比较难以调试。一般不要选择这个类型
  • buster:基于 Debian Linux 发行的版本,比较新且支持全面。一般使用这个类型即可(大多镜像默认就以此为基础)
  • stretch:另一个基础 Debian Linux 发行的版本,相对于 Buster 系统比较老舅,建议不要使用(除非硬件不支持新系统);

最终基础镜像选择为:Debian Linux 10.10(buster)。在搜索上选择了最新的 nginx 版本,即:1.21.1。如果本地已经安装好了 Docker 环境,则直接黑窗口执行:docker pull nginx:1.21.1

基础镜像下载到本地之后,就可以定制环境了。一般有两个姿势:

纯手工打造

  1. 启动容器,安装基础工具(curl/procps/vim 等)和项目运行环境(node/nrm/yarn/cnpm/pm2 等)。考虑 node 未来升级的可扩展性,我们可以通过 nvm 来安装 node。需要注意的是通过 nvm 安装后,一定要把环境变量导出。否则容器启动后会因找不到类似 npm 命令导入安装依赖或在线打包失败……
  2. 系统参数调优,比如 nginx 默认的一些配置,是否开启 GZIP 等;
  3. 通过容器 ID 提交新的镜像并 push 到镜像仓库,具体参考以下步骤:
# 1. 从官方拉取基础镜像
docker pull nginx:1.21.1

# 2. 查看本地镜像编号/名称
docker images

# 3. 启动镜像容器(映射宿主机的一个目录到容器内,主要方便文件交换)
docker run -itv /Users/tangkunyin/docker:/home ${镜像ID} /bin/bash

# 4. 提交容器到镜像
## 查看容器id
docker container ls -as

## 提交容器并打标
docker commit 9cae32ff7102 registry.cn-guangzhou.aliyuncs.com/thomax/nginx-node:1.21.1-14.16.0

# 登录镜像仓库并push
## 名字注意换成你的
docker login registry.cn-guangzhou.aliyuncs.com --username=xxx

## 确认无误后推送镜像
docker push registry.cn-guangzhou.aliyuncs.com/thomax/nginx-node:1.21.1-14.16.0

通过 Dockerfile 自动打造

FROM nginx:stable

LABEL maintainer="Thomas Tang <[email protected]>"
LABEL description="Based on the nginx:stable, node installed by nvm and nrm yarn all installed"


ENV NVM_VERSION 0.38.0
ENV NODE_VERSION 14.16.0
ENV NVM_DIR /usr/local/nvm

# Replace shell with bash first so we can source files
RUN rm /bin/sh && ln -s /bin/bash /bin/sh && \
apt-get update && \
apt-get install -y curl vim procps net-tools iputils-ping openssh-client openssh-server && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /usr/local/nvm && \
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash && \
. $NVM_DIR/nvm.sh && \
nvm install ${NODE_VERSION} && \
nvm alias default ${NODE_VERSION} && \
nvm use ${NODE_VERSION} && \
npm install -g nrm yarn && \
nrm use cnpm && \
nvm cache clear

ENV NODE_PATH $NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH

在文件同级目录下执行编译:docker build -t registry.cn-guangzhou.aliyuncs.com/thomax/nginx-node:1.21.1-14.16.0 .

以上构建成功后,参考手动部分再 push 到镜像仓库,即可完成镜像的定制!

2、子项目依赖

要实现一个容器里跑多个系统,项目代码最好就集中到一个主仓库,一来简化开发流程,二来也方便 Gitlab 打包的镜像包含所有的 dist。所以 Git Submodule 就用到了,不过特别需要注意的是子项目路径依赖最好用相对路径,而不是直接 https/ssh。对于项目都在同一个 gitlab 甚至是一个群组内,这个方式非常适合。如果是分布在不同 git 服务器上,则需要使用其他姿势了,具体自行谷歌容器之间配置 SSH 信任。

另外 Gitlab 的 Runner 也需要额外配置一下,因为构建时它默认并不会去拉 submodule 仓库。我们只需要在 gitlab-ci.yml 中 variables 里添加一行:GIT_SUBMODULE_STRATEGY: recursive

git submodule里的内容参考如下:

[submodule "platform-v1"]
    path = platform-v1
    url = ../platform-v1.git
    branch = "master"

3、缓存策略的使用

通过 Gitlab 进行 CI/CD 中最浪费时间的地方就是安装依赖和在线打包,其中前者往往是很多前端同学的噩梦,为了尽可能提升流水线效率。我们就需要用到合理的缓存策略来加速。默认的策略是:push-pull,在默认方式下,每个 Job 开始执行前都会去检测并下载缓存文件,任务结束后又会上传一遍文件。但并不是每个 Job 都需要这样:

  • 依赖安装:把首次安装好的 node_modules 缓存到 FDS 上,如果 package 文件不变,则后续流程就不用在进行依赖安装。对于这个阶段来说我们不需要检测 FDS 是否有缓存,要做的只是变更后 push 新的缓存包。因此这个阶段改成:push
  • 编译打包:用安装阶段已经缓存的 node_modules 直接编译项目,完后不需要再上传文件。因此这个阶段改:pull

这样,FDS 下载和上传的时间就被节省掉(node_modules 包特别大时效果明显)。再有,缓存如果是被多个 Job 所共享,需要注意使用一致的名称和一致的 path,否则 Job 执行时会相互影响

这里建议用分支+自定义标识为缓存包做命名,比如这种:key: "${CI_COMMIT_REF_NAME}-dependenciesCache"

其中,CI_COMMIT_REF_NAME 是 gitlab 预定义变量值,参见:https://docs.gitlab.com/ee/ci/variables/predefined_variables.html

这种命名的好处是生产环境依赖和测试环境依赖可以有效区分开,避免可能的影响!

4、入口文件优化

每一个使用 Docker 来部署的应用的项目在其根目录都有一个 Dockerfile 文件,这个文件用来定义容器的运行时环境以及启动时应该执行的脚本任务。但对于复杂场景来说,Dockerfile 中的 ENTRYPOINT 或 CMD 指令就不太好描述了,特别是当需要判断环境执行不同任务时。此时一个 shell 文件就会解决所有难题,例如:

#!/bin/bash

set -e

mkdir -p /home/work/log

# Starting nginx server
nginx

# Starting node server. Note that staging won't run old-server
if [[ $runEnvArg == 'prd' ]]; then
    cd /home/work/app/xxx-platform/platform-v1/server && npm run online >> /home/work/log/xxx-old.log &
    echo "old server is running................................"
fi

cd /home/work/app/xxx-platform/server && npm run $runEnvArg >> /home/work/log/xxx-new.log

exec "$@"

原来的ENTRYPOINT指令改为:ENTRYPOINT ["./docker-entrypoint.sh"]

5、容器启动失败

docker 容器不同于虚拟机,我们可以简单粗暴的理解为宿主机内的一个进程。因此从这个角度来说,容器需要有一个前台任务“卡”着才能正常运行。否则“进程”启动后就会自动退出。所以重点来了,如果你的容器启动后无故自动退出且没有其他报错信息,请第一时间检查是否有前台命令……对于前端来说这个命令不是 nginx 就是 node。注意我再说一遍,不管哪个命令一定是前台执行,即:卡着黑窗口不退出的那种……

另外值得一提的是容器本地调试,如果发布平台上操作哪哪都不对,又不想浪费 Gitlab 流水线时间,那完全可以把已构建成功的镜像(你要部署的那条)直接下载到本地调试!

6、子系统访问路径的问题

这方面,需要注意两个问题,一是项目本身的publicPath ,二是 nginx 的root/alias指令。比如我开始提到的,我要一个域名跑新旧两个项目:

此时,旧项目在打包 dist 时就需要配置publishPath 为 v1,而 nginx 的配置就取决于旧项目包绝对路径,事实上使用 alias 指令,自由度会更高

7、CI 文件优化不完全指北

总的来说就是用 gitlab 手动维护多套基础模板,业务使用时,直接include基础模板并把需要的变量传递进去,而不是在每个项目的gitlab-ci.yml文件写一大堆冗余的配置。这样做的好处不言而喻,除了简单便捷,也在宏观层面尽可能统一了研发/运维的标准。尤其是对于新手同学,统一姿势会节省的大量宝贵的时间。

三、阅读资料

赞赏

声明: 本文内容由托码斯创作整理,由于知识水平和时效性问题,行文可能存在差错,欢迎留言交流。读者若需转载,请保留出处,谢谢!