Docker实践(28) – 直接运行容器内的命令

在docker早期,许多用户添加SSH server到它们的镜像里,以便他们能通过外部的shell来访问容器。这样做的话相当于把容器当作虚拟机用了,添加了一个进程无疑增加了系统的开销。于是Docker引入了exec命令,提供了不需要安装ssh server直接在容器内执行命令的方法。下面我们介绍这个命令。

问题

你想在一个运行的容器内执行命令。

解决方法

使用docker exec命令。

讨论

为了演示exec命令,我们先在后台启动一个容器,名称为sleeper,仅执行一个sleep命令。

  1. docker run -d –name sleeper debian sleep infinity

现在我们启动了一个容器,你可以在上面使用docker exec命令完成各种试验。exec命令有三种模式,如下:

  • 基本模式 – 在命令行中同步运行容器中的命令
  • 后台模式 – 后台执行容器中的命令
  • 交互模式 – 执行一个命令,允许用户与之交互
  • 首先我们学习基本模式。下面的命令在容器sleeper内执行一个echo命令:

    1. $ docker exec sleeper echo "hello host from container"
    2. hello host from container

    有没有注意到这个命令的结构与docker run命令类似,只是我们给的ID是正式运行容器的ID而不是镜像ID。运行的echo命令是容器内的命令,不是主机上的。
    后台模式是在后台运行命令,在你的终端不会看到输出。这个模式通常用于执行花费比较长的任务,并且不关心它的输出,如清除日志文件:

    1. $ docker exec -d sleeper find / -ctime 7 -name ‘*log’ -exec rm {} ;

    最后是交互模式。通常用来执行一个shell,然后与之交互:

    1. $ docker exec -i -t sleeper /bin/bash
    2. root@d46dc042480f:/#

    Docker实践(27) – 使用UI For Docker Web界面管理Docker

    一般情况下我们使用docker自带的cli来管理docker,如镜像及容器等。除了通过cli来管理docker,docker来提供了完整的API来管理各种组件,这就方便开发者来开发各种方便的工具来管理docker,比如通过浏览器的web界面。目前docker web界面开发得较好的是https://github.com/kevana/ui-for-docker。

    问题

    你想通过其它更简便的方式来管理docker,而不是用cli。

    解决方法

    使用ui-for-docker。

    讨论

    ui-for-docker项目托管在github上,地址为https://github.com/kevana/ui-for-docker。安装非常简单,就一个命令:

    1. docker run -d -p 9000:9000 –privileged -v /var/run/docker.sock:/var/run/docker.sock uifd/ui-for-docker

    完成后你就可以打开浏览器输入http://[你的主机IP]:9000来打开docker web管理界面了,如图:
    虚拟化技术

    虚拟化技术

    虚拟化技术
    虚拟化技术
    通过ui-for-docker,你可以对容器,容器网络,镜像,主机网络,Volumes进行管理。

    Docker实践(26) – 设置从Dockerfile指定点缓存失效

    使用–no-cache构建镜像大多情况下足够解决由于缓存引起的问题。不过有时候你想要一个更细粒度的解决方案。例如你构建的镜像需要时间比较长,你仍然想一些步骤使用缓存,然后从指定的点开始不使用缓存重新运行命令构建镜像。

    问题

    你想在构建镜像时设置从Dockerfile的指定位置开始使缓存失效。

    解决方法

    在命令的后面添加注释来使缓存失效。

    讨论

    例如我们在以下的Dockerfile中的CMD命令后添加注释以让缓存从这里失效:

    1. FROM node
    2. MAINTAINER [email protected]
    3. RUN git clone https://github.com/docker-in-practice/todo.git
    4. WORKDIR todo
    5. RUN npm install
    6. RUN chmod -R 777 /todo
    7. EXPOSE 8000
    8. CMD ["npm","start"] #bust the cache

    输出为:

    1. $ docker build .
    2. Sending build context to Docker daemon  2.56 kB
    3. Sending build context to Docker daemon
    4. Step 0 : FROM node
    5.  —> 91cbcf796c2c
    6. Step 1 : MAINTAINER [email protected]
    7.  —> Using cache
    8. A “normal” docker build
    9.    —> 8f5a8a3d9240
    10. Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
    11.  —> Using cache
    12.  —> 48db97331aa2
    13. Step 3 : WORKDIR todo
    14.  —> Using cache
    15.  —> c5c85db751d6
    16. Step 4 : RUN npm install
    17.  —> Using cache
    18.  —> be943c45c55b
    19. Step 5 : EXPOSE 8000
    20.  —> Using cache
    21.  —> 805b18d28a65
    22. Step 6 : CMD ["npm","start"] #bust the cache
    23.  —> Running in fc6c4cd487ce
    24.  —> d66d9572115e
    25. Removing intermediate container fc6c4cd487ce
    26. Successfully built d66d9572115e

    从输出你会看到在第6步已经不使用缓存了,而之前的步骤仍使用缓存,这既缩短了构建镜像的时间,又能解决缓存可能引起的问题。
    这个技巧工作的原因是我们在命令后添加了非空字符,所以Docker认为这是一个新的命令,也就不使用缓存了。
    你会好奇如果我们更改的是第4步命令RUN npm install,那后面的第5步第6步还会使用缓存吗?答案是从更改的第4步开始下面的就不能使用缓存了。所以由于这个原因,建议尽可能的把一些不常需要变更的命令往上移。

    Docker实践(25) – 不使用缓存重建镜像

    使用Dockerfile构建镜像可以利用它的缓存功能:只有在命令已更改的情况下,才会重建已构建的步骤。下面是重新构建之前涉及到的to-do app的示例:

    1. $ docker build .
    2. Sending build context to Docker daemon  2.56 kB
    3. Sending build context to Docker daemon
    4. Step 0 : FROM node
    5.   —> 91cbcf796c2c
    6. Step 1 : MAINTAINER [email protected]
    7.  —> Using cache
    8. Indicates you’re using the cache
    9. Specifies the cached image/layer ID
    10.    —> 8f5a8a3d9240
    11. Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
    12.  —> Using cache
    13.  —> 48db97331aa2
    14. Step 3 : WORKDIR todo
    15.  —> Using cache
    16.  —> c5c85db751d6
    17. Step 4 : RUN npm install > /dev/null
    18.  —> Using cache
    19.  —> be943c45c55b
    20. Step 5 : EXPOSE 8000
    21.  —> Using cache
    22.  —> 805b18d28a65
    23. Step 6 : CMD npm start
    24.  —> Using cache
    25.  —> 19525d4ec794
    26. Successfully built 19525d4ec794

    缓存非常有用并且省时间,不过有时候docker缓存的行为不都能达到你的期望。
    用以上Dockerfile作为示例,假设你更改了代码并push到Git仓库。新代码不会check out下来,因为git clone命令没有更改。在Docker看来git clone的步骤一样,所以使用了缓存。
    在这种情况下,你可能不想开启docker的缓存了。

    问题

    你想不用缓存重建Dockerfile。

    解决方法

    构建镜像时使用–no-cache参数。

    讨论

    为了强制docker构建镜像时不用缓存,执行带–no-cache参数的docker build命令。下面的示例是使用了–no-cache构建镜像。

    1. $ docker build –no-cache .
    2. Sending build context to Docker daemon  2.56 kB
    3. Sending build context to Docker daemon
    4. Step 0 : FROM node
    5.  —> 91cbcf796c2c
    6. Step 1 : MAINTAINER [email protected]
    7.  —> Running in ca243b77f6a1
    8.  —> 602f1294d7f1
    9. Removing intermediate container ca243b77f6a1
    10. Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
    11.  —> Running in f2c0ac021247
    12.  —> 04ee24faaf18
    13. Removing intermediate container f2c0ac021247
    14. Step 3 : WORKDIR todo
    15.  —> Running in c2d9cd32c182
    16.  —> 4e0029de9074
    17. Removing intermediate container c2d9cd32c182
    18. Step 4 : RUN npm install > /dev/null
    19.  —> Running in 79122dbf9e52
    20. npm WARN package.json [email protected] No repository field.
    21.  —> 9b6531f2036a
    22. Removing intermediate container 79122dbf9e52
    23. Step 5 : EXPOSE 8000
    24.  —> Running in d1d58e1c4b15
    25.  —> f7c1b9151108
    26. Removing intermediate container d1d58e1c4b15
    27. Step 6 : CMD npm start
    28.  —> Running in 697713ebb185
    29.  —> 74f9ad384859
    30. Removing intermediate container 697713ebb185
    31. Successfully built 74f9ad384859

    以上的构建镜像步骤没有使用到缓存,每一层的镜像ID都与之间的不同。

    Docker实践(24) – 使用ADD命令添加文件到镜像

    虽然在Dockerfile内能使用RUN命令或者shell命令来添加文件到镜像,不过这可能很快变得难以管理。Dockerfile命令之一的ADD命令设计用来满足将大量文件放入镜像的需求。

    问题

    你想以一个简单的方法下载和解压一个tarball文件到你的镜像。

    解决方法

    tar打包和压缩你的文件,并在你的Dockerfile使用ADD指令。

    讨论

    使用mkdir add_example && cd add_example为Docker构建镜像准备一个新的环境。然后获取tarball文件:

    1. $ curl https://www.flamingspork.com/projects/libeatmydata/libeatmydata-105.tar.gz >  my.tar.gz

    创建Dockerfile:

    1. FROM debian
    2. RUN mkdir -p /opt/libeatmydata
    3. ADD my.tar.gz /opt/libeatmydata/
    4. RUN ls -lRt /opt/libeatmydata

    使用docker build –no-cache构建Dockerfile,输出类似如下:

    1. $ docker build –no-cache .
    2. Sending build context to Docker daemon 422.9 kB
    3. Sending build context to Docker daemon
    4. Step 0 : FROM debian
    5.  —> c90d655b99b2
    6. Step 1 : RUN mkdir -p /opt/libeatmydata
    7.  —> Running in fe04bac7df74
    8.  —> c0ab8c88bb46
    9. Removing intermediate container fe04bac7df74
    10. Step 2 : ADD my.tar.gz /opt/libeatmydata/
    11.  —> 06dcd7a88eb7
    12. Removing intermediate container 3f093a1f9e33
    13. Step 3 : RUN ls -lRt /opt/libeatmydata
    14.  —> Running in e3283848ad65
    15. /opt/libeatmydata:
    16. total 4
    17. drwxr-xr-x 7 1000 1000 4096 Oct 29 23:02 libeatmydata-105
    18. /opt/libeatmydata/libeatmydata-105:
    19. total 880
    20. drwxr-xr-x 2 1000 1000  4096 Oct  29 23:02 config
    21. drwxr-xr-x 3 1000 1000  4096 Oct  29 23:02 debian
    22. drwxr-xr-x 2 1000 1000  4096 Oct  29 23:02 docs
    23. drwxr-xr-x 3 1000 1000  4096 Oct  29 23:02 libeatmydata
    24. drwxr-xr-x 2 1000 1000  4096 Oct  29 23:02 m4
    25. -rw-r–r– 1 1000 1000  4096 Oct  29 23:01 config.h.in
    26. […edited…]
    27. -rw-r–r– 1 1000 1000   1824 Jun 18  2012 pandora_have_better_malloc.m4
    28. -rw-r–r– 1 1000 1000    742 Jun 18  2012 pandora_header_assert.m4
    29. -rw-r–r– 1 1000 1000    431 Jun 18  2012 pandora_version.m4
    30.  —> 2ee9b4c8059f
    31. Removing intermediate container e3283848ad65
    32. Successfully built 2ee9b4c8059f

    你从输出看到tarball文件已经被Docker daemon解压到了目标目录。Docker支持大多数类型的压缩文件,如.gz,.bz2,.xz,.tar。
    值得注意的是如果你在Dockerfile直接从url下载tarball文件,那么它们是不会自动解压的,Docker daemon只解压使用ADD命令添加本地的压缩文件。
    如果你使用如下的Dockerfile再次构建镜像,你会发现文件只下载没有解压:

    1. FROM debian The file is retrieved from
    2. RUN mkdir -p /opt/libeatmydata the internet using a URL.
    3. ADD https://www.flamingspork.com/projects/libeatmydata/libeatmydata-105.tar.gz /opt/libeatmydata/
    4. RUN ls -lRt /opt/libeatmydat

    下面是输出:

    1. Sending build context to Docker daemon 422.9 kB
    2. Sending build context to Docker daemon
    3. Step 0 : FROM debian
    4.  —> c90d655b99b2
    5. Step 1 : RUN mkdir -p /opt/libeatmydata
    6.  —> Running in 6ac454c52962
    7. —> bdd948e413c1
    8. Removing intermediate container 6ac454c52962
    9. Step 2 : ADD  https://www.flamingspork.com/projects/libeatmydata/libeatmydata-105.tar.gz  /opt/libeatmydata/
    10. Downloading [==================================================>]
    11. 419.4 kB/419.4 kB
    12.  —> 9d8758e90b64
    13. Removing intermediate container 02545663f13f
    14. Step 3 : RUN ls -lRt /opt/libeatmydata
    15.  —> Running in a947eaa04b8e
    16. /opt/libeatmydata:
    17. total 412
    18. -rw——- 1 root root 419427 Jan  1  1970
    19. libeatmydata-105.tar.gz
    20.  —> f18886c2418a
    21. Removing intermediate container a947eaa04b8e
    22. Successfully built f18886c2418a

    如果你想添加一个压缩文件到镜像而不想解压,你可以使用COPY命令,这个命令与ADD命令类似,区别就是COPY命令不解压文件。

    Docker实践(23) – 找出容器IP

    虽然Docker命令让你能够访问有关镜像和容器的信息,但有时你想要了解这些Docker对象的内部元数据,如IP地址。

    问题

    你想找出容器IP地址。

    解决方法

    使用docker inspect命令获取和过滤容器元数据。

    讨论

    通过docker inspect命令能得到JSON格式的docker内部元数据。这会得到很多数据,所以下面只列出一部分。
    镜像的原始数据:

    1. $ docker inspect ubuntu | head
    2. [{
    3.     "Architecture": "amd64",
    4.     "Author": "",
    5.     "Comment": "",
    6.     "Config": {
    7.         "AttachStderr": false,
    8.         "AttachStdin": false,
    9.         "AttachStdout": false,
    10.         "Cmd": [
    11.             "/bin/bash"
    12. $

    你可以通过名称或ID来查看镜像和容器的信息。当然它们的元数据会有所不同 – 比如容器会有“state”字段,而镜像没有。
    你可以通过docker inspect命令,带一个format参数来找出容器的IP:

    1. docker inspect –format ‘{{.NetworkSettings.IPAddress}}’

    这个命令对于自动化可能会比较有用,而且这个命令获取的IP比通过其它docker命令要可靠得多。下面的命令是获取所有运行的容器的IP并尝试ping它们。

    1. $ docker ps -q | xargs docker inspect –format='{{.NetworkSettings.IPAddress}}’ | xargs -l1 ping -c1
    2. PING 172.17.0.5 (172.17.0.5) 56(84) bytes of data.
    3. 64 bytes from 172.17.0.5: icmp_seq=1 ttl=64 time=0.095 ms
    4. — 172.17.0.5 ping statistics —
    5. 1 packets transmitted, 1 received, 0% packet loss, time 0ms
    6. rtt min/avg/max/mdev = 0.095/0.095/0.095/0.000 ms

    SaltStack配置管理(6) – 管理文件

    在系统上获取正确的文件通常比安装正确的软件更有挑战性。salt有一个内置的文件服务器可以用来分发文件和目录到受控系统。

    SALT://

    你对放置在srv/salt目录的salt state文件应该熟悉了,不过你可能不知道的是,你放置在此目录中的任何其他文件和文件夹也可用于你的Salt minion。你可以在salt state文件中使用salt://引用srv/salt目录里的文件。

    FILE.MANAGED

    这个salt state函数允许你通过指定salt master上的源文件来管理本地文件。

    1. deploy the http.conf file:
    2.   file.managed:
    3.     – name: /etc/http/conf/http.conf
    4.     – source: salt://apache/http.conf

    由于源路径以salt://开始,我们可以推断出salt master源文件实际路径为/srv/salt/apache/http.conf。
    每一次应用这个salt state,salt会确保本地的文件与minion中的文件一致。这有助于确保应用程序在不同系统上配置相同。
    例如,如果你需要分发包含自定义设置限制下载速度的lftp全局配置文件,我们可以使用file.managed来实现:

    1. install network packages:
    2.   pkg.installed:
    3.     – pkgs:
    4.       – rsync
    5.       – lftp
    6.       – curl
    7.  
    8. copy lftp config file:
    9.   file.managed:
    10.     – name: /etc/lftp.conf
    11.     – source: salt://_tmpl_lftp.conf

    另一个选项,由于我们的配置文件只更改一行,所以我们可以使用file.append来简单地插入新的一行:

    1. install network packages:
    2.   pkg.installed:
    3.     – pkgs:
    4.       – rsync
    5.       – lftp
    6.       – curl
    7.  
    8. update lftp conf:
    9.   file.append:
    10.     – name: /etc/lftp.conf
    11.     – text: set net:limit-rate 100000:500000

    FILE.RECURSE

    这个salt state函数复制整个目录。

    1. copy some files to the web server:
    2.   file.recurse:
    3.     – name: /var/www
    4.     – source: salt://apache/www

    SaltStack配置管理(5) – JINJA

    salt引入了Jinja2模板引擎,可用于salt state文件,salt pillar文件和其它由salt管理的文件。
    salt允许你使用Jinja访问minion配置值,grains和salt pillar数据,和调用salt执行模块。这些是除了Jinja提供的标准控制结构和Python数据类型之外的功能。

    条件语句

    Jinja最常用的功能是在salt pillar文件中插入控制声明语句。
    由于许多发行版本有不同的包名称,你可以使用os grain来设置平台特定的路径,软件包名称和其它值。
    例如:

    1. {% if grains[‘os_family’] == ‘RedHat’ %}
    2. apache: httpd
    3. git: git
    4. {% elif grains[‘os_family’] == ‘Debian’ %}
    5. apache: apache2
    6. git: git-core
    7. {% endif %}

    如你所见,salt grains跟salt pillar的数据字典都是字典。这个示例检查salt grain值来设置操作系统特定的salt pillar键值。
    保存上面的代码到saltstack/pillar/common.sls文件,然后运行如下命令来刷新和列出每一个minions的salt pillar值:

    1. salt ‘*’ saltutil.refresh_pillar
    2. salt ‘*’ pillar.items

    设置这些值后,当应用如下salt state:

    1. install apache:
    2.   pkg.installed:
    3.     – name: {{ pillar[‘apache’] }}

    httpd软件包安装在RedHat,apache2安装在Debian系统。

    循环语句

    在salt state中创建多个用户和目录使用循环语句会很方便。

    1. {% for usr in [‘moe’,’larry’,’curly’] %}
    2. {{ usr }}:
    3.   user.present
    4. {% endfor %}
    1. {% for DIR in [‘/dir1′,’/dir2′,’/dir3’] %}
    2. {{ DIR }}:
    3.   file.directory:
    4.     – user: root
    5.     – group: root
    6.     – mode: 774
    7. {% endfor %}

    一般来说,你应该努力保持你的salt state足够简单。如果你发现你必须编写复杂的Jinja才能实现功能,你应该考虑把一个任务分割为多个salt state文件,或者编写一个自定义的salt执行模块。

    使用salt获取数据

    你可以在Jinja中调用salt执行函数来实时获取数据。

    1. {{ salt.cmd.run(‘whoami’) }}

    SaltStack配置管理(4) – 使用require声明salt state执行顺序

    执行顺序

    配置管理最重要的(和复杂的)方面之一是确保每个任务在正确的时间执行。
    默认情况下,salt state文件中的每个ID是按在文件中出现的顺序来执行。此外,在Top文件中,每个salt state文件是在列表的顺序来应用。例如下图中的ID是按salt state文件中在Top文件出现的次序来执行的。
    运维自动化
    通过组织Top文件中Salt state出现的顺序,可以在不显式定义依赖性的情况下控制执行顺序。

    require

    require可以显性地指定salt state中ID的依赖。如果你添加一个声明指示id1依赖id4,那么就首先应用id4。
    运维自动化
    你可以使用state.show_sls执行函数来查看salt state的执行顺序。例如:

    1. salt ‘minion1’ state.show_sls sls1[,sls2,…]

    查看examples.sls文件中salt state的执行顺序,使用如下命令:

    1. salt ‘minion1’ state.show_sls examples

    更多声明顺序方法

    还有几个其它用来控制执行顺序的声明。你可以在这里找到https://docs.saltstack.com/en/latest/ref/states/requisites.html

    SaltStack配置管理(3) – INCLUDE

    为了保持你的salt state模块化和可重用,每一个配置任务应该只在salt state树描述一次。如果你需要在多个地方使用同样的配置任务,你可以使用include指令。
    include的使用很简单。在你的state文件的顶部(任何ID的外部),使用如下格式添加include:

    1. include:
    2.   – sls1
    3.   – sls2

    其中sls1和sls2是你想include的sls文件名称。注意不需要添加.sls后缀。
    如果你想include的state文件在salt state树的子目录,可以使用小圆点(.)作为目录分隔符:

    1. include:
    2.   – dir.sls1

    include的state文件是插入到当前state文件的顶部,并且会首先处理。

    示例

    还记得之前下面的示例吗?

    1. sync directory using lftp:
    2.   cmd.run:
    3.     – name: lftp -c "open -u {{ pillar[‘ftpusername’] }},{{ pillar[‘ftppassword’] }}
    4.            -p 22 sftp://example.com;mirror -c -R /local /remote"

    这个salt state依赖lftp命令,所以最好是创建另一个salt state来确保lftp已经安装。然后我们可以使用include来连接它们。
    srv/salt/lftp.sls:

    1. install lftp:
    2.   pkg.installed:
    3.     – name: lftp

    srv/salt/dir-sync.sls:

    1. include:
    2.   – lftp
    3.  
    4. sync directory using lftp:
    5.   cmd.run:
    6.     – name: lftp -c "open -u {{ pillar[‘ftpusername’] }},{{ pillar[‘ftppassword’] }}
    7.            -p 22 sftp://example.com;mirror -c -R /local /remote"