再考 GAE/Goのプロジェクト構成

  • 16
    Like
  • 3
    Comment

以前、GAE/Goでglideを使用する場合のプロジェクト構成についての記事を書いた。
GAE/Go+glide的な構成での環境構築 ~ローカルサーバー立ち上げまで~ - Qiita

あれからしばらくGAE/Goで開発を続けていて、こんな構成もよさそうだなーと思うものが出てきたので、改めてまとめてみる。

前回はパッケージ管理にglideを使用したが、今時depだろjkという気分なので、今後はdepを使って行くことになると思う。

ディレクトリ構成

前回の記事ではこんな感じの構成だった。

$GOPATH(PROJECT_ROOT)
  ├── app
  │   ├── app.yaml
  │   └── main.go
  └── src
      ├── glide.yaml
      ├── glide.lock
      ├── PACKAGE
      └── vendor

つまりは、プロジェクトルートにGOPATHを設定する感じ。

それで今回試すのがこんな構成

$GOPATH
  └── src
       └── PROJECT_ROOT
            ├── Gopkg.lock
            ├── Gopkg.toml
            ├── Makefile
            ├── app
            │   ├── app.yaml
            │   └── main.go
            ├── SUB_PACKAGE
            └── vendor

違いとしては、プロジェクトルートにGOPATHを設定するのをやめ、基本的なGoFlowに沿って、グルーバルなGOPATHにプロジェクトを配置するやり方。

src直下じゃなくて、github.com/{USER}/{REPOSITORY}のように通常のGoプロジェクトのようにしてもOK。

この構成の利点としては、わざわざプロジェクトごとにGOPATHの変更が必要無くなって、goenvとかdirenvで変更する必要が無いから色々と楽。

Makefileはアプリのデプロイとかテスト実行だったり、開発が進むに連れて色々と発生してくる手間を省くためのタスクランナーとして使ってる。
シェルスクリプト書いてもいいけど、その場合でもシェル叩くのはmakeタスクからやらせるようにしている。
タブ保管ができるし、同じ構文で様々なタスク走らせられるので色々捗る。

ぶっちゃけこれで話は終わりなんだけど、せっかくなのでこの構成でアプリデプロイするところまで解説してみようと思う。

サンプルアプリの作成

というわけで、サンプルの作成。
完成版 - github.com

上で解説してるように、まずはこんな感じでプロジェクトを作成する。
サブパッケージ名はわりと適当。

$GOPATH
  └── src
       └── github.com/...
            ├── app
            └── gae

app.yaml

これがなくては始まらないのでapp.yamlを作成する。

app.ayml
runtime: go
api_version: go1.8

handlers:
- url: /.*
  script: _go_app

app.yamlを追加した後はこうなる

$GOPATH
  └── src
       └── github.com/...
            ├── app
            │   └── app.yaml
            └── gae

ハンドラー作成

リクエストを処理するハンドラーを書く。
ハローワールド返すだけ。

sayhello.go
package gae

import (
    "fmt"
    "net/http"
)

func SayHello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello world")
}

これは、サブパッケージ直下に配置してあげる。

$GOPATH
  └── src
       └── github.com/...
            ├── app
            │   └── app.yaml
            └── gae
                └── hello.go

main.go作成

ハンドラーを作っただけじゃアプリは動かない。
作ったハンドラーをルーティングとして設定しなきゃいけないわけで、その設定はapp以下に配置するmain.goにやらせる

main.go
package main

import (
    "net/http"

    "github.com/gorilla/mux"
    "github.com/ryutah/gae-structure-sample/gae"
)

func init() {
    r := mux.NewRouter()
    r.HandleFunc("/", gae.SayHello)
    http.Handle("/", r)
}

せっかくなので、ルーティング処理にはgorilla/muxを使ってる。
ライブラリの取得は普通にgo getでやった。

$ go get -u github.com/gorilla/mux

ここまで終わると、ディレクトリ構成はこうなる

$GOPATH
  └── src
       └── github.com/...
            ├── app
            │   ├── app.yaml
            │   └── main.go
            └── gae
                └── hello.go

ここまで終わると、goapp serve appでローカルサーバは立ち上がるようになる。

Makefile

せっかくなので、いろんなタスクをMakefileに書いていく

.PHONY: all

help: ## Print this help
    @echo 'Usage: make [target]'
    @echo ''
    @echo 'Targets:'
    @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

serve: ## ローカルサーバ実行
    goapp serve app

このMakefileはプロジェクトルートに配置しておく。

$GOPATH
  └── src
       └── github.com/...
            ├── Makefile
            ├── app
            │   ├── app.yaml
            │   └── main.go
            └── gae
                └── hello.go

ローカルサーバを起動したい場合はこんな感じでmakeタスクを実行する

$ make serve

dep

やっとdepを使う。
dep自体は普通にgo getでインストール

$ go get -u github.com/golang/dep/cmd/dep

vendoringのために、プロジェクトルートでdep initを実行

$ dep init

今回のプロジェクトだと、main.gogorilla/muxに依存してるので、勝手にパッケージ取ってきてくれる。
dep initが終わるとこんな感じの構成になる

$GOPATH
  └── src
       └── github.com/...
           ├── Gopkg.lock
           ├── Gopkg.toml
           ├── Makefile
           ├── app
           │   ├── app.yaml
           │   └── main.go
           ├── gae
           │   └── hello.go
           └── vendor
               └── github.com
                   └── gorilla
                       ├── context
                       │   ├── LICENSE
                       略...

というわけで、サンプルアプリ完成。

デプロイしてみる

早速できたアプリをデプロイしてみる。
せっかくなので、これもMakefileで定義しておく。

.PHONY: all

project_id := ${PROJECT_ID}
version := ${GAE_VERSION}

help: ## Print this help
    @echo 'Usage: make [target]'
    @echo ''
    @echo 'Targets:'
    @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

serve: ## ローカルサーバ実行
    goapp serve app

deploy: ## gaeへデプロイ OPTIONS: project_id=${PROJECT_ID} version=${VERSION}
    goapp deploy -application ${project_id} -version ${version} app

プロジェクトIDとバージョンを環境変数を設定するか、引数として指定するようにしてみた。
gcloud app deployを使いたいところだが、そっちはまだGoのVendoringに対応してないので仕方ないのでgoapp deployを使う

というわけでデプロイ。

$ make deploy project_id={PROJECT_ID} version=1
  goapp deploy -application {PROJECT_ID} -version 1 app
  07:28 PM Application: {PROJECT_ID} (was: None); version: 1 (was: None)
  07:28 PM Host: appengine.google.com
  07:28 PM Starting update of app: {PROJECT_ID}, version: 1
  07:28 PM Getting current resource limits.
  07:28 PM Scanning files on local disk.
  07:28 PM Cloning 13 application files.
  07:28 PM Uploading 4 files and blobs.
  07:28 PM Uploaded 4 files and blobs.
  07:28 PM Compilation starting.
  07:28 PM Compilation: 10 files left.
  07:28 PM Compilation completed.
  07:28 PM Starting deployment.
  07:28 PM Checking if deployment succeeded.
  07:28 PM Deployment successful.
  07:28 PM Checking if updated app version is serving.
  07:28 PM Completed update of app: {PROJECT_ID}, version: 1

OK

動作確認

せっかくなので、ちゃんと動くか確認する

$ gcloud app browse --version 1

動作確認.png

ちゃんと動いた。

まとめ

というわけで、駆け足でプロジェクト構成からサンプルアプリの作成までやってみた。
GAE/Goのプロジェクト構成に関するベストプラクティスは未だに無いと思ってるので、今後も色々と試行錯誤していければと思う。

参考

Go でツール書くときの Makefile 晒す - Qiita
Makefileを自己文書化する - POSTD

838contribution

静的ファイルを置く場合はどこが良いのでしょう? appディレクトリ内?:thinking:

127contribution

はい、静的ファイルはappディレクトリに置きます。
僕はGoからHTMLテンプレート使いたいときなんかもappディレクトリに置いてます。

$GOPATH
  └── src
       └── github.com/...
            ├── Makefile
            ├── app
            │   ├── app.yaml
            │   ├── index.html
            │   └── main.go
            └── gae
                └── hello.go
index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Hello</title>
</head>

<body>
  {{.Body}}
</body>

</html>
hello.go
package gae

import (
    "html/template"
    "net/http"
)

func SayHello(w http.ResponseWriter, r *http.Request) {
    t, err := template.ParseFiles("index.html")
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := t.Execute(w, struct{ Body string }{Body: "Hello world!!"}); err != nil {
        http.Error(w, err.Error(), 500)
    }
}
838contribution

ありがとうございます!!:bow: