笔者正在漫漫前端路上,文中如有不正确、不恰当的地方请评论告诉我,非常感谢! 😊
现在我们有一个文档中心前端项目,我们要把它部署到服务器上发布,但每次更新项目里的文档或代码,都需要重新打包部署,这太麻烦了。如何让每次代码的提交、打包、部署更加自动化呢?本文将介绍利用Jenkins与Docker实现持续集成持续部署。主要内容如下:
Jenkins:jenkins介绍、安装、创建任务、pipeline部署流水线、pipeline两种语法、自动化触发执行。 Docker:docker介绍、安装、docker基础命令、使用dockerfile构建自定义镜像、docker-compose构建多个镜像容器。
Jenkins
认识Jenkins
Jenkins是一个开源的、提供友好操作界面的持续集成(CI)工具,主要用于持续、自动的构建/测试软件项目。
CI持续集成:频繁自动将代码集成到主干和生产环境,尽快地发现集成错误。流程:提交代码->拉取代码->编译->打包->测试->反馈问题->开发处理->提交代码。
CD持续交付:基于持续集成,频繁地将软件的新版本,交付给质量团队或者用户,以供评审。不管怎么更新,软件是随时随地可以交付的。
持续部署:持续交付的下一步,代码通过评审,自动化部署到生产环境。
所以我们可以在Jenkins创建任务,让Jenkins帮我们做一些持续重复的工作。
安装
在服务器上安装配置jenkins,参考jenkins安装与基本配置(Linux平台。
也可以用docker安装jenkins,本节不介绍。
安装启动后,主界面如下图:
创建任务
点击“新建任务”,我们可以创建一个普通的任务,或是流水线任务。
自由风格
结合任何SCM和任何构建系统来构建项目,一般做简单的任务。
我们可以设置源码配置,在构建里描述任务,使用Node插件,执行npm run build脚本将项目打包然后部署到服务器上。每次点击构建任务时,jenkins会拉取gitlab的代码,放置在服务器上的jenkins的workspace文件夹下,构建脚本时将会以这个代码文件夹为工作目录。
pipeline
如果构建任务比较复杂时,可以创建pipleline流水线任务。
部署流水线Pipeline是Jenkins 2.x的精髓,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程,形成流水式发布,构建步骤视图化。Pipeline适用的场景更广泛,能胜任更复杂的发布流程。(甚至可以处理多个项目)
1、pipeline两种语法
- 脚本式语法(使用用Groovy语言)
灵活、可扩展,更复杂,有学习成本。
node {
stage('build') {
try{
def dockerName='documentcontainer' //def关键字定义变量
def imageUrl = "${dockerName}:${dockerTag}"
def customImage = docker.build(imageUrl)
sh "docker rm -f ${dockerName} | true"
customImage.run("-it -p 8900:80 --name ${dockerName}")
...
currentBuild.result="SUCCESS"
}catch(e){
currentBuild.result="FAILURE"
throw e
}
}
}
- 声明式语法
更简单、更结构化
pipeline { //代表整条流水线
agent any //指定流水线的执行位置
stages { //包含一个或多个stage的容器
stage('Docker build') { //阶段,一个stage里只有一个steps,这些阶段可被复用
steps { //包含一个或多个具体步骤的容器
echo 'hello world' //echo就是一个步骤
}
}
stage(‘test') { //阶段,一个stage里只有一个steps,这些阶段可被复用
steps { //包含一个或多个具体步骤的容器
echo 'hello world' //echo就是一个步骤
}
}
}
}
这是一个声明式pipleline的基本结构,每一个部分都是必须的。
More:
1、post部分是在整个pipeline或阶段完成后的一些附加步骤
post{
success{
echo "========pipeline executed successfully ========"
}
failure{
echo "========pipeline execution failed========"
}
always/changed/aborted...
}
2、在声明式pipeline中无法使用if-else等,于是jenkins提供了script步骤,这样可以在steps里写groovy代码。
stage('Docker build') {
steps {
script{
def browsers=['chrome','firefox']
for(int i=0;i<browsers.size();++i){
echo "${browsers[i]}"
}
}
}
}
3、支持一些指令,对基础结构的补充,指令有自己的作用域
如environment:设置环境变量,可定义在stage或pipeline部分
2、jenkinsfile :
可以把部署流水线的逻辑写在jenkinsfile文件中,放在项目里。
设置Pipeline Script from SCM从版本控制库里拉取pipeline
触发任务执行
现在我们每次提交代码,然后切到jenkins界面点击构建,这样不够自动化。
有几种触发任务的条件,可配置构建触发器。
-
时间触发 定时触发、轮询代码仓库
-
事件触发 如GitLab通知触发:当GitLab发现源代码有变化时,触发jenkins执行构建。(不错,很好用!)
利用GitLab提供的webhook钩子
现在我们利用jenkins可以做到持续集成部署了,但我们需要在服务器上部署好nginx,再在jenkins里创造node环境安装依赖打包部署到nginx,这有两个问题:一是需要提前部署好nginx,或者如果这是后端项目需要部署数据库等等,部署环境过程复杂,如果我们要迁移服务器又要重新部署;二是安装依赖的时间太长了每次都要重新安装。
所以接下来我们运用Docker,把应用和运行环境放到一个docker容器里,方便快速地部署到任何一台服务器上,利用镜像缓存减少依赖安装的时间。
Docker
认识Docker
1、什么是docker
Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,该容器包含了应用程序的代码、运行环境、依赖库、配置文件等必需的资源,通过容器就可以实现方便快速并且与平台解耦的自动化部署方式,无论你部署时的环境如何,容器中的应用程序都会运行在同一种环境下。
(总结:利用docker可以打包我们的应用以及依赖等包到一个容器中,通过容器就方便快速地部署到任何一台机器上,并且容器里的应用程序会运行在同一种环境下。)
2、docker解决了什么问题
-
频繁搭建环境
运维工程师搭建多台机器、服务器的迁移
-
环境不一致
window与linux操作系统不同、依赖版本不同
-
二次虚拟化
不是很懂。像云计算平台就是一种虚拟化,将计算机资源抽象。docker是操作系统级的虚拟化,每个容器都有自己的文件系统、进程管理(maybe),与外部隔离。
镜像、容器、仓库的区别
镜像(Image):相当于是一个 root
文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像本身是只读的,其内容在构建之后也不会被改变。
容器(Container):镜像的运行实例。容器基于镜像启动的时候,docker会在镜像的最上一层创建一个可写层,在容器里的操作不会修改镜像本身。
仓库(Repository):集中存放镜像的地方,分为公有仓库(DockerHub、dockerpool)和私有仓库。DockerHub:docker官方维护的一个公共仓库hub.docker.com,其中包括了15000多个的镜像,大部分都可以通过dockerhub直接下载镜像。也可通过docker search和docker pull命令来下载。
安装
1、Windows安装 暂时在本地电脑上安装,学习docker命令。
遇到的问题一:
net localgroup docker-users username /add
或者Add-LocalGroupMember -Group "docker-users" -Member "username "`
然后重启
遇到的问题二:
docker pull failed
设置源镜像
{
"experimental": false,
"features": {
"buildkit": true
},
"registry-mirrors": [
"https://h3j9xv2v.mirror.aliyuncs.com"
]
}
2、linux服务器安装
使用docker
docker的基础命令,主要分为镜像和容器的命令
获取镜像 docker pull node [镜像仓库地址]
查看所有镜像 docker image ls -a (-a 包括中间层镜像,无标签,是其它镜像所依赖的镜像)
删除镜像 docker image rm <container_id 如08ec>
构建镜像 docker build -t documentcontainer . (-t给镜像命名)基于dockerfile .代表dockerfile在当前目录
使用镜像创建容器 docker run -p 3000:80 -d --name documentApp documentcontainer
-
-p 3000:80
端口映射,将宿主的3000端口映射到容器的80端口 -
-d
后台方式运行 -
--name
容器名 -
-v 文件映射 /root/code:/data/code
查看所有容器 docker ps
开启/停止/重启容器 docker start/stop/restart <容器名>
进入容器 docker attach <container_id> 接管容器内的输出
docker exec -ti [container_id] /bin/bash
删除容器 docker rm -f <container...>
基于Dockerfile来构建自定义镜像
- dockerfile指令
FROM :基础镜像,表示当前新镜像是基于哪个镜像进行更改的
RUN :执行shell命令,在docker build时运行
CMD :在docker run 时运行,为启动的容器指定默认要运行的程序,可被 docker run 命令行参数中指定要运行的程序所覆盖。
COPY 、ADD:把文件复制到容器中
EXPOSE:暴露容器端口
WORKDIR: 切换目录 类似于cd
以UCF3.0文档中心项目为例,部署这个前端应用的流程:
-
npm install
, 安装依赖 -
npm run build
,编译,打包,生成静态资源 -
服务化静态资源
FROM node:alpine as builder
ENV NODE_ENV production
WORKDIR /code
ADD . /code
RUN npm install --registry=https://registry.npm.taobao.org --production && npm run build
//为了跨域,需要把打包后的文件夹部署到nginx下
//获取nginx镜像
FROM nginx
COPY --from=builder /code/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /code/build/ /usr/share/nginx/html/
EXPOSE 82
CMD ["nginx", "-g", "daemon off;"]
这样会获得一个以nginx为基础的镜像和一个无标签的中间镜像(需要删除)。
利用jenkins在jenkinsfile中使用docker命令构建docker镜像容器。
def dateFormat = new SimpleDateFormat("yyyyMMddHHmm")
def dockerTag = dateFormat.format(new Date())
pipeline {
agent any
environment {
dockerName='documentcontainer'
}
stages {
stage('Docker build') {
steps {
sh 'pwd'
sh 'ls'
sh "docker stop ${dockerName} || true"
sh "docker rm -f ${dockerName} || true"
sh "docker build -t ${dockerName}:${dockerTag} ."
sh "docker rmi $(docker images -f dangling=true -q | sed -n '3,$p' | awk '{print $0}') --force || true"
sh "docker run -it -p 82:82 -d --name ${dockerName} ${dockerName}:${dockerTag}"
//only retain last 3 images
sh "docker rmi $(docker images | grep ${dockerName} | sed -n '4,$p' | awk '{print $3}') --force || true"
}
}
}
post{
success{
echo "========pipeline executed successfully ========"
}
failure{
echo "========pipeline execution failed========"
}
}
}
这样我们已经实现持续集成持续部署了,但构建镜像的时间还太长,想想可以怎么优化。
如何优化,让镜像构建更高效
构建镜像时,经常会有镜像体积过大、构建镜像时间过长的问题,那如何优化呢?
- dependencies 和 devDependencies
在生产环境中使用 npm install --production
装包
- 利用镜像缓存
...
WORKDIR /code
ADD package.json /code //package.json 与源文件分隔开写入镜像
RUN npm install --registry=https://registry.npm.taobao.org --production
ADD . /code
RUN npm run build
...
- 多阶段构建
关于镜像体积的过大,很大一部分是因为node_modules 臭名昭著的体积。
可以利用 Docker 的多阶段构建,仅来提取编译后文件。
Docker Compose
如果我们的项目需要一个后端服务、mongo数据库,我们需要写很多docker命令构建各个镜像容器,我们可以用Docker Compose帮我们管理多个Docker容器,只需要简单的一条命令就可以构建启动各个镜像容器。
Docker Compose 通过一个配置文件docker-compose.yml来管理多个Docker容器,docker-compose.yml描述了多个容器的属性。
version: '3.2'
services:
mongodb:
image: mongo
build:
context: ./
dockerfile: mongo-Dockerfile
restart: always
ports:
- '27017:27017'
volumes:
- /usr/local/egg-app/mongo/backup/o45:/mongo/backup/o45
- /usr/local/egg-app/mongo/data/db:/data/db
server:
image: egg-server
volumes:
- /usr/local/egg-app/run:/egg-app/run
- /usr/local/egg-app/logs/server:/root/logs/server
build: ./
restart: always
depends_on:
- mongodb
links:
- mongodb
ports:
- '7001:7001'
- docker-compose 基础命令
关闭容器 docker-compose stop || true;
删除容器 docker-compose down || true;
构建镜像 docker-compose build;
启动并后台运行 docker-compose up -d;
遇到的问题
1、后端服务无法连接数据库
①把后端dockerfile里的RUN npm run start 改为CMD npm run start
-
RUN 是在 docker build时运行
-
CMD 在docker run 时运行。所以后端服务应在数据库容器build构建后再启动
②mongoose连接url
mongoose: {
client: {
url: 'mongodb://root:123456@172.27.24.217:27017/o45?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false',//当有账号密码时
options: {
useNewUrlParser: true,
useUnifiedTopology: true,//warning
},
},
},
2、后端服务容器一直启动失败
原因:egg-scripts start 改为前台运行
启动命令
$ egg-scripts start --port=7001 --daemon --title=egg-server-showcase
如上示例,支持以下参数:
--port=7001
端口号,默认会读取环境变量process.env.PORT
,如未传递将使用框架内置端口7001
。
--daemon
是否允许在后台模式,无需nohup
。若使用 Docker 建议直接前台运行。