** 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 格式推送事件數據到已知配置的端點。