教程:使用系统软件包


本教程介绍如何构建自定义 Cloud Run 服务,以将图表说明输入参数转换为 PNG 图片格式的图表。本教程使用 Graphviz,并将其作为系统软件包安装在服务的容器环境中。您可以通过命令行实用程序使用 Graphviz 来处理请求。

目标

  • 使用 Dockerfile 编写并构建一个自定义容器
  • 编写、构建并部署 Cloud Run 服务
  • 使用 Graphviz dot 实用程序生成图表
  • 通过使用 DOT 语法发布集合中的图表或您自己创建的图表来测试服务

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

准备工作

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure 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.

    Go to project selector

  5. Make sure 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 价格

如果您已创建 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/

直观呈现架构

基本架构如下所示:

该图表显示了从用户到 Web 服务再到 graphviz dot 实用程序的请求流程。
如需了解图表来源,请参阅 DOT 说明

用户向 Cloud Run 服务发出 HTTP 请求,然后该服务执行 Graphviz 实用程序来将请求转换为图片,并将得到的图片以 HTTP 响应形式传递给用户。

了解代码

使用 Dockerfile 定义您的环境配置

您的 Dockerfile 取决于服务将使用的语言和基础运营环境,例如 Ubuntu。

《快速入门:构建和部署》中介绍了各种 Dockerfiles,您可以使用它们作为参考来为其他服务构建 Dockerfile

此服务还需要一个或多个其他系统软件包,但这些软件包不会默认提供。

  1. 在编辑器中打开 Dockerfile

  2. 查找 Dockerfile RUN 语句。该语句允许运行任意 shell 命令来修改环境。如果 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 ttf-ubuntu-font-family

    要确定容器映像的操作系统,请检查 FROM 语句中的名称或与基础映像关联的 README 文件。例如,如果您的 Dockerfile 从 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;
    });

您需要区分内部服务器错误和无效用户输入。此示例服务会为所有 dot 命令行错误都返回一个内部服务器错误,除非错误消息包含指示用户输入问题的 syntax 字符串。

生成图表

图表生成的核心逻辑是使用 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 工具中的任何漏洞都会成为 Web 服务的潜在漏洞。通过定期重新构建容器映像,您可以使用 graphviz 软件包的最新版本来缓解这种情况。

如果您将当前示例扩展为接受用户输入作为命令行参数,则应防范命令注入攻击。以下是可以防范此类注入攻击的一些方法:

  • 将输入映射到由受支持参数组成的字典
  • 验证输入是否匹配一系列已知安全的值(可能使用正则表达式)
  • 对输入进行转义,以确保不对 shell 语法进行评估

您可以通过使用未被授予使用 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 是您要为服务指定的名称。

    成功完成后,您将看到一条包含 ID、创建时间和映像名称的 SUCCESS 消息。 该映像存储在 Artifact Registry 中,并可根据需要重复使用。

    Python

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

    其中 PROJECT_ID 是您的 Google Cloud 项目 ID,graphviz 是您要为服务指定的名称。

    成功完成后,您将看到一条包含 ID、创建时间和映像名称的 SUCCESS 消息。 该映像存储在 Artifact Registry 中,并可根据需要重复使用。

    Go

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

    其中 PROJECT_ID 是您的 Google Cloud 项目 ID,graphviz 是您要为服务指定的名称。

    成功完成后,您将看到一条包含 ID、创建时间和映像名称的 SUCCESS 消息。 该映像存储在 Artifact Registry 中,并可根据需要重复使用。

    Java

    此示例使用 Jib 利用常见 Java 工具构建 Docker 映像。无需编写 Dockerfile 或安装 Docker,Jib 便可以优化容器构建。详细了解如何使用 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.12_7-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 是服务的名称。在出现“允许未经身份验证的调用”提示时回复 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. 如果要将代码更新部署到该服务,请重复执行上述步骤。向服务执行的每次部署都会创建一个新的修订版本,该修订版本准备就绪后会自动开始处理流量。

试试看

要试用您的服务,您可以发送 HTTP POST 请求,并使用 DOT 语法在请求负载中添加说明。

  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

清理

如果您为本教程创建了一个新项目,请删除项目。 如果您使用的是现有项目,希望保留此项目且不保留本教程中添加的任何更改,请删除为教程创建的资源

删除项目

为了避免产生费用,最简单的方法是删除您为本教程创建的项目。

如需删除项目,请执行以下操作:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

删除教程资源

  1. 删除您在本教程中部署的 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

后续步骤