システム パッケージの使用

このチュートリアルでは、グラフの説明の入力パラメータを PNG 形式の図に変換する Cloud Run for Anthos on Google Cloud カスタム サービスをビルドする方法について説明します。ここでは、Graphviz をシステム パッケージとしてサービスのコンテナ環境にインストールします。リクエストを処理するときに、コマンドライン ユーティリティから Graphviz を使用します。

目標

  • カスタム コンテナを作成し、Dockerfile と一緒にビルドする。
  • Cloud Run サービスを作成、構築、デプロイする。
  • Graphviz dot ユーティリティを使用して図を生成する。
  • コレクションの DOT 構文図または独自の構文図を投稿してサービスをテストする。

料金

このチュートリアルでは、以下を含む Cloud Platform の有料コンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを出すことができます。

新しい Cloud Platform ユーザーは無料トライアルをご利用いただけます。

始める前に

  1. Google アカウントにログインします。

    Google アカウントをまだお持ちでない場合は、新しいアカウントを登録します。

  2. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    [プロジェクトの選択] ページに移動

  3. Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  4. Cloud Run for Anthos on Google Cloud API を有効にする
  5. Cloud SDK をインストールし、初期化します
  6. kubectl コンポーネントをインストールします。
    gcloud components install kubectl
  7. beta コンポーネントをインストールします。
    gcloud components install beta
  8. コンポーネントを更新します。
    gcloud components update
  9. curl をインストールして、サービスを試します。
  10. Cloud Run for Anthos on Google Cloud の設定の手順に沿って新しいクラスタを作成します。

gcloud のデフォルトを設定する

Cloud Run for Anthos on Google Cloud サービスのデフォルトとして gcloud を構成するには:

  1. デフォルト プロジェクトを設定します。

    gcloud config set project PROJECT_ID

    PROJECT_ID は、このチュートリアルで使用するプロジェクトの名前に置き換えます。

  2. クラスタに gcloud を構成します。

    gcloud config set kuberun/cluster CLUSTER-NAME
    gcloud config set kuberun/cluster_location REGION

    次のように置き換えます。

    • CLUSTER-NAME は、クラスタに対して使用した名前に置き換えます。
    • REGION は、選択したサポート対象のクラスタの場所に置き換えます。

コードサンプルを取得する

使用するサンプルコードを取得するには:

  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 for Anthos on Google Cloud のサンプルコードがあるディレクトリに移動します。

    Node.js

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

    Python

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

    Go

    cd golang-samples/kuberun/system_package/

    Java

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

アーキテクチャを可視化する

基本的なアーキテクチャは次のようになります。

ユーザーのリクエストをウェブサービスから graphviz dot ユーティリティに送信する流れ
図のソースについては、DOT の説明をご覧ください。

ユーザーが HTTP リクエストを Cloud Run for Anthos on Google Cloud サービスに送信すると、このサービスが 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 は、フォント サポート用に 2 番目のパッケージを必要とします。
    RUN apk --no-cache add graphviz ttf-ubuntu-font-family

    コンテナ イメージのオペレーティング システムを確認するには、FROM ステートメントで名前を確認するか、ベースイメージの README を確認します。たとえば、node から拡張する場合、Docker Hub でドキュメントと親 Dockerfile を見つけることができます。

  3. ローカルでの docker build または Cloud Build を使用してイメージをビルドし、カスタマイズをテストします。

受信リクエストを処理する

サンプル サービスは、受信 HTTP リクエストのパラメータを使用して、適切な dot ユーティリティ コマンドを実行するシステムコールを呼び出します。

以下の HTTP ハンドラでは、グラフの説明の入力パラメータが dot クエリ文字列変数から抽出されます。

クエリ文字列で使用するために、グラフの説明に含まれている文字の URL エンコードが必要になる場合があります。

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("error: {}".format(e))

        # If no graphviz definition or bad graphviz def, return 400
        if "syntax" in str(e):
            return "Bad Request: {}".format(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 KubeRun"',
    '-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.
    if not dot:
        raise Exception("syntax: no graphviz definition provided")

    dot_args = [  # These args add a watermark to the dot graphic.
        "-Glabel=Made on KubeRun",
        "-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 KubeRun",
		"-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 (%v): %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<String>();
  args.add("/usr/bin/dot");
  args.add("-Glabel=\"Made on KubeRun\"");
  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 パッケージの最新バージョンが使用されるようにします。

ユーザーの入力をコマンドライン パラメータとして受け入れるように現在のサンプルを拡張する場合は、コマンド インジェクション攻撃の対策を講じる必要があります。たとえば、次のような方法でインジェクション攻撃を防ぎます。

  • サポートされているパラメータの辞書と入力をマッピングする
  • 入力値を検証し、既知の安全な範囲内にあることを確認する(正規表現などを使用)
  • シェル構文が評価されないように入力をエスケープする

コードを配布する

コードを配布するには、Cloud Build でビルドを行い、Container Registry にアップロードして、Cloud Run for Anthos on Google Cloud にデプロイします。

  1. 次のコマンドを実行してコンテナをビルドし、Container Registry に公開します。

    Node.js

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

    ここで、PROJECT_ID は GCP プロジェクト ID、graphviz はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Python

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

    ここで、PROJECT_ID は GCP プロジェクト ID、graphviz はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Go

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

    ここで、PROJECT_ID は GCP プロジェクト ID、graphviz はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Java

    このサンプルでは、Jib を使用して一般的な Java ツールにより Docker イメージをビルドします。Jib は、Dockerfile や Docker をインストールせずにコンテナのビルドを最適化します。Jib を使用して Java コンテナを構築する方法の詳細を確認します。

    1. Dockerfile を使用して、インストールしたシステム パッケージでベースイメージの構成とビルドを行い、Jib のデフォルト ベースイメージをオーバーライドします。

      # Use the Official OpenJDK image for a lean production stage of our multi-stage build.
      # https://hub.docker.com/r/adoptopenjdk/openjdk11/
      FROM adoptopenjdk/openjdk11:alpine
      
      RUN apk --no-cache add graphviz ttf-ubuntu-font-family
      gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz-base

      PROJECT_ID は、GCP のプロジェクト ID です。

    2. Jib で最終的なコンテナを作成し、Container Registry で公開します。

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.7.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 は、GCP のプロジェクト ID です。

  2. 次のコマンドを使用してデプロイします。

    gcloud kuberun core services update graphviz-web --create-if-missing --image gcr.io/PROJECT_ID/graphviz

    ここで、PROJECT_ID は GCP プロジェクト ID、graphviz は上述のコンテナ名、graphviz-web はサービス名です。

    デプロイが完了するまで待ちます。30 秒ほどかかる場合があります。

  3. サービスにコードの更新をデプロイする場合は、上記のステップを繰り返します。サービスをデプロイするたびに、リビジョンが作成されます。準備ができると、トラフィックの送信が自動的に開始します。

試してみる

リクエスト ペイロードで DOT 構文の説明を含む HTTP POST リクエストを送信し、サービスを試してみましょう。

  1. サービスに HTTP リクエストを送信します。

    生成された図はウェブページに埋め込むことができます。

    1. Istio Ingress ゲートウェイの外部 IP を取得するには: ```sh kubectl get svc istio-ingress -n gke-system ``` 出力結果は次のようになります。 ```sh NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) istio-ingress LoadBalancer XX.XX.XXX.XX pending 80:32380/TCP,443:32390/TCP,32400:32400/TCP ``` 使用する IP アドレスは、ロードバランサの EXTERNAL-IP です。
    2. この EXTERNAL-IP アドレスを URL に使用して、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. Chrome などの PNG ファイルをサポートするアプリケーションで、結果の 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. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

チュートリアル リソースを削除する

  1. このチュートリアルでデプロイした Cloud Run for Anthos on Google Cloud サービスを削除します。

    gcloud kuberun core services delete SERVICE-NAME

    SERVICE-NAME は、選択したサービス名です。

    Google Cloud Console から Cloud Run for Anthos on Google Cloud サービスを削除することもできます。

  2. チュートリアルを設定したときに追加した gcloud のデフォルト構成を削除します。

     gcloud config unset kuberun/cluster
     gcloud config unset kuberun/cluster_location
    
  3. プロジェクト構成を削除します。

     gcloud config unset project
    
  4. このチュートリアルで作成した他の Google Cloud リソースを削除します。

次のステップ

  • graphviz アプリを試す。
    • 他の graphviz ユーティリティのサポートを追加し、別のアルゴリズムを適用して図を生成する。
    • 図を Cloud Storage に保存する。イメージまたは DOT 構文を保存します。
    • Cloud Natural Language API を使用して、コンテンツの不正利用を防ぐ対策を講じる。
    • イメージの処理でシステム パッケージの別の例を確認する。
  • Google Cloud のその他の機能を試す。チュートリアルをご覧ください。