目录
前言
一、模拟部署上线流程
1、制作镜像
1.1 选择基础镜像
1.2 针对部署的项目制作镜像
2、启动mysql镜像
3、k8s部署
二、总结要点
前面开发了几个项目,上线时都是固定一个同事负责的,当时看他花了挺久的时间,觉得肯定费事的。当时就在想,应该没有那么难把,为什么会这么久?
然后这次上线,赶上同事不在,所以由我来准备上线的事情。我发现,流程不难,但也没有我想的那么简单。而最耽误时间的是上线资源的申请,如pvc、数据库等等,申请周期非常长。
所以大家如果承担上线部署工作,一定要把流程和周期提前问清楚,要不然很容易错过上线窗口。
一、部署流程说明
废话不多说了,针对这次上线流程简要的做个说明。
使用工具:docker、k8s
网络环境:内网(无法连internet)
app:后端服务
部署流程:选择基础镜像 -> 写build.bsh脚本 -> 写dockerFile脚本 ->打包 -> 写yaml文件 -> 部署 -> 测试
当然,上面说的部署流程是极度简化过的了,有很多坑,我会在后面内容中详细说明。
本次上线是完全在我们内网开发环境部署的,相信在真实的项目投产过程中大家也都是这样的。不会像是在某个服务器上随便部署,可能很小很不规范的公司会这样随意的上线,但是只要稍微正规一些的公司都会有自己的开发环境以及生产环境的,在这个环境里,绝对不允许随意连接外网的。
因此一些在一些需要联网的操作久相对麻烦一些。
以下是我的项目目录,可以看以下后面的代码,只有参考这个目录结构,才能看懂代码干了些什么。
其中dist目录是我存放 部署上线 所需要的脚本和要安装的软件。
我后面粘贴出来的脚本代码,为了整洁性,我都不会增加注释,而是会在脚本文件的后面,增加一个专门解释的文件。
公司里都有固定的基础镜像,选择一个符合自己要求的即可。那么选择依据是什么呢?其实就是基础镜像环境可以尽量满足你的使用需要,如安装了jdk、python3.7这种要求。
但是公司的镜像一般为了小型化,都会给你一个最最简洁、啥都没有安装的基础镜像。然后你需要啥,你就装啥呗,然后打包成你要的镜像。其实也是,人家就叫基础镜像,所以就提供基础环境。
我的项目需要环境是
但是没有能够满足的基础镜像,而有一个jdk的基础镜像,安装了jdk。
因此我选择了这个基础镜像,所以我只需安装好python环境即可。
这个基础镜像存在方 public仓库,在配置dockerfile的时候将base-image地址写入,就自动会给你拉下来了。
我的项目版本是在svn上(类似于git)进行管理的。所以为了保证一致性,我肯定是打镜像的过程中,实时从svn上拉取代码,进行打包。而不是把代码先下载下来,然后对其打包。
所以我要写 build.bsh脚本,把打包的全部前期工作都在build.bsh脚本中实现了,这样就可以一劳永逸啦。
我的目标就是,只要我执行 bash build.bsh 脚本,那么就会一下子把我要的镜像打包好。
ok,直接贴出 build.bsh 脚本内容
#!/usr/bin/env bash
##-x-coding:utf8;-*-##
set-ex
svn_auth='--no-auth-cache--username xxx --password xxx'
source_engine=http://190.0.123.21:36931/branches/api-gateway
version=$(svn info $svn_auth -r HEAD $source_engine | grep '^Revision: 'cut-d' -f2).
export_root=/tmp/api-gateway-backend-$version.$(date"+%Y%m%d%H%M%S")
svn --non-interactive \
$svn_auth \
export \
$source_engine \
$export_root
cd $export_root/dist/image \
&& mkdir-p app \
&& cp -r ../../app \
../../config \
../../static \
../../tools \
app \
&& cp -r ../../requirements \
. \
&& docker build -t 191.123.123.123/api-gateway-backend:r$version-dt$(date +%s) .
###### 脚本代码注释 ######
# 执行脚本过程,遇异常退出
set -ex
# svn验证权限,设置成变量
svn_auth='--no-auth-cache--username xxx --password xxx'
# svn项目代码地址,设置成变量
source_engine=http://190.0.123.21:36931/branches/api-gateway
# 定义镜像版本,设置成变量
version=$(svn info $svn_auth -r HEAD $source_engine | grep '^Revision: 'cut-d' -f2)
# svn项目代码导出后所在路径,设置成变量
export_root=/tmp/api-gateway-backend-$version.$(date"+%Y%m%d%H%M%S")
# svn拉取项目代码,并放置在自定义目录
svn --non-interactive \
$svn_auth \
export \
$source_engine \
$export_root
### 下面代码,请参考我开头贴的项目结构,否则你看不懂
# 进入打包目录
cd $export_root/dist/image \
# 在项目目录下创建 app 文件夹
&& mkdir -p app \
# 将项目目录下 aap、config、static、tools文件夹都拷贝到刚才创建的app文件夹下
&& cp -r ../../app \
../../config \
../../static \
../../tools \
app \
# 将项目下requirements文件夹拷贝到当前目录
&& cp -r ../../requirements \
. \
### 以上就把要打包的文件准备好了,下面就是要进行 build 了,进而找到dockerfile执行
&& docker build -t 191.123.123.123/api-gateway-backend:r$version-dt$(date +%s) .
build.bsh脚本干的事情,其实在脚本注释里已经写的很清楚了,就不再赘述了。
那么,现在就要执行 dockrfile了,同样,我直接贴出代码,如下。它主要完成了:拉取基础镜像,设置容器中环境变量,安装conda软件,安装虚拟环境,安装python依赖包
FROM 123.0.123.22/public/sles12sp3_jdk8: 20181114001
MAINTAINER "xxxxxxxx. com. cn>"
ENV INSTALLERS_ROOT=/app/installers \
APP_ROOT=/app/api-gateway
ENV CONDA_PATH=SAPP_ROOT/miniconda3-py37 \
VENV_PATH=SAPP_ROOT/venv \
АPР_PATH=SAPP_ROOT/backend
COPY Miniconda3-latest-Linux-x86_64. sh $INSTALLERS_ROOT/
COPY requirements/ $INSTALLERS_ROOT/requirements/
COPY python-deps/ $INSTALLERS_ROOT/python-deps/
COPY app $APP_PATH/
RUN set -ex \
&& bash $INSTALLERS_ROOT/Miniconda3-latest-Linux-x86_64.sh -bfp $CONDA_PATH \
&& $CONDA_PATH/bin/conda create -p $VENV_PATH --clone=base \
&& source $CONDA_PATH/bin/activate $VENV_PATH
&& $VENV_PATH/bin/python -m pip install -U --no-cache-dir --no-index -f $INSTALLERS_ROOT/python-deps/ pip \
&& $VENV_PATH/bin/pip install --no-cache-dir --no-index-f $INSTALLERS_ROOT/python-deps/ -r $INSTALLERS_ROOT/requirements/prd.txt \
&& source $CONDA_PATH/bin/deactivate \
&& $CONDA_PATH/bin/conda clean --all --yes \
&& rm -rf $INSTALLERS_ROOT
WORKDIR SAPP_PATH
###### 脚本代码注释 ######
# 从仓库拉取基础镜像,此镜像已经安装了jdk,因此我后面的命令就没有安装
FROM 123.0.123.22/public/sles12sp3_jdk8: 20181114001
# 这是公司要求脚本格式,可忽略
MAINTAINER "xxxxxxxx. com. cn>"
# 配置软件安装包的放置目录:INSTALLERS_ROOT,app的root工作目录APP_ROOT
ENV INSTALLERS_ROOT=/app/installers \
APP_ROOT=/app/api-gateway
# 配置conda的安装目录,虚拟环境安装目录(python虚拟环境),项目目录APP_PATH
ENV CONDA_PATH=SAPP_ROOT/miniconda3-py37 \
VENV_PATH=SAPP_ROOT/venv \
АPР_PATH=SAPP_ROOT/backend
### 后面的三个copy是把安装包复制到一个文件中,然后在安装完包后,我会把这些安装包都删除,看本脚本倒数第二行 rm -rf $INSTALLERS_ROOT
# 把conda安装包赋值到INSTALLERS_ROOT,方便后面安装
COPY Miniconda3-latest-Linux-x86_64. sh $INSTALLERS_ROOT/
# 复制依赖包目录到INSTALLERS_ROOT
COPY requirements/ $INSTALLERS_ROOT/requirements/
# 复制依赖包到INSTALLERS_ROOT(在制作python-deps时需要一些额外操作,详细继续看文章)
COPY python-deps/ $INSTALLERS_ROOT/python-deps/
# 还记得build.bsh文件吗,我们已经把项目得运行文件复制到app文件中了,下面是把项目运行文件复制到APP_PATH,以后我们就要运行这里的代码启动app
COPY app $APP_PATH/
RUN set -ex \
# 安装miniconda 到 CONDA_PATH 下
&& bash $INSTALLERS_ROOT/Miniconda3-latest-Linux-x86_64.sh -bfp $CONDA_PATH \
# 创建虚拟环境到VENV_PATH下
&& $CONDA_PATH/bin/conda create -p $VENV_PATH --clone=base \
# 激活虚拟环境
&& source $CONDA_PATH/bin/activate $VENV_PATH
# 更新pip(提前把最新版本pip放在的python-deps文件夹中)
&& $VENV_PATH/bin/python -m pip install -U --no-cache-dir --no-index -f $INSTALLERS_ROOT/python-deps/ pip \
# 安装python-deps依赖包
&& $VENV_PATH/bin/pip install --no-cache-dir --no-index-f $INSTALLERS_ROOT/python-deps/ -r $INSTALLERS_ROOT/requirements/prd.txt \
# 退出虚拟环境
&& source $CONDA_PATH/bin/deactivate \
# 清除缓存
&& $CONDA_PATH/bin/conda clean --all --yes \
# 删除所有安装包
&& rm -rf $INSTALLERS_ROOT
# 设置工作目录
WORKDIR SAPP_PATH
fockerfile要做的事情在注释脚本里也说的比较清晰,但是有几个点还是要说一下,因为这里隐藏了一些前期准备工作。
1、python-deps制作
由于安装过程中是不能连接外网的,所以必须提前将虚拟环境中需要的python依赖包site-pakages的whl文件下载下来,放在python-deps中。
这里就用到了命令 pip download命令: pip download -d 要存放whl文件的目录 -r requirements.txt
并且为了保证一致性,我的操作是:
1、先生成一个创建好虚拟环境并且没有安装python依赖包的images,然后使用docker exec -it image_id /bin/bash 的命令进入容器
2、在容器里使用 pip download -d 要存放whl文件的目录 -r requirements.txt 命令进行python第三方包的下载
3、使用命令 docker cp image_name:文件名称 宿主机文件名 ,将第2步下载下来的python依赖包拷贝出来,然后放到我们的代码目录中,提交svn,下次就可以直接安装了。
一般公司的测试环境都会给你一个 proxy 地址,比如代理到清华源,你就可以在容器里下载依赖包了。
为什么不直接在本地下载呢?因为毕竟以后代码是在容器里跑,去容器里下载依赖包,是最能保证没有问题的。
现在是镜像包打好了,那么还有一些外部资源我们需要使用。
首先肯定是数据库,然后是存储资源。
先说一下存储资源,为什么要有这个东西呢?直接存储在容器里不就行了?
这是肯定不行的,你那么多重要的资源存储在容器,一旦容器不小心被删除了,资源就都找不到了。所以必须要将存储内容的文件夹挂在出来。这块大家可以自行查阅资料,就不多说了。
我们公司在存储这块是要申请专门的存储资源的,也就是pvc,周期比较长,要提前申请。如果你的公司也是这样,要提前注意一下。
由于我这此部署的项目比较简单,不用pvc,就跳过这步不讲了。
然后就是数据库了,这个基本每个项目都是要使用的,我上线的这个项目也不例外。我使用的是mysql。
为了方便测试,我都是使用容器启动mysql的,这样数据库的能力不受其他项目的库的影响了。
这里详细说一下,mysql容器的配置过程。
1、启动mysql容器
先去找安装有mysql的镜像,公司的仓库里是有这个镜像的。我直接拉下来,然后执行
sudo docker run --name=mysql -it -p 13306:3306 -e MYSQL_ROOT_PASSWORD=xxx123 -d mysql
# --name 是你给这个容器起的名字
# -it 不用说了把,docker内容了
# -p 13306表示对外暴露的端口,3306表示对容器内分发到哪个端口,所以我们外界访问时,要连13306端口
# -e 额外参数,MYSQL_ROOT_PASSWORD=xxx123,表示我要把root密码设置成MYSQL_ROOT_PASSWORD=xxx123
# 最后mysql是image名称
# -d 后台运行
2、创建用户
之后输入docker exec -it mysql bash进入容器bash,
接着输入mysql -uroot -p进入mysql。
由于我们登录的是root用户,拥有所有权限。但是在实际成产中,只会给你一个只能用于某一个数据库权限的账号。只能对数据表的数据进行操作,而不能对表进行增加或删除。因此为了保持一致,我们也需要创建这样一个权限的账号。
为什么不直接用root账号,权限多就省事了,可以使用sqlachemy中的建表数据创建表。但是你想过没,到了生产上,很可能你会因为一个操作没有权限而报错了,就是因为你在测试环境使用了root账号,所以没有查出来。生产出问题,可是大问题。
ok,那我们下面进行创建用户的操作。
create datebase my_project_database;
use mysql;
CREATE USER 'sit_test_user'@'localhost' IDENTIFIED BY 'xxx_password';
GRANT SELECT, INSERT, UPDATE, REFERENCES, DELETE, CREATE, DROP, ALTER, INDEX, CREATE VIEW, SHOW VIEW ON sit_test_user.* TO 'my_project_database'@'%' identified by 'xxx_password';
flush privileges;
# 为自己的项目创建database
create datebase my_project_database;
# 使用 mysql数据库
use mysql;
# 创建用户,名字 sit_test_user, 密码xxx_password
CREATE USER 'sit_test_user'@'localhost' IDENTIFIED BY 'xxx_password';
# grant语句授权,sit_test_user为授权给哪个用户, my_project_database为将这个数据库的权限授予出去。最后的 “%” 表示外界IP可以使用sit_test_user这个用户名进程连接。注意,要写identified by 用户密码
GRANT SELECT, INSERT, UPDATE, REFERENCES, DELETE, CREATE, DROP, ALTER, INDEX, CREATE VIEW, SHOW VIEW ON sit_test_user.* TO 'my_project_database'@'%' identified by 'xxx_password';
# 刷新缓存,将配置生效
flush privileges;
3、导入创建表的sql语句
其实在开发中,我们都是直接在项目启动时,如果database中没有数据表,sqlalchemy会直接创建好表。
但是在生产中,怎么可能随便让你的代码去增删数据表?
所以一般真实上线时,都是我们把建表语句写好,然后运维提前把表给你建好,轮到你上线了,直接把项目启动起来,使用运维给你建好的表就ok了。
所以,我们就要进行数据表的建表操作。
这个就比较简单了,在navicat等工具里直接将开发环境的建表语句导出,然后到docker的mysql中执行就行了。
就不详细说了。
最后,如果你当时使用 的docker run -it mysql,可以使用 Ctrl+P+Q #容器不停止退出
否则可以使用exit直接退出docker 容器。
这个时候,mysql的就算是弄好了。
k8s部署前,肯定要写yaml文件。由于此次上线的项目比较简单,启动一个app就可以了,所以yaml文件写起来很简单。就不多介绍了。
之前部署模型引擎集群时,有一些费事,有机会单独说一下模型引擎的部署。
我一共写了2个yaml文件: api_deploy.yaml api_service.yaml
顾名思义,api_deploy.yaml是用于设置pod部署使用,里面参数主要约定了部署参数;
api_service.yaml 主要设置一些网络关系,如要对外、对内端口,集群通讯端口等。
yaml文件的内容如下:
1、api_deploy.yaml
apiVersion: extensions/vibetal
kind: Deployment
metadata
name: api-gateway
spec:
replicas: 3
template:
metadata:
Labels:
app: api-gateway
spec:
containers:
-image: "123.23.123/backend:r10157-dt16142411161
name: xxx
env:
-name: PROFILE
value: "sit"
resources:
equests:
cpu: "50m"
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
nodeselector:
app_name: testapp
2、 api-service.yaml
apiVersion: v1
kind: Service
metadata:
name: xxxxx
spec:
type: NodePort
ports:
-port: 8080
targetPort: 8080
nodePort: 32001
selector:
app: xxxx
由于都是最简单的yaml文件,这里就不详细写脚本的注释了。
并且的不同项目的yaml文件差异也是比较大的。大家可以自行去学习一下k8s的yaml文件的写法。
最后命令进行部署:
kubectl apply -f api_service.yaml
kubectl apply -f api_deploy.yaml
项目就正式启动起来了。
然后使用 kucectl get pods 查看pod的启动状态。
我是启动了3个副本,如图就已经启动成功了。
那么怎么看按他们对外暴露的端口呢?
可以使用 kubectl get svc命令查看,如下,可以看到对外暴露的端口是32002,外界可以进行访问
另外,如果启动pods后,发现pod没有启动成功。这时别慌,可以使用 kubectl logs image_id 查看当前这个pod的启动日志。看是哪里错了,进行相应修改。
模拟投产是为投产前的预演,所以要尽最大可能与上线一样,主要包括网络关系的设置、权限的设置、软件安装等等。
不要怕费事,因为稍微有一些不一样,就有可能造成上线出现问题。特别是权限这块,就比如,我们必须要设置一个和上线时用户一样的权限。否则上线时,极有可能会因为权限不足造成报错,直接导致上线失败。而这种低级错误,被领导知道就相当不好了。
好了,今天就说到这里,后面如果还有更复杂的部署项目,再更新。