返回文章列表

容器化單頁應用佈署Knative

本文闡述如何使用 Docker 將 React 單頁應用程式容器化,並佈署至 Knative 平台。文章涵蓋 Dockerfile 撰寫、映像檔建構、測試與推播,以及 Knative 佈署流程。同時,深入解析 Knative Serving 的請求處理機制,並以動態單頁應用為例,示範如何整合靜態內容與動態

Web 開發 容器化

現代 Web 應用常以容器化技術簡化佈署與管理。本文以 React 單頁應用為例,示範如何利用 Docker 建構容器映像檔,並佈署至 Knative 平台。首先,撰寫 Dockerfile 定義應用程式環境,包含 Node.js 基礎映像檔、工作目錄設定、套件安裝與啟動指令。接著,建構映像檔並透過本地端測試驗證功能,確認埠號對映與應用程式運作正常。完成本地端測試後,將映像檔推播至容器登入檔,以便 Knative 平台存取。最後,使用 Knative 命令佈署應用程式,自動設定路由與擴縮減機制。實際佈署中,需注意冷啟動造成的延遲,並可透過監控 Pod 狀態確認應用程式運作。

建構容器化單頁應用程式

在現代軟體開發中,容器化技術已經成為佈署和管理應用程式的重要工具。Docker 是目前最流行的容器化平台之一,它允許開發者將應用程式及其依賴項封裝成一個可移植的容器映像檔。在本章中,我們將探討如何使用 Docker 將一個 React 單頁應用程式容器化,並將其佈署到 Knative 平台上。

撰寫 Dockerfile

Dockerfile 是一個文字檔案,用於描述如何建構 Docker 映像檔。對於我們的 React 應用程式,我們需要一個簡單的 Dockerfile,如例 2-6 所示。

例 2-6. 簡單的 React Dockerfile

# 使用 node:18 作為基礎映像檔
FROM node:18

# 設定工作目錄為 /app
WORKDIR /app

# 複製 package*.json 到 Docker 環境中
COPY package*.json ./

# 安裝所有 Node 套件
RUN npm install

# 複製所有檔案到 Docker 環境中
COPY . .

# 暴露應用程式使用的埠號
EXPOSE 3000

# 最終執行應用程式
CMD [ "npm", "start" ]

#### 內容解密:

  1. FROM node:18:使用官方的 Node.js 18 映像檔作為基礎映像檔,確保我們有一個穩定的環境來執行 React 應用程式。
  2. WORKDIR /app:設定容器中的工作目錄為 /app,後續的指令將在這個目錄下執行。
  3. COPY package.json ./*:將 package.jsonpackage-lock.json 複製到容器中的 /app 目錄下。這個步驟確保了依賴項的安裝是根據這些檔案的內容。
  4. RUN npm install:執行 npm install 命令來安裝 package.json 中定義的所有依賴項。
  5. COPY . .:將當前目錄下的所有檔案複製到容器中的 /app 目錄下,確保所有應用程式碼都包含在映像檔中。
  6. EXPOSE 3000:宣告容器將監聽 3000 埠,這是 React 開發伺服器的預設埠號。
  7. CMD [ “npm”, “start” ]:指定容器啟動時執行的命令,這裡是啟動 React 開發伺服器。

建構和測試 Docker 映像檔

一旦我們有了 Dockerfile,就可以透過執行 docker build --tag localhost:5001/dashboard . 命令來建構 Docker 映像檔。這將建立一個名為 localhost:5001/dashboard 的映像檔,可以在本地機器上使用。

接下來,我們可以使用 docker run --publish 127.0.0.1:8080:3000 localhost:5001/dashboard 命令來執行映像檔,並將容器的 3000 埠對映到本地機器的 8080 埠。這樣,我們就可以透過瀏覽器存取 http://localhost:8080 來檢視我們的 React 應用程式。

#### 疑難排解:

  • 使用 docker ps 命令列出正在執行的容器,以檢查是否有任何錯誤或未正確對映的埠。
  • 使用 docker exec 命令進入容器內部,檢查是否有任何檔案遺失或路徑不正確的問題。

將映像檔推播到容器登入檔

在本地測試透過後,我們可以將映像檔推播到容器登入檔,如 localhost:5001。這可以使用 docker push localhost:5001/dashboard 命令完成。

在 Knative 上佈署應用程式

現在,我們可以使用 kn service create dashboard --image localhost:5001/dashboard --port 3000 命令在 Knative 上佈署我們的應用程式。這將建立一個新的服務,並自動組態路由和擴縮減。

佈署完成後,Knative 將提供一個 URL 用於存取我們的應用程式。我們可以使用 kubectl get pods 命令檢查 Pod 的狀態,以確認應用程式正在執行。

#### 冷啟動:

當第一次存取應用程式時,可能會遇到冷啟動的問題,這是因為 Knative 需要時間來提取映像檔並啟動新的 Pod。這種延遲是正常的,但對於使用者經驗有一定的影響。

Knative Serving 請求處理機制解析

Knative Serving 是 Knative 的核心元件,負責處理 HTTP 請求並動態調整 Pod 數量。本章節將探討 Knative Serving 的運作機制,並透過例項展示如何建立一個動態的單頁應用。

請求生命週期

當 Knative 接收到 HTTP 請求時,會根據請求的主機名稱和路徑將請求路由到對應的服務。請求首先會經過 HTTP 負載平衡器,該平衡器負責將不同版本的 HTTP 請求轉換為統一格式。

接著,請求會被轉發到 Activator 元件,該元件負責判斷是否有可用的 Pod 可以處理請求。如果沒有可用的 Pod 或所有 Pod 都忙碌中,Activator 會通知 Autoscaler 需要建立新的 Pod,並暫停請求直到 Pod 就緒。

Autoscaler 負責根據請求數量動態調整 Pod 的數量。當 Pod 就緒後,Activator 會將請求轉發給 Pod 進行處理。

值得注意的是,Pod 本身並不關心請求的生命週期,它只需要處理接收到的 HTTP 請求。Knative Serving 的主要任務是將請求傳遞給 Pod 並收集相關的監控指標。

建立動態單頁應用

本章節將展示如何將一個靜態的單頁應用改造成動態的。首先,我們需要在 public 目錄下新增一個名為 my/dashboard 的靜態檔案,用於存放初始資料。

{
  "items": [
    {
      "title": "First Item",
      "content": "Some Info",
      "image": {
        "url": "https://knative.dev/docs/images/logo/rgb/knative-logo-rgb.png",
        "alt": "Knative logo"
      }
    },
    {
      "title": "Another item",
      "content": "Put some words here"
    }
  ]
}

接著,我們需要修改 Dashboard 元件以動態載入資料。我們使用 fetch API 從 /my/dashboard 路徑取得資料,並使用 setState 更新元件狀態。

import React from 'react';
import { Grid } from '@mui/material';
import DataCard, { CardData } from './DataCard';

interface Params {
  source?: string;
  url?: string;
}

interface State {
  items: CardData[];
}

class Dashboard extends React.Component<Params, State> {
  state: State = {
    items: []
  };

  timer?: NodeJS.Timeout;

  componentDidMount(): void {
    if (this.props.source !== undefined) {
      this.setState(() => ({ items: JSON.parse(this.props.source ?? '[]') }));
    }
    if (this.props.url !== undefined) {
      this.Poll();
    }
  }

  componentWillUnmount(): void {
    clearTimeout(this.timer);
  }

  async Poll(): Promise<void> {
    clearTimeout(this.timer);
    if (this.props.url === undefined) {
      console.log('No URL set');
      return;
    }
    const response = await window.fetch(this.props.url);
    const { items } = await response.json();
    if (response.ok) {
      this.setState(() => ({ items: items }));
    } else {
      console.log('Failed to fetch from %s', this.props.url);
    }
    this.timer = setTimeout(() => this.Poll(), 2000);
  }

  render() {
    return (
      <Grid container justifyContent="center" alignItems="center" spacing={2}>
        {this.state.items.map((value, index) => {
          return (
            <Grid item xs={6} key={index}>
              <DataCard {...value}></DataCard>
            </Grid>
          );
        })}
      </Grid>
    );
  }
}

export default Dashboard;

程式碼解密:

  1. 引入必要的模組:程式碼首先引入了 React 和 Material-UI 的 Grid 元件,以及自定義的 DataCard 元件。
  2. 定義介面與狀態:定義了 ParamsState 介面,分別用於描述元件的屬性(props)和狀態(state)。
  3. 建立 Dashboard 元件:Dashboard 元件是一個類別元件,它繼承自 React.Component,並定義了自己的狀態和生命週期方法。
  4. 生命週期方法:在 componentDidMount 中,如果提供了 sourceurl 屬性,元件會相應地初始化狀態或開始輪詢資料。
  5. 輪詢資料Poll 方法負責從指定的 URL 取得資料,並更新元件狀態。如果取得資料失敗,會在控制檯輸出錯誤訊息。
  6. 渲染元件:在 render 方法中,元件根據狀態中的 items 陣列渲染多個 DataCard 元件。

新增 API

為了使應用程式更加動態,我們將新增一個 Python 伺服器,用於提供動態的 /my/dashboard 路徑。首先,我們需要在 api 子目錄下建立一個簡單的 Python 應用程式。

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/my/dashboard', methods=['GET'])
def get_dashboard():
    data = {
        "items": [
            {"title": "Dynamic Item", "content": "This is a dynamic item"}
        ]
    }
    return jsonify(data)

if __name__ == '__main__':
    app.run(debug=True)

程式碼解密:

  1. 引入 Flask:程式碼首先引入了 Flask 網頁框架。
  2. 建立 Flask 應用:建立了一個新的 Flask 應使用案例項。
  3. 定義路由:使用 @app.route 裝飾器定義了一個路由 /my/dashboard,並指定了 HTTP 方法為 GET。
  4. 處理請求:在 get_dashboard 函式中,回傳了一個包含動態資料的 JSON 回應。
  5. 啟動應用:最後,如果該指令碼是直接執行的,則啟動 Flask 開發伺服器。

建構靜態網站與動態API的整合應用

在現代化的網頁應用開發中,將靜態內容與動態API結合是一種常見的架構模式。本文將探討如何使用Flask框架建立一個能夠提供靜態內容和動態API的應用,並進一步探討如何使用Docker和Knative佈署此應用。

建立靜態網站伺服器

首先,我們使用Flask建立一個簡單的靜態網站伺服器。以下是一個範例程式碼(main.py):

from flask import Flask, send_file, send_from_directory

app = Flask(__name__)

@app.route("/")
def serve_root():
    return send_file("static/index.html")

@app.route("/<path:path>")
def serve_static(path):
    return send_from_directory("static", path)

內容解密:

  1. Flask應用初始化:首先,我們從flask模組匯入必要的類別和函式,並建立一個Flask應使用案例項。
  2. 靜態內容路由:定義了兩個路由,一個用於處理根目錄請求("/"),另一個用於處理其他靜態資源請求("/<path:path>")。
  3. serve_root函式:當使用者請求根目錄時,傳回static/index.html檔案。
  4. serve_static函式:當使用者請求其他路徑時,從static目錄下提供相應的檔案。

為了使這個應用正常運作,我們需要建立一個requirements.txt檔案,並在其中指定所需的Flask版本,例如:

Flask>=2.3.2

建構結合Python與Node.js的Docker映像

為了將靜態網站與動態API結合,我們需要建立一個Docker映像,該映像將同時包含Python環境和編譯後的React應用。以下是一個範例Dockerfile:

# 使用Node.js映像作為第一階段建構環境
FROM node:18 AS package
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY src ./src/
COPY public ./public/
COPY tsconfig.json ./
RUN npm run-script build

# 使用Python映像作為第二階段執行環境
FROM python:3.10-slim AS run
COPY api/requirements.txt .
RUN pip install -r requirements.txt
COPY api .
COPY public ./static/
COPY --from=package /app/build/* ./static/
EXPOSE 3000
CMD [ "python", "-m", "flask", "--app", "main.py", "run", "--host", "0.0.0.0", "--port", "3000" ]

內容解密:

  1. 多階段建構:Dockerfile使用了多階段建構,先使用Node.js環境編譯React應用,再使用Python環境執行Flask應用。
  2. 第一階段建構:在Node.js環境中,安裝依賴、複製必要檔案並編譯React應用。
  3. 第二階段建構:在Python環境中,安裝Flask依賴、複製API程式碼和靜態內容,最後設定執行命令。

新增動態API

接下來,我們可以為應用新增動態API,以提供即時的天氣資訊為例。以下是範例程式碼(main.py新增部分):

import noaa_sdk

noaa = noaa_sdk.NOAA()

def get_weather(zip):
    observations = noaa.get_observations(str(zip), "US")
    current_weather = next(observations)
    content = current_weather["textDescription"]
    image = {'url': current_weather["icon"], 'alt': content}
    if current_weather.get("temperature", {}).get("value", 0):
        unit = current_weather["temperature"]["unitCode"][-1]
        content += f': {current_weather["temperature"]["value"]} {unit}'
    return dict(title=f"Weather at {zip}", content=content, image=image)

@app.route("/my/dashboard")
def serve_dashboard():
    return {"items": [get_weather(96813)]}

內容解密:

  1. 初始化NOAA SDK:建立一個NOAA SDK例項,用於取得天氣資料。
  2. get_weather函式:根據郵遞區號取得當前天氣狀況,包括描述、圖示和溫度資訊。
  3. serve_dashboard函式:提供一個API端點,傳回特定郵遞區號的天氣資訊。