Help us understand the problem. What is going on with this article?

Google App Engine に Go言語 1.13 の複数サービスを deploy する

はじめに

ちょうど3日前に Google App Engine (GAE) と Go言語を初めて触ってみました。
Go言語がバージョン 11 以降、少し勝手が違ったことで、構築時に若干迷ったので記事にしてみました。

いろんなドキュメントを見て判断はしていますが、なんせ経験が浅いので間違い等あれば指摘ください。

はじめる前に…

Go言語と GAE について私が学んだサイト等まとめておきます。

環境

OS

  • Windows 10

言語

  • go version go1.13.6 windows/amd64

どういう構成にしようか。

まずは GAE の複数サービス構築についてのドキュメントを読んでみる。

まず、私が見たのは下記サイト。

英語は自身無いのですが、要約すると、下記のような感じしょうか。
- app.yaml でアプリケーションを定義します
- service1.yaml, service2.yaml みたいにサービス分けることができる
- ディレクトリ構造はサービス毎にディスプレイ切ってもいいし、yaml をルートに置いてソースコードだけディレクトリ切ってもいい
- dispatch.yaml, index.yaml, cron.yaml などの定義ファイルはルートに置いてね
- default サービス (app.yaml) は最初に必ず deploy してね

どうやら Go言語にはディレクトリ構造の流儀みたいなのがあるらしい

詳しくは下記を参照ください。
- Goにはディレクトリ構成のスタンダードがあるらしい。
- Golangプロジェクトのディレクトリ構成について考えてみた
- Golangパッケージの配置ルールとディレクトリ構成

目標のディレクトリ構成

GAE と Go言語の流儀から、私はこんな感じがいいんでないかな?と思いました。

gae-sample (application root directory)
+-- dispatch.yaml
+-- app.yaml
+-- service1.yaml
+-- src
    +-- cmd
    |   +-- default
    |   |   +-- main.go
    |   |   +-- main_test.go
    |   +-- service1
    |       +-- main.go
    |       +-- main_test.go
    +-- api
    +-- pkg
    +-- lib

※ api, pkg, lib はこの記事では使用しません。

環境構築

アプリケーションのルートディレクトリに移動して git init

$ cd gae-sample
$ git init

Go言語の初期化(モジュール化)

$ go mod init gae-sample

パッと書いていますが、ここも迷った点で、
Go言語の 11 以降はモジュール化すれば $GOPATH 配下にいなくても OK ?

GAE の yaml ファイル作成

$ type NUL > app.yaml
$ type NUL > dispatch.yaml
$ type NUL > service1.yaml
app.yaml
# デフォルト app.yaml です。

service: default
runtime: go113
main: ./src/cmd/default
service1.yaml
# service1 の app.yaml です。

service: service1
runtime: go113
main: ./src/cmd/service1
dispatch.yaml
dispatch:
  - url: "*/service1/*"
    service: service1

Go言語の必要ディレクトリを作成

※必要なければスキップしてください。

$ mkdir src
$ cd src
$ mkdir cmd
$ mkdir pkg
$ mkdir lib

main package を作成

$ cd cmd
$ mkdir default
$ mkdir service1
$ cd default
$ echo package main > main.go
$ echo package main > main_test.go
$ cd ..\service1
$ echo package main > main.go
$ echo package main > main_test.go

main.go は GAE の Quickstart でも動かした helloworld さんを使います。(service1 で若干改修)
ほんと、コピメでごめんなさい。

cmd/default/main.go
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// [START gae_go111_app]

// Sample helloworld is an App Engine app.
package main

// [START import]
import (
    "fmt"
    "log"
    "net/http"
    "os"
)

// [END import]
// [START main_func]

func main() {
    http.HandleFunc("/", indexHandler)

    // [START setting_port]
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("Defaulting to port %s", port)
    }

    log.Printf("Listening on port %s", port)
    if err := http.ListenAndServe(":"+port, nil); err != nil {
        log.Fatal(err)
    }
    // [END setting_port]
}

// [END main_func]

// [START indexHandler]

// indexHandler responds to requests with our greeting.
func indexHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
    fmt.Fprint(w, "Hello, World!")
}

// [END indexHandler]
// [END gae_go111_app]
cmd/default/main_test.go
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestIndexHandler(t *testing.T) {
    req, err := http.NewRequest("GET", "/", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(indexHandler)
    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf(
            "unexpected status: got (%v) want (%v)",
            status,
            http.StatusOK,
        )
    }

    expected := "Hello, World!"
    if rr.Body.String() != expected {
        t.Errorf(
            "unexpected body: got (%v) want (%v)",
            rr.Body.String(),
            "Hello, World!",
        )
    }
}

func TestIndexHandlerNotFound(t *testing.T) {
    req, err := http.NewRequest("GET", "/404", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(indexHandler)
    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusNotFound {
        t.Errorf(
            "unexpected status: got (%v) want (%v)",
            status,
            http.StatusNotFound,
        )
    }
}
cmd/service1/main.go
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// [START gae_go111_app]

// Sample helloworld is an App Engine app.
package main

// [START import]
import (
    "fmt"
    "log"
    "net/http"
    "os"
)

// [END import]
// [START main_func]

func main() {
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/service1/", service1Handler)

    // [START setting_port]
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("Defaulting to port %s", port)
    }

    log.Printf("Listening on port %s", port)
    if err := http.ListenAndServe(":"+port, nil); err != nil {
        log.Fatal(err)
    }
    // [END setting_port]
}

// [END main_func]

// [START indexHandler]

// indexHandler responds to requests with our greeting.
func indexHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
    fmt.Fprint(w, "Hello, service1 World!")
}

// [END indexHandler]
// [START service1Handler]

// indexHandler responds to requests with our greeting.
func service1Handler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/service1/" {
        http.NotFound(w, r)
        return
    }
    fmt.Fprint(w, "This is the service1 page!")
}

// [END service1Handler]
// [END gae_go111_app]
cmd/service1/main_test.go
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestService1Handler(t *testing.T) {
    req, err := http.NewRequest("GET", "/service1/", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(service1Handler)
    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf(
            "unexpected status: got (%v) want (%v)",
            status,
            http.StatusOK,
        )
    }

    expected := "This is the service1 page!"
    if rr.Body.String() != expected {
        t.Errorf(
            "unexpected body: got (%v) want (%v)",
            rr.Body.String(),
            "This is the service1 page!",
        )
    }
}

func TestService1HandlerNotFound(t *testing.T) {
    req, err := http.NewRequest("GET", "/service1/404", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(service1Handler)
    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusNotFound {
        t.Errorf(
            "unexpected status: got (%v) want (%v)",
            status,
            http.StatusNotFound,
        )
    }
}

deploy apps

$ cd <application root directory>
$ gcloud app deploy app.yaml service1.yaml dispatch.yaml
$ gcloud app browse
$ gcloud app browse -s service1
  • gcloud app browse でブラウザが開き、「Hello, World」が表示されたら OK です。
  • gcloud app browse -s service1 でブラウザが開き、「Hello, service1 World」が表示されたら OK です。
  • 最後に default サービスの URL 末尾に /service1/ を付け、その URL にアクセスすると、「This is the service1 page!」と表示されれば、dispatch.yaml も正常に機能しているはずです。

さいごに

Go言語 ver 1.13 で複数サービスを作成し、GAE にアップロードしてみました。
まだ Go言語の GOPATH と Module が曖昧な感じもあるので、むしろ助言いただけると助かります。
ご参考までに…。

ryo-hashioka
大阪在住のプログラマ。 AWS や GCP のクラウド含む Webアプリ開発や Android アプリ開発をしています。 趣味で Arduino を使って遊んでたりもします。 ##言語 Java, C#, Kotlin, PHP, Javascript, Python, etc...
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした