【AWS】S3 PUTトリガー設定
1. 使用するサービス
(AWS)
- S3
- Lambda
2.概要
S3のバケットに対するPUTアクションに対するLambdaの処理を説明します。
主に、簡単なS3の設定と、Lambdaでのその内容の取得の仕方を説明します。
3. 実装
3-1. S3の設定
通知設定の部分が、トリガーの設定になります。Lambda側のトリガー設定でも同様です。
イベント名等の設定を行います。Prefixを設定すれば、Prefix毎のS3トリガーを設定出来ます。Event TypeはPUTなどS3のトリガーアクションの設定になります。PUT意外にもDELETEなど多種あります。
トリガーの設定でLambdaを設定します。指定方法はARNとLambda名がありますが、どちらでもいいです。
3-2. Lambda実装
Lambdaのevent引数からトリガーから送られてくる内容を取得します。
import json def handler(event, context): messages = event['Records'] for message in messages: body = json.loads(message['body']) print(body) """ Lambdaの処理 """
以上でわかるようにトリガーの内容は配列で複数来ることがあるので、
for文で回して対応するのがbetterかと思います。
4. おわりに
今回はLambdaの非同期処理であるS3のトリガーを説明しましたが、SQSでの非同期処理、特にVPC内のLambdaからSQSを投げる方法を説明しようかと思います。
【AWS】S3署名付きURLによる画像保存
1. 使用するサービス
(AWS)
- S3
- Lambda
2.概要
S3に画像をアップロードする際にいくつか手法があると思いますが、API GatewayとLambdaのサーバレスの構成の際、Lambdaに対してbase64でエンコードして送るなど考えがちかと思います。少し、時間はかかると思いますが、悪くない選択だと思います。しかし、API Gatewayの制約として、入力ペイロードの最大が10MBまでということもあり、複数画像の一括アップロードや動画の保存は現実的に不可能です。
そこで、今回、クライアント側から直接S3へセキュアな方法でアップロードする方法を説明します。
全体的な流れは下記のようになります。
API Gatewayの設定等は前回の記事参考
buffalokusojima.hatenablog.com
3. 実装
【バックエンド実装】
3-1. S3署名付きURL発行Lambda
S3に署名付きURLの発行をS3に依頼し、署名付きURLと各種認証情報等を受け取り、レスポンスとして返すLambdaを作成します。
import json import os import boto3 import uuid s3Client = boto3.client('s3') FOLDER = '/tmp' BUCKET_NAME = os.environ["PhotoBucketName"] def handler(event, context): print(event) body = event["queryStringParameters"] file_name = body["file_name"] file_type = body["file_type"] file_num = body["file_num"] userName = event['requestContext']['authorizer']['claims']['cognito:username'] file_extention = file_name.split(".")[-1] folder = os.path.join(userName, file_num) key = os.path.join(folder, str(uuid.uuid4())+"."+file_extention) presigned_post = s3Client.generate_presigned_post( Bucket = BUCKET_NAME, Key = key, Fields = {"acl": "public-read", "Content-Type": file_type}, Conditions = [ {"acl": "public-read"}, {"Content-Type": file_type} ], ExpiresIn = 3600 ) return { "statusCode": 200, "body": json.dumps({"success": True, "url": presigned_post["url"], "data": presigned_post}), "headers": { "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS" } }
3-2. S3のPUTトリガーが設定されたLambda
これはおまけですが、S3のPUTに対して反応するLambdaを作成します。イメージとしては、S3にPUTされた画像のURLをRDS等に保存するといった感じです。(今回はSQSに投げるまでにします)
import json import os import boto3 import uuid queueClient = boto3.client('sqs') QUEUE_URL = os.environ["ImageDataWriteQueue"] def handler(event, context): print(event) records = event["Records"] for record in records: s3 = record["s3"] bucket_name = s3["bucket"]["name"] + ".s3.amazonaws.com" key = s3["object"]["key"] folder = "/".join(key.split("/")[:-1]) url = "https://"+os.path.join(bucket_name, key) msg = {'messageId': str(uuid.uuid4()), "image_url": url, "folder": folder} queueClient.send_message(QueueUrl=QUEUE_URL, MessageBody=json.dumps(msg), MessageGroupId="group")
【フロントエンド】
3-3. 画像のアップロードとダウンロード
フロント側の実装になります。画像の表示とアップロードを行っています。
注意点としては、アップロード中に更新等で処理を中断すると、写真が古いままになる可能性があります。フロント側で処理が中断されないようにする必要があります。
getImageファンクションで叩いているAPIは別途作成する必要あります。RDS等に画像のURLを保存するなりして、そこからURLを引っ張り、レスポンスとして返すLambdaが理想です。機会があれば別途、そこら辺も紹介出来たらと思います。
<html> <head> <meta http-equiv="Cache-Control" content="no-store"> <title>test</title> </head> <body onload="begining()"> <input type="file" id="postfile1"/> <input type="file" id="postfile2"/> <input type="file" id="postfile3"/> <input type="file" id="postfile4"/> <input type="file" id="postfile5"/> <input type="file" id="postfile6"/> <input type="file" id="postfile7"/> <input type="file" id="postfile8"/> <input type="file" id="postfile9"/> <input type="file" id="postfile10"/> <input type="button" value="submit" onclick="postImage()" /> <div id="image_field"></div> <script> const URL = "https://URL"; function sendRequest(data, callback, callbackValue){ var request = new XMLHttpRequest(); let storage = localStorage; let id_token = storage.getItem("idToken"); request.open(data.method, data.url); request.setRequestHeader("Content-Type", "application/json"); request.onload = function(err){ console.log(err); if(request.readyState == 4){ if(request.status == 200){ console.log("success"); var res = JSON.parse(request.responseText); console.log(res); callback(res, callbackValue); }else if(request.status == 401){ var res = JSON.parse(request.responseText); console.log(res); singnIn(); }else{ console.log(request.status, request.response); console.log(request.responseText); } } } request.onerror = function(){ console.log("err:" + request.status); console.log(request.responseText) } console.log("sending"); var body; if(data.body){ /* body = { "body": JSON.stringify(data.body) };*/ body = JSON.stringify(data.body); //body = data.body } console.log(body); request.send(body); } function begining(){ getImage(); } function getImage(){ var data = { "method": "GET", "url": URL + "getImageURL", "body": null } sendRequest(data, showImage, null); } function showImage(data){ var url_list = data.url_list; var image_filed = document.getElementById("image_field"); for(var i=0; i<url_list.length; i++){ var imgSrc = document.createElement("img"); imgSrc.id = i+1; imgSrc.src = url_list[i]; image_filed.appendChild(imgSrc); } } function postImage(){ for(var i=1; i<11; i++){ var file = document.getElementById("postfile"+i); console.log(file); file = file.files[0]; if(!file){ continue; } var data = { "method": "GET", "url": URL + "getImagePresignedURL?file_name=" + file.name + "&file_type=" +file.type + "&file_num=" + i, "body": null } sendRequest(data, uploadFile, file); } } function uploadFile(s3Data, file){ var xhr = new XMLHttpRequest(); xhr.open("POST", s3Data.url); var postData = new FormData(); for(var key in s3Data.data.fields){ //console.log(key, s3Data.data.fields[key]) postData.append(key, s3Data.data.fields[key]); } postData.append('file', file); xhr.onreadystatechange = function() { if(xhr.readyState === 4){ if(xhr.status === 200 || xhr.status === 204){ //document.getElementById("preview").src = url; //document.getElementById("avatar-url").value = url; } else{ alert("Could not upload file."); } } }; xhr.send(postData); } </script> </body> </html>
4. おわりに
以上で簡単なS3の署名付きURLによる画像アップロードになります。
次回以降、PUTトリガー、SQSによるRDSとの連携とかも出来たらと思います。
【AWS】API GatewayのLambda統合Proxy使用時のCORS設定
1. 使用するサービス
(AWS)
- Lambda
2.概要
API Gatewayの設定は下記参考。
buffalokusojima.hatenablog.com
記事の内容だと、フロントのJavascriptからajaxなどで通信した際のレスポンスがCORSエラーとなります。
それを防ぐ方法として、Lambdaのレスポンスヘッダに明示的にCORSの設定を入れておきます。
3. 実装
下記コード参考
import json def handler(event, context): data = { "data": "Hello World" } return { "statusCode": 200, "body": json.dumps(body), "headers": { "Access-Control-Allow-Headers": "Content-type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS" } }
4. おわりに
わかってみれば簡単なことでした。Orginが*で全てを許容してますが、環境変数に実際のURL等を入れましょう。
【AWS】API GatewayのGETメソッドでクエリパラメータ取得
. 使用するサービス
(AWS)
- Cloudformation
- Lambda
2.概要
API GatewayをLambdaの呼び出しに使用することが多いと思います。その際、メソッドの選択肢としてGETがあります。GETで何かしらの値をLambdaからもらう際に、取得する内容の条件等にフロントからクエリパラメータを送ることで、Lambdaでパラメータを受け取り、内部で処理をすることで実現します。
実装の際、Lambdaとメソッドを指定するだけでは、クエリパラメータを受け取れない事実が最近発覚したので説明します。(開発中はなかなか気づかず、恥ずかしい限りです)
3. 実装
3-1. Lambda例
Lambdaは以下の内容で作成
import json def lambda_handler(event, context): # TODO implement parameter = event["queryStringParameters"] name = parameter["name"] return { 'statusCode': 200, 'body': json.dumps('Hello ' + name) }
普通に実装すると以下のようにLambdaを設定、またフロントから通信する際にCORSを設定して終了です。
また、クエリのテンプレート設定等が面倒なので、Lambda統合プロキシを使用します。
3-3. テスト
API Gatewayのテスト機能でテストすると、エラーになります。
QueryStringPArametersが設定されていない為です。
3-4. クエリパラメータの設定
Request MethodのURL Query String Parametersに今回の例ではnameを必須設定で入れます。
再度、nameにhogeを入れてテストしてみます。
Query Stringがオンになり、クエリパラメータを入れることが出来るようになり、nameにhogeの値を設定して渡します。
無事にレスポンスボディが返ってきたので成功です。
4 おわりに
他にもLambda統合プロキシなど重要な項目もあるので、別記事で紹介しようかと思います。
【AWS】Cloud9上でLambda Layer使用
1. 使用するサービス
(AWS)
- Cloud9
- Lambda
- Lambda Layer
2.概要
Cloud9上でアップロード済のLambda Layerを使用する方法を説明します。
Cloud9でLambdaを開発していると、共通関数をまとめたくなることがあると思います。その際、Lambda Layerに共通関数をまとめ、LayerをLambdaにアタッチし、モジュールを呼び出すことでLayerを活用すると思います。Cloud9ではもちろんLayerをアタッチすることは出来ないので、今回はCloud9上でLambdaが載ったDocker上にLayerの中身をインストールすることでCloud9上での実行を実現します。
3. 実装
具体的にはtemplate.ymlにLayerのArnを記述するだけです。
LayerFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub "${Project}-${Stage}-LayerFunction" Handler: index.handler Runtime: python3.8 CodeUri: ./layerfunction Role: !GetAtt layerRole.Arn Layers: - !Sub "arn:aws:lambda:${AWSRegion}:${AccountId}:layer:${Project}-${Stage}-TestLayer:1"
最後のLayersの箇所のArnを変数なしに変えるだけです。
Cloud9上だと擬似パラメータがデフォルトのままなどの問題がある為です。gitでコミットする際は書き直すよう注意しましょう。
4. おわりに
Layerに関して検索しても、何故かCloud9上では実行出来ないなど出てきたが、案外やってみれば出来る物でした。
余裕があれば、templateの擬似パラメータのoverrideなどに挑戦し、template.ymlの中身を書き換える必要のない方法を模索してみようと思います。
【AWS】CloudformationでLambda Layer実装
1. 使用するサービス
(AWS)
- Cloudformation
- Lambda
- Lambda Layer
2.概要
CloudformationでLambda Layerを実装する手順を説明します。
大まかな手順としては以下になります。
①Layerの中身を作成し、zipにする
②CodeBuildでzip化したLayerを指定したS3に入れる
③Cloudformationで指定したS3からzipファイルを選択してLayerを構築する
3. 実装
3-1. Layerの中身作成
フォルダ構成は以下のようにします。
Layer | |--LayerContent | |--python | |--layer.py
例としてファイル名はlayer.pyとし、zip化する対象はLayerContent直下になります。理由としては、Lambda上でのLayerの展開先はopt/pythonになるからです。
layer.pyの中身は任意の物で大丈夫です。importしたい形にしてください。
3-2. CodeBuildでzip化してS3に保存
CodePipelineで実装するイメージで、github等で作成したLayerをCodeBuidのステージでzip化し、S3に送ります。
version: 0.2 phases: install: runtime-versions: python: 3.8 commands: - pip install --upgrade awscli boto3 mock pymysql==0.10.1 - cd layer/LayerContent - zip -r layer.zip python - aws s3 cp ./layer.zip s3のURL - rm rdsconnection.zip - cp -r python/rdsConnection ../../ - cd ../.. build: commands: - python -m unittest discover tests/Unit - aws cloudformation package --template-file template.yml --s3-bucket $BUCKET --output-template-file outputtemplate.yml - aws s3 sync ./public/ $WEB_BUCKET$Stage --delete
主にinstallの部分がLayer関係です。zipにして指定したS3に送っているだけです。適宜S3のURLを編集してください。
3-3. CloudformationでLayer作成
以下、yamlでLayer及び、Lambdaへのアタッチを行います。
TestLayer: Type: "AWS::Lambda::LayerVersion" Properties: CompatibleRuntimes: - python3.8 Content: S3Bucket: !Ref LayerBucketName S3Key: layer.zip #Layer File Description: "test module layer" LayerName: !Sub "${Project}-${Stage}-TestLayer" LayerFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub "${Project}-${Stage}-LayerFunction" Handler: index.handler Runtime: python3.8 CodeUri: ./layerfunction Role: !GetAtt layerRole.Arn Layers: - !Sub "arn:aws:lambda:${AWSRegion}:${AccountId}:layer:${Project}-${Stage}-TestLayer:1"
LambdaからLayerを指定する場合は、バージョンまで必要なので注意が必要です。
各種パラメータも適宜編集お願いします。
また、Runtimeも合わせる必要があります。
4. おわりに
以上で、Layerの実装が出来ました。注意点としては、Layerをソースレベルで更新しても、Layerのデプロイまではされないので、yamlの更新やバージョンの再指定を忘れずにお願いします。