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

#当記事について
2019/11/15に[GAになった](https://cloud.google.com/blog/products/serverless/knative-based-cloud-run-services-are-ga)GCPのコンテナサーバレス実行環境であるGoogle Cloud Runのポートについて理解を深める記事です。
公式ドキュメントのクイックスタートのからポートに関する部分を読み解きつつ解説します。

#参考にしたサイト

 - [クイックスタート: ビルドとデプロイ](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)
 - [Cloud Runの制約](https://cloud.google.com/run/docs/reference/container-contract)


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

```/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でログを確認すると次の画像のようになっていました。
![Logging](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168941/553e1825-e8f7-9e22-882f-bef69cbad3a4.png)

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"経由で紐づけられているのが解ります。
図示するとこのようになります。

![ポートの紐づけ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168941/aaf8cfe8-2329-c8ef-04d3-c4601debbe33.png)

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

#待ち受けポート実装の仕方
デプロイ時に自動で紐づけられるということは、コンテナ内部で待ち受けるポートはGoogle Cloud Runの制約として決められているということになりますね。[Cloud Runの制約](https://cloud.google.com/run/docs/reference/container-contract)を読んでみると、コンテナ内部で待ち受けるポートは環境変数"PORT"に収められていると書いてあります。
Google Cloud Runで動かすアプリケーションは環境変数"PORT"を取得してその番号のポートで待ち受けるようにしましょう。

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