Go
GoogleCloudPlatform

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

GCPCloud SQL へアプリケーションから接続する場合、 アプリケーションを配置しているホストのIPアドレスをホワイトリストに追加するか、 Cloud SQL Proxy を使用するかと思います。

しかし、アプリケーションが配置されるホストのIPアドレスが不定(GKE などを使っており、配置されるホストが不定など)の場合は、 Cloud SQL Proxy を使うことになると思います。
もし、そのアプリケーションを Golang で書いている場合は、 Cloud SQL Proxy の Github の 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
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));
PostgreSQL
CREATE TABLE guestbook (guestName VARCHAR(255) NOT NULL, content VARCHAR(255) NOT NULL, date TIMESTAMP NOT NULL, entryID SERIAL PRIMARY KEY);
挿入レコード
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 のパッケージを取得します。

$ go get github.com/GoogleCloudPlatform/cloudsql-proxy

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

$ go get github.com/go-sql-driver/mysql

PostgreSQL にしている場合は

$ go get github.com/lib/pq

を取得します。

サービスアカウント作成

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

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

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

ソースコード

MySQL

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

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 パッケージ内で認証情報として使用されます。

$ 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環境変数にセットされているファイルを使用します。
もし、アプリケーション内で独自に読み込みをしたい場合は、以下のように処理を追加します。

パッケージ追加
$ go get golang.org/x/oauth2

MySQL

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

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

参考