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

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

お題

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

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

前提

  • GCPは知っている。
  • Golangは(少しくらい)触っている。

以下は済んだ上での作業。
- GCPプロジェクトの作成
- Cloud SDKのインストールと初期化・認証

開発環境

# 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

プロジェクト構成

$ tree
.
├── app.yaml
├── main.go
└── secret.yaml

※今回の説明に関係あるものだけ抜粋

Golangソース(main.go)解説

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

[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接続情報は以下のように記載する。

[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のデータベース名】

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

[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管理しても問題なし)

[app.yaml]
runtime: go111

includes:
  -  secret.yaml

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

参考
GAE で秘匿したい環境変数を安全に設定する

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

[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の定義

[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 構造体の定義

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

WebAPIサーバー起動ロジック

[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

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だからどうこうという内容の記事じゃなかった・・・。

Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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