Ubuntu18.04下使用 Docker 構建並測試 Web 應用程序

構建 Sinatra 應用程序

創建文件夾:

mkdir sample01 && cd sample01
touch Dockerfile

編輯 Dockerfile :

FROM ubuntu:18.04
MAINTAINER nikki [email protected]
ENV REFRESHED_AT 2019-10-19

RUN apt-get update
RUN apt-get -y install ruby-full
RUN apt-get -y install  build-essential redis-tools
RUN gem install --no-rdoc --no-ri sinatra json redis

RUN mkdir -p /opt/webapp

EXPOSE 4567

CMD [ "/opt/webapp/bin/webapp" ]

Dockerfile 依舊完成了一些安裝任務,並在最後使用 CMD 指定 /opt/webapp/bin/webapp 作爲 Web 應用程序的啓動文件

構建:

sudo docker build -t nikki01/sinatra .

創建 Sinatra 容器

創建鏡像之後,下載 Sinatra Web 應用程序的源代碼,因爲原書給的鏈接失效了,所以我將整本書的代碼都下載下來

鏈接:https://pan.baidu.com/s/1mw_I0mBv6n78N2UGDTW_DQ&shfl=sharepset
提取碼:m68w

這裏用到的文件目錄在:

dockerbook-code-master/code/5/sinatra

找到其中的 webapp 目錄,將其複製到 sample01 中並查看:

ls -l webapp

應該有 bin、lib、Dockerfile 三個文件夾

給其中的文件添加可執行權限:

chmod +x $PWD/webapp/bin/webapp

使用 docker run 從鏡像創建一個新容器:

sudo docker run -d -p 4567 --name webapp -v $PWD/webapp:/opt/webapp nikki01/sinatra

這裏從 nikki01/sinatra 鏡像創建了一個新的名爲 webapp 的容器,指定了一個新卷 $PWD/webapp 存放新的 Sinatra Web 應用程序,並將這個卷掛載到在 Dockerfile 裏創建的目錄 /opt/webapp

CMD 指令指定了要運行的命令,此時可以使用 docker logs 查看輸出:

sudo docker logs webapp

使用 docker top 命令查看 Docker 容器里正在運行的進程:

sudo docker top webapp

查看剛纔指定的端口映射到本地宿主機的哪個端口:

sudo docker port webapp 4567

在我的機器上是映射到 32772
在這裏插入圖片描述
目前的 Sinatra 應用沒做什麼,只是接受輸入參數,可以使用 curl 命令(先確保安裝了 curl)測試這個程序:

curl -i -H 'Accept:application/json' -d 'name=Foo&status=Bar' http://localhost:32772/json

在這裏插入圖片描述
可以看到,傳入的參數轉化成 JSON 散列後的輸出:{“name”:“Foo”,“status”:“Bar”}

構建 Redis 鏡像和容器

現在我們要擴展 Sinatra 應用程序,加入 Redis 後端數據庫,並在 Redis 數據庫中存儲輸入的參數,因此需要構建全新的鏡像和容器運行 Redis 數據庫,之後利用 Docker 的特性關聯兩個容器

創建一個新的鏡像:

mkdir sample02 && cd sample02
touch Dockerfile

Dockerfile 內容如下:

FROM ubuntu:18.04
MAINTAINER nikki [email protected]
ENV REFRESHED_AT 2019-10-20
RUN apt-get update
RUN apt-get -y install redis-server redis-tools
EXPOSE 6379
ENTRYPOINT ["/usr/bin/redis-server"]
CMD []

Dockerfile 裏指定了安裝 Redis 服務器,公開 6379 端口,並指定了啓動 Redis 服務器的 ENTRYPOINT。構建此鏡像:

sudo docker build -t nikki01/redis .

構建容器:

sudo docker run -d -p 6379 --name redis nikki01/redis

查看 6379 映射到宿主機的哪個端口:

sudo docker port redis 6379

我的是 32773 端口
在這裏插入圖片描述
在本地安裝 redis-tools 包:

sudo apt-get -y install redis-tools

使用 redis-cli 命令確認 Redis 服務器工作是否正常:

redis-cli -h 127.0.0.1 -p 32773

在這裏插入圖片描述

連接到 Redis 容器

現在更新 Sinatra 應用程序,讓其連接到 Redis 並存儲傳入的參數。爲此需要能夠與 Redis 服務器對話。要做到這一點,有如下辦法:

  • 第一種方法涉及 Docker 的網絡棧,到現在爲止,我們的 Docker 容器都是公開端口並綁定到本地網絡接口的,這樣可以把容器裏的服務在本地 Docker 宿主機所在的外部網絡上公開
  • 第二種做法是內部網絡

安裝 Docker 時,會創建一個新的網絡接口,名字是 dicker0,每個 Docker 容器都會在這個接口上分配一個 IP 地址,查看目前 Docker 宿主機上這個網絡接口的信息:

ip a show docker0

在這裏插入圖片描述
docker0 接口有符合 RFC1918 的私有 IP 地址,範圍是 172.16 ~ 172.30。接口本身的地址 172.17.0.1 是這個 Docker 網絡的網關地址,也是所有 Docker 容器的網關地址

Docker 會默認使用 172.17.x.x作爲子網地址,除非已經有別人佔用了這個子網,如果這個子網被佔用了,Docker 會在 172.16 ~ 172.30 這個範圍內嘗試創建子網

接口 docker0 是一個虛擬的以太網橋,用於連接容器和本地宿主網絡。如果進一步查看 Docker 宿主機的其他網絡接口,會發現一系列名字以 veth 開頭的接口

Docker 每創建一個容器就會創建一組互聯的網絡接口。這組接口就像管道的兩端。這組接口其中一端作爲容器裏的 eth0 接口,另一端統一命名爲類似 vethec6a 這種名字,作爲宿主機的一個端口。你可以把 veth 接口認爲是虛擬網線的一端。這個虛擬網線一端插在名爲 docker0 的網橋上,另一端插到容器裏。通過把每個 veth* 接口綁定到 docker0 網橋,Docker 創建了一個虛擬子網,這個子網由宿主機和所有的 Docker 容器共享

進入容器,查看子網管道的另一端:

sudo docker run -t -i ubuntu:18.04 /bin/bash
/# ip a show eth0

如果提示 bash: ip: command not found

請運行:

/# apt update
/# apt -y install iproute2

結果如下:
在這裏插入圖片描述
可以看到,Docker 給容器分配了 IP 地址 172.17.0.3 作爲宿主虛擬接口的另一端,這樣就能讓宿主網絡與容器互相通信了

接下來從容器內跟蹤對外通信的路由,看看是如何建立連接的:

/# apt update && apt -y install traceroute
/# traceroute baidu.com

如果你的結果像這樣:
在這裏插入圖片描述
說明沒有達到我們希望跟蹤數據包的目的,原因是 traceroute 命令默認使用UDP協議來探測包,而很多路由器都禁用了traceroute 命令的 UDP 包,即不會處理 traceroute 命令發送的 UDP 包,導致無法探測出路徑中的路由器。可以使用選項 -I 來強制 traceroute 命令使用 ICMP 協議,例如:

/# traceroute -I baidu.com

結果如下:
在這裏插入圖片描述
從上面可以看出,數據包從 localhost 到目標主機 baidu.com 經過 18 個路由,有些路由無法探測出其 IP 地址

容器地址後的下一跳是宿主網絡上 docker0 接口的網關 IP 172.17.0.1

不過 Docker 網絡還有另一個部分配置才能允許建立連接:防火牆規則和 NAT 配置。這些配置允許 Docker 在宿主網絡和容器間路由。現在查看一下宿主機上的 IPTables NAT 配置(新開一個終端):

sudo iptables -t nat -L -n

結果如下:

在這裏插入圖片描述

首先,容器默認是無法訪問的。從宿主網絡與容器通信時,必須明確指定打開的端口。下面以 DNAT(即目標 NAT)這個規則爲例,這個規則把容器裏的訪問路由映射到 Docker 宿主機的 32773 端口

連接 Redis

用 docker inspect 命令查看新的 Redis 容器的網絡配置,會展示 Docker 的細節:

sudo docker inspect redis

從 Ports 中可以看出 6379 端口被映射到本地宿主機的 32773 端口

此外,圖中 Bridge 對應是 “”,按道理應該是使用 docker0 接口作爲網關地址,我往上翻了一下發現:

在這裏插入圖片描述

Driver 是 overlay2(18.06.0上的默認存儲驅動,在這之前的版本都是默認橋接方式,因此上面的規則適合之前版本),它和橋接模式的區別這裏就不贅述了,感興趣的可以看一下:Docker network命令

可以在命令中用 -f 只獲取 IP 地址

sudo docker inspect -f '{{ .NetworkSettings.IPAddress }}' redis

我這裏是 172.17.0.2

在這裏插入圖片描述

利用 redis-cli 讓 172.17.0.2 地址與 Redis 服務器的 6379 端口通信:

redis-cli -h 172.17.0.2

這樣就實現通信了,但是這種方法存在兩個問題:

  • 要在應用程序裏對 Redis 容器的 IP 地址做硬編碼
  • 如果重啓容器,Docker 會改變容器的 IP 地址
sudo docker restart redis
sudo docker inspect -f '{{ .NetworkSettings.IPAddress }}' redis

因爲我這裏是 overlay2 方式,所以 IP 地址沒變(?),具體我之後再深究

讓 Docker 容器互連

新建一個 Redis 容器:

sudo docker run -d --name redis01 nikki01/redis

啓動 Web 應用程序容器,並把它連接到新的 Redis 容器上:

sudo docker run -p 4567 --name webapp01 --link redis01:db -t -i -v $PWD/webapp:/opt/webapp nikki01/sinatra /bin/bash

-p 標誌公開了 4567 端口,這樣就能從容器外面訪問 Web 應用程序

–name 給容器命名爲 webapp01,並使用了 -v 標誌把 Web 應用程序作爲卷掛載到了容器裏

–link 標誌創建了兩個容器間的父子連接,這個標誌需要兩個參數:

  • 連接的容器名字
  • 連接後容器的別名

這個例子中,我們把新容器連接到 redis 容器,並使用 db 作爲別名。別名可以讓我們訪問公開的信息,而無需關注底層容器的名字。連接讓父容器有能力訪問子容器,並且把子容器的一些連接細節分享給父容器,這些細節有助於配置應用程序並使用這個連接

連接也能得到一些安全上的好處。在啓動 Redis 容器時,並沒有使用 -p 公開 Redis 的端口。因爲不需要這麼做,通過把容器連接在一起,可以讓父容器直接訪問任意子容器的公開端口。而且只有使用 --link 標誌連接到這個容器的容器才能連接到這個端口。容器的端口不需要對本地宿主機公開,現在我們已經擁有一個非常安全的模型。通過這個安全模型,就可以限制容器化應用程序的被攻擊面,減少應用暴露的網絡

也可以把多個容器連接在一起:

sudo docker run -p 4567 --name webapp02 --link redis:db ...
sudo docker run -p 4567 --name webapp03 --link redis:db ...

被連接的容器必須運行在用一個 Docker 宿主機上,不同 Docker 宿主機上運行的容器無法連接

最後,讓容器啓動時加載 shell,而不是服務守護進程,這樣可以查看容器是如何連接在一起的。Docker 在父容器裏的一下兩個地方寫入了連接信息:

  • /etc/hosts 文件中
  • 包含連接信息的環境變量中

查看 /etc/hosts 文件:

/# cat /etc/hosts

在這裏插入圖片描述
還記得 172.17.0.2 和 172.17.0.3 嗎,它們分別是 redis01 和 webapp01 的 IP 地址

現在試着 ping 一下 db 容器:

/# ping db

如果你沒有 ping 命令,那麼:

apt update
apt install -y iputils-ping

結果如下:

在這裏插入圖片描述

我們已經連接到了 Redis 數據庫,先看看環境變量裏包含的其他連接信息:

/# env

在這裏插入圖片描述

可以看到不少環境變量,其中一些以 DB 開頭。Docker 在連接 webapp 和 redis 容器時,自動創建了這些以 DB 開頭的環境變量。以 DB 開頭是因爲 DB 是創建連接時使用的別名。這些自動創建的環境變量包含以下信息:

  • 子容器的名字
  • 容器裏運行的服務所使用的協議、IP 和端口號
  • 容器裏運行的不同服務所指定的協議、IP 和端口號
  • 容器裏由 Docker 設置的環境變量的值

這些環境變量會隨容器不同而變化,取決於容器是如何配置的。更重要的是,這些連接信息可以讓容器內的應用程序使用相同的方法與別的容器進行連接,而不用關心被連接的容器的具體細節

使用容器連接來通信

給 Sinatra 應用程序加入一些連接信息,以便於 Redis 通信,有以下兩種方法可以讓應用程序連接到 Redis:

  • 使用環境變量裏的一些連接信息
  • 使用 DNS 和 /etc/hosts 信息

先試試第一種方法,在dockerbook-code-master/code/5/sinatra/ 中的 webapp/lib/app.rb:

在這裏插入圖片描述
這裏和書中的代碼不一樣,書中代碼:

require 'uri'
...
uri=URO.parse([ENV'DB_PORT'])
redis = Redis.new(:host => uri.host, "port => uri.port")

這裏使用 Ruby 的 URI 模塊來解析 DB_PORT 環境變量,並使用解析後的結果配置 Redis 連接,應用程序現在可以使用這個連接信息找到相連的 Redis 容器。通過環境變量,這裏不再需要硬編碼 IP 地址和端口進行連接。這是一種發現服務的方法

另外一種方法,在 dockerbook-code-master/code/5/sinatra/ 中的 webapp_redis/lib/app.rb:

在這裏插入圖片描述

圖中

redis = Redis.new(:host => 'db', :port => '6379')

應用程序會在本地查找名叫 db 的主機,找到 /etc/hosts 文件裏的項並解析到正確的 IP 地址。這也解決了硬編碼 IP 地址的問題

在容器裏啓動應用程序,看看 DNS 本地解析能不能工作:

/# nohub /opt/webapp/bin/webapp &

你可能會發現它找不到 nohub,彆着急下載,進入 /usr/bin/,然後就可以看到 nohub 了,如果還沒有請參考:-bash: nohup: command not found

不過你可能會有疑問, /opt/webapp/ 中並沒有任何東西,沒錯,就是沒有,原版書上這裏可能有誤,所以你需要先 down 文件下來:

/# /opt/webapp  git clone https://github.com/Nikkio3o/book.git

接下來你只需要找到 /book/dockerbook-code-master/code/5/sinatra/webapp/bin/webapp 就可以了,如果它提示你 Permission denied,你就這樣:

/# sudo /usr/bin/nohub sudo /opt/webapp/book/dockerbook-code-master/code/5/sinatra/webapp/bin/webapp &

沒有 sudo 的話就安裝 sudo

這裏啓動了 Sinatra 應用程序並讓其在後臺運行,現在在 Docker 宿主機(新開一個終端)上再次使用 curl 命令測試應用程序:

curl -i -H 'Accept:application/json' -d 'name=Foo&status=Bar' http://localhost:32780/json

爲什麼是 32780 呢,因爲:

sudo docker ps

在這裏插入圖片描述
你可能得到

在這裏插入圖片描述

這樣的結果,這個具體原因可能跟現在版本容器運行機制有關,所以直接進入 /opt/webapp/book/dockerbook-code-master/code/5/sinatra/webapp/bin/ 這個目錄,然後:

chmod +x webapp
./webapp

然後再運行

curl -i -H 'Accept:application/json' -d 'name=Foo&status=Bar' http://localhost:32780/json

就可以看到:
在這裏插入圖片描述
現在來確認一下 Redis 實例接收到了這個更新:

curl -i http://localhost:32780/json "[{\"name\":\"Foo\",\"status\":\"Bar\"}]"

在這裏插入圖片描述

我也不知道爲什麼是 404。。

現在已經連接到了應用程序(應用程序連接到了 Redis),應用程序會檢查 Redis 裏存儲的鍵,找出一個叫 params 的鍵,然後查詢這個鍵來看看我們的兩個參數(name=Foo 和 status=Bar)已經存入了 Redis

至此,用於演示 Web 應用程序棧的例子終於寫完了,這個 Web 應用程序棧由以下幾部分組成:

  • 一個運行 Sinatra 的 Web 服務器容器
  • 一個 Redis 數據庫容器
  • 這兩個容器間的一個安全連接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章