diff --git a/SUMMARY.md b/SUMMARY.md index ff2833b..ad96098 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -11,7 +11,16 @@ * [私有镜像仓库](docs/5.私有镜像仓库.md) * [数据共享与持久化](docs/6.数据共享与持久化.md) * [Docker 的网络模式](docs/7.Docker的网络模式.md) + +### Docker 三架马车 * [Docker Compose](docs/8.Docker Compose.md) +* [Docker Machine](docs/9.Docker Machine.md) +* [Docker Swarm](docs/10.Docker Swarm.md) + +### Docker 实践 +* [图形化管理和监控](docs/11.图形化管理和监控.md) +* [Docker 的多阶段构建](docs/12.Docker的多阶段构建.md) +* [Dockerfile 最佳实践](docs/13.Dockerfile最佳实践.md) ### kubeadm 搭建集群 * [使用 kubeadm 搭建集群环境](docs/16.用 kubeadm 搭建集群环境.md) diff --git a/docs/10.Docker Swarm.md b/docs/10.Docker Swarm.md new file mode 100644 index 0000000..9f3130d --- /dev/null +++ b/docs/10.Docker Swarm.md @@ -0,0 +1,266 @@ +# 10. Docker Swarm + +## 基本概念 +`Swarm`是使用[SwarmKit](https://github.com/docker/swarmkit/)构建的 Docker 引擎内置(原生)的集群管理和编排工具。`Docker Swarm`是 Docker 官方三剑客项目之一,提供 Docker 容器集群服务,是 Docker 官方对容器云生态进行支持的核心方案。 + +使用它,用户可以将多个 Docker 主机封装为单个大型的虚拟 Docker 主机,快速打造一套容器云平台。Swarm mode 内置 kv 存储功能,提供了众多的新特性,比如:具有容错能力的去中心化设计、内置服务发现、负载均衡、路由网格、动态伸缩、滚动更新、安全传输等。使得 Docker 原生的 Swarm 集群具备与`Mesos`、`Kubernetes`竞争的实力。使用 Swarm 集群之前需要了解以下几个概念。 + +### 节点 +运行 Docker 的主机可以主动初始化一个 Swarm 集群或者加入一个已存在的 Swarm 集群,这样这个运行 Docker 的主机就成为一个 Swarm 集群的节点 (node) 。节点分为`管理 (manager) 节点和工作 (worker) 节点`。 + +管理节点用于`Swarm`集群的管理,`docker swarm`命令基本只能在管理节点执行(节点退出集群命令`docker swarm leave`可以在工作节点执行)。一个 Swarm 集群可以有多个管理节点,但只有一个管理节点可以成为`leader`,leader 通过`raft`协议实现。 + +工作节点是任务执行节点,管理节点将服务 (`service`) 下发至工作节点执行。管理节点默认也作为工作节点。你也可以通过配置让服务只运行在管理节点。来自`Docker`官网的这张图片形象的展示了集群中管理节点与工作节点的关系。 +![docker swarm structrue](./images/docker-swarm-structrue.png) + +​​ +### 服务和任务 +`任务(Task)`是 Swarm 中的最小的调度单位,目前来说就是一个单一的容器;`服务(Services)`是指一组任务的集合,服务定义了任务的属性。服务有两种模式: + +* `replicated services`按照一定规则在各个工作节点上运行指定个数的任务。 +* `global services`每个工作节点上运行一个任务 + +两种模式通过`docker service create`的`--mode`参数指定。来自 Docker 官网的这张图片形象的展示了容器、任务、服务的关系。 +![docker swarm task service](./images/docker-swarm-task-service.png) +​​ + +## 初始化集群 +我们这里利用上一节的`docker machine`来充当集群的主机,首先先创建一个`manager`节点,然后在该节点上执行初始化集群命令: +```shell +☁ ~ docker-machine create -d virtualbox manager +Running pre-create checks... +Creating machine... +(manager) Copying /Users/ych/.docker/machine/cache/boot2docker.iso to /Users/ych/.docker/machine/machines/manager/boot2docker.iso... +(manager) Creating VirtualBox VM... +(manager) Creating SSH key... +(manager) Starting the VM... +(manager) Check network to re-create if needed... +(manager) Waiting for an IP... +Waiting for machine to be running, this may take a few minutes... +Detecting operating system of created instance... +Waiting for SSH to be available... +Detecting the provisioner... +Provisioning with boot2docker... +Copying certs to the local machine directory... +Copying certs to the remote machine... +Setting Docker configuration on the remote daemon... +Checking connection to Docker... +Docker is up and running! +To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env manager +☁ ~ docker-machine env manager +export DOCKER_TLS_VERIFY="1" +export DOCKER_HOST="tcp://192.168.99.101:2376" +export DOCKER_CERT_PATH="/Users/ych/.docker/machine/machines/manager" +export DOCKER_MACHINE_NAME="manager" +# Run this command to configure your shell: +# eval $(docker-machine env manager) +☁ ~ eval $(docker-machine env manager) +☁ ~ docker-machine ssh manager + ## . + ## ## ## == + ## ## ## ## ## === + /"""""""""""""""""\___/ === + ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ / ===- ~~~ + \______ o __/ + \ \ __/ + \____\_______/ + _ _ ____ _ _ +| |__ ___ ___ | |_|___ \ __| | ___ ___| | _____ _ __ +| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__| +| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__| < __/ | +|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_| +Boot2Docker version 18.03.1-ce, build HEAD : cb77972 - Thu Apr 26 16:40:36 UTC 2018 +Docker version 18.03.1-ce, build 9ee9f40 +docker@manager:~$ docker swarm init --advertise-addr 192.168.99.101 +Swarm initialized: current node (3gsjpckj5ag1vvdg44fgzylow) is now a manager. + +To add a worker to this swarm, run the following command: + + docker swarm join --token SWMTKN-1-1aqikkhsz91l4n7k9ig3xinjz0iv0fh4gcrlhp9mk3643rblca-aqgqldlrw33k8heiao7yx27w5 192.168.99.101:2377 + +To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. +``` + +执行`docker swarm init`命令的节点自动成为管理节点。 + + +## 增加工作节点 +管理节点初始化完成后,然后同样的用`docker-machine`创建工作节点,然后将其加入到管理节点之中去即可: +```shell +☁ ~ docker-machine create -d virtualbox worker1 +Running pre-create checks... +Creating machine... +(worker1) Copying /Users/ych/.docker/machine/cache/boot2docker.iso to /Users/ych/.docker/machine/machines/worker1/boot2docker.iso... +(worker1) Creating VirtualBox VM... +(worker1) Creating SSH key... +(worker1) Starting the VM... +(worker1) Check network to re-create if needed... +(worker1) Waiting for an IP... +Waiting for machine to be running, this may take a few minutes... +Detecting operating system of created instance... +Waiting for SSH to be available... +Detecting the provisioner... +Provisioning with boot2docker... +Copying certs to the local machine directory... +Copying certs to the remote machine... +Setting Docker configuration on the remote daemon... +Checking connection to Docker... +Docker is up and running! +To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env worker1 + +☁ ~ docker-machine ssh worker1 + ## . + ## ## ## == + ## ## ## ## ## === + /"""""""""""""""""\___/ === + ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ / ===- ~~~ + \______ o __/ + \ \ __/ + \____\_______/ + _ _ ____ _ _ +| |__ ___ ___ | |_|___ \ __| | ___ ___| | _____ _ __ +| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__| +| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__| < __/ | +|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_| +Boot2Docker version 18.03.1-ce, build HEAD : cb77972 - Thu Apr 26 16:40:36 UTC 2018 +Docker version 18.03.1-ce, build 9ee9f40 +docker@worker1:~$ docker swarm join --token SWMTKN-1-1aqikkhsz91l4n7k9ig3xinjz0iv0fh4gcrlhp9mk364 +3rblca-aqgqldlrw33k8heiao7yx27w5 192.168.99.101:2377 +This node joined a swarm as a worker. +``` + +我们可以看到上面的提示信息:**This node joined a swarm as a worker.**,表明节点已经加入到`swarm`集群之中了。 + + +## 查看集群 +经过上边的两步,我们已经拥有了一个最小的 Swarm 集群,包含一个管理节点和两个工作节点。 + +管理节点使用`docker node ls`查看集群: +```shell +☁ ~ docker node ls +ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION +3gsjpckj5ag1vvdg44fgzylow * manager Ready Active Leader 18.03.1-ce +cxmj5lr0vbwo1em9y9oang5m8 worker1 Ready Active 18.03.1-ce +ksruum3uc1c265ywm4kn9a88g worker2 Ready Active 18.03.1-ce +☁ ~ docker service ls +ID NAME MODE REPLICAS IMAGE PORTS +☁ ~ docker service create --replicas 3 -p 80:80 --name nginx nginx:1.13.7-alpine +4k9cbna8ive87p4or9mny9kfs +overall progress: 3 out of 3 tasks +1/3: running [==================================================>] +2/3: running [==================================================>] +3/3: running [==================================================>] +verify: Service converged + +☁ ~ docker-machine ls +NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS +manager * virtualbox Running tcp://192.168.99.101:2376 v18.03.1-ce +worker1 - virtualbox Running tcp://192.168.99.102:2376 v18.03.1-ce +worker2 - virtualbox Running tcp://192.168.99.103:2376 v18.03.1-ce +☁ ~ docker service ls +ID NAME MODE REPLICAS IMAGE PORTS +4k9cbna8ive8 nginx replicated 3/3 nginx:1.13.7-alpine *:80->80/tcp +☁ ~ docker service ps nginx +ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS +r7hmzkqsri8p nginx.1 nginx:1.13.7-alpine worker1 Running Running about a minute ago +y0xgrfwmjfrj nginx.2 nginx:1.13.7-alpine worker2 Running Running about a minute ago +j8k7be8xkbg3 nginx.3 nginx:1.13.7-alpine manager Running Running about a minute ago +``` + +使用`docker service logs`来查看某个服务的日志。 +```shell +☁ ~ docker service logs nginx +``` + +使用`docker service rm`来从 Swarm 集群移除某个服务: +```shell +☁ ~ docker service rm nginx +nginx +``` + +正如之前使用`docker-compose.yml`来一次配置、启动多个容器,在`Swarm`集群中也可以使用`compose`文件(docker-compose.yml)来配置、启动多个服务。 + +上一节中,我们使用`docker service create`一次只能部署一个服务,使用`docker-compose.yml`我们可以一次启动多个关联的服务。 + +我们以在`Swarm`集群中部署`WordPress`为例进行说明:(docker-compose.yml) +```yaml +version: "3" + +services: + wordpress: + image: wordpress + ports: + - 80:80 + networks: + - overlay + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + deploy: + mode: replicated + replicas: 3 + + db: + image: mysql + networks: + - overlay + volumes: + - db-data:/var/lib/mysql + environment: + MYSQL_ROOT_PASSWORD: somewordpress + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: wordpress + deploy: + placement: + constraints: [node.role == manager] + + visualizer: + image: dockersamples/visualizer:stable + ports: + - "8080:8080" + stop_grace_period: 1m30s + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + deploy: + placement: + constraints: [node.role == manager] + +volumes: + db-data: +networks: + overlay: +``` + +其中**constraints: [node.role == manager]**是调度策略,文档地址:https://docs.docker.com/swarm/scheduler/filter/ + +在 Swarm 集群管理节点新建该文件,其中的 visualizer 服务提供一个可视化页面,我们可以从浏览器中很直观的查看集群中各个服务的运行节点。 + +在 Swarm 集群中使用 docker-compose.yml 我们用`docker stack`命令,下面我们对该命令进行详细讲解。 + +### 部署服务 +部署服务使用`docker stack deploy`,其中`-c`参数指定 compose 文件名。 +```shell +$ docker stack deploy -c docker-compose.yml wordpress +``` + +### 查看服务 +```shell +$ docker stack ls +NAME SERVICES +wordpress 3 +``` + +### 移除服务 +要移除服务,使用`docker stack down`: +```shell +$ docker stack down wordpress +Removing service wordpress_db +Removing service wordpress_visualizer +Removing service wordpress_wordpress +Removing network wordpress_overlay +Removing network wordpress_default +``` + +该命令不会移除服务所使用的`数据卷`,如果你想移除数据卷请使用`docker volume rm`。 diff --git "a/docs/11.\345\233\276\345\275\242\345\214\226\347\256\241\347\220\206\345\222\214\347\233\221\346\216\247.md" "b/docs/11.\345\233\276\345\275\242\345\214\226\347\256\241\347\220\206\345\222\214\347\233\221\346\216\247.md" new file mode 100644 index 0000000..c8fa287 --- /dev/null +++ "b/docs/11.\345\233\276\345\275\242\345\214\226\347\256\241\347\220\206\345\222\214\347\233\221\346\216\247.md" @@ -0,0 +1,92 @@ +# 11. 图形化管理和监控 +下面我们介绍几个可以用图形化的方式来管理`Docker`的工具。 + +> Shipyard:https://github.com/shipyard/shipyard(已停止维护) + +## Portainer +[Portainer](https://portainer.io/)(基于 Go)是一个轻量级的管理界面,可让您轻松管理`Docker`主机或`Swarm`集群。 + +`Portainer`的使用意图是简单部署。它包含可以在任何 Docker 引擎上运行的单个容器(Docker for Linux 和 Docker for Windows)。 + +`Portainer`允许您管理 Docker 容器、image、volume、network 等。 它与独立的 Docker 引擎和 Docker Swarm 兼容。 + +Docker 命令安装: +```shell +$ docker volume create portainer_data +$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer +``` + +Swarm集群部署: +```shell +$ docker volume create portainer_data +$ docker service create \ +--name portainer \ +--publish 9000:9000 \ +--replicas=1 \ +--constraint 'node.role == manager' \ +--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \ +--mount type=volume,src=portainer_data,dst=/data \ +portainer/portainer \ +-H unix:///var/run/docker.sock +``` + +Docker Compose 部署: +```yaml +version: '2' +services: + portainer: + image: portainer/portainer + command: -H unix:///var/run/docker.sock + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer_data:/data +volumes: + portainer_data: +``` +![portainer](./images/docker-ui-portainer.png) + +## Rancher +`Rancher`是一个开源的企业级容器管理平台。通过`Rancher`,企业不必自己使用一系列的开源软件去从头搭建容器服务平台。`Rancher`提供了在生产环境中使用管理`Docker`和`Kubernetes`的全栈化容器部署与管理平台。 +​​![rancher](./images/docker-ui-rancher.png) + +在后面学习`kubernetes`的课程的时候会给大家演示,用于我们快速搭建一个可运行`kubernetes`集群环境,非常方便。 + +## cAdvisor +`cAdvisor`是`Google`开发的容器监控工具,我们来看看 cAdvisor 有什么能耐。 + +* 监控 Docker Host +cAdvisor 会显示当前 host 的资源使用情况,包括 CPU、内存、网络、文件系统等。 + +* 监控容器 +点击 Docker Containers 链接,显示容器列表。点击某个容器,比如 sysdig,进入该容器的监控页面。 + +以上就是 cAdvisor 的主要功能,总结起来主要两点: +* 展示 Host 和容器两个层次的监控数据。 +* 展示历史变化数据。 + +由于`cAdvisor`提供的操作界面略显简陋,而且需要在不同页面之间跳转,并且只能监控一个 host,这不免会让人质疑它的实用性。但 cAdvisor 的一个亮点是它可以将监控到的数据导出给第三方工具,由这些工具进一步加工处理。 + +我们可以把 cAdvisor 定位为一个监控数据收集器,收集和导出数据是它的强项,而非展示数据。 +cAdvisor 支持很多第三方工具,其中就包括后面我们重点要学习的`Prometheus`。 +```shell: +$ docker run \ + --volume=/:/rootfs:ro \ + --volume=/var/run:/var/run:rw \ + --volume=/sys:/sys:ro \ + --volume=/var/lib/docker/:/var/lib/docker:ro \ + --volume=/dev/disk/:/dev/disk:ro \ + --publish=8080:8080 \ + --detach=true \ + --name=cadvisor \ + google/cadvisor:latest +``` + +通过访问地址:http://127.0.0.1:8080/containers/ 可以查看所有容器信息: +![docker cAdvisor](./images/docker-cadvisor.png) +​​ +除此之外,cAdvisor 还提供了一个 Rest API:https://github.com/google/cadvisor/blob/master/docs/api.md + +cAdvisor 通过该 REST API 暴露监控数据,格式如下: +``` +http://:/api// +``` diff --git "a/docs/12.Docker\347\232\204\345\244\232\351\230\266\346\256\265\346\236\204\345\273\272.md" "b/docs/12.Docker\347\232\204\345\244\232\351\230\266\346\256\265\346\236\204\345\273\272.md" new file mode 100644 index 0000000..73db19a --- /dev/null +++ "b/docs/12.Docker\347\232\204\345\244\232\351\230\266\346\256\265\346\236\204\345\273\272.md" @@ -0,0 +1,103 @@ +# 12. Docker 的多阶段构建 + +`Docker`的口号是 **Build,Ship,and Run Any App,Anywhere**,在我们使用 Docker 的大部分时候,的确能感觉到其优越性,但是往往在我们 Build 一个应用的时候,是将我们的源代码也构建进去的,这对于类似于 golang 这样的编译型语言肯定是不行的,因为实际运行的时候我只需要把最终构建的二进制包给你就行,把源码也一起打包在镜像中,需要承担很多风险,即使是脚本语言,在构建的时候也可能需要使用到一些上线的工具,这样无疑也增大了我们的镜像体积。 + + +## 示例 +比如我们现在有一个最简单的 golang 服务,需要构建一个最小的`Docker` 镜像,源码如下: +```go +package main +import ( + "github.com/gin-gonic/gin" + "net/http" +) +func main() { + router := gin.Default() + router.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "PONG") + }) + router.Run(":8080") +} +``` + +## 解决方案 +我们最终的目的都是将最终的可执行文件放到一个最小的镜像(比如`alpine`)中去执行,怎样得到最终的编译好的文件呢?基于 `Docker` 的指导思想,我们需要在一个标准的容器中编译,比如在一个 Ubuntu 镜像中先安装编译的环境,然后编译,最后也在该容器中执行即可。 + +但是如果我们想把编译后的文件放置到 `alpine` 镜像中执行呢?我们就得通过上面的 Ubuntu 镜像将编译完成的文件通过 `volume` 挂载到我们的主机上,然后我们再将这个文件挂载到 `alpine` 镜像中去。 + +这种解决方案理论上肯定是可行的,但是这样的话在构建镜像的时候我们就得定义两步了,第一步是先用一个通用的镜像编译镜像,第二步是将编译后的文件复制到 `alpine` 镜像中执行,而且通用镜像编译后的文件在 `alpine` 镜像中不一定能执行。 + +定义编译阶段的 `Dockerfile`:(保存为**Dockerfile.build**) +```docker +FROM golang +WORKDIR /go/src/app +ADD . /go/src/app +RUN go get -u -v github.com/kardianos/govendor +RUN govendor sync +RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server +``` + +定义`alpine`镜像:(保存为**Dockerfile.old**) +```docker +FROM alpine:latest +RUN apk add -U tzdata +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +WORKDIR /root/ +COPY app-server . +CMD ["./app-server"] +``` + +根据我们的执行步骤,我们还可以简单定义成一个脚本:(保存为**build.sh**) +```shell +#!/bin/sh +echo Building cnych/docker-multi-stage-demo:build + +docker build -t cnych/docker-multi-stage-demo:build . -f Dockerfile.build + +docker create --name extract cnych/docker-multi-stage-demo:build +docker cp extract:/go/src/app/app-server ./app-server +docker rm -f extract + +echo Building cnych/docker-multi-stage-demo:old + +docker build --no-cache -t cnych/docker-multi-stage-demo:old . -f Dockerfile.old +rm ./app-server +``` +当我们执行完上面的构建脚本后,就实现了我们的目标。 + +## 多阶段构建 +有没有一种更加简单的方式来实现上面的镜像构建过程呢?**Docker 17.05**版本以后,官方就提供了一个新的特性:`Multi-stage builds`(多阶段构建)。 +使用多阶段构建,你可以在一个 `Dockerfile` 中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的基础镜像,并表示开始一个新的构建阶段。你可以很方便的将一个阶段的文件复制到另外一个阶段,在最终的镜像中保留下你需要的内容即可。 + +我们可以调整前面一节的 `Dockerfile` 来使用多阶段构建:(保存为**Dockerfile**) +```docker +FROM golang AS build-env +ADD . /go/src/app +WORKDIR /go/src/app +RUN go get -u -v github.com/kardianos/govendor +RUN govendor sync +RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server + +FROM alpine +RUN apk add -U tzdata +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +COPY --from=build-env /go/src/app/app-server /usr/local/bin/app-server +EXPOSE 8080 +CMD [ "app-server" ] +``` +现在我们只需要一个`Dockerfile`文件即可,也不需要拆分构建脚本了,只需要执行 build 命令即可: +```shell +$ docker build -t cnych/docker-multi-stage-demo:latest . +``` + +默认情况下,构建阶段是没有命令的,我们可以通过它们的索引来引用它们,第一个 FROM 指令从`0`开始,我们也可以用`AS`指令为阶段命令,比如我们这里的将第一阶段命名为`build-env`,然后在其他阶段需要引用的时候使用`--from=build-env`参数即可。 + +最后我们简单的运行下该容器测试: +```shell +$ docker run --rm -p 8080:8080 cnych/docker-multi-stage-demo:latest +``` +运行成功后,我们可以在浏览器中打开`http://127.0.0.1:8080/ping`地址,可以看到**PONG**返回。 + +现在我们就把两个镜像的文件最终合并到一个镜像里面了。 + +文章中涉及到代码可以前往 github 查看:[https://github.com/cnych/docker-multi-stage-demo](https://github.com/cnych/docker-multi-stage-demo) diff --git "a/docs/13.Dockerfile\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/docs/13.Dockerfile\346\234\200\344\275\263\345\256\236\350\267\265.md" new file mode 100644 index 0000000..f953e4e --- /dev/null +++ "b/docs/13.Dockerfile\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -0,0 +1,367 @@ +# 13. Dockerfile 最佳实践 +`Docker`官方关于`Dockerfile`最佳实践原文链接地址:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/ + +Docker 可以通过从 Dockerfile 包含所有命令的文本文件中读取指令自动构建镜像,以便构建给定镜像。 + +Dockerfiles 使用特定的格式并使用一组特定的指令。您可以在[Dockerfile Reference](https://docs.docker.com/engine/reference/builder/)页面上了解基础知识 。如果你是新手写作`Dockerfile`,你应该从那里开始。 + +本文档介绍了由 Docker,Inc. 和 Docker 社区推荐的用于构建高效镜像的最佳实践和方法。要查看许多实践和建议,请查看[Dockerfile for buildpack-deps](https://github.com/docker-library/buildpack-deps/blob/master/jessie/Dockerfile)。 + +## 一般准则和建议 + +### 容器应该是短暂的 +通过 Dockerfile 构建的镜像所启动的容器应该尽可能短暂(生命周期短)。「短暂」意味着可以停止和销毁容器,并且创建一个新容器并部署好所需的设置和配置工作量应该是极小的。我们可以查看下[12 Factor(12要素)应用程序方法](https://12factor.net/zh_cn/processes)的进程部分,可以让我们理解这种无状态方式运行容器的动机。 + +### 建立上下文 +当你发出一个`docker build`命令时,当前的工作目录被称为构建上下文。默认情况下,Dockerfile 就位于该路径下,当然您也可以使用-f参数来指定不同的位置。无论 Dockerfile 在什么地方,当前目录中的所有文件内容都将作为构建上下文发送到 Docker 守护进程中去。 + +下面是一个构建上下文的示例,为构建上下文创建一个目录并 cd 放入其中。将“hello”写入一个文本文件hello,然后并创建一个`Dockerfile`并运行`cat`。从构建上下文(.)中构建图像: +```shell +mkdir myproject && cd myproject +echo "hello" > hello +echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile +docker build -t helloapp:v1 . +``` + +现在移动 Dockerfile 和 hello 到不同的目录,并建立了图像的第二个版本(不依赖于缓存中的最后一个版本)。使用`-f`指向 Dockerfile 并指定构建上下文的目录: +```shell +mkdir -p dockerfiles context +mv Dockerfile dockerfiles && mv hello context +docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context +``` + +在构建的时候包含不需要的文件会导致更大的构建上下文和更大的镜像大小。这会增加构建时间,拉取和推送镜像的时间以及容器的运行时间大小。要查看您的构建环境有多大,请在构建您的系统时查找这样的消息 +```shell +Dockerfile: +Sending build context to Docker daemon 187.8MB +``` + +### 使用`.dockerignore`文件 +使用 Dockerfile 构建镜像时最好是将 Dockerfile 放置在一个新建的空目录下。然后将构建镜像所需要的文件添加到该目录中。为了提高构建镜像的效率,你可以在目录下新建一个`.dockerignore`文件来指定要忽略的文件和目录。.dockerignore 文件的排除模式语法和 Git 的 .gitignore 文件相似。 + +### 使用多阶段构建 +在 Docker 17.05 以上版本中,你可以使用 多阶段构建 来减少所构建镜像的大小。上一节课我们已经重点讲解过了。 + +### 避免安装不必要的包 +为了降低复杂性、减少依赖、减小文件大小和构建时间,应该避免安装额外的或者不必要的软件包。例如,不要在数据库镜像中包含一个文本编辑器。 + +### 一个容器只专注做一件事情 +应该保证在一个容器中只运行一个进程。将多个应用解耦到不同容器中,保证了容器的横向扩展和复用。例如一个 web 应用程序可能包含三个独立的容器:web应用、数据库、缓存,每个容器都是独立的镜像,分开运行。但这并不是说一个容器就只跑一个进程,因为有的程序可能会自行产生其他进程,比如 Celery 就可以有很多个工作进程。虽然“每个容器跑一个进程”是一条很好的法则,但这并不是一条硬性的规定。我们主要是希望一个容器只关注意见事情,尽量保持干净和模块化。 + +如果容器互相依赖,你可以使用[Docker 容器网络](https://docs.docker.com/engine/userguide/networking/)来把这些容器连接起来,我们前面已经跟大家讲解过 Docker 的容器网络模式了。 + +### 最小化镜像层数 +在 Docker 17.05 甚至更早 1.10之 前,尽量减少镜像层数是非常重要的,不过现在的版本已经有了一定的改善了: + +* 在 1.10 以后,只有 RUN、COPY 和 ADD 指令会创建层,其他指令会创建临时的中间镜像,但是不会直接增加构建的镜像大小了。 +* 上节课我们也讲解到了 17.05 版本以后增加了多阶段构建的支持,允许我们把需要的数据直接复制到最终的镜像中,这就允许我们在中间阶段包含一些工具或者调试信息了,而且不会增加最终的镜像大小。 + +当然减少`RUN、COPY、ADD`的指令仍然是很有必要的,但是我们也需要在 Dockerfile 可读性(也包括长期的可维护性)和减少层数之间做一个平衡。 + +### 对多行参数排序 +只要有可能,就将多行参数按字母顺序排序(比如要安装多个包时)。这可以帮助你避免重复包含同一个包,更新包列表时也更容易,也更容易阅读和审查。建议在反斜杠符号 \ 之前添加一个空格,可以增加可读性。 +下面是来自 buildpack-deps 镜像的例子: +```docker +RUN apt-get update && apt-get install -y \ + bzr \ + cvs \ + git \ + mercurial \ + subversion +``` + +### 构建缓存 +在镜像的构建过程中,Docker 根据 Dockerfile 指定的顺序执行每个指令。在执行每条指令之前,Docker 都会在缓存中查找是否已经存在可重用的镜像,如果有就使用现存的镜像,不再重复创建。当然如果你不想在构建过程中使用缓存,你可以在 docker build 命令中使用`--no-cache=true`选项。 + +如果你想在构建的过程中使用了缓存,那么了解什么时候可以什么时候无法找到匹配的镜像就很重要了,Docker中缓存遵循的基本规则如下: + +* 从一个基础镜像开始(FROM 指令指定),下一条指令将和该基础镜像的所有子镜像进行匹配,检查这些子镜像被创建时使用的指令是否和被检查的指令完全一样。如果不是,则缓存失效。 +* 在大多数情况下,只需要简单地对比 Dockerfile 中的指令和子镜像。然而,有些指令需要更多的检查和解释。 +* 对于 ADD 和 COPY 指令,镜像中对应文件的内容也会被检查,每个文件都会计算出一个校验值。这些文件的修改时间和最后访问时间不会被纳入校验的范围。在缓存的查找过程中,会将这些校验和和已存在镜像中的文件校验值进行对比。如果文件有任何改变,比如内容和元数据,则缓存失效。 +* 除了 ADD 和 COPY 指令,缓存匹配过程不会查看临时容器中的文件来决定缓存是否匹配。例如,当执行完 RUN apt-get -y update 指令后,容器中一些文件被更新,但 Docker 不会检查这些文件。这种情况下,只有指令字符串本身被用来匹配缓存。 +* 一旦缓存失效,所有后续的 Dockerfile 指令都将产生新的镜像,缓存不会被使用。 + + +## Dockerfile 指令 +下面是一些常用的 Dockerfile 指令,我们也分别来总结下,根据上面的建议和下面这些指令的合理使用,可以帮助我们编写高效且易维护的 Dockerfile 文件。 + +### FROM +尽可能使用当前官方仓库作为你构建镜像的基础。推荐使用[Alpine](https://hub.docker.com/_/alpine/)镜像,因为它被严格控制并保持最小尺寸(目前小于 5 MB),但它仍然是一个完整的发行版。 + +### LABEL +你可以给镜像添加标签来帮助组织镜像、记录许可信息、辅助自动化构建等。每个标签一行,由 LABEL 开头加上一个或多个标签对。 + +下面的示例展示了各种不同的可能格式。`#`开头的行是注释内容。 + +> 注意:如果你的字符串包含空格,那么它必须被引用或者空格必须被转义。如果您的字符串包含内部引号字符("),则也可以将其转义。 + +```docker +# Set one or more individual labels +LABEL com.example.version="0.0.1-beta" +LABEL vendor="ACME Incorporated" +LABEL com.example.release-date="2015-02-12" +LABEL com.example.version.is-production="" +``` + +一个镜像可以包含多个标签,在 1.10 之前,建议将所有标签合并为一条`LABEL`指令,以防止创建额外的层,但是现在这个不再是必须的了,以上内容也可以写成下面这样: +```docker +# Set multiple labels at once, using line-continuation characters to break long lines +LABEL vendor=ACME\ Incorporated \ + com.example.is-production="" \ + com.example.version="0.0.1-beta" \ + com.example.release-date="2015-02-12" +``` + +关于标签可以接受的键值对,参考[Understanding object labels](https://docs.docker.com/config/labels-custom-metadata/)。 + +### RUN +为了保持 Dockerfile 文件的可读性,以及可维护性,建议将长的或复杂的`RUN`指令用反斜杠`\`分割成多行。 + +RUN 指令最常见的用法是安装包用的`apt-get`。因为`RUN apt-get`指令会安装包,所以有几个问题需要注意。 + +* 不要使用 RUN apt-get upgrade 或 dist-upgrade,如果基础镜像中的某个包过时了,你应该联系它的维护者。如果你确定某个特定的包,比如 foo,需要升级,使用 apt-get install -y foo 就行,该指令会自动升级 foo 包。 +* 永远将 RUN apt-get update 和 apt-get install 组合成一条 RUN 声明,例如: +```docker +RUN apt-get update && apt-get install -y \ + package-bar \ + package-baz \ + package-foo +``` + +将 apt-get update 放在一条单独的 RUN 声明中会导致缓存问题以及后续的 apt-get install 失败。比如,假设你有一个 Dockerfile 文件: +```docker +FROM ubuntu:14.04 +RUN apt-get update +RUN apt-get install -y curl +``` + +构建镜像后,所有的层都在 Docker 的缓存中。假设你后来又修改了其中的 apt-get install 添加了一个包: +```docker +FROM ubuntu:14.04 +RUN apt-get update +RUN apt-get install -y curl nginx +``` + +Docker 发现修改后的 RUN apt-get update 指令和之前的完全一样。所以,apt-get update 不会执行,而是使用之前的缓存镜像。因为 apt-get update 没有运行,后面的 apt-get install 可能安装的是过时的 curl 和 nginx 版本。 + +使用**RUN apt-get update && apt-get install -y**可以确保你的`Dockerfiles`每次安装的都是包的最新的版本,而且这个过程不需要进一步的编码或额外干预。这项技术叫作`cache busting(缓存破坏)`。你也可以显示指定一个包的版本号来达到 cache-busting,这就是所谓的固定版本,例如: +```docker +RUN apt-get update && apt-get install -y \ + package-bar \ + package-baz \ + package-foo=1.3.* +``` + +固定版本会迫使构建过程检索特定的版本,而不管缓存中有什么。这项技术也可以减少因所需包中未预料到的变化而导致的失败。 + +下面是一个 RUN 指令的示例模板,展示了所有关于 apt-get 的建议。 +```docker +RUN apt-get update && apt-get install -y \ + aufs-tools \ + automake \ + build-essential \ + curl \ + dpkg-sig \ + libcap-dev \ + libsqlite3-dev \ + mercurial \ + reprepro \ + ruby1.9.1 \ + ruby1.9.1-dev \ + s3cmd=1.1.* \ + && rm -rf /var/lib/apt/lists/* +``` + +其中 s3cmd 指令指定了一个版本号 1.1.*。如果之前的镜像使用的是更旧的版本,指定新的版本会导致 apt-get udpate 缓存失效并确保安装的是新版本。 +另外,清理掉 apt 缓存 var/lib/apt/lists 可以减小镜像大小。因为 RUN 指令的开头为 apt-get udpate,包缓存总是会在 apt-get install 之前刷新。 +> 注意:官方的 Debian 和 Ubuntu 镜像会自动运行 apt-get clean,所以不需要显式的调用 apt-get clean。 + +### CMD +`CMD`指令用于执行目标镜像中包含的软件和任何参数。CMD 几乎都是以`CMD ["executable", "param1", "param2"...]`的形式使用。因此,如果创建镜像的目的是为了部署某个服务(比如 Apache),你可能会执行类似于`CMD ["apache2", "-DFOREGROUND"]`形式的命令。 + +多数情况下,CMD 都需要一个交互式的 shell (bash, Python, perl 等),例如 CMD ["perl", "-de0"],或者 CMD ["PHP", "-a"]。使用这种形式意味着,当你执行类似`docker run -it python`时,你会进入一个准备好的 shell 中。 + +CMD 在极少的情况下才会以 CMD ["param", "param"] 的形式与`ENTRYPOINT`协同使用,除非你和你的镜像使用者都对 ENTRYPOINT 的工作方式十分熟悉。 + +### EXPOSE +`EXPOSE`指令用于指定容器将要监听的端口。因此,你应该为你的应用程序使用常见的端口。 + +例如,提供 Apache web 服务的镜像应该使用 EXPOSE 80,而提供 MongoDB 服务的镜像使用 EXPOSE 27017。 + +对于外部访问,用户可以在执行 docker run 时使用一个标志来指示如何将指定的端口映射到所选择的端口。 + +### ENV +为了方便新程序运行,你可以使用`ENV`来为容器中安装的程序更新 PATH 环境变量。例如使用**ENV PATH /usr/local/nginx/bin:$PATH**来确保`CMD ["nginx"]`能正确运行。 + +ENV 指令也可用于为你想要容器化的服务提供必要的环境变量,比如 Postgres 需要的 PGDATA。 +最后,ENV 也能用于设置常见的版本号,比如下面的示例: +```docker +ENV PG_MAJOR 9.3 +ENV PG_VERSION 9.3.4 +RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH +``` + +类似于程序中的常量,这种方法可以让你只需改变 ENV 指令来自动的改变容器中的软件版本。 + +### ADD 和 COPY +虽然`ADD`和`COPY`功能类似,但一般优先使用 COPY。因为它比 ADD 更透明。COPY 只支持简单将本地文件拷贝到容器中,而 ADD 有一些并不明显的功能(比如本地 tar 提取和远程 URL 支持)。因此,ADD的最佳用例是将本地 tar 文件自动提取到镜像中,例如**ADD rootfs.tar.xz**。 + +如果你的 Dockerfile 有多个步骤需要使用上下文中不同的文件。单独 COPY 每个文件,而不是一次性的 COPY 所有文件,这将保证每个步骤的构建缓存只在特定的文件变化时失效。例如: +```docker +COPY requirements.txt /tmp/ +RUN pip install --requirement /tmp/requirements.txt +COPY . /tmp/ +``` +如果将`COPY . /tmp/`放置在 RUN 指令之前,只要 . 目录中任何一个文件变化,都会导致后续指令的缓存失效。 + +为了让镜像尽量小,最好不要使用 ADD 指令从远程 URL 获取包,而是使用 curl 和 wget。这样你可以在文件提取完之后删掉不再需要的文件来避免在镜像中额外添加一层。比如尽量避免下面的用法: +```docker +ADD http://example.com/big.tar.xz /usr/src/things/ +RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things +RUN make -C /usr/src/things all +``` + +而是应该使用下面这种方法: +```docker +RUN mkdir -p /usr/src/things \ + && curl -SL http://example.com/big.tar.xz \ + | tar -xJC /usr/src/things \ + && make -C /usr/src/things all +``` + +上面使用的管道操作,所以没有中间文件需要删除。 +对于其他不需要 ADD 的自动提取功能的文件或目录,你应该使用 COPY。 + +### ENTRYPOINT +`ENTRYPOINT`的最佳用处是设置镜像的主命令,允许将镜像当成命令本身来运行(用 CMD 提供默认选项)。 + +例如,下面的示例镜像提供了命令行工具 s3cmd: +```docker +ENTRYPOINT ["s3cmd"] +CMD ["--help"] +``` + +现在直接运行该镜像创建的容器会显示命令帮助: +```shell +$ docker run s3cmd +``` + +或者提供正确的参数来执行某个命令: +```shell +$ docker run s3cmd ls s3://mybucket +``` + +这样镜像名可以当成命令行的参考。ENTRYPOINT 指令也可以结合一个辅助脚本使用,和前面命令行风格类似,即使启动工具需要不止一个步骤。 + +例如,Postgres 官方镜像使用下面的脚本作为 ENTRYPOINT: +```shell +#!/bin/bash +set -e +if [ "$1" = 'postgres' ]; then + chown -R postgres "$PGDATA" + + if [ -z "$(ls -A "$PGDATA")" ]; then + gosu postgres initdb + fi + + exec gosu postgres "$@"fi +exec "$@" +``` + +> 注意:该脚本使用了 Bash 的内置命令 exec,所以最后运行的进程就是容器的 PID 为 1 的进程。这样,进程就可以接收到任何发送给容器的 Unix 信号了。 + +该辅助脚本被拷贝到容器,并在容器启动时通过 ENTRYPOINT 执行: +```docker +COPY ./docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] +``` + +该脚本可以让用户用几种不同的方式和 Postgres 交互。你可以很简单地启动 Postgres: +```shell +$ docker run postgres +``` + +也可以执行 Postgres 并传递参数: +```shell +$ docker run postgres postgres --help +``` + +最后,你还可以启动另外一个完全不同的工具,比如 Bash: +```shell +$ docker run --rm -it postgres bash +``` + +### VOLUME +`VOLUME`指令用于暴露任何数据库存储文件,配置文件,或容器创建的文件和目录。强烈建议使用 VOLUME来管理镜像中的可变部分和用户可以改变的部分。 + +### USER +如果某个服务不需要特权执行,建议使用 USER 指令切换到非 root 用户。先在 Dockerfile 中使用类似 RUN groupadd -r postgres && useradd -r -g postgres postgres 的指令创建用户和用户组。 + +> 注意:在镜像中,用户和用户组每次被分配的 UID/GID 都是不确定的,下次重新构建镜像时被分配到的 UID/GID 可能会不一样。如果要依赖确定的 UID/GID,你应该显示的指定一个 UID/GID。 + +你应该避免使用 sudo,因为它不可预期的 TTY 和信号转发行为可能造成的问题比它能解决的问题还多。如果你真的需要和 sudo 类似的功能(例如,以 root 权限初始化某个守护进程,以非 root 权限执行它),你可以使用 gosu。 + +最后,为了减少层数和复杂度,避免频繁地使用 USER 来回切换用户。 + +### WORKDIR +为了清晰性和可靠性,你应该总是在`WORKDIR`中使用**绝对路径**。另外,你应该使用 WORKDIR 来替代类似于 RUN cd ... && do-something 的指令,后者难以阅读、排错和维护。 + +### ONBUILD +格式:**ONBUILD <其它指令>**。 +`ONBUILD`是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。 + +假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖。然后就可以通过 npm start 来启动应用。因此,一般来说会这样写 Dockerfile: +```docker +FROM node:slim +RUN mkdir /app +WORKDIR /app +COPY ./package.json /app +RUN [ "npm", "install" ] +COPY . /app/ +CMD [ "npm", "start" ] +``` + +把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题: + +如果第一个 Node.js 项目在开发过程中,发现这个 Dockerfile 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 Dockerfile,再次构建,问题解决。第一个项目没问题了,但是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 Dockerfile,而第二个项目的 Dockerfile 就会被自动修复。 + +那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,重新构建后就继承了基础镜像的更新?好吧,可以,让我们看看这样的结果。那么上面的这个 Dockerfile 就会变为: +```docker +FROM node:slim +RUN mkdir /app +WORKDIR /app +CMD [ "npm", "start" ] +``` + +这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的自己的 Dockerfile 就变为: +```docker +FROM my-node +COPY ./package.json /app +RUN [ "npm", "install" ] +COPY . /app/ +``` + +基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。 + +那么,问题解决了么?没有。准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的 ./package.json,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。 + +ONBUILD 可以解决这个问题。让我们用 ONBUILD 重新写一下基础镜像的 Dockerfile: +```docker +FROM node:slim +RUN mkdir /app +WORKDIR /app +ONBUILD COPY ./package.json /app +ONBUILD RUN [ "npm", "install" ] +ONBUILD COPY . /app/ +CMD [ "npm", "start" ] +``` + +这次我们回到原始的 Dockerfile,但是这次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 Dockerfile 就变成了简单地: +```docker +FROM my-node +``` + +是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。 + +### 官方仓库示例 +这些官方仓库的 Dockerfile 都是参考典范:[https://github.com/docker-library/docs](https://github.com/docker-library/docs) diff --git "a/docs/45.Helm\346\250\241\346\235\277\344\271\213\347\256\241\351\201\223\344\270\216\346\216\247\345\210\266\347\273\223\346\236\204.md" "b/docs/45.Helm\346\250\241\346\235\277\344\271\213\347\256\241\351\201\223\344\270\216\346\216\247\345\210\266\347\273\223\346\236\204.md" index 5ac2b42..c9c84ad 100644 --- "a/docs/45.Helm\346\250\241\346\235\277\344\271\213\347\256\241\351\201\223\344\270\216\346\216\247\345\210\266\347\273\223\346\236\204.md" +++ "b/docs/45.Helm\346\250\241\346\235\277\344\271\213\347\256\241\351\201\223\344\270\216\346\216\247\345\210\266\347\273\223\346\236\204.md" @@ -1,4 +1,4 @@ -# 45. Helm 模板之管道与控制结构 +# 45. Helm 模板之模板函数与管道 上节课我们学习了如何将信息渲染到模板之中,但是这些信息都是直接传入模板引擎中进行渲染的,有的时候我们想要转换一下这些数据才进行渲染,这就需要使用到 Go 模板语言中的一些其他用法。 ## 模板函数 @@ -179,224 +179,3 @@ data: ``` 我们可以看到`myvalue`值被渲染成了**Hello World**,证明我们的默认值生效了。 - - -## 控制结构 -`模板函数和管道`是通过转换信息并将其插入到`YAML`文件中的强大方法。但有时候需要添加一些比插入字符串更复杂一些的模板逻辑。这就需要使用到模板语言中提供的控制结构了。 - -控制流程为我们提供了控制模板生成流程的一种能力,Helm 的模板语言提供了以下几种流程控制: - -* `if/else` 条件块 -* `with` 指定范围 -* `range` 循环块 - -除此之外,它还提供了一些声明和使用命名模板段的操作: - -* `define`在模板中声明一个新的命名模板 -* `template`导入一个命名模板 -* `block`声明了一种特殊的可填写的模板区域 - -关于`命名模板`的相关知识点,我们会在后面的课程中和大家接触到,这里我们暂时和大家介绍`if/else`、`with`、`range`这3中控制流程的用法。 - -### if/else 条件 -`if/else`块是用于在模板中有条件地包含文本块的方法,条件块的基本结构如下: -```yaml -{{ if PIPELINE }} - # Do something -{{ else if OTHER PIPELINE }} - # Do something else -{{ else }} - # Default case -{{ end }} -``` - -当然要使用条件块就得判断条件是否为真,如果值为下面的几种情况,则管道的结果为 false: - -* 一个布尔类型的`假` -* 一个数字`零` -* 一个`空`的字符串 -* 一个`nil`(空或`null`) -* 一个空的集合(`map`、`slice`、`tuple`、`dict`、`array`) - -除了上面的这些情况外,其他所有条件都为`真`。 - -同样还是以上面的 ConfigMap 模板文件为例,添加一个简单的条件判断,如果 python 被设置为 django,则添加一个`web: true`:(tempaltes/configmap.yaml) -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: {{ .Values.hello | default "Hello World" | quote }} - k8s: {{ .Values.course.k8s | upper | quote }} - python: {{ .Values.course.python | repeat 3 | quote }} - {{ if eq .Values.course.python "django" }}web: true{{ end }} -``` - -在上面的模板文件中我们增加了一个条件语句判断`{{ if eq .Values.course.python "django" }}web: true{{ end }}`,其中运算符`eq`是判断是否相等的操作,除此之外,还有`ne`、`lt`、`gt`、`and`、`or`等运算符都是 Helm 模板已经实现了的,直接使用即可。这里我们`{{ .Values.course.python }}`的值在`values.yaml`文件中默认被设置为了**django**,所以正常来说下面的条件语句判断为**真**,所以模板文件最终被渲染后会有`web: true`这样的的一个条目: -```shell -$ helm install --dry-run --debug . -[debug] Created tunnel using local port: '40143' - -...... - ---- -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: fallacious-prawn-configmap -data: - myvalue: "Hello World" - k8s: "DEVOPS" - python: "djangodjangodjangodjangodjango" - web: true -``` - -可以看到上面模板被渲染后出现了`web: true`的条目,如果我们在安装的时候覆盖下 python 的值呢,比如我们改成 ai: -```shell -helm install --dry-run --debug --set course.python=ai . -[debug] Created tunnel using local port: '42802' - -...... - ---- -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: dull-mite-configmap -data: - myvalue: "Hello World" - k8s: "DEVOPS" - python: "aiaiai" -``` - -根据我们模板文件中的定义,如果`{{ .Values.course.python }}`的值为`django`的话就会新增`web: true`这样的一个条目,但是现在我们是不是通过参数`--set`将值设置为了 ai,所以这里条件判断为**假**,正常来说就不应该出现这个条目了,上面我们通过 debug 模式查看最终被渲染的值也没有出现这个条目,证明条件判断是正确的。 - -### 空格控制 -上面我们的条件判断语句是在一整行中的,如果平时经常写代码的同学可能非常不习惯了,我们一般会将其格式化为更容易阅读的形式,比如: -```yaml -{{ if eq .Values.course.python "django" }} -web: true -{{ end }} -``` - -这样的话看上去比之前要清晰很多了,但是我们通过模板引擎来渲染一下,会得到如下结果: -```shell -$ helm install --dry-run --debug . -[debug] Created tunnel using local port: '44537' - -...... - ---- -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: bald-narwhal-configmap -data: - myvalue: "Hello World" - k8s: "DEVOPS" - python: "djangodjangodjango" - - web: true -``` - -我们可以看到渲染出来会有多余的空行,这是因为当模板引擎运行时,它将一些值渲染过后,之前的指令被删除,但它之前所占的位置完全按原样保留剩余的空白了,所以就出现了多余的空行。`YAML`文件中的空格是非常严格的,所以对于空格的管理非常重要,一不小心就会导致你的`YAML`文件格式错误。 - -我们可以通过使用在模板标识`{{`后面添加破折号和空格`{{- `来表示将空白左移,而在`}}`前面添加一个空格和破折号` -}}`表示应该删除右边的空格,另外需要注意的是**换行符也是空格!** - -使用这个语法,我们来修改我们上面的模板文件去掉多余的空格:(templates/configmap.yaml) -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: {{ .Values.hello | default "Hello World" | quote }} - k8s: {{ .Values.course.k8s | upper | quote }} - python: {{ .Values.course.python | repeat 3 | quote }} - {{- if eq .Values.course.python "django" }} - web: true - {{- end }} -``` - -现在我们来查看上面模板渲染过后的样子: -```shell -$ helm install --dry-run --debug . -[debug] Created tunnel using local port: '34702' - -...... - ---- -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: mangy-olm-configmap -data: - myvalue: "Hello World" - k8s: "DEVOPS" - python: "djangodjangodjango" - web: true -``` - -现在是不是没有多余的空格了,另外我们需要谨慎使用` -}}`,比如上面模板文件中: -```yaml -python: {{ .Values.course.python | repeat 3 | quote }} -{{- if eq .Values.course.python "django" -}} -web: true -{{- end }} -``` - -如果我们在`if`条件后面增加` -}}`,这会渲染成: -```yaml -python: "djangodjangodjango"web: true -``` - -因为`-}}`它删除了双方的换行符,显然这是不正确的。 - -> 有关模板中空格控制的详细信息,请参阅官方 Go 模板文档[Official Go template documentation](https://godoc.org/text/template) - - -### 使用 with 修改范围 -接下来我们来看下`with`关键词的使用,它用来控制变量作用域。还记得之前我们的`{{ .Release.xxx }}`或者`{{ .Values.xxx }}`吗?其中的`.`就是表示对当前范围的引用,`.Values`就是告诉模板在当前范围中查找`Values`对象的值。而`with`语句就可以来控制变量的作用域范围,其语法和一个简单的`if`语句比较类似: -```yaml -{{ with PIPELINE }} - # restricted scope -{{ end }} -``` - -`with`语句可以允许将当前范围`.`设置为特定的对象,比如我们前面一直使用的`.Values.course`,我们可以使用`with`来将`.`范围指向`.Values.course`:(templates/configmap.yaml) -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: {{ .Values.hello | default "Hello World" | quote }} - {{- with .Values.course }} - k8s: {{ .k8s | upper | quote }} - python: {{ .python | repeat 3 | quote }} - {{- if eq .python "django" }} - web: true - {{- end }} - {{- end }} -``` - -可以看到上面我们增加了一个`{{- with .Values.course }}xxx{{- end }}`的一个块,这样的话我们就可以在当前的块里面直接引用`.python`和`.k8s`了,而不需要进行限定了,这是因为该`with`声明将`.`指向了`.Values.course`,在`{{- end }}`后`.`就会复原其之前的作用范围了,我们可以使用模板引擎来渲染上面的模板查看是否符合预期结果。 - -不过需要注意的是在`with`声明的范围内,此时将无法从父范围访问到其他对象了,比如下面的模板渲染的时候将会报错,因为显然`.Release`根本就不在当前的`.`范围内,当然如果我们最后两行交换下位置就正常了,因为`{{- end }}`之后范围就被重置了: -```yaml -{{- with .Values.course }} -k8s: {{ .k8s | upper | quote }} -python: {{ .python | repeat 3 | quote }} -release: {{ .Release.Name }} -{{- end }} -``` - -### range 循环 -如果大家对编程语言熟悉的话,几乎所有的编程语言都支持类似于`for`、`foreach`或者类似功能的循环机制,在 Helm 模板语言中,是使用`range`关键字来进行循环操作。 - -todo diff --git "a/docs/46.Helm\346\250\241\346\235\277\344\271\213\346\216\247\345\210\266\346\265\201\347\250\213.md" "b/docs/46.Helm\346\250\241\346\235\277\344\271\213\346\216\247\345\210\266\346\265\201\347\250\213.md" new file mode 100644 index 0000000..4cc118c --- /dev/null +++ "b/docs/46.Helm\346\250\241\346\235\277\344\271\213\346\216\247\345\210\266\346\265\201\347\250\213.md" @@ -0,0 +1,281 @@ +46. Helm 模板之控制流程 + +`模板函数和管道`是通过转换信息并将其插入到`YAML`文件中的强大方法。但有时候需要添加一些比插入字符串更复杂一些的模板逻辑。这就需要使用到模板语言中提供的控制结构了。 + +控制流程为我们提供了控制模板生成流程的一种能力,Helm 的模板语言提供了以下几种流程控制: + +* `if/else` 条件块 +* `with` 指定范围 +* `range` 循环块 + +除此之外,它还提供了一些声明和使用命名模板段的操作: + +* `define`在模板中声明一个新的命名模板 +* `template`导入一个命名模板 +* `block`声明了一种特殊的可填写的模板区域 + +关于`命名模板`的相关知识点,我们会在后面的课程中和大家接触到,这里我们暂时和大家介绍`if/else`、`with`、`range`这3中控制流程的用法。 + +## if/else 条件 +`if/else`块是用于在模板中有条件地包含文本块的方法,条件块的基本结构如下: +```yaml +{{ if PIPELINE }} + # Do something +{{ else if OTHER PIPELINE }} + # Do something else +{{ else }} + # Default case +{{ end }} +``` + +当然要使用条件块就得判断条件是否为真,如果值为下面的几种情况,则管道的结果为 false: + +* 一个布尔类型的`假` +* 一个数字`零` +* 一个`空`的字符串 +* 一个`nil`(空或`null`) +* 一个空的集合(`map`、`slice`、`tuple`、`dict`、`array`) + +除了上面的这些情况外,其他所有条件都为`真`。 + +同样还是以上面的 ConfigMap 模板文件为例,添加一个简单的条件判断,如果 python 被设置为 django,则添加一个`web: true`:(tempaltes/configmap.yaml) +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-configmap +data: + myvalue: {{ .Values.hello | default "Hello World" | quote }} + k8s: {{ .Values.course.k8s | upper | quote }} + python: {{ .Values.course.python | repeat 3 | quote }} + {{ if eq .Values.course.python "django" }}web: true{{ end }} +``` + +在上面的模板文件中我们增加了一个条件语句判断`{{ if eq .Values.course.python "django" }}web: true{{ end }}`,其中运算符`eq`是判断是否相等的操作,除此之外,还有`ne`、`lt`、`gt`、`and`、`or`等运算符都是 Helm 模板已经实现了的,直接使用即可。这里我们`{{ .Values.course.python }}`的值在`values.yaml`文件中默认被设置为了**django**,所以正常来说下面的条件语句判断为**真**,所以模板文件最终被渲染后会有`web: true`这样的的一个条目: +```shell +$ helm install --dry-run --debug . +[debug] Created tunnel using local port: '40143' + +...... + +--- +# Source: mychart/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: fallacious-prawn-configmap +data: + myvalue: "Hello World" + k8s: "DEVOPS" + python: "djangodjangodjangodjangodjango" + web: true +``` + +可以看到上面模板被渲染后出现了`web: true`的条目,如果我们在安装的时候覆盖下 python 的值呢,比如我们改成 ai: +```shell +helm install --dry-run --debug --set course.python=ai . +[debug] Created tunnel using local port: '42802' + +...... + +--- +# Source: mychart/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: dull-mite-configmap +data: + myvalue: "Hello World" + k8s: "DEVOPS" + python: "aiaiai" +``` + +根据我们模板文件中的定义,如果`{{ .Values.course.python }}`的值为`django`的话就会新增`web: true`这样的一个条目,但是现在我们是不是通过参数`--set`将值设置为了 ai,所以这里条件判断为**假**,正常来说就不应该出现这个条目了,上面我们通过 debug 模式查看最终被渲染的值也没有出现这个条目,证明条件判断是正确的。 + +## 空格控制 +上面我们的条件判断语句是在一整行中的,如果平时经常写代码的同学可能非常不习惯了,我们一般会将其格式化为更容易阅读的形式,比如: +```yaml +{{ if eq .Values.course.python "django" }} +web: true +{{ end }} +``` + +这样的话看上去比之前要清晰很多了,但是我们通过模板引擎来渲染一下,会得到如下结果: +```shell +$ helm install --dry-run --debug . +[debug] Created tunnel using local port: '44537' + +...... + +--- +# Source: mychart/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: bald-narwhal-configmap +data: + myvalue: "Hello World" + k8s: "DEVOPS" + python: "djangodjangodjango" + + web: true +``` + +我们可以看到渲染出来会有多余的空行,这是因为当模板引擎运行时,它将一些值渲染过后,之前的指令被删除,但它之前所占的位置完全按原样保留剩余的空白了,所以就出现了多余的空行。`YAML`文件中的空格是非常严格的,所以对于空格的管理非常重要,一不小心就会导致你的`YAML`文件格式错误。 + +我们可以通过使用在模板标识`{{`后面添加破折号和空格`{{- `来表示将空白左移,而在`}}`前面添加一个空格和破折号` -}}`表示应该删除右边的空格,另外需要注意的是**换行符也是空格!** + +使用这个语法,我们来修改我们上面的模板文件去掉多余的空格:(templates/configmap.yaml) +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-configmap +data: + myvalue: {{ .Values.hello | default "Hello World" | quote }} + k8s: {{ .Values.course.k8s | upper | quote }} + python: {{ .Values.course.python | repeat 3 | quote }} + {{- if eq .Values.course.python "django" }} + web: true + {{- end }} +``` + +现在我们来查看上面模板渲染过后的样子: +```shell +$ helm install --dry-run --debug . +[debug] Created tunnel using local port: '34702' + +...... + +--- +# Source: mychart/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: mangy-olm-configmap +data: + myvalue: "Hello World" + k8s: "DEVOPS" + python: "djangodjangodjango" + web: true +``` + +现在是不是没有多余的空格了,另外我们需要谨慎使用` -}}`,比如上面模板文件中: +```yaml +python: {{ .Values.course.python | repeat 3 | quote }} +{{- if eq .Values.course.python "django" -}} +web: true +{{- end }} +``` + +如果我们在`if`条件后面增加` -}}`,这会渲染成: +```yaml +python: "djangodjangodjango"web: true +``` + +因为`-}}`它删除了双方的换行符,显然这是不正确的。 + +> 有关模板中空格控制的详细信息,请参阅官方 Go 模板文档[Official Go template documentation](https://godoc.org/text/template) + + +## 使用 with 修改范围 +接下来我们来看下`with`关键词的使用,它用来控制变量作用域。还记得之前我们的`{{ .Release.xxx }}`或者`{{ .Values.xxx }}`吗?其中的`.`就是表示对当前范围的引用,`.Values`就是告诉模板在当前范围中查找`Values`对象的值。而`with`语句就可以来控制变量的作用域范围,其语法和一个简单的`if`语句比较类似: +```yaml +{{ with PIPELINE }} + # restricted scope +{{ end }} +``` + +`with`语句可以允许将当前范围`.`设置为特定的对象,比如我们前面一直使用的`.Values.course`,我们可以使用`with`来将`.`范围指向`.Values.course`:(templates/configmap.yaml) +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-configmap +data: + myvalue: {{ .Values.hello | default "Hello World" | quote }} + {{- with .Values.course }} + k8s: {{ .k8s | upper | quote }} + python: {{ .python | repeat 3 | quote }} + {{- if eq .python "django" }} + web: true + {{- end }} + {{- end }} +``` + +可以看到上面我们增加了一个`{{- with .Values.course }}xxx{{- end }}`的一个块,这样的话我们就可以在当前的块里面直接引用`.python`和`.k8s`了,而不需要进行限定了,这是因为该`with`声明将`.`指向了`.Values.course`,在`{{- end }}`后`.`就会复原其之前的作用范围了,我们可以使用模板引擎来渲染上面的模板查看是否符合预期结果。 + +不过需要注意的是在`with`声明的范围内,此时将无法从父范围访问到其他对象了,比如下面的模板渲染的时候将会报错,因为显然`.Release`根本就不在当前的`.`范围内,当然如果我们最后两行交换下位置就正常了,因为`{{- end }}`之后范围就被重置了: +```yaml +{{- with .Values.course }} +k8s: {{ .k8s | upper | quote }} +python: {{ .python | repeat 3 | quote }} +release: {{ .Release.Name }} +{{- end }} +``` + +## range 循环 +如果大家对编程语言熟悉的话,几乎所有的编程语言都支持类似于`for`、`foreach`或者类似功能的循环机制,在 Helm 模板语言中,是使用`range`关键字来进行循环操作。 + +我们在`values.yaml`文件中添加上一个课程列表: +```yaml +course: + k8s: devops + python: django +courselist: +- k8s +- python +- search +- golang +``` + +现在我们有一个课程列表,修改 ConfigMap 模板文件来循环打印出该列表: +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-configmap +data: + myvalue: {{ .Values.hello | default "Hello World" | quote }} + {{- with .Values.course }} + k8s: {{ .k8s | upper | quote }} + python: {{ .python | repeat 3 | quote }} + {{- if eq .python "django" }} + web: true + {{- end }} + {{- end }} + courselist: + {{- range .Values.courselist }} + - {{ . | title | quote }} + {{- end }} +``` + +可以看到最下面我们使用了一个`range`函数,该函数将会遍历`{{ .Values.courselist }}`列表,循环内部我们使用的是一个`.`,这是因为当前的作用域就在当前循环内,这个`.`从列表的第一个元素一直遍历到最后一个元素,然后在遍历过程中使用了`title`和`quote`这两个函数,前面这个函数是将字符串首字母变成大写,后面就是加上双引号变成字符串,所以按照上面这个模板被渲染过后的结果为: +```shell +$ helm install --dry-run --debug . +[debug] Created tunnel using local port: '34626' + +...... + +--- +# Source: mychart/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: dining-terrier-configmap +data: + myvalue: "Hello World" + k8s: "DEVOPS" + python: "djangodjangodjango" + web: true + courselist: + - "K8s" + - "Python" + - "Search" + - "Golang" +``` + +我们可以看到`courselist`按照我们的要求循环出来了。除了 list 或者 tuple,range 还可以用于遍历具有键和值的集合(如map 或 dict),这个就需要用到变量的概念了。 + +## 变量 + diff --git a/docs/9.Docker Machine.md b/docs/9.Docker Machine.md new file mode 100644 index 0000000..979e000 --- /dev/null +++ b/docs/9.Docker Machine.md @@ -0,0 +1,158 @@ +# 9. Docker Machine +[`Docker Machine`](https://docs.docker.com/machine/overview/)是`Docker`官方编排(Orchestration)项目之一,负责在多种平台上快速安装 Docker 环境。 + +`Docker Machine`项目基于`Go`语言实现,目前在[Github](https://github.com/docker/machine)上进行维护。 + +`Docker Machine`是 Docker 官方提供的一个工具,它可以帮助我们在远程的机器上安装 Docker,或者在虚拟机 host 上直接安装虚拟机并在虚拟机中安装 Docker。我们还可以通过 `docker-machine`命令来管理这些虚拟机和 Docker。 + +本章将介绍 Docker Machine 的安装及使用。 + +## 安装 +Docker Machine 可以在多种操作系统平台上安装,包括 Linux、macOS,以及 Windows。 + +### macOS、Windows +Docker for Mac、Docker for Windows 自带 docker-machine 二进制包,安装之后即可使用。查看版本信息。 +```shell +$ docker-machine -v +docker-machine version 0.13.0, build 9ba6da9 +``` + +### Linux +在 Linux 上的也安装十分简单,从[官方 GitHub Release](https://github.com/docker/machine/releases)处直接下载编译好的二进制文件即可。 +例如,在 Linux 64 位系统上直接下载对应的二进制包。 +```shell +$ sudo curl -L https://github.com/docker/machine/releases/download/v0.13.0/docker-machine-`uname -s`-`uname -m` > /usr/local/bin/docker-machine +$ sudo chmod +x /usr/local/bin/docker-machine +``` + +完成后,查看版本信息。 +```shell +$ docker-machine -v +docker-machine version 0.13.0, build 9ba6da9 +``` + +## 使用 +Docker Machine 支持多种后端驱动,包括虚拟机、本地主机和云平台等。 + +### 创建本地主机实例Virtualbox 驱动 +使用`virtualbox`类型的驱动,创建一台 Docker 主机,命名为 test。 +```shell +$ docker-machine create -d virtualbox test +``` + +你也可以在创建时加上如下参数,来配置主机或者主机上的 Docker。 + +* **--engine-opt dns=114.114.114.114**配置 Docker 的默认 DNS +* **--engine-registry-mirror https://registry.docker-cn.com**配置 Docker 的仓库镜像 +* **--virtualbox-memory 2048** 配置主机内存 +* **--virtualbox-cpu-count 2** 配置主机 CPU + +更多参数请使用`docker-machine create --driver virtualbox --help`命令查看。 + +```shell +$ docker-machine create -d generic \ + --generic-ip-address=123.59.188.19 \ + --generic-ssh-user=root \ + --generic-ssh-key ~/.ssh/id_rsa \ + dev +``` + +### MacOS xhyve 驱动 +`xhyve` 驱动 GitHub: https://github.com/zchee/docker-machine-driver-xhyve,`xhyve`是`MacOS`上轻量化的虚拟引擎,使用其创建的 Docker Machine 较 VirtualBox 驱动创建的运行效率要高。 +```shell +$ brew install docker-machine-driver-xhyve +...... +$ docker-machine create \ + -d xhyve \ + # --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso \ + --engine-opt dns=114.114.114.114 \ + --engine-registry-mirror https://registry.docker-cn.com \ + --xhyve-memory-size 2048 \ + --xhyve-rawdisk \ + --xhyve-cpu-count 2 \ + xhyve +``` + +> 注意:非首次创建时建议加上**--xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso**参数,避免每次创建时都从 GitHub 下载 ISO 镜像。 + +更多参数请使用`docker-machine create --driver xhyve --help`命令查看。 + +### Windows 10 +`Windows 10`安装`Docker for Windows`之后不能再安装`VirtualBox`,也就不能使用 virtualbox 驱动来创建 Docker Machine,我们可以选择使用 hyperv 驱动。 +```shell +$ docker-machine create --driver hyperv vm +``` + +更多参数请使用`docker-machine create --driver hyperv --help`命令查看。 + + +## 使用介绍 +创建好主机之后,查看主机 +```shell +$ docker-machine ls + +NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORStest - virtualbox Running tcp://192.168.99.187:2376 v17.10.0-ce +``` + +创建主机成功后,可以通过`env`命令来让后续操作对象都是目标主机。 +```shell +$ docker-machine env test +``` + +后续根据提示在命令行输入命令之后就可以操作 test 主机。也可以通过`SSH`登录到主机。 +```shell +$ docker-machine ssh test + +docker@test:~$ docker --version +Docker version 17.10.0-ce, build f4ffd25 +``` + +连接到主机之后你就可以在其上使用 Docker 了。 + +### 官方支持驱动 +通过`-d`选项可以选择支持的驱动类型: + +* amazonec2 +* azure +* digitalocean +* exoscale +* generic +* google +* hyperv +* none +* openstack +* rackspace +* softlayer +* virtualbox +* vmwarevcloudair +* vmwarefusion +* vmwarevsphere + +### 操作命令 +* active 查看活跃的 Docker 主机 +* config 输出连接的配置信息 +* create 创建一个 Docker 主机 +* env 显示连接到某个主机需要的环境变量 +* inspect 输出主机更多信息 +* ip 获取主机地址 +* kill 停止某个主机 +* ls 列出所有管理的主机 +* provision 重新设置一个已存在的主机 +* regenerate-certs 为某个主机重新生成 TLS 认证信息 +* restart 重启主机 +* rm 删除某台主机 +* ssh SSH 到主机上执行命令 +* scp 在主机之间复制文件 +* mount 挂载主机目录到本地 +* start 启动一个主机 +* status 查看主机状态 +* stop 停止一个主机 +* upgrade 更新主机 Docker 版本为最新 +* url 获取主机的 URL +* version 输出 docker-machine 版本信息 +* help 输出帮助信息 + +每个命令,又带有不同的参数,可以通过如下命令来查看具体的用法: +```shell +$ docker-machine COMMAND --help +``` diff --git a/docs/images/docker-cadvisor.png b/docs/images/docker-cadvisor.png new file mode 100644 index 0000000..c5757de Binary files /dev/null and b/docs/images/docker-cadvisor.png differ diff --git a/docs/images/docker-swarm-structrue.png b/docs/images/docker-swarm-structrue.png new file mode 100644 index 0000000..875330c Binary files /dev/null and b/docs/images/docker-swarm-structrue.png differ diff --git a/docs/images/docker-swarm-task-service.png b/docs/images/docker-swarm-task-service.png new file mode 100644 index 0000000..e30302e Binary files /dev/null and b/docs/images/docker-swarm-task-service.png differ diff --git a/docs/images/docker-ui-portainer.png b/docs/images/docker-ui-portainer.png new file mode 100644 index 0000000..95bb606 Binary files /dev/null and b/docs/images/docker-ui-portainer.png differ diff --git a/docs/images/docker-ui-rancher.png b/docs/images/docker-ui-rancher.png new file mode 100644 index 0000000..0978e97 Binary files /dev/null and b/docs/images/docker-ui-rancher.png differ