《Docker 实战》阅读笔记 (Part2:镜像发布:如何打包软件)

** 7. 在镜像中打包软件
*** 1. 手动的镜像构建和练习
    1. [ ] 打包 Hello World 
       1. docker run --name container ... /bin/sh
       2. Docker 创建了一个新的容器和镜像的 UFS 挂载
       3. touch /HelloWorld.txt 
       4. 文件被拷贝到新的 UFS 文件层
       5. exit
       6. 容器被停止,用户返回到 host 终端上
       7. docker commit container image
       8. 一个名为 image 的新的仓库被创建
       9. docker images #输出结果的列表中包含 "image" 镜像
       
    2. [ ] 创建一个名为 hw_image 的新镜像
       docker run --name hw_container \
       ubuntu:latest \
       touch /HelloWorld

       docker commit hw_container hw_image

       docker rm -vf hw_container

       docker run --rm \
       hw_image \
       ls -l /HelloWorld

    3. 打包 git 
       docker run -it --name image-dev ubuntu:latest /bin/bash
       
       apt-get update //自己加的,先更新
       
       apt-get -y install git
       
       git version 
       
       1. 审查文件系统的改动
          docker diff image-dev 
          输出结果是一个非常行的文件改动列表
          以 A 开头的行表示文件被添加。以 C 开头表示修改,以 D 开头表示删除。安
          装 Git 会包含多个改动,不利于区分,因此,我们使用一些更加特殊的例子会
          更好理解些。
          
          docker run --name tweak-a busybox:latest touch /HelloWorld //添加新文
          件到 busybox 镜像中
          docker diff tweak-a

          docker run --name tweak-b busybox:latest rm  /bin/vi //从 busybox 镜像
          中移除现有文件
          docker diff tweak-b
          
          docker run --name tweak-c busybox:latest touch /bin/vi  //修改 busybox
          镜像中现有的文件
          docker diff tweak-c 
          
          清理容器
          docker rm -vf tweak-a 
          docker rm -vf tweak-b 
          docker rm -vf tweak-c 
          
       2. Commit --- 创建新镜像
          docker commit -a "@dockerinaction" -m "Added git" image-dev ubuntu-git  
          一旦提交了这个镜像,它就会显示在你计算机的已安装镜像列表中。运行
          docker images 
          
          可以从新镜像中创建一个容器,并且在其中测试 git 来确保新镜像正确工作
          docker run --rm ubuntu-git git version 
          
          docker run --rm ubuntu-git 
          运行这个命令时似乎什么都不会发生。这是因为你启动原始容器时附带的命令会
          被提交到新镜像中,而之前你启动创建新镜像的容器时附带的命令时 /bin/bash。
          因此,当你使用这个默认命令从新镜像中创建一个容器时,它会启动一个 shell
          并且立马停止它。显然,这并不是一个非常有用的默认命令

          设置入口点程序
          docker run --name cmd-git --entrypoint git ubuntu-git //显示标准的 git
          帮助命令,然后退出 

          docker commit  -m "Set CMD git" \
          -a "@dockerinaction" cmd-git ubuntu-git //提交新镜像并保持名字不变

          清除
          docker rm -vf cmd-git 
          docker run --name cmd-git ubuntu-git version //测试
          
          现在入口点被设置为 Git,用户再也不需要在最后输入 git  命令了。
    
       3. 可配置的镜像属性
          被记录进新镜像的有:
          所有的环境变量
          工作目录
          开放端口集合
          所有的卷定义
          容器入口点
          命令和参数
          
          如果这些值没有别明确地指定,那么这些值会从原始镜像继承

          明确指定了两个环境变量
          docker run --name rich-image-example \
          -e ENV_EXAMPLE1=Rich -e ENV_EXAMPLE2=Example \
          busybox:latest

          docker commit rich-image-example rie

          docker run --rm rie \
          /bin/sh -c "echo \$ENV_EXAMPLE1 \$ENV_EXAMPLE2"
          输出结果 Rich Example 

          第二个例子在前一个例子的容器上,明确地指定了入口点和命令
          docker run --name rich-image-example-2 \
          --entrypoint "/bin/sh" \
          rie \
          -c "echo \$ENV_EXAMPLE1 \$ENV_EXAMPLE2"   //设置默认命令

          docker commit rich-image-example-2 rie 
          
          docker run --rm rie
*** 2. 从打包的角度看待镜像
    1. [ ] 深入 Docker 镜像层
       1. 深入联合文件系统
          对已存在镜像的修改
          docker run --name mod_ubuntu ubuntu:latest touch /mychange

          docker run --name mod_busybox_delete busybox:latest rm /etc/profile 
          docker diff mod_busybox_delete
          输出结果
          C /etc
          D /etc/profile
          
          docker run --name mod_busybox_change busybox:latest touch /etc/profile 
          docker diff mod_busybox_change
          输出结果
          C /etc
          C /etc/profile
          
          docker commit mod_ubuntu 

          放入仓库,并且带有标签
          docker commit mod_ubuntu myuser/myfirstrepo:mytag 

          复制镜像
          docker tag myuser/myfirstrepo:mytag myuser/mod_ubuntu

          创建镜像会创建一个可写层,所有在可写层下面的层都是不可变的,这意味着它
          们永远不会被改变。这个特性使得共享镜像访问权变得更加可行,而不是为每个
          容器创建独立的副本。它也使得每层变得高度可复用。另一方面,当你对镜像进
          行改动时,你仅仅需要添加一个新的层,老的层永远不需要被改动。镜像不可避
          免地需要被改动,你需要意识到任何镜像的限制,并且将改动如何影响镜像大小
          牢记在心。

       2. 镜像体积和层数限制
          为老版本分配新标签,为新版本分配 latest 标签
          docker tag ubuntu-git:latest ubuntu-git:1.9 //创建新的标签:1.9

          构建新镜像的第一件事就是将 git 卸载
          docker run --name image-dev2 \
          --entrypoint /bin/bash \
          ubuntu-git:latest -c "apt-get remove -y git" //执行 bash 命 
          
          提交镜像 
          docker commit image-dev2 ubuntu-git:removed 
          
          重新分配标签
          docker tag  ubuntu-git:removed ubuntu-git:latest

          docker images
          
          联合文件系统可能有一个数量的限制。这个限制取决于文件系统,但42层限制在
          使用 AUFS 系统的计算机上是非常常见的。这个数字看起来很大,但并不是不能
          够达到的。
          docker history 命令查看镜像所有层,输出内容包括
          缩写的层ID 
          层的年龄
          创建容器时的初始命令
          这一层的全部文件大小          
          
*** 3. 扁平镜像
    1. [ ] 导出和导入扁平文件系统
       Docker 提供了两个命令来导入和导出文件归档(archives of files)
       docker export 命令会将扁平的联合文件系统的所有内容导出到标准输出或者一个
       压缩文件上。输出信息包含了所有从容器角度能够观察到的文件。

       创建一个新容器并且使用 export 子命令来获得新容器文件系统的扁平复制
       docker run --name export-test \
       dockerinaction/ch7_packed:latest ./echo For Export //导出文件系统

       docker export --output contents.tar export-test
       
       docker rm export-test 

       tar -tf contents.tar //显示归档内容

       docker import 命令会将压缩格式的内容导入到一个新镜像中。import 命令能够识
       别多种压缩或未压缩文件格式。在文件系统被导入的过程中,一个可选的 Dockfile
       指令也能被应用。导入文件系统是一个将最小文件集合导入到新镜像的简单方法
       
       hello-world.go 
       
       package main 
       import "fmt"
       func main() {
       fmt.Println("hello, world!")
       }
       
       docker run --rm -v "$(pwd)":/usr/src/hello  -w /usr/src/hello golang:1.3 go build -v 
       
       将这个程序(二进制文件)放到压缩文件中
       tar -cf static_hello.tar hello 

       使用 docker import 命令将压缩文件导入到镜像中
       docker import -c "ENTRYPOINT" [\"/hello\"]" - \ 
       dockerinaction/ch7_static < static_hello.tar  //通过 UNIX 管道将 tar 文件
       重定向
       
       上述命令中的 -c 选项来设置一个 Dockefile 命令。使用的命令设置了新镜像的入
       口点。Dockefile 命令的具体语法将在第 8 章讲述。在这个命令中更加有趣的是第
       一行最后的连字符-。这个连字符表示压缩文件的内容会通过标准输入导入。如果你
       不从本地文件系统获取压缩文件,而是从远程 Web 服务器抓取压缩文件,你也可以
       在这个位置指定一个 URL 来实现。
       你将生成的镜像标记为 dockerinaction/ch7_static_repository。花一些时间去研
       究它的结果
       
       docker run dockerinaction/ch7_static_repository //输出结果 hello,world
       docker history dockerinaction/ch7_static 

       这个镜像的历史只存在一层
              
*** 4. 镜像版本控制的最佳实践
    1. [ ] docker tag 是唯一一个能够将应用于已存在的镜像的命令

*** 5. 小结       
    1. 当使用 docker commit 命令提交容器时,新的镜像被创建
    2. 当你一个容器被提交,启动容器时的配置也会被编码进新镜像的配置文件中
    3. 一个镜像由多层以栈形式组成,且镜像由其中的最顶层来标识
    4. 镜像的磁盘大小就是组成镜像的层大小总和
    5. 可以使用 docker export 和 docker import 命令将镜像导出为压缩文件格式,或
       将压缩文件导入到镜像
    6. docker tag 命令能够被用来对同一个仓库赋予多个标签
    7. 仓库维护者应该保持标签的实用性,让用户更容易采用和迁移控制
    8. 将软件的最新稳定版本标记我latest
    9. 提供细粒度,重叠的标签,这有利于用户掌握软件的版本进展
** 8. 构建自动化和高级镜像设置
*** 1. 使用 Dockefile 自动化打包 
    1. [ ] Dockerfile 是一个文件,它由构建镜像的指令组成。指令由 Docker 镜像构建
       者自上而下排列,能够被用来修改镜像的任何信息。
       1. 使用 Dockefile 打包 git 
          # An example Dockerfile for installing Git on Ubuntu
          FROM ubuntu:latest
          MAINTAINER "[email protected]"
          RUN apt-get install -y git
          ENTRYPOINT ["git"]

          docker build --tag ubuntu-git:auto .
          
          分析 Dockfile 中的指令
          FROM ubuntu:latest --- 和手工创建类似,告诉 Docker 从最新的 Ubuntu 镜
          像创建新镜像
          
          MAINTAINER --- 设置镜像维护这的名字和邮箱。当用户遇到问题时,这些信息
          能够帮助这些人联系维护者。设置这些信息之前都是通过调用 commit 子命令来
          完成的
          
          RUN apt-get install -y git --- 告诉 Docker 运行该命令来安装 git 

          ENTRYPOINT ["git"] --- 将镜像的入口点设置为 git
          #开头的表示注解
          
          Dockerfile 唯一一条特殊的规则就是第一个指令必须是 FROM。如果你从一个空
          镜像开始,且想要打包的软件没有依赖,或者你能够自己提供所有的依赖,那么
          你可以从一个特殊的空镜像开始,它的名字就是 scratch 
          
*** 2. 元数据指令
     1. [ ] 元数据指令
          .dockerignore 文件中指定需要忽略的文件(复制文件到新镜像中需要忽略的文
          件)
          .dockerignore
          mailer-base.df 
          mailer-logging.df
          mailer-live.df 

          上面的内容会防止 .dockerignore 文件和名为 mailer-base.df ,
          mailer-logging.df, mailer-live.df 的文件在构建过程中被复制到新镜像中。

          每个 Dockefile 指令都会导致一个新层被创建。指令应该尽可能合并,这是因
          为构建程序不会镜像任何的优化。
          
          新建 mailer-base 文件,并复制一下内容到文件中
          
          FROM debian:wheezy 
          MAINTAINER Jeff Nickoloff "[email protected]"
          RUN groupadd -r -g 2000 example && \
              useradd -rM -g example -u 2200 example
          ENV APPROOT="/app" \
              APP="mailer.sh" \
              VERSION="0.6"
          LABEL base.name="Mailer Archetype" \
              base.version="${VERSION}"
          WORKDIR $APPROOT 
          ADD . $APPROOT
          ENTRYPOINT ["/app/mailer.sh"]   //这个文件不存在
          EXPOSE 33333
          #不要再基础镜像中设置默认用户,否则
          #接下来的实现将不能够更新镜像
          #USER example:example

          docker build -t dockerinaction/mailer-base:0.6 -f mailer-base.df .

          docker inspect 命令只能够被用来查看容器或镜像的元数据。
          docker inspect dockerinaction/mailer-base:0.6
          
*** 3. 文件系统指令
    1. [ ] 拥有自定义功能的镜像需要修改文件系统;COPY, VOLUME,ADD
        mailer-logging.df 文件内容
        FROM dockerinaction/mailer-base:0.6 
        COPY ["./log-impl", "${APPROOT}"]
        RUN chmod a+x ${APPROOT}/${APP} && \
            chown example:example /var/log 
        USER example:example 
        VOLUME ["/var/log"]
        CMD ["/var/log/mailer.log"]

        COPY 指令将会从镜像被创建的文件系统上复制文件到容器中。COPY 指令至少需要
       两个参数。最后一个参数是目的目录,其他所有参数则为源文件。这个指令只拥有
       一个意外的特性:任何被复制的文件的所有权都会被设置为 root 用户。无论在
       COPY 指令前面设置的默认用户是什么,这种情况都会发生。因此,最好在所有需要
       更新文的文件都复制到镜像后,再使用 RUN 指令来修改文件的所有权。
       
       和 ENTRYPOINT 等指令类似,COPY 指令同样支持 shell 格式和 exec 格式。但是
       如果任何一个参数包含了空格,那么你必须要使用 exec 格式。

       尽可能使用 exec(或字符串数组)格式是一个最佳实践。

       第二个指令时 VOLUME 。在字符串数组参数中的每一个值都会在产生的新层中被创
       建为一个新的卷定义。在镜像构建时定义卷比在运行时更加受到限制。你没有办法
       在 镜像构建时指定一个 绑定-挂载(bind-mount)卷或者只读卷。这个指令只能够
       在文件系统中创建一个指定的位置,然后将一个卷定义添加到镜像元数据中
       
       最后一个指令是 CMD, CMD 和 ENTRYPOINT 很相关 。它们都能够支持 shell 格式
       和 exec 格式,并且都能够被用来在容器中启动一个进程。
       
       CMD 指令表示入口点的一个参数列表。一个容器的默认入口点是 /bin/sh。如果一
       个容器的入口点没有被设置,这个默认值会被使用。

       mailer.sh 文件
       #!/bin/sh
       printf "Logging Mailer has started. \n"
       while true 
       do 
          MESSAGE=$(nc -l -p 33333)
          printf "[Message]: %s\n" "$MESSAGE" > $1
          sleep 1
      done
      
      使用下面的命令从包含 mailer-logging.df 文件的目录中构建 mailer-logging 镜
       像
       docker build -t dockerinaction/mailer-logging -f mailer-logging.df .

       构建邮件程序
       docker run -d --name logging-mailer dockerinaction/mailer-logging 
       
       mailer-live.df  的 Dockerfile 
       FROM dockerinaction/mailer-base:0.6
       ADD ["./live-impl", "${APPROOT}"]
       RUN apt-get update && \
           apt-get install -y curl python && \
           curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py" && \
           python get-pip.py && \
           pip install awscli && \
           rm get-pip.py && \
           chmod a+x "${APPROOT}/${APP}"
       RUN apt-get install -y netcat 
       USER example:example
       CMD ["[email protected]", "[email protected]"]

       ADD 指令类似于 COPY 指令,但它们有以下两点重要区别。 ADD 指令:
       1. 如果指定了一个 URL,会拉取远程源文件
       2. 会被判定为存档文件的源中的文件提取出来
          
        自动提取存档文件更为有用。使用 ADD 指令的远程拉取功能并不是一个好的实践。
        原因在于尽管这个特性非常方便,但是它没有提供任何机制来清理不被使用的文件,
        这会导致额外的层。作为替代品,你应该使用链状的 RUN 指令,就像
        mailer-live.df 的第三个指令。 CMD 有两个参数,分别指定了你要发送邮件的发
        件人和收件人。而 mailer-logging.df 仅仅指定了一个参数,这是它们的不同之处。

        在 mailer-live.di 文件的目录下面创建一个名为 live-impl 的子目录,并在这个
        子目录下,新建一个 mailer.sh 文件,内容如下
        #!/bin/sh 
        printf "Live Mailer has started. \n" 
        while true 
        do 
          MESSAGE=$(nc -l -p 33333)
          aws ses send-email --from $1 \
              --destination {\"ToAddress\":[\"$2\"]} \
              --message "{\"Subject\":{\"Data\":\"Mailer Alert\"}, \
                          \"Body\":{\"Text\":{\"Data\":\"$MESSAGE}\"}}}"
          sleep 1 
          done 
         
       docker build -t  dockerinaction/mailer-live -f mailer-live.df .
       docker run -d -name live-mailer dockerinaction/mailer-live 
      
       aws 程序需要设置指定的环境变量
       AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 和 AWS_DEFAULT_REGION 

       并不是所有的镜像都包含应用。有一些是作为下游镜像的平台而被构建的。这些情况
        能够从注入下游构建时(build-time) 行为的能力中收益

    2. [ ] 注入下游镜像在构建时发生的操作
       ONBUILD 指令
       如果生成的镜像被作为另一个构建的基础镜像,则 ONBUILD 指令定义了需要被执行
       的那些指令。举个例子,你可以使用 ONBUILD  指令来编译下游层提供的程序,上
       游的 Dockerfile 将构建目录的内容复制到一个已知目录,然后在这个目录中编译
       代码。上游的 Dockfile 一般会使用类似以下形式的指令
       ONBUILD COPY [".", "/var/myapp"]
       ONBUILD RUN go build /var/myapp

       跟随在 ONBUILD 后的指令不会再包含它们的 Dockerfile 被构建时被执行,这些指
       令会被记录再生成镜像的元数据 ContainerConfig.OnBuild 下。上面的指令将会产
       生以下元数据:
       "ContainerConfig": {
       "OnBuild": [
       "COPY [\".\", \"/var/myapp\"]",
              "RUN go build /var/myapp"
       ],
       ...
       
       这个元数据会一直被保留,直到生成的镜像被另外的 Dockefile 作为基础镜像。当
       一个校友的 Dockerfile 通过 FROM 指令使用了上游的镜像(带有 ONBUILD 指令的
       Dockerfile 产生的镜像),那么这些在 ONBUILD 后跟随的指令会在 FROM 指令后,
       下一条指令前被执行。
       
       ONBUILD指令被注入到构建中的例子如下
       上游 base.df 文件内容如下
       
       FROM busybox:latest
       WORKDIR /app 
       RUN touch /app/base-evidence 
       ONBUILD RUN ls -al /app 
       
       下游 downstream.df 内容如下
       
       FROM dockerinaction/ch8_onbuild 
       RUN touch downstream-evidence
       RUN ls -al .
       
       构建上游镜像
       docker build -t dockerinaction/ch8_onbuild -f base.df .

       构建下游镜像
       docker build -t dockerinaction/ch8_onbuild_down -f downstream.df .

       Docker Hub 中带有 onbuild 前缀的标签,部分镜像
       https://registry.hub.docker.com/_/python/
       https://registry.hub.docker.com/_/golang/
       https://registry.hub.docker.com/_/node/
      
*** 4. 多进程和持久的容器
    1. [ ] 使用启动脚本和多进程容器
       1. 验证环境相关的先决条件
          在软件设计领域,越早触发错误和先决条件检测都是最佳实践。这对镜像设计也
          是同样有效的。应该被检测的先决条件就是上下文的假设
          
          WordPress 镜像使用一个脚本作为容器的入口点。这个脚本验证了容器上下文配
          置是否和当前包含版本的 WordPress 兼容。如果任何需求没有被满足(一个链
          接没有被定义或者一个变量没有设置),那么这个脚本会在启动 WordPress 前
          退出,容器也会意外地停止
          
          为特定软件写一个脚本,验证先决条件,包含内容如下:
          1. 假定的链接(和别名)
          2. 环境变量
          3. 网络访问
          4. 网络端口可用性
          5. 根文件系统挂载参数(可读写或只读)
          6. 卷
          7. 当前用户
             
       2. shell 脚本验证一个程序,该程序依赖于一个 web 服务
          #!/bin/bash 
          set -e 
          
          if [ -n "$WEB_PORT_80_TCP"]; then 
            if [ -z "$WEB_HOST"]; then
              WEB_HOST='web'
            else 
             echo >$2 '[WARN]: Linked container, "web" overridden by $WEB_HOST.'
             echo >$2 "===》 Connecting to WEB_HOST ($WEB_HOST)"
            fi 
          fi 
          
          if [ -z "$WEB_HOST"]; then 
            echo >$2 '[ERROR]: specify a linked container, "web" or WEB_HOST
            environo-ment variable'
            exit 1
          fi 
          exec "$@" # run the default command

       3. 初始化进程
          使用 init 进程对于应用容器来说是最佳实践,但是并不存在一个适合所有情况
          的完美 init 程序。
          使用 init 程序需要考虑的因素
          1. init 程序会将额外的依赖带入到镜像中
          2. 文件大小
          3. init 程序如何将信号量传递到它的子进程(如果它做了的话)
          4. 需要的用户权限
          5. 监控和重启功能(backoff-on-restart 特性是加分项)
          6. 僵尸进程清理功能
             
*** 5. 可信的基础镜像
    1. [ ] 加固应用镜像
       加固一个镜像就是塑造镜像,使得基于这个镜像创建的任何 Docker 容器的攻击面
       减少的过程。
       加固应用镜像的一个通用策略是最小化包含在其中的软件。按照常理推断,包含越
       少的组件就能够减少潜在漏洞的数量。
       还有三件事能够用来加固镜像
       1. 你可以强制 基于某个特定的镜像来构建镜像。
       2. 你能够确保无论容器如何基于你的镜像来构建,它们都会拥有一个合适的默认用
          户
       3. 你应该去除 root 用户提权的通用途径
       
    2. 内容可寻址镜像标识符
       docker pull debian:jessie 
       #Output:
       #...
       #Digest: sha256:d5e87cfcb730...
       
       #Dockefile
       FROM debian@sha256:d5e87cfcb730...

       尽管这个不能直接限制 镜像的攻击面,但是使用 CAIID 能够防止镜像在你无意识
       的状态下被改动。       
       
*** 6. 用户相关的内容
*** 7. 降低镜像的攻击面
    1. [ ] 著名的容器逃离手段都依赖与获得容器中的管理员权限
       如果你过早的消减特权,那么活动用户(active user)可能没有特权来完成
       Dockerfile 的其它指令。举个例子,下面的 Dockerfile 将不能够被正确构建
       
       FROM busybox:latest
       USER 1000:1000 
       RUN touch /bin/busybox
       构建这个 Dockefile 将会在第2步失败,错误信息类似于
       touch:/bin/busybox:Permission denied。用户的改变明显地影响到了文件的访问
       权。在这个例子中,UID 1000 没有改动文件 /bin/busybox 的所有者的权限。那个
       文件当前的所有者是 root。将第二行和第三行对换一下就能够修复这个问题
       
       第二个关于时间的考虑就是运行时所需要的权限和能力(capability)。如果镜像
       在运行时启动了一个需要管理员权限的进程,那么在这个行为发生前将用户改为非
       root 用户是没有意义的。
       
       新建 UserPermissionDenied.df 文件
       FROM busybox:latest
       USER 1000:1000 
       ENTRYPOINT ["nc"]
       CMD ["-l", "-p", "80","0.0.0.0"]
       
       构建这个 Dockefile 生产性镜像,并且使用这个镜像创建一个容器,在这个例子,
       UID 为 1000 的用户将会缺少需要的权限,导致命令失败:
       docker build -t dockerinaction/ch8_perm_denied -f UserPermissionDenied.df .
       
       docker run dockerinaction/ch8_perm_denied
       #输出结果:
       #nc: bind: Permission denied 
       
       能够确定的事情就是使用常见的或系统级别的 UID/GID 是不合适的。直接使用原始
       数字会降低脚本和 Dockefile 可读性。因此,较为经典的做法是使用 RUN 指令创
       建镜像所要使用的用户和用户组。下面的内容就是 Postgres Dockefile 的第二个
       指令:
       # 首先,添加我们自己的用户和用户组,以此确保它们的 ID 一直存在
       # 无论添加了哪些依赖
       RUN groupadd -r postgres && useradd -r -g postgres postgres 
       
       这个指令简单地创建了一个 postgres 用户和用户组,它们的 UID 和 GID 都是自
       动生成的。这条指令在早期就放入到 Dockefile 中,因此在重新构建的过程中它的
       内容总是能够被缓存,并且不管构建过程中其他被添加进来的用户,这些 ID 将会
       保持一致。然后这些用户和用户组就能够在 USER 指令中使用了。

    2. SUID 和 SGID 权限
       最后一个加固方法就是缓解 SUID 和 SGID 的权限。

       FROM ubuntu:latest
       # 设置 whoami 程序的 SUID 位
       RUN chmod u+s /usr/bin/whoami
       # 创建一个 example 用户,并且将它设置为默认用户
       RUN adduser --system --no-create-home --disabled-password  --disabled-login \
         --shell /bin/sh example 
       USER example 
       #设置默认命令,比较容器用户和
       #执行 whoami 程序的有效用户
       CMD printf "Container runing as:      %s\n" $(id -u -n) && \
           printf "Effectively running whoami as:%s\n" $(whoami) 
       
       docker build -t dockerinaction/ch8_whoami 
       docker run dockerinaction/ch8_whoami

       运行一个快速的查找就能够知道拥有这些权限的文件有多少,分别是什么
       docker run --rm debian:wheezy find / -perm +6000 -type f
       输出结果 
       /usr/bin/wall
      /usr/bin/chsh
      /usr/bin/chfn
     /usr/bin/expiry
     /usr/bin/gpasswd
     /usr/bin/newgrp
     /usr/bin/passwd
     /usr/bin/chage
     /usr/lib/pt_chown
     /sbin/unix_chkpwd
     /bin/ping
     /bin/umount
     /bin/ping6
     /bin/mount
     /bin/su

       下面的命令将会找出所有的 SGID 文件 
       docker run --rm debian:wheezy find / -perm +2000 -type f
       
       输出结果
       /usr/bin/wall
       /usr/bin/expiry
       /usr/bin/chage
       /sbin/unix_chkpwd

       每个列出的文件在这个具体的镜像中都拥有 SGID 或 SUID 权限

       将所有文件的 SUID 和 SGID 的权限都去除
       RUN for i in $(find / -type f (-perm +6000 -o -perm +2000)); do chmod ug-s $i; done

  
       

       
*** 8. 小结
    1. Docker 提供了一个镜像自动化构建程序,它会从 Dockerfile 中读取指令来构建镜
       像。
    2. 每一个 Dockerfile 指令都会创建一个镜像层
    3. 尽可能地合并指令,这样能够减少镜像的大小和层的数量
    4. Dockefile 包含了能够设置镜像元数据的指令,比如默认用户,开发端口,默认命令
       和入口点
    5. 其他的 Dockefile 指令能从本地文件系统或远程目录复制文件到构建的镜像中
    6. 下游的构建会继承上游 Dockefile 中 ONBUILD 指令设置的构建触发
    7. 启动脚本应该用来在启动主要应用前验证容器的执行上下文
    8. 一个有效的执行上下文应该拥有正确的环境变量集合,网络依赖的可用性和一个合
       适的用户配置
    9. init 程序 能够被用来启动多个进程,监控这些进程,清除孤立的进程和转发信号量
       到子进程中。
    10. 应该使用内容可寻址镜像标识符,创建非 root 的默认用户和禁止或去除任何带有
        SUID 和 SGID 权限的可执行文件来加固镜像

** 9. 公有和私有软件分发
*** 1. 选择一个项目的分发方法
    1. 分发选项图谱
       选择分发方式的参考因素:
       1. 成本
       2. 可见性
       3. 传输速度和带宽开销
       4. 生命周期控制
       5. 可用性控制
       6. 访问控制
       7. 产品完整性
       8. 产品保密性
       9. 必要的专业知识
          
*** 2. 使用托管基础设施
    1. [ ] 通过托管 Registry 发布
       一个托管 Registry 是一个由第三方供应商拥有和运营的 Docker Registry 服务。
       Docker Hub,Quay.io, Tutum.co 和 GoogleContainer Registry 都是托管
       Registry 供应商的例子。
    2. 通过公有仓库发布:你好! Docker Hub
       HelloWorld.df 
       FROM busybox:latest 
       CMD echo Hello World 
       
       构建新镜像
       docker build \
       -t cnddydocker/hello-dockerfile \
       -f HelloWorld.df \ .
       
    3. Docker Hub 登录认证
       docker login 
       --username , --email, --password 

       docker login
       Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
       Username:
       Password: 
       WARNING! Your password will be stored unencrypted in /home/yuandd/.docker/config.json.
       Configure a credential helper to remove this warning. See
       https://docs.docker.com/engine/reference/commandline/login/#credentials-store

       Login Succeeded
       
       docker push /hello-dockerfile:latest
       
       The push refers to repository [docker.io//hello-dockerfile]
       1da8e4c8d307: Mounted from library/busybox 
       latest: digest: sha256:a68eebcfaa57ef89de926305c89fe0d67deee5a40367d13ff812747a6c84ec56 size: 527
       
*** 3. 运行和使用你自己的 Registry
    1. [ ] 私有托管仓库
       先执行 docker login 
       登录成功之后再
       docker run 或 docker pull 远程私有仓库镜像
       
       docker login (默认登录 docker hub)
       docker login tutum.co 
       docker login quay.io

    2. [ ] 私有 Registry 介绍
       Docker Registry 软件(称之为 Distribution)是开源软件并且按照 Apache2 许
       可证分发。这款软件的可用性和宽容的许可证让运行自己的 Registry 的工程成本
       非常低廉。可以通过 Docker Hub 运行,易于在非生产环境下使用。
       如果你有类似如下的特殊的基础设施使用案例,那么运行一个私有的 Registry 是
       很好的:
       区域镜像缓存
       团队特定的镜像分发位置或可见性
       环境或者部署特定阶段的镜像池
       公司的镜像审批流程
       外部镜像的生命周期控制

    3. [ ] 使用 Registry 镜像
       开始使用 Docker Registry 软件是很容易的
       在 Docker Hub 的名为 registry 的仓库有可用的分发软件,可以用一个单独的命
       令在容器中启动一个本地 registry:
       docker run -d -p 5000:5000 \
         -v "$(pwd)"/data:/tmp/registry-dev \
         --restart=always --name local-registry registry:2
         
       如果想了解 Registry 是怎么工作的,可以考虑一下将镜像从 Docker Hub 复制到
       你的新 Registry 的工作流程:
       
       从 Docker Hub 拉取 demo 镜像
       docker pull dockerinaction/ch9_registry_bound 
       
       通过标签过滤器验证镜像可发现
       docker images -f "label=dia_excercise=ch9_registry_bound"

       推送 demo 镜像到你的私有 registry 
       docker tag dockerinaction/ch9_registry_bound \
       localhost:5000/dockerinaction/ch9_registry_bound 

       docker push localhost:5000/dockerinaction/ch9_registry_bound 

       在运行这四个命令时,你从 Docker Hub 复制一个示例仓库到你的本地 Registry。
       如果你从启动 Registry 的同一位置开始执行这些命令,你会发现新创建的数据子
       目录包含新的 Registry 数据。

    4. [ ] 从 Registry 使用镜像
       从你的 Docker Damon 本地缓存删除示例仓库来展示它们消失了,然后重新从你的
       个人 Registry 安装:
       docker rmi \
       dockerinaction/ch9_registry_bound   //移除标记的引用
       
       再次从 registry 拉取
       docker images -f "label=dia_excercise=ch9_registry_bound"
       
       docker pull localhost:5000/dockerinaction/ch9_registry_bound 
       
       docker images -f "label=dia_excercise=ch9_registry_bound"//演示镜像又回来
       了。

       docker rm -vf local-registry
     
*** 4. 理解镜像手动分发流程
    1. [ ] 镜像的手动发布和分发
       Docker 文件 -> docker build -> 当地镜像缓存 -> docker save/docker export
       -> .tar  -> 上传 -> SFTP server/Blob storage/Web server/Email server/Usb
       key -> 下载 -> .tar -> docker load/docker import -> 当地镜像缓存 ->
       docker run -> 容器

    2. 使用文件传输协议的分发基础设施示例 
       1. FTP 发布基础设施 
          本地镜像缓存 -> docker save -> .tar -> 上传 -> FTP 服务器

       2. docker pull registry:2 
          
          docker run -d --name ftp-transport -p 21:12 dockerinaction/ch9_ftpd          
          这个命令将会启动一个在 TCP 端口21(默认端口)上允许 FTP 连接的 FTP 服务,
          不要在生成环境使用这个镜像。这个服务将被配置为允许匿名并在
          pub/incoming 目录下写入访问,你的分发基础设备将会使用这个目录作为镜像
          分发接入点
          
          导出文件格式的镜像
          docker save -o ./registry.2.tar registry:2 
          
          dockerinaction/ch9ftpclient 镜像有一个安装好的 ftp 客户端,可以用来上
          传你的新镜像到你的 ftp 服务器。

          docker run --rm --link ftp-transport:ftp_server \
          -v "$(pwd)":/data \
          dockerinaction/ch9_ftp_client \
          -e 'cd pub/incoming; put registry.2.tar; exit' ftp_server

          查看ftp 服务器目录
          docker run --rm --link ftp-transport:ftp_server \
          -v "$(pwd)":/data \
          dockerinaction/ch9_ftp_client \
          -e "pwd; cd pub/incoming; ls; exit" ftp_server
          
          使用 registry 镜像从 FTP 服务器获得客户端如何集成的信息
          1, 从你的本地镜像缓存中删除 registry 镜像,并从你的本地目录删除文件
          
          首先要移除任何 registry 
          rm registry.2.tar 
          docker rmi registry:2 
          
          然后从你的 FTP 服务器下载镜像文件
          docker run --rm --link ftp-transport:ftp_server \
          -v "$(pwd)":/data \
          dockerinaction/ch9_ftp_client \
          -e 'cd pub/incoming; get registry.2.tar; exit' ftp_server

          docker load 命令重新加载镜像到你的本地镜像缓存
          docker load -i registry.2.tar

          这是一个有关镜像手动发布和分发的基础设施如何搭建的最小的例子,借助于一
          些扩展,你可以搭建一个符号生产环境质量 要求的基于 FTP 的分发中心。
          
*** 5. 分发镜像资源
    1. [ ] 镜像源代码分发流程
       当分发镜像源代码而不是镜像时,你可以关闭所有的 Docker 分发工作流程,仅仅
       依靠 Docker 镜像构建器,镜像手动发布和分发时,源代码分发工作流程应该按照
       一个特定实现内容的选择标准来评估
       
       在某种程度上,源代码分发的工作流程是那些镜像手动发布和分发工作流程关注问
       题的超集。你必须构建自己的工作流,但是没有 docker save,load,export 或者
       import 命令的帮助。生成者需要确定他们如何打包他们的源代码,消费者需要了解
       这些源代码是如何打包的,就像他们自己如何构建一个镜像一样。这些扩展接口使
       源代码分发的工作流程成为最有弹性 的和潜在的最复杂的分发方法。

    2. [ ] 在 GitHub 上使用 Docker 来分发一个项目
       git init
       git config --global user.email "[email protected]"
       git config --global user.name "Your Name"
       git add Dockerfile 
       # git add  *whatever other files you need for the image*
       git commit -m "firt commit"
       git remote add origin https://github.com//.git
       git push -u origin master
       
       与此同时,消费者会使用这样的一组通用命令集 
       git clone https://github.com//.git
       cd
       docker build -t / .
       
       镜像源代码分发与所有的 Docker 分发工具是脱离的,仅仅依靠镜像构建器,你可
       以采用任何可用的分发工具集。如果你为分发或源代码版本控制锁定了一个特定的
       工具集,这也许是唯一的符合标准的选择。
       
*** 6. 小结
    1. 有一个选择的选项图谱显示了你的选择范围
    2. 你应该总是使用一组一致的选择标准,以评估你的分发选择并确定应该使用哪个方
       法
    3. 托管的公有仓库提供了出色的项目可见性,是免费的,并且只需要非常少的经验就
       可以采用
    4. 应为镜像是由一个受信任的第三方构建的,所以消费者将对其自动构建产生的镜像
       有更高程度的信任
    5. 托管的私有仓库对于小型团队是划算的,提供了令人满意的访问控制
    6. 运行自己的 Registry 使你能够构建适合特殊使用案例的基础设施,并且不需要放
       弃 Docker 的分发设施。
    7. 将镜像分发为文件,可以用任何文件共享系统来完成
    8. 镜像源代码分发是非常弹性的,但是在你运用的时候会非常复杂,使用流行的源代
       码分发工具和模式会使事情变得简单。
       
** 10. 运行自定义 Registry 
*** 1. 直接使用 Registry API
    1. [ ] 运行个人 Registry
       个人 Registry 很少需要定制,可以使用官方镜像
       docker run -d --name personal_registry \
       -p 5000:5000 --restart=always \
       registry:2
       
       当你连接到 Registry ,你需要显示地声明 Registry 运行的端口
       你从 Registry 镜像启动的容器将会存储你发送到此的仓库数据到一个挂载点为
       /var/lib/registry 的管理卷,这意味着你不必担心数据会被存储到镜像的主分层
       系统。
       
       打标签并推送一个镜像到这个 Registry 
       docker tag registry:2 localhost:5000/distribution:2
       docker push localhost:5000/distribution:2 

       删掉本地镜像缓存
       docker rmi localhost:5000/distribution:2
       docker pull localhost:5000/distribution:2
       
    2. [ ] 介绍 V2 API 
       Registry V2 的 API 是 RESTful 风格的,如果你不熟悉 RESTful API, 只要知道
       RESTful API 是一个遵守文本传输协议(HTTP) 以及按照 HTTP 协议原语来访问和
       操作远程资源来使用的就足够了。
       
       curl.df 
       FROM gliderlabs/alpine:latest 
       LABEL source=dockerinaction 
       LABEL category=utility 
       RUN apk --update add curl
       ENTRYPOINT ["curl"]
       CMD ["--help"]
       
       docker build -t dockerinaction/curl -f curl.df .
       通过这个新的 dockerinaction/curl 镜像,你可以执示例中的 cURL 命令,而不用
       担心 cURL 是否需要在你计算机上安装或安装什么版本。
       给正在运行的 Registry 发送一个简单的请求来开始使用 Registry API
       
       docker run --rm --net host dockerinaction/curl -Is http://localhost:5000/v2/
       响应结果 
       HTTP/1.1 200 OK
       Content-Length: 2
       Content-Type: application/json; charset=utf-8
       Docker-Distribution-Api-Version: registry/2.0
       X-Content-Type-Options: nosniff
       Date: Thu, 14 Nov 2019 09:23:22 GMT
       
       这个命令用来验证 Registry 运行的是 V2 API,并在 HTTP 响应头部返回特定的
       API 版本,请求的最后组成部分/v2/,是每一个基于 V2 API 的资源的前缀
       
       在 Registry 里的分发仓库里面获得标签列表
       docker run --rm -u 1000:1000 --net host \
       dockerinaction/curl -s http://localhost:5000/v2/distribution/tags/list
       
       响应结果
       {"name":"distribution","tags":["2"]}
       
       docker tag \
       localhost:5000/distribution:2 \
       localhost:5000/distribution:two  //创建 tag 名称 
       
       docker push localhost:5000/distribution:two 
       
       docker run --rm \
       -u 1000:1000 \  //以非特权模式运行
       --net host \  // 以无 network 命名空间方式运行
       dockerinaction/curl \
       -s http://localhost:5000/v2/distribution/tags/list 
       
       响应结果
       {"name":"distribution","tags":["2","two"]}
       
    3. [ ] 定制镜像
       关键组件
       registry 的基础镜像时基于 Debian 的,已经更新了依赖关系
       主程序被命名为 registry ,并在 PATH 路径上可用
       默认的配置文件为 config.yml
       
       Debian 有一个对于分发功能齐全的最小封装,只需要占用大约 125M 硬盘空间,它
       还附带了一个广受欢迎的包管理器,所以安装或升级依赖关系已经不是一个问题了。
       主程序命名为 registry ,并且设置为镜像的 Entrypoint,这意味着从镜像启动一
       个容器时,你可以省略任何命令行参数获得默认行为,或者直接添加自己的参数到
       docker run 命令的后面部分
       
       config.yml 是本章的核心,该配置文件包含了9个顶级字段,每个字段定义了
       Registry 的主要功能组件
       1. version  这是一个必需的字段,指定了配置版本(不是软件版本)
       2. log 本节中的这个配置控制由分发项目产生的日志输出
       3. storage 存储配置控制在何处,以及如何进行镜像存储和维护
       4. auth 这个配置控制 Registry 中身份认证机制
       5. middleware 中间件配置是可选的,它用于配置存储,注册表或者使用中的仓库
          中间件
       6. reporting 某些报告工具已经整合到了分发项目里,这些工具包括 Bugsnag 和
          NewRelic,这个字段配置这些工具集
       7. http 这一字段指定分发系统应用如何在网络上可用
       8. notification 最后再 redis 字段中提供 redis 缓存的配置
          
*** 2. 搭建一个中央 Registry
    1. 集中式 Registry 的增强
       映射 Registry 容器端口到正在运行(docker run ... -p 80:5000 ...)的计算机
       网络接口的80端口。

    2. [ ] 创建一个反向代理 
       你的反向代理配置将包括两个容器,第一个运行 Nginx 反向代理,第二个运行你的
       Registry,反向代理容器将会通过别名 registry 链接到主机上的 Registry 容器。
       创建一个名为 basic-proxy.conf 的新文件,包含如下的配置
       
       basic-proxy.conf
       upstream docker-registry {
       server registry:5000;#链接别名需求
       }
       server {
       listen 80;
       # Use the localhost name for testing purposes server_name localhost;
       # A real deployment would use the real hostname where it is deployed
       # server_name mytotallyawesomeregistry.com;
       
       client_max_body_size 0;
       chunked_transfer_encoding on;
       #We're going to forward all traffic bound for the registry 
       location /v2/ {#注意 v2 前缀
       #Upstream 解析
       proxy_pass     http://docker-registry;
       proxy_set_header Host            $http_host;
       proxy_set_header X-Real-IP       $remote_addr;
       proxy_set_header X-Forward-For   $proxy_add_x_forwarded_for;
       proxy_set_header X-Forward-Rroto $scheme;
       proxy_read_timeout               900;
       }
       }
       
       basic-proxy.df
       FROM nginx:latest 
       LABEL source=dockerinaction
       LABEL category=infrastructure
       COPY ./basic-proxy.conf /etc/nginx/conf.d/default.conf

       docker build -t dockerinaction/basic_proxy -f basic-proxy.df .
       
       启动反向代理 
       docker run -d --name basic_proxy -p 80:80 \
       --link personal_registry:registry \
       dockerinaction/basic_proxy 
       
       通过反向代理运行 cURL 命令查询你的 Registry 

       docker run --rm -u 1000:1000 --net host \
       dockerinaction/curl \
       -s http://localhost:80/v2/distribution/tags/list

    3. [ ] 在反向代理上配置 HTTPS (TLS)
       客户端使用这样的命令行创建隧道
       ssh -f -i my_key user@ssh-host -L 4000:localhost:5000 -N 
       
       在代理添加一个 HTTP (TLS) 端点
       1. 生成私钥和公钥对以及自签名证书。没有 Docker 的话,你需要 Docker 的话,
          你需要安装 OpenSSL 并运行三个复杂的命令。有了 Docker ,以及一个由
          CenturyLink 创建的公有镜像,你可以用一个命令做整件事情:
          docker run --rm -e COMMON_NAME=localhost -e KEY_NAME=localhost \
          -v "$(pwd)":/certs centurylink/openssl 

          此命令将生成一个 4096 比特位的 RSA 密钥对,并且在你当前的工作 目录中存
          储私钥文件和自签名证书。镜像在 Docker Hub 中公开可用的,并且由自动化构
          建维护。它是完全可审核的,所以必要时越是偏执,就越是可用免费验证(或重
          建)镜像的。在创建的三个文件中,你将使用两个,第三个是证书签名请求
          (CSR),可以被删掉。

       2. 创建反向代理配置文件,新建 tls-proxy.conf 文件
          upstream docker-registry {
          server registry:5000;
          }
          server {
          listen 443 ssl;#注意端口 443 和 SSL 的使用
          server_name localhost; #命令为 localhost
          client_max_body_size 0;
          chunked_transfer_encoding on;
          
          ssl_certificate  /etc/nginx/conf.d/localhost.crt;
          ssl_certificate_key //etc/nginx/conf.d/localhost.key;
          
          location /v2/ {
          proxy_pass                             http://docker-registry;
          proxy_set_header  Host                 $http_post;
          proxy_set_header  X-Real-IP            $remote_addr;
          proxy_set_header  X-Forwarded-For      $proxy_add_x_forwarded_for;
          proxy_set_header  X-Forwarded-Proto    $scheme;
          proxy_read_timeout                     900;
          }
          
          }
          
          新建 tls-proxy.df 文件
          FROM nginx:latest
          LABEL source=dockerinaction
          LABEL category=infrastructure
          COPY ["./tls-proxy.conf", \
                "./localhost.crt", \
                "./localhost.key",\
                "/etc/nginx/conf.d/"]

          构建新镜像 
          docker build -t  dockerinaction/tls_proxy -f tls-proxy.df .

          使用 curl 测试 
          docker run -d --name tls-proxy -p 443:443 \
          --link personal_registry:registry \
          dockerinaction/tls_proxy 
          
          docker run --rm \
          --net host \
          dockerinaction/curl -ks \
          https://localhost:443/v2/distribution/tags/list
          
          本示例中的 curl 使用了 -k 选项,该选项指示 curl 忽略请求端点的任何证书
          错误,在使用一个自签名证书的场景下需要使用这个选项,处理这个细微差别外,
          你可以通过 HTTPS 成功地发出对 Registry的请求

*** 3. Registry 认证工具
     1. [ ] 有三种机制进行身份认证:silly,token和htpasswd。
        在反向代理层配置各种不同的身份认证机制
        
        silly:是完全不安全的,应该被忽略掉,它仅仅适用于开发目的
        token:使用 JSON web Token(JWT),这是与 Docker Hub 相同的认证机制。使用此
        机制要求你部署一个单独的身份认证服务
        htppasswd: 是以一个 Apache web 服务器附带的工具命名的开源项目。htppasswd
        用于生成编码后的用户名和密码对,其中密码已经用 bcrypt 算法进行了加密。采
        用 htppasswd 身份认证方式时,你应该意识到从客户端发送到你的 Registry 的
        密码时未加密的,这叫做 HTTP 基本身份认证。

        有两种方法可以添加 htppasswd 认证到你的 Registry ,分别在反向代理层和
        Registry 本身,这两种情况你都需要使用 htppasswd 创建一个密码文件。
        
        下面使用 Docker 安装 htpasswd 
        htpasswd.df 
        FROM debian:jessie 
        LABEL source=dockerinaction 
        LABEL category=utility 
        RUN apt-get update && \
            apt-get install -y apache2-utils 
        ENTRYPOINT ["htpasswd"]
        
        构建镜像 
        docker build -t htpasswd -f htpasswd.df .
        
        为密码文件创建新条目
        docker run -it --rm htpasswd -nB
        
        创建 tls-auth-proxy.conf 
        #filename: tls-auth-proxy.conf 
        upstream docker-registry {
        server registry:5000;
        }
        
        server {
        listen 443 ssl;
        server_name localhost;

        client_max_body_size 0;
        chunked_transfer_encoding on;
        
        #SSL 
        ssl_certificate /etc/nginx/conf.d/localhost.crt;
        ssl_certificate_key /etc/nginx/conf.d/localhost.key;
        
        location /v2/ {
        auth_basic "registry.localhost";
        auth_basic_user_file /etc/nginx/conf.d/registry.password;
        
          proxy_pass                             http://docker-registry;
          proxy_set_header  Host                 $http_post;
          proxy_set_header  X-Real-IP            $remote_addr;
          proxy_set_header  X-Forwarded-For      $proxy_add_x_forwarded_for;
          proxy_set_header  X-Forwarded-Proto    $scheme;
          proxy_read_timeout                     900;
        
        }
        }

        新建一个 tls-auth-proxy.df 
        FROM nginx:latest 
        LABEL source=dockerinaction
        LABEL category=infrastructure
        COPY ["./tls-auth-proxy.conf", \
                "./localhost.crt", \
                "./localhost.key",\
                "./registry.password", \
                "/etc/nginx/conf.d/"]

        docker run -d --name tls-auth-proxy -p 443:443 \
          --link personal_registry:registry \
          dockerinaction/tls_auth_proxy 

        curl 请求会响应401
        
        添加 TLS 和 HTTP 基本身份认证到另外一个默认的分发容器中 
        tls_auth_registry.yml

        version: 0.1
        log:
            level: debug
            fields:
               service: registry
               environment: development
        storage:
           filesystem:
               rootdirectory: /var/lib/registry 
           cache:
               layerinfo: inmemory 
           maintenance:
               uploadpurging:
                      enabled: false 
        http:
           addr: :5000
           secret: asecretforlocaldevelopment
           tls:
               certificate: /localhost.crt 
               key: /localhost.key 
           debug:
               addr: localhost:5001
        auth:
           htpasswd:
               realm: registry.localhost
               path: /registry.password

         新建 tls-auth-registry.df
         #Filename: tls-auth-registry.df
         From registry:2
         LABEL source=dockerinaction
         LABEL category=infrastructure
         # Set the default argument to specify the config file to use 
         #Setting it early will enable layer caching if the 
         #tls-auth-registry.yml changes.
         CMD ["/tls-auth-registry.yml", \
              "./localhost.crt", \
              "./localhost.key", \
              "./registry.password",\
              "/"]

         docker build -t dockerinaction/secure_registry -f tls-auth-registry.df .
         
         docker run -d --name secure_registry \
           -p  5443:5000 --restart=always \
           dockerinaction/secure_registry

     2. 客户端兼容性
        1. 创建一个 Nginx 配置文件 (dual-client-proxy.conf)
        2. 创建一个简洁的 Dockefile (dual-client-proxy.df)
        3. 构建一个新的镜像
        
        dual-client-proxy.conf 
        
        upstream docker-registry-v2 {
        server registry2:5000;
        }
        
        upstream docker-registry-v2 {
        server registry1:5000;
        }
        
        server {
        listen 80;
        server_name localhost;
        
        client_max_body_size 0;
        chunked_transfer_encoding on;
        
        location /v1/ {
        proxy_pass     http://docker-registry-v1;
        proxy_set_header Host            $http_host;
        proxy_set_header X-Real-IP       $remote_addr;
        proxy_set_header X-Forward-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forward-Rroto $scheme;
        proxy_read_timeout               900;
        }

        location /v2/ {
        proxy_pass     http://docker-registry-v2;
        proxy_set_header Host            $http_host;
        proxy_set_header X-Real-IP       $remote_addr;
        proxy_set_header X-Forward-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forward-Rroto $scheme;
        proxy_read_timeout               900;
        }
        }
        
        新建 dual-client-proxy.df 文件
        FROM nginx:latest
        LABEL source=dockerinaction
        LABEL category=infrastructure
        COPY ./dual-client-proxy.conf /etc/nginx/conf.d/default.conf 

        docker build -t dual_client_proxy -f dual-client-proxy.df .
        
        需要运行一个 V1 Registry 
        docker run -d --name registry_v1 registry:0.9.1
        
        docker run -d --name dual_client_proxy \
        -p 80:80 \
        --link personal_registry:registry2 \
        --link registry_v1:registry1 \
        dual_client_proxy 
        
        docker run --rm -u 1000:1000 \
        --net host \
        dockerinaction/curl -s http://localhost:80/v1/_ping 
        响应结果
        {"host": ["Linux", "c1cf7bb35be3", "4.15.0-66-generic", "#75-Ubuntu SMP
        Tue Oct 1 05:24:09 UTC 2019", "x86_64", "x86_64"], "launch":
        ["/usr/local/bin/gunicorn", "--access-logfile", "-", "--error-logfile",
        "-", "--max-requests", "100", "-k", "gevent", "--graceful-timeout",
        "3600", "-t", "3600", "-w", "4", "-b", "0.0.0.0:5000", "--reload",
        "docker_registry.wsgi:application"], "versions":
        {"M2Crypto.m2xmlrpclib": "0.22", "SocketServer": "0.4", "argparse":
        "1.1", "backports.lzma": "0.0.3", "blinker": "1.3", "cPickle": "1.71",
        "cgi": "2.6", "ctypes": "1.1.0", "decimal": "1.70", "distutils":
        "2.7.6", "docker_registry.app": "0.9.1", "docker_registry.core":
        "2.0.3", "docker_registry.server": "0.9.1", "email": "4.0.3", "flask":
        "0.10.1", "gevent": "1.0.1", "greenlet": "0.4.9", "gunicorn": "19.1.1",
        "gunicorn.arbiter": "19.1.1", "gunicorn.config": "19.1.1",
        "gunicorn.six": "1.2.0", "jinja2": "2.8", "json": "2.0.9", "logging":
        "0.5.1.2", "parser": "0.5", "pickle": "$Revision: 72223 $", "platform":
        "1.0.7", "pyexpat": "2.7.6", "python": "2.7.6 (default, Jun 22 2015,
        17:58:13) \n[GCC 4.8.2]", "re": "2.2.1", "redis": "2.10.3", "requests":
        "2.3.0", "requests.packages.chardet": "2.2.1",
        "requests.packages.urllib3": "dev",
        "requests.packages.urllib3.packages.six": "1.2.0", "requests.utils":
        "2.3.0", "simplejson": "3.6.2", "sqlalchemy": "0.9.4", "tarfile":
        "$Revision: 85213 $", "urllib": "1.17", "urllib2": "2.7", "werkzeug":
        "0.11.3", "xml.parsers.expat": "$Revision: 17640 $", "xmlrpclib":
        "1.0.1", "yaml": "3.11", "zlib": "1.0"}}
        
        docker run --rm -u 1000:1000 \
        --net host \
        dockerinaction/curl -Is http://localhost:80/v2/
        响应结果 
        HTTP/1.1 200 OK
        Server: nginx/1.17.5
        Date: Fri, 15 Nov 2019 07:56:05 GMT
        Content-Type: application/json; charset=utf-8
        Content-Length: 2
        Connection: keep-alive
        Docker-Distribution-Api-Version: registry/2.0
        X-Content-Type-Options: nosniff

     3. 应用于生成环境之前
        分发项目中使用了不同的机密材料
        TLS 私钥
        SMTP 用户名和密码
        Redis 机密
        各种远程存储账户 ID 和密钥对
        客户端状态签名密钥
       
        上面这些材料不应该提交到你的生成环境 Registry 配置中,或者包含在你创建的
        镜像中。相反,应该考虑通过绑定加载卷注入秘密文件,这些卷可以挂载在 tmpfs
        或者 RAMDisk 设备上,并设置受限的文件权限。直接源自配置文件的机密可以使
        用环境变量来注入
        
        前缀 REGISTRY- 的环境变量将用作覆盖有分发项目加载的配置,配置变量时完全
        合格的,并且用下画线分割作为缩进级别。
        http:
           secret: somedefaultsecret 
        可以命名 REGISTRY-HTTP-SECRET 的环境变量来覆盖
        docker run -d -e REGISTRY-HTTP-SECRET= registry:2 
        
        warn级别:
        docker run -d -e REGISTRY_LOG_LEVEL=error registry:2
        
        禁用调试端点
        docker run -d -e REGISTRY_HTTP_DEBUG='' registry:2 
        
*** 4. 大规模配置 Registry
    1. 持久化的 BLOB 存储
       有效的 Registry 配置中只会出现其中的一个属性
       filesystem
       azure 
       s3 
       rados 
       
       默认配置使用的 filesystem 属性只有一个子属性 rootdirectory,它指定 用于本
       地存储的基本目录,例如,下面是一个默认配置的示例 
       storage:
           filesystem:
              rootdirectory: /var/lib/registry

    2. 微软 Azure 托管远程存储
       使用 azure 属性并且设置三个子属性:accountname,accountkey 和 container。
       在此上下文中,container 是指 Azure 存储容器,而不是 Linux 容器
       
       一个最小的 Azure 配置文件可能命名为 azure-config.yml,包括以下配置
       #Filename: azure-config.yml 
       version: 0.1
       log:
           level: debug 
           fileds:
                service: registry 
                environment: development
       storage:
           azure:
                accountname:
                accountkey:  
                container:
                realm: core.windows.net 
           cache:
                layerinfo: inmemory
           maintenance:
                uploadpurging:
                     enabled: false
       http:
           addr: :5000 
           secret: asecretforlocaldevelopment
           debug: localhost:5001

      realm 属性应该被设置为你想要存储的镜像的范围,realm 并不是一个必需的属性,
       默认设置为 core.windows.net 
       
       新建 azure-config.df 
       #Filename: azure-config.df 
       FROM registry:2
       LABEL source=dockerinaction
       LABEL category=infrastructure 
       #Set the default argument to specify the config file to use 
       #Setting it early will enable layer caching if the 
       #azure-config.yml changes 
       CMD ["/azure-config.yml"]
       COPY ["./azure-config.yml","/azure-config.yml"]

       构建镜像
       docker build -t dockerinaction/azure-registry -f azure-config.df .

    3. AWS S3 托管远程存储
       有四个必需的子属性:
       accesskey, secretkey, region 和 bucket 这些都是对你的账户进行身份认证和设
       置 BLOB 读写位置必需的属性,其他子属性指定分发项目应该如何使用 BLOB 存储,
       包括 encrypt, secure, v4auth, chunksize 和 rootdirectory 
       
       设置 encrypt 属性为 true 时,将会对于你的 Registry 保存到 S3 的数据启用数
       据闲时加密功能

       secure 属性控制与 S3 通信时 HTTPS 协议的使用,默认是 false,此时使用 HTTP。
       如果你存储私有镜像材料,应该设置为 true

       v4auth 属性告知 Registry 使用 AWS 认证协议的 v4 版本,一般来说这应该设置
       为 true, 但默认是 false
        
       chunksize 设置文件应该切割的大小,超过这个值就需要切分成小文件,最小的文
       件大小为 5MB
       
       rootdirectory 属性设置在你的 S3 bucket 内 Registry 数据的 根目录,如果你
       想从相同的 bucket 运行多个 Registry, 这个设置是很有用的

       #FileName s3-config.yml 
       version: 0.1 
       log: 
           level: debug 
           fileds: 
                service: registry
                environment: development
       storage: 
           cache:
                layerinfo: inmemory
           s3:
                accesskey:
                secretkey:
                region:
                bucket:
                encrypt: true 
                secure: true 
                v4auth: true
                chunksize:  5242880
                rootdirectory: /s3/object/name/prefix 
            maintenance:
                uploadpurging:
                    enabled: false
       http: 
           addr: :5000 
           secret: asecretforlocaldevelopment
           debug:
                 addr: localhost:5001

      构建镜像 s3-config.df 
      #Filename s2-config.df 
      FROM registry:2
      LABEL source=dockerinaction
      LABEL category=infrastructure
      #Set the default argument to specify the config file to use 
      #Setting it early will enable layer caching if the 
      #s3-config.yml changes 
      CMD ["/s3-config.yml"]
      COPY ["./s3-cofig.yml","s3-config.yml"]
      
      构建新镜像
      docker build -t dockerinaction/s3-registry -f s3-config.df .

    4. RADOS (Ceph) 的内部远程存储
       可靠的自主分布式对象存储(RADOS)由名为 Ceph (http://ceph.com)的软件项
       目提供。 Ceph 是一个用来构建类似 Azure Stroage 或者 AWS S3 的分布式 BLOB
       存储服务的软件,如果你有预算,时间和专业知识,你可以部署自己的 Ceph 集群。

       rados 存储属性
       version: 0.1
       log: 
           level: debug
           fileds: 
               service: registry
               environment: development
       stroage: 
           cache: 
               layerinfo: inmemory
       storage:
           rados:
               poolname: radospool 
               username: radosuser
               chunksize: 4194304
           maintenance:
               uploadpurging: 
                   enabled: false
       http:
           addr: :5000 
           secret: asecretforlocaldevelopment
           debug:
               addr: localhost:5001
               
     三个子属性分别是:poolname, username, chunksize
     poolname: Ceph 在池中存储 BLOB, 池是被配置为有一定冗余,分布式和行为。池将会
       指示 BLOB 是如何通过你的 Ceph 存储集群来存储的,poolname 属性告知
       Registry 哪一个池被用作为 BLOB 存储 
     chunksize: 默认大小为 4MB

    5. 扩展访问和延迟的改进
       1. 与元数据缓存集成
          分发项目中的元数据缓存配置可以用 storage 属性的 cache 子属性来设置,而
          cache 有一个名为 blobdescriptor 的子属性,该属性有两个潜在的值,分别是
          inmemory 和 redis。如果你使用 inmemory,那么设置该值是唯一需要的配置,
          但是如果你使用 redis,你需要提供额外的连接池配置
          顶层的 redis 属性只有一个必要的 addr 子属性,该属性指定了用于缓存的
          Redis 服务器的位置,这个服务可以在同一台机器或者不同的机器上运行,但是
          如果你使用了本地主机名称,那么就必须在同一个容器或者加入网络的另一个容
          器上运行。使用一个已知的主机别名可以让你灵活地代理一个在运行时配置的连
          接,在以下配置示例中,Registry 将尝试连接到一个 redis-host 端口为 6379
          的 Redis 服务。
          
          #Filename: redis-config.yml 
          version: 0.1 
          log: 
              level: debug 
              fields:
                  service: registry
                  environment: development
          http: 
              addr: :5000 
              secret: asecretforlocaldevelopment
              debug:
                  addr: localhost:5001 
          storage:
              cache:
                  blobdescriptor:redis
              s3:
                  accesskey:
                  secretkey:
                  region:
                  bucket:
                  encrypt: true
                  secure: true 
                  v4auth: true 
                  chunksize: 5242880 
                  rootdirectory: /s3/object/name/prefix
               maintenance:
                  uploadpurging:
                      enabled: fasle
           redis:
               addr: redis-host:6379
               password: asecret
               dialtimeout: 10ms
               readtimeout: 10ms
               writetimeout: 10ms
               pool:
                   maxidle: 16
                   maxactive: 64
                   idletimeout: 300s
                   
           password 属性定义了在连接时传给 Redis AUTH 命令的密码,
          dialtimeout,readtimeout 和 writetimeout 属性指定了连接,读取和写入
          Redis 服务的超时值,最后一个属性 pool 有三个子属性,定义了连接池的属性
          
          池大小的最小值可以用 maxidle 属性指定,而最大值 maxactive 属性设置。
          构建一个 Registry 并链接到一个 Redis 容器
          docker run -d --name redis redis 
          docker build -t dockerinaction/redis-registry -f redis-config.df .
          docker run -d --name redis-registry \
            --link redis:redis-host -p 5001:5000 \
            dockerinaction/redis-registry

       2. 使用存储中间件简化 BLOB 传输
          
          #Filename: scalable-config.yml 
          version: 0.1 
          log: 
              level: debug 
              fields:
                  service: registry
                  environment: development
          http: 
              addr: :5000 
              secret: asecretforlocaldevelopment
              debug:
                  addr: localhost:5001 
          storage:
              cache:
                  blobdescriptor:redis
              s3:
                  accesskey:
                  secretkey:
                  region:
                  bucket:
                  encrypt: true
                  secure: true 
                  v4auth: true 
                  chunksize: 5242880 
                  rootdirectory: /s3/object/name/prefix
               maintenance:
                  uploadpurging:
                      enabled: fasle
           redis:
               addr: redis-host:6379
               password: asecret
               dialtimeout: 10ms
               readtimeout: 10ms
               writetimeout: 10ms
               pool:
                   maxidle: 16
                   maxactive: 64
                   idletimeout: 300s
            middleware:
                storage:
                   - name: cloudfront 
                     options:
                         baseurl:
                         privatekey:
                         keypairid:
                         duration: 3000 
                      
*** 5. 通过通知集成
    1. [ ] 通知是一个简单的 Webhook 集成工具
       最后一个例子把分发项目和 Elasticsearch 项目
       (https://github.com/elastic/elasticsearch) 以及一个 web 接口集成来创建
       一个完全可搜索的 Registry 事件数据库
       
       Elasticsearch 是一个可伸缩的文档索引数据库,它提供了运行你自己的搜索引擎
       所有必需的功能,其中 Calaca 是一个流行的 Elasticsearch 开源 web 界面。
       
       docker pull elasticsearch::1.6
       docker pull dockerinaction/ch10_calaca
       docker pull dockerinaction/ch10_pump 

       Registry 上的每个有效的动作都会导致一个通知,包括如下所示:
       仓库清单上传和下载
       BLOB 元数据请求,上传和下载
       通知 JSON 对象
       {"events": [{
        "id":92xxxxxx-xxx-xxxx-xxxxxxx",
        "timestamp":...
        "action": "push",
        "target": {
        "mediaType":...
        "length":...
        "digest":...
        "repository":...
        "url":...
        },
        "request": {
        "id":...
        "addr":...
        "host":...
        "method":...
        "useragent":...
        },
        "actor":{},
        "source":{
        "addr":...
        "instanceID":...
        }
       }

       dockerinaction/ch10_pump 容器中的服务会检查事件列表中的每个元素,然后将合
       适的事件转发到 ElasticSearch 节点 
       启动 Elasticsearch 和 pump 容器
       docker -d --name elasticsearch: -p 9200:9200 \
       elasticsearch::1.6 -Des.http.cors.enabled=true 
       
       docker run -d --name es-pump -p 8000 \
       --link elasticsearch::esnode \
       dockerinaction/ch10_pump 

       可以通过传递环境变量到 Elasticsearch 程序本身,从而不需要创建一个完整的镜
       像就可以定制化由 Elasticsearch 镜像 创建的容器,在前面的命令中启用 CORS
       头部,这样你就可以将这个容器与 Calaca 集成。
       
       启动容器运行 Calaca web 接口:
       docker run -d --name calaca -p 3000:3000 \
       dockerinaction/ch10_calaca 
       
       注意运行 Calaca 的容器不需要链接到 Elasticsearch 容器,而是从 web 浏览器
       使用一个直接到了 Elasticsearch 节点的链接。在这种情况下,所提供的镜像配置
       为使用运行在本地主机上的 Elasticsearch 节点,如果你运行 VirtualBox ,下一
       步可能会非常棘手。
       
       Virtualbox 用户在技术上没有绑定 Elasticsearch 容器的端口到本地主机,相反
       绑定到是 Virtualbox 虚拟机的 IP 地址。你可以使用包含在 VirtualBox 里面
       VBoxManage 程序来解决这个问题,使用者程序来创建你的主机和默认虚拟机直接的
       端口转发规则,你可以用两个命令创建你所需要的规则
       
       VBoxManage controlvm "$(docker-machine active)" natpf1 \
          "tcp-port9200,tcp,,9200,,9200"
          
       VBoxManage controlvm "$(docker-machine active)" natpf1 \
          "tcp-port3000,tcp,,3000,,3000"

       这些命令创建了两个规则:转发本地主机的 9200 端口到默认虚拟机的 9200 端口,
       同样的对于端口 3000 也一样。现在 VirtualBox 用户就可以跟原生的 Docker 用
       户一样以相同的方式与这些端口交互

       使用默认的 Registry 配置并添加一个 notification 分段,创建一个新文件并复
       制以下配置
       #Filename: hooks-config.yml
       version: 0.1
       log: 
           level: debug 
           formatter: text 
           fields:
               service: registry 
               environment: staging 
       storage: 
           filesystem:
               rootdirectory: /var/lib/registry
           maintenance:
               uploadpurging:
                   enabled: true
                   age: 168h 
                   interval: 24h
                   dryrun: false 
       http: 
           addr: 0.0.0.0:5000 
           secret: asecretforlocaldevelopment
           debug:
               addr: localhost:5001 
       notifications:
           endpoints:
       - names: webhookmonitor 
         disabled: false 
         url: http://webhookmonitor:8000/
         timeout: 500 
         threshold: 5 
         backoff: 1000 

       最后一个 notification 指定了需要通知的端点的列表,每个端点的配置包括一个
       名称,URL,尝试超时时间,尝试阀值和重试时间。也可以通过 disabled 属性为
       false 来禁用单个端点,而不需要删除配置
       
       下面的命令将创建一个 Registry ,它使用一个基础镜像,使用绑定挂载卷注入配
       置,最后一个参数是对要使用的配置文件进行设置。该命令创建一个连接到 pump
       容器,并分配为别名 webhookmonitor。最后,它将 Registry 绑定到本地主机(或
       者 Boot2Docker IP 地址)的 5555 端口:
       
       docker run -d --name ch10-hooks-registry -p 5555:5000 \
         --link es-pump:webhookmonitor \
         -v "$(pwd)"/hooks-config.yml:/hooks-config.yml \
         registry:2 /hook.config.yml 

       docker tag  dockerinaction/curl localhost:5555/dockerinaction/curl 
       docker push localhost:5555/dockerinaction/curl
       
       docker pull localhost:5555/dockerinaction/curl 
       
    2. Elasticsearch 可以对整个文档做索引,所以事件的任何字段都是一个潜在的搜索
    词。
       1. 搜索 pull 或者 push,查看所有的拉取或者推送事件
       2. 寻找一个特定的仓库前缀,获得带有这个前缀的所有事件的列表
       3. 根据特定的镜像指纹追踪活动
       4. 通过请求一个 IP 地址发现客户端。
       5. 发现客户端访问的所以仓库

*** 6. 小结
    1. 一个 Docker Registry 是由其公开的 API 定义的,分发项目是一个对于 Registry
       API V2 的开源实现
    2. 运行你自己的 的 Registry 很简单,从 Registry:2 镜像启动一个容器即可
    3. 分发工程通过 YMAL 文件配置
    4. 实现有多个客户端的集中式的 Registry , 通常需要实现一个反向代理,采用 TLS
       ,并添加身份认证机制
    5. 身份认证可以移到反向代理或者由 Registry 本身实现
    6. 虽然有其他身份认证机制可用,HTTP 基础身份认证是最简单的配置,并最受欢迎。
    7. 反向代理层可以帮助解决 Registry API 对于多个客户端版本的兼容性问题
    8. 在生产环境中通过绑定加载卷和环境变量配置覆盖来注入机密材料,不要提交机密
       材料到镜像里。
    9. 集中式 Registry 考虑采用远程 BLOB 存储,比如 Azure,S3 或者 Ceph。
    10. 分发项目可以通过创建一个元数据缓存(基于 Redis) 或者采用 Amazone web 服
        务 CloudFront 存储中间件这两种方式配置为可伸缩的。
    11. 将分发项目与你剩下的部署项目,分布式系统和数据中心基础设施通过通知集成,
        非常简单。
    12. 通知以 JSON 格式推送事件数据到已知配置的端点。
 

你可能感兴趣的:(Docker,Docker)