跳至主要内容

[Note] Docker and Kubernetes: The Complete Guide @ Udemy 學習筆記

此篇為筆記之整理,非原創內容,資料來源主要來自:Docker and Kubernetes: The Complete Guide @ Udemy

重要概念

  • Image(映像檔):有所有執行程式時所需要相依的內容、設定、指令和執行檔。當在執行 container 時,它會使用一個獨立的檔案系統,而這個檔案系統即是透過 image 來提供。
  • Container(容器):image 的 instance,真正執行的程式,它是獨立於本機所有 process 以外的一個 process。Container 包含了 image 中的 FS snapshot 和 Startup Command。
  • Docker Client(Docker CLI):輸入 docker 指令的地方,可以透過安裝 Docker Desktop 取得
  • Docker Server(daemon):建立映像檔、執行容器的地方

Kernel 和 Container

Kernel 介於硬體和軟體的中間,當應用程式在執行時,會透過 system call 告知 kernel 要執行的項目(例如,寫檔),kernel 收到指令後會再對硬體進行操作:

Imgur

  • name spacing:可以將單一 process(或 group of processes)使用到的資源加以隔離
  • control groups(cgroups):可以限制每個 process 可以使用到的資源量多寡(例如,memory 的大小、CPU 的效能)

Imgur

Container 的概念就如同下圖中紅色框線的位置:

Imgur

Image 和 Container

  • image 會在 container 中執行
  • 當我們在講 image 的時候,實際上包含了一個「檔案系統的快照(FS Snapshot)」和「啟動他的指令(Startup Command)」
  • 當一個 image 要在 container 中執行時,會把 image 中對應的 FS Snapshot 和 Startup Command 放一份到 container 中加以執行

Imgur

Docker Life Cycle(Status of Container)

  • created
  • up(表示在執行中)
  • exited

建立 Image

Dockerfile 建立的流程如下:

  1. 選定一個現有的 image 當作 base image
  2. 執行一些指令來安裝其他的程式
  3. 定義一些指令來啟動該 container

建立 Dockerfile

The Build Process in Detail @ Docker and Kubernetes: The Complete Guide @ Udemy

Dockerfile 的內容就像是告訴一台沒有內建 OS 的電腦,要如何重零開始把應用程式安裝進去。

# Dockerfile
# Use an existing docker image as a base
FROM alpine

# Download and install a dependency
RUN apk add --update redis

# Tell the image what to do when it starts as a container
CMD ["redis-server"]
  • 我們之所以要安裝 OS,是因為它通常內建了我們所需要的程式,這裡使用 alpine 是因為它有內建 apk 的指令,方便我們安裝所需的套件

docker build:建立 image

在建立好 Dockerfile 後,可以執行 docker build 指令:

# 建立好 Docker file 後
$ docker build . # 執行建立好的 Dockerfile 後,會得到 image 的 id
$ docker run b9124cbfdc30 # 透過 docker run 可以執行

docker build 會做以下行為:

  • 每一個 Step 都會啟動一個 container 執行對應的行為,完成後會把它存成一個暫時的 image。
  • 接著,在下一個 Step 開始時,會去使用上一個 Step 留下的 Image,並以此再次啟動一個 Container,執行該 Step 對應的指令,該指令執行完後,同樣的會再把最終完成的結果存在一個 Image,以這樣的流程重複完成每個 Step。
  • 待所有 Step 都執行完後,最終會有一個 Image 可以讓使用者執行 docker run <image-id>

一般我們在使用別人的 image 時,是根據 image 來建立對應的 container,但當我們要透過 Dockerfile 來建立 image 時,則是根據 container 來產生出 image。也就是說,逐步執行這個 Dockerfile 的過程會像是下面這樣(通常不會這麼做,單純示範用):

# 從 container 建立一個 image
$ docker run -it alpine sh
$ / apk add --update redis

# 開啟另一個 Terminal
$ docker commit -c 'CMD ["redis-server"]' <container-id> # 會回傳 image-id
$ docker run <image-id>

tagging a image:幫 image 加上 tag

每次都要輸入 image-id 非常麻煩,透過 image tag 可以為 image 建立標籤。慣例來說,標籤的命名會是 dockerId/projectName:version,例如 pjchender/redis:latest

# tagging docker image
# docker build -t <tag-name> .
$ docker build -t pjchender/redis-server:latest .
$ docker run pjchender/redis-server # 預設會使用 latest version

這整個為 image 建立標籤的流程稱作 tagging a image,但就技術上來說,只有標籤名稱最後的版本號是真正的 tag,前面單純只是 dockerId 和 projectName。

Rebuild with cache

  • 每一次 Dockerfile 被執行後,Docker 都會將該次執行的內容快取下來,只要 Dockerfile 沒有改變(添加或變更順序),下次重複執行 docker build 指令時,會直接取用 cache 中的內容,在終端機中會看到 Using Cache 的說明文字。

Dockerize an Node.js App

將 App Dockerize 的過程包含以下步驟:

  1. 建立 Node JS Web App
  2. 建立 Dockerfile
  3. 根據 Dockerfile 來 build image
  4. 將 image 以 container 方式執行
  5. 從瀏覽器連到該 Web App

1. 建立 App

const express = require('express');
const app = express();

app.get('/', (req, res) => {
res.send('Hi there');
});

app.listen(8080, () => {
console.log('Listening on port 8080');
});

2. 建立 Dockerfile

  • COPY <local-machine> <container-drive>
    • local-machine 的路徑是相對於 build context,也就是 docker build .,後面的這個 . 就是 build context
  • 其中 COPY ./ ./ 的目的是要把專案資料夾中的所有檔案都複製一份到 docker container 的根目錄(root directory)
  • 由於只使用 COPY ././ 會把本機專案的所有檔案都複製到 container FS 的根目錄中,這麼做並不是很好,因此可以使用 WORKDIR <path> 這個指令來指定工作目錄,如此,所有後續的指令都會在這個工作資料夾中被執行
# Specify a base image
FROM node:alpine

# 定義 container 中的工作資料夾
WORKDIR /usr/app

# 將本機資料夾的檔案複製到工作資料夾中
COPY ./ ./ # COPY ./local ./container

# Install some dependencies
RUN npm install

# Default command
CMD ["npm", "start"]

這樣的寫法有一個問題,因為 Dockerfile 會由上往下執行,如果沒有改變就直接拿 cache,但若其中一行對應的內容有改變,則該行後的所有指令都會被迫重新執行,因此可以盡可能把容易改變的那行抽成獨立的指令,放到 Dockerfile 的越後面越好。

舉例來說,在原本的 Dockerfile 中,每次只要專案的中任何檔案(例如,index.js)有變更時,就算套件(即package.json 的內容沒有改變)沒有變更,都會重新執行一次 npm install。因為使用的是 COPY ./ ./,Docker 會發現 index.js 有改變而重新執行一次該行及其後續的指令。

因此,可以改成只有在 package.json 的檔案有變更時,再執行 npm install 這個指令,就可以減少很多不必要的重 build 而直接使用 cache。把 Dockerfile 改成:

# Specify a base image
FROM node:alpine

# 定義 container 中的工作資料夾
WORKDIR /usr/app

# 先將 package.json 複製到 container 的工作資料夾中
# 只有當 package.json 有改變時,才會執行 npm install 的指令
COPY ./package.json ./
RUN npm install

# 將本機資料夾的檔案複製到工作資料夾中
COPY ./ ./

# Default command
CMD ["npm", "start"]

3. 根據 Dockerfile 來 build image

# -t 表示為該 image 建立 tag
$ docker build -t pjchender/docker-node-sandbox .

4. 將 image 以 container 方式執行

$ docker run pjchender/docker-node-sandbox

5. 從瀏覽器連進該 Web App

  • 透過 port mapping 的方式將 localhost 的 port 和 container 的 port 相對應
 # docker run with port mapping
# -p [localhost-port]:[container-port]
$ docker run -p 8080:8080 <image-id>

# 在本機瀏覽器輸入 localhost:5000 將會連到 container 的 8080 port
$ docker run -p 5000:8080 pjchender/docker-node-sandbox

當 container 已經在執行時,希望能夠對同一個 container 執行某一些指令時,可以使用 docker exec 的指令:

$ docker exec -t <container-id> sh

Docker Compose CLI

sample code @ pjchender gist

為什麼需要使用 docker compose

  • 雖然可以把 Database Server 和 Web App Server 放在同一個 Docker Container 中,但這並不是個好的作法,因為在未來流量變大,需要啟動多個 Web Server Container 來處理時,因為 Database 是包在和 web server 同一個 container 內,因此每個 Container 間的 Database 都會是獨立的,等同於有多個獨立的 database。
  • 這種情境比較適合把 database 獨立在另一個 container 中,不要和 web app 的 container 放在一起,並透過 docker compose 來管理多個 container 間的網路溝通。

Docker Compose CLI 是什麼

  • Docker Compose CLI 和 Docker CLI 是獨立不同的工具,但在安裝 docker client 時會一併安裝。
  • 它可以用來簡化每次都需要在 terminal 中透過 Docker CLI 輸入許多指令的困擾(類似 docker CLI 的 script)
  • 它可以用來一次啟動多個 Docker Container
  • 它可以設定各個 container 間的網路溝通

建立 docker compose file

  • docker compose file 內定義的 services 不需要額外設定,預設就會在同一個 network 內,彼此就能溝通
# 定義 docker-compose 使用的版本
version: '3'

# 定義兩個 services,一個是 redis-server,另一個是 node-app
# 這兩個 services 不需要額外設定,預設就會在同一個 network 內,彼此就能溝通
services:
redis-server:
image: 'redis'
node-app:
build: . # 根據當前根目錄的 Dockerfile 執行 build
ports:
- '8081:8081' # 將 local 的 8081 對應到 container 內的 8081

要讓 node-app 知道如何連線到 redis server,需要在 node-app 與 redis 建立連線的地方使用 docker-compose 中定義的 server 名稱:

// index.js
// ...
const client = redis.createClient({
host: 'redis-server', // 原本可能會填的是 localhost:6379,現在要用 service 的名稱
port: 6379,
});

使用 docker compose CLI

  • docker-compose up 後面可以不用加路徑,會直接去吃 docker-compose.yml 檔案
  • 如果需要根據 Dockerfile rebuild 新的 image 時,可以在最後加上 --build
# docker-compose 的指令需要在有 docker-compose.yml 的資料夾中執行
$ docker-compose up # 對應到類似 docker run <image-id>
$ docker-compose up --build # 對應到 docker build . 和 docker run <image-id>
$ docker-compose up -d # 在背景執行

$ docker-compose down

$ docker-compose ps # 列出該 docker-compose 中有哪些 services 在執行

自動重新啟動 container

重新啟動的策略包含:

  • "no":不要嘗試重新啟動,使用 no 要加上引號,讓它知道是字串
  • always:總是嘗試重新啟動,適合用在 Web Server
  • on-failure:只有在 container 停止且有 error code 時才重新啟動(以 Node.js 來說,就是 exit 內的 code 是 0 以外的代碼),也就是程式正確終止時,不用重新啟動,適合用在 worker
  • unless-stopped:除非是開發者強制停止它,否則總是重新啟動
...
services:
redis-server:
image: 'redis'
node-app:
+ restart: always
build: . # 根據當前根目錄的 Dockerfile 執行 build
ports:
- '8081:8081' # 將 local 的 8081 對應到 container 內的 8081

課程資源