GolangでAWS AppSyncのAPIを叩く
久しぶりにGolang触ったけど楽しいですね。
AWSをいじるにはやっぱり github.com/aws/aws-sdk-go-v2 を使います。だいたいの用途ではこれで済むけど、AppSyncのクライアントは入っていないのが悩みどころ。
なんとSonyからgithub.com/sony/appsync-client-goっていうライブラリが出ているけど、ドキュメントとスター数が少なめだったので念のため今回は見送り。
GraphQL公式に掲載されていてドキュメントやスターもそれなりにある汎用のGraphQLクライアントgithub.com/shurcooL/graphqlをベースにやってみることにしました。
AppSyncが普通のGraphSQLのAPIと違って特殊なのはSignature v4による署名が必要であるという点。これを乗り越えれば普通のGraphQLと同じように使えるはず。
今回はCLIから利用することを前提に、IAM認証でやっていきます(試していないけどLambdaからもいけるはず)。
1. awscliをセットアップ
awscliをインストールして、aws configure
でAccess Key IDやSecret Access Keyの設定を行います。詳しくは公式ドキュメントなどを参照。
2. 依存パッケージをインストール
go mod init go-appsync-client #適当に名前をつけてください
go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/shurcooL/graphql
3. AppSync用のhttp.Clientを用意
github.com/shurcooL/graphql
には認証のための機能はなくて、「http.Client
を渡せるようにしておくからそれでなんとかやって」というポリシーです。
AppSyncの認証を通すためには、リクエストボディやらSecret Access Keyやらを組み合わせて作った署名をHTTPヘッダーに入れる必要があるため、その部分を自前のhttp.Clientを用意することで乗り越えます。
ポイントは、http.ClientのTransport
フィールドにTransportインターフェイスを満たす構造体を渡してあげることです。
Transportインターフェイスは、func (t *transport) RoundTrip(req *http.Request) (*http.Response, error)
のようなメソッドを生やせばそれだけでOK。このメソッドで「リクエストを受け取ってレスポンスを返すまで」の処理を書き換えることができます。
ここでは、以下のようなtransport
という構造体を定義しました。
type transport struct {
ctx context.Context
credentials aws.Credentials
region string
}
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
// request bodyをコピーして取得
body, err := req.GetBody()
if err != nil {
return nil, err
}
// payloadHashを作成
buf := new(bytes.Buffer)
buf.ReadFrom(body)
b := sha256.Sum256(buf.Bytes())
payloadHash := hex.EncodeToString(b[:])
// リクエストに署名を付加
signer := v4.NewSigner()
err = signer.SignHTTP(t.ctx, t.credentials, req, payloadHash, "appsync", t.region, time.Now())
if err != nil {
return nil, fmt.Errorf("failed to sign request: %v", err)
}
// リクエストを実行して結果を返す
client := &http.Client{}
return client.Do(req)
}
ContextとAWSの認証情報・リージョンを保持し、それをRoundTrip
のなかでの署名に利用しています。署名部分の実装では以下を参考にしました。
- AWS Lambda から AppSync の API を ぶん殴る|R3 Cloud Journey: JavaScriptやPythonでの実装例
- v4 · pkg.go.dev: 署名に使える公式SDKのドキュメント
- Amazon Elasticsearch Service への HTTP リクエストの署名 - Amazon Elasticsearch Service: Elasticsearch Service × Golangでの実装例
これを記述するため、ルートディレクトリに client.go
というファイルを作りました(名前はなんでもOK)。
ファイル全体はこのような感じです。
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
)
type transport struct {
ctx context.Context
credentials aws.Credentials
region string
}
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
body, err := req.GetBody()
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
buf.ReadFrom(body)
b := sha256.Sum256(buf.Bytes())
payloadHash := hex.EncodeToString(b[:])
signer := v4.NewSigner()
err = signer.SignHTTP(t.ctx, t.credentials, req, payloadHash, "appsync", t.region, time.Now())
if err != nil {
return nil, fmt.Errorf("failed to sign request: %v", err)
}
client := &http.Client{}
return client.Do(req)
}
func NewAppSyncHTTPClient(ctx context.Context, credentials aws.Credentials, region string) *http.Client {
return &http.Client{
Transport: &transport{
ctx: ctx,
credentials: credentials,
region: region,
},
}
}
AppSync用のhttp.Clientを作るためのNewAppSyncHTTPClient
関数も入れました。
4. ついにAppSyncのAPIを叩く
下ごしらえはこんなところです。あとはgithub.com/shurcooL/graphqlのドキュメントを参考にしながら自前のhttp.Client
を食わせてclient.Query
やclient.Mutate
を使えばAppSyncを叩けるはずです。
以下はMutationの例。同じくルートディレクトリにmain.go
を作ります。クエリ本文や変数はご自分のスキーマに合わせて記述してください。
package main
import (
"context"
"log"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/shurcooL/graphql"
)
const Region = "ap-northeast-1"
const AppSyncAPIURL = "https://xxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql"
// ご自分のスキーマに合わせてください
type CreateUserInput struct {
DisplayName graphql.String `json:"displayName,omitempty"`
Birthdate graphql.String `json:"birthdate,omitempty"`
}
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithRegion("ap-northeast-1"),
)
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
credentials, err := cfg.Credentials.Retrieve(context.TODO())
if err != nil {
log.Fatalf("failed to retrieve credentials")
}
// このあたりのお作法は`github.com/shurcooL/graphql`のドキュメントを参照
var m struct {
CreateUser struct {
ID graphql.String
} `graphql:"createUser(input: $input)"`
}
variables := map[string]interface{}{
"input": CreateUserInput{
DisplayName: graphql.String("Gopher"),
Birthdate: graphql.String("2009-11-10"),
},
}
client := NewAppSyncHTTPClient(context.TODO(), credentials, Region)
appsyncClient := graphql.NewClient(AppSyncAPIURL, client)
err = appsyncClient.Mutate(context.TODO(), &m, variables)
if err != nil {
log.Fatalf("failed to request to appsync: %v", err)
}
}
あとは go run *.go
などで実行できるはず。
Discussion