Google App EngineのGo1.11スタンダード環境がGAになっていたので、お試しでJSONリクエストをCloud SQLに書き込むコードを書いてみる。

# お題
以下にある通り、３月２０日に[Google App Engine](https://cloud.google.com/appengine/?hl=ja)の[Go 1.11スタンダード環境](https://cloud.google.com/appengine/docs/standard/go111/?hl=ja)の「ベータ版」表記が外れ、GA(General Availability)となっていた。
https://cloud.google.com/appengine/docs/standard/go111/release-notes?hl=ja

表題の通り、とりあえずJSONリクエストを受け取って、その内容をCloud SQLに書き込むだけの簡単なサンプルコードでも書いてみる。

# 前提
- [GCP](https://cloud.google.com/?hl=ja)は知っている。
- [Golang](https://golang.org)は（少しくらい）触っている。

以下は済んだ上での作業。
- GCPプロジェクトの作成
- [Cloud SDK](https://cloud.google.com/sdk/docs/)のインストールと初期化・認証

# 開発環境
### # OS
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
```

### # Golang
```
$ go version
go version go1.11.4 linux/amd64
```

### # Cloud SDK
```
$ gcloud version
Google Cloud SDK 240.0.0
app-engine-go 
```

# 実践
今回のソース全量は下記。
https://github.com/sky0621/go-webapi-for-gae-study

## プロジェクト構成

<pre>
$ tree
.
├── app.yaml
├── main.go
└── secret.yaml
</pre>
※今回の説明に関係あるものだけ抜粋

## Golangソース(`main.go`)解説

### 環境変数よりCloud SQL接続情報を取得

```go:[main.go]
	var (
		connectionName = os.Getenv("CLOUDSQL_CONNECTION_NAME")
		user           = os.Getenv("CLOUDSQL_USER")
		password       = os.Getenv("CLOUDSQL_PASSWORD")
		database       = os.Getenv("CLOUDSQL_DATABASE")
	)
```

この接続情報は、ローカル環境であれば上記の名称でセットしておけばよいけど、App Engine にデプロイする上では、どこにどのようにセットしておけばよいのか？
CI/CDのような方式ではなくローカル環境のコマンドプロンプト等から`gcloud`コマンドを用いてデプロイする方式が前提にはなるけど、以下のようにすればよい。

### 機密情報を記載したファイル(`secret.yaml`)を用意してApp Engineデプロイ用のファイル(`app.yaml`)からインクルード

先ほどのCloud SQL接続情報は以下のように記載する。

```yaml:[secret.yaml]
env_variables:
  # For Cloud SQL 2nd generation instances, this should be in the form of "project:region:instance".
  CLOUDSQL_CONNECTION_NAME: 【自分で作成したGCPプロジェクトID】:【Cloud SQLの存在するリージョン】:【Cloud SQLのインスタンス名】
  CLOUDSQL_USER: 【Cloud SQLの接続ユーザー名】
  CLOUDSQL_PASSWORD: 【Cloud SQLの接続ユーザーのパスワード】
  CLOUDSQL_DATABASE: 【Cloud SQLのデータベース名】
```

上記だとイメージわきづらいので、ダミー値でサンプルを記載。

```yaml:[secret.yaml]
env_variables:
  # For Cloud SQL 2nd generation instances, this should be in the form of "project:region:instance".
  CLOUDSQL_CONNECTION_NAME: my-gcp-proj:asia-northeast1:my-cloudsql-instance
  CLOUDSQL_USER: root
  CLOUDSQL_PASSWORD: password12345678
  CLOUDSQL_DATABASE: mydb
```

**※注意点：`.gitignore`に記載するなどして、決してpublicなGitHub環境等にアップされないようにすること！**

上記ファイルを読み込むデプロイ用のファイルは下記。（こちらはGitHub管理しても問題なし）

```yaml:[app.yaml]
runtime: go111

includes:
  -  secret.yaml
```

上記のファイル群を用意した上で、同ファイル群が存在するパス上でコマンドプロンプト等から `gcloud app deploy` と叩くと、記載した環境変数をセットした上でGolangアプリがGoogle App Engine上にデプロイされる。

参考
・[GAE で秘匿したい環境変数を安全に設定する](https://qiita.com/nirasan/items/9527ffa7d2f2fac2933e)

### Cloud SQL(MySQLインスタンス)に接続

```go:[main.go]
	dataSource := fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/%s?parseTime=True", user, password, connectionName, database)

	db, err := sql.Open("mysql", dataSource)
	if err != nil {
		panic(err)
	}
	defer func() {
		if db != nil {
			err := db.Close()
			if err != nil {
				panic(err)
			}
		}
	}()
```

MySQL接続用のデータソースとしては通常「「ユーザー名」:「パスワード」@tcp(「ホスト名」:3306)/「DB名」」といった形式になる。
GAE-Goの1.11スタンダード環境は特殊で上記ソース記載の形式になる。
【参考】
https://qiita.com/sky0621/items/d91ffd1faf63dbf34e77#gaegoのスタンダード環境におけるcloud-sql接続方法go-v111の場合

### ユーザー登録を行うWebAPIの定義

```go:[main.go]
	http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
		user, err := parseJsonRequest(r)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		switch r.Method {
		case http.MethodPost:
			result, err := db.ExecContext(context.Background(), "INSERT INTO user(id, name, mail, create_user) VALUES(?, ?, ?, ?)",
				uuid.New().String(), user.Name, user.Mail, "admin")
			if err != nil {
				if _, err := fmt.Fprintln(w, err.Error()); err != nil {
					panic(err)
				}
			}
			if result == nil {
				panic(errors.New("result is nil"))
			}
		}
	})
```

`parseJsonRequest(r)` 関数の中身は省略。
HTTPリクエストからBodyをJSON形式でパースして、下記構造体にマッピングすることをやっている。

#### `user` 構造体の定義

```go
type user struct {
	Name string `json:"name"`
	Mail string `json:"mail"`
}
```

### WebAPIサーバー起動ロジック

```go:[main.go]
	port := fmt.Sprintf(":%s", os.Getenv("PORT"))

	if err := http.ListenAndServe(port, nil); err != nil {
		panic(err)
	}
```

接続ポートを環境変数から取得する。これが重要。
Google App Engine環境にアプリをデプロイすると `PORT` という名前で接続可能なポートが定義される。
そのため、上記のようにアプリ側で立ち上げるサーバーは環境変数「`PORT`」から取得するようにしないといけない。

## Google App Engineデプロイ

```
$ ls -l app.yaml 
-rw-r--r-- 1 sky0621 sky0621 43  4月  1 09:02 app.yaml
$
$ gcloud app deploy
Services to deploy:

descriptor:      [/home/sky0621/work/src/go111/src/github.com/sky0621/go-webapi-for-gae-study/app.yaml]
source:          [/home/sky0621/work/src/go111/src/github.com/sky0621/go-webapi-for-gae-study]
target project:  [【プロジェクトID】]
target service:  [default]
target version:  [20190401t092302]
target url:      [https://【プロジェクトID】.appspot.com]


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

Beginning deployment of service [default]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 1 file to Google Cloud Storage                 ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.                                                                                                                                                                           
Setting traffic split for service [default]...done.                                                                                                                                                          
Deployed service [default] to [https://【プロジェクトID】.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
```

## WebAPIにリクエスト

### Postmanを使ってリクエスト実行（【プロジェクトID】の部分にはGoogle App Engineが存在するGCPプロジェクトのIDが入る。

![Screenshot from 2019-04-05 01-42-27.png](https://qiita-image-store.s3.amazonaws.com/0/180599/31ea2b17-07fe-769e-38f2-227089c220c3.png)

### Cloud SQLに接続して `user` テーブルの中身を確認

```
MySQL [【DB名】]> select * from user;
+--------------------------------------+-----------------------+------------------+-------------+---------------------+-------------+------------+
| id                                   | name                  | mail             | create_user | created_at          | update_user | updated_at |
+--------------------------------------+-----------------------+------------------+-------------+---------------------+-------------+------------+
| 720da418-a7ed-41c4-bf51-1fea81ffeca1 | テストユーザー        | test@example.com | admin       | 2019-04-04 16:35:48 | NULL        | NULL       |
+--------------------------------------+-----------------------+------------------+-------------+---------------------+-------------+------------+
1 row in set (0.04 sec)
```

# まとめ
GCPプロジェクト作成から`gcloud`コマンドのインストールにセットアップ等、事前の準備が済んでさえいれば、あとは、アプリを作って `app.yaml` 用意して、`gcloud app deploy` と叩く。
これだけでマネージドな環境にアプリをデプロイできるのは、やっぱり便利。
・・・「GAになっていたので」と言いつつ、特にGAだからどうこうという内容の記事じゃなかった・・・。