概要
GolangのWebフレームワークであるGinを使ったAPIサーバーを、Dockerコンテナ上にデプロイして動かそうとしたところ、APIにアクセスできずかなり長い時間悩まされました。
結果的には、Ginサーバーのコードの書き方の問題だったことが分かったのですが、解決方法を念のためここにメモしておきます。
ちなみに、このエラーはWindows10及びAWS上のUbuntuサーバー(t2.small)で起こりました(尤も、実行環境はこのエラーの発生にあまり関係ないようでしたが)。
状況
Ginを用いたAPIサーバーを立てようとしていました。まだ環境構築の段階なので、コードは以下のようなモックのものになっています。
package main
import (
"log"
"os"
"github.com/gin-gonic/gin"
)
func main() {
logConfig()
r := gin.Default()
r.GET("/accounting-api", func(c *gin.Context) {
log.Println("GET")
c.JSON(200, gin.H{
"state": "success",
})
})
r.DELETE("/accounting-api", func(c *gin.Context) {
log.Println("DELETE")
c.JSON(200, gin.H{
"state": "success",
})
})
log.Println("Start Server")
r.Run()
}
func logConfig() {
logFile, _ := os.OpenFile("log/log.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
log.SetOutput(logFile)
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
log.SetPrefix("[LOG] ")
}
単純に、GET
かDELETE
メソッドで特定のパスへのリクエストが来たら、{"state": "success"}
というjsonを返すだけのサーバーです。
そして、このサーバーを動かすためのDockerfileが以下です。
FROM golang:alpine
RUN apk update && apk add --no-cache git
RUN go get -u github.com/gin-gonic/gin && mkdir /usr/src && mkdir /usr/src/api
COPY ./api /usr/src/api
WORKDIR /usr/src/api
CMD ["go","run","main.go"]
ホスト上のapi
というディレクトリには上記のGoファイル等があるため、それをコンテナ上にコピーして、サーバーを立ち上げます。このDockerfileをapi
という名前でビルドして、それを以下のコマンドで立ち上げました。
docker run -p 8083:8080 api
ホスト上のポート8083
をコンテナ上のポート8080
にマッピングしています。上記のコマンドを実行すると、以下のような出力がなされ、Ginサーバーが立ち上がっていることが確認できます。
[GIN-debug] GET /accounting-api --> main.main.func1 (3 handlers)
[GIN-debug] DELETE /accounting-api --> main.main.func4 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on localhost:8080
ポートを指定しなかったので、デフォルトでポート8080
で動いています。このように一見ちゃんと動いているようにも見えますが、ホスト側でhttp:localhost:8083
にcurlでアクセスしてみても、以下のようにエラーが出てしまいました。
curl: (52) Empty reply from server
エラー解消のため試みたこと
1. コンテナの中に入って、APIにアクセスできるか確認
まず最初に、コンテナ上で本当にGoのプログラムがちゃんと動いているのかを確認します。そのために立ち上げたdockerコンテナの中に入ってみます。
# apiサーバーの動くコンテナの中に入る
docker exec -it api /bin/ash
このコンテナのベースとなっているAlpineには/bin/bash
がなかったので、/bin/ash
を使います。そして以下のコマンドを打って、ちゃんとプログラムが動いているのか確かめます。
# そもそもcurlが入っていないので、インストールする
apk add --no-cache curl
# 念のためプロキシを無効にして、curlでapiサーバーにアクセスする
curl -x "" http://localhost:8080/accounting-api
curlの実行結果がこちら。
{"state": "success"}
ちゃんと結果が取れています。なので、コンテナ上ではちゃんとGinサーバーのプログラムが動いているようです。
2. DockerfileでポートをEXPOSE
コンテナ上ではプログラムは動いているようなので、次はポートマッピングの部分がうまくいっていないのではないかと疑ってみます。調べてみるとDockerfileにはEXPOSE
というコマンドが書けるようなので、追加してみます。
EXPOSE 8080
これをやっても、解決しませんでした。
そもそも公式ドキュメントによると、EXPOSE
コマンドは実際は何の働きもせず、特定のポートを開放する旨を開発者に知らせるための、ドキュメントのような役割しかもっていないようです。なので、EXPOSE
コマンドをつけただけで問題が解決するはずがありませんでした。
3. Windowsのファイアウォールの設定の確認
開発は基本的にWindows上のDockerで行っていたため、Windowsのファイアウォールの設定を見直してみましたが、これも意味がありませんでした。
そもそも、このDockerfileをUbuntu上でビルドして立ち上げてみても、同様にAPIサーバーにアクセスできなかったため、最初からなんとなくWindowsのファイアウォールのせいではないことが分かっていましたが。
4. (これで解決)Ginサーバー側でポートの指定
Goのプログラムの中で、GInサーバーを立ち上げる部分でポートを指定するようにしたところ、上手くアクセスできるようになりました。具体的には、以下の部分です。
r := gin.Default()
r.Run(":8080")
何も指定しなくてもデフォルトで8080
で立ち上がるため気にしていなかったのですが、しっかりと指定しないとどうやらダメなようです。
なぜポートはデフォルトではダメで、明示的に記さなければならないかは、よくわかりません。分かったら追記します。