見出し画像

GAE/Go 1.11 → 1.12 へ移行をそろそろ検討し始める...

こんにちは Qiita を卒業して今後はその時の自分に適したプラットフォーム(今は note なだけで将来は変わるかもしれません)にアウトプットしていこうと思う気まぐれな @wezardnet です。Qiita の記事はこちらになります。

はじめに

Google App Engine(GAE) は 2008 年に世に登場し 10 年を迎えて大きな変革の時期に来たようです。私はふだん Go 言語を使っており Go 1.11 ランタイムまでを GAE 第一世代(1st gen)で Go 1.12 ランタイム以降が GAE 第二世代(2nd gen)に分類されるそうです。

私自身 GAE Standard Environment は強力なサーバレスプラットフォームであると思っていますし、インフラまわりを考えずにアプリケーション開発に専念できるので GCP の中では大好きなプロダクトの一つですね 😆
尚、本記事における GAE は Standard Environment(SE) になります。

これから作るアプリやシステムは心機一転、気持ち良く 2nd gen で作ればよいのですが、既存の 1st gen で作られたモノは 2nd gen に移行していかなければなりません。現時点(2019 年 11 月)ではグーグルより 1st gen サポート停止について言及されていませんが、いつかはその日がやって来るので対策を考えておく必要があります。

1st gen に分類されるランタイムの長期サポートが公式よりアナウンスされています。しばらくの間は Go 1.11 は大丈夫そうですが、古いランタイムはコミュニティのサポートやアップデートから取り残されることもありますし、セキュリティ上の懸念もあります。Go 1.12, 1.13, 1.14... と上がっていった時、どこかのタイミングで移行させる事になると思います。


本題に戻りますが GAE/Go 1.12 への移行については公式ドキュメントで移行先や代替手段など提案されていますが、なかなか大変そうな感じです (>_<)

というわけで、まずは情報収集から始めることにしました。今後、気付いたことや新しく試したことなどはその都度、加筆/追記していくかもしれません。

情報収集(先達の知見)

Google App Engineを第一世代から第二世代に乗り換えた
GAE Go 1.11 ランタイムが公式には 2nd gen ではなくなった件について
AppEngineの旧Log APIを脱却したい話
GAE第二世代+Go v1.12でechoサーバーを構築する(2019年度版)
GAE 2nd-gen でのサービス間認証
GAE上で動くアプリケーションをGo1.12にアップデートする上でやったこと
Echoを使ってGAE/Go1.12(第二世代)でいい感じにログを出す
GAE/Goでgen2を試してみた
App Engine を布教したくて Go + Datastore の開発環境を Docker Compose でシュッと立ち上げた話
gcpug/nouhau


見出しの記号について

本記事中の見出しは自分用タスクでもあり、末尾の記号は自分の判断で以下の意味を表します。

 ○ ・・・移行できそう(な気がする)
 △ ・・・ 移行できそうだが課題あり
 ✗ ・・・ 移行できない → どうしようか思案中...
 − ・・・ 移行の必要なし


2nd gen 用の GCP プロジェクト作る 【○】

既存のプロジェクトとは別にプロジェクトを作成し、トラブったときに原因を特定しやすいように小さいコードに分けて機能ごとに試していくことにしました。

画像1


プロジェクトのディレクトリ構成 【○】

ディレクトリ構成は main パッケージの配置場所を変更しました。ディレクトリ構成はパッケージの用途などによって変えるつもりです。

$ tree

.
├── app.yaml
├── cmd
│   └── main
│       └── main.go
├── controllers.go
├── cron.yaml
├── go.mod
├── go.sum
├── index.yaml
├── lib
│   ├── common
│   │   └── common.go
│   ├── config
│   └── helper
├── model
│   ├── account.go
│   └── application.go
├── queue.yaml
├── readme.md
├── static
│   ├── css
│   │   ├── base.css
│   │   └── base.min.css
│   ├── images
│   └── js
└── templates
   └── test.tmpl


app.yaml の移行 【△】

2nd gen では app.yaml の一部の構成要素が Deprecated されてます。私にとっての大きな変更点は login が使えなくなったことで、認証が必要なリクエストを今後どうしようか思案中です。。。(Cloud Identity-Aware Proxy になりそうな予感)

【Go 1.11 (1st gen)】

service: default
runtime: go111
instance_class: F1

nobuild_files:
- vendor

handlers:
- url: /admin/.*
 script: auto
 login: admin
 secure: always

- url: /cron/.*
 script: auto
 login: admin
 secure: always

- url: /.*
 script: auto
 secure: always

【Go 1.12 (2nd gen)】

service: default
runtime: go112
instance_class: F1

main: ./cmd/main

nobuild_files:
- vendor

handlers:
- url: /(.*\.html)$
 mime_type: text/html
 static_files: static/\1
 upload: static/(.*\.html)

- url: /css
 static_dir: static/css
 expiration: "1d"

- url: /images
 static_dir: static/images
 expiration: "1d"

- url: /js
 static_dir: static/js

- url: /.*
 script: auto
 secure: always

env_variables:
 PROJECT_ID: "myapp"

2nd gen では環境変数で PROJECT_ID を取得できるように env_variables に記述しておきます。


cron.yaml について 【−】

App Engine Cron サービスは Cloud Scheduler へ移行するのかと思いきや、特に公式では記載がありません。cron.yaml もこれまでと同じように利用できるようですし、デベロッパーコンソール上の GAE メニュー内の Cron jobs は残り続けるようです。

画像11

cron.yaml でデプロイした場合は GAE の Cron jobs に表示され Cloud Scheduler には表示されません。ただし Cloud Scheduler でも GAE の cron を設定することはできるので、どちらでも良いのでしょう...

画像12


appengine をインポートから外す 【△】

2nd gen では GAE にロックインされないように appengine 独自の API は使えなくなるそうなので、頑張って実装を代替していく必要があります...
appengine context をあちこちで使っているので、地道に変更していくしかないですね (;´д`)トホホ…


main パッケージのルーティングまわりの移行 【○】

私はフレームワークに favclip/ucon を採用し、ルーティングまわりを次のように実装していましたが 2nd gen に移行するにあたり、外部パッケージは使わず標準パッケージだけにすることにしました。

【Go 1.11 (1st gen)】

package main

import (
	"net/http"
	"reflect"
	"strings"

	"github.com/favclip/ucon"
	"github.com/favclip/ucon/swagger"

	"google.golang.org/appengine"
)

func main() {
	ucon.Orthodox()
	ucon.Middleware(swagger.RequestValidator())

	swPlugin := swagger.NewPlugin(&swagger.Options{
		Object: &swagger.Object{
			Info: &swagger.Info{
				Title:   "myapp",
				Version: "v1",
			},
			Schemes: []string{"http"},
		},
		DefinitionNameModifier: func(refT reflect.Type, defName string) string {
			if strings.HasSuffix(defName, "JSON") {
				return defName[:len(defName)-4]
			}
			return defName
		},
	})
	ucon.Plugin(swPlugin)
	ucon.DefaultMux.Prepare()
	http.Handle("/", ucon.DefaultMux)

	ucon.HandleFunc("GET", "/", rootHandler)

	SetUpServices(swPlugin)

	appengine.Main()
}

【Go 1.12 (2nd gen)】

package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/wezardnet/myapp"
)

func main() {
	myapp.SetUpControllers()

	http.HandleFunc("/", myapp.RootHandler)
	http.HandleFunc("/test", myapp.TestHandler)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
}

実装してみてから気付いたんだけど API とか作る際にパスパラメータまわりの扱いが面倒なのでフレームワークを使うことにします。候補としては軽量と GitHub のスターを加味して gin-gonic/gin にしようかと考えてます。こちらは本題から外れるため、機会があれば別で書くかも知れません。。。


appengine/urlfetch → net/http に移行 【○】

1st gen では GAE から外部へ HTTP リクエストを出すには appengine/urlfetch を使わなければなりませんでしたが 2nd gen では標準の net/http を使います。以下は Google の UserInfo API を叩く実装例になります。

【Go 1.11 (1st gen)】

import (
	"net/http"

	"google.golang.org/appengine"
	"google.golang.org/appengine/urlfetch"
)

-省略-

ctx := appengine.NewContext(r)

req, err := http.NewRequest(
	"GET",
	"https://www.googleapis.com/oauth2/v2/userinfo",
	nil,
)
if err != nil {
	http.Error(w, "http request error.", http.StatusInternalServerError)
	return
}
req.Header.Set("Authorization", "Bearer " + accessToken)

client := urlfetch.Client(ctx)
resp, err := client.Do(req)
if err != nil {
	http.Error(w, "http request error: " + err.Error(), http.StatusInternalServerError)
	return
}

【Go 1.12 (2nd gen)】

import (
	"net/http"
)

-省略-

req, err := http.NewRequest(
	"GET",
	"https://www.googleapis.com/oauth2/v2/userinfo",
	nil,
)
if err != nil {
	http.Error(w, "http request error.", http.StatusInternalServerError)
	return
}
req.Header.Set("Authorization", "Bearer " + accessToken)

client := new(http.Client)
resp, err := client.Do(req)
if err != nil {
	http.Error(w, "http request error: " + err.Error(), http.StatusInternalServerError)
	return
}


appengine/log の移行 【○】

コイツに関しては「AppEngineの旧Log APIを脱却したい話」で触れられていますが、かなり面倒デス。ログ出力として appengine/log が優秀過ぎたんですね😱

【Go 1.11 (1st gen)】

import (
	"encoding/json"
	"io/ioutil"

	"github.com/bitly/go-simplejson"

	"google.golang.org/appengine"
	"google.golang.org/appengine/log"
)

-省略-

ctx := appengine.NewContext(r)

body, _ := ioutil.ReadAll(resp.Body)
data, err := simplejson.NewJson(body)
if err != nil {
	log.Errorf(ctx, "failed to api request: %v", err)
	return
} else if resp.StatusCode != http.StatusOK {
	log.Errorf(ctx, "failed to api request: %d, %s", resp.StatusCode, string(body))
	return
}
results, _ := json.MarshalIndent(data, "", "    ")
log.Infof(ctx, "success! results = %s", string(results))

実際の出力をデベロッパーコンソールで確認すると、次のように出力されます。

(log.Errorf 出力)

画像2

(log.Infof 出力)

画像3


〜 ここから先は有料記事となります 〜
投げ銭として寄付していただけると嬉しいです♪

ここから先は

13,121字 / 11画像

¥ 3,000

この記事が気に入ったらサポートをしてみませんか?