使用系統套件


本教學課程說明如何建構自訂的 Knative 服務,這項服務可將圖表說明輸入參數轉換成 PNG 圖片格式的圖表。它使用 Graphviz,並且會安裝為服務容器環境中的系統套件。您可透過指令列公用程式使用 Graphviz 提供要求。

目標

  • 使用 Dockerfile 撰寫並建構自訂容器
  • 撰寫、建構及部署 Knative serving 服務
  • 使用 Graphviz dot 公用程式產生圖表
  • 從收集內容或您自己的創作張貼 DOT 語法圖表來測試服務

費用

在本文件中,您會使用 Google Cloud的下列計費元件:

如要根據預測用量估算費用,請使用 Pricing Calculator

初次使用 Google Cloud 的使用者可能符合免費試用資格。

事前準備

擷取程式碼範例

如要擷取要使用的程式碼範例:

  1. 將應用程式存放區範例複製到本機電腦中:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    您也可以 下載 zip 格式的範例,然後解壓縮該檔案。

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    您也可以 下載 zip 格式的範例,然後解壓縮該檔案。

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    您也可以 下載 zip 格式的範例,然後解壓縮該檔案。

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    您也可以 下載 zip 格式的範例,然後解壓縮該檔案。

  2. 變更為包含 Knative serving 範例程式碼的目錄:

    Node.js

    cd nodejs-docs-samples/run/system-package/

    Python

    cd python-docs-samples/run/system-package/

    Go

    cd golang-samples/run/system_package/

    Java

    cd java-docs-samples/run/system-package/

將架構視覺化

基本架構如下所示:

顯示從使用者到網路服務,再到 graphviz dot 公用程式的要求流程圖表。
如需圖表來源,請參閱 DOT 說明

使用者將 HTTP 要求傳送至 Knative 服務服務,這個服務會執行 Graphviz 公用程式,將要求轉換成圖片,再將該圖片做為 HTTP 回應傳送給使用者。

瞭解程式碼

使用 Dockerfile 定義環境設定

您的 Dockerfile 僅適用於您的服務使用的語言和基本作業環境 (例如 Ubuntu)。

這項服務需要一或多個額外系統套件 (預設不提供)。

  1. 在編輯器中開啟 Dockerfile

  2. 尋找 Dockerfile RUN 陳述式。這個陳述式允許執行任意殼層指令來修改環境。如果 Dockerfile 有多個階段,可藉由尋找多個 FROM 陳述式加以識別,您會在最後的階段中找到它。

    需要的特定套件和安裝機制,會因容器內部宣告的作業系統而異。

    如要取得作業系統或基本映像檔適用的操作說明,請按一下相關的分頁標籤。

    Debian/Ubuntu
    RUN apt-get update -y && apt-get install -y \
      graphviz \
      && apt-get clean
    Alpine
    Alpine 需要第二個套件才能取得字體支援。
    RUN apk --no-cache add graphviz

    如果要判斷容器映像檔的作業系統,請查看 FROM 陳述式中的名稱或與基本映像檔相關聯的 README。舉例來說,如果您從 node 擴充,可以在 Docker Hub 找到說明文件和父項 Dockerfile

  3. 使用 docker build 本機Cloud Build 建構映像檔,測試自訂項目。

處理傳入要求

範例服務使用傳入 HTTP 要求中的參數來叫用系統呼叫,以執行適當的 dot 公用程式指令。

在下面的 HTTP 處理常式中,圖表說明輸入參數是從 dot 查詢字串變數中擷取出的。

圖表說明所包含的字元必須使用網址編碼,以便在查詢字串中使用。

Node.js

app.get('/diagram.png', (req, res) => {
  try {
    const image = createDiagram(req.query.dot);
    res.setHeader('Content-Type', 'image/png');
    res.setHeader('Content-Length', image.length);
    res.setHeader('Cache-Control', 'public, max-age=86400');
    res.send(image);
  } catch (err) {
    console.error(`error: ${err.message}`);
    const errDetails = (err.stderr || err.message).toString();
    if (errDetails.includes('syntax')) {
      res.status(400).send(`Bad Request: ${err.message}`);
    } else {
      res.status(500).send('Internal Server Error');
    }
  }
});

Python

@app.route("/diagram.png", methods=["GET"])
def index():
    """Takes an HTTP GET request with query param dot and
    returns a png with the rendered DOT diagram in a HTTP response.
    """
    try:
        image = create_diagram(request.args.get("dot"))
        response = make_response(image)
        response.headers.set("Content-Type", "image/png")
        return response

    except Exception as e:
        print(f"error: {e}")

        # If no graphviz definition or bad graphviz def, return 400
        if "syntax" in str(e):
            return f"Bad Request: {e}", 400

        return "Internal Server Error", 500

Go


// diagramHandler renders a diagram using HTTP request parameters and the dot command.
func diagramHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		log.Printf("method not allowed: %s", r.Method)
		http.Error(w, fmt.Sprintf("HTTP Method %s Not Allowed", r.Method), http.StatusMethodNotAllowed)
		return
	}

	q := r.URL.Query()
	dot := q.Get("dot")
	if dot == "" {
		log.Print("no graphviz definition provided")
		http.Error(w, "Bad Request", http.StatusBadRequest)
		return
	}

	// Cache header must be set before writing a response.
	w.Header().Set("Cache-Control", "public, max-age=86400")

	input := strings.NewReader(dot)
	if err := createDiagram(w, input); err != nil {
		log.Printf("createDiagram: %v", err)
		// Do not cache error responses.
		w.Header().Del("Cache-Control")
		if strings.Contains(err.Error(), "syntax") {
			http.Error(w, "Bad Request: DOT syntax error", http.StatusBadRequest)
		} else {
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}
}

Java

get(
    "/diagram.png",
    (req, res) -> {
      InputStream image = null;
      try {
        String dot = req.queryParams("dot");
        image = createDiagram(dot);
        res.header("Content-Type", "image/png");
        res.header("Content-Length", Integer.toString(image.available()));
        res.header("Cache-Control", "public, max-age=86400");
      } catch (Exception e) {
        if (e.getMessage().contains("syntax")) {
          res.status(400);
          return String.format("Bad Request: %s", e.getMessage());
        } else {
          res.status(500);
          return "Internal Server Error";
        }
      }
      return image;
    });

您需要分清楚內部伺服器錯誤與無效的使用者輸入。除非錯誤訊息含有 syntax 字串來表示使用者輸入問題,否則對於所有 dot 指令列錯誤,這個範例服務都會傳回內部伺服器錯誤。

產生圖表

產生圖表的核心邏輯使用 dot 指令列工具,將圖表說明輸入參數處理為 PNG 圖片格式的圖表。

Node.js

// Generate a diagram based on a graphviz DOT diagram description.
const createDiagram = dot => {
  if (!dot) {
    throw new Error('syntax: no graphviz definition provided');
  }

  // Adds a watermark to the dot graphic.
  const dotFlags = [
    '-Glabel="Made on Cloud Run"',
    '-Gfontsize=10',
    '-Glabeljust=right',
    '-Glabelloc=bottom',
    '-Gfontcolor=gray',
  ].join(' ');

  const image = execSync(`/usr/bin/dot ${dotFlags} -Tpng`, {
    input: dot,
  });
  return image;
};

Python

def create_diagram(dot):
    """Generates a diagram based on a graphviz DOT diagram description.

    Args:
        dot: diagram description in graphviz DOT syntax

    Returns:
        A diagram in the PNG image format.
    """
    if not dot:
        raise Exception("syntax: no graphviz definition provided")

    dot_args = [  # These args add a watermark to the dot graphic.
        "-Glabel=Made on Cloud Run",
        "-Gfontsize=10",
        "-Glabeljust=right",
        "-Glabelloc=bottom",
        "-Gfontcolor=gray",
        "-Tpng",
    ]

    # Uses local `dot` binary from Graphviz:
    # https://graphviz.gitlab.io
    image = subprocess.run(
        ["dot"] + dot_args, input=dot.encode("utf-8"), stdout=subprocess.PIPE
    ).stdout

    if not image:
        raise Exception("syntax: bad graphviz definition provided")
    return image

Go


// createDiagram generates a diagram image from the provided io.Reader written to the io.Writer.
func createDiagram(w io.Writer, r io.Reader) error {
	stderr := new(bytes.Buffer)
	args := []string{
		"-Glabel=Made on Cloud Run",
		"-Gfontsize=10",
		"-Glabeljust=right",
		"-Glabelloc=bottom",
		"-Gfontcolor=gray",
		"-Tpng",
	}
	cmd := exec.Command("/usr/bin/dot", args...)
	cmd.Stdin = r
	cmd.Stdout = w
	cmd.Stderr = stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("exec(%s) failed (%w): %s", cmd.Path, err, stderr.String())
	}

	return nil
}

Java

// Generate a diagram based on a graphviz DOT diagram description.
public static InputStream createDiagram(String dot) {
  if (dot == null || dot.isEmpty()) {
    throw new NullPointerException("syntax: no graphviz definition provided");
  }
  // Adds a watermark to the dot graphic.
  List<String> args = new ArrayList<>();
  args.add("/usr/bin/dot");
  args.add("-Glabel=\"Made on Cloud Run\"");
  args.add("-Gfontsize=10");
  args.add("-Glabeljust=right");
  args.add("-Glabelloc=bottom");
  args.add("-Gfontcolor=gray");
  args.add("-Tpng");

  StringBuilder output = new StringBuilder();
  InputStream stdout = null;
  try {
    ProcessBuilder pb = new ProcessBuilder(args);
    Process process = pb.start();
    OutputStream stdin = process.getOutputStream();
    stdout = process.getInputStream();
    // The Graphviz dot program reads from stdin.
    Writer writer = new OutputStreamWriter(stdin, "UTF-8");
    writer.write(dot);
    writer.close();
    process.waitFor();
  } catch (Exception e) {
    System.out.println(e);
  }
  return stdout;
}

設計安全的服務

dot 工具的任何安全漏洞都是網路服務的潛在安全漏洞。您可以透過定期重新建構容器映像檔,使用最新版 graphviz 套件來減緩這種情況。

如果您將目前的範例擴充為接受使用者輸入做為指令列參數,則應防範指令植入 (command-injection) 攻擊。防範植入攻擊的幾個方法包括:

  • 將輸入內容對應至支援參數的字典
  • 驗證輸入內容是否與範圍廣泛的已知安全值相符 (可使用規則運算式)
  • 逸出輸入內容以確保不會評估殼層語法

推送程式碼

如要推送您的程式碼,您可使用 Cloud Build 建構程式碼,並上傳至 Container Registry,然後部署到 Knative 服務:

  1. 執行下列指令以建構您的容器,然後發布到 Container Registry。

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz

    其中 PROJECT_ID 是您的 Google Cloud 專案 ID,graphviz 則是您要給予服務的名稱。

    若成功執行,您會看到包含 ID、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔儲存在 Container Registry 中,日後如有需要,可以重複使用。

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz

    其中 PROJECT_ID 是您的 Google Cloud 專案 ID,graphviz 則是您要給予服務的名稱。

    若成功執行,您會看到包含 ID、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔儲存在 Container Registry 中,日後如有需要,可以重複使用。

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz

    其中 PROJECT_ID 是您的 Google Cloud 專案 ID,graphviz 則是您要給予服務的名稱。

    若成功執行,您會看到包含 ID、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔儲存在 Container Registry 中,日後如有需要,可以重複使用。

    Java

    本範例使用 Jib,透過常見的 Java 工具建構 Docker 映像檔。Jib 可最佳化容器建構作業,不需要 Dockerfile,也不必安裝 Docker。進一步瞭解如何使用 Jib 建構 Java 容器

    1. 使用 Dockerfile 設定及建構基本映像檔,並安裝系統套件來覆寫 Jib 的預設基本映像檔:

      # Use the Official eclipse-temurin image for a lean production stage of our multi-stage build.
      # https://hub.docker.com/_/eclipse-temurin/
      FROM eclipse-temurin:17.0.15_6-jre
      
      RUN apt-get update -y && apt-get install -y \
        graphviz \
        && apt-get clean
      gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz-base

      其中 PROJECT_ID 是您的 Google Cloud 專案 ID。

    2. 使用 Jib 建構最終容器,並發布至 Container Registry:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.4.0</version>
        <configuration>
          <from>
            <image>gcr.io/PROJECT_ID/graphviz-base</image>
          </from>
          <to>
            <image>gcr.io/PROJECT_ID/graphviz</image>
          </to>
        </configuration>
      </plugin>
      mvn compile jib:build \
       -Dimage=gcr.io/PROJECT_ID/graphviz \
       -Djib.from.image=gcr.io/PROJECT_ID/graphviz-base

      其中 PROJECT_ID 是您的 Google Cloud 專案 ID。

  2. 使用下列指令進行部署:

    gcloud run deploy graphviz-web --create-if-missing --image gcr.io/PROJECT_ID/graphviz

    其中 PROJECT_ID 是您的 Google Cloud 專案 ID,graphviz 是上述的容器名稱,graphviz-web 則是服務名稱。

    請等待部署完成,這可能需要半分鐘的時間。

  3. 如果您要將程式碼更新部署到服務,請重複上述步驟。每次部署到服務都會建立一個新的修訂版本,並會在就緒時自動開始處理流量。

立即體驗

如要開始試用您的服務,請在要求酬載中搭配 DOT 語法說明來傳送 HTTP POST 要求。

  1. 將 HTTP 要求傳送到您的服務。

    您可以在網頁中嵌入圖表:

    1. 如要取得負載平衡器的外部 IP,請執行下列指令:

      kubectl get svc istio-ingressgateway -n ASM-INGRESS-NAMESPACE

      ASM-INGRESS-NAMESPACE 替換為 Cloud Service Mesh Ingress 所在的命名空間。如果您使用預設設定安裝 Cloud Service Mesh,請指定 istio-system

      輸出結果類似如下:

      NAME                   TYPE           CLUSTER-IP     EXTERNAL-IP  PORT(S)
      istio-ingressgateway   LoadBalancer   XX.XX.XXX.XX   pending      80:32380/TCP,443:32390/TCP,32400:32400/TCP

      其中,EXTERNAL-IP 值是負載平衡器的外部 IP 位址。

    2. 使用網址中的 EXTERNAL-IP 位址來執行 curl 指令。請勿加入通訊協定 (例如:http://) 位於 SERVICE_DOMAIN

      curl -G -H "Host: SERVICE_DOMAIN" http://EXTERNAL-IP/diagram.png \
         --data-urlencode "dot=digraph Run { rankdir=LR Code -> Build -> Deploy -> Run }" \
         > diagram.png
  2. 使用任何支援 PNG 檔案的應用程式 (例如 Chrome) 開啟產生的 diagram.png 檔案。

    內容應該會類似這樣:

    顯示程式碼從建構、部署到「執行」的階段流程圖表。
    來源: DOT 說明

您可以瀏覽一小組現成的圖表說明

  1. 複製所選 .dot 檔案的內容
  2. 將其貼到 curl 指令中:

    curl -G -H "Host: SERVICE_DOMAIN" http://EXTERNAL-IP/diagram.png \
    --data-urlencode "dot=digraph Run { rankdir=LR Code -> Build -> Deploy -> Run }" \
    > diagram.png

清除所用資源

您可以刪除為本教學課程建立的資源,以免產生費用。

刪除教學課程資源

  1. 刪除您在本教學課程中部署的 Knative Serving 服務:

    gcloud run services delete SERVICE-NAME

    其中 SERVICE-NAME 是您選擇的服務名稱。

    您也可以從Google Cloud 控制台刪除 Knative 服務:

    前往 Knative serving

  2. 移除您在教學課程設定期間新增的 gcloud 預設設定:

     gcloud config unset run/platform
     gcloud config unset run/cluster
     gcloud config unset run/cluster_location
    
  3. 移除專案設定:

     gcloud config unset project
    
  4. 刪除在本教學課程中建立的其他 Google Cloud 資源:

後續步驟

  • 利用您的 graphviz 應用程式進行實驗:
    • 針對可將不同演算法套用至圖表產生作業的其他 graphviz 公用程式,新增對這類公用程式的支援。
    • 將圖表儲存到 Cloud Storage。是否要儲存圖片或 DOT 語法?
    • 利用 Cloud Natural Language API 實作內容濫用防護。
  • 探索 Google Cloud 的參考架構、圖表和最佳做法。 歡迎瀏覽我們的雲端架構中心