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

実践的なGAE/Goの構成について #golang #gcpja

More than 3 years have passed since last update.

2016年のGCPはFirebaseのアップデートから始まって東京リージョン開設と色々進化がありました。GAEよりはその他のGCPのサービスに注力していたのかなという印象です。
でもアジア初のGAEが日本というのは大きなニュースでした!
2017年にもどんどんデータセンターを増やすみたいで色々楽しみですね。
ちなみにGAEのChannel APIは来年シャットダウンされるらしいです。移行先としてリアルタイムの通信はFirebaseを使うのが公式のオススメのようです。

そう言えばGoogle Cloud PlatformもGoogle Cloudに改名してましたけどGCだと口頭でもわかりにくいしググラビリティも良くないのでGCPって言っていきたいです。
(追記: GCPでいいようです)

そして本題の今年のGCPのアドベントカレンダーでは、皆悩んでいるらしいGAE/Goの構成についてです。
前置きとしてタイトルの実践的というのはあくまで個人的にということです。

前提

コマンド操作とGoがある程度触れてGAE/GoのSDKを入れてHello Worldまでやってそれをデプロイまではできた方を対象にしています。

まだの方は Quickstart for Go App Engine Standard Environment から始めてみてください。

今回僕が試している環境はMacです。
作業ディレクトリはGOPATH内です(理由は後ほど)

GAEのアーキテクチャ

Microservices Architecture on Google App Engine

ここのドキュメントにあるようにまずプロジェクトというものがあってその中にサービス(以前はモジュール)があってさらにその中にバージョンがあるというイメージです。

もっと細かいことをいうとバージョンの中にさらにインスタンスがありますが構成についてはとりあえず関係ないので置いておきます。

そしてサービスやバージョンはruntimeが異なっていても大丈夫です。
webはPHPで書いてapiサーバーはGoで書くということもできますし
Javaで書いていたwebを次のバージョンからGoにするということもできます。

サービスはwebとかapiとかbotとかそれ専用にすべきです。
バージョンはトラフィックを操作できるので徐々に新しいバージョンにルーティングしていき問題があれば即座にロールバックするといったことができたりA/Bテストをするなどに使えます。

これを踏まえた上で以下のapp.yamlのプロジェクトをGAEに初めてdeployすると何が起きているかというと

app.yaml
application: vendor-example
version: 1
runtime: go
api_version: go1

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico

- url: /.*
  script: _go_app

サービスが指定されていないのでdefaultと言うサービス名になります。
そしてバージョンは1でデプロイされます。

コンソールで確認するとこんな感じ

スクリーンショット 2016-11-28 18.47.29.png

バージョンは1しかないのでここにhttps://vendor-example.appspot.com/へのトラフィックの100%が割り当てられるということになります。

ディレクトリ構成

プロジェクトのディレクトリはGOPATH内に作成しています。
GOPATHはgithubのPATHと同じようになってるのでわかりやすくていいです。
このやり方を知ってからGo以外にも色んなプロジェクト含めてGoのお作法に則って管理するほうがなにかと都合が良くてやっています。

それとGo1.5からはベンダリングの条件としてGOPATH内でないと効かないのでGOPATH内での作業を前提としています。

GOPATH内での作業を楽にしてくれるghqというツールがお薦めです。

使い方は以下の記事を参考にしてください。

ghq: リモートリポジトリのローカルクローンをシンプルに管理する

まず先にディレクトリツリーです。
https://github.com/k2wanko/appengine-vendor-example

$GOPATH/src/github.com/k2wanko/appengine-vendor-example
├── README.md
├── backend
│   ├── app.yaml
│   ├── favicon.ico
│   ├── server.go
│   └── server_test.go
├── circle.yml
├── node_modules
├── package.json
├── service_account.json.enc
├── tools
│   └── set_appcfg_token.sh
└── vendor
    └── github.com
        ├── labstack
        │   ├── echo
        (省略)

タスク管理はpackage.jsonを使っています。App Engineを使うということはWebを作るということだと思うので基本的にJavaScriptのパッケージマネージャと一緒にしたほうがフロントの人と協業しやすいと思います。

僕の場合は一人で作ってるので全部yarn runとかで統一したほうが楽ちんなのでそうしています。

Goのコードはbackendにあります。
demoのhello worldとかではプロジェクトのルートディレクトリにapp.yamlを置いてあると思いますがサービスごとにディレクトリを作ってその中にapp.yamlを置くのがいいと思います。

複数のサービスを一つのリポジトリで管理したい場合は以下のようにサービスごとにディレクトリを作成していけばいいと思います。

(project root)
├── README.md
├── backend
│   ├── app.yaml
│   └── server.go
├── api
│   ├── app.yaml
│   └── server.go
├── bot
│   ├── app.yaml
│   └── server.go
├── user [サービス共通で使うパッケージ]
│   └── user.go
└── vendor

サービス名はdefaultじゃないのかよと思われるかもしれませんがGoで書く場合パッケージ名にdefaultは使えないのでbackendとしています。

backendという名称はGoogleIOのWebサイトで使われていたのでそこを参考にして使っています。

またデプロイする時app.yamlと同じ位置にあるファイルは全部デプロイされてしまうので必要なものだけをデプロイするという点でもいいと思います。
一応skip_filesを設定することで弾くことはできますがREADME.mdpackage.jsonなどをで一々設定するのもめんどくさいと思うので単一サービスでもサブディレクトリに分けるのが便利です。

Goのパッケージ管理用のvendorディレクトリをプロジェクトのルートに置きました。
こうすることによりapp.yaml以下のGoファイル全部をbuildするという制約を受けません。
ベンダリングのためにnobuild_filesを書く必要もなくなります。

GoのPackage Managerにはmanulを使っています。(2017年現在はdepを利用してます。)
manulはgitのsubmoduleだけで管理してくれるツールです。
なので特別なメタファイルも必要ないですし取り込む側はgitが入ってるはずなのでfetchするためのツールを入れる必要がなくて便利です。
GitHubでコードを参照する場合、依存パッケージも同じようにGitHubで管理されていたらそのままリンクを辿っていけるのが僕的には嬉しかったです。

スクリーンショット 2016-11-28 19.39.43.png

使い方はREADMEを参照してください。
git submoduleのaliasみたいなものなのでそんなに難しくはないと思います。
サンプルではnpm installされた後にsubmoduleのcheckoutとgo getをするようにしています。コマンドはpackage.jsonを参照してください。

補足: 複数のサービスと複数のvendor

最初に紹介した構成だと複数サービスの場合で単一のvendorしか使えないと思うかもしれませんが、その場合はGitのリポジトリごと分けて複数のリポジトリで一つのプロジェクトを管理していくというやり方もできます。

$GOPATH/src/github.com/k2wanko/appengine-vendor-example
├── README.md
├── backend
│   ├── app.yaml
│   └── server.go
└── vendor

$GOPATH/src/github.com/k2wanko/appengine-vendor-example-api
├── README.md
├── backend
│   ├── app.yaml
│   └── server.go
└── vendor

$GOPATH/src/github.com/k2wanko/appengine-vendor-example-bot
├── README.md
├── backend
│   ├── app.yaml
│   └── server.go
└── vendor

CircleCIでデプロイする 🚀

ただベンダリングを使って終わりでは実践的とは言えないと思ったのでサンプルにはCircleCIを使ってのデプロイまでを含めています。

CircleCIは無料でGitHubのプライベートリポジトリも回すことができるので学生の身分としてはとてもありがたいです。

登録や使い方はドキュメントを参照してください。

GAEをCIでデプロイするにはCloud Consoleからサービスアカウントを作ります。

サービスアカウントの役割はApp Engineのデプロイを選択しておいて、新しい秘密鍵の提供にチェックをしてJSONを選択します。

スクリーンショット 2016-11-28 23.22.15.png

ダウンロードしたjsonはservice_account.jsonとしてプロジェクトのルートに保存しておきましょう。
そして以下のコマンドで暗号化します。

$ export KEY=`pwgen -1 32`
$ openssl aes-256-cbc -e -in service_account.json -out service_account.json.enc -k $KEY

KEYにはできるだけ長めのランダムな文字列を設定しましょう。
そしてCircleCIのSettingsのEnvironment VariablesでKEYを設定します。
復号は以下のコマンドでできます。

$ openssl aes-256-cbc -d -in service_account.json.enc -k $KEY > service_account.json

それと.gitignoreservice_account.jsonを追加して平文の方はリモートに上げてしまわないように注意しましょう。

次にプロジェクトのルートにcircle.ymlを設置します。

CircleCIのデフォルトのプロジェクトルートは~/{$CIRCLE_PROJECT_REPONAME}なのでこれをGOPATH内に持っていきます。

標準のGOPATHは~/.go_workspaceですが環境変数には:区切りで複数していされているのでmachine: environment:内にGOPATH: "${HOME}/.go_workspace"と設定します。

次にrsyncを使ってコピーします。GOPATHはキャッシュが取られてるのでrsyncにしたほうが少ないファイルのコピーで済むのでいいかなと思います。

あとgcloudコマンドは標準で入っているのですがバージョンが古いのでアップデートするようにしています。

    # Update gcloud
    - >
      test -e ~/.google-cloud-sdk || sudo mv /opt/google-cloud-sdk ~/.google-cloud-sdk;
      sudo rm -rf /opt/google-cloud-sdk && 
      sudo ln -fs ~/.google-cloud-sdk /opt/google-cloud-sdk &&
      sudo /opt/google-cloud-sdk/bin/gcloud components update -q &&
      sudo /opt/google-cloud-sdk/bin/gcloud components install app-engine-go -q &&
      sudo chmod +x /opt/google-cloud-sdk/platform/google_appengine/goapp &&
      sudo chown $USER:$USER -R ~/.config/gcloud

毎回ダウンロードしないようにdependencies: cache_directories:~/.google-cloud-sdkを追加しています。

次にトークンの設定をします。

まずは先程の複合するためのコマンドをtest: pre:に書きます。

$ openssl aes-256-cbc -d -in service_account.json.enc -k $KEY > service_account.json

平文になったjsonを使ってログインするコマンドは以下です。

$ gcloud auth activate-service-account --key-file service_account.json

GAE/Goをデプロイする方法でよく使うのはgoapp deployかと思いますがこれはappcfg.py updateのラッパーで初回このコマンドを使うとブラウザが開いて認証をしてトークンは~/.appcfg_oauth2_tokensに保存されます。
なのでCIにも同じ場所にファイルを作ってあげればgoapp deployでデプロイができるということです。

gcloudコマンドを使うことでサービスアカウントのアクセストークンが取得できるのでそれを設定してあげれば動きます。以下がアクセストークンを取得するためのコマンドです。

$ gcloud auth print-access-token

そしてサンプルのtools/内に置いているset_appcfg_token.sh~/.appcfg_oauth2_tokensに設置するためのスクリプトで内容はとてもシンプルです。

#!/bin/bash

access_token=`gcloud auth print-access-token`

cat <<EOF > ~/.appcfg_oauth2_tokens
{
    "_module": "oauth2client.client",
    "token_expiry": null,
    "access_token": "${access_token}",
    "token_uri": "https://accounts.google.com/o/oauth2/token",
    "invalid": false,
    "token_response": null,
    "client_id": null,
    "id_token": null,
    "client_secret": null,
    "revoke_uri": null,
    "_class": "OAuth2Credentials",
    "refresh_token": null,
    "user_agent": null
}
EOF

gcloud auth activate-service-accountの後に実行しましょう。

今回の構成ではgoapp deploy backendでデプロイできます。
でもほとんど必要なコードはpackage.jsonにまとめているのでそこのscriptsを確認してください。

全体のcircle.ymlはサンプルを確認してください。

まとめ

  • Goの作業する時はGOPATH内でやりましょう。
  • app.yamlはプロジェクトルートじゃなくてサブディレクトリに分けたほうがいいよ。
  • タスク管理はpackage.json(npmかyarn)を使うのが良さそう。
  • Goのパッケージマネージャはmanulが今は良さそう。
  • Goのベンダリングは計画的に
    • golang.org/x/netとかgoogle.golang.org/appengineとか公式のパッケージは取る必要がほとんどなさそう。(GAEはPaaSなのでSDKを更新しないのは死を意味しそう)
    • やんちゃなパッケージだけとかにしといたほうが管理しやすそう (ech○とか)
  • テスト書いてCIで回して自動でデプロイしましょう!
  • 詳しくはGitHubを見てください。

後は感想、

サービスごとに分けるという構成はGoogleIO 2015のコードを見て知ってから使っていましたが、今回はベンダリングを使った場合のやり方を含めて紹介させていただきました。

初心者の方には難しそうに思うかもしれませんがGoogleに身も心も故郷も捧げる(というくらい割り切った気持ちでやる)とすごい楽ちんです難しくないです。でも、「イヤイヤ流石にそこまでは」って人でも軽い気持ちでGCPをGAEから始めてもいいと思います。それなりのパフォーマンスのプラットフォームを無料で使えるのでGoでWebアプリケーションを作りたい人には是非オススメしたいです。

何かわからないことがあればGCPUGSlackとかスタックオーバーフローのgoogle-app-engineタグとかで気軽に聞いてください。GCPお兄さんたちが優しく答えてくれると思います。

それではGCPの発展とGAEでgRPCを受け取れるようになるかCloud PubSubでFirebaseのRealtime Databaseの変更が受け取れるかCloud Endpoints v2とかCloud DebuggerがGAE/Go Standard Environmentでもサポートされることを願って新年を迎えましょう。 :innocent:

koki_cheese
Go/JavaScript/Firebase/GCP/Securityとかが好きです。
http://github.com/k2wanko
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