超簡単 Google App Engineで始めるWebアプリケーション 〜リクエスト分割機能がすごかった〜

120件のシェア(ちょっぴり話題の記事)

どうも、GCPヨチヨチ歩きの城岸です。

本日は、Google App Engine(以下GAE)を使って簡単にWebアプリケーションをデプロイする方法を紹介したいと思います。 アプリケーションのバージョンごとにリクエストを分割できる機能が素敵すぎてブログを書かずにはいられませんでした!

それではいってみましょう!

GAEとは

GCP (Google Cloud Platform)が提供するPaaSです。Java、Node.js、Python、Go など好きな言語で作成したアプリケーションをGCPが管理するインフラに簡単にデプロイすることができます。AWSのAWS Elastic Beanstalkと同じカテゴリのサービスです。

詳細は公式ドキュメントをご覧ください。

やってみた

公式のチュートリアルをベースに進めていきます。 アプリケーションはGo 1.11で作成します。

検証環境

ブラウザベースのコマンドラインCloudShellを利用します。 (AWSにもこの機能欲しい。。) gcloudのバージョンは以下の通りです。

cloudshell:~$ gcloud -v
Google Cloud SDK 246.0.0
alpha 2019.02.22
app-engine-go
app-engine-java 1.9.74
app-engine-php " "
app-engine-python 1.9.85
app-engine-python-extras 1.9.74
beta 2019.02.22
bq 2.0.43
cbt
cloud-build-local
cloud-datastore-emulator 2.1.0
core 2019.05.10
datalab 20190116
docker-credential-gcr
gcd-emulator v1beta3-1.0.0
gsutil 4.38
kubectl 2019.05.10
pubsub-emulator 2019.04.26

CloudShell起動

GCPにログイン後、CloudShellを起動します。

以降のシェルはCloudShell上での実行結果です。

プロジェクト作成

プロジェクトを作成します。AWSにはない概念ですが、プロジェクトという単位でシステムを管理します。

$ gcloud projects create blog-project-20190519 --set-as-default
Create in progress for [https://cloudresourcemanager.googleapis.com/v1/projects/blog-project-20190519].
Waiting for [operations/cp.XXXXXXXXXXXXXXXXXXXXX] to finish...done.

GAE作成

先ほど作成したプロジェクトにGAEを作成します。リージョンは東京リージョンとします。

 $ gcloud app create --project=blog-project-20190519
You are creating an app for project [blog-project-20190519].
WARNING: Creating an App Engine application for a project is irreversible and the region
cannot be changed. More information about regions is at
<https://cloud.google.com/appengine/docs/locations>.

Please choose the region where you want your App Engine application
located:

 [1] asia-east2    (supports standard and flexible)
 [2] asia-northeast1 (supports standard and flexible)
 [3] asia-northeast2 (supports standard and flexible)
 [4] asia-south1   (supports standard and flexible)
 [5] australia-southeast1 (supports standard and flexible)
 [6] europe-west   (supports standard and flexible)
 [7] europe-west2  (supports standard and flexible)
 [8] europe-west3  (supports standard and flexible)
 [9] europe-west6  (supports standard and flexible)
 [10] northamerica-northeast1 (supports standard and flexible)
 [11] southamerica-east1 (supports standard and flexible)
 [12] us-central    (supports standard and flexible)
 [13] us-east1      (supports standard and flexible)
 [14] us-east4      (supports standard and flexible)
 [15] us-west2      (supports standard and flexible)
 [16] cancel
Please enter your numeric choice:  2 
Creating App Engine application in project [blog-project-20190519] and region [asia-northeast1]....done.
Success! The app is now created. Please use `gcloud app deploy` to deploy your first app.
To take a quick anonymous survey, run:
  $ gcloud alpha survey

請求アカウント設定

GAEを利用するため、プロジェクトに対しての請求を有効にします。

サンプルアプリケーションダウンロード

次のコマンドを実行しサンプルアプリケーションをダウンロードします。

 $ git clone https://github.com/GoogleCloudPlatform/golang-samples.git
$ cd golang-samples/appengine/go11x/helloworld

以下のファイルがダウンロードされます。

 $ tree
.
├── app.yaml
├── helloworld.go
└── helloworld_test.go
0 directories, 3 files

app.yamlには、ランタイムが指定されています。

# 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.
runtime: go111

helloworld.goは、httpリクエストに対し"Hello, World!"という文字列を返却するアプリケーションとなっています。

// 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)
        log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
        // [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]

helloworld_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.StatusOK,
                )
        }
}

サンプルアプリケーションデプロイ

サンプルアプリケーションをデプロイします。

$ gcloud app deploy
Services to deploy:

descriptor:      [/home/jogan_naoki/golang-samples/appengine/go11x/helloworld/app.yaml]
source:          [/home/jogan_naoki/golang-samples/appengine/go11x/helloworld]
target project:  [blog-project-20190519]
target service:  [default]
target version:  [20190519t083733]
target url:      [https://blog-project-20190519.appspot.com]


Do you want to continue (Y/n)?  Y

Beginning deployment of service [default]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 3 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://blog-project-20190519.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse

target urlにアクセスしてみましょう!

$ curl https://blog-project-20190519.appspot.com/
Hello, World!

Hello, World!という文字列が表示されました。

デプロイはこれだけ。これだけでGCPが管理するインフラに自身のアプリケーションをデプロイすることができます!簡単ですね!

サンプルアプリケーション変更

出力される文字列をHello, World!からHello, World!!!に変更してみましょう。

// 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)
        log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
        // [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]

テストコードも同様に変更しテストを実行します。

$ go test
PASS
ok      _/home/jogan_naoki/golang-samples/appengine/go11x/helloworld    0.004s

サンプルアプリケーション再デプロイ

サンプルアプリケーションを再デプロイします。

$ gcloud app deploy
Services to deploy:

descriptor:      [/home/jogan_naoki/golang-samples/appengine/go11x/helloworld/app.yaml]
source:          [/home/jogan_naoki/golang-samples/appengine/go11x/helloworld]
target project:  [blog-project-20190519]
target service:  [default]
target version:  [20190519t085640]
target url:      [https://blog-project-20190519.appspot.com]


Do you want to continue (Y/n)?  Y

Beginning deployment of service [default]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 2 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://blog-project-20190519.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse

もう一度target urlにアクセスしてみましょう!

$ curl https://blog-project-20190519.appspot.com/
Hello, World!!!

変更が反映されました!

トラフィック分割

ここからが一番伝えたかったところです。

先ほどのアプリケーションはバーション管理されています。そしてどのバージョンにどれくらいトラフィックが流れているかを確認することができます。

今は再デプロイしたアプリケーションに100%のトラフィックが流れています。古いバージョンにもトラフィックを流してみましょう。「トラフィックを分割」をクリックします。

「トラフィック分割の基準」、「トラフィック割り当て」を指定し、「保存」をクリックします。

トラフィックが分割されました。(これだけでそんな高度なことができるだと!?

対象のURLにアクセスしトラフィックが分割されていることを確認してみましょう!

$ curl https://blog-project-20190519.appspot.com/
Hello, World!
$ curl https://blog-project-20190519.appspot.com/
Hello, World!!!                           
$ curl https://blog-project-20190519.appspot.com/
Hello, World!
$ curl https://blog-project-20190519.appspot.com/
Hello, World!
$ curl https://blog-project-20190519.appspot.com/
Hello, World!!!

ちゃんと分割されていることが確認できます!これでA/Bテストも楽々

トラフィック移行

もちろんバージョンを指定して全てのトラフィックを移行することもできます。

あっ、やばい!バージョン戻したい!なんて時には非常に便利ですね。

プロジェクトの削除

最後はdeleteコマンドでリソースを削除します。

$ gcloud projects delete blog-project-20190519
Your project will be deleted.

Do you want to continue (Y/n)?  Y

Deleted [https://cloudresourcemanager.googleapis.com/v1/projects/blog-project-20190519].

You can undo this operation for a limited period by running:
  $ gcloud projects undelete blog-project-20190519


To take a quick anonymous survey, run:
  $ gcloud alpha survey

プロジェクトを削除した時点で課金は停止されます。が、30日間(復元期間中)であれば削除したプロジェクトは復元可能です。なんとお優しい!

さいごに

GAEを使って簡単にWebアプリケーションをデプロイする方法を紹介しました。GAEのスケールなどまだまだ紹介しきれていない機能があるので、これからもブログを書いていこうと思います!!

また、エンジニアとしてGCPで何ができるのか?を知っておくことはとても重要なことであると感じました。その上で自身のアプリケーションに最適なクラウド(AWS/GCP/Azureなど)を選択したいものですね!

AWSがいいなと感じた場合は、弊社メンバーズをご検討ください!