はじめに
こんにちは~、BFT名古屋支店の猫です。季節の変わり目のせいか、毎日眠いです。
そんな眠たい時でもできるのが、そう、検証!
ということで、"RESTful API の構築" をやってみました。
使用したのは「Amazon API Gateway 」「AWS Lambda」「Amazon RDS」の3つです。
これらを選択した理由は、業務で扱ったことがあり取り組みやすかった&ざっと調べた感じ情報が色々落ちてそうと思ったためです。ありがとうインターネット!
今回はLambdaの実装部分を中心にご紹介します。
「API Gateway とLambdaの連携が上手くいかない」「LambdaとRDSの連携が上手くいかない」といった方の参考になればと思います。
この記事を読むのにあるといい知識
ここでは作ったAPI の構成や機能についてご紹介します。
あまり興味ない方は飛ばしていただいてOKです。
今回は、「料理のレシピを登録・更新・削除・閲覧できるAPI 」を作りました。理由は特にありません。
構成イメージ
構成
誰が何をしているのかを簡単にご説明します。
Amazon API Gateway
ユーザからのリクエス トを受け取って、中間処理を実行し、ユーザにレスポンスを返します。
中間処理は、リソース(ex:/recipes, /recipes/{id})やメソッド(ex:GET, POST, DELETE, …)ごとに定義することができます。今回の構成ではLambdaを呼び出します。
実際の画面
AWS Lambda
API Gateway から呼び出されると、所定の処理を実行し、結果をAPI Gateway に返します。
今回作成したLambdaでは「リクエス トに応じてSQL 文を生成し、RDSに対してクエリを実行する」という処理を実行します。
Amazon RDS
登録されているデータ(=料理のレシピ)を持っています。
Lambdaから投げられたSQL 文を実行し、その結果をLambdaに返します。
機能
どんな機能があるのか簡単にご説明します。
データベース処理として一般的なCRUD (Create, Read, Update, Delete)の機能を実装しています。
Lambdaの実装
ではさっそく実装したコードを紹介していきます。
実装のポイントは以下の二点です。
API Gateway から連携されたリクエス トの、bodyやパスパラメータを抽出していること
pymysqlというモジュールを使って、RDSへの接続やクエリ実行処理を実装していること
これらについて、詳しくははまた別の記事にまとめたいと思いますので
今日のところは(ふ~ん、こんな書き方するんだ~)くらいに思っていただければと思います。
使用環境
登録機能
レシピをDBに登録する機能です。
リクエス トbodyで指定されたレシピをDBに登録し、登録できた場合はそのレコードを、登録できなかった(リクエス トの形式がおかしかったなど)場合はエラー文を返します。
大まかな流れは以下の通りです。
1. DB接続
2. レコード登録
3. レコード取得(登録したレコードを取得)
4. レスポンス生成
import json
import sys
import os
import pymysql
DB_USER = "admin"
DB_PASSWORD = "my_password"
DB_HOST = "recipe-database.***********.ap-northeast-1.rds.amazonaws.com"
DB_NAME = "recipe_db"
try :
conn = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, db=DB_NAME)
except Exception as e:
print ("Fail connecting to RDS mysql instance" )
print (e)
sys.exit()
print ("Success connecting to RDS mysql instance" )
def lambda_handler (event, context):
req = event['body' ]
try :
with conn.cursor() as cur:
SQL = """
INSERT INTO recipes (title, making_time, serves, ingredients, cost, created_at, updated_at)
VALUES ( %s, %s, %s, %s, %s, now(), now() );
"""
cur.execute(SQL,(req['title' ], req['making_time' ], req['serves' ], req['ingredients' ], req['cost' ],))
conn.commit()
cur.execute("select * from recipes order by id DESC LIMIT 1;" )
cur_res = cur.fetchall()
data_list = list ()
for (id , title, making_time, serves, ingredients, cost, created_at, updated_at) in cur_res:
data = dict ()
data = {'id' : id ,
'title' : title,
'making_time' : making_time,
'serves' : serves,
'ingredients' : ingredients,
'cost' : cost,
'created_at' : str (created_at),
'updated_at' : str (updated_at)}
data_list.append(data)
cur.close()
res = dict ()
res = {"message" : "Recipe successfully created!" ,
"recipe" : data_list}
return res
except Exception as e:
cur.close()
err_res = dict ()
err_res = {"message" : "Recipe creation failed!" ,
"required" : "title, making_time, serves, ingredients, cost" }
return err_res
更新機能
DBに登録されているレシピを更新する機能です。
リクエス トのパスパラメータで指定されたidのレコードに対して、 リクエス トbodyで指定された項目の値を更新します。
更新できた場合はそのレコードを、更新できなかった(指定されたidのレコードが存在しないなど)場合はエラー文を返します。
大まかな流れは以下の通りです。
1. DB接続
2. 存在確認(指定されたidのレコードがDBに存在するか確認)
3. レコード更新
4. レコード取得(更新したレコードを取得)
5. レスポンス生成
import json
import sys
import os
import pymysql
DB_USER = "admin"
DB_PASSWORD = "my_password"
DB_HOST = "recipe-database.***********.ap-northeast-1.rds.amazonaws.com"
DB_NAME = "recipe_db"
try :
conn = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, db=DB_NAME)
except Exception as e:
print ("Fail connecting to RDS mysql instance" )
print (e)
sys.exit()
print ("Success connecting to RDS mysql instance" )
def lambda_handler (event, context):
req = json.loads(event['body' ])
try :
with conn.cursor() as cur:
req_id = int (event['pathParameters' ]['id' ])
SQL = "SELECT * FROM recipes WHERE id = %s"
cur.execute(SQL, (req_id,))
cur_res = cur.fetchall()
if len (cur_res) == 0 :
raise Exception
for key in req:
SQL = ""
if key == 'title' : SQL = "UPDATE recipes SET title = %s WHERE id = %s;"
if key == 'making_time' : SQL = "UPDATE recipes SET making_time = %s WHERE id = %s;"
if key == 'serves' : SQL = "UPDATE recipes SET serves = %s WHERE id = %s;"
if key == 'ingredients' : SQL = "UPDATE recipes SET ingredients = %s WHERE id = %s;"
if key == 'cost' : SQL = "UPDATE recipes SET cost = %s WHERE id = %s;"
if len (SQL) > 0 :
cur.execute(SQL,(req[key], req_id))
SQL = "UPDATE recipes SET updated_at = now() WHERE id = %s;"
cur.execute(SQL,(req_id,))
conn.commit()
SQL = "SELECT * FROM recipes WHERE id = %s"
cur.execute(SQL, (req_id,))
cur_res = cur.fetchall()
data_list = list ()
for (id , title, making_time, serves, ingredients, cost, created_at, updated_at) in cur_res:
data = dict ()
data = {'id' : id ,
'title' : title,
'making_time' : making_time,
'serves' : serves,
'ingredients' : ingredients,
'cost' : cost,
'created_at' : str (created_at),
'updated_at' : str (updated_at)}
data_list.append(data)
cur.close()
res = dict ()
res = {"message" : "Recipe successfully updated!" ,
"recipe" : data_list}
responce = {
"isBase64Encoded" : True ,
"statusCode" : 200 ,
"headers" : {"MyHeader" : "MyHeaderVal" },
"body" : json.dumps(res)
}
print (responce)
return responce
except Exception as e:
cur.close()
responce = {
"isBase64Encoded" : True ,
"statusCode" : 500 ,
"headers" : {"MyHeader" : "MyHeaderVal" },
"body" : "No Recipe found"
}
return responce
削除機能
DBに登録されているレシピを削除する機能です。
リクエス トのパスパラメータで指定されたidのレコードをデータベースから削除します。
削除できた場合はその旨を伝えるメッセージを、削除できなかった(指定されたidのレコードが存在しないなど)場合はエラー文を返します。
大まかな流れは以下の通りです。
1. DB接続
2. 存在確認(指定されたidのレコードがDBに存在するか確認)
3. レコード削除
4. レスポンス生成
import json
import sys
import os
import pymysql
DB_USER = "admin"
DB_PASSWORD = "my_password"
DB_HOST = "recipe-database.***********.ap-northeast-1.rds.amazonaws.com"
DB_NAME = "recipe_db"
try :
conn = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, db=DB_NAME)
except Exception as e:
print ("Fail connecting to RDS mysql instance" )
print (e)
sys.exit()
print ("Success connecting to RDS mysql instance" )
def lambda_handler (event, context):
try :
with conn.cursor() as cur:
req_id = int (event['pathParameters' ]['id' ])
print (req_id)
SQL = "SELECT * FROM recipes WHERE id = %s"
cur.execute(SQL, (req_id,))
cur_res = cur.fetchall()
if len (cur_res) == 0 :
raise Exception
SQL = "DELETE FROM recipes WHERE id = %s;"
cur.execute(SQL, (req_id,))
conn.commit()
responce = {
"isBase64Encoded" : True ,
"statusCode" : 200 ,
"headers" : {"MyHeader" : "MyHeaderVal" },
"body" : '{"message": "Recipe successfully removed!"}'
}
print (responce)
return responce
except Exception as e:
cur.close()
responce = {
"isBase64Encoded" : True ,
"statusCode" : 500 ,
"headers" : {"MyHeader" : "MyHeaderVal" },
"body" : '{"message": "No Recipe found"}'
}
return responce
閲覧機能(全レシピ)
DBに登録されている全レコードを取得する機能です。
取得できた場合はそのレコードを、取得できなかった(レコードが存在しないなど)場合はエラー文を返します。
大まかな流れは以下の通りです。
1. DB接続
2. レコード取得(全レコードを取得)
3. レスポンス生成
import json
import sys
import os
import pymysql
DB_USER = "admin"
DB_PASSWORD = "my_password"
DB_HOST = "recipe-database.***********.ap-northeast-1.rds.amazonaws.com"
DB_NAME = "recipe_db"
try :
conn = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, db=DB_NAME)
except Exception as e:
print ("Fail connecting to RDS mysql instance" )
print (e)
sys.exit()
print ("Success connecting to RDS mysql instance" )
def lambda_handler (event, context):
try :
with conn.cursor() as cur:
SQL = "SELECT * FROM recipes;"
cur.execute(SQL)
cur_res = cur.fetchall()
if len (cur_res) == 0 :
raise Exception
data_list = list ()
for (id , title, making_time, serves, ingredients, cost, created_at, updated_at) in cur_res:
data = dict ()
data = {'id' : id ,
'title' : title,
'making_time' : making_time,
'serves' : serves,
'ingredients' : ingredients,
'cost' : cost,
'created_at' : str (created_at),
'updated_at' : str (updated_at)}
data_list.append(data)
cur.close()
res = list ()
res = {"recipes" : data_list}
return res
except Exception as e:
cur.close()
res = list ()
res = {"message" : "No Recipe found" }
return res
閲覧機能(id指定)
先ほどご紹介した「閲覧機能(全レシピ)」のid指定するバージョンです。
リクエス トのパスパラメータで指定されたidのレコードのみを取得します。
取得できた場合はそのレコードを、取得できなかった(指定されたidのレコードが存在しないなど)場合はエラー文を返します。
大まかな流れは以下の通りです。
1. DB接続
2. レコード取得(指定されたidのレコードのみを取得)
3. レスポンス作成
import json
import sys
import os
import pymysql
DB_USER = "admin"
DB_PASSWORD = "my_password"
DB_HOST = "recipe-database.***********.ap-northeast-1.rds.amazonaws.com"
DB_NAME = "recipe_db"
try :
conn = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, db=DB_NAME)
except Exception as e:
print ("Fail connecting to RDS mysql instance" )
print (e)
sys.exit()
print ("Success connecting to RDS mysql instance" )
def lambda_handler (event, context):
try :
with conn.cursor() as cur:
req_id = int (event['pathParameters' ]['id' ])
SQL = "SELECT * FROM recipes WHERE id = %s"
cur.execute(SQL, (req_id,))
cur_res = cur.fetchall()
if len (cur_res) == 0 :
raise Exception
data_list = list ()
for (id , title, making_time, serves, ingredients, cost, created_at, updated_at) in cur_res:
data = dict ()
data = {'id' : id ,
'title' : title,
'making_time' : making_time,
'serves' : serves,
'ingredients' : ingredients,
'cost' : cost,
'created_at' : str (created_at),
'updated_at' : str (updated_at)}
data_list.append(data)
cur.close()
res = dict ()
res = {"message" : "Recipe details by id" ,
"recipe" : data_list}
responce = {
"isBase64Encoded" : True ,
"statusCode" : 200 ,
"headers" : {"MyHeader" : "MyHeaderVal" },
"body" : json.dumps(res)
}
print (responce)
return responce
except Exception as e:
responce = {
"isBase64Encoded" : True ,
"statusCode" : 500 ,
"headers" : {"MyHeader" : "MyHeaderVal" },
"body" : '{"message": "No Recipe found"}'
}
print (responce)
return (responce)
使ってみた
できあがったAPI を使ってみました!
今回は画面を作っていないので、curl コマンドを使用してAPI を叩きます。
①まずはデータベースが空っぽであることを確認します。
[ myuser@myhost ~] $ curl -X GET https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/my-recipe-prod/recipes | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 15 100 15 0 0 269 0 --:--:-- --:--:-- --:--:-- 272
{
" recipes " : []
}
[ myuser@myhost ~] $
データが登録されていないことが確認できました。
②次にデータを登録してみます。
[ myuser@myhost ~] $ curl -X POST -H " Content-Type: application/json " -d ' {"title":"カレー", "making_time":"40分", "serves":"4人", "ingredients":"肉,玉ねぎ,人
参,スパイス,水", "cost":1000 ' https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/my-recipe-prod/recipes | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 451 100 322 100 129 767 307 --:--:-- --:--:-- --:--:-- 1076
{
" message " : " Recipe successfully created! " ,
" recipe " : [
{
" id " : 1 ,
" title " : " カレー " ,
" making_time " : " 40分 " ,
" serves " : " 4人 " ,
" ingredients " : " 肉,玉ねぎ,人参,スパイス,水 " ,
" cost " : 1000 ,
" created_at " : " 2022-10-12 05:00:21 " ,
" updated_at " : " 2022-10-12 05:00:21 "
}
]
}
[ myuser@myhost ~] $
登録が成功したことを示すメッセージが出力されました!
このあと同様に2つほど登録しました。
③登録したデータを見てみます。
こちらは全レシピを閲覧するリクエス トとその結果です。
データを3つ登録したので、3つのデータが出力されれば成功です。
[ myuser@myhost ~] $ curl -X GET https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/my-recipe-prod/recipes | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 772 100 772 0 0 15858 0 --:--:-- --:--:-- --:--:-- 16083
{
" recipes " : [
{
" id " : 1 ,
" title " : " カレー " ,
" making_time " : " 40分 " ,
" serves " : " 4人 " ,
" ingredients " : " 肉,玉ねぎ,人参,スパイス,水 " ,
" cost " : 1000 ,
" created_at " : " 2022-10-12 05:00:21 " ,
" updated_at " : " 2022-10-12 05:00:21 "
} ,
{
" id " : 2 ,
" title " : " スープ " ,
" making_time " : " 15分 " ,
" serves " : " 2人 " ,
" ingredients " : " トマト,ツナ,コンソメ,水 " ,
" cost " : 300 ,
" created_at " : " 2022-10-12 05:22:57 " ,
" updated_at " : " 2022-10-12 05:22:57 "
} ,
{
" id " : 3 ,
" title " : " だし巻き卵 " ,
" making_time " : " 5分 " ,
" serves " : " 2人 " ,
" ingredients " : " 卵,だし,水 " ,
" cost " : 150 ,
" created_at " : " 2022-10-12 05:24:36 " ,
" updated_at " : " 2022-10-12 05:24:36 "
}
]
}
[ myuser@myhost ~] $
こちらはidを指定してレシピを閲覧するリクエス トとその結果です。
指定したレシピのみが出力されれば成功です。
[ myuser@myhost ~] $ curl -X GET https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/my-recipe-prod/recipes/1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 314 100 314 0 0 758 0 --:--:-- --:--:-- --:--:-- 758
{
" message " : " Recipe details by id " ,
" recipe " : [
{
" id " : 1 ,
" title " : " カレー " ,
" making_time " : " 40分 " ,
" serves " : " 4人 " ,
" ingredients " : " 肉,玉ねぎ,人参,スパイス,水 " ,
" cost " : 1000 ,
" created_at " : " 2022-10-12 05:00:21 " ,
" updated_at " : " 2022-10-12 05:00:21 "
}
]
}
[ myuser@myhost ~] $
どちらも想定通りに動くことが確認できました!
④続いて、レシピを更新してみます。
最初に登録したレシピのタイトルを「カレー」から「最強カレー」にしてみます。
[ myuser@myhost ~] $ curl -X PATCH -H " Content-Type: application/json " -d ' {"title":"最強カレー"} ' https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/my-recipe-prod/recipes/1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 413 100 386 100 27 1047 73 --:--:-- --:--:-- --:--:-- 1119
{
" message " : " Recipe successfully updated! " ,
" recipe " : [
{
" id " : 1 ,
" title " : " 最強カレー " ,
" making_time " : " 40分 " ,
" serves " : " 4人 " ,
" ingredients " : " 肉,玉ねぎ,人参,スパイス,水 " ,
" cost " : 1000 ,
" created_at " : " 2022-10-12 05:00:21 " ,
" updated_at " : " 2022-10-12 06:14:48 "
}
]
}
[ myuser@myhost ~] $
タイトルと更新日時の値が変わっていることが確認できました。
⑤最後に、レシピを削除してみます。
先ほど更新したカレーのレシピのidを指定してリクエス トを投げます。
[ myuser@myhost ~] $ curl -X DELETE https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/my-recipe-prod/recipes/1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 43 100 43 0 0 103 0 --:--:-- --:--:-- --:--:-- 103
{
" message " : " Recipe successfully removed! "
}
[ myuser@myhost ~] $
削除が成功したことを示すメッセージが出力されました!
再び全レシピを閲覧するリクエス トを投げてみると・・・
[ myuser@myhost ~] $ curl -X GET https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/my-recipe-prod/recipes/ | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 529 100 529 0 0 1426 0 --:--:-- --:--:-- --:--:-- 1429
{
" recipes " : [
{
" id " : 2 ,
" title " : " スープ " ,
" making_time " : " 15分 " ,
" serves " : " 2人 " ,
" ingredients " : " トマト,ツナ,コンソメ,水 " ,
" cost " : 300 ,
" created_at " : " 2022-10-12 05:22:57 " ,
" updated_at " : " 2022-10-12 05:22:57 "
} ,
{
" id " : 3 ,
" title " : " だし巻き卵 " ,
" making_time " : " 5分 " ,
" serves " : " 2人 " ,
" ingredients " : " 卵,だし,水 " ,
" cost " : 150 ,
" created_at " : " 2022-10-12 05:24:36 " ,
" updated_at " : " 2022-10-12 05:24:36 "
}
]
}
[ myuser@myhost ~] $
カレーのレシピが削除されていることが確認できました!
おわりに
今回はAWS 上でRESTful API を作ってみました。
作業前は3時間くらいで作れるかな~と思っていたのですが、実際には8時間くらいかかってしまいました。
この差分5時間のほとんどは、API Gateway - Lambda間のデータのやり取りを実装するのに費やしました。
API Gateway - Lambdaのやり取りでなんでそんなに苦労したの?という話は次回の記事で書きたいと思いますので、ぜひお楽しみに!
ここまで読んでいただきありがとうございました^^