前言

分享一些关于部署网站的简单技巧,和jenkins、docker等CICD和容器的技术。
一系列操作涉及到的技术工具和平台有:阿里云服务器, CentOS系统, 宝塔(服务器图形界面工具), jenkins, docker, docker-compose, docker swarm
内容简介如下:

  • Jenkins登场,用Jenkins简单配置流水线,在服务器上部署网站。

  • 使用docker,docker-compose等配置文件和相应使用。

  • docker容器与容器网络连接,以及容器与宿主机的连接。

  • docker swarm的集群部署和滚动升级的简单使用。

    Jenkins登场

    Jenkins是一个开源的、提供友好操作界面的持续集成(CI)和持续部署(CD)的工具。在CI/CD这一块,Jenkins是一张王牌。持续集成和持续部署的目的很简单,如下图所示,可以利用hook或手动,对开发项目的新提交进行构建、测试和部署到生产环境上。以及附带日志收集和状态监控等等功能。

    既然是自用的一些工具和云服务器,会希望简介易用一些,所以我自己给云服务器安装了宝塔系统。宝塔系统是一款不错的服务器图形界面工具,更容易操作云服务器和部署业务。大体界面,如下图所示。

    如果不用图形化工具的话,可在终端中输入命令,选定一个适合的绝对路径(我用的/root/env),下载好jenkins的war包(官网下载很方便)。使用命令nohup java -jar /root/env/jenkins.war & ,这里 nohup XXXXX &是Linux系统的挂起命令,即使关闭终端,这个挂起的命令也不会退出,除了错误退出和重启云服务器之外,都会一直在进程中。使用了nohup命令后,会在终端的路径之中生产nohup.out日志文件。当然我们也可以在命令之中指定,生成的日志文件的名字和路径,如nohup java -jar /root/env/jenkins.war > /root/env/jenkins.out 2>&1 & 则指定jenkins.out日志文件生成在/root/env目录之下。
    jenkins默认端口为8080,如果自己的云服务器的安全组和对应的服务器工具没有放开8080端口,那么利用公网ip也访问不到。
    如果是新接触jenkins的朋友,可以搜一搜网上相关资料,需要进行一些简单的配置,一些相关的插件的下载。
    这里我简单地使用jenkins(本文是汉化插件加持)。Dashboard -> 新建任务 -> 流水线(pipline) -> 选择github项目,输入github库的URL -> 下滑到流水线定义那,选择Pipline script from SCM -> SCM 选择Git ,配置好git库URL和凭证(私库才需要配置) -> 其他配置默认即可,点击保存
    jenkins的pipline相应差不多了,另外的需要的是自己Git项目里的根目录中的Jenkinsfile文件。我的Jenkinsfile文件配置得比较简单:

    pipeline
    {
    agent any
    stages {
      stage('Build')
      {
        steps {
          sh" gradle clean build -x test "
        }
      }
        stage('Deploy')
      {
        steps {
           sh" kill -9 \$(lsof -i:8034) "
           sh" JENKINS_NODE_COOKIE=dontKillMe nohup java -jar /root/project/myservice.jar & "
        }
      }
    }
    }
    

    分别在build阶段,用gradle构建工具重新打包和跳过测试。在deploy阶段,也就是jenkins在构建出jar包之后的部署阶段。因为我直接部署在云服务器之中,端口占用为8034,我需要先杀死8034端口的进程,然后在jenkins里用JENKINS_NODE_COOKIE=dontKillMe加上部署命令,这里也是用的jenkins生成jar包的绝对路径的命令。当jenkins构建和部署成功后我们最希望看到的绿色流水线阶段试图出现了:

    这样一趟操作下来,构建和部署都有了。但相对的缺点也很明显,如果之前的服务没问题,新部署的服务出问题,岂不是网站就崩了,也没法回滚原版本;并且服务在更新之时,也会出现网站服务停掉后更新的情况;以及项目依赖多了对一个云服务器环境依赖也是麻烦事。而这则是真正项目会用到docker,k8s等容器技术的原因。

    使用 docker

    docker容器技术,我就不多赘述了,隔离、切换环境等等操作很好用。简单讲一下自己的理解,dockerhub类似于github;image类似于git项目,可以推上hub也可以本地放置;container是image的实例,docker都是跑的container,每一个container跑起来都是一个残血版的Linux系统,有独立的进程和端口,可以通过映射端口到宿主机。
    我在项目中,加入简单的docker运用。先在项目文件夹的根目录中加入Dockerfile文件。这里为的是创造一个需要的容器,file内FROM是拉取远程镜像或本地镜像,Dockerfile本质就是为了创建一个自己定义的镜像。
    比如这里,我就是拉取合适的jdk版本的远端镜像,WORKDIR定义镜像的终端一开始的目录,COPY 为把项目中的构建工具的目录里的文件复制进镜像定义的路径和指定的文件名字。ENV定义镜像的环境变量,比如时区,甚至一些ip和端口之类的。EXPOSE是镜像生成容器后,服务在容器内的端口。可以在项目根目录再创建一个.dockerignore文件,类似于.gitignore文件,当镜像要打包文件进入的时候,会自动排除。

    FROM adoptopenjdk/openjdk11:latest
    WORKDIR /app
    COPY ./build/libs/myservice-1.0.6.jar /app/myservice.jar
    ENV LANG=en_US.UTF8
    ENV TZ=Asia/Shanghai
    EXPOSE 8034
    

    有了上述的文件,在对应目录下,打开终端,输入命令docker image build -t myservice . 则可创建名为myservice的镜像,命令中的-t是指定名字和tag,末尾的点是指定镜像上下文为当前目录。
    有了镜像文件,要run起来成为容器,需要命令docker run , 如docker run --name myapp -p 8034:8034 -d myservice —name参数为容器命名;-p参数为端口映射,比如这里就是容器内的8034端口映射到宿主机的8034;-d参数为指定非后台运行。run起来的容器在mac的docker desktop上能看日志和使用命令行,在Liunx服务器终端上可以用命令docker logs <你的容器名或容器ID> 容器名如果没有自己在配置中制定名字,可以用docker ps查看各个容器的信息。要进入容器的终端用命令docker exec -it <你的容器名或容器ID> /bin/sh
    一些复杂的docker run命令,大量参数涉及到多行,在终端上每一行末尾加上空格和\ ,如下所示:

    docker run --name mydb \
      --network app-network \
      -v mysql-data:/var/lib/mysql \
      -e MYSQL_ROOT_PASSWORD=secret \
      -e MYSQL_DATABASE=myapp \
      -d mysql:8.0
    

    使用 docker-compose

    docker-compose是在docker之中涉及到容器编排的技术。使用也很广泛。它的使用,是为了集中管理多个容器,试想一下,如果一个微服务项目,有多个模块和镜像需要docker启动,能快速部署、彼此独立、分工合作是不是很不错的一件事。
    使用compose,首先在项目根目录下,建立docker-compose.yml文件。如下只是一个简单的demo:

    version: "3.9"                      # 此处compose的版本影响部分参数是否有效,具体参数可以查询本文参考里的docker官网
    services: 
    myapp:                            # 单独的服务名
      build: ./                       # dockerfile文件在当前上下文目录
      image: myservice                # image名字
      volumes:                        # 挂载在docker宿主机上的目录,指定的好处是可以删除镜像和容器后,目录不丢失内容。比如mysql的表内容可以一直存在直到卷被删除。冒号前为宿主机卷位置,冒号后为容器内位置。macos里是找不到实际挂载位置的,因为是虚拟raw文件。
        - app-data:/app
      ports:
        - "8034:8034"                 # 映射端口,冒号前为宿主机,冒号后为容器
      environment:                    # 环境变量
        DOCKER_HOST: 172.20.0.3
      depends_on:                     # 依赖项,依赖项会更早启动,保证启动正常
        - myredis
      restart: always                 # docker重启,会同时重启容器
      entrypoint: java -jar XXX.jar   # 启动容器后运行的命令
    myredis:
      image: redis:latest
      volumes:
        - redis-data:/var/lib/redis
      ports:
        - "6380:6379"
    volumes:                            # 书写格式,用到了挂载卷,就要加
    app-data:
    

    用命令docker-compose up -d 启动相关的多个容器。如果代码更新后需要的镜像也要更新,可使用docker-compose up --build -d,不会走缓存的镜像,会强制更新镜像,然后run容器。
    dockerc重新生成镜像会走一些缓存之内的,比如你只更新镜像中的jar包或前端打的包,剩余FROM拉取的远端镜像不会再从头拉取,而是走的缓存。终端输出如下图所示,可以看到例如FROM拉取镜像是在瞬间完成的,这就是走的缓存。而之前说的参数--build只是重新更新这个完整镜像,必要走缓存的步骤依旧会走,以此节省性能。

    并且可以使用docker-compose up --no-deps --build -d myapp命令来更新具体想要更新的那个镜像,而不用把整个服务的所有镜像都重塑一遍。在jenkins里,就可以在deploy阶段,加入这个命令去更新你想更新的服务。
    当代码更新之后,更新docker-compose文件更改tag参数,这样可以做到旧版本的image仍旧存在,如果新的image的容器出一些不可预测的问题,可以及时手动回滚调整。
    springboot中的application配置可以跟docker的环境变量配合,比如

    url: jdbc:mysql://${DOCKER_HOST:localhost}:3306/myapp?useSSL=false&serverTimezone=Asia/Shanghai
    

    ${DOCKER_HOST:localhost}的意思就是,如果有DOCKER_HOST环境变量则用它,如果没有,则使用localhost这个数据。
    jenkins的pipline和docker容器之间最好进行恰当的职责分离,如测试、构建、打包环节可以交给jenkins,而docker部署的镜像环境尽可能纯粹。比如前端的依赖包留在服务器目录中,只把打好的包装入docker镜像。镜像里尽可能只有一些必要的代码产物和运行插件。值得注意的是,如果前端项目在jenkins已经打好静态包了,则docker里配置的环境变量则无法在前端的静态文件包里生效了。

    docker内容器之间的连接

    docker内各个容器之间,和docker容器跟宿主机之间的连接的细节和知识点可以看看网上资料的docker网络模式。

    docker各个容器之间的连接很常见,比如我有一个后端项目,需要连接数据库,两个服务分别在两个镜像之中,分别开启两个容器,则之间需要连接。这里我举例一个后端容器与一个mysql容器的连接命令。

    docker run --name mydb \
      --network app-network \
      --network-alias mydb \
      -v mysql-data:/var/lib/mysql \
      -e MYSQL_ROOT_PASSWORD=secret \
      -e MYSQL_DATABASE=myservice \
      -d mysql:8.0
    

    这里的—network是在容器中创建一个网络,这个网络可以供其他容器去连接,—network-alias是网络别名,这个参数很重要,如果你需要你的后端项目连接容器的数据库,则不可以使用localhost作为host,而是这个网络别名。比如在springboot的application配置中,
    url: jdbc:mysql://mydb:3306/myservice?useSSL=false&serverTimezone=Asia/Shanghai,这里的mydb就是mysql容器网络别名,myservice则是docker run 环境变量会自动生成的空的有效的数据库。对应的后端项目命令docker run --name myapp --link mydb -p 8034:8034 -d myservice 这个命令中 —link后的参数就是上述的容器网络别名。这样就可以完成两个容器之间的连接。
    那么在docker-compose里如何完成多个容器的互相连接呢,这里我给出我的方案,配置compose文件:

    version: "3.9"
    services:
    myapp:
      build: ./
      image: myservice:1.0.6      # image的tag
      ports:
        - "8034:8034"
      environment:
        DOCKER_HOST: database     # 可以给到application配置文件的环境变量
      links:
        - "mydb:database"         # 将服务名别名为database,然后通过环境变量传递给jar包 
      depends_on:
        - mydb
      restart: always
      entrypoint: java -jar /app/hok-myservice.jar
    mydb:
      image: mysql:8.0
      volumes:
        - mysql-data:/var/lib/mysql
      ports:
        - "3305:3306"
      environment:
        MYSQL_ROOT_PASSWORD: secret
        MYSQL_DATABASE: myapp
        TZ: Asia/Shanghai
      restart: always
    volumes:
    mysql-data:
    

    当然也可以显式地创建和制定容器连接的网络。external字段和 docker create network命令,这个如果有需要可以去了解下。

    docker容器与宿主机连接

    那么还有个问题是,容器能跟宿主机进行连接吗?
    当然是可以的,这里面分两种情况。

  • 第一种Linux系统上,使用ifconfig,就能查询到docker0的inet 网络ip是什么,一般为172.18.0.1,在后端镜像中,用这个ip代替localhost的host即可,当然这也涉及到宿主机的db的权限是否对外开放的问题,可以查一查如何修改和刷新数据库访问权限。

  • 第二种就是在MacOS上,MacOS上跟Linux上不一样,不能通过ifconfig查询ip,但是可以通过其他途径。比如通过docker desktop -> setting界面 -> resources选项 -> Network 界面 -> Docker subnet ; 默认都是192.168.65.0/ ,在后端项目中的url配置host要用192.168.65.2,因为最后的子网掩码0和1都被占用了,1作为docker的网关。docker desktop上的subnet ip界面如下图所示:

    如果是本地开发调试,可以使用容器里装代码产物,连接宿主机的数据库,但在生产环境,不建议这样使用,这样破坏了最初使用容器环境之间的隔离思想,对于整个docker的使用流程和思想背道而驰。

    使用 docker swarm

    上述一堆docker的使用,但貌似还是没解决最关键的问题,构建部署之后,如果现版本出问题,没有特别方便的办法回滚和及时恢复之前服务的版本,会影响业务使用。这就需要docker swarm滚动升级了(k8s也可,这里我用的是swarm)Docker Swarm比起k8s要更简单,并且与docker-compose兼容。对于没有接触过集群部署的小伙伴,swarm是上佳的选择。

    关于Docker Swarm,是一个容器编排技术。Docker Swarm 和 Docker Compose 的区别在于 Compose 用于在同一主机上配置多个容器。Docker Swarm可以在多个主机配置多个容器。节点、集群管理、滚动升级、均衡负载等都是Docker Swarm的优势。

    我对于Swarm和集群相关的使用比较浅,如果未来有机会深入理解和使用,再给大家分享更好的经验和文章。这里分享一些命令吧。
    Swarm单词本身就是群的意思,docker swarm <option>命令可以创建、加入、离开一个集群。在使用swarm之前,要使用命令docker swarm init,以及之后docker操作不需要swarm需要关闭,应该使用命令docker swarm leave,如果只需要使用compose功能而不关闭swarm,则终端会一直报warning提醒。
    service是一类容器,service是和swarm非常紧密的内容,使用命令docker service create --replicas 3 -p 80:80 --name nginx nginx:latest即创建了一个swarm的service,replicas参数指定实例数量,其他参数跟docker run的意思大差不差。docker service update --image nginx:3.0.7 nginxdocker service rollback nginx分别是滚动升级镜像和回滚镜像的命令,有了滚动升级的命令,则不用担心在更新服务的时候,网站无法使用影响体验了,回滚命令也给予了极大的容错空间。
    因为swarm和compose是兼容的,所以可以在docker-compose文件中也可以加入一些独属于swarm的配置参数,如deploy等等。

    services:
    myapp:
      省略内容
      deploy:
        mode: replicated
        replicas: 3
    

    如果不想compose和swarm配置文件在一起,可以另外重命名自己的swarm配置文件,比如docker-deploy.yml,这里使用命令docker stack deploy -c <docker-deploy.yml> <你的service名字>可以指定docker swarm的配置文件启动service,之后可以使用命令docker service logs <你的容器名>查询容器日志、docker service ps <你的容器名>来查询具体容器的状态以及docker service ls 查询所有service的容器状态,关闭service可以用docker stack down <你的service名字>

    参考

    本文到此就全部结束了,大家关于本文在实践和内容上有想交流的地方,都可以找我交流和提出宝贵的意见哈。
    这是自己在使用docker之时参考的一些资料,其中不乏官方资料和优秀资料。

  • docker官方文档

  • docker 简介

  • runoob-docker资料

  • 教程:创建具有 MySQL 和 Docker Compose 的多容器应用

  • Docker中文文档

  • Mac宿主机访问Docker容器网络