フューチャー技術ブログ

Go Cloud#5 AWSのローカルモック環境であるLocalStackを活用する

はじめに

こんにちは、TIG DXチームの真野です。Go Cloud記事の第5弾です。


本記事ではGo Cloud経由でAWSのローカルモック環境であるLocalStackに接続してみたいと思います。

LocalStackはモックとして活用することが多いと思いますが、Go Cloudそのものにも Blob、Docstore、Pub/Subなどを利用する際に memfile スキーマベースを指定することで動作検証が可能です。そのためGo Cloudを利用する場合のローカル開発や、CIでの自動テスト時はこちらを活用する場面も多いかなと思います。

一方で、プロセスのライフサイクルとは別に、データを永続化をさせておいて、次回起動時にも再利用したいといった場合に不便な場面もあると思います。あるいは、既存のGUIツールを用いて実行結果を確認したいといったこともあり得ると思います。こういった要求に備え、Localstackへの接続手法も抑えていきます。

LocalStackとは

💻 A fully functional local AWS cloud stack. Develop and test your cloud & Serverless apps offline! https://localstack.cloud

オフラインでAWSの開発やテストができるスタックです。

事前準備

LocalStackを起動しておきます。Dockerで起動する場合は以下のようにportを開けておきます。

docker run -it -p 4567-4584:4567-4584 -p 8080:8080 localstack/localstack:0.10.5

公式のドキュメントにも記載されていますが、4567-4584ポートがエミュレートするAWS相当のリソースが利用し(AWSサービス増加に従いだんだん増えていきますので詳細は公式ドキュメントを確認ください)、8080ポートはWeb UIが利用します。

Versionは2019/11/15時点で最新の 0.10.5 を利用していますが latest でも良いかと思います。

Blob

まずAWS CLIを用いてLocalStackにS3バケットを作成します。 --endpoint-url オプションで向き先をLocalStackに切り替えるのが特徴です。

aws --endpoint-url=http://localhost:4572 s3api create-bucket --bucket future-example

作成した future-example バケットにオブジェクトを書き込んでみます。

package main

import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"gocloud.dev/blob"
"gocloud.dev/blob/s3blob"
"io"
"log"
)

func main() {
w, err := writer("future-example", "test.txt")
if err != nil {
log.Fatal(err)
}
defer w.Close()

if _, err := w.Write([]byte("1234567890")); err != nil { // 適当な値
log.Fatal(err)
}
}

func writer(bucketURL, key string) (io.WriteCloser, error) {
sess := session.Must(session.NewSession(&aws.Config{
Endpoint: aws.String("http://localhost:4572"), // ★必要
S3ForcePathStyle: aws.Bool(true), // ★必要
}))
bucket, err := s3blob.OpenBucket(context.Background(), sess, bucketURL, nil)
if err != nil {
return nil, err
}
return bucket.NewWriter(context.Background(), key, nil)
}

LocalStackへのアクセスの場合は、driver のAWS実装側にEndpointなどを指定する必要があります。

LocalStack固有の設定
sess := session.Must(session.NewSession(&aws.Config{
Endpoint: aws.String("http://localhost:4572"),
S3ForcePathStyle: aws.Bool(true),
}))

さらに、BucketのURLも S3ForcePathStyle を指定してしまう関係で、 s3://future-example ではなく future-example とする必要があります。このあたりはまだ実装がこなれていないだけで今後改善される可能性があるので、Go Cloud側のアップデートの都度確認したほうが良いと思います。

さて、呼び出し側でURLスキーマ s3:// の有無を意識することはあまりしたくないと思います。
そのため、より業務に近いコードにするには、以下のようにEndpointが環境変数で指定されている場合には、s3スキーマを外す処理を入れると良くなると思います。

func openBucket(bucketURL string) (*blob.Bucket, error) {
endpoint := os.Getenv("ENDPOINT_URL") // ★環境変数化する
if len(endpoint) == 0 {
return blob.OpenBucket(context.Background(), bucketURL)
}

// LocalStackアクセス用の処理
u, err := url.Parse(bucketURL)
if err != nil {
return nil, err
}

sess := session.Must(session.NewSession(&aws.Config{
Endpoint: aws.String(endpoint),
S3ForcePathStyle: aws.Bool(true),
}))
return s3blob.OpenBucket(context.Background(), sess, u.Host, nil) // ★u.Hostで、s3://を切り捨てる処理
}

例としてはput-objectの実装を上げましたが、get-objectやdelete-objectに関しても、同様に session を指定することで、後はLocalStackを意識せず扱えることができます。

Pub/Sub

事前に、AWS CLI経由でqueueを作成します。

aws --endpoint-url=http://localhost:4576 sqs create-queue --queue-name test-queue
{
"QueueUrl": "http://localhost:4576/queue/test-queue"
}

このQueueUrlを用いてGo Cloud経由でSQSにアクセスします。LocalStackのSQSのポートは 4576 なので設定間違いには注意していきます。

package main

import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"gocloud.dev/pubsub"
"gocloud.dev/pubsub/awssnssqs"
"log"
)

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

sess, err := session.NewSession(&aws.Config{
Endpoint: aws.String("http://localhost:4576"), // ★
})
if err != nil {
log.Fatal(err)
}

topic := awssnssqs.OpenSQSTopic(ctx, sess, "http://localhost:4576/queue/test-queue", nil) // ★
defer topic.Shutdown(ctx)

err = topic.Send(ctx, &pubsub.Message{
Body: []byte("Hello, World!\n"),
Metadata: map[string]string{"Env": "test"},
})
if err != nil {
log.Fatal(err)
}

Blobとは微妙に driver の設定が異なり、今度は Endpoint だけで接続ができました。
その代わりに、QueueのURLを OpenSQSTopic 時に指定する必要があります。

EndpointとQueueURLに重複した設定がなされておりどちらかを省略できそうですが、現時点では両方とも設定する必要がありました。

Subscribe側も同様のdriverの設定で、後はLocalStackを意識すること無くアクセスが可能です。

DocStore

最後にDocStoreでLocalStackのDynamoDB(エミュレータ)にアクセスしてみます。

最初にAWS CLI経由でテーブルを作成します。

aws --endpoint-url=http://localhost:4569 dynamodb create-table \
--table-name test \
--attribute-definitions AttributeName=ID,AttributeType=S \
--key-schema AttributeName=ID,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

このテーブルに対してGo Cloudを利用してアクセスします。

DocStoreの場合はdriver側に Endpoint を指定するだけでアクセスできます。
LocalStackのDyanmoDBのポートは 4569 なので設定間違いには注意していきます。

package main

import (
"gocloud.dev/docstore/awsdynamodb"
// 省略
)

type Entity struct {
ID string `docstore:"ID"`
Name string `docstore:"NAME"`
}

func main() {
sess := session.Must(session.NewSession(&aws.Config{
Endpoint: aws.String("http://localhost:4569"), // ★
}))
coll, err := awsdynamodb.OpenCollection(dynamodb.New(sess), "test", "ID", "", nil)
if err != nil {
log.Fatal(err)
}
defer coll.Close()

// 書き込み
row := Entity{
ID: "1",
Name: "hoge",
}
coll.Create(context.Background(), &row)

// 読み込み
rowToRead := Entity{
ID: "1",
}
coll.Get(context.Background(), &rowToRead)
fmt.Printf("get: %+v\n", rowToRead)
}

こちらも、いったんOpenCollectionをした後は、LocalStackであることを意識せずに操作できました。

まとめ

Go Cloudはまだまだ発展途上とはいえ、現時点でも十分にLocalStackを用いてアクセスができます。
処理レイテンシや消費するリソースは memfile に比べLocalStackのプロセス群を立ち上げるだけオーバーヘッドがありますが、作成したリソースをGUIツールなどで確認可能なため、利用したい場面もあり得ると思います。

LocalStackのアクセスのためには、基本的には AWS CLIでは --endpoint-url でローカルのURLを指定しますが、Go Cloudの場合は現時点ではリソースによっては追加のオプションを指定したり、前処理をして上げる必要があります。このあたりはGo CloudのAWS driver側に自らコミットしていくか、ライブラリを被せるか判断してうまく付き合っていきたいですね。