Google Cloud Runのポートについて考える


当記事について

2019/11/15にGAになったGCPのコンテナサーバレス実行環境であるGoogle Cloud Runのポートについて理解を深める記事です。
公式ドキュメントのクイックスタートのからポートに関する部分を読み解きつつ解説します。

参考にしたサイト

コンテナ内部で使用するポート

初めに、コンテナ内部で使用しているポートを理会するためにクイックスタートのアプリケーションを見ていきましょう。

/helloworld.go
package main

import (
        "fmt"
        "log"
        "net/http"
        "os"
)

func handler(w http.ResponseWriter, r *http.Request) {
        log.Print("Hello world received a request.")
        target := os.Getenv("TARGET")
        if target == "" {
                target = "World"
        }
        fmt.Fprintf(w, "Hello %s!\n", target)
}

func main() {
        log.Print("Hello world sample started.")

        http.HandleFunc("/", handler)

        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }

        log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

handler関数

handler関数では、環境変数"TARGET"から値を取得して、Helloの後ろにくっつけて引数のResponseWriterに出力しているのが解ります。環境変数"TARGET"がなければ"World"が入ります。

main関数

main関数では以下の二つのことを行っているのが解ります。
1. ルートでリクエストを待ち受けて、リクエストが来た場合に上のhandler関数を呼び出すようにしています。
2. 環境変数"PORT"から値を取得して取得した値のポート番号でリクエストを受け付けます。環境変数"PORT"がなければ"8080"が入ります。

コンテナ内部で動くアプリケーションは環境変数"PORT"の値で動いているのが解ります。

環境変数"PORT"の中身

Google Cloud Runの内部の環境変数"PORT"がどのようになっているのか、クックスタートにあるサンプルソースのmain関数を以下のようにしてビルド&デプロイしてみます。

/helloworld.go
func main() {
        log.Print("Hello world sample started.")

        http.HandleFunc("/", handler)

        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        } else {
            log.Printf("PORT is %s", port)
        }

        log.Printf("Hello world sample is listening on port %s.", port)

        log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
$ gcloud builds submit --tag gcr.io/[PROJECT_ID]/helloworld
(省略)
$ gcloud  run deploy --image gcr.io/[PROJECT_ID]/helloworld
(省略)
Service [helloworld] revision [helloworld-XXXXXX-vof] has been deployed and is serving 100 percent of traffic at https://helloworld-XXXXXXXXX-an.a.run.app

デプロイ後、Loggingでログを確認すると次の画像のようになっていました。

Google Cloud Run上で動くコンテナの環境変数"PORT"は8080であることが解ります。

外部から接続されるポート

外部から接続されるポートを調べるために、実際にcURLを使いアクセスしてみます。

$ curl https://helloworld-XXXXXXXXX-an.a.run.app
Hello World!
$ curl https://helloworld-XXXXXXXXX-an.a.run.app:443
Hello World!
$ curl https://helloworld-XXXXXXXXX-an.a.run.app:80
curl: (35) error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol
$ curl https://helloworld-XXXXXXXXX-an.a.run.app:8080
^C
$ curl https://helloworld-XXXXXXXXX-an.a.run.app:8081
^C

ポート番号443を使用しているのが解ります。8080や8081のように関係のないポートは応答がありませんでした。

外部のポートと内部のポートの紐づけ

外部の443ポートとコンテナ内部の8080ポートが環境変数"PORT"経由で紐づけられているのが解ります。
図示するとこのようになります。

デプロイ時のコマンドにポートを紐づけるオプションは無いので、Dockerコマンドでいう"-p 443:8080"のようなパラメータはデプロイ時に自動で行ってくれていることになります。

待ち受けポート実装の仕方

デプロイ時に自動で紐づけられるということは、コンテナ内部で待ち受けるポートはGoogle Cloud Runの制約として決められているということになりますね。Cloud Runの制約を読んでみると、コンテナ内部で待ち受けるポートは環境変数"PORT"に収められていると書いてあります。
Google Cloud Runで動かすアプリケーションは環境変数"PORT"を取得してその番号のポートで待ち受けるようにしましょう。

また、既に構築済みのコンテナをGoogle Cloud Runにデプロイする場合は待ち受けポートが何になっているかをきちんと確認するようにしましょう。