【AWS】S3 PUTトリガー設定

1. 使用するサービス

(AWS)

  • S3
  • Lambda


2.概要

S3のバケットに対するPUTアクションに対するLambdaの処理を説明します。
主に、簡単なS3の設定と、Lambdaでのその内容の取得の仕方を説明します。

3. 実装

3-1. S3の設定

通知設定の部分が、トリガーの設定になります。Lambda側のトリガー設定でも同様です。

s3_notification
S3トリガー設定

イベント名等の設定を行います。Prefixを設定すれば、Prefix毎のS3トリガーを設定出来ます。Event TypeはPUTなどS3のトリガーアクションの設定になります。PUT意外にもDELETEなど多種あります。

S3_event
S3イベント設定

トリガーの設定でLambdaを設定します。指定方法はARNとLambda名がありますが、どちらでもいいです。

S3_trigger
S3トリガー設定


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】SAA受験 リモートはやめた方がいい

個人PCでSAA受験して見たが、エラー画面が毎回表示されて受験出来なかったので体験記を軽く記載。
原因と今後の対応は待ち状態。


1. 状況

受験開始ボタンを押下すると、下記のような画面遷移される

saa_error
受験エラー画面

また、ブラウザの環境チェックは下記のように問題なし

check_browser
ブラウザ環境確認


2. 対応

AWSの受験ダッシュボード内のチャットで対応を聞くと、ブラウザの再起動を指示されましたが、解消せず。
PSIのサポートページに飛ばされるが、特に対応はなし

3. 現状
再受験出来るかを確認中

【AWS】S3署名付きURLによる画像保存

1. 使用するサービス

(AWS)

  • S3
  • Lambda


2.概要

S3に画像をアップロードする際にいくつか手法があると思いますが、API GatewayとLambdaのサーバレスの構成の際、Lambdaに対してbase64エンコードして送るなど考えがちかと思います。少し、時間はかかると思いますが、悪くない選択だと思います。しかし、API Gatewayの制約として、入力ペイロードの最大が10MBまでということもあり、複数画像の一括アップロードや動画の保存は現実的に不可能です。
そこで、今回、クライアント側から直接S3へセキュアな方法でアップロードする方法を説明します。

全体的な流れは下記のようになります。

s3_presigned_url
画像アップロード流れ

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)
    }

3-2. API Gatewayの実装

普通に実装すると以下のようにLambdaを設定、またフロントから通信する際にCORSを設定して終了です。
また、クエリのテンプレート設定等が面倒なので、Lambda統合プロキシを使用します。

api_gateway_invoke_lambda
API Gateway Lambda呼び出し

3-3. テスト

API Gatewayのテスト機能でテストすると、エラーになります。

api_gateway_test
API Gateway テスト

QueryStringPArametersが設定されていない為です。

3-4. クエリパラメータの設定

Request MethodのURL Query String Parametersに今回の例ではnameを必須設定で入れます。

api_gateway_query
クエリ設定

再度、nameにhogeを入れてテストしてみます。

api_gateway_test2
API Gateway 再テスト

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の更新やバージョンの再指定を忘れずにお願いします。