2
0

More than 1 year has passed since last update.

Ruby で Lambda 入門 (1)

Last updated at Posted at 2022-07-26

概要

  • Lambda と DynamoDB の勉強がしたい
  • API Gateway を使わなくとも Lambda に HTTP エンドポイントを作成できるようになったらしいので使ってみたい

以上の状況だったのでちょっとそのあたりを触ってみました。
なおタイトルにもある通り、ランタイムは Ruby 2.7 を選択しています。
※選択肢の中では自分が Ruby に慣れているというだけです

やったこと

  1. コンソールから Lambda 関数を作成
  2. コンソールから DynamoDB のテーブルを作成
  3. ローカルの apache にファイルを置いて、そこから Lambda にリクエスト
  4. 受け取ったデータを使って DynamoDB のアイテムを作成
  5. コンソールから追加されたアイテムを確認

手順詳細

コンソールから Lambda 関数を作成


今回は簡単に認証なしとします。
また、CORS をよしなにやってくれる設定も ON にしておきます。

「詳細設定」の 「関数 URL を有効化」 というのは比較的最近できたオプションです。

少し前までは、今回作成しようとしているように、関数に HTTP(S) でリクエストを送ろうと思うと API Gateway と組み合わせる必要がありました。

それがこのオプションを ON にするだけで関数にエンドポイントを割り当てることができるようになったので、随分お手軽に試せるようになりました。

実行ロールの調整

設定 > アクセス権限 > 実行ロール
を確認します。

勝手にこの関数用のロールを作ってくれています。
このロールにポリシーを追加します。
追加するのは、DynamoDB にアクセス・書き込みするためのロールです。

  • AmazonDynamoDBFullAccess
  • AWSLambdaDynamoDBExecutionRole

この 2ポリシーを足しておけばOKでしょう。

コンソールから 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 をあえて残しています。

ログを確認

ログの確認には
モニタリング > CloudWatch のログを表示
で飛ぶと早いでしょう。

ruby なら puts で出力した内容はここのログで確認できます。
手軽にデバッグするのに便利だと思います。

なお、ログストリームはチョイチョイ変わるので、ログストリームを開いて更新しているのに新しいログが表示されない、というときはいったんログストリーム一覧に戻ってみると、新しいログストリームが作られていたりします。

おそらく前回の実行から少し時間が経過して、新しく Lambda 実行環境が作り直されるタイミングでログストリームが変わるのだろうと推測します。

送信パラメータを整形する

先ほどのフォームから以上のような値を送信してみます。

Lambda 側では、フォームから飛んできた値を受け取り、それを body に入れます。
このとき、リクエストの情報は lambda_handler の仮引数 event に入ってきます。
eventHash で、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つの連続した処理で行っています。

  1. sequence テーブルの seq を更新してその値を返す
  2. user テーブルの新しい項目の id としてその値を利用する

2、つまり userput_item でエラーになった場合は、1 の更新だけが行われた状態になります。

次の施行時には再度 1 が実行されるため、ここで id の連続性が失われることがわかります。

基本的には連番でなくなっても問題はないはず(重複しない値が欲しいだけなので)ですが、それが問題になるような場合はそれを保証するための方法を取る必要があります(1, 2 の処理をトランザクション操作するイメージ)。
条件付きの書き込み

コンソールから追加されたアイテムを確認

コンソール上から簡単に確認してみます。

テーブル > user > テーブルアイテムの探索

画像の場所にアイテムが追加されていることが確認できました。

ちなみに sequence テーブルも確認してみると、tablename = user のアイテムの seq 属性も 1 になっていることが確認できます。

この後続けてフォームから何度かリクエストを送信すると、その分だけ seq が増えて、user テーブルに登録されるデータの id もうまく連番が作成されていくと思います。

今後

  • DynamoDB の API をもうちょい触りたい
  • 次回はデータ取得とか
  • コンソールからではなく、SAM 利用してデプロイしたい

参考文献

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0