Docker Compose 之进阶篇

笔者在前文(https://www.cnblogs.com/sparkdev/p/9753793.html)和(https://www.cnblogs.com/sparkdev/p/9787915.html)两篇文章中分别介绍了 docker compose 的基本概念以及实现原理。本文我们将继续探索 docker compose,并通过 demo 介绍一些主要的用法。
说明:本文的演示环境为 ubuntu 16.04。

应用多个 compose 配置文件

docker-compose 命令默认使用的配置文件是当前目录中的 docker-compose.yml 文件,当然我们可以通过 -f 选项指定一个其它名称的配置文件,比如:

$ docker-compose -f docker-compose-dev.yml up

更酷的是我们可以添加多个 -f 选项,docker-compose 会自动合并它们,当然也会根据先后顺序把一些重复的配置项覆盖掉。 下面我们来演示一个常见的使用场景,先创建一个名称为 docker-compose-base.yml 的配置文件,其内容如下:

version: '3'
services:
  web:
    build: .
  redis:
    image: "redis:latest"

然后再创建名称为 docker-compose-dev.yml 的配置文件:

version: '3'
services:
  web:
    ports:
     - "5000:5000"

下面的命令会同时应用这两个配置文件:

$ docker-compose -f docker-compose-base.yml -f docker-compose-dev.yml config

config 命令不会执行真正的操作,而是显示 docker-compose 程序解析到的配置文件内容:

未分类

很显然,我们指定的两个配置文件的内容被合并了。接下来我们再来看看配置文件覆盖的情况。新创建一个名为 docker-compose-prod.yml 的配置文件,编辑其内容如下:

version: '3'
services:
  web:
    ports:
     - "80:5000"
  redis:
    image: "redis:alpine"

然后执行下面的命令:

$ docker-compose -f docker-compose-base.yml -f docker-compose-prod.yml config

未分类

这次 docker-compose-prod.yml 文件中的 image 设置覆盖了 docker-compose-base.yml 文件中的设置,并且映射的端口也改成了 80:5000。
就像 demo 中演示的那样,我们可以通过多次指定 -f 选项的方式配置不同的环境,并且共用一份基础的配置文件。

其实 docker-compse 还默认还支持一种合并、覆盖配置文件的写法,就是使用约定的文件名称 docker-compose.yml 和 docker-compose.override.yml。下面我们把 docker-compose-base.yml 文件改名为 docker-compose.yml,把 docker-compose-prod.yml 文件改名为 docker-compose.override.yml,并直接执行不带 -f 选项的命令:

$ docker-compose config

结果和前面是一样的,docker-compose 自动合并了配置文件 docker-compose.yml 和 docker-compose.override.yml。这种方式虽然省去了指定 -f 选项的麻烦但其缺点也是很明显的,就是无法指定更多不同的应用场景。

使用 network

Docker 提供的 network 功能能够对容器进行网络上的隔离,下面的 demo 中我们创建三个 service 和两个虚拟网络(注意,该 demo 主要是演示 network 的用法,所以笔者并没有配置 proxy service 中的 nginx):

version: '3'
services:
  proxy:
    image: nginx
    ports:
      - "80:80"
    networks:
      - frantnet
  webapp:
    build: .
    networks:
      - frantnet
      - endnet
  redis:
    image: redis
    networks:
      - endnet
networks:
  frantnet:
  endnet:

其中的 proxy 和 webapp 连接到网络 frantnet 上,webapp 和 redis 连接在了 endnet 上(请使用《Docker Compose 简介》一文中介绍的 web 应用和 Dockerfile 来创建 webapp service)。请使用下面的命令来启动应用:

$ docker-compose -p testnet -f docker-compose-net.yml up -d

未分类

从上图我们可以看到该命令一共创建了两个 network 和 三个容器。然后我们检查一下这三个容器的网络连接状态。先从 testnet_webapp_1 中 ping 另外的两个容器:

未分类

因为 webapp 服务同时连接到了 frantnet 和 endnet 两个网络中,所以它可以同时连接这两个网络中的其它容器(proxy 和 redis)。接下来再看看容器 proxy 和 redis 是否可以直接连通,我们从容器 testnet_redis_1 中 ping proxy(注意,执行这个操作前需要在容器 testnet_redis_1 中通过 apt-get update && apt-get install iputils-ping 命令安装 ping 命令):

未分类

无法从容器 testnet_redis_1 中 ping 通 proxy 容器,这也就说明我们通过不同的虚拟网络实现了容器网络之间的隔离,从而在最大程度上去保护后端网络的安全。

按顺序启动容器

默认情况下 compose 启动容器的顺序是不确定的,但是有些场景下我们希望能够控制容器的启动顺序,比如应该让运行数据库的程序先启动。我们可以通过 depends_on 来解决有依赖关系的容器的启动顺序问题,看下面的 demo:

version: '3'
services:
  proxy:
    image: nginx
    ports:
      - "80:80"
    depends_on:
      - webapp
      - redis
  webapp:
    build: .
    depends_on:
      - redis
  redis:
    image: redis

启动应用:

未分类

无论我们执行多少次这样的启动操作,这三个容器的启动顺序都是不变的。如果不应用 depends_on,每次执行 up 命令容器的启动顺序可能都是不一样的。
需要注意的是 depends_on 只是解决了控制容器启动顺序的问题,如果一个容器的启动时间非常长,后面的容器并不会等待它完成启动。如果要解决这类问题(等待容器完成启动并开始提供服务),需要使用 wait-for-it 等工具。

配置数据卷(volume)

数据卷是处理容器中的持久化数据的主要方式,在 compose 中我们可以通过两种方式来指定数据卷:

  • 使用命名的数据卷
  • 直接指定主机上的路径来创建数据卷

下面的 demo 演示了这两种数据卷的配置方式:

version: "3.2"
services:
  web:
    image: nginx:alpine
    volumes:
      - type: volume
        source: mydata
        target: /data
      - type: bind
        source: ./nginx/logs
        target: /var/log/nginx
  jenkins:
    image: jenkins/jenkins:lts
    volumes:
      - jenkins_home:/var/jenkins_home
      - mydata:/data
volumes:
  mydata:
  jenkins_home:

在这个例子中我们一共创建了三个数据卷,分别是两个命名的数据卷 jenkins_home 和 mydata:

未分类

其中的 jenkins_home 数据卷是给 jenkins 保存数据的。如果要在多个容器之间共享数据卷,就必须在顶级的 volumes 节点中定义这个数据卷,比如 mydata 数据卷,它被 web 和 jenkins service 共享了。比如我们在 web service 中的 mydata 数据卷中创建一个名为 hello 的文件,该文件会同时出现在 jenkins service 中:

未分类

我们还创建了一个 bind 类型的 volume 在当前目录下的 nginx/logs 目录下保存 nginx 的日志:

未分类

配置日志驱动

我们还可以通过 logging 节点为 service 指定日志驱动及其相关的选项:

version: '3'
services:
  web:
    build: .
    ports:
     - "5000:5000"
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"
  redis:
    image: "redis:latest"

上面的代码指定日志驱动为 json-file,存储日志的最大文件 size 为 200k,最多存储 10 这样大的文件。

在 compose file 文件中应用模板

从版本 3.4 开始,可以在 compose file 文件中使用 extension fields,其实我们可以简单的把它理解为可以重用的代码模板。模板的定义必须以 x- 开头,然后以 & 开头的字符串为模板命名,之后就可以以 * 加上模板的名称引用模板:

version: '3.4'
x-logging:
  &default-logging
  driver: json-file
  options:
    max-size: "200k"
    max-file: "10"

services:
  web:
    build: .
    ports:
     - "5000:5000"
    logging: *default-logging
  redis:
    image: "redis:latest"
    logging: *default-logging

运行下面的命令看看模板替换的情况:

$ docker-compose -p template -f docker-compose-template.yml config

未分类

上图显示所有对模板的引用都被替换成了模板的内容。

总结

Docker compose 是一件强有力的效率工具,本文只是介绍了一些常见的用法。如果你还想掌握更多内容,请参考 compose file 的官方文档。

docker指令学习记录

前言

本文为学习整理和参考文章,不具有教程的功能。其次,后面将会陆续更新各种应用的容器化部署的实践,如MySQL容器化,Jenkins容器化,以供读者参考。

镜像获取

docker pull [options] [Docker Registry地址]<仓库名>:<标签>

-a, --all-tags: 下载该镜像的所有版本

Docker Registry地址默认为Docker Hub,一般格式为IP:端口号
仓库名为两段式 <用户名>:<软件名> 默认用户名为library
标签不填则默认为latest

列出镜像

docker images [options] [Repository[:tag]]
默认情况会展示所有最终镜像,如果加上了镜像名,则会展示该镜像的所有信息
-a, --all: 展示所有镜像,包括中间层
-f, --filter filter: 根据某种条件对镜像进行筛选
--format string: 使用go的模板语法
-q, --quiet: 只返回镜像的ID

docker images -f since=mongo:3.2  #查看mongo3.2版本之后建立的镜像,如果是要在之前,则使用before
docker images --format "{{.ID}}:{{.Repository}}" #输出结构为ID:Repository

虚悬镜像

虚悬镜像是指既没有仓库名,也没有标签的镜像。这种镜像的产生常常由于当前的仓库名和标签被更新版本占用,导致当前境像失效。

docker images -f danling=true #列出所有虚悬镜像
docker rmi $(docker images -q -f dangling=true) #利用复合指令删除虚悬镜像

commit镜像

commit会将容器的存储层保存下来成为新的镜像

docker commit [options] <容器ID或容器名> [<仓库名>[:<标签>]]
-a, --author string: 容器所有者
-c, --change list: 在容器上执行Dockerfile指令
-m, --message string: 提交信息
-p, --pause: 提交过程中停止容器的运行,默认为true

docker history IMAGE #显示镜像的历史记录
docker diff CONTAINER #查看容器的改动

尽量不要使用commit指令构建镜像

Dockerfile

构建镜像

利用Dockerfile构建镜像。

docker build [options] PATH | URL | -
-f, --file string: Dockerfile的路径
--rm: 成功构建后删除中间镜像
-t, --tag: 以name:tag的形式为镜像命名
docker build -t nginx:v3 . #执行当前目录下的Dockerfile并构建镜像,新的镜像名为nginx:v3
docker build https://......   #直接从github构建,会自动clone这个项目,切换到指定分支(默认为master),并进入指定目录进行构建

最后的路径是指镜像构建的上下文,docker在build的时候会把该上下文中的而所有内容全部打包上传给docker引擎。当在Dockerfile中需要引用相对路径时,就是以该上下文作为当前指令执行的目录。可以编写.dockerignore文件来剔除无需打包的文件。
在默认情况下,如果不指定Dockerfile的位置,就会从构建的上下文寻找Dockerfile来执行

FROM

指定基础镜像,Dockerfile的第一行必须制定基础镜像

RUN

执行命令。RUN指令会新建一层并在其上执行指令,指令完成之后再commit该镜像。所以RUN指令中的内容应当尽可能合并,并且记得清除冗余的内容如缓存等。

RUN <指令>
RUN ["可执行文件", "参数1", "参数2"]
RUN mkdir newDir 
   && touch newFile

COPY

将构建上下文中源路径中的内容复制到目标路径之下。可以使用通配符。如果目标目录不存在,容器会帮助创建。复制过程不改变文件属性。

COPY 源路径 目标路径
COPY ["源路径",...,"目标路径"]

COPY hom* /mydir/

CMD

默认的容器的主进程的启动命令,在运行时可以指定新的命令来替代镜像设置中的默认命令。比如ubuntu的默认指令是/bin/bash。如果使用第一种形式,则会以sh -c的形式执行,这样就能够得到环境变量。容器中的应用都应该前台执行。

CMD <命令>
CMD ["可执行文件", "参数一", "参数二", ...]
CMD ["参数一", "参数二"...]

CMD ["nginx", "-g", "daemon off;"]
docker run -it ubuntu #直接进入bash,因为默认指令为/bin/bash
docker run -it ubuntu /etc/os-release #默认指令变成/etc/os-release

ENTRYPOINT

指定容器启动程序及参数,当指定了ENTRYPOINT之后,CMD的含义就变成了ENTRYPOINT的参数。从而实现我们在build镜像时可以根据配置修改启动指令的参数。在docker run运行时可以用–entrypoint覆盖

ENTRYPOINT "CMD"
ENTRYPOINT ["可执行文件", "参数一", "参数二"...]

ENV

设置环境变量

ENV KEY VALUE
ENV KEY1=VALUE2 KEY2=VALUE2

ARG

同ENV,设置环境变量并为其提供默认值,不同的是在容器运行时,这些值将不存在。在运行时可以用–build-arg <参数名>:<值>覆盖

ARG <参数名>[=默认值]

VOLUMN

指定匿名卷,防止用户忘记挂载,运行时用-v HOST_DIR/CONTAINER_DIR进行覆盖

VOLUMN PATH

EXPOSE

声明运行时容器提供的服务端口,运行时应用并不会因为这个声明而打开这个端口。docker run -P时会对声明的端口随机映射

EXPOSE 端口一 端口二

WORKDIR

指定容器之后各层的工作目录。因为本层的cd并不会顺带到下一层。

WORKDIR PATH

USER

改变之后层执行RUN,ENTRYPOINT等指令的身份

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN ["redis-server"]

ONBUILD

ONBUILD 其它指令

用于构建基础镜像,被引用是才会真正执行。可以提取出重复的部分,方便维护

删除

docker rmi [options] <image1> [<image2>....] #删除镜像
docker rm [options] <container1> [<container2>...] #删除容器

进入容器

docker attach CONTAINER_NAME

查看数据卷信息

docker inspect CONTAINER_NAME

匿名的数据卷默认位于/var/lib/docker/volumes之下

查看容器

docker logs [-f] container

查看端口映射配置

docker port container container_port

容器链接

--link container_name:alias

Docker入门总结

原文地址: http://zhenhua-lee.github.io/container/docker.html

未分类

Docker是一个虚拟环境容器,可以将应用代码、环境配置、系统环境等一并打包在一起,生成一个镜像,然后就可以发布到任意平台上。

与VM的区别

未分类

VM在物理机的操作系统上建立了一个中间软件层 hypervisor,利用物理机资源,虚拟出多个硬件资源,这些新的虚拟硬件环境、安装的操作系统、相应软件便构成了一台虚拟机

而 Docker 对硬件资源,在不同docker container上做了隔离,使得每个docker container拥有不同的环境,同时可以共享硬件资源

日常使用的基本概念

未分类

核心功能简答介绍:

  • Docker是C/S模式
  • images:docker镜像,是Docker run的原材料
  • container: Docker运行的内容,是独立存在的
  • data volumes: 通过数据挂载的方式,实现数据共享
  • network:用户容器与外部、容器之间的通信,常用的方法有端口映射、link等

使用流程

未分类

基本操作

  • docker version: 查看基本版本信息,包括client、server

关于镜像的基本操作

  • docker search: 默认在 https://hub.docker.com 中查询镜像,当然可以修改registry
  • docker pull: 镜像拉取 docker pull imageName:version
  • docker push: 镜像提交
  • docker images: 查看本地镜像
  • docker rmi: 删除本地镜像
  • docker build:利用 Dockerfile 制作镜像,例如 docker build -t newImageName -f dockerFile [contextPath]
  • docker commit: 基于运行的 container 制作镜像

关于容器的基本操作

  • docker run
    • 镜像的运行
    • d: 在后台运行
    • v: 用户数据挂载
    • p: 端口映射,实现外部与容器之间的通信
    • rm: 容器推出时,直接删除容器
    • i: 交互式的方式
    • t: 在容器中启动一个终端
  • docker ps
    • 查询当前存在的容器
    • a: 列出所有容器
    • q: 仅出 container id
  • dock exec: 在容器中执行命名,例如可以使用 docker exec -it containerId /bin/bash 进入到容器内部

  • docker stop: 停止容器的运行
  • docker restart: 重新启动容器的运行
  • docker rm: 容器删除

DockerFile

# 指定基础镜像
FROM NODE:10.12

## 从本地 copy 文件到镜像中
COPY ./ /data/my-node/

## 切换 container 的工作目录
WORKDIR /data/my/node

## 执行命令
RUN npm install

## 容器的启动命名
ENTRYPOINT ["node", "./index.js"]

使用Docker和GitLab构建一个CI/CD Pipeline

本文主要讲述了如何在GitLab上使用Docker镜像构建一个CI/CD的Pipeline。

现如今持续集成(CI)和持续交付(CD)大家已经不陌生了,它们是为了辅助你的产品/工程项目能够更快、更容易地运行最新版本。在这篇文章中,我将讲述如何使用Docker镜像和GitLab的CI/CD工具构建一个Pipeline,在一个VPS/KVM Linux服务器上进行部署。

前提要求

  • 对Linux、Docker以及CI/CD有基本的了解。
  • GitLab帐号(免费计划即可)。
  • 一台具备SSH访问权限的Linux服务器(非root用户即可)。我使用的是带有LAMP技术栈的Ubuntu 16.04 LTS系统。
  • 装有SSH和LFTP的轻量级Docker镜像。

在开始之前,你需要确保:

  • 你已经登录GitLab
  • 你是某个project/repository的拥有者
  • 你能够在本地机器通过Git访问这个repo进行pull和push操作

我用的是GitKraken,一个Git GUI工具,能够较为方面的进行Git操作。

关于GitLab的CI/CD

GitLab提供了一种通过Docker和Shared Runners处理CI/CD Pipeline的简单方法。每次运行Pipeline时,GitLab都会创建一个独立的虚拟机并构建一个Docker镜像。Pipeline可以使用YAML配置文件进行配置,一个Pipeline可以有多个job,但如果job太多,Pipeline的运行时间就较长。我们肯定不希望这样,因为使用免费计划,每月最多可以有2000分钟的构建时间。

“GitLab.com上的Shared Runners以自动缩放模式运行,由DigitalOcean提供支持。自动缩放意味着减少启动构建的等待时间,并为每个项目建立隔离虚拟机,从而最大限度地提高安全性。”
——来自GitLab文档中的描述

为GitLab的runner创建SSH密钥

注意:即使你的服务器上已有具备SSH访问方式,还是建议你为CI/CD创建一套新的密钥,同时为部署流程创建一个新的非root用户。

我们将在Docker容器中通过SSH连接我们的服务器,这就意味着我们不能输入用户密码(即非交互式登录),因此我们需要在本地计算机中创建无密码的SSH密钥对。通常我会创建一个2048字节的RSA密钥,因为这足够安全。

$ ssh-keygen rsa -b 2048

输入以上命令,跟随创建步骤,如果对创建步骤有疑问,使用man ssh-key。记住不要为密钥对设置密码。创建完成后,我们需要把私钥导入我们的服务器:

$ ssh-copy-id -i /path/to/key user@host

现在你可以尝试通过以下命令连接:

$ ssh -i /path/to/key user@host

连接过程应该不会让你输入密码。这个私钥我们后面会使用到。

选择Dockerfile

我使用Docker Hub来存放我的定制化Dockerfile,这个Dockerfile将基于Alpine构建一个安装有OpenSSH和LFTP的轻量级镜像(大约8Mb)。在GitLab的CI/CD中我们需要使用这个镜像来运行Pipeline的job和脚本,镜像越轻量意味着下载镜像的时间就越少。你可以用你自己的镜像或者用我的Dockerfile。

Pipleline的配置

在正式构建前,你需要在你repo的根目录创建一个”.gitlab-ci.yml”文件。接下来我将解释我使用的配置文件,如果有兴趣,你可以先到GitLab官网阅读配置文件格式以及所有可以使用的配置项。

我的配置文件如下:

image: jimmyadaro/gitlab-ci-cd:latest
Deploy:
stage: deploy
only:
— ‘master’
when: manual
allow_failure: false
before_script:
#Create .ssh directory
— mkdir -p ~/.ssh
#Save the SSH private key
— echo “$SSH_PRIVATE_KEY” > ~/.ssh/id_rsa
— chmod 700 ~/.ssh
— chmod 600 ~/.ssh/id_rsa
— eval $(ssh-agent -s)
— ssh-add ~/.ssh/id_rsa
script:
#Backup everything in /var/www/html/
— ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/”
#Deploy new files to /var/www/html
— lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST
— rm -f ~/.ssh/id_rsa
— ‘echo Deploy done: $(date “+%F %H:%M:%S”)’

让我们逐行看看配置文件的每一步都在做什么。

image: jimmyadaro/gitlab-ci-cd:latest

这行将告诉runner从Docker Hub上拉取并运行最新版本的容器。你可以在这里设置你想要使用的镜像,但别忘了给镜像安装OpenSSH和LFTP。

Deploy:

这行设置了pipeline的job名字,创建一个job必须设置这行内容。

stage: deploy

这行设置了job的stage名字,如果你需要运行多个stage,例如“backup”、“build”、“deploy”等,stage名字将帮助你识别当前Pipeline处于什么状态。由于我不需要其他stage,所以我只用了一个job,并且这个job只有一个stage。对于job和stage的名字可以任意设置,例如你的job可以叫“ASDF”,stage可以叫“GHJK”,不过如果你有多个stage,你肯定需要鉴别不同的stage,因此我建议还是规范化这些名字。

only:
— ‘master’

这行表示Pipeline只有当你repo的master分支收到一个更新(例如git merge)时才会被触发。因此,我建议开发使用其他分支(例如development、wip等),然后使用master分支作为“产品分支”。

when: manual

这行表示你需要进入你的project的CI/CD配置中手动触发整个部署流程。当然,这一步是可以跳过的,只是我更喜欢手动触发Pipeline。如果去掉这行,你所选分支(本例中为master)的任何改动都会触发一次Pipeline。

allow_failure: false

这行表示如果你的Pipeline中有其他stage,当一个job中发生错误时,不允许继续执行剩余任务。这是一个可选配置。

before_script:
#Create .ssh directory
— mkdir -p ~/.ssh
#Save the SSH private key
— echo “$SSH_PRIVATE_KEY” > ~/.ssh/id_rsa
— chmod 700 ~/.ssh
— chmod 600 ~/.ssh/id_rsa
— eval $(ssh-agent -s)
— ssh-add ~/.ssh/id_rsa

before_script单元设置的所有命令都会在执行主单元(main script)之前执行。如你所见,每行shell命令需要用短横线(“-“)指定。上面的命令将把我们刚刚生成的SSH私钥保存到容器默认的SSH路径下,这样我们就可以免密连接我们的服务器。

刚刚生成的私钥将作为Protected变量保存在我的project的CI/CD配置中,在GitLab的web UI上,点击Settings > CI/CD > Variables将看到这个变量。同样,我将服务器地址和部署使用的用户名(非root用户)也使用Protected变量保存。

未分类

script:
#Backup everything in /var/www/html/
— ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/”
#Deploy new files to /var/www/html
— lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST
— rm -f ~/.ssh/id_rsa
— ‘echo Deploy done: $(date “+%F %H:%M:%S”)’

script下的内容就是GitLab的runner执行的主单元。首先,我会连接到我的服务器将所有内容备份到一个ZIP文件中,这个ZIP文件将使用当前时间(格式为yyyy-mm-dd_hh-mm-ss)进行命名:

— ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/”

注意:你需要在你的服务器上安装ZIP CLI。

在将/var/www/html备份后,使用LFTP连接到我的服务器并且上传最新的repo文件。这里我用的是SFTP,FTP配置有点不一样:

— lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST

使用mirror -Rnev ./ /var/www/html让LFTP上传./(我repo的根目录)下的所有文件到我服务器的/var/www/html路径下。上面部分参数的意思如下:

  • -u设置了我们sftp://$HOSTSSH用户名。
  • -e用于设置执行命令(使用单引号进行配置)。
  • -R用于设置reverse mirror。
  • -n表示只上传新的文件。
  • -e用于删除在我们源中不存在的文件。
  • -v用于配置verbose日志。
  • ignore-time将在决定是否下载时忽略时间。
  • exclude-glob .git*将会排除任何目录中匹配.git*的所有文件(例如.gitignore以及.gitkeep)。你可以在这里设置其他文件匹配方式。
  • exclude .git/这个配置将会保证不上传我们repo中的git文件。
  • exit将会停止LFTP和SSH执行。

注意:所有在我们服务上但是不在我们repository中的文件将被删除,记住上面所述的’源’指的就是我们GitLab的repository。

最终,脚本会在shared runner的容器中删除我们的私钥(这是一个安全措施),并且输出带有当前时间的结束语句。

— rm -f ~/.ssh/id_rsa
— ‘echo Deploy done: $(date “+%F %H:%M:%S”)’

以上部分就是我配置文件的所有内容。在GitLab中一个成功的Pipeline执行流程如下图所示:

未分类

运行Docker镜像

未分类

Pipeline的最终状态

结论

我尝试了一些其他的方式,例如使用rsync替代LFTP、使用多阶段以及缓存依赖(我能够重用SSH密钥)的Jobs、使用Docker的ENTRYPOINT和CMD等等,但我发现上面描述的方式对我来说是最快和最容易的。

Docker Compose 原理

Docker 的优势非常明显,尤其是对于开发者来说,它提供了一种全新的软件发布机制。也就是说使用 docker 镜像作为软件产品的载体,使用 docker 容器提供独立的软件运行上下文环境,使用 docker hub 等提供镜像的集中管理,这其中最重要的是使用 Dockerfile 定义容器的内部行为和关键属性来支持软件运行。但是实际的生产环境往往需要定义数量庞大的 docker 容器,并且容器之间具有错综复杂的联系。手动的记录和配置这些复杂的容器关系,不仅效率低下而且容易出错。所以,我们迫切需要一种像 Dockerfile 定义 docker 容器一样能够定义容器集群的编排和部署工具。于是,Docker Compose 出现了(其实应该说 Fig 出现了,docker 收购了 Fig 并改名为 compose)。
Dockerfile 重现一个容器,compose 则用来重现容器的集群。
说明:本文的演示环境为 ubuntu 16.04。

编排和部署

编排(orchestration)

编排指根据被部署的对象之间的耦合关系,以及被部署对象对环境的依赖,制定部署流程中各个动作的执行顺序,部署过程所需要的依赖文件和被部署文件的存储位置和获取方式,以及如何验证部署成功。这些信息都会在编排工具中以指定的格式(比如配置文件或特定的代码)来要求运维人员定义并保存起来,从而保证这个流程能够随时在全新的环境中可靠有序地重现出来。

部署(deployment)

部署是指按照编排所指定的内容和流程,在目标机器上执行环境初始化,存放指定的依赖文件,运行指定的部署动作,最终按照编排中的规则来确认部署成功。

所以说,编排是一个指挥家,他的大脑里存储了整个乐曲此起彼伏的演奏流程,对于每一个小节每一段音乐的演奏方式都了然于胸。而部署就是整个乐队,他们严格按照指挥家的意图用乐器来完成乐谱的执行。最终,两者通过协作就能把每一位演奏者独立的演奏通过组合、重叠、衔接来形成高品位的交响乐。这也是 docker compose 要完成的使命。

Compose 原理

笔者在前文《Docker Compose 简介》 (https://www.cnblogs.com/sparkdev/p/9753793.html)中演示了官方的示例,本文不再赘述,接下来我们去探索 compose 的工作原理。先来了解两个 compose 中常常提及的概念:

project

通过 docker compose 管理的一个项目被抽象称为一个 project,它是由一组关联的应用容器组成的一个完整的业务单元。简单点说就是一个 docker-compose.yml 文件定义一个 project。
我们可以在执行 docker-compose 命令时通过 -p 选项指定 project 的名称,如果不指定,则默认是 docker-compose.yml 文件所在的目录名称。

service

运行一个应用的容器,实际上可以是一个或多个运行相同镜像的容器。可以通过 docker-compose up 命令的 –scale 选项指定某个 service 运行的容器个数,比如:

$ docker-compose up -d --scale redis=2

未分类

了解了上面的基本概念之后,让我们一起看看 compose 的一次调用流程:

未分类

右上角的 docker-compose 定义了一组 service 来组成一个 project,通过 docker-compose.yml 中 service 的定义与 container 建立关系(service 与容器的对应关系),最后使用 container 来完成对 docker-py(Python 版的 docker client) 的调用,向 docker daemon 发起 http 请求。注意,这里的 project, service 和 container 对应的都是 docker-compose 实现中的数据结构。下面让我们结合上图来介绍 docker-compose 工作的大致流程。

首先,用户执行的 docker-compose up 命令调用了命令行中的启动方法,功能非常简单。一个 docker-compose.yml 文件定义了一个 project,docker-compose up 提供的命令行参数则作为这个 project 的启动参数交由 project 模块处理。

然后,如果当前宿主机已经存在与该应用对应的容器,docker-compose 则进行行为逻辑判断。如果用户指定可以重新启动已有服务,docker-compose 就会执行 service 模块的容器重启方法,否则就直接启动已有容器。这两种操作的区别在于前者会停止旧的容器,创建并启动新的容器,并把旧容器移除掉。在这个过程中创建容器的各项自定义参数都是从 docker-compose up 命令和 docker-compose.yml 中传入的。

接下来,启动容器的方法也很简洁,这个方法中完成了一个 docker 容器启动所需的主要参数的封装,并在 container 模块执行启动。

最后,contaier 模块会调用 docker-py 客户端来执行向 docker daemon 发起创建容器的 POST 请求。

由此可见 docker-compose 工作的整体流程非常清晰、简洁!

重新启动 services

前面我们提到当前宿主机已经存在与该应用对应的容器,docker-compose 会进行判断并决定是否重新启动已有服务。下面我们就通过 demo 来演示几个常见的场景(我们依然使用前文中提到的官方 demo)。

强制 recreate

Recreate 就是删除现有的容器并且重新创建新的容器,为 docker-compose up 命令指定 –force-recreate 选项可以强制 recreate 容器:

未分类

创建个别容器

如果应用中的个别 service 对应的容器被删除了,docker-compose up 命令会新建相关的容器:

未分类

启动个别容器

与上面类似,如果应用中的个别 service 对应的容器被停止(stop)了,docker-compose up 命令会重新启动相关的容器:

未分类

总结

Docker-compose 总体上给人的感觉是并不复杂。本文只是从概览的角度梳理了一遍 docker-compose 的整体执行流程,主要目的是理解它的工作原理。至于相关的使用技巧等细节,笔者会在接下来的文章中进行介绍。

Docker的网络类型及驱动器

计算机如果不能够联网,其价值就要大打折扣。类似的,一个Docker容器也需要通过网络访问其他资源,或者被其他资源访问。这就涉及到Docker容器实例的网络,也与Docker宿主机的网络息息相关。

总的来说,Docker的网络是一个通过网络驱动器(driver)实现的Docker子系统。不同的网络驱动器,能够创建具有不同网络特性的Docker容器实例。

目前(Docker v18.03),Docker内置提供如下的网络驱动器:

1. bridge

Docker容器实例默认即使用该网络驱动器,-d=bridge明确使用该网络驱动器。

该网络驱动器适用于独立的容器实例之间通信,但是这些容器实例必须位于同一个宿主机。在bridge网络中,各个独立的容器实例都是连接到一个bridge,并通过bridge通信。

在Docker环境中,默认即已经创建了一个名为bridge的网络。后续启动的容器实例,如果没有指明网络驱动器,则默认加入到该网络。也可以专门创建一个定制的bridge网络,创建的bridge网络会覆盖默认的bridge网络。定制的bridge网络,最大的好处是默认即支持服务的自动发现。

在bridge网络中,每次启动一个容器实例,都会按照顺序获取网络IP。所以重启容器实例后,可能会发现容器的IP变化了。

bridge网络默认不支持IPv6。

2. host

-d=host明确使用该网络驱动器。

该网络驱动器适用于Linux宿主机上的独立的容器实例。容器实例与容器宿主机之间没有网络隔离,容器实例直接使用宿主机的网络。
host网络默认不支持IPv6。

3. overlay

-d=overlay明确使用该网络驱动器。

该网络驱动器适用于Docker宿主机集群中的各个独立的容器实例之间通信。为集群中的Docker容器实例提供跨多个Docker引擎的网络连接。

4. macvlan

-d=macvlan明确使用该网络驱动器。
该网络驱动器适用于与需要有MAC地址的容器实例。
在某些历史遗留应用中,只能通过MAC通信,与之通信的容器实例也必须拥有MAC地址。这时容器实例就如同真实的物理设备一样。

5. none

-d=none禁用容器实例中的网络功能。通常,这表明要使用其他的第三方网络驱动器。

关于其他第三方网络驱动器,请参考https://store.docker.com/search?category=network&q=&type=plugin

参考链接:

https://docs.docker.com/network/

在Docker的工作流中常见问题及最终方案

这次我们创建一个Hello world的web服务器。

mkdir -p identidock/app   #首先创建一个新的multiidentidock来存放我们的项目,在这个目录下面,创建一个app目录来存放Python代码。
touch app/identidock.py  #在app目录下创建identidock.py
# 编辑identidock.py的内容
from flask import Flask
app = Flask(__name__)  #初始化Flask和设置应用对象
@app.route('/')     #创建一个与URL关联的路由,当这个URL被请求,它会调用hello_world函数。
def hello_world():
        return "Hello Docker!n"
if __name__ == '__main__':
        app.run(debug=True,host='0.0.0.0')  #初始化Python web服务器,使用0.0.0.0作为主机参数绑定了所有的网络接口
#现在我们需要一个存放Python代码的容器然后运行它。在identidock目录下面创建一个Dockerfile文件,并且编辑如下内容
FROM python:3.4
RUN pip install Flask==0.10.1
WORKDIR /app
COPY app /app
CMD ["python" "identidock.py"]

现在我们可以构建我们的简单镜像了

docker build -t identidock .
……
docker run -d -p 5000:5000 identidock
  • -d 代表在后台运行容器
  • -p 代表我们想转发主机的5000端口到容器的5000端口
curl localhost:5000    #进行测试
Hello World!

但是有一些问题存在:每次代码的改变,我们都需要重新构建镜像然后重启这个容器。

对此,有一个简单的解决方案,我们可以绑定主机的源代码文件夹到内部容器文件夹中。

docker stop $(docker ps -ql)  #停止最近创建的容器
docker rm $(docker ps -lq)    #删除最近创建的容器
docker run -d -p 5000:5000 -v $(pwd)/app:/app identidock
-v $(pwd)/app:/app 参数绑定app目录到容器的/app。 它会覆盖容器内/app的内容,同时对于容器内部也是可写的(你也可以挂载为只读模式)-v 参数必须是绝对路径。
curl localhost:5000  #测试
Hello world!

现在我们可以在主机上编辑文件看看

sed -i 's/World/Docker/' app/identidock.py  #使用sed快速替换World为Docker,你也可以使用正常的文本编辑器。
curl localhost:5000
Hello Docker!

现在除了容器内容封装的一些依赖关系,我们就拥有了一个相对正常的开发环境了。然而这里还有一个问题,那就是我们不能在生产环境使用这个容器,因为它正在运行的是默认的Flask webserver,它只适用于开发者,在生产环境中则效率低下并且不安全。一个好的解决方法就是采纳Docker减少开发环境和生产环境的区别,现在让我们在看一下怎么处理吧。

uWSGI是一个为生产环境准备的应用服务器,它也可以位于类似于Nginx的web server后面。使用uWSGI代替Flask webserver会提供我们一个灵活的容器,方便我们进行设置。我们可以转换这个容器到使用uwSGI容器只需要修改Dockerfile中的两行。

FROM python:3.4
RUN pip install Flask==0.10.1 uWSGI==2.0.8 
WORKDIR /app
COPY app /app
CMD ["uwsgi", "--http", "0.0.0.0:9090", "--wsgi-file", "/app/identidock.py", 
     "--callable", "app", "--stats", "0.0.0.0:9191"]
docker build -t identidock  #构建这个镜像,然后运行它我们可以看到其中的不同
……
docker run -d -p 9090:9090 -p 9191:9191 identidock
curl localhost:9090
Hello Docker!

你可以使用docker logs来看一下日志uWSGI的日志信息。当然我们也可以在http://localhost:9191中看到一些uWSGI暴露的状态信息。

但是实际上,上面会提示一个安全问题,我们使用root来运行服务了。我们可以在Dockerfile中很容易的修复这个问题,同时我们在声明一下容器监听的端口。

FROM python:3.4
RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
RUN pip install Flask==0.10.1 uWSGI==2.0.8
WORKDIR /app
COPY app /app
COPY cmd.sh /
EXPOSE 9090 9191
USER uwsgi
CMD ["uwsgi","--http","0.0.0.0:9090","--wsgi-file","/app/identidock.py", 
"--callable","app","--stats","0.0.0.0:9191"]
docker build -t identidock . #重建这个镜像
...
docker run identidock whoami 
uwsgi

#你最好在你的所有的Dockerfile中设置用户,或者在ENTRYPOINT或CMD脚本中改变用户。

现在在容器呢把的命令不是以root来运行了,让我们运行一下这个容器试试。。

docker run -d -P --name port-test identidock
#-P 让Docker自动的映射一个大数字的本地主机端口到容器内部。

我们可以看下那个端口被映射了

docker port port-test 
9090/tcp -> 0.0.0.0:32769 
9191/tcp -> 0.0.0.0:32768
curl localhost:32769 #现在做个测试
Hello Docker!

现在还存在一个问题,那就是我们缺失了开发工具,例如调试输出和实时的代码重载。理想情况下,我们想要使用这个镜像既可以作为开发环境又可以作为生产环境。我们可以使用环境变量和一个简单的脚本来实现这个需求。

在和Dockerfile相同的目录下创建cmd.sh,然后编辑如下内容

#!/bin/bash set -e
if [ "$ENV" = 'DEV' ]; then
  echo "Running Development Server"
  exec python "identidock.py" 
else
  echo "Running Production Server"
  exec uwsgi --http 0.0.0.0:9090 --wsgi-file /app/identidock.py 
             --callable app --stats 0.0.0.0:9191 
fi
chmod +x cmd.sh
#编辑Dockerfile
FROM python:3.4
RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
RUN pip install Flask==0.10.1 uWSGI==2.0.8
WORKDIR /app
COPY app /app
COPY cmd.sh /
EXPOSE 9090 9191
USER uwsgi
CMD ["/cmd.sh"]
docker stop $(docker ps -q)
docker rmr $(docker ps -aq)
docker build -t identidock .  #重建镜像
docker run -e "ENV=DEV" -p 5000:5000 identidock
Running in development environment.
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger pin code: 290-812-275
docker run -p 5000:5000 -v $(pwd)/app:/app -e "ENV=DEV" identidock #这时我们可以实时修改代码了

然后再不加ENV=DEV的情况下运行就是成产环境了。

小结:
上面依次展示了一些Docker生产中可能会存在的问题。并且提供了最后的解决方案。

中文版cacti多端口叠加流量

cacti是我们经常使用的开源监控软件,而且非常好用,使用插件super links还可以自己自定义网页,今天主要介绍端口流量的叠加和监控。
使用的版本是0.8.7b加了中文补丁和字体,上张图

未分类

首先要确认要叠加的几个端口,比如设备的interface g8/0/8 和 g8/0/16要叠加这两个端口,然后在图像管理这个项,点添加

未分类

未分类

选择图像模板 “无” 设备“无”然后点创建

未分类

标题要写,比如“清水衙门10G联通出口” 点创建

未分类

右侧图像项目 点添加 (也就是添加数据源)

未分类

设备 选择8口和16口那台设备,数据模板当然是 interface-Traffic(接口流量模板)
图像项目 数据源选择 设备的8口in方向,颜色 选择00CF00 (估计是绿)图像项目类型选择 区域显示(AREA)CDEF函数项 选择 turn bytes into bits (字节转比特,电脑显示的是字节,而接口流量是比特)好了点创建

未分类

未分类

看,有图了,同理再添加16口的in方向,8口的out方向,16口的out方向,注意选out方向,颜色和图像类型要改,颜色我使用了(蓝色)002A97,类型我选了 线条显示(STACK)

未分类

未分类

最终完成图

未分类

保存,然后添加流量图下面的图例,就是当前值、平均值、最大值,同理,只要注意,颜色 “无”,图像项目类型 “图例”(legend),CDEF为字节转比特,就可以了。

awk将相同键值的字符串拼接一起输出

awk将相同键值的字符串拼接一起输出。每一行中第一列的数据是键,其余列为值。

文本1.txt中的内容是

abc 1 2 3 
abc a1 b1 c1
abc a2 b2 c2
abd a2 b2 c2
hello  hello_value1 hello_value2
hello  hello_value3 hello_value456
awk '{key=$1;$1="";value=$0;sum[key]=sum[key]""value} END{for(i in sum) print i,"=",sum[i]}' 1.txt 

注意,其中字符串拼接的操作是双引号””。

输出结果为

[root@localhost ~]# awk '{key=$1;$1="";value=$0;sum[key]=sum[key]""value} END{for(i in sum) print i,"=",sum[i]}' 2.txt 
hello =  hello_value1 hello_value2 hello_value3 hello_value456
abc =  1 2 3 a1 b1 c1 a2 b2 c2
abd =  a2 b2 c2
[root@localhost ~]#

实践中学习 awk

内置变量

awk 中预先定义好的,内置在 awk 内部的变量。

未分类

记录

1、awk 把每一个换行符结束的行称为一个记录,$0 变量:它指的是整条记录。

# 输出 test.txt 文件中的所有记录 
awk '{print $0}' test.txt 

2、变量 NR:一个计数器,每处理完一条记录,NR 的值就增加 1。

# 输出 test.txt 中的所有记录,并在记录前显示行号
awk '{print NR, $0}' test.txt

记录中的每个单词都称作「域」,默认情况下以空格分隔。awk 可跟踪域的个数,并在内建变量 NF 中保存该值。

# 打印第一和第三个以空格分开的列(域)
awk '{print $1, $3}' test.txt

awk 命令格式

有了上边域和记录的概念,来看看 awk 的命令格式:

awk [options] 'pattern{action}' file

eg1:

# $3 == 0:是 pattern
# print $0:是 action 
awk '$3==0 {print $0}' employee  # 如果第三个域等于 0,则将这行打印

eg2:
下面这个 awk 脚本没有指定 action,但结果和 eg1 一样,没有指定 action 时默认是 {print $0}(打印整行)。

awk '$3 == 0' employee

eg3:

#将结果重定向到文件
awk '$3 == 0' employee > other.txt

域分隔符

1、输入分隔符(field separator),就是 test.txt 中每个列是以什么进行分隔的,awk 默认以空格对每一行进行分隔,分隔符得值保存在内建变量 FS中,可以通过-F命令行选项修改FS的值。

# 指定 : 作为分隔符
awk -F: '{print $1, $3}' test.txt
# 显示指定空格作为分隔符 
awk -F'[ ]' '{print $1, $3}' test.txt
# 指定空格、冒号、tab 作为分隔符
awk -F'[:t ]' '{print $1, $3}' test.txt

# 指定以逗号(,)作为分隔符
awk -F, '{print $1, $2}' separator.txt
# 使用 -v 选项对内建变量设置分隔符,和 awk -F, 效果一样  
awk -v FS=',' '{print $1, $2}' separator.txt

2、输出分隔符(out field separator), 大白话表示就是 awk 在处理完文本后以什么字符作为分隔符将每行输出,默认也是空格,保存在内建变量 OFS 中。

eg1:

# 对内建变量 OFS 赋值
awk -v OFS="->" '{print $1, $2, $3}' other.txt

输出如:

Beth->4.00->0
Dan->3.75->0

eg2:

# 同时指定输入和输出分隔符 
awk -v FS=',' -v OFS='->' '{print $1, $2}' separator.txt

模式-Pattern

根据前面的一些例子,awk 的语法如下:

awk [options] 'Pattern {Action}' file1 file2 
options(选项): 如前面使用过的 -v -F
Action(动作):如 print 

Pattern:也就是条件,一个关系表达式,awk 会逐行处理文本,处理完当前行,然后再处理下一行。如果不指定任何的「条件」,awk 会一行一行的处理完文件的每一行,如果指定了「条件」,只处理满足条件的行。这即 awk 中的模式。

# 将有四列的行打印出来 
awk 'NF == 4 {print $0}' column.txt
# 没有指定模式则是空模式,空模式会匹配文本中每一行,每一行都满足条件 
awk '{print $0}' test.txt

1、正则模式

# 将包含 in 的记录行进行打印 
awk '/in/ {print $0}' pattern.txt

2、行范围模式

eg1:

# 从第一行 到 正则匹配到的第一行 之间的所有行进行打印
awk 'NR == 1, /in/{print $0}' pattern.txt
# /xx/ 没有匹配到第二个模式,打印第一个模式出现的行到文本末尾
awk 'NR == 1, /xx/{print NR, $0}' pattern.txt

看一下结果:

未分类

eg2:

# 将第一行 到 正则匹配到的第一行中的 in 替换为 on(从记录行的左边开始,只替换一次)
# 怎么理解这个 1 呢?
# 这里有两个模式,awk 读出每行记录都会经过这两个模式的判断
# 1 表示这个模式为真,没有指定模式默认的 action 就是打印整行 
awk 'NR == 1,/in/{sub(/in/, "on")} 1' pattern.txt

未分类

再来一波例子估计就懂 eg2 中的用法了:

未分类

在 1 处,模式指定为 1 表示为真,走默认的 action 打印整行。
在 2 处,指定了两个模式,每行记录都会经过这两个模式的处理,然后分别执行模式自己的动作。
在 3 处,有 3 个模式:BEGIN 模式和 END 模式,中间的是空模式(没有指定模式)。BEGIN 模式:处理文本之前先执行的操作;END 模式:处理完所有行后需要执行的操作。

范围模式的第一个模式和第二个模式都以第一次匹配到的行为准。

awk 内建函数

字符串函数

1、sub

sub (regular expression, substitution string):
sub (regular expression, substitution string, target string)

对每一个记录行从左到右第一个匹配到的域进行替换,每一行只会匹配替换一次。

# 将每行第一次出现的域 hello 替换为 hi,每行只会匹配替换一次 
awk '{sub(/hello/, "hi"); print}' test.txt
# 对每行的第一个域进行替换 
awk '{sub(/hello/, "hi", $1); print}' test.txt