Cloud SQL Proxy を Golang のパッケージとして使用する

[GCP](https://cloud.google.com/?hl=ja "Google Cloud Platform") の [Cloud SQL](https://cloud.google.com/sql/ "Cloud SQL") へアプリケーションから接続する場合、 アプリケーションを配置しているホストのIPアドレスをホワイトリストに追加するか、 [Cloud SQL Proxy](https://cloud.google.com/sql/docs/mysql/sql-proxy "Cloud SQL Proxy") を使用するかと思います。

しかし、アプリケーションが配置されるホストのIPアドレスが不定（[GKE](https://cloud.google.com/container-engine/?hl=ja "Google Container Engine") などを使っており、配置されるホストが不定など)の場合は、 Cloud SQL Proxy を使うことになると思います。
もし、そのアプリケーションを Golang で書いている場合は、  Cloud SQL Proxy  の [Github](https://github.com/GoogleCloudPlatform/cloudsql-proxy "GoogleCloudPlatform/cloudsql-proxy") の README に

>If your program is written in Go you can use the Cloud SQL Proxy as a library, avoiding the need to start the Proxy as a companion process.

と書かれている通り、 Cloud SQL Proxy を Golang のパッケージとして使用できます。
そのため、別途 Cloud SQL Proxy を用意する必要なく、アプリケーションから直接データベースへ接続ができるようになります。

それでは、実際にやってみましょう。
なお、 Cloud SQL のインスタンス・テーブルは既に作成済みとして話を進めます。
この記事で使用するテーブルなどは以下のとおりです。

```mysql:MySQL
CREATE DATABASE mydb;
USE mydb;
CREATE TABLE guestbook (guestName VARCHAR(255) NOT NULL, content VARCHAR(255) NOT NULL, date DATETIME, entryID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(entryID));
```

```postgres:PostgreSQL
CREATE TABLE guestbook (guestName VARCHAR(255) NOT NULL, content VARCHAR(255) NOT NULL, date TIMESTAMP NOT NULL, entryID SERIAL PRIMARY KEY);
```

```sql:挿入レコード
INSERT INTO guestbook (guestName, content, date) values ('first guest', 'I got here!', '2017-08-06 12:00:00');
INSERT INTO guestbook (guestName, content, date) values ('second guest', 'Me too!', '2017-08-06 13:00:00');
```

# パッケージ取得
Cloud SQL Proxy のパッケージを取得します。

```shell-session
$ go get github.com/GoogleCloudPlatform/cloudsql-proxy
```

次にデータベースのドライバパッケージを取得します。
インスタンスを MySQL にしている場合は

```shell-session
$ go get github.com/go-sql-driver/mysql
```

PostgreSQL にしている場合は

```shell-session
$ go get github.com/lib/pq
```

を取得します。

# サービスアカウント作成
Cloud SQL Proxy を使用する場合は、適切な権限が付与されたサービスアカウントの作成が必要です。
ここでは Cloud SQL Proxy 用のサービスアカウントを作成して使用します。
IAMページに移動し、サービスアカウントを作成して秘密鍵をダウンロードしてください。
この秘密鍵はアプリケーション実行時に必要になります。

サービスアカウントに必要な権限は以下のとおりです。

* Cloud SQL クライアント
* Cloud SQL 編集者

# ソースコード
## MySQL

```go:main.go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	cloudsqlproxy "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/mysql"
	"github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()

	if err := showRecords(ctx, "project:region:instance-name", "mydb", "user", "user_password"); err != nil {
		log.Fatal(err)
	}
}

func showRecords(ctx context.Context, dbAddress, dbName, dbUser, dbPassword string) error {
	// 接続するだけなら以下のコードになります。
	// db, err := cloudsqlproxy.DialPassword(dbAddress, dbUser, dbPassword)

	// 詳細に設定したい場合は以下のコードになります。
	db, err := cloudsqlproxy.DialCfg(&mysql.Config{
		Addr:      dbAddress,  // インスタンス接続名
		DBName:    dbName,     // データベース名
		User:      dbUser,     // ユーザ名
		Passwd:    dbPassword, // ユーザパスワード
		Net:       "cloudsql", // Cloud SQL Proxy で接続する場合は cloudsql 固定です
		ParseTime: true,       // DATE/DATETIME 型を time.Time へパースする
		TLSConfig: "",         // TLSConfig は空文字を設定しなければなりません
	})
	if err != nil {
		return err
	}
	defer db.Close()

	rows, err := db.QueryContext(ctx, "SELECT * FROM guestbook")
	if err != nil {
		return err
	}
	defer rows.Close()

	for rows.Next() {
		var guestName, content string
		var date time.Time
		var entryID int64
		if err := rows.Scan(&guestName, &content, &date, &entryID); err != nil {
			return err
		}
		fmt.Printf("%s\t%s\t%s\t%d\n", guestName, content, date.Format(time.RFC3339), entryID)
	}

	return nil
}
```

## PostgreSQL

```go:main.go
package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"time"

	_ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres"
)

func main() {
	ctx := context.Background()

	if err := showRecords(ctx, "project:region:instance-name", "postgres", "user", "user_password"); err != nil {
		log.Fatal(err)
	}
}

func showRecords(ctx context.Context, dbAddress, dbName, dbUser, dbPassword string) error {
	// sslmode は必ず disable にする必要があります。
	dsn := fmt.Sprintf("host=%s user=%s dbname=%s password=%s sslmode=disable", dbAddress, dbUser, dbName, dbPassword)
	db, err := sql.Open("cloudsqlpostgres", dsn)
	if err != nil {
		return err
	}
	defer db.Close()

	rows, err := db.QueryContext(ctx, "SELECT * FROM guestbook")
	if err != nil {
		return err
	}
	defer rows.Close()

	for rows.Next() {
		var guestName, content string
		var date time.Time
		var entryID int64
		if err := rows.Scan(&guestName, &content, &date, &entryID); err != nil {
			return err
		}
		fmt.Printf("%s\t%s\t%s\t%d\n", guestName, content, date.Format(time.RFC3339), entryID)
	}

	return nil
}
```

# ビルド・実行
`GOOGLE_APPLICATION_CREDENTIALS`環境変数には、作成したサービスアカウントの秘密鍵へのパスをセットしてください。
ここでセットした秘密鍵が、 Cloud SQL Proxy パッケージ内で認証情報として使用されます。

```shell-session
$ go build -o example-cloudsqlproxypackage main.go
$ GOOGLE_APPLICATION_CREDENTIALS=PATH_TO_CREDENTIAL_FILE ./example-cloudsqlproxypackage
first guest     I got here!     2017-08-06T12:00:00Z    1
second guest    Me too! 2017-08-06T13:00:00Z    2
```

# サービスアカウント秘密鍵の読み込みについて
Cloud SQL Proxy で使用されるサービスアカウントの秘密鍵は、デフォルトでは`GOOGLE_APPLICATION_CREDENTIALS`環境変数にセットされているファイルを使用します。
もし、アプリケーション内で独自に読み込みをしたい場合は、以下のように処理を追加します。

```shell-session:パッケージ追加
$ go get golang.org/x/oauth2
```

## MySQL

```diff:main.go
package main

import (
	"context"
	"fmt"
+	"io/ioutil"
	"log"
+	"net/http"
	"time"

	cloudsqlproxy "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/mysql"
+	"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy"
	"github.com/go-sql-driver/mysql"
+	goauth "golang.org/x/oauth2/google"
)

func main() {
	ctx := context.Background()

+	// 秘密鍵を読み込んで、 Proxy で使用する Client をセット。
+	client, err := clientFromCredentials(ctx, "path_to_credential_file")
+	if err != nil {
+		log.Fatal(err)
+	}
+	proxy.Init(client, nil, nil)

	if err := showRecords(ctx, "project:region:instance-name", "mydb", "root", "password"); err != nil {
		log.Fatal(err)
	}
}

func showRecords(ctx context.Context, dbAddress, dbName, dbUser, dbPassword string) error {
	// 省略
	return nil
}

+// 参考: https://github.com/GoogleCloudPlatform/cloudsql-proxy/blob/master/tests/dialers_test.go#L89
+// 秘密鍵より Proxy で使用する Client を作成。
+func clientFromCredentials(ctx context.Context, file string) (*http.Client, error) {
+	const SQLScope = "https://www.googleapis.com/auth/sqlservice.admin"
+	var client *http.Client
+
+	all, err := ioutil.ReadFile(file)
+	if err != nil {
+		return nil, err
+	}
+
+	cfg, err := goauth.JWTConfigFromJSON(all, SQLScope)
+	if err != nil {
+		return nil, err
+	}
+
+	client = cfg.Client(ctx)
+
+	return client, nil
+}
```

## PostgreSQL

```diff:main.go
package main

import (
	"context"
	"database/sql"
	"fmt"
+	"io/ioutil"
	"log"
+	"net/http"
	"time"

	_ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres"
+	"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy"
+	goauth "golang.org/x/oauth2/google"
)

func main() {
	ctx := context.Background()

+	// 秘密鍵を読み込んで、 Proxy で使用する Client をセット。
+	client, err := clientFromCredentials(ctx, "path_to_credential_file")
+	if err != nil {
+		log.Fatal(err)
+	}
+	proxy.Init(client, nil, nil)

	if err := showRecords(ctx, "project:region:instance-name", "postgres", "user", "user_password"); err != nil {
		log.Fatal(err)
	}
}

func showRecords(ctx context.Context, dbAddress, dbName, dbUser, dbPassword string) error {
	// 省略
	return nil
}

+// 参考: https://github.com/GoogleCloudPlatform/cloudsql-proxy/blob/master/tests/dialers_test.go#L89
+// 秘密鍵より Proxy で使用する Client を作成。
+func clientFromCredentials(ctx context.Context, file string) (*http.Client, error) {
+	const SQLScope = "https://www.googleapis.com/auth/sqlservice.admin"
+	var client *http.Client
+
+	all, err := ioutil.ReadFile(file)
+	if err != nil {
+		return nil, err
+	}
+
+	cfg, err := goauth.JWTConfigFromJSON(all, SQLScope)
+	if err != nil {
+		return nil, err
+	}
+
+	client = cfg.Client(ctx)
+
+	return client, nil
+}
```

# サンプルコードのリポジトリ
最終的なサンプルコードはこちらのリポジトリに上げています。
[hirsim/example-cloudsqlproxypackage](https://github.com/hirsim/example-cloudsqlproxypackage "Github")

# 参考

* [Cloud SQL for MySQL のクイックスタート](https://cloud.google.com/sql/docs/mysql/quickstart?hl=ja "Google Cloud Platform")
* [Cloud SQL for PostgreSQL のクイックスタート](https://cloud.google.com/sql/docs/postgres/quickstart?hl=ja "Google Cloud Platform")
* [Cloud SQL Proxy について](https://cloud.google.com/sql/docs/mysql/sql-proxy "Google Cloud Platform")
* [GoogleCloudPlatform/cloudsql-proxy](https://github.com/GoogleCloudPlatform/cloudsql-proxy "Github")