Go版のGoogleCloudFunctionsのプロジェクト導入を考えてみる

# Go版のGoogleCloudFunctionsのプロジェクト導入を考えてみる
GCFについて色々調べてみたり手元で実行してきたが、
`プロジェクト導入` という観点から調査・動作確認してみる。


# 課題・疑問
今までGCFについて調査してきた上で出てきた課題や不明点について

* ディレクトリ構成どうなるの？
* 外部、ローカル依存パッケージの取り込みどうすればよい？
* デプロイは1つずつ

他にもあるのかもしれないが、一旦上記について考えていく。


# ディレクトリ構成どうなるの？
## HelloWorldのようにシンプルな関数の場合
これはあまり深く考えない。

```
$ tree
.
├── foo.go
├── foo_test.go
└── go.mod
```

少し大きくなってきて関数を複数に分けたい、別ファイルに分けたいといった場合には、

* foo.goの中でprivate関数を作成する
* 別ファイルに切り出してfooの中から参照
  * foo.goと同一パッケージとして作成

あまり実用的でないのでこれについてはこのくらいで。


## 少し複雑になってきた場合
データストア連携部分などが必要になってきて、その部分を別パッケージとして追加したい場合など。

GCFのGoからCloudSQLに接続するサンプルを利用して考えてみることにする。
CloudSQL連携部分を `datastore/mysql/mysql.go` として置く。

### ディレクトリ構成

```
$ tree
.
├── datastore
│   └── mysql
│       └── mysql.go
├── fuga.go
├── fuga_test.go
└── go.mod
```

### fuga.go
```fuga.go
package fuga

import (
	"fmt"
	"net/http"

	sql "path/to/fuga/datastore/mysql"
)

func Fuga(w http.ResponseWriter, r *http.Request) {
	now := sql.MySQLDemo()
	fmt.Fprintf(w, "Now is %s", now)
}
```

### mysql.go
```mysql.go
// Package sql contains examples of using Cloud SQL.
package sql

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	// Import the MySQL SQL driver.
	_ "github.com/go-sql-driver/mysql"
)

var (
	db *sql.DB

	connectionName = os.Getenv("MYSQL_INSTANCE_CONNECTION_NAME")
	dbUser         = os.Getenv("MYSQL_USER")
	dbPassword     = os.Getenv("MYSQL_PASSWORD")
	dsn            = fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/", dbUser, dbPassword, connectionName)
)

func init() {
	var err error
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Could not open db: %v", err)
	}

	// Only allow 1 connection to the database to avoid overloading it.
	db.SetMaxIdleConns(1)
	db.SetMaxOpenConns(1)
}

// MySQLDemo is an example of making a MySQL database query.
// ※公式のサンプルから現在日時を返却するだけの関数に変更
func MySQLDemo() string {
	rows, err := db.Query("SELECT NOW() as now")
	if err != nil {
		log.Printf("db.Query: %v", err)
		return ""
	}
	defer rows.Close()

	now := ""
	rows.Next()
	if err := rows.Scan(&now); err != nil {
		log.Printf("rows.Scan: %v", err)
		return ""
	}

	return now
}
```
https://cloud.google.com/functions/docs/sql#functions_sql_mysql-go

### buildを実行してみる
```
$ go build
fuga.go:7:2: unknown import path "path/to/fuga/datastore/mysql": cannot find module providing package path/to/fuga/datastore/mysql
```

上記エラーが出てビルドが通らない・・・
go1.11使っているから `datastore/mysql` もモジュールとして機能させないとダメかな？と思い `datastore/mysql` 下で `go mod init` をやってみる。
go.modファイルができたのでもう一度ビルドしてみるが、結果は同じエラーが出て失敗。

GOPATHが通っている状況下でこんなことあったかな・・・と思いつつ諸々ググってみる。
※go1.11からはGOPATHを意識する必要がなくなったとどっかで見た覚えが


ググってみたところ公式に以下の記載を見つける

https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive
https://github.com/golang/go/wiki/Modules#can-i-work-entirely-outside-of-vcs-on-my-local-filesystem

特に

>This is very simple if you have a single module you want to edit at a time outside of VCS (and you either have only one module in total, or if the other modules reside in VCS). In this case, you can place the file tree containing the single go.mod in a convenient location. Your go build, go test and similar commands will work even if your single module is outside of VCS (without requiring any use of replace in your go.mod).

>If you want to have multiple inter-related modules on your local disk that you want to edit at the same time, then replace directives are one approach. Here is a sample go.mod that uses a replace with a relative path to point the hello module at the on-disk location of the goodbye module (without relying on any VCS):

この部分にあるようにgithubのようなVersionControllSystem以外の例えばローカルにあるようなファイルについては、go.mod内で `replaceディレクティブ` を使えば良いとある。

んーなるほど分からん。
なので試してみる。

```go.mod
module path/to/fuga

require (
	path/to/fuga/datastore/mysql v0.0.0
)

replace path/to/fuga/datastore/mysql v0.0.0 => ./datastore/mysql

```

↑で作成したdatastore/mysqlを `require` と `replace` に上記のような記述を追加してあげる。これでビルドしたら無事通った。

ちなみに、 `replace` だけでは

```
fuga.go:7:2: unknown import path "path/to/fuga/datastore/mysql": cannot find module providing package path/to/fuga/datastore/mysql
```

というエラーが出て通らなかったので注意。
やっとこれで先に進める。


# デプロイ
上記のreplaceを使ったやり方でビルドが通ったのでGCFにデプロイしてみる。

```
$ gcloud functions deploy fuga --entry-point Fuga --runtime go111 --trigger-http
Deploying function (may take a while - up to 2 minutes)...failed.
ERROR: (gcloud.functions.deploy) OperationError: code=3, message=Build failed: go: finding path/to/fuga/datastore/mysql v0.0.0
go: finding github.com/go-sql-driver/mysql v1.4.1
go: path/to/fuga/datastore/mysql@v0.0.0: unknown revision fuga/datastore/mysql/v0.0.0
go: error loading module requirements
```

意気揚々とデプロイしてサクッと撃沈しました。datastore/mysqlが見つからないよう。
ローカルのパッケージを指定するためにreplaceしていたので当然っちゃ当然なのですが・・・

ちょっと調べてみたのですが、シンプルに直感的に解決できるような方法は見つからず。

## 暫定対応
githubにプッシュして確認とったわけではないので絶対とは言えないが・・・
go.modに追加したrequireとreplaceを消した上で

`githubにプッシュ→デプロイ`

と実行すればデプロイできないってエラーは解消される気がする。


## 恒久対応はどうしたらいい？
* go.modを環境別で使用できる？
* GOPROXYなるものを利用してlocalにproxyサーバー立てて取得先をうまいことやる？
* その他には？

もし詳しい人がいたら是非教えてください・・・


# デプロイは1つずつ
デプロイは関数一つずつなので、CIやCloudBuilderなどを使って地道にデプロイコマンド書いていく。
個人的にはCI上でコマンド丸ごと管理するよりは、CloudBuilderをつかてエントリポイントなど重要なところに集中できるような管理が良いと感じた。


# まとめ
開発中のローカルパッケージの依存関係問題を解消しないとプロジェクトへの導入は難しいのではないかと。
デプロイについては、ちょっと泥臭くなってしまうが大きな障壁とはならないはず。

なので次はGOPROXYを利用したパッケージ管理方法を調査してみることとする。