フューチャー技術ブログ

Go Cloud#1 概要とBlobへの活用方法

概要

TIG DX Unit所属の多賀です。今回は、Go Cloudシリーズとしていくつか Go Cloudに関する記事をリレー形式で書いていきたいと思います。

第一弾としては、Go Cloud についての概要と、案件でも活用した Blob を利用したサンプルについて解説します。また、認証系の情報の扱いについても記載します。

Go Cloud記事はこちらもご参考ください。

Go Cloud とは

2018/07 に Google の Go チームが立ち上げたプロジェクトで、Go アプリケーションを各クラウド間でポータブルにすることを目指して、実装されています。

昨今の開発では、マルチクラウドやハイブリッドクラウドの導入により、アプリケーションを複数のクラウドに対応させる要件がでてきています。現状では、各クラウドサービスの SDK を利用して、クラウドごとにアプリケーションコードを実装する必要がありますが、正直大変な部分もあるかと思います。

ですが、各クラウドのサービスをまとめて抽象化して、統一的なコードでアクセスできれば、ソースコードの重複を排除できて便利になりそうです。(実際、各クラウドごとに似たようなサービスは出ているので抽象化はさほど困難ではないですよね。)
この 「各クラウドサービスに統一的なコードでアクセスする」 ことを実現して、Goのアプリケーションをクラウド間でポータブルにするためのツールとして Go Cloud は開発されています。

イメージとしては、データベースアクセスがあげられるかと思います。データベースも様々な種類(MySQL,PostgreSQL,Oracle,SQLite, etc..)が存在しますが、アプリケーションとして実装する際は統一的なコードを通して、アクセスすることが多いかと思います。実際は、言語により driver interface が提供されて、各データベースごとに driver が実装されているため、この機能は実現できています。同様なことをクラウドの SDK に対して実現していきたい思惑があるのだと考えています。

実際に API の仕様設計で、データベースと似たような設計思想で作られていることが言及されていて面白かったです。(link)
(個人的には、Developers and Operatorsの章も面白かったのでおすすめです。)

実装は OSS で公開されております。
https://github.com/google/go-cloud

ドキュメントのベージは別でこちらです。
https://gocloud.dev/

ここで少々余談ですが、Go Cloud は公式的な呼称は「Go CDK (Cloud Development Kit)」となっています(Blog)。当シリーズ記事では、他サービスとの命名がかぶることでググラビリティが下がることを懸念して、「Go Cloud」と統一しております。

プロジェクトの状況

2019/11 現在 Alpha ステータスですが、Production Ready である旨は言及されています。(breaking change があるとの文言は 2019/02 に削除されています) (link)

対応クラウド

下記クラウドの一部サービスに対応しています。

  • AWS
  • GCP
  • Azure
  • HashiCorp

vendor-neutral なAPIを実装していくと言及されているので、
対象クラウドを絞っているわけではなく、順次拡大する方針のようです。

導入の検討対象

どういったケースで、利用を検討できるか The Go Blog にて言及されている内容を引用いたします。

1. You develop cloud applications locally.
-> クラウドアプリケーションをローカルで開発

2. You have on-premise applications that you want to run in the cloud (permanently, or as part of a migration).
-> オンプレのアプリケーションをクラウド上で動かしたい

3. You want portability across multiple clouds.
-> 複数クラウド間で動作するアプリケーションを開発したい

4. You are creating a new Go application that will use cloud services.
-> クラウドが提供するサービスを利用したアプリケーションを作りたい

少々、補足コメントとして私の考えを記載していきます。

1.クラウドアプリケーションをローカルで開発

クラウドアプリケーションをローカルで開発する際に、利用するクラウドサービスへの接続に困るケースが出てくるかと思います(権限/通信 etc..)。
現状の解決策としては、AWS だと LocalStackAWS SAM, GCP だと Emulator の利用があるかと思います。これはローカル環境に実際のクラウドサービスのモックを立ち上げて、処理を実行させる解決策で、とても便利です(私もよく利用しています)。

Go Cloud ではこの課題を、「各環境に合わせて実装を差し替える」ことで解決しています。例えば、S3 からファイルを読み出すコードを実装したい場合は、ローカル環境では実際に S3 にアクセスする代わりに、ローカルのファイルシステムにアクセスさせます。ローカルのファイルシステム上にファイルを用意しておくことで、クラウド上と同一の挙動を実現できます。開発時はファイルシステム、デプロイ後は S3 アクセスとすることを、Go Cloud を間に挟むことで簡単に実現できます。(ファイルパスを s3://file:// と変える)
また、単体テストを実装するのにも便利そうです

2.オンプレのアプリケーションをクラウド上で動かしたい

オンプレでのアプリケーションの実装を Go Cloud での実装に差し替えることで、オンプレでも動作させつつ、クラウド上でも同一のコードで動作させることができます。

3.複数クラウド間で動作するアプリケーションを開発したい

マルチクラウドで実装する際に、各クラウドごとのサービスは利用しない or 各サービスの抽象化レイアーを自作して実装等が考えられるかと思います。後者の抽象化レイアーを自作部分を Go Cloud が担ってくれることで、AWS 上では DynamoDB, GCP 上では Firestore, オンプレでは MongoDB といった使い分けも実現することができます。

4.クラウドが提供するサービスを利用したアプリケーションを作りたい

3 と少し異なり、動作させる環境は AWS 上ですが、アプリケーションの挙動内で各クラウドへ接続するケースでも利用できます。例としてはアプリケーション内で、S3/GCS/Azure Storage へアクセスしてファイルを取得する必要がある場合があげられます。
私の案件での導入はこの例のケースで、各 Blob システムに対してのアクセスをシンプルに実装したいと考えて利用しています。

用語

下記の実装の説明で、用語が出てくるため補足しておきます。

driver

driver interface としてGo Cloud上で定義されています(例: blob driver)。各クラウド SDK ごとに driver の実装をします。(データベースと同様ですね)
driverの実装も、現状は合わせて Go Cloud上で行われています。 (s3 driver実装)

portable type

ユーザーが実際に利用する API で、concrete type(interface ではない)が返却されます。

driver interface を利用して、共通の API が実装されています。driver interface を直接ユーザーに公開せず、共通的な実装を driver-user 間にいれたいため、このような形になっています。

また concrete type 利用の理由は、共通実装が interface の場合は重複してしまう(各 driver ごとに複製の実装が必要)ため、スケールしないためと記載されています。
(Desing Doc 参照)

Blob での活用

Blob Storage での活用方法について、記載していきます。

サポート済みサービス

  • Amazon Simple Storage Service(S3)
  • Google Cloud Storage(GCS)
  • Azure Blob Storage
  • ローカルファイルシステム
  • オンメモリ

サンプル実装

Blob(S3)から指定した key のファイルを取得するサンプルです。

blob.OpenBucket() を呼び出して、 Bucket オブジェクトを取得して、オブジェクトのメソッドを通して、各種処理を実行します。 Bucket オブジェクトから NewReader()/NewWriter() を呼び出すことで、 io.Reader/io.Writer インターフェイスを得ることもできます。 (blob - GoDoc)

main.go

package main

import (
"context"
"fmt"
"log"

"gocloud.dev/blob"
// 対象の driver を blank import する
_ "gocloud.dev/blob/s3blob"
)

func checkBlobFileContent(bucketURL, key string) (string, error) {
bucket, err := blob.OpenBucket(context.Background(), bucketURL)
if err != nil {
return "", err
}
defer bucket.Close()

// blob から 指定した key の content を取得
b, err := bucket.ReadAll(context.Background(), key)
if err != nil {
return "", err
}

return string(b), nil
}

func main() {
content, err := checkBlobFileContent("s3://future-example", "hoge.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(content)
}

サンプルを見ていただいたとおり、 OpenBucket() 呼び出し時には特に S3 固有の処理を実行していません。

Go Cloud 側でクラウドサービスごとに実装を切り替えているのですが、実際の切り替えソースとしては URL Schema が利用されています。

少し脱線しますが、実装の切替方法としては下記の通りになっています。

  1. 対象の driver (s3blob) を blank import
  2. driver 内の init 関数で Schema が defaultURLMux に登録 (src)
  3. s3 の Schema は default で s3 が定義されている (src)
  4. OpenBucket 内で defaultURLMux を利用して、Bucket オブジェクト生成 (src)

そのため blank import がないと、切り替え対象がないエラーになってしまうので注意です。

テストコード

動作確認したかったため、テストコードを実装しました。

テストでは S3 に接続したくなかったため、ローカルファイルシステムを利用しています。

先程とは違い、fileblob を blank import しています。

hoge.txt

SUCCESS!!!

main_test.go

package main

import (
"fmt"
"os"
"testing"

_ "gocloud.dev/blob/fileblob"
)

func Test_checkBlobFileContent(t *testing.T) {
pwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
type args struct {
bucketURL string
key string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "local storage test",
args: args{
bucketURL: fmt.Sprintf("file:///%v", pwd),
key: "hoge.txt",
},
want: "SUCCESS!!!",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := checkBlobFileContent(tt.args.bucketURL, tt.args.key)
if (err != nil) != tt.wantErr {
t.Errorf("checkBlobFileContent() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("checkBlobFileContent() = %v, want %v", got, tt.want)
}
})
}
}

テスト結果

$ go test -v .
=== RUN Test_checkBlobFileContent
=== RUN Test_checkBlobFileContent/local_storage_test
--- PASS: Test_checkBlobFileContent (0.00s)
--- PASS: Test_checkBlobFileContent/local_storage_test (0.00s)
PASS
ok example.com/xxxx/gocdk-blog 0.019s

無事テストがパスして、ローカルファイル上のファイルが読み込めております。

認証情報について

OpenBucket() を利用することで、S3/ローカルファイルシステムを切り替えられることは説明いたしました。

ですが実運用となると、認証情報付きで扱いたいケースが多くなってきます。

その場合は、driver の実装側に認証情報付きで Bucket オブジェクトを生成する関数が用意されているためそちらを利用します。

go-cloud/blob/s3blog.go から引用

// OpenBucket returns a *blob.Bucket backed by S3.
// AWS buckets are bound to a region; sess must have been created using an
// aws.Config with Region set to the right region for bucketName.
// See the package documentation for an example.
func OpenBucket(ctx context.Context, sess client.ConfigProvider, bucketName string, opts *Options) (*blob.Bucket, error) {
drv, err := openBucket(ctx, sess, bucketName, opts)
if err != nil {
return nil, err
}
return blob.NewBucket(drv), nil
}

こちらを利用することで実現できるのですが、実装側がクラウドごとの仕様に汚染されることが想像できます。

func checkBlobFileContent(bucketName, key string) (string, error) {
// S3固有の処理になってしまう
sess := session.Must(session.NewSession(&aws.Config{}))
bucket, err := s3blob.OpenBucket(context.Background(), sess, bucketName, nil)
if err != nil {
return "", err
}

認証系を扱いたい場合はどうしても、処理を各クラウドごとに分ける必要があります。

ですが、Bucket オブジェクトを生成する関数と利用する関数を分けることで汚染先を減らすことができます。

私が実際に実装したコードはだいたい、下記の通りにしております。ビジネスロジック側には、 Bucket オブジェクトを渡すことでクラウド SDK ごとの依存を New 関数 内に閉じ込めています。

// Session は各クラウドごとの認証情報を格納する struct
type Session struct {
S3Config *aws.Config
GCSCredential *google.Credentials
AzureCredential *azblob.SharedKeyCredential
}

func NewBucket(ctx context.Context, urlstr string, sess Session) (*blob.Bucket, error) {
// 切り替えは url schema で実現されているため raw url で受け取って変換
u, err := url.Parse(urlstr)
if err != nil {
return nil, err
}

// Go Cloud側の実装に合わせて、schemaでswitchさせる
switch u.Scheme {
case "s3":
sess, err := session.NewSession(sess.S3Config)
if err != nil {
return nil, fmt.Errorf("create s3 session failed: %w", err)
}
return s3blob.OpenBucket(ctx, sess, u.Host, nil)
case "gs":
token := gcp.CredentialsTokenSource(sess.GCSCredential)
client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), token)
if err != nil {
return nil, err
}
return gcsblob.OpenBucket(ctx, client, u.Host, nil)
case "azblob":
pipeline := azureblob.NewPipeline(sess.AzureCredential, azblob.PipelineOptions{})
return azureblob.OpenBucket(ctx, pipeline, azureblob.AccountName(sess.AzureCredential.AccountName()), u.Host,
&azureblob.Options{Credential: sess.AzureCredential})
case "mem":
return blob.OpenBucket(ctx, urlstr)
default:
return nil, fmt.Errorf("unsupported scheme: %v", u.Scheme)
}
}

このあたりは、 正直あまりイケていないので、もっと良い実装がしたいなと思っています。

Wire を利用することで解決できないかなと、もやもや考えてます。
(Wireとは、過去Go Cloudリポジトリに同梱されていて、今は別リポジトリで管理されている DI ツールです。コード生成をすることで、DIを実現しています。)

まとめ

Go Cloud の概要と目指す先について、また Blob のサンプル実装について解説しました。

プロジェクト自体で実現したいことも非常に面白いかつ有用ですし、実際に Blob で利用してみて、オブジェクト生成部分のみ気を使えば、GCS,Azure Storage などの接続先をかんたんに追加できました。また、OSS ですので内部のコードを追うことで SDK の利用方法についても知ることができます。Go Cloud側で SDK を適切に使えているかチェックすることもできますし(安心)、利用したことのない SDK の参考実装を調べることもできます。Azure Storage は初めて利用したのですが、GoDoc を追うことでなにの情報を渡せば接続できるか等が簡単に理解できました。このように、学習する範囲を狭めてくれる点も、良い点ではないかと思いました。

今後も、活用していきたいですし、動向をWatchしていきたいです。


Go Cloud記事はこちらもご参考ください。