使用系統套件教學課程

本教學課程說明如何建構自訂 Cloud Run 服務,將圖表說明輸入參數轉換為 PNG 圖片格式的圖表。這項工具使用 Graphviz,並以系統套件的形式安裝在服務的容器環境中。Graphviz 是透過指令列公用程式使用,用於處理要求。

目標

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

費用

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

您可以使用 Pricing Calculator,根據預測用量估算費用。

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

事前準備

  1. 登入 Google Cloud 帳戶。如果您未曾使用過 Google Cloud,歡迎 建立帳戶,親自體驗實際使用我們產品的成效。新客戶還能獲得價值 $300 美元的免費抵免額,用於執行、測試及部署工作負載。
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  5. Verify that billing is enabled for your Google Cloud project.

  6. 啟用 Cloud Run Admin API
  7. 安裝並初始化 gcloud CLI。
  8. 更新元件:
    gcloud components update

必要的角色

如要取得完成教學課程所需的權限,請要求管理員在專案中授予您下列 IAM 角色:

如要進一步瞭解如何授予角色,請參閱「管理專案、資料夾和組織的存取權」。

您或許也能透過自訂角色或其他預先定義的角色,取得必要權限。

設定 gcloud 預設值

如要針對 Cloud Run 服務設定 gcloud 的預設值:

  1. 設定您的預設專案:

    gcloud config set project PROJECT_ID

    將 PROJECT_ID 替換為您為本教學課程建立的專案名稱。

  2. 為所選區域設定 gcloud:

    gcloud config set run/region REGION

    將 REGION 替換為您選擇的支援 Cloud Run 區域。

Cloud Run 位置

Cloud Run 具有區域性,這表示執行 Cloud Run 服務的基礎架構位於特定區域,並由 Google 負責管理,實現該區域內所有可用區之間的備援可用性。

選擇執行 Cloud Run 服務的區域時,主要考量重點在於延遲時間、可用性或耐用性需求。原則上可以選擇距離使用者最近的區域,但也應考慮 Cloud Run 服務所用其他 Google Cloud產品的位置。如果跨多個位置使用 Google Cloud 產品,可能會影響服務的延遲時間和費用。

Cloud Run 可在下列地區使用:

採用級別 1 定價

採用級別 2 定價

  • africa-south1 (約翰尼斯堡)
  • asia-east2 (香港)
  • asia-northeast3 (韓國首爾)
  • asia-southeast1 (新加坡)
  • asia-southeast2 (雅加達)
  • asia-south2 (印度德里)
  • australia-southeast1 (雪梨)
  • australia-southeast2 (墨爾本)
  • europe-central2 (波蘭華沙) 節能綠葉圖示 低二氧化碳
  • europe-west10 (柏林)
  • europe-west12 (杜林) 節能綠葉圖示 低二氧化碳排放
  • europe-west2 (英國倫敦) 節能綠葉圖示 低二氧化碳排放
  • europe-west3 (德國法蘭克福)
  • europe-west6 (瑞士蘇黎世) 節能綠葉圖示 低二氧化碳
  • me-central1 (杜哈)
  • me-central2 (達曼)
  • northamerica-northeast1 (蒙特婁) 節能綠葉圖示 低二氧化碳排放
  • northamerica-northeast2 (多倫多) 節能綠葉圖示 低二氧化碳排放
  • southamerica-east1 (巴西聖保羅) 節能綠葉圖示 低二氧化碳排放
  • southamerica-west1 (智利聖地牙哥) 節能綠葉圖示 低二氧化碳排放
  • us-west2 (洛杉磯)
  • us-west3 (鹽湖城)
  • us-west4 (拉斯維加斯)

如果您已建立 Cloud Run 服務,可以在Google Cloud 控制台的 Cloud Run 資訊主頁中查看區域。

擷取程式碼範例

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

  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. 變更為包含 Cloud Run 程式碼範例的目錄:

    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 要求傳送至 Cloud Run 服務,這個服務會執行 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) 攻擊。防範注入式攻擊的方法包括:

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

如要進一步降低潛在安全漏洞,請使用未獲授權使用 Google Cloud服務的服務帳戶部署服務,而非使用具備常用權限的預設帳戶。因此,本教學課程中的步驟會建立並使用新的服務帳戶。

推送程式碼

如要推送程式碼,請使用 Cloud Build 建構、上傳至 Artifact Registry,並部署至 Cloud Run:

  1. 建立 Artifact Registry:

    gcloud artifacts repositories create REPOSITORY \
        --repository-format docker \
        --location REGION

    更改項目:

    • REPOSITORY:存放區的專屬名稱。在專案中的每個存放區位置,存放區名稱不得重複。
    • REGION:Artifact Registry 存放區要使用的 Google Cloud 區域。
  2. 執行下列指令,建構容器並發布至 Artifact Registry。

    Node.js

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/graphviz

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

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

    Python

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/graphviz

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

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

    Go

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/graphviz

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

    成功後,您會看到「SUCCESS」訊息,其中包含 ID、建立時間和映像檔名稱。映像檔會儲存在 Artifact 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.18_8-jre
      
      RUN apt-get update -y && apt-get install -y \
        graphviz \
        && apt-get clean
      gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/graphviz-base

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

    2. 使用 gcloud 憑證輔助程式,授權 Docker 推送至 Artifact Registry。

      gcloud auth configure-docker

    3. 使用 Jib 建構最終容器,並發布至 Artifact 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=REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/graphviz \
       -Djib.from.image=REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/graphviz-base

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

  3. 使用下列方式部署:

    gcloud

    1. 建立新的服務帳戶。您的程式碼 (包括使用的任何系統套件) 只能使用已授予這個服務帳戶的Google Cloud 服務。
      gcloud iam service-accounts create SA_NAME
      其中 SA_NAME 是您為這個服務帳戶指定的名稱。如果程式碼有錯誤或安全漏洞,就無法存取任何其他 Google Cloud 專案資源。
    2. 部署程式碼,並指定服務帳戶。
      gcloud run deploy graphviz-web --service-account SA_NAME@PROJECT_ID.iam.gserviceaccount.com  --image REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/graphviz
      其中 PROJECT_ID 是您的 Google Cloud 專案 ID,SA_NAME 是您建立的服務帳戶名稱,graphviz 是上述容器的名稱,graphviz-web 則是服務名稱。對「allow unauthenticated」提示回應 Y。如要進一步瞭解 IAM 驗證,請參閱「管理存取權」。
    3. 等待部署作業完成。這項作業大約需要半分鐘。 部署成功後,指令列會顯示服務網址。

    Terraform

    如要瞭解如何套用或移除 Terraform 設定,請參閱「基本 Terraform 指令」。

    下列 Terraform 程式碼會建立 Cloud Run 服務。

    resource "google_service_account" "graphviz" {
      account_id   = "graphviz"
      display_name = "GraphViz Tutorial Service Account"
    }
    
    resource "google_cloud_run_v2_service" "default" {
      name     = "graphviz-example"
      location = "us-central1"
    
      deletion_protection = false # set to "true" in production
    
      template {
        containers {
          # Replace with the URL of your graphviz image
          #   gcr.io/<YOUR_GCP_PROJECT_ID>/graphviz
          image = "us-docker.pkg.dev/cloudrun/container/hello"
        }
    
        service_account = google_service_account.graphviz.email
      }
    }

    將 IMAGE_URL 替換為容器映像檔的參照,例如 us-docker.pkg.dev/cloudrun/container/hello:latest。如果使用 Artifact Registry,則必須先建立存放區 REPO_NAME。網址格式為 LOCATION-docker.pkg.dev/PROJECT_ID/REPO_NAME/PATH:TAG。

    下列 Terraform 程式碼會將 Cloud Run 服務設為公開。

    # Make Cloud Run service publicly accessible
    resource "google_cloud_run_service_iam_member" "allow_unauthenticated" {
      service  = google_cloud_run_v2_service.default.name
      location = google_cloud_run_v2_service.default.location
      role     = "roles/run.invoker"
      member   = "allUsers"
    }
  4. 如果您要將程式碼更新部署到服務,請重複上述步驟。每次部署到服務都會建立一個新的修訂版本,並會在就緒時自動開始處理流量。

立即體驗

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

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

    將網址複製到瀏覽器網址列,並更新 [SERVICE_DOMAIN]:

    https://SERVICE_DOMAIN/diagram.png?dot=digraph Run { rankdir=LR Code -> Build -> Deploy -> Run }

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

    <img src="https://SERVICE_DOMAIN/diagram.png?dot=digraph Run { rankdir=LR Code -> Build -> Deploy -> Run }" />
  2. 在任何支援 PNG 檔案的應用程式 (例如 Chrome) 中開啟產生的 diagram.png 檔案。

    內容應該會類似這樣:

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

您可以瀏覽現成的圖表說明。

  1. 複製所選 .dot 檔案的內容
  2. 將 HTTP 要求傳送到您的服務。

    將網址複製到瀏覽器網址列

    https://SERVICE_DOMAIN/diagram.png?dot=SELECTED DOTFILE CONTENTS

清除所用資源

為避免系統向您的 Google Cloud 帳戶收取額外費用,請刪除您在本教學課程中部署的所有資源。

刪除專案

如果您是為了這個教學課程建立新專案,請刪除專案。如果您使用現有專案,並想保留專案,但不要本教學課程新增的變更,請刪除為本教學課程建立的資源。

如要避免付費,最簡單的方法就是刪除您為了本教學課程所建立的專案。

刪除專案的方法如下:

  1. 前往 Google Cloud 控制台的「Manage resources」(管理資源) 頁面。

    前往「Manage resources」(管理資源)

  2. 在專案清單中選取要刪除的專案,然後點選「Delete」(刪除)。
  3. 在對話方塊中輸入專案 ID,然後按一下 [Shut down] (關閉) 以刪除專案。

刪除教學課程資源

  1. 刪除您在本教學課程中部署的 Cloud Run 服務。Cloud Run 服務在收到要求前不會產生費用。

    如要刪除 Cloud Run 服務,請執行下列指令:

    gcloud run services delete SERVICE-NAME

    將 SERVICE-NAME 改為您的服務名稱。

    您也可以從Google Cloud 控制台刪除 Cloud Run 服務。

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

     gcloud config unset run/region
    
  3. 移除專案設定:

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

    • 從 Artifact Registry 刪除容器映像檔 REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/graphviz。

    • 刪除服務帳戶 SA_NAME。

      gcloud iam service-accounts delete SA_NAME@PROJECT_ID.iam.gserviceaccount.com

後續步驟