tl;dr
こちらをcloneして、READMEの通りにやってみてください!
DynamoDB、API Gatewayのサンプルコードがありますが、
他のAWSサービスにも応用はしやすくなっています。
背景
DynamoDBにデータを入出力するときにはAWS SDKを使う場合がほとんどだと思いますが、SDKの内部ではWeb APIを実行していて、SDKを使わずに生のAPIをそのまま叩くこともできます。公式ドキュメントには「低レベルAPI」として紹介されています。
低レベルAPIを実行するには、認証のため「APIリクエストの署名」というのが必要です。通常はSDK側で吸収してくれてる部分ですね。
署名ヘッダを使うサンプルは、検索すると色々と出てきます。たとえばクラスメソッドさんのこの記事は、AWS SDK v3にも対応していて分かりやすかった。
しかしAPI Gateway宛にリクエストするサンプルばかりで、DynamoDBに対してそのまま使えるサンプルコードがすぐには見つけられませんでした。
ということで、作ってみました!
解説
DynamoDBテーブルの作成
公式ドキュメントに書いてあるとおりの構造でテーブルを作って、サンプルデータを入れておきます。
- テーブル名:
Pets
- パーティションキー:
AnimalType
- ソートキー:
Name
- サンプルデータ:
{ "AnimalType": "Dog", "Name": "Fido" }
SigV4署名ヘッダを使ったリクエスト
クラスメソッドさんの記事のコードをベースに、
汎用的に使えるようにモジュールにしました。
API GatewayでもDynamoDBでも、その他のサービスでも、
このpost
関数を使って署名ヘッダを使ったAPIリクエストができます。
import dotenv from 'dotenv'
import { HttpRequest } from '@aws-sdk/protocol-http'
import { SignatureV4 } from '@aws-sdk/signature-v4'
import { Sha256 } from '@aws-crypto/sha256-universal'
import { defaultProvider } from '@aws-sdk/credential-provider-node'
import { request } from 'undici'
dotenv.config()
export const post = async ({
serviceName,
region,
url,
headers,
body,
}: {
serviceName: string
region: string
url: string
headers: Record<string, string>
body: any
}) => {
const apiUrl = new URL(url)
const signatureV4 = new SignatureV4({
service: serviceName,
region: region,
credentials: defaultProvider(),
sha256: Sha256,
})
const httpRequest = new HttpRequest({
headers: {
...headers,
host: apiUrl.hostname,
},
hostname: apiUrl.hostname,
method: 'POST',
path: apiUrl.pathname,
body: JSON.stringify(body),
})
const signedRequest = await signatureV4.sign(httpRequest)
return request(signedRequest)
}
DynamoDBの低レベルAPIを叩く
DynamoDBの場合、
リクエスト先URLはリージョンごとに共通、
リクエストヘッダのX-Amz-Target
でオペレーション名を指定、
というのが特徴ですね。
コンテンツタイプのapplication/x-amz-json-1.0
というのは初めて見た。
post
関数内ではヘッダ&ボディの両方を使って署名ヘッダが作られ、その署名をさらにヘッダに追加して、AWSにリクエストしています。
リクエストボディが変わると署名は変わるし、署名の有効期限も短いので、「事前に署名を作っておいて何度も使い回す」ということは基本はできません。毎回新しく署名することで、APIトークンやパスワードよりも安全に通信できるわけですね。
import { inspect } from 'util'
import { post } from './awsLowLevelApi'
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
post({
serviceName: 'dynamodb',
region: 'ap-northeast-1',
url: 'https://dynamodb.ap-northeast-1.amazonaws.com',
headers: {
'Accept-Encoding': 'identity',
'Content-Type': 'application/x-amz-json-1.0',
'X-Amz-Target': 'DynamoDB_20120810.GetItem',
},
body: {
TableName: 'Pets',
Key: marshall({
AnimalType: 'Dog',
Name: 'Fido',
}),
},
}).then(async (res) => {
const body = await res.body.json()
let unmarshalled
try {
unmarshalled = unmarshall(body?.Item)
} catch {
unmarshalled = {}
}
console.log(
inspect(
{
statusCode: res.statusCode,
data: body,
unmarshalled,
},
{ colors: true, depth: Infinity }
)
)
})
実行結果はコチラ
{
statusCode: 200,
data: { Item: { AnimalType: { S: 'Dog' }, Name: { S: 'Fido' } } },
unmarshalled: { AnimalType: 'Dog', Name: 'Fido' }
}
参考: marshallとunmarshall
DynamoDBで使うJSONのフォーマットは、本来こんな形をしています。
{
"AnimalType": { "S": "Dog" },
"Name": { "S": "Fido" }
}
このままでは人間が読み書きしづらいので、
普通のJSON形式と相互に変換する機能がSDKに用意されています。
{
"AnimalType": "Dog",
"Name": "Fido"
}
普通のJSON → DynamoDB JSONへの変換をmarshall
DynamoDB JSON → 普通のJSONへの変換をunmarshall
と呼びます。
marshallって単語は「軍隊の整列」みたいな意味らしい。
今回SDKを使わないのはあくまで「低レベルAPIを直に叩く」部分なので、
通信に関係ないmarshall, unmarshallは遠慮なくSDKを使いましたw
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
// DynamoDB JSON形式に変換
marshall({
AnimalType: 'Dog',
Name: 'Fido',
})
// 普通のJSON形式に戻す
unmarshall({
AnimalType: { S: 'Dog' },
Name: { S: 'Fido' },
})
これは覚えておくと、いろんな場面で便利ですよ〜。
おわりに
SDKの裏側で何が行われているのか興味があって実際に動かしてみましたが、
少なくともDynamoDBをNode.jsで使う限りは、おとなしくSDKを使ったほうが絶対良いですw
この記事を応用して、SDKがない別の言語でAPIリクエストするとか、
Node.jsでAWSの他のサービスにAPIリクエストするとか、
そういう用途に役立てばよいなーと思っとります。
ではまた!