sci

最果て風呂

Heroku で Go アプリをデプロイするのがすんなりいかなかった

Vim のユーザーマニュアルをスマホで見やすいようにタグづけをしました。コンテンツの内容がコピペなので、著作権的にどうなの?というのがあるのですけれど、ちょっと今コミュニティがざわざわしているので相談し難い雰囲気(あくまで個人の感想です)。とりあえず、怒られたら削除する方針で公開することにしました。

当初は GitHub で公開しようと思ってリポジトリを作成したのですけれど、GitHub は意外と Google に見張られているので表面化しやすい。そこで、Heroku で公開するように方針転換をしました。

Heroku での自分の経験は、データベースを使わない単純なウェブアプリケーションSinatra と Flask で作っただけです。今回のプロジェクトはリクエストを受けたら HTML の内容を返すだけなので、これらのフレームワークを使う必要もありません。Ruby, Python ときたので、Go で作ることにしました。

「go http」で検索すると、いかに簡単に実装ができるのかというページがうじゃうじゃ引っ掛かります。いわゆる “Hello, World!” 的な雛形にちょっと修正をして下記のようにしました。

package main

import (
    "io/ioutil"
    "log"
    "net/http"
    "os"
)

func rootHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(200)
    w.Header().Set("Content-Type", "text/html; charset=utf8")
    content, err := ioutil.ReadFile("usrman.html")
    if err != nil {
        w.Write([]byte("HTMLファイルがないやん"))
    }
    w.Write([]byte(content))
}

func hogeHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(200)
    w.Header().Set("Content-Type", "text/html; charset=utf8")
    w.Write([]byte("<html><body><h1>ほげふがぴよにゃん!</h1></body></html>"))
}

func main() {
    http.HandleFunc("/", rootHandler)
    http.HandleFunc("/hoge", hogeHandler)
    log.Fatal(http.ListenAndServe(":"+os.Getenv("PORT"), nil))
}

関数的に使う例とメソッドチェーン的に使う例とがあり、自分は後者のものを参考にしました。自分が追加したのは PORT を受ける部分と HTML ファイルを読み込んで出力する部分だけです。ファイルの内容を返すのはもっと良い方法があるのかもしれませんね。セキュリティやエラー処理はしていないのが痛いところです。

手元で PORT=$5555 go run main.go として、ブラウザで localhost:5555 にアクセスすると HTML の内容が表示されました!

ここから先は Heroku へのデプロイ作業です。ofuro はすでに使われていたので ofurovim で作成。Procfile の内容を web: PORT=$PORT go run main.go にして PUSH。しかしエラーになってデプロイができません。Go のプロジェクトだと認識できていないみたい。buildpacks?

ここではじめてドキュメントを読んでみましたが、いまいち良くわかりません。これは自分が Heroku のデプロイや Go のパッケージングの仕組みを知らないことによります。govendor, godep, GB のどれかを使う必要があるとのことです。bundler みたいなものかしら?

依存するものなんて無いのですけれど、自分は godep を選んでインストールして進めることに。ProcfileGodeps/Godeps.json を交互に修正しながら試行錯誤すること 6 回目にして “Build succeeded” まで行けるようになりました!けれども、"bash: main: command not found" 等の部分が解決できない……。Heroku には go のコマンドが無い?

思えば今までのプロジェクトはスクリプト言語で、Ruby や Python3 で呼び出していたのですけれども、Go はコンパイルして使われるので、そのバイナリ名でなければいけないのかな?でもその名前ってどうやって指定するの?

プロジェクトのフォルダを ~/.go/src 以下に移動してからプロジェクトフォルダ内で godep save ./... (ドットは 3 つあるよ) を実行します。すると、ImportPath の部分がフォルダの名前に変わった!あ〜 echo "web: $(basename `pwd`)" > Procfile とか解説している人がいたけれども、フォルダ名になるんですね〜。他に依存しているものはないけれども、自分自身が依存物であると考えると良いのかもしれません。

{
    "ImportPath": ".",
    "GoVersion": "go1.8",
    "GodepVersion": "v79",
    "Packages": [
        "./..."
    ],
    "Deps": []
}

{
    "ImportPath": "ofurovim",
    "GoVersion": "go1.8",
    "GodepVersion": "v79",
    "Packages": [
        "./..."
    ],
    "Deps": []
}

そして Procfile も下記のように変更。

web: PORT=$PORT ofurovim

これでデプロイすると動くようになりました。試行錯誤すること 14 回目にしてやっとです。Godeps はもう使わないので削除しました。

http.Request の RequestURI でパスを得られるので、これを使って場合分けをすれば、複数のファイルを表示できそうですね。もっと多くなったらフレームワークを使いますか。

費やした時間が誰かの役に立てば無駄にはならないということで。でも、お風呂でゆっくりできる IT 戦士なんておらんやろな……

追記: Go のマニュアル に書いてあって、ファイルのサーブをするだけなら、下記の記述だけで良いことがわかった。

例えば、main.go のあるディレクトリ内の docs/ 以下を / でサーブするなら、下記のようにすれば良い。なんて簡単なんだろう。

package main
import (
    "net/http"
    "os"
)
func main() {
    http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("docs"))))
    http.ListenAndServe(":"+os.Getenv("PORT"), nil)
}