🦍

GolangでAWS AppSyncのAPIを叩く

2021/03/15に公開

久しぶりに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の設定を行います。詳しくは公式ドキュメントなどを参照。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html

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のなかでの署名に利用しています。署名部分の実装では以下を参考にしました。

これを記述するため、ルートディレクトリに 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.Queryclient.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