docker

docker 基本原理

docker是一个开源的应用容器引擎,开发者可以利用 docker 打包自己的应用依赖包到一个可移植的容器中,发布到各linux发行版中,这个也一种虚拟化,但是请注意不同于 vmware kvm xen 那种虚拟化。

  1. docker容器中运行的程序在宿主机中是可以访问的: ps

  2. docker容器工作目录在宿主机同样可以访问:df

为什么要用docker

  1. 微服务: docker 提供了轻量,简单的建模方式。一个应用一个容器,减少耦合度,docker官方也是推荐一个容器一个应用,这个场景下服务器中的分布式应用就相当于一群互联的容器。

  2. 启动速度快,支持快速部署,版本回滚,滚动升级。

  3. 保持环境一致

  4. CI

与 VM 的区别

docker基础

  1. docker核心组件

  • docker客户端与docker服务,这两个在一起就是docker引擎:docker客户端可以是cli ,可以是http restful client , docker服务就是docker daemon

  • docker镜像:

    1. docker 镜像由是文件系统叠加而来的, 最底层是 bootfs(启动之后会被卸载)

    2. rootfs 可以提供多种操作系统( readonly )

  • registry: 类似于 git 仓库, 它包括了 镜像、层以及 镜像的 metadata

  • docker 容器

docker部署

运行一个 docker

  • 安装完 docker 并且启动 docker 守护进程后通过 docker info 查看 docker daemon 服务端信息

  • 创建一个容器

root@photon:~ # docker run -it --name test_docker ubuntu /bin/bash
########################################################
# -i 表示容器内的 STDIN 是开启的
# -t 表示为容器分配一个伪 tty
# ubuntu 表示容器的 image 名字(如果不指定 tag ,默认为 latest)
# /bin/bash 表示在容器内执行的命令
########################################################
root@6706395e88a3:/# cat /etc/*release*
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
NAME="Ubuntu"
VERSION="18.04.3 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.3 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
root@6706395e88a3:/#

########################################################
# docker 已经在 hosts 文件中为该容器的 IP 添加 hosts 记录
########################################################

root@896840bdedf6:/# cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.18.0.3      896840bdedf6
root@896840bdedf6:/#

docker 常用命令

  • docker pull/push : 获取/推送 image

  • docker search: 搜索 image ( 从 docker-hub 中搜索)

  • docker rm/rmi : 删除容器/镜像

  • docker images : 列出 images

  • docker start/stop/restart

  • docker ps: list containers

  • docker logs [ -f ]: Fetch the logs of a container

  • docker top / docker stats:

  • docker exec: Run a command in a running container

  • docker inspect: Return low-level information on Docker objects

  • docker save: 保存 image 到文件 docker save busybox:latest > docker-image-bb.tar

  • docker load: 从文件导入 image docker load < docker-image-bb.tar

构建 docker

docker commit

可以理解为 往版本控制系统内提交变更。 其中 commit 提交知识创建容器的镜像与当前容器之间的差异部分。

步骤

  1. 创建一个容器

  2. 在容器内变更( 比如使用 ubuntu:latest image 创建出来的容器,缺少常用测试工具,我们通过 apt 在 容器内安装应用)

  3. 通过 docker commit 把此容器提交为 image

docker run -it ubuntu --name test-commit /bin/bash
##
apt update
apt install iputils-ping
exit
##
docker commit test-commit ubuntu-ping
docker images

docker build with a Dockerfile(推荐)

Dockerfile 文件与 docker build 构建镜像: Dockerfile 使用基于 DSL 语法构建一个 Docker 镜像,推荐使用 Dockerfile 方式的原因是:通过 Dockerfile 方式更具备 可重复性、幂等性。

步骤

  1. 创建目录( build env)

  2. 创建 Dockerfile

  3. docker build

详细说明一下 Dockerfile:

Version: 0.0.1
FROM ubuntu:16.04
MAINTAINER abc@gmail.com
RUN apt-get update; apt-get install -y nginx
RUN echo 'Hi, I am in your container' \
         >/var/www/html/index.html
EXPOSE 80

Dockerfile 由一组指令和参数构成,每条指令必须大些( FROM ),每一条指令都会创建一个新的分层并且提交。

workflow:

• Docker runs a container from the image.
• An instruction executes and makes a change to the container.
• Docker runs the equivalent of docker commit to commit a new layer.
• Docker then runs a new container from this new image.
• The next instruction in the file is executed, and the process repeats until all instructions have been executed.

Dockerfile 指令

  • CMD:指定一个容器启动时要运行的命令( RUN 指令时构建时运行的命令)如果在 Dockerfile 中有多条 CMD 指令,则最后一条生效(如果需要在一个容器中运行多个进程或者命令,可以使用 Supervisor/runit 之类的进程管理工具),可以被 docker run 后的参数覆盖

小技巧

如果需要在一个容器中运行多个进程或者命令,可以使用Supervisor/runit 之类的进程管理工具

  • ENTRYPOINT:与 CMD 指令类似,在 docker run 后面的参数会被当作参数传递给 ENTRYPOINT。

小技巧

docker run --entrypoint="" 参数可以覆盖 ENTRYPOINT 指令

  • WORKDIR:从 image 创建一个新的容器时,在容器内部设置一个工作目录, ENTRYPOINT 和 CMD 指定的程序会在这个目录下执行。

小技巧

docker run -w 参数可以覆盖工作目录

  • ENV: image 构建过程设置环境变量,后续的 RUN 指令会使用 ENV。 ENV 一条指令设置多个环境变量 ENV PYTHON_PATH=/usr/local/bin/python3 ARCHFLAGS="-arch i386", 也可以在其他的指令里面使用ENV TARGET_DIR /opt/app WORKDIR $TARGET_DIR,

小技巧

docker run -e 参数可以传递环境变量

  • USER:基于该 image 启动的容器以 USER 用户身份来运行,也可以指定 UID以及 GID, 如不指定用户,默认用户为 rootdocker run -u覆盖该值

USER nginx
# 组合
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
  • VOLUME: 用来向基于 image 创建的容器添加卷。一个卷可以是存在与一个或者多个容器内的特定目录,这个目录可以绕过联合文件系统,为容器提供数据持久化。添加多个卷VOLUME ["/opt/project", "/data"]Dockerfile 中的 VOLUME 与 docker run -v /data /data 有一个区别是,通过 VOLUME 指令创建的挂载点,无法指定主机上对应的目录,是自动生成的。

  • The VOLUME instruction adds volumes to any container created from the image. A volume is a specially designated directory within one or more containers that bypasses the Union File System to provide several useful features for persistent or shared data:

  • Volumes can be shared and reused between containers.

  • A container doesn’t have to be running to share its volumes.

  • Changes to a volume are made directly.

  • Changes to a volume will not be included when you update an image. • Volumes persist until no containers use them.

  • ADD:将构建环境下的文件和目录复制到镜像中 ADD app.py /home/data/web/app.py ,Docker 以目的地址的字符判断文件源是目录还是文件。如果目的位置不存在,Docker 会创建这个全路径,包括路径中的目录,新创建的目录与文件属性都为0755 UID:0 GID:0。

注意

另外 ADD 有点特别,就是在特殊情况下对 压缩包 解压缩

  • COPY :与 ADD 类似,但是不会做 extra 操作

  • LABEL: 向 image 中添加 kv 形式元数据,LABEL version="1.0"

  • STOPSIGNAL: 设置停止容器时发送什么样的系统调用信号给容器。如 9( SIGKILL )

  • SHELL: SHELL ["executable", "parameters"],这里我们看一下默认值即可:

  • Linux: ["/bin/sh", "-c"]

  • Windows is ["cmd", "/S", "/C"] 针对 windows 比较有效( cmd/ps )

  • HEALTHCHECK:

HEALTHCHECK --interval=5m --timeout=3s  CMD curl -f http://localhost/ || exit 1
--interval=DURATION (default: 30s)
--timeout=DURATION (default: 30s)
--start-period=DURATION (default: 0s)
--retries=N (default: 3)
# return code:
# 0: healthy - 容器健康, 可以使用;
# 1: unhealthy - 容器工不正常, 需要诊断;
# 2: reserved -保留, 不要使用这个返回值;
  • EXPOSE: 为构建的镜像设置监听端口,使容器在运行时监听。

注意

默认为 TCP,如果协议为 UDP:EXPOSE 80/udp

  • RUN: RUN 指令将在当前 image 顶部的 new layer 中执行命令,并提交, 生成的提交 image。两种形式:

  • RUN <command>

  • RUN ["executable", "param1", "param2" ....]

资源限制

默认情况下,容器没有资源限制,并且可以使用主机内核调度程序允许的尽可能多的给定资源。

  • CPU:

    • CPU affinity:CPUs in which to allow execution (0-3, 0,1)

      • docker run --cpuset-cpus="0" ubuntu-ping /bin/bash

      • docker run --cpuset-cpus="0,1" ubuntu-ping /bin/bash

      • docker run --cpuset-cpus="0-2" ubuntu-ping /bin/bash

    • CPU 资源相对限制

    • CPU 资源绝对限制

  • Memory:

使用多个容器

docker compose

docker-compose 使用场景

  • Start, stop, and rebuild services

  • View the status of running services

  • Stream the log output of running services

  • Run a one-off command on a service

安装

sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 也可以通过 pip 安装 pip install docker-compose
chmod +x /usr/local/bin/docker-compose

创建步骤

  1. Define your app’s environment with a Dockerfile so it can be reproduced anywhere.

  2. Define the services that make up your app in docker-compose.yml so they can be run together in an isolated environment.

  3. Run docker-compose up and Compose starts and runs your entire app.

例子

docker-compose.yml

compose-file doc

version: '3'
services:
  web:
    image: reg.test.k8s/cicd-test/flask-compose:0.1
    command: python app.py
    ports:
     - "5000:5000"
    volumes:
     - .:/home/web
  redis:
    image: reg.test.k8s/library/redis

Dockerfile

FROM reg.test.k8s/cicd-test/flask-base:0.2

ADD . /home/web

WORKDIR /home/web

RUN pip install -r requirements.txt

CI/CD

引言

在传统软件开发过程中,集成通常发生在每个人都完成了各自的工作之后。在项目尾声阶段,通常集成还要花费很长的时间来完成。 持续集成是一个将集成提前至开发周期的早期阶段的实践方式,让构建、测试和集成代码更经常与快速的发生。 持续交付让软件在短周期可以被可靠地发布。其目的在于更快、更频繁地构建、测试以及发布软件。通过加强对生产环境的应用进行渐进式更新。

系统整体设计

digraph G {
graph [rankdir=LR ]
    subgraph cluster0 {
        style=filled;
        color=lightgrey;
        node [style=filled,color=white];
        Harbor[shape=cylinder];
        test_master01 -> test_node01[ fontcolor = "black", label = "rollupdate" width = 3];
        test_master01 -> test_node02[ fontcolor = "black", label = "rollupdate"];
        test_master01 -> test_node03[ fontcolor = "black", label = "rollupdate"];

        label = "k8s-test-env";
    }

    subgraph cluster1 {
        style=filled;
        color=lightgrey;
        node [style=filled,color=white];
        ECR[shape=cylinder];
        aws_master01 -> aws_node01[ fontcolor = "black", label = "rollupdate"];
        aws_master01 -> aws_node02[ fontcolor = "black", label = "rollupdate"];
        aws_master01 -> aws_node03[ fontcolor = "black", label = "rollupdate"];
        label = "k8s-aws_env";
    }



    Jenkins[shape=tab];
    GithubLocal[shape=cylinder];


    subgraph cluster2 {
        node [style=filled];
        DEV_A;
        DEV_B;
        DEV_C;
        label = "Git";
        color=blue
    }

    DEV_C -> GithubLocal[ color = "blue"];
    DEV_A -> GithubLocal[ color = "blue"];
    DEV_B -> GithubLocal[color="blue", fontcolor = "blue", label = "git push"];
    QA[shape=circle];

    GithubLocal -> Jenkins[ fontcolor = "black", label = "webhook"];
    QA -> Jenkins[ color="green";fontcolor = "green", label = "confirm test" ];
    Jenkins -> Harbor[ color="blue", fontcolor = "blue" label= "push image" weight=10, penwidth=2 arrowsize=1];

    Jenkins -> test_master01 [color="red";fontcolor = "red", label = "update"];
    test_master01 -> Harbor[fontcolor = "black", label = "pull image" len=0000.1] ;

    Jenkins -> ECR[ color="blue", fontcolor = "blue" label= "push image" weight=20];
    Jenkins -> aws_master01[label = "update canary(1)"]
    aws_master01 -> ECR[ label="pull image"]
    QA -> Jenkins[ color="red";fontcolor = "red", label = "confirm staging"];
    Jenkins -> aws_master01[label = "update all(2)"]
}

开发人员提交的到 Git 仓库中应包含以下文件:

  • 应用代码

  • Jenkinsfile:Jenkins 的 pipelines 文件

  • Dockerfile: docker 根据 Dockerfile 文件构建 image

  • Helm value 文件: Kubernetes 的包管理工具,类似于 yum/rpm apt/dpkg

  1. 应用以 Docker image / helm chart 方式发布(开发人员需在提交代码的同时提交 Dockerfile Jenkinsfile 和 Helm 相关配置)

  2. 代码推送到 GitHub 仓库后会触发 CI webhook,Github 发送触发 CI 的 http request 到 Jenkins 服务器

  3. Jenkins 服务器收到 Github 的 request,从指定的 repo 拉取代码并读取 Jenkinsfile 开始构建

  4. Jenkins pipleline 应包含以下 stage:

    1. 获取 Git

    2. 从 harbor 中拉取 base image

    3. 根据 Dockerfile 构建 image

    4. 构建成功后 Jenkins 使用新构建的 image 启动容器执行 unittest

    5. unittest 结束后将 image push 到 harbor

    6. Jenkins 通过 Helm upgrade / set new image ( deployment ) 方式更新到测试环境的 Kubernetes,执行完毕这一步 Jenkins 进入暂停状态

    7. kubernetes 的 nodes 滚动更新,完成应用升级,升级完毕后,QA 根据测试用例测试,如果结果符合预期,测试登录 Jenkins 恢复操作

    8. Jenkins 把刚才构建的 image 推送到 aws ECR

    9. Jenkins 通过 Helm upgrade / set new image ( deployment ) 方式更新到AWS 生产环境的 Kubernetes 的 灰度发布环境,执行完毕这一步 Jenkins 再一次进入暂停状态

    10. kubernetes 的 nodes 滚动更新,完成应用升级,升级完毕后,QA 根据测试用例测试,如果结果符合预期,测试登录 Jenkins 恢复操作

    11. Jenkins 通过 Helm upgrade / set new image ( deployment ) 方式更新到 AWS 生产环境的 Kubernetes 正式环境发布应用

  5. 生产环境应用更新完毕。

Jenkins 在构建过程中任意一步 shell 执行的 ``exit code !=0`` 认为本次构建失败。

灰度发布

通过 k8s 提供强大的 label selector ,我们可以实现灰度发布:

回退方案

k8s 集群提供了 undo 和 history 等方式 rollback:

  • undo 为直接 rollback 到上一个版本 kubectl rollout undo  deployment/flask-deployment-canary

  • history 可以根据历史版本 rollback

helm 也提供类似功能:

helm rollback [flags] [RELEASE] [REVISION]

运行环境

软件

  • 版本管理软件:Github

  • 自动化构建工具:Jenkins

  • Kubernetes 集群:测试环境一套集群, AWS生产环境一套集群

  • docker image 镜像仓库: 测试环境 harbor, 生产环境是 ECR

  • 自动化测试工具: 待补充

硬件

测试环境

在 ESXI 虚拟化平台创建虚拟机,目前的环境为: - k8s: master(1台),node(3台) - Jenkins(1台) - Harbor(1台) - Github-local

正式环境

部分部署:

  • Jenkins

  • Github-local

  • k8s 集群(待定)

安全问题

  • Jenkins 需要有很多的权限:

  • git仓库访问权限,

  • 对 K8S 集群可以更新 helm 或 set new image

  • Github webhook 的安全性待确认

  • staging 阶段 canary pod 会使用现网数据,可能会产生脏数据

优点

  • Dockerfile, Jenkinsfile, 以及 Helm 配置文件都存在 Git 仓库中,可以很方便 diff 版本之间的差异

  • 提升集成与发布的效率

不足与改进

  • 对 Jenkins 的依赖比较大,

  • 部分服务存在单点问题( Jenkins, Harbor)