《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: <Docker Hub 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 <Docker Hub userName>/hello-dockerfile:latest
       
       The push refers to repository [docker.io/<Docker Hub userName>/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/<your username>/<your repo>.git
       git push -u origin master
       
       與此同時,消費者會使用這樣的一組通用命令集 
       git clone https://github.com/<your userrname>/<your repo>.git
       cd <your-repo>
       docker build -t <your username>/<your repo> .
       
       鏡像源代碼分發與所有的 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 <USERNAME>
        
        創建 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=<MY_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: <your account name>
                accountkey: <your base64 encoded account key> 
                container: <your 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: <your awsaccesskey>
                secretkey: <your awssecretkey>
                region: <your bucket region>
                bucket: <your bucketname>
                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: <your awsaccesskey>
                  secretkey: <your awssecretkey>
                  region: <your bucket region>
                  bucket: <your bucketname>
                  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: <your awsaccesskey>
                  secretkey: <your awssecretkey>
                  region: <your bucket region>
                  bucket: <your bucketname>
                  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: <https://my.cloudfronted.domain.com/>
                         privatekey: </path/to/pem>
                         keypairid: <cloudfrontkeypairid>
                         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 格式推送事件數據到已知配置的端點。
 

發佈了13 篇原創文章 · 獲贊 2 · 訪問量 2398
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章