iT邦幫忙

2024 iThome 鐵人賽

DAY 16
3
Software Development

透過 nestjs 框架,讓 nodejs 系統維護度增加系列 第 25

nestjs 系統設計 - 活動訂票管理系統 containerize

  • 分享至 

  • xImage
  •  

nestjs 系統設計 - 活動訂票管理系統 containerize

目標

昨天做完 graceful shutdown 之後。今天會來講述,如何把 nestjs 的應用作容器化。

概念

應用程式容器化,是把應用程式的運行環境也透過標準化的方式打包起來。不同於以往針對每個應用程式還需要到執行的伺服器設定運行環境。簡化了部署的難度。

Docker 提供了一個標準的打包文件格式,可以把運行環境連同程式打包成映像檔案。透過推送到映像檔儲存庫,伺服器只要具備 Docker 運行環境,就可以從映像檔儲存庫拉取最新的應用程式映像檔案來執行。這種可運行的檔案,一般會被稱作 artifact 。一般會需要根據針對 artifact 作版本控制,來確認部屬的應用程式具有哪些行為。

img

撰寫 Dockerfile

FROM node:20.10.0-alpine as build
RUN mkdir /app
WORKDIR /app
RUN npm i -g pnpm
COPY src nest-cli.json package.json pnpm-lock.yaml tsconfig.build.json tsconfig.json /app/
RUN pnpm install --frozen-lockfile && pnpm run build
FROM node:20.10.0-alpine as prod
RUN mkdir /app
WORKDIR /app
RUN npm i -g pnpm
COPY --from=build /app/dist /app/dist
COPY --from=build /app/package.json /app/pnpm-lock.yaml /app/
COPY --from=build /app/lua /app/dist/lua
RUN pnpm install --production && npm uninstall -g pnpm
USER node
ENTRYPOINT [ "node", "./dist/main" ]

Dockerfile 主要會撰寫幾個重要的屬性:

  1. 運行環境: 比如 nestjs 應用會需要用到 nodejs ,所以就必須要在 FROM 語法作聲明
  2. 執行時所需要的執行身份:預設會使用 root 身份,但 nodejs 映像檔有一個專屬於 nodejs 運行的執行身份 node ,可以使用 User 來指定。這樣可以避免權限過大。
  3. 執行所需的指令:一共有兩種 ENTRYPOINT 或是 CMD 。差別是在於使用 CMD 的話, Docker Engine 會透過 docker-entrypoint.sh 來執行寫在 CMD 的指令。 ENTRYPOINT 則是直接執行 。但各家容器引擎不同,比如是 AWS 或是 Google 的容器服務,雖然遵循規範。但各自有其實作。
  4. 建制環境的指令:這部份就是可以透過各種 RUN 去設定,當下執行環境。

備註

上面範例,使用的是 Multi Stage build 。目標是為了,讓 build tool 不會影響到真正執行的 image size 。概念上是,每一行在 Dockefile 上的命令都會變成一個 commit 在 image 上。

在 docker compose 上,可以透過 target 指定要作哪個 stage 的 build 如下

  ticket-mn-api:
    container_name: ticket-mn-api
    build: 
      context: .
      dockerfile: ./Dockerfile
      target: prod
    image: ticket-mn-api
    ports:
      - 3000:3000

透過 docker history 指令,可以查看每個 docker commit 的大小:
image

設定 github action 來 build image 到 github package

name: Image build

on:
  push:
    branches:
      - master
    tags:
      - v*
  pull_request:

env:
  IMAGE_NAME: ticket-mn-api
  JWT_ACCESS_TOKEN_SECRET: ${{ secrets.JWT_ACCESS_TOKEN_SECRET }}
  JWT_ACCESS_TOKEN_EXPIRATION_MS: 10000
  JWT_REFRESH_TOKEN_SECRET: ${{ secrets.JWT_REFRESH_TOKEN_SECRET }}
  JWT_REFRESH_TOKEN_EXPIRATION_MS: 36000
  NODE_ENV: dev
  REDIS_URL: ${{ secrets.REDIS_URL }}
  DB_URI: ${{ secrets.DB_URI }}
  GIN_MODE: release
jobs:

  push:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker compose build ticket-mn-api
      - name: Tag with Image
        run: docker tag ticket-mn-api:latest $IMAGE_NAME:$GITHUB_SHA
      - name: Log in to registry
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
      - name: Push to registry
        run: |
          IMAGE_ID=ghcr.io/nodejs-typescript-classroom/$IMAGE_NAME
          IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
          VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
          [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
          [ "$VERSION" == "main" ] && VERSION=latest
          echo IMAGE_ID=$IMAGE_ID
          echo VERSION=$VERSION
          docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
          docker push $IMAGE_ID:$VERSION

以上設定是透過 docker compose build 來建立 image。然後把 image 送到對應的 github organization 的 package

這樣一旦 commit 到特定的 repository 或是下 tag v 就會啟動 build image 並且 push 到 image registry 如下
image

image

設定 測試的 github action

name: Node

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
env:
  NODE_ENV: dev
  JWT_ACCESS_TOKEN_SECRET: ${{ secrets.JWT_ACCESS_TOKEN_SECRET }}
  JWT_ACCESS_TOKEN_EXPIRATION_MS: ${{ secrets.JWT_ACCESS_TOKEN_EXPIRATION_MS }}
  JWT_REFRESH_TOKEN_SECRET: ${{ secrets.JWT_REFRESH_TOKEN_SECRET }}
  JWT_REFRESH_TOKEN_EXPIRATION_MS: ${{ secrets.JWT_REFRESH_TOKEN_EXPIRATION_MS }}
  DB_URI: ${{ secrets.DB_URI }}

jobs:
  cache-and-install:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        name: Install pnpm
        with:
          version: 9
          run_install: false

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.10.0
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      - name: Run Test
        run: pnpm test
      - name: Run Test
        run: pnpm test:e2e

每次 commit 也都會有測試驗證,如下圖

image

測試 docker stop event

  1. 先啟動 ticket-mn-api
docker start ticket-mn-api
  1. 停止 ticket-mn-api
docker stop ticket-mn-api
  1. 查看 log
docker logs -f ticket-mn-api

image

  1. docker ps -a 查看執行狀態值
    image

服務相依性

前面有說過, Dockerfile 可以撰寫 health_check 屬性來決定當下 service 的狀態。這個屬性在服務間有啟動的相依性時,就可以發揮作用。

假設是透過 docker compose 的文件去撰寫,就可以使用 depends_on 條件去作處理。例如: ticket-mn-api 需要 db 跟 redis 狀態為 healthy 才啟動就可以撰寫如下:

  depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

結論

雖然本篇是 nodejs 的 Port Mapping 沒有作修改是採用原本的 3000。然而,透過像是容器編排服務比如像是 k8s 的 Service 或是 AWS Fargate 等等本身就能作到服務阜口切換。這是可以讓撰寫程式與部署狀態不會相互被影響的技術。

當然還是可能會遇到一些說 N 年經驗的 "Devops" 職銜的人,跟開發者說你的 Port 要修改,他才能部屬。這類人也許真的處在跟我們一般人是不相同的異世界。這就不在本文所撰寫的相同時空不在討論之列。

身為開發者,還是必須要能夠掌握一些關於系統部屬的技術。這樣在程式真正載運行時,才能對一些正式環境發生的事件,有掌握度來分析 bug 事件。


上一篇
nestjs 系統設計 - 活動訂票管理系統 - graceful shutdown
下一篇
nestjs 系統設計 - 活動訂票管理系統 - fly.io
系列文
透過 nestjs 框架,讓 nodejs 系統維護度增加26
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言