LoginSignup
4
1

More than 3 years have passed since last update.

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

Posted at

はじめに

ちょうど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 が曖昧な感じもあるので、むしろ助言いただけると助かります。
ご参考までに…。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1