この広告は、90日以上更新していないブログに表示しています。
Lambda
関数を用いたサーバーレス開発をもっと知っておこうと思って読んだ本の感想です。2018年4月刊行、サーバーレスの主要サービス解説にコードはPython
、のみならずフロントはVue.js
を使った本格開発まで、実践的な内容が詰まった本です。
作者は現Amazon Web Services Japan所属のKeisuke69こと西谷圭介さん。Twitterでもよくお見掛けします。(@Keisuke69)
以下、自分の学びのために一部写経ライクなイメージのコードも一緒に載せています。
まずは基本、サーバーレスの特長を捉えていく節。
Lambda
用の基盤の中で何もなくても毎日サーバーは新しく保たれている。SSHでログインしたりもそもそもできないので侵入不可。EC2
だと1秒あたり。ロギングなどもCloudWatch
と自動で連携するので、監視用の別サーバーもいらない。Lambda
を中心に小さな処理を繋げて要件を実現できる。ストレージはS3
、DBはDynamoDB
、APIプロキシとしてAPI Gateway
、ストリーミングデータの分析にKinesis
、メッセージングにSNS
、キューがSQS
、オーケストレーションと状態管理にStep Functions
、診断にX-Ray
。どんなユースケースがあるのかの話。
S3
静的ホスティングを使えばコンテンツのサイズとリクエストの費用だけで、サーバーがいらない。S3→API Gateway→Lambda
、認証はCognito
が使える。S3
からAPIリクエストを行う際にJavaScriptが活躍。aws-serverless-express
やaws-serverless-java-container
のようなライブラリもある。API Gateway→Lambda→DynamoDB
でフルマネージドなばバックエンド。モバイルからAWSSDKを使ってLambda
を直接呼ぶこともでき、IoTで使われる。S3バケットのイベント→Lambda起動
やDynamoDB Stream で更新イベント→Lambda起動
のようなイベントドリブンでデータ処理が可能。Kinesis Stream
もある。Alexaに話す→バックエンドがLambda
、で構築可能。CloudWatch
のモニタリング、SNS
の通知とLambda
を繋げるなど。「システムがある状態になったら何かする」を、専用サーバーなしで実現できる。AWS Toolkit for Visual Studio
,AWS Toolkit for Eclipse
がLambdaのコーディング/パッケージング/デプロイ/呼び出しまでサポート。AWS Cloud9
が完全にクラウド上でIDEとして使える。AWS SAM(Serverless Application Model)
が便利。aws.amazon.comaws.amazon.comaws.amazon.com
アカウントの取得方法が丁寧にスクショ入りで解説されています。認証と認可についても一通り解説があります。
リージョンの選択方法も説明がありますが、サーバーレスなサービスではありがたいことにほとんどリージョンは関係なし。
最後にはAWSリソースをコマンドから操作できるAWS CLI (Command Line Interface)
の導入方法の説明もあります。画面構成がその後変わったりして本のスクショが古くなってしまう問題を考え、本書の開発ストーリーではできる限り画面からの入力でなくAWSCLIを使うという方針で記述されています。
なかなかの英断です。頑張るとここまでコマンドでできるんだ……!というのが分かります。すでに本格的に使われている方にはAWSCLIの実行時サンプルとしても役に立ちそうです。
CloudWatch
がKinesis
に飛んでくるレコードの量を常時監視中。アラームが起こったらSNS
トピックへ通知→そのイベントでLambda
が起動、対象のKinesis
の処理が間に合うようにシャードの数を増やす……というユースケース例。
Kinesis
へのテストデータ登録もLambdaから行い、
kinesis = boto3.client('kinesis')kinesis.put_record( StreamName={ストリーム名}, Data={入れる内容}, PartitionKey={グループ分けに使われるパーティションキー。サンプルでは現在時刻より} )
と簡単です。CloudWatch
のアラーム設定もすべてコマンドで行い、1分で10件以上飛んで来たらアラームが発生しSNSトピックへ通知。これをイベントトリガーとして別のLambda関数が起動します。
kinesis = boto3.client('kinesis')# 関数の外で宣言するとパフォーマンス向上cloudwatch = boto3.client('cloudwatch')deflambda_handler(event, context): message = json.load(event['Records'][0]['Sns']['Message'])#SNSトピック alarm_name = message['AlarmName'] stream_name = message['Trigger']['Dimensions'][0]['value']# ここでアラーム名が対象の物かの判定をしないと、全アラームが対象になってしまう# Kinesisから取得 stream_summ = kinesis.describe_stream_summary(StreamName={ストリーム名}) curr_open_shard_count = stream_summ['StreamDescriptionSumamry']['OpenShardCount']# Kinesisを更新 response = kinesis.update_shard_count( StreamName={ストリーム名}, TargetShardCount={演算した新しいシャード数}, ScalingType='UNIFORM_SCALING'# ここは固定 )# CloudWatchのアラームを更新する例 response = cloudwatch.put_metric_alarm( AlarmName={アラーム名}, MetricName='incomingRecords', Namespace='AWS/Kinesis', Period= ...,#ここから先は設定値 )
今度はAWSのサイトへのGETレスポンスにに"AWS"という文字が含まれているかで、サイトが正常稼働なのを確かめる例。
lambda-canary-python3
を選ぶと雛形が入っている。作成時のcloudwatch-events
で新規ルール名を作る。SITE = os.environ['site']# コードの外側、環境変数から取得できるEXPECTED = os.environ['expected']# ここにチェック対象の文字列を入れておくdefvalidate(res):return EXPECTEDinstr(res)deflambda_handler(event, context):try:ifnot validate(urlopen(SITE).read()):raiseException('https://awas.amazon.com/ にAWSの文字がない!')except:print('サイトが死んでる!')raiseelse:print('サイトは生きてるよ')return event['time']finally:print('定期チェック終了')
何のことはない、変数SITEで示されたURLをGETリクエストで見て指定の文字列があるかを判定することで可能でした。Python特有のtry-except
の後にelse
が入る書き方はやっぱり独特だなあと、他言語が多い身からは思います。
今度はTwitterのタイムラインをそのままKinesis
へ→Lambdaが起動しDynamoDB
へ保存という、恥ずかしいツイートも黒歴史として永久保存できそうなユースケースの実現。
Consumer key, Consumer Secret, Access Token, Access Token Secret
の4つの認証情報が手に入る。Kinesis
のストリームを作り、DynamoDB
にもテーブルを作っておく。from TwitterAPIimport TwitterAPI# 他は割愛# ツイート群をまるっと取得twitter = TwitterAPI({引数4つ、4種の認証情報})res = twitter.request('statuses/filter', {'locations':'{緯度経度の文字列}'} )kinesis = boto3.client('kinesis')# ひとつづつKinesisに投入!for tweet_itemin res: kinesis.put_record( StreamName={作ったストリーム名}, Data=json.dumps(tweet_item), PartitionKey='filter',# ここがよく分かりませんでした )
このコードをローカルマシン上やEC2
インスタンス上で実行すると、ツイートが文字列に変換されてKinesis
へ投入され始めます。twitter.requestのところは範囲を絞った方がよさそうです。
そしてイベントソースとして上記のたまっていくKinesis
を指定して、別のLambda関数を作っていきます。
# 変数tableに対象のDynamoDBテーブルをいれておくdeflambda_handler(event, context):try: batch_item_list = []for recordsin event['Records']:# Kinesis から取り出すときにデコードがいる payload = base64.b64decode(record['kinesis']['data']) data = json.loads(payload) item = {dataを元に1アイテム分を準備} batch_item_list.append[item]# 最大25件も意識せずにバッチ処理可能with table.batch_writer()as batch:for itemin batch_item_list: batch.put_item(Item=item)returnexceptExceptionas e:# エラー処理raise
手順が細かく説明されているのですが、IAMロールやポリシーの作成、イベントソースを指定したLambda関数の作成も頑張るとAWS CLI
からできてしまうのですね。
今度は以下のようなInstagramライクな本格的なアプリを作っていく章。300ページあまりの本書のボリュームの半分以上を割いて詳しく解説しており、目玉となっています。
S3
静的ホスティングで写真投稿サイトを作成、認証はCognito
を利用。実装はフロントにVue.js
を使ったSPA
。API Gateway→Lambda→DynamoDB
と処理。S3
に保存、ここでもイベント通知でLambdaが起動しAmazon Rekognition
というサービスが画像認識をしてくれる。最初にindex.html
でHello Worldを返す所から始まりますが、ここだけはVueインスタンスはHTML内のscriptタグの中のJS実装で書いています。Node.jsビルドシステムも入れない一番基本のやり方ですね。
APIGatewayの話の後で、LambdaとRDSの相性がなぜ非推奨なのかの話があります。
ここからサーバーレス構成であればRDSではなくDynamoDBを推奨している……とあります。これが本書の出た2018年時点の話。2019年のアップデートで改善したという話が2020年の本『基礎から学ぶ サーバーレス開発』にはありますね。
作者さんご本人のブログ記事でもこの話は詳しく解説されています。サーバーレス元年始まった……!
続いてAPIとしてはGET
で画像一覧の取得、POST
で新規投稿(アップロード)、PUT
で更新、GET
でID指定の画像1券のURL取得、DELETE
で画像削除……をRestfulなURIで設定、それぞれ対応するLambda関数を後ろに準備する形で準備していきます。
画像のようなバイナリファイルの投稿について、本書では以下のような解法を示しています。
Content-Type: multipart/form-data
でリクエストボディにファイル本体を入れて送信。RESTが流行る以前のWebアプリケーションではデフォルトなよくある王道の方式。ボディがJSONでないという欠点がある。API Gateway
に送ってLambdaで保存して紐づける。でかいファイル本体の送信をS3だけに抑えられる。署名付きURLの取得やエラー処理など最初の手間がかかるのが欠点。しかし一度開発すれば将来の変更もなく、スケールもしやすい。API Gateway
は実はバイナリ送信もOKなので、ファイル本体を含めて送り、その奥のLambdaで取得してS3に登録する手もある。同期呼び出しのLambdaで扱えるデータ量(ペイロード)は最大6MBの制限あり。またLambda側の処理時間がそれだけ掛かるので、コストが増大する欠点がある。本章ではこの中から3.の方法を採用しています。後で出てきますがコード量が増えるといってもほぼ定型ですし、やっぱりこの方式が良いのだろうなあという感じ。スケールまで考えているあたりがさすがにAWSJの中の人らしい考察です。
保存用のDynamoDBテーブルを作った後、POST /images
で飛んでくる投稿処理のLambda関数を作っていきます。イベントソースはS3アップロードを検知してではなく、HTML画面でボタンを押してJavaScriptからのHTTPリクエストで起動するというところが要注意。
# ハンドラの外側でパフォーマンス向上dynamodb = boto3.resouces('dynamodb')table = dynamodb = dynamodb.Table({テーブル名})# UUIDからランダムなIDを生成defgenerate_id():returnstr(uuid.uuid4())# DynamoDBは数値のfloat型を使えないので、現在日付はintに変換defget_timestamp(): now = datetime.datetime.utcnow()returnint(now.timestamp())# 1時間使える署名付きURL コード例だと引数3にcontent-typeがあるが、# その後の説明だと抜けてるような?defget_presigned_url(bucket_name, key): s3 = boto3.client('s3') url = s3.generate_presigned_url( ClientMethod='put_object', Params={'Bucket': bucket_name,'Key': key}, Expiresln =60*60, HttpMethod='PUT' )return urldeflambda_handler(event, context):# 画面側のVue.jsの中からaxiosを使って送信してくる body = json.load(event['body']) url = get_presigned_url({バケット名}, generate_id()) item = {IDやタイムスタンプなど1アイテム分を準備。staus:'Waiting'}try:# insert into dynamodb_tbl values(itemの各属性); 的な処理 table.put_item(Item=item)except ClientErroras e:# ログしてエラーレスポンスelse:# ボディに生成した署名付きURLのurlをJSON形式で入れて、200の正常レスポンスを返す
DBの接続処理など、何回も使う処理はlambda_handler関数の外側に出しておくのはウォームスタートだと性能が上がるため常道とのこと、これはAWS認定の問題でも見た覚えがあります。
しかし逆に、あまりに大きい処理を外側に出しておくと逆にコールドスタートの場合は時間がより掛かってしまうこともあるそうです。
実際の開発用にはAWS CloudFormation
の機能を使ってAPIGateway+Lambda周りのデプロイを容易にしてくれるデプロイメントツール、AWS SAM
を解説しています。本格的な開発になると役に立ちそうです。
続いて、画面から上の関数をコールしてS3へのアップロードが成功した後、また画面から飛んでくるPUT /images
の更新処理を受け取るLambda関数の実装。
# 最初にJSONの中にFloat型があってもうまく処理してくれるクラスを用意# ボディに3つのキーが入っていればバリデーションOKdefvalidate(body):return body.keys() >= {'photo_id','timestamp','status'}deflambda_handler(event, context):# またVue.jsの中のaxiosから飛んでくるのでボディを取得 body = json.load(event['body'])ifnot validate(body):# エラーレスポンスを返して終了 photo_id = body['photo_id'] timestamp = body['timestamp'] status = body['status']# 'Uploaded'が渡っててくるtry:try:# DynamoDBのテーブル、idで指定したアイテムのstatusだけを、# 画面から渡ってくる値に更新# 抜けてるけどtimestampも一緒に更新する意図?# update dynamodb_tbl set status = 'xx', timestamp = yy where photo_id = 1234; table.update_item( Key={'photo_id': photo_id}, AttributeUpdates={'status': {'Value': status,'Action':'PUT'} } ) response = table.get_item( Key={'photo_id': photo_id} )except ClientErroras e:# ロギングとエラーレスポンスelse:# response['item']の内容をボディにJSON形式で入れ、200の正常レスポンスを返すexceptExceptionas e:# エラーをロギング
そしてこれまた画面からJavaScript経由で画像の一覧を取得するAPIが呼ばれた時に処理するコードが、以下のような感じ。
# 前処理は省略deflambda_handler(event, context)try:try:# RDBで言うと select * from dynamodb_tbl where status = 'Uploaded'; のフルスキャンを敢行! response = table.scan( FilterExpression=Attr('status').eq('Uploaded') )except ClientErroras e:# エラーログとエラーレスポンスelse:# 変数responseをJSON化、ボディに入れて200の正常応答を返すexcept:
検索についても特に記述があり、RDBに比べると検索が弱いDynamoDBでは事前の正しいテーブル設計がより重要なのだなと改めて思います。
GetItem
で1アイテム取得が速い。一覧検索には使えない。Query
。Elasticsearch
を別途用いる方法もある。Scan
。ただしアイテム数が多いとシステムリソースを大量消費するので課金注意!そして、画面からIDを指定したGET /images/{id}
が来た時の1件検索の処理コードが以下のような感じ。
# 色々省略deflambda_handler(event, context):try:# /images/{id}の部分を取得 photo_id = event['pathParameters']['id']try:# DynamoDBをキー指定で1アイテム取得# select * from dynamodb_tbl where photo_id = 1234 response = table.get_item( Key={'photo_id' = photo_id} )if'item'notin response:# ロギングと404 Not Foundでエラーレスポンスexcept ClientErroras e:# ロギングと400 Internal Server Errorのエラーレスポンスelse:# response['item']をJSON化してボディに入れ、200の正常レスポンスexceptExceptionas e:# ロギング
最後のexceptはURL不正でphoto_idが取れない場合しか来ない気がするので、最初の方で処理してもよいのかな?と思いました。
画面からIDを指定したDELETE /images/{id}
が来た時の1件削除の処理コードが最後。
deflambda_handler(event, context):try:# /images/{id}の部分を取得 photo_id = event['pathParameters']['id']try:# DynamoDBをキー指定で1アイテム取得 response = table.get_item( Key='photo_id' = photo_id} )if'item'notin response:# ロギングと404 Not Foundでエラーレスポンスelse:# RDBなら delete from dynamodb_tbl where photo_id = 1234; response = table.delete_item( Key={'photo_id' = photo_id} )except ClientErroras e:# ロギングと400 Internal Server Errorのエラーレスポンスelse:# 正常終了。ボディは空、204 No Contentで正常レスポンスexceptExceptionas e:# ロギング
ちゃんとステータスコード204を使っていて偉い……!と思いました。削除時に200を返すか204を返すかは考え方が両方あるようです。
バックエンド側は全て準備完了、今度はフロントエンド側です。構成にはvue-cli
を使って一式フォルダ準備、Vue-Router
も使ってSPA、単一ファイルコンポーネント形式でVueを書いていく本格的なやり方です。
技術スタックの概要も記述があるのですが、有名なのはAngular
とReact
だが今回は最近人気のVue.js
を使う、ルーティング機能はReact
だと本体内包だがVue.js
は外出し……など、情報が一部古いですね。(正しくはAngularは内包、ReactはVue.jsと同じで本体でなく外出し)
本書は2018年刊行ですので執筆されたのは2017頃でしょうか、まあこのへんはしょうがないのかなと。
vue cli
をインストール、serverless-spaというディレクトリを作ってここで作業。Pure
という軽量CSSフレームワークを設定。Bootstrap
,Foundation
,SemanticUI
より軽量、jQuery不要のため採用。Home.vue
の中を実装していく。Home.vue
を構成するJavaScriptコードは以下のような感じです。
exportdefault{ data:function(){return{//image_url_base, uploadFile, images: [] だけ}}, created:{// listImagesを呼んで一覧表示}, methods:{ listImages:function(){// ライブラリのaxiosを使ってAPI Gatewayの GET /images をコール、// 結果の配列をimagesに入れる}, onFileChange:function(){// HTMLのinput type="file"要素のonChangeでここに来る。// ファイルの中身をdataプロパティ内に格納。// この時点ではまだ送信しない。this.uploadFile =event.target.files[0]}, uploadImage:function(){// HTMLのbutton要素のアップロードボタンを押したらここの処理。let data ={size:this.uploadFile.size, type:this.uploadFile.type}// まずaxiosを使ってAPI Gatewayの POST /imagesでID登録、// →署名付きURLがレスポンスボディで返る// 次に署名付きURLに向かって this.uploadFileをPUT。// →これで実体がS3に登録。// これも成功したらAPI GatewayにPUT /images で// ボディのJSONに status:'Uploaded' を入れて更新},},}
アップロードボタン押下で動くuploadImage
関数の中で、3回通信をするという仕組みでした。Vue-Router
の設定なども必要で、この節の実装はやることが多くてなかなか複雑です。Vue.js
自体の説明もありますが、難易度が高いと思った初心者の方はCSSのPure
を抜いて見た目は後にしてまっさらでやる、vue-cli
のSPAもやめてHTMLの中のJavaScriptにVueインスタンスを書く一番簡単なやり方でまず一部の動作を確認する、などなどしてエラーの切り分けをしながら進むとよいかと思いました。
ファイルの実体のアップロードにはJavaScript版のAWSサービス操作用ライブラリがいたりするのかな……と勝手に思っていたのですが、署名付きURLがあればそこを宛先に実体をPUTするだけで行けてしまうんですね。
以下のクラスメソッドさんの記事では似たようなことをやっていますが、IAMとセッションも必要になっています。
こちらは最初からJavaScriptの中でaws-sdkライブラリを使ってS3と通信して署名付きURL取得、そこにPUTと、ブラウザ側で完結するパターン。
https://techblog.timers-inc.com/entry/2019/11/26/120351/qiita.com
今度はユーザ管理や認証機能を提供するのでサインアップ(ユーザ登録)やサインイン(=ログイン)を代行できるAmazon Cognito
を使い、認証機能を追加していきます。認証周りもここに任せることでビジネス価値のある開発に集中できる訳です。Cognitoの主な機能は以下。サンプルアプリではユーザプールだけを使用するとのことです。
Amazon Cognito
ユーザープール:サインアップ、サインイン、ユーザ管理。GoogleやFacebook、Amazonなどとも連携できる。メール認証や多要素認証など一通り。Amazon Cognito
フェデレーテッドアイデンティティ:ユーザごとにユニークなIDを提供、他のIDプロバイダーとも連携。IAMロールに紐づいた認証情報を提供するので、アプリ側にハードコードしなくてよい。Amazon Cognito Sync
:複数デバイスでユーザのプライベート情報を同期。複数のユーザでデータをシェアするのはAppSync
で別。CloudFormation
のYAMLファイルでコマンドから、ユーザープールとユーザープールクライアントを新規作成。結果として、UserPoolId, UserPoolClientId
の2つの値が取得できる。中身は乱数の文字列。Open ID Connect
準拠で、nameとusernameが別だったりサインイン時に使う値を変えたり、色々カスタマイズできる。src/config.js
に2つの値を保存。auth.js
という別ファイルのモジュールに外出し。ここにAWS用のライブラリを組み込む。import appConfig from'./config';// UserPoolId、UserPoolClientIdがここから取れるimport * as AWS from"aws-sdk";import{ CognitoUserPool, CognitoUserAttribute, CognitoUser} from"amazon-cognito-identity-js";exportdefault{ signup:function(username, email, password){// サインアップ画面からユーザ名、メアド、パスワードをもってこの関数へ。// 固定のUserPoolId、UserPoolClientIdを使ってCognitoUserPoolクラスを// 作り、メアドも入力情報に含めてCognito側のsingup()をコール。},confirm:function(username, confirmation_number){// 確認画面でユーザ名、メールに書いてある確認番号を入れたらこの関数へ。// 固定のUserPoolId、UserPoolClientIdを使ってCognitoUserPoolクラスを作り、// ユーザ名も入れたCognitoUserクラスを作り、// 確認番号を入力にしてCognito側のconfirmRegjstration() をコール。// どれもreturn new Promise(() => ...の中にラップすることで非同期処理を// 閉じ込め、Vueコンポーネント側から呼びやすくする。}, authenticate:function(email, password){// サインイン画面でメアドとパスワードを入れたらこの関数へ。// 固定のUserPoolId、UserPoolClientIdを使ってCognitoUserPoolクラスを作り、// ユーザ名も入れたCognitoUserクラスを作り、// メアドとパスワードを入力にCognito側の authenticateUsser() をコール。// コールバックが成功/失敗の他に強制パスワード変更もある。}, loggedIn:function(){// Vueコンポーネントで実装された各画面の初期表示時にこの関数へ。// 固定のUserPoolId、UserPoolClientIdを使ってCognitoUserPoolクラスを作り、// getCurrentUser() を呼ぶと今のユーザーが取得できる。// セッションが取得出来てメアドを持ってればログインOKとしtrueを返す。}, logout:function(){// ログアウトボタンからこの関数へ。// 固定のUserPoolId、UserPoolClientIdを使ってCognitoUserPoolクラスを作り、// cognitoUserPool.getCurrentUser().signOut() とするとログアウト処理。},}
続いて画面側を作っていきます。
Signup.vue
として作成。auth
モジュールのsignup()
を呼ぶように。Vuex
のstore
がアプリ全体に組み込まれているので、入力値はこちらに保存。Confirm.vue
として作成。store
から取得したユーザ名も含めてauth
モジュールのconfirm()
を呼ぶように。正常に通過したらRouter
の機能でホーム画面に遷移。Login.vue
として作成。auth
モジュールのauthenticate()
を呼ぶように。成功したらホーム画面に遷移。Router
クラスを修正。beforeEnter:
で別関数を呼ぶ。ここでauth
モジュールのloggedIn()
を呼んでログインチェック、結果がtrue
なら行きたい画面へ、false
ならログイン画面に遷移。Router
の中で設定。auth
モジュールのlogout()
を呼ぶように。 モジュール形式のJavaScript実装やPromise
、Vue.js
側、Vue Router
の実装と幾つも技術要素が絡み、コード量もあってけっこう難しいのですが、読み返して整理してやっと理解できました。
自分的にはサーバーサイド(バックエンド)の認証周りの実装はよくやってきたのですが、ブラウザの中で閉じるフロントエンドのJavaScriptだけでも似たようなことができちゃうのか……!とちょっと感動。authモジュールのその先、中で隠蔽されているCognito
側の機能の中でAWSと通信していろいろやってるのでしょうね。
クライアント側が終わったので今度はAPI側に認証を追加していきます。
API Gateway
周りもSAM
を使っているので、設定ファイルの中にCognitoの話を追加。これでHTTPリクエストにAuthorization
ヘッダがないと呼んでも動かなくなる。
続いてフロントエンド側、auth
モジュールに関数追加。
get_id_token:function(){// 固定のUserPoolId、UserPoolClientIdを使ってCognitoUserPoolクラスを// 作り、cognitoUserPool.GetCurrentUser().getSession() 。// その結果からgetIdToken().getJwtToken() するとトークンの文字列が取得できる。},
Home.vue
の中で、API Gateway
を呼んでいるところは全て修正。リクエストヘッダのAuthorization:
に上の関数を呼んで得られたトークンを追加してから投げるようにする。最後に画像の削除機能の実装例も載っています。
Photo.vue
を追加。data:
プロパティで持っているphoto_id
は、Vue-Router
の機能を使ってURLのパスから取得。created:
プロパティでthis.getImages()
を呼んで画像1件取得。<template>
タグ内のHTMLでは、photo_id
を使ってURLを掲載、S3上にある画像をそのまま<img>
タグで表示。methods:
プロパティの関数getImage()
で、APIGatewayのGET /images/{id}
をコール。deleteImage()
で、APIGatewayのDELETE /images/{id}
をコール。その後ホーム画面に遷移する。S3
にある画像本体は消えない。 紙面の都合等によりS3からの削除はカットとのこと。DynamoDB
から検索されないので一覧画面からは消えますが、画像1件の詳細表示をしているphoto_id付きのURLを直打ちすると……API Gateway→Lambda
に行ってDynamoDB
にアイテムはないから404が返るけど、JavaScript側の処理は続行するので画像が表示される動きでは...? と思います。
このS3からの削除についてはサポートページでPDFで公開されていました。DynamoDB Streams
を有効化すると対象テーブル操作のイベント検知が可能。これをイベントソースにしたまた別のLambda関数を作り、引数のevent
から情報を取って削除のイベントだったらファイル名を取ってきてs3.delete_object()
で削除……というものでした。
https://book.mynavi.jp/files/user/support/9784839964566/5-4_appendix.pdf
最後は> npm run build
すると/dist
にトランスコンパイルされたアプリ一式が生成。/dist
に行ってから、
> aws s3 sync . s3://{バケット名}
するとフォルダの内容が全てS3に反映されて、Vue.js
を使った完全SPA構成+S3
静的ホスティング、S3
とAPI Gateway+PythonによるLambda関数
と連携した、認証機能付きの完全サーバーレスのWebアプリケーションが稼働開始……となります。
いやはや本格的な開発例でした。出てくる技術要素が多いのでけっこう理解に時間が掛かりました。
初めて見た素人の身からするとCognito
周りの組みこみはけっこうやることあるな~という印象も受けたのですが(笑)、よくよく考えれば2回目以降もフロントエンド主体のWebアプリやモバイルアプリを作る際は毎回同じようなことを組み込むだけになるわけです。バックエンド側に[user]
テーブルを持ったりして独自の認証処理を作りこむよりは、Cognito
側に任せるこちらの方が確実に良いのでしょうね。
今度はRecognizeでなくRekognize、Rekognition
を使ったいかにも最新ぽい機能を組み込みます。
S3
バケットがRekognition
と同一リージョンになければならない制限あり。今回のアプリは東京リージョンに上げているので、プログラム内で一旦ダウンロードしてからRekognition
に渡す方式に。本書ではこう記述されていますが、その後2018/2月より、東京リージョンでも使えるようにアップデートされていました。
s3 = boto3.client('s3')rekog = boto3.client('rekognition','{リージョン名。北米us-east1}')dynamodb = boto3.resource('dynamodb','{東京リージョン}'}table = dynamodb.Table({テーブル名})deflambda_handler(event, context):# バケット名とキーのファイル名を取得 bucket = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'],'utf8')try:# S3から対象オブジェクトのバイナリの実体を取得 obj = s3.get_object(Bucket=bucket, Key=key) body = obj['Body'].read()# Rekognitionをコールして画像の中のラベルを検知! 信用度が75以上のものだけ labels = rekog.detect_labels( Image={'Bytes' : body, MinConfidence=75} )# Rekognitionをコールして画像の中の顔を解析 faces = rekog.detece_faces( {Image={'Bytes': body}}, Attributes={'ALL'} )# 解析結果をまとめておく rekognized_label = {'Labels': labels['Labels'],'FaceDetails': faces['FaceDetails'], }# キーのファイル名から拡張子を除くと乱数からなるIDになる photo_id = key.split('.')[0]# DynamoDBの対象アイテムを更新。# update dynamodb_tbl set labels = '{解析結果}' where photo_id = 1234; table.update_item( Key={'photo_id': photo_id}, AttributeValues = {'labels': {'Value': {rekognized_labelをJSON化した文字列}'Action':'PUT' } } )returnexceptExceptionas e:#ロギング
S3
のオブジェクト生成をイベントソースにこのLambda関数を設定しておけば、画像のアップロード時に走ってDynamoDB
が更新されるというものでした。
難しそうですが分かるとなんということはない、画像ファイルの実体のバイナリ文字列を渡すと解析してくれるということでこれは便利そうです。思ったより簡単にできてしまうのですね。
「フレームワーク」のように言われることもあって実際その意味合いもあるSAM
ですが、本書ではAWS CloudFormation
の拡張であると定義して深掘りしています。
CFn
の定義のxx.yml
ファイル。先頭のバージョン行の次にTransform: 'AWS::Serverless-2016-10-31'
を追記。Resources:
の次に関数の名前などリソースの名前。その次のType:
に書くリソースタイプが以下3種。AWS::Serverless::Function
、AWS::Serverless:Api
、AWS::Servlerless:SimpleTable
。CodeUri: s3://{バケット名}/{ファイル名}.zip
のようにコードの実体をアップロードしたS3の場所を書く。S3
にアップロードした後CodeUri
に書かずに以下のコマンド> aws cloudformation package --template-file xx.yml --output-template-file xx-out.yml --s3-bucket {バケット名}
CodeUri
に設定してくれるというやり方もある。> aws cloudformation deploy --template-file xx-out.yml --stack-name {スタック名} ...
のようにしてデプロイ。どちらもメリットデメリットありますが、チームが大きくなってくると2が推奨。また2016年提供開始のAWS Organizations
を使うと楽になると本書では使用を勧めています。
このへんは有識者の話としては技術同人誌から商業本にもなった『AWSの薄い本』シリーズでディープなところが書いてあります。
CI/CDといえばCode3兄弟のシリーズ。CodeDeploy
やCodePipeline
を用いた自動処理の例が、本書ではここでもほぼすべてコマンドからの実行という例で記述されています。
各サービスの監視で確認できる値の種類であるメトリクスについて、サーバーレスの各サービスごとにメトリクス名や意味、単位まで表にまとまっています。
ロギングについてはLambda
関数はデフォルトでCloudWatch Logs
に出力。そしてAPI Gateway
もCloudWatch Logs
へのログ出力を有効化することで監視可能。これもコマンドベースで手順が述べられています。
サーバーレスやマイクロサービス特有の、粒度が小さいかたまり群がそれぞれ処理するので関連が分かりづらく問題が追いにくい……という問題を解決してくれるX-Ray
の使い方。
EC2, ECS, Elastic Beanstalk, Lambda
が対象。Lambda
関数内からのAWSリソースへのアクセスもこれで採れる。Lambda
の場合は実行ロールになっているIAMロールに管理ポリシーAWSXrayWriteOnlyAccess
のアタッチがいる。X-ray
がサンプリングで間引きして効率的にトレースしてくれる「アクティブ」モードがある。Lambda
関数ごとに設定。AWS X-Ray
の画面からログを見て色々確認できる。ずっとコマンドベースのハードモード(笑)だったところ、ここだけは管理コンソールのスクショが出てきて、おおカラフルだ……と思いました。
作者さんは2017年6月にも『実践AWS Lambda』という国内初のLambda本を出しています。
『実践AWS Lambda』という本を書きました - Sweet Escape
その後ということもあってか、サーバーレスの概念や基本も書いてありますがより高度な内容、応用的な所がメインなのかなと思いました。自分の場合はまた書名が似た別の本でややこしいのですが『AWS Lambda実践ガイド』の後に読んだので基本→応用という感じになってちょうど良い塩梅でした。
現AWSJの方らしく実際の開発で役に立ちそうな知見がだいぶ詰まっているのですが、やはり最大の見どころは本の半分以上を占める第5章、Vue.jsに
よるSPA+バックエンドのREST API
でサービスを一式作るところでしょう。自分もここを読破してだいぶ解像度が上がって理解が深まりました。なんかもうWebサービスが作れちゃいそうな気がするぞ……!(気がするだけw)
自分はとりあえずコマンドラインのところは最重要ではないので注視しなかったのですが、可能な限りすべての操作をコマンドラインベースで書いているところも、実際に使っている方にはお役立ちだと思います。
難点はというと……作者さんご本人のブログ記事にもありますがLambda
関数のPython
コードのところどころに謎のインデントミスや名前付き引数の後の謎のスペース、「これほんとはこういう意図なんじゃないかな?」というところ、誤植が幾つかハッケンされました。まあこのへんは技術書の宿命なので、自力で分かるぐらいまで進歩するのも学びのうちということで(笑)、これから読む方もチャレンジすればよろしいかと思います! LambdaでPython完全に理解した…(2回目)
作者のid:Keisuke69 こと西谷圭介さんによる紹介記事。www.keisuke69.net
サポートページもあり、ソースコードもDLできます。こちらは誤植が直っている模様。book.mynavi.jp
作者さんが登壇した2020年4月の「みんなのPython勉強会#56」の記事。この記事自体がかなりお役立ちです。logmi.jp
AWS認定のSAAオレンジの本を始めAWS関係の書籍でも知られるid:kentacho_jp さんによる紹介記事。www.ketancho.net
こちらもAWSといえばお馴染みクラメソさんの紹介記事。ここでも好評です。dev.classmethod.jp
はてなブログの書評記事。katsuki.hatenablog.com
サーバーレス関連は以前上げた『基礎から学ぶサーバーレス開発』の感想記事の最後にまとめています。
引退した元TRPGゲーマー。COBOLでもなくPL/Iの金融系レガシー紙駆動開発から脱出→国産メーカー系総合ITベンダーのITエンジニア。所謂SIerのSEだけど仕事はほぼソフトウェアエンジニア/ソフトウェアアーキテクトとして、Web開発でコードを書いたり技術を追ったり時々イベントに行ったり、楽しいエンジニアリングを目指しています。Views are my own.
お気軽にどうぞ~
リンク集:https://lit.link/iwasiman
AIイラスト関連の活動はこちら↓
https://www.chichi-pui.com/users/iwasiman/
https://pixiv.me/iwasiman
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。