iT邦幫忙

2023 iThome 鐵人賽

0
Software Development

救救我啊我救我!CRUD 工程師的惡補日記系列 第 39

【Docker】利用 Docker Compose 完成多容器部署(二)

  • 分享至 

  • xImage
  •  

上一篇初步認識了 Docker Compose,並以現有的映像檔為練習對象。本文將介紹深入一點的配置,包含使用 Dockerfile、掛載,並設計服務之間的相依,過程中也會定義健康狀態的檢測方式。

此篇亦轉載到個人部落格


一、透過 Dockerfile 提供映像檔

在 docker-compose.yml 配置檔中,也可將事先寫好的 Dockerfile 作為映像檔來源。

會使用這種做法,筆者認為有兩種情況。第一種是想針對 Docker Hub 上或本地現有的映像檔作客製化。第二種是我們自己在本地有一些程式成品(例如專案原始碼、JAR 檔等),想先透過 Dockerfile 打包為映像檔跑起來。

(一)前情提要

Day 36,我們將 Spring Boot 後端程式的專案,透過 Dockerfile 製作成映像檔。

該程式專案的結構節錄如下。

backend
  |_ target
  |   |_ backend-app.jar
  |_ env-config
  |    |_ test
  |    |   |_ application.properties
  |    |_ prod
  |        |_ application.properties
  |_ Dockerfile

其中「application.properties」檔案是用來提供一些如 DB 的連線字串、帳號密碼等配置。JAR 檔為程式的執行檔,需根據要部署的環境(如測試、生產),選擇其中一個配置檔來讀取。

Dockerfile 的部份內容如下。

# ...
ARG SERVER_TYPE
# ...
COPY ["./target/backend-app.jar", ...]
COPY ["./env-config/${SERVER_TYPE}", ...]
# ...

建立映像檔時,可根據 docker image build 指令的 --build-arg 參數,將「test」或「prod」字串傳入。而 Dockerfile 會由「ARG」指令接收,並給予名稱「SERVER_TYPE」,藉此將指定路徑下的配置檔複製到映像檔中。

(二)撰寫配置

假設我們把 Spring Boot 的程式專案搬移到 compose 的根目錄裡,則目前檔案的相對位置節錄如下。

my-project
  |_ backend
  |   |_ Dockerfile
  |   |_ env-config
  |      |_ test
  |      |_ prod
  |_ docker-compose.yml
  |_ service-env-files
      |_ mysql.env
      |_ rabbitmq.env

那麼在配置檔中可透過如下的寫法,提供 Dockerfile 作為映像檔。

services:
  backend:
    build:
      context: /backend
      dockerfile: Dockerfile
      args:
        - SERVER_TYPE=prod
    container_name: DemoBackend
    ports:
      - 8080:8080

在「build」階層中,能定義 Dockerfile 相關的資訊。

  • context:Dockerfile 所在的資料夾
  • dockerfile:Dockerfile 的檔案名稱
  • args:要傳遞給 Dockerfile 的「ARG」指令的參數

要注意的是,若 Dockerfile 中有透過「COPY」指令複製主機的資料,且主機路徑使用相對路徑,則該路徑依然會以 Dockerfile 的所在位置為基準。

二、將參數傳進配置檔

在第一節配置檔的範例中,傳入了名為「SERVER_TYPE」的參數,值為「prod」。用意是根據要部署的目標環境,從對應的路徑複製檔案,而目前是寫死的(hard code)。為了保有彈性,讓我們設計成由外部檔案帶入參數。

請在與配置檔相同的路徑下,建立一個名為「.env」的檔案(不需要有主檔名)。目前檔案的相對位置節錄如下。

my-project
  |_ docker-compose.yml
  |_ service-env-files
  |_ .env

接著在「.env」檔案中寫下參數。

BACKEND_SERVER_TYPE=prod

並且在配置檔中,將 Dockerfile 所需要的「SERVER_TYPE」參數值,以 ${參數名稱} 的寫法挖空,如下。

services:
  backend:
    build:
      # ...
      args:
        - SERVER_TYPE=${BACKEND_SERVER_TYPE}

如此一來,不論是在執行 docker compose config 指令進行確認,還是 docker compose up 實際建置映像檔並運行,此處的 ${BACKEND_SERVER_TYPE} 將會被 .env 檔案中定義好的參數值填入。

這種從 .env 檔案帶入參數的做法,亦可用於配置檔的其他地方。以下的範例配置,是將服務所用到的映像檔名稱,也設計為帶入參數。

services:
  db:
    image: ${MYSQL_IMAGE}
    # ...
  mq:
    image: ${RABBITMQ_IMAGE}
    # ...

而 .env 檔案的對應參數如此撰寫。

MYSQL_IMAGE=mysql:8.2.0
RABBITMQ_IMAGE=rabbitmq:3.12.4-management

三、配置掛載

若我們想對容器做掛載,也能進行配置。寫法是在服務的「volumes」階層下,將配置列出來。

(一)主機掛載

若掛載的來源是主機的檔案空間,那麼配置寫法的格式為 {主機相對路徑}:{容器絕對路徑}:{唯讀}。要注意的是,主機路徑必須在 compose 根目錄底下。

以下的範例是將 compose 根目錄下的「backend-app-logs」資料夾,掛載到容器的「/app/log」路徑。

services:
  backend:
    # ...
    volumes:
      - "./backend-app-logs:/app/log"

而以下的範例則是將 compose 根目錄下的「/backend/env-config/prod」資料夾,掛載到容器的「/app/config」路徑。且設為唯讀,代表容器不可異動該資料夾。

services:
  backend:
    # ...
    volumes:
      - "./backend/env-config/prod:/app/config:ro"

(二)Volume 掛載

由於一個 volume 可掛載到多個容器與多個路徑,因此做法上比較特別。會事先在配置檔的第一階層定義好 volume 的資訊,並給它一個 key。接著填寫服務的參數時,再去指向該 key,藉此達到重複利用。

以下的範例,是在配置檔第一階層的「volumes」,定義一個名為「backend_app_logs」的 volume,並給予「key_backend_log」這個 key。

接著在服務參數的「volumes」階層,將該 volume 掛載到容器的「/app/log」路徑。配置寫法的格式為 {volume 的 key}:{容器絕對路徑}

services:
  backend:
    # ...
    volumes:
      - "key_backend_log:/app/log"
volumes:
  key_backend_log:
    name: backend_app_logs
    external: false

在定義 volume 時,用到了「external」參數。若值為 false,代表要讓 Docker 自動建立 volume,因此可透過「name」參數來命名。若為 true,代表我們要使用事先建立好的 volume,故無法在此做如命名之類的設定。

四、服務相依

在這個 compose 中,我們有資料庫(MySQL)、訊息佇列(RabbitMQ)與後端程式(Spring Boot)三個服務。照理來說,後端程式在啟動時,它所需要連接的外部服務都應該處於「準備完成」的狀態。

這裡說的準備完成,指的是容器裡面的軟體。假設啟動容器後,MySQL 需要 2 分鐘才能運行起來,而 Spring Boot 只要 30 秒,這個時間差有可能導致 Spring Boot 啟動失敗,因為連不上 MySQL。為了維護服務之間的相依性,我們需要對啟動順序做配置。

以下範例是設定 backend 服務依賴於 db 和 mq 這兩個服務,等到 db 與 mq 進入健康狀態,backend 的容器才會啟動。至於 db 與 mq 的健康狀態該如何檢測,我們可自行定義。

services:
  backend:
    # ...
    depends_on:
      db:
        condition: service_healthy
      mq:
        condition: service_healthy
  db:
    image: mysql:8.2.0
    # ...
    healthcheck:
      test: ["CMD", "mysqladmin", "-u$MYSQL_USER", "-p$MYSQL_PASSWORD",  "ping", "-h", "localhost"]
      start_period: 90s
      interval: 60s
      timeout: 10s
      retries: 2
  mq:
    image: ${RABBITMQ_IMAGE}
    # ...
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      start_period: 90s
      interval: 60s
      timeout: 10s
      retries: 2

上面的配置檔中看到了許多新東西,接下來就一一認識。

在 backend 服務使用 depends_on 參數,可設定它會依賴於哪些服務。此處依賴 db 與 mq。

該階層下的 condition 參數,則定義被依賴的服務進入何種狀態,本服務才能啟動。這裡提供的值為「service_healthy」,代表進入健康狀態。另一個可用的值為「service_started」,代表容器啟動。

至於一個服務的健康狀態檢測方式,若來源映像檔的 Dockerfile 並未定義,或者不適合我們,則可於配置檔進行覆寫。範例裡,筆者在 healthcheck 階層中使用了多個參數,分別說明如下。

  • test:進行檢測的 command line 指令。
  • start_period:容器啟動多久,會開始第一次檢測。
  • interval:兩次檢測的時間間隔。
  • timeout:檢測的逾時時間。超過則視為不健康。
  • retries:不健康時,允許重新檢測的次數。若重測達一定次數均不健康,才將容器視為不健康。

各個服務可用來當作檢測的指令不盡相同,需要自行上網查詢,或找找是否有類似「ping」效果的指令。

五、留意連線字串

(一)問題

這兩篇文章,我們透過 Docker Compose 啟動了資料庫、訊息佇列與 Spring Boot 後端程式的容器。

在開發 Spring Boot 程式時,會在 application.properties 配置檔,撰寫其他服務的「連線字串」。例如資料庫的 IP 地址、帳號密碼等。

以 Spring Data JPA 為例,連線字串是這樣寫:

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/your-db-name

假設資料庫與後端程式位於同一台主機,我們可能會直覺地使用「127.0.0.1」或「localhost」當作資料庫的地址。如今軟體已經在容器內運行,那麼該地址指的將是容器自己,故無法連上另一個容器。

(二)在 Windows 或 Mac 系統上

本地開發時,我們會使用 Windows 或 Mac 系統。

為了能在一個容器內,連上另一個 MySQL 資料庫容器,請使用「host.docker.internal」作為地址,如下:

spring.datasource.url=jdbc:mysql://host.docker.internal:3306/your-db-name

根據官方文件的解釋,「host.docker.internal」是一個特殊的 DNS 名稱,會對應到主機的地址。

另外,即便後端程式只是在主機本地運行,沒有打包到容器,此連線字串亦可連上運行中的容器。換句話說,主機端的軟體若想連線容器,同樣可使用這個 DNS 名稱。

(三)在 Linux 系統上

交付的軟體,最終會部署到測試或生產環境的 Linux 系統,此時連線字串仍可使用「127.0.0.1」的地址。但在 docker-compose.yml 配置檔中,需對後端程式的服務添加「extra_hosts」的參數,如下:

services:
  backend:
    # ...
    extra_hosts:
      - "host.docker.internal:host-gateway"

Ref:


今日文章到此結束!
最後宣傳一下自己的部落格,我是「新手工程師的程式教室」的作者,主要發表後端相關文章,請多指教/images/emoticon/emoticon41.gif


上一篇
【Docker】利用 Docker Compose 完成多容器部署(一)
下一篇
【MySQL】資料表、資料型態與欄位限制
系列文
救救我啊我救我!CRUD 工程師的惡補日記50
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言