设想一下这些场景:Nuxt 版本出现重大更新需要升级;新功能的开发需要添加新的生产环境依赖;线上版本出现 bug 急需快速回滚至上一版本;开发环境和生产环境依赖版本一致性的保持……
我们需要解决的不仅仅是不停机更新,还要使开发环境和生产环境的版本保持强一致性,并且可以轻松地追溯历史版本,以及更新过程使用户无感知等等。综合以上特质,可以选择 docker 一试。
Docker 是一个开源的应用容器引擎,基于 Go 语言并遵从 Apache2.0 协议开源。
它号称自己是实现在任何地方安全构建、分享和运行现代应用的最快实现方式。它可以让我们把应用及其依赖全部打包到一个容器中,从而轻松地实现在任何系统下极速迁移。就像其 logo 展示的那样,一艘鲸鱼样子的大船载满集装箱飘荡在海面上。各个 OS 就是海洋,docker 承载着一个个容器飘荡在 OS 的海洋里。
以 Nuxt 应用为例,如有需要,我们可以将其运行需要依赖的 Nginx 、 NodeJs 、应用本身及其依赖等统统打包到一起。此时就算你拿到一个新的服务器,只要上面安装了 docker,只需要几个简单的指令就可以让应用运行起来,而不需要再进行繁琐地配置。
具体的使用细节不再絮叨了,茫茫多的 docker 官方文档 正向你招手~菜鸟教程和 docker 中文社区也是系统学习的不错选择。
惯例,抛出 docker cli 的常用指令:
# 基于当前文件夹下的 dockerfile 创建一个镜像 docker build -t helloworld . # 上面指令的全写 docker build --tag=helloworld . # 运行这个镜像,并将本机 4000 端口映射到容器对外暴露的 80 端口,外部通过 4000端口访问 docker run -p 4000:80 helloworld # 使 container 在后台运行 docker run -d -p 4000:80 helloworld # 所有正在运行的容器列表 docker container ls # 所有容器列表 docker container ls -a # 停用一个容器 docker container stop <hash> # 强制停止一个容器 docker container kill <hash> # 移除一个容器 docker container rm <hash> # 移除所有容器 docker container rm $(docker container ls -a -q) # 所有镜像列表 docker image ls -a # 移除一个镜像 docker image rm <image id> # 移除所有镜像 docker image rm $(docker image ls -a -q) # 登录注册过的 docker hub docker login # 为镜像打标签 docker tag <image> username/repository:tag # 将镜像推送至远程仓库 docker push username/repository:tag # 运行这个镜像 docker run username/repository:tag 复制代码
此时,容器 (container) 即当前承载应用的进程,它拥有一个独立的文件系统,里面包含了应用所需的所有代码、依赖和运行时等等。这被称之为镜像 (image) 的存在,正是我们需要在之后生产并保存起来的东西。
假如每一次版本发布我们都生成一个镜像 (image),并基于项目和版本号为其打上独一无二的标签,然后把它们保存到一起,需要的时候随时取用,依赖更新的问题自然而然得到解决。而当需要发布新版本的时候,我们只需要提前拉新版本的镜像到生产环境,然后移除旧版本镜像正在运行的容器,同时执行新版本的容器,开启新的 container,这样就可以实现无缝升级,也实现了某种意义上的不停机。
接下来,我们要有一个存储版本镜像的仓库。docker 自己推出了一个镜像组织和托管平台, docker hub ,学习的时候可以简单地用它来熟悉整个操作流程。之后可以搭建自己的私有仓库来满足实际的业务需求。因为我们实际业务中各种服务器都基于阿里云产品,所以这里也选择 阿里云容器镜像服务 来管理镜像。
仓库的问题解决了,我们又希望在每次 push 一个 tag 的时候能够自动地生成一个镜像并上传至仓库。我们的项目代码托管在私有 GitLab 上,它本身就集成了不错的 CI/CD 功能,可以作为备选的目标之一。我们这里选择使用 Jenkins (一个基于 java 的持续集成工具)并结合 GitLab 的 webhooks 来实现这一需求。
最终方案定为 GitLab + Jenkins + Docker + 阿里云镜像服务,下面来看一下大体的实施步骤。
安装 java -> yum install java
添加 Jenkins 库至 yum -> sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
导入公钥 -> sudo rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key
安装 -> sudo yum -y install jenkins
查看 Jenkins 根目录 -> cd /var/lib/jenkins
配置 Jenkins 根权限:
vim /etc/sysconfig/jenkins
,修改或添加项 -> JENKINS_USER="root"
和 JENKINS_GROUP="root"
,保存退出 执行 gpasswd -a root jenkins
这里jenkins服务的默认端口为 8080,可在配置文件中修改,保存后执行 service Jenkins restart
重启服务
启动Jenkins -> service jenkins start
。此时访问 http://<服务器公网ip>:8080
开启 Jenkins 界面
在 /var/lib/jenkins/secrets/initalAdminPassword
下获取管理员密码以开启 Jenkins
设置管理员账号密码后,即可开始使用 Jenkins
Docker 社区版和企业版,这里选择安装社区版本
$ sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest- logrotate docker-logrotate docker-engine 复制代码
$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2 $ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 复制代码
$ sudo yum install docker-ce docker-ce-cli containerd.io $ sudo systemctl start docker $ sudo docker run hello-world nuxt-demo
FROM node:10.16.0 ENV HOST 0.0.0.0 RUN mkdir -p /app COPY . /app WORKDIR /app EXPOSE 3000 RUN npm install RUN npm run build CMD ["npm", "start"] 复制代码
访问阿里云容器镜像服务
登录后,创建命名空间,创建镜像仓库,这里以 psl_one
为例
由于这里通过 jenkins 来构建 docker 镜像,所以不再配置代码源,选择本地仓库并创建 (阿里云镜像服务本身也提供了镜像构建功能)
进入新创建的镜像仓库,可以在 镜像版本 中查看已上传的镜像列表,在 基本信息 中查看该仓库的基本信息,其第三条说明 将镜像推送到Registry 中有接下来要用到的指令:
$ sudo docker login --username=[username] registry.cn-qingdao.aliyuncs.com $ sudo docker tag [ImageId] registry.cn-qingdao.aliyuncs.com/[命名空间]/hjxy_test:[镜像版本号] $ sudo docker push registry.cn-qingdao.aliyuncs.com/[命名空间]/hjxy_test:[镜像版本号] 复制代码
首先安装一些必要的插件。进入系统管理->插件管理->可选插件,在 过滤 中搜索需要的插件
http://mirror.xmission.com/jenkins/updates/update-center.json
Localization: Chinese (Simplified)
简体中文语言包 SSH Plugin
通过ssh连接远程服务器执行shell脚本 Git Parameter Plug-In
在 参数化构建过程 选项下获得git相关参数,如branch/tag等 Generic Webhook Trigger Plugin
构建触发器插件,接收一个http请求,与webhook配合,触发job执行构建 nvm-wrapper
提供一个nodejs的构建环境 Email Extension Plugin
设置邮件通知的插件 进入系统管理 -> 系统设置,进行 ssh 配置
进入系统管理 -> 管理用户,查看|生成用户 id 和用户 token
进入系统管理 -> 系统设置,配置邮件通知,这里以 qq 邮箱为例
点击 新建任务 ,选择创建一个自由风格的项目,为 nuxt_demo 创建一个镜像构建、上传并部署的 jenkins job。 约定这个任务只用于当开发者向仓库提交 tag 时触发构建,不能进行主动构建(就算主动构建,因为缺少 tag 信息也会失败)
Generic Webhook Trigger
由于 GitLab 的 webhook 传递来的 ref 信息形如 refs/tags/v1.0.0
,所以需要在执行脚本中进行字符串拆分,获取需要的部分: $ref|cut -c11-
Execute shell script on remote host using ssh
ssh site
即刚才在系统设置中配置的ssh , pre build script
可以置空或根据需要填写,这里主要配置 post build script
,其中 ***-deploy.sh
为远程服务器中的部署脚本,将在构建任务结束后触发执行
tag push events
,点击
add webhook
添加钩子,并点击
test 按钮测试是否可用
若成功,将触发相应的 jenkins job 执行
/var/myprojects/shell_script/***-deploy.sh
#!/bin/bash echo "deploy start" preImageId=$(docker inspect -f {{.Image}} <Container Name>) # 获取当前运行容器的镜像Id,稍后用以删除镜像 docker login -u <username> -p <password> <Docker Registry> # 登录远程镜像仓库 docker <Image>:$1 # 拉取最新构建的镜像 docker rm -f <Container Name> || true # 强制删除当前运行的容器 docker run -d --name <Container Name> -p 3000:3001 --restart=always <Image>:$1 # 使用最新拉取的镜像开启新的容器 docker rm -f <Container Name2> || true # 强制删除当前运行的容器 docker run -d --name <Container Name2> -p 3001:3001 --restart=always <Image>:$1 # 使用最新拉取的镜像开启新的容器 docker image rm -f ${preImageId} # 移除上一个版本的镜像 echo "deploy end" 复制代码
upstream nuxt_demo { server localhost:3000 max_fails=1 fail_timeout=15s weight=1; server localhost:3001 max_fails=1 fail_timeout=15s weight=1; } server { listen 80; server_name test.xyz.docker.com; location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Nginx-Proxy true; proxy_cache_bypass $http_upgrade; proxy_pass http://nuxt_demo; } } 复制代码
参考第六节中的部分,但不再配置构建步骤。 在 general 选项卡中勾选参数化构建过程选项,并配置如下参数:
此时,侧边栏的 立即构建 将变成build with parameters
点击
build with parameters
将看到如下界面。releasetag 默认值为配置时填写的 notag,此时可参照 taglist 填写正确的值执行构建操作:
最终的任务列表大致如下: