GoでDynamoDBにdatabase/sqlを使ってアクセスする

2024/05/09に公開

こちらの記事で一部紹介したbtnguyen2k/godynamoの掘り下げ記事です
https://zenn.dev/miyamo2/articles/9603130983545c

PartiQL

PartiQLとはAmazonがメンテナンスするOSSプロジェクトでNoSQLのためのSQL互換のクエリ言語です

PartiQL は、構造化データ、半構造化データ、ネストされたデータを含む複数のデータストア間で、SQL 互換のクエリアクセスを提供します。PartiQL は、Amazon 内で広く使用されており、現在、DynamoDB を含む多くの AWS のサービスの一部として利用できます。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/ql-reference.html

DynamoDBで使用する場合にはJOIN句やサブクエリが使えなかったり、
SELECTと(INSERT|UPDATE|DELETE)が同一のトランザクションが使えなかったり、
DynamoDB由来の制約はあるものの、個人的にはもっと注目されるべきソリューションだと思ってます

btnguyen2k/godynamo

https://github.com/btnguyen2k/godynamo

btnguyen2k/godynamoはPartiQLステートメントを使用してDynamoDBへアクセスするためのSQLドライバです
PartiQLステートメントの実行自体はaws-sdk-go-v2で提供されているAPIを使用しているため、database/sqlのIFと互換を生むためのラッパーと捉えてもらってよいかと思います

https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/dynamodb#Client.ExecuteStatement

CRUD

  • SELECT
  • INSERT
  • UPDATE
  • DELETE

をサポートしています

https://github.com/btnguyen2k/godynamo/blob/main/SQL_DOCUMENT.md

DDL

PartiQL for DynamoDB自体はCREATE TABLE等のDDLはサポートしていないものの、btnguyen2k/godynamoでは独自の拡張PartiQL構文として

  • CREATE TABLE
  • ALTER TABLE
  • CREATE GSI
  • DROP TABLE
  • DROP GSI

が用意されています

https://github.com/btnguyen2k/godynamo/blob/main/SQL_TABLE.md

https://github.com/btnguyen2k/godynamo/blob/main/SQL_INDEX.md

接続文字列

下記が接続文字列のフォーマットです

Region=<aws-region>;AkId=<aws-access-key-id>;Secret_Key=<aws-secret-key>[;Endpoint=<aws-dynamodb-endpoint>][;TimeoutMs=<timeout-in-milliseconds>]

Data Source Name (DSN) format for AWS DynamoDB

読んで字のごとくですが各パラメータの意味合いはこんな感じです

パラメータ 設定項目
Region AWSリージョン
AkId アクセスキーID
Secret_Key シークレットキー
Endpoint DynamoDBエンドポイント
TimeoutMs タイムアウト時間(ミリ秒)

TimeoutMsが設定されていない場合、タイムアウト時間はデフォルト値の10000msが適用されます

またRegionAkIdSecret_Keyが設定されていない場合は、それぞれ以下の環境変数から取得して解決されます

リージョン アクセスキーID シークレットキー
AWS_REGION AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY

実行環境がAWS Lambdaであれば、上記3つはどれもランタイム環境変数の為、VPCエンドポイント経由でアクセスしたい、もしくはタイムアウト時間を独自に設定したい場合を除けば接続文字列は空文字で渡してしまっても問題ないです

package main

import (
	"context"
    "log"
	"database/sql"
	"fmt"
	"github.com/aws/aws-lambda-go/lambda"
	_ "github.com/btnguyen2k/godynamo"
)

const driver string = "godynamo"

var (
	dynamodb *sql.DB
)

func Handler(ctx context.Context) error {
	// DynamoDBを利用したなにがしかの処理
}

func main() {
	dynamodb = func() *sql.DB {
		db, err := sql.Open(driver, "")
		if err != nil {
			log.Fatal(err)
		}
		return db
	}()
	defer dynamodb.Close()
	lambda.Start(Handler)
}

1MB問題

DynamoDBで開発経験のある方なら馴染み深いであろう1MB問題は内部で自動的にフェッチしてくれるので、よりRDBに近い書き味でDynamoDBにアクセスできます

O11y

DataDogNew RelicAWS X-Ray共にaws.Configにミドルウェアを設定することで各AWSリソースのトレースを実現しています

btnguyen2k/godynamoではaws.Configを基にDynamoDBとのコネクションを確立するためのRegisterAWSConfigという関数が用意されているため、この関数にミドルウェアを設定したaws.Configを渡すだけで簡単に自動計装をすることができます

btnguyen2k/godynamoではaws.Configの更新/参照をsync.RWMutexで排他制御していますが、RegisterAWSConfigの呼び出しは極力コールドスタート時の一度だけとなるのが望ましいかと思います

DataDog

package main

import (
	"context"
	"log"
	"os"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/btnguyen2k/godynamo"
	awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go-v2/aws"
)

const driver string = "godynamo"

var (
	dynamodb *sql.DB
)

func Handler(ctx context.Context) error {
	// DynamoDBを利用したなにがしかの処理
}

func main() {
	ctx := context.Background()
	// aws.Configを取得
	awsConfig, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		log.Fatal(err)
	}

	// 計装用のミドルウェアを設定
	awstrace.AppendMiddleware(&awsConfig)
	// aws.ConfigをSQLドライバに登録
	godynamo.RegisterAWSConfig(awsConfig)
    
	dynamodb = func() *sql.DB {
		db, err := sql.Open(driver, "")
		if err != nil {
			log.Fatal(err)
		}
		return db
	}()
	defer dynamodb.Close()
	lambda.Start(Handler)
}

New Relic

package main

import (
	"context"
	"log"
	"os"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/btnguyen2k/godynamo"
	nraws "github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2"
	"github.com/newrelic/go-agent/v3/newrelic"
)

const driver string = "godynamo"

var (
	dynamodb *sql.DB
	nrapp *newrelic.Application
)

func Handler(ctx context.Context) error {
	txn := app.StartTransaction("Foo")
	defer txn.End()
	ctx = newrelic.NewContext(ctx, txn)
	// DynamoDBを利用したなにがしかの処理
}

func main() {
	ctx := context.Background()
	// aws.Configを取得
	awsConfig, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		log.Fatal(err)
	}

	nrapp = func() *newrelic.Application {
		app, err := newrelic.NewApplication(
			newrelic.ConfigAppName("test-app"),
			newrelic.ConfigLicense(os.Getenv("NEW_RELIC_CONFIG_LICENSE")),
			newrelic.ConfigDistributedTracerEnabled(true),
		)
		if err != nil {
			log.Fatal(err)
		}
		return app
	}
	// 計装用のミドルウェアを設定
	nraws.AppendMiddlewares(&awsConfig.APIOptions, nil)

	// aws.ConfigをSQLドライバに登録
	godynamo.RegisterAWSConfig(awsConfig)
    
	dynamodb = func() *sql.DB {
		db, err := sql.Open(driver, "")
		if err != nil {
			log.Fatal(err)
		}
		return db
	}()
	defer dynamodb.Close()
	lambda.Start(Handler)
}

Discussion