【Lambda関数構築編】開発未経験がクラウドを企業に導入するプロジェクトごっこをしてみた~その6~

# はじめに
２２年度文系学部卒のたいきです。この記事では、システムの開発経験もないのに、見よう見まねで「プロジェクトごっこ」をしていきます。
・保有資格 : 応用情報技術者・AWS-SAP
・プログラミング言語:軽くPython
本記事を最初から読んでいただける方は「[開発未経験がクラウドを企業に導入するプロジェクトごっこをしてみた~その１~](https://qiita.com/Ishii_Taiki/items/e3035c12b31385f8156b)」から読んでいただけると嬉しいです。
# 本記事について
今回は、[【モデリング編】開発未経験がクラウドを企業に導入するプロジェクトごっこをしてみた~その5~](https://qiita.com/Ishii_Taiki/items/f76265a6cb181dcf84f0)のモデルを利用してLambda関数を構築していきます。このLambda関数は、S3イベントをトリガーにして、画像分析をはじめ、推論結果をDynamo DBに格納する関数です。

# PythonコードのLambdaへのアップロード
前回のモデリングの際に作成したコードを、Lambdaにアップロードしていきます。コードは以下のようなものを使いました。
```python
import boto3
import numpy as np
from PIL import Image
from PIL import ImageColor
import urllib.parse
import json
from datetime import datetime

def lambda_handler(event, context):
    s3 = boto3.resource("s3")
    #jsonより、バケット名とファイル名を取得
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    
    #画像を一時的に格納するtmpファイルのパス
    file_path = '/tmp/' + key + '_' + datetime.now().strftime('%Y-%m-%d-%H-%M-%S-')
    
    #バケットとファイルを変数に格納
    s3_bucket = s3.Bucket(bucket)
    s3_key = s3_bucket.Object(key)
    
    
    try:
        response = s3_key.get() 
        
        body = response['Body'].read()
        
        s3_bucket.download_file(key, file_path)
        with open(file_path, 'rb') as f:
            img_bin = f.read()
        
        smr_client = boto3.client('sagemaker-runtime')
        # エンドポイントの名前
        ENDPOINT_NAME='###sageaker_endpoint_name###'
        # 推論する画像の場所
        TEST_IMAGE_FILE = file_path
        
        # 推論対象の画像を開いて変数に格納}
        with open(TEST_IMAGE_FILE, 'rb') as f:
            img_bin = f.read()
        
        # 推論を実行
        response = smr_client.invoke_endpoint(EndpointName=ENDPOINT_NAME, ContentType='application/x-image', Body=img_bin)
        
        # 推論結果を読み込む
        model_predictions = json.loads(response['Body'].read())
        
        # テスト画像を PIL を通して numpy array として開く
        image_np = np.array(Image.open(TEST_IMAGE_FILE))
        # 推論結果を変数に展開
        bboxes, classes, confidences = model_predictions['normalized_boxes'], model_predictions['classes'], model_predictions['scores']
        # 物体検出結果を検出した分だけループする
        take = 0
        kino = 0
        
        for idx in range(len(bboxes)):
            # 信頼度スコアが 0.5 以上のみ可視化する
            if confidences[idx]>0.5:
                # 検出した座標（左上を(0,0),右下を(1,1)とした相対座標)を取得
                left, bot, right, top = bboxes[idx]
                # 相対座標を絶対座標に変換する
                x, w = [val * image_np.shape[1] for val in [left, right - left]]
                y, h = [val * image_np.shape[0] for val in [bot, top - bot]]
                # 検出した物体によって、変数take,kinoへ加算する
                if int(classes[idx])==0:
                    take += 1
                else:
                    kino += 1

        dynamoDB = boto3.resource("dynamodb")
        table = dynamoDB.Table("###dynamo_db_table_name###") # DynamoDBのテーブル名

        # DynamoDBへのPut処理実行
        table.put_item(
          Item = {
            "file_name": key, # Partition Keyのデータ
            "kino": kino, # Sort Keyのデータ
            "take": take # その他のデータ
          }
        )
        print("kino:",kino)
        print("take:",take)
    except Exception as e:
        print(e) 
        print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
        raise e
```
# テスト
Lambdaにコードをアップロードし、テストしようとした際にこのようなエラーが出ました。
モジュールが見つからないというエラーだそうです。どうやら、SagemakerのNotebookにインストールされているからといってLambdaにインストールされているとは限らないようです。なんなら、感覚的にLambdaには最低限のライブラリのみしかインストールされていない感触でした。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608302/2830171d-ee7f-4c78-e3b4-789b375b7bd7.png)
さて、しかし、エラーの原因は解決できました。しかし、Lambdaにモジュールをどのようにインストールすればいいのか・・・。ローカルでは、pipインストールでもすればすぐに解決なんですが、、、

# Lambdaへの外部モジュールインストール方法
どうやら、pipインストールなどが使える環境で作成した.pyファイルをモジュールをインストールしたディレクトリごとzip化してLambdaにアップロードすることで解決できるようです。今回は、PCを先日壊してまだVScodeすらPCにインストールしていないのでCloud9を利用していきます。利用したいモジュールをインストールし、フォルダを保存するだけです。zip内にモジュールのインストールされた環境が構築されているので、Lambdaにアップロードすればそのまま利用できます。
## zip化失敗例
zip化する際に、少し気を付けなければエラーが起きてしまいます。その失敗例を少し紹介させていただきます。
下図の失敗例では、エラーが発生します。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608302/d6d6d328-f900-d816-015d-2dc5971aa3e0.png)
以下のようなmoduleがありませんというものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608302/b0440654-62ba-18ea-7d72-0b462e774a63.png)
この理由としては、ディレクトリごとzip化してしまうとディレクトリ階層がLambdaで使えるものよりも１段階上がってしまうためです。階層が違うことにより、Lambdaファンクションの.pyファイルを見つけることができないようです。
## zip化成功例
そのため、個別のファイルをコマンドでzip化しましょう。この例では「Zip_file」というファイルが作られるはずです。
```
$ cd [実行ファイルのディレクトリ]
$ zip -r zip_file ./*
```
# 再テスト
さっそく、SageMakerへのアクセス権がLambdaにありませんよーと怒られてしまいました。その修正の後に無事以下のような出力が出てコンパイルに成功できました。約１５時間以上かけてやっと成功しました。このプロジェクトが完成するのか心配になります笑まだ、printでの出力ですがたけのこ:11 きのこ：14で無事に出力できています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608302/ac21204b-f6f3-6b0b-091b-0740d6f60138.png)
以下がテストに利用した画像です。sagemakerの推論テストに利用した画像と同一のものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608302/1875c12b-8eaf-6665-0d0c-ceeb196ffb67.png)

# Dynamo DBへの推論結果の格納
以下のコードを先ほどのコードに追加していただき、Dynamo table名を変更していただければ利用できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608302/9a85f94e-847b-204d-095c-6ef496a11d67.png)

```
        dynamoDB = boto3.resource("dynamodb")
        table = dynamoDB.Table("###table_name###") # DynamoDBのテーブル名

        # DynamoDBへのPut処理実行
        table.put_item(
          Item = {
            "file_name": key, # Partition Keyのデータ
            "kino": kino, # きのこの山のデータ
            "take": take # たけのこの里のデータ
          }
        )
```

一つのエラーを除き（後述）無事順調にlambdaからDynamoDBに対して値を送信することができました。DynamoDBのポリシー関連のエラーもなかったので、もしかしたらDynamoDBはあまりセキュリティを高める必要がないのかな？？と感じました。ユースケースとしては、やっぱりバッチ処理するまでのちょっとしたストレージの確保だからでしょうか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608302/c440c1c0-6ad7-d807-64e4-f4d407e305bd.png)
## 今日の学び
先ほど後述と書かせていただいた、一つのエラーの解決がとても自分的に大きな学びだったのでここに残させてもらいます。S3バケットに対して画像をアップロードしても、DynamoDBに結果が出力されていませんでした。それだけでなく、Lambdaコンソールのテストでは機能していたはずのLambdaが起動したログすら見つからなかったのです。そこで、藁にも縋る思いでLambdaの結果をSNSに出力するように設定をしたところ、SNSがエラーメッセージをメールで届けてくれました。Lambdaにイベントが送られているがLambdaがエラーメッセージを出していたようです。メッセージを解析した結果Lambdaのruntimeエラーであることが分かりました。改めて、出力の偉大さ、そして、僕にプログラミングを教えてくれたフランス人の言葉「コードと会話しろ」という言葉を思い出しました。(なにかあったらSNSで出力してみよ…)
```
"statusCode":200,"executedVersion":"$LATEST","functionError":"Unhandled"},"responsePayload":{"errorMessage":"RequestId: 546eb48b-857d-427c-826c-6894ae754ca7 Error: Runtime exited with error: signal: killed","errorType":"Runtime.ExitError"}}
```
> 例のエラーコード

# おわりに
AWS-SAPの勉強の時とは違い、自分のコードや操作がそのままアウトプットに反映されるのでやっぱり、技術のアウトプットは非常に楽しいです。けど、最近なんらかの資格勉強でも始めて視野を広げたいなーっていう気持ちもあります。候補の資格としては、CCNA, AWS-DOP, AWS-ANS（高度なネットワーキング）あたりですかねー、、、？とりあえずこのプロジェクトを完遂させたいところですね。
また次回を楽しみにしていただけると嬉しいです。