概要
- Lambda と DynamoDB の勉強がしたい
- API Gateway を使わなくとも Lambda に HTTP エンドポイントを作成できるようになったらしいので使ってみたい
以上の状況だったのでちょっとそのあたりを触ってみました。
なおタイトルにもある通り、ランタイムは Ruby 2.7 を選択しています。
※選択肢の中では自分が Ruby に慣れているというだけです
やったこと
- コンソールから Lambda 関数を作成
- コンソールから DynamoDB のテーブルを作成
- ローカルの apache にファイルを置いて、そこから Lambda にリクエスト
- 受け取ったデータを使って DynamoDB のアイテムを作成
- コンソールから追加されたアイテムを確認
手順詳細
コンソールから Lambda 関数を作成
今回は簡単に認証なしとします。
また、CORS をよしなにやってくれる設定も ON にしておきます。
「詳細設定」の 「関数 URL を有効化」 というのは比較的最近できたオプションです。
少し前までは、今回作成しようとしているように、関数に HTTP(S) でリクエストを送ろうと思うと API Gateway と組み合わせる必要がありました。
それがこのオプションを ON にするだけで関数にエンドポイントを割り当てることができるようになったので、随分お手軽に試せるようになりました。
実行ロールの調整
設定 > アクセス権限 > 実行ロール
を確認します。
勝手にこの関数用のロールを作ってくれています。
このロールにポリシーを追加します。
追加するのは、DynamoDB にアクセス・書き込みするためのロールです。
AmazonDynamoDBFullAccess
AWSLambdaDynamoDBExecutionRole
コンソールから DynamoDB のテーブルを作成
user
テーブルを作成します。
コンソールから作るときは感覚的に作れるので特に迷わないと思います。
user
テーブルのパーティションキーは数値型の id にしておきます。
パーティションキーの説明はここではしませんが、この場合はプライマリーキーと考えればいいと思います。
このuser
テーブルの id を 1 からの連番にしたいのですが、DynamoDB には MySQL のような連番を作ってくれるしくみがないらしいので、自分でうまいことやります。
その「うまいことやる」ために sequence
テーブルを作成します。
こちらはパーティションキーを文字列型の tablename
というのにしておきます。
続いて、この sequence
テーブルに「項目を作成」します。
「項目」はアイテム、RDB ではレコードに相当するものです。
パーティションキーとして設定した tablename
には user
という文字列を登録します。
また、seq
という属性(RDB で言うところのカラム的なもの)も追加したいので、「新しい属性の追加」で数値型の seq
属性を追加して、こちらは値 0
として「項目を作成」します。
なんのためにこのようなことをしたのか ですが、先ほど書いたように DynamoDB には MySQL のような連番を作ってくれるしくみがありません。
なので、この sequence
テーブルのレコードで、テーブルごとの連番を管理しようとしています。
今作成した tablename = "user", seq = 0
というアイテムは「user
テーブルの現在の id
の最大値は 0
」という事を表しています。
user
テーブルに新しくレコードを追加しようとする(新しい連番が必要になる)ときに、この項目の seq
を +1
して、その数字を使うようにすればよい、という感じです。
そんなことしなくても user
テーブルの一番大きな数字を確認して、+1
すれば良いのでは?という気もしますが、それだと例えば同時に2つのレコードを作ろうとしたときに同じ id で作成しようとしてしまうことになり、プライマリーキーの重複 → エラー、となるためこういう方法を取るらしいです。
※「アトミックカウンター」というワードで調べると良いです
ローカルの apache にファイルを置いて、そこから Lambda にリクエスト
ここは Lambda と関係ないのでサラッと。
ローカルの apache でアクセスできる場所に以下のようなファイルを配置してアクセスできるようにしました。
いくつか入力できる input があって、リクエスト送信できるだけです。
なんでもいいですが、jQuery が慣れてて楽なので CDN で利用しています。
見た目は最低限を作るために Bootstrap 5 をこちらも CDN で利用しています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous"
/>
<title>lambda</title>
</head>
<body>
<div class="container mt-2">
<h1>Lambda Test</h1>
<div class="mb-3">
<label for="userNameInput">ユーザ名</label>
<input
type="text"
class="form-control"
id="userNameInput"
/>
</div>
<div class="mb-3">
<label for="userAgeInput">年齢</label>
<input
type="number"
class="form-control"
id="userAgeInput"
/>
</div>
<div class="mb-3">
<label for="userEmailInput">メールアドレス</label>
<input
type="email"
class="form-control"
id="userEmailInput"
/>
</div>
<div>
<button type="button" id="userInfoSubmitButton" class="btn btn-primary">
送信
</button>
</div>
</div>
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"
></script>
<script>
$(function () {
$("#userInfoSubmitButton").on("click", function () {
$.ajax({
// ↓ここだけ伏せています
url: "https://xxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/",
type: "POST",
data: {
name: $("#userNameInput").val(),
age: $("#userAgeInput").val(),
email: $("#userEmailInput").val(),
},
})
.done(() => {
alert("登録しました");
})
.fail(() => {
alert("エラーが発生しました");
});
});
});
</script>
</body>
</html>
受け取ったデータを使って DynamoDB のアイテムを作成
Lambda 関数の本体を書いていきます。
とりあえず今回の完成形が以下になりました。
require 'json'
require 'aws-sdk-dynamodb'
require 'base64'
require 'uri'
TABLE_NAME_SEQUENCE = "sequence"
TABLE_NAME_USER = "user"
def lambda_handler(event:, context:)
puts "event: #{event}"
body = event["body"]
puts "event['body']: #{body}"
base64_decoded_body = Base64.decode64(body)
puts "base64_decoded_body: #{base64_decoded_body}"
decode_www_form = URI.decode_www_form(base64_decoded_body)
puts "decode_www_form: #{decode_www_form}"
data = decode_www_form.to_h
puts "data: #{data}"
resource = get_resource
new_id = get_new_id(resource)
puts "new_id: #{new_id}"
user_table = get_table(resource)
data["id"] = new_id
put_item(user_table, data)
{ statusCode: 200, body: JSON.generate("count: #{user_table.item_count}") }
end
def get_resource
Aws::DynamoDB::Resource.new(region: 'ap-northeast-1')
end
def get_table(resource, name: TABLE_NAME_USER)
resource.table(name)
end
def add_item(table, data)
table.put_item(data)
end
def get_new_id(resource)
table = get_table(resource, name: TABLE_NAME_SEQUENCE)
hash = {
tablename: TABLE_NAME_USER
}
update_option = {}
# アイテムを特定するための情報
# よってプライマリキーである tablename を指定した Hash を渡している
update_option[:key] = hash
# これを指定することで、更新後の値が取得できる
# インクリメント語の seq の値が欲しいので指定する必要がある
update_option[:return_values] = "UPDATED_NEW"
# この後で指定する :update_expression にて:increment という部分に 1 を代入することを表す
# プレースホルダ的な感じ
update_option[:expression_attribute_values] = {":increment": 1}
# どういう変更を加えるのかを指定
# この書き方で seq 属性を 1 インクリメント を表す
update_option[:update_expression] = "ADD seq :increment"
# 更新する
# res には、更新後の情報が含まれる
res = table.update_item(update_option)
puts("res", res)
res[:attributes]["seq"]
end
def put_item(table, data)
table.put_item({item: data})
end
解説記事用・勉強用を意識しているためコメント多め、デバッグ用 puts
をあえて残しています。
送信パラメータを整形する
先ほどのフォームから以上のような値を送信してみます。
Lambda 側では、フォームから飛んできた値を受け取り、それを body
に入れます。
このとき、リクエストの情報は lambda_handler
の仮引数 event
に入ってきます。
event
は Hash
で、body の情報は event["body"]
で取得できます。
※この event
がどんな値になっているかは puts event
でログに記録できるので、そのようにして確認するとお手軽です
puts "event: #{event}"
ここで注意すべき事があり、この event["body"]
は base64 でエンコードされています。
イマイチなんのためなのかよくわかりませんが、そういうものらしいです。
body = event["body"]
puts "event['body']: #{body}"
event['body']: bmFtZT0lRTMlODMlODYlRTMlODIlQjklRTMlODMlODglRTMlODMlQTYlRTMlODMlQkMlRTMlODIlQjYmYWdlPTI2JmVtYWlsPXRlc3QlNDBmb29iYXIuY29t
Base64 Encode されるのはどんなとき?
「テキスト型ではない Content-Type
(text/
以外) の場合、body 部分を Base64 Econde する」
というルールらしいです。
API Gateway がこうなっているようですが、「関数 URL を有効化」オプションを利用した場合にも同様のようですね。
base64 でデコードすると base64_decoded_body
が得られます。
base64_decoded_body = Base64.decode64(body)
puts "base64_decoded_body: #{base64_decoded_body}"
base64_decoded_body: name=%E3%83%86%E3%82%B9%E3%83%88%E3%83%A6%E3%83%BC%E3%82%B6&age=26&email=test%40foobar.com
今度はURLエンコードされています。
これもデコードが必要ですね。
decode_www_form = URI.decode_www_form(base64_decoded_body)
puts "decode_www_form: #{decode_www_form}"
decode_www_form: [["name", "テストユーザ"], ["age", "26"], ["email", "test@foobar.com"]]
ようやく送信した値がきれいに読めるようになりました。
ついでに .to_h
でいい具合の Hash にできるのでしておきます
data = decode_www_form.to_h
puts "data: #{data}"
data: {"name"=>"テストユーザ", "age"=>"26", "email"=>"test@foobar.com"}
これでようやく送信したパラメータが扱いやすい形になりました。
最新の id を取得する
前述したように、最新の id 番号は sequence
テーブルを使って取得します。
ここからは DynamoDB へのアクセスが必要になるので、require 'aws-sdk-dynamodb'
して SDK を利用しています。
Module: Aws::DynamoDB — AWS SDK for Ruby V3
Aws::DynamoDB::Resource
Aws::DynamoDB::Client
これはどちらを使ってもいいですが、私はオブジェクト指向的に扱いやすくて分かりやすい Resource
が好みなのでこちらを使います。
最新のid 取得用には get_new_id
というメソッドを定義しました。
Resource
を取得し、そこから sequence
テーブルの Table
オブジェクトを取得しています。
table = get_table(resource, name: TABLE_NAME_SEQUENCE)
sequence
テーブルの tablename = "user"
の項目の seq
属性の値が、今の user
テーブルの最大 id 値を表している、ということは既に説明したとおりです。
update_item メソッドを使って、この seq
属性を 1 だけインクリメントし、インクリメント後の値を返してもらいます。この値こそが新しい user
アイテムに設定すべき id
を表します。
update_item の使い方はドキュメントを参照すべきですが、今回は以下のようにして利用できます。
hash = {
tablename: TABLE_NAME_USER
}
update_option = {}
# アイテムを特定するための情報
# よってプライマリキーである tablename を指定した Hash を渡している
update_option[:key] = hash
# これを指定することで、更新後の値が取得できる
# インクリメント語の seq の値が欲しいので指定する必要がある
update_option[:return_values] = "UPDATED_NEW"
# この後で指定する :update_expression にて:increment という部分に 1 を代入することを表す
# プレースホルダ的な感じ
update_option[:expression_attribute_values] = {":increment": 1}
# どういう変更を加えるのかを指定
# この書き方で seq 属性を 1 インクリメント を表す
update_option[:update_expression] = "ADD seq :increment"
# 更新する
# res には、更新後の情報が含まれる
res = table.update_item(update_option)
これで res
には更新後の情報が入ってきます。
return_values
に "UPDATED_NEW"
を指定したので、更新された属性の情報だけが res[:attributes]
の中に入ってきます。
res:
{:attributes=>{"seq"=>0.1e1}, :consumed_capacity=>nil, :item_collection_metrics=>nil}
ここでほしいのは seq
の値ですので res[:attributes]["seq"]
で取得可能です。
このあたり、Hash の key が symbol だったり string だったりでややこしい点に注意。間違えると nil が取れちゃってハマる可能性があります。
ちなみにここで期待していたのは 1
だったのですが、 0.1e1
という謎の値が取れました。
これは BigDecimal
です。
見かけはアレですが、値自体は 1 を表しているのでこのままで良いでしょう。
どうやら DynamoDB の数値型を利用すると Ruby のコード上では BigDecimal として扱うことになるようですね。
user テーブルにアイテムを追加する
- フォームから送信したデータ
- 最新の連番の id
これらの情報が準備できたので、いよいよ user
テーブルにアイテムを追加します。
RDB でいうところのレコード追加に相当します。
put_item メソッドを使えば可能です。
先ほど取得した id と、フォームから送信したデータ(を整形した data
) をガッチャンコして put_item
でポイします。
table.put_item({item: data})
ここまで問題なくできていれば、これにて DynamoDB の user
テーブルにアイテムが追加されているはずです。
備考:put_item
でエラーになると id が連番ではなくなる
今回のアトミックカウンタの手法を用いる場合、user
テーブルにアイテムを追加する流れは、以下の2つの連続した処理で行っています。
-
sequence
テーブルのseq
を更新してその値を返す -
user
テーブルの新しい項目の id としてその値を利用する
2、つまり user
の put_item
でエラーになった場合は、1 の更新だけが行われた状態になります。
次の施行時には再度 1 が実行されるため、ここで id の連続性が失われることがわかります。
基本的には連番でなくなっても問題はないはず(重複しない値が欲しいだけなので)ですが、それが問題になるような場合はそれを保証するための方法を取る必要があります(1, 2 の処理をトランザクション操作するイメージ)。
→条件付きの書き込み
コンソールから追加されたアイテムを確認
コンソール上から簡単に確認してみます。
テーブル > user > テーブルアイテムの探索
画像の場所にアイテムが追加されていることが確認できました。
ちなみに sequence
テーブルも確認してみると、tablename = user
のアイテムの seq
属性も 1
になっていることが確認できます。
この後続けてフォームから何度かリクエストを送信すると、その分だけ seq
が増えて、user
テーブルに登録されるデータの id もうまく連番が作成されていくと思います。
今後
- DynamoDB の API をもうちょい触りたい
- 次回はデータ取得とか
- コンソールからではなく、SAM 利用してデプロイしたい