AWSを使ってサーバーレスにRestAPIを作ってみた。23AWSを使ってサーバーレスにRestAPIを作ってみた。GoAWSlambdaREST-APIサーバーレスDeNA20新卒アドベントカレンダー23日目です。サーバーレスサーバーレスアーキテクチャーがここ最近だいぶ浸透してきたと思います。面倒な(そしてある意味楽しい)サーバーの保守から解放されて、よりアプリケーションそのものに集中できるようになったのは開発者として嬉しいポイントです。他のマネージドサービスとの連携も容易で低いコストでウェブアプリケーションが作れるようになりました。APIを作ってみました。ちなみにRestではなくGraphQLを使うのも考えましたが、学習が間に合わずに断念...ちなみに元々掲示板のサーバーサイドだけを作って、各種APIを公開して各々のユーザーがクライアントを作ると言うコンセプトの下にAPIを作っていました。やっていくうちに色々と知見がたまったので公開すると言うのがこの記事の主旨になります。作成するAPIの構成は以下のようになります。本当はS3を使って画像のアップロードもできるようにしたかったけど間に合いませんでした。Lambdaはクラウド上でプログラムを走らせるためのサービスです。リクエストごとに料金がかかる仕組みですが、無料分が非常に大きく並大抵のリクエスト程度ならタダで使えます。Lambdaで動いていました。(C++のプログラムをスタティックビルドして盤面情報をコマンドライン引数としてPythonのsubprocessで動かすと言う力技)今回はバックエンドのロジックを全てLambdaを使用して実装しました。C#が使えるようですが、今回は自身の練習を兼ねてGoで実装しました。DBデータベースにはDynamoDBを使用しています。普段MySQLばかりでNoSQLのデータベースを使ってみたいと言う理由で使いましたが、スケールが容易だったり料金が安いなどのメリットが多い一方、なかなかに癖が強く慣れるのに時間がかかりました。DynamoDBではパーティションキー、もしくはパーティションキーとレンジキーのセットがプライマリキー(=ユニーク)となるのですが、複数のデータを持ってくるような場合はただ一つのパーティションキーとレンジキーによる範囲指定が必要になります。この時、レンジキーによる範囲指定を行うと出力されるデータは自動的に降順もしくは昇順でソートされます。複数のデータを取得する際は同じパーティションキーを持つデータに対して、レンジキーで範囲を指定するが、パーティションキーとレンジキーはユニークになっている必要があることになります。これらの制約のせいでテーブル設計がなかなか難しく、作り直しを何回か行う羽目になりました。今回の掲示板APIによるデータベースの操作をまとめると以下のようになります。スレッド一覧の取得(スレッドの一覧は更新された時間順に100件までとする。)スレッドの投稿あるスレッドのレスポンスの取得(レスポンスの一覧は投稿順とする。)レスポンスの投稿(スレッドの最終更新時間を更新する。)従ってスレッドは全て同じパーティションキー、更新時間をレンジキーとしたいのですが、パーティションキーとレンジキーのセットのユニーク性を考えると、パーティションキーが同じであればレンジキーによってのみユニーク性が担保される必要性が出てきます。更新時間がユニークなのはあまり綺麗な実装とは言えない気がするので別のテーブルを考えます。ちなみにスキャンをすることで全データの摘出ができ、それらについてフィルターをかける方法もありますが、テーブルの要素が多くなることはあっても減ることはないのでできる限り避けたいところです。データ転送量で課金なことを考えるとコスト的にもなしです。DynamoDBでは基本的にパーティションキーとレンジキーの組み合わせでデータを取得しますが、ローカルセカンダリインデックス(LSI)やグローバルセカンダリインデックス(GSI)を追加することで、同じテーブルについて異なるキーで検索をかけることができます。LSIでは同じパーティションキーについてレンジキー相当のキーの追加、GSIではパーティションキー相当のキーとレンジキー相当のキーの追加が可能ですが、内部実装的にはテーブルを複製しているらしいのでDynamoDBのコストとの相談が必要です。ここでキー相当と表現したのは、LSIについては追加したレンジキー相当のキーと既存のパーティションキーとの組み合わせがユニークでなくても良く、GSIについては追加したキー二つがユニークでなくても良いためです。ちなみにテーブルへのLSIの追加はテーブル作成時にしかできないので注意してください。今回の要件を考えると更新時間をLSIに追加すれば更新時間によるスレッド一覧の取得が可能になります。するとレンジキーは実質スレッドをユニークにするためだけに存在するので、邪道な気もしますがこれをRDBにおけるプライマリキーと見なすことにしました。Incrementのようなオプションは存在しない(できないことはないが別途インデックスを管理するテーブルが必要になる。)のでUUIDを使用し、このUUIDをプライマリキーとしてスレッドに対するレスポンステーブルを作成しました。threadsテーブルpartをパーティションキー、idをレンジキー、updated_atをLSIとしています。パーティションキーであるpartが全てのスレッドについて0で、毎回のリクエストでパーティションキーに0を入れるのは設計としてどうなんだと言う気もしますが実際にDynamoDBのドキュメントにディスカッションフォーラムにおけるテーブル設計の例があります。https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/SampleData.CreateTables.htmlこのページのthreadテーブルを見てみるとプライマリキーがフォーラム名となっています。プライマリキーをフォーラム名とかカテゴリ名として掲示板を分けていくとすれば、今回の例ではフォーラムが一つしかない場合と考えられるので、そこまで変な実装でもないのかなと言う気がします。2...と増やしていきパーティションキーごとに掲示板を分けていけば、負荷的にも問題ないと思われます。responsesテーブルresponsesカラムにはDynamoDB用に変換されたjsonオブジェクトが入っています。クエリでデータを取り出す際に取り出すカラムを指定できるので、今回の場合はスレッドのデータにそのままレスポンスのデータをくっつけるのもアリだと思いますが、上記のAWSのサンプルに則って分けました。Goの実装LambdaでGoを使いdynamodbにアクセスするにはSDKが必要です。必要なものだけをインストールしてもいいのですが、面倒なので下記コマンドでまとめてAWS用のGoSDKをインストールしました。github.com/aws/aws-sdk-go/...LambdaでGoを使うにあたって必要なパッケージはaws-lambda-go/eventsとaws-lambda-go/lambdaになります。前者がLambdaへの各種データの入出力を扱うパッケージ、後者がlambdaを実行するためのパッケージになっているようです。GoでLambdaを動かす解説サイトがあまりないので割とドキュメントと睨めっこが多くなるかもしれません。https://godoc.org/github.com/aws/aws-lambda-go/lambdahttps://godoc.org/github.com/aws/aws-lambda-go/eventsGoのmain関数でハンドラー関数をlambdaで実行すると言う命令を出します。下のような定型文を書けばあとはLambdaがよしなにしてくれるので、ハンドラー関数の方で受け取ったデータを処理して返すと言うのが一連の流れです。Gatewayから渡されますが、この時データがどう渡されるかによって処理の書き方が異なります。Gatewayを通す時にURL文字列を処理する必要があります。Gatewayからは以下のような構造体のデータがハンドラ関数に送られます。前者であればPathParameters後者であればQueryStringParametersにそれぞれmapで入っています。Gatewayの方でマッピングを行う必要です。今回はどちらでも受け取れるようにしました。postでデータを渡す場合、jsonで渡していればそのままハンドラー関数の引数にjsonが入ってくるので、適当な構造体を用意しておけばそのままデータが使えます。`json:"content"`}ハンドラー関数では以下のように中身が参照できます。Gatewayがそのままデータを渡してくれるので簡潔に受け取ることができます。events.APIGatewayProxyRequest型で受け取ってもBodyにデータが入っていると思われますが、string型で入ってくるので別途構造体に変換する必要があります。Gatewayを介していないためエラーが発生します。Gateway用に書き換える手間が発生するので、getでもjsonでデータを渡すようにする方が楽かもしれません。Gatewayを通してデータを送るにはevents.APIGatewayProxyResponse型のデータを返す必要があります。Gatewayを通してデータを返却するためには必須です。StatusCodeはhttpパッケージのものを利用すれば大丈夫です。HeadersはCORSを行うために'Access-Control-Allow-Originを適切に設定する必要があります。以下が実際に実装したコードです。githubはこちらGoでdynamoDBを操作していますが、これについて詳しく解説すると記事が丸々一本書けそうなので割愛します。一応はAWSのドキュメントを読めばできますが、ハマるポイントも多く結構苦戦しました。質問があればコメント欄に書いてもらえればできる限り調査します。ハンドラー関数、スレッド一覧は何も受け取らない。データベースから取得したスレッド一覧のみ返却する。上で定義したクエリを使って、データを取り出す。それがリストになっている(=ListOfMaps)が、この取り出したデータはDynamoDB用のフォーマットになっている。UnmarshalListOfMapsでスレッド構造体のスライスに入れられるようにデータを変換する。取得したデータをjsonで返却する。今回はコンセプトのために*にしているが、実際の場面では適切に設定してください。データをデータベースに格納するための関数、タイムアウト処理用のcontextを受け取る。ハンドラー関数、データをjsonで受け取るため、Request型のrequestを引数にもつ。取得するレスのIDをクエリパラメータもしくパスパラメータで受け取るため、events.APIGatewayProxyRequest型の引数を持つ。パスパラメータかクエリでスレのidを取得する。取り出した中身をjsonに変換する。dynamoDBでのリストへの追加はlist_append関数を使う。Lambdaでプログラムを実行するためには、実行したいファイルをzip形式でアップロード、もしくはウェブ上のエディタで直接コードを編集する必要があります。Goに関しては前者しかできないようなので、これらのプログラムをまとめてコンパイルしてzipにするMakefileもついでに作成しました。Lambdaの関数に登録すればLambda側の準備は完了です。zipは関数コードのパネルからアップロードできます。ハンドラには実行するファイル名を入力します。ここではget_threads.goをget_threadsにコンパイルしてget_threads.zipにしたものをアップロードしました。Lambdaの特定の関数を発火させます。Lambdaに渡すように設定する必要があります。補足として、外部からのリクエストを受け付ける際はURLのオリジンが異なるためCORSを有効化する必要があります。APIを作ると言う名目(=不特定多数からのアクセスを想定)のため制限はかけていませんが、実際のアプリケーションに組み込む際は適宜設定することをお勧めします。ちなみに今回のAPIは不特定多数からのアクセスを想定してはいますが、悪戯されても嫌なのでスロットリングを有効化してレートとバーストに制限をかけています。Gatewayについての説明はたくさんあるので、省略気味に紹介します。ルート直下に試しにcreate-threadのAPIを作成します。これでOPTIONSメソッドが自動的に作られます。次にメソッド作成ですが、ここで使用するlambdaの関数を選択します。プロキシ統合の使用にチェックを入れるかどうかですが、これはevents.APIGatewayProxyRequestでデータを受け渡ししたい場合はチェックを入れてください。Gatewayを通してパスパラメータを渡したいのでチェックを入れています。get-threadsは何もデータを渡さないのでどちらでも問題ありません。いずれにせよ全てのデータをevents.APIGatewayProxyRequestで受け取るならば全部チェックで問題ありません。Gatewayの方で流してくれるみたいです。各種設定を終えると以下のようになります。CORSを許可する設定になっていれば各種GETやPOSTなどのメソッドの下にOPTIONSメソッドが追加されます。パスパラメータの設定ができていれば{id}のようにマッピングが表示されます。GatewayではswaggerやOpenAPI3用のjsonやyamlをエクスポートできます。editorなどでインポートするとOpenAPI3でAPIの一覧が見られるようになります。実際にOpenAPI3上でメソッドの実行もでき、レスポンスが返ってくるのが確認できます。APIが完成しました。今回はSDKを使ってGoでdynamoDBを操作しましたが、dynamoDBの癖の強さとGoの型ら辺が作用してかなりしんどいです。良い感じに面倒なところやってくれるライブラリがあるみたいなのでそっちを使った方がいいと思います。https://github.com/guregu/dynamo終わりに以上でバックエンドの方が完成しました。Hooksが使いたいのもあってReactでクライアントの方も作ろうとしたのですが間に合いませんでした。(某アプリが悪い)そのうちクライアント側も作って記事公開します。staticを使っています。当初はgatsby.jsを使おうとしたのですがどうもtypescriptで書くには早いかな感と、そもそもgraphQLを使ってないので止めました。staticにしました。Hooksを使ったコンポーネントの作成やらなんやらはうまくいっているので、あとはcssがうまく書けたら公開になると思います。requestStockLike20@coil_msp123Follow株式会社ディー・エヌ・エー1999年に創業した株式会社ディー・エヌ・エーはゲームを中心に、オートモーティブ、ヘルスケア、スポーツなどの事業分野にも進出し、インターネットや人工知能(AI)を活用した新しい挑戦を続けています。upLoginユーザーは見つかりませんでした。