AWSのLambdaを使ってS3間のファイルコピーを試してみた。

## はじめに
普段何かお試しで作りたい時は、主にGCPにVMインスタンスを立てて、そこで色々といじってみる事が多かったため、本格的に色々なクラウドサービスを試したことはなく、AWSもほとんど使ったことがなかった。
周りではクラウドと言えば **"AWS"** の選択肢が多い気がするので、基本的なサービスくらいは多少はいじれた方が良いかと思い、今回は触りながら、調べながら学んだことをメモとして残しておく。


## 前提
そもそもから勘違いをしていた部分なのだが、まずAWSアカウントを作成した場合、各リージョンにそれぞれデフォルトでVPCができるのだが、このVPCの中に色々なサービスを作成（追加）していく様なものかと想像していた。
しかし、実際には以下の様なイメージで、VPCに属させるサービスの方が少ないのではないかと思っている。
![Qiita-no049_img01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/220242cc-125f-8858-b642-b27434439f0b.jpeg)
サーバーレスのサービスは基本的にVPCに紐づかないイメージ。
（そもそも、サーバーレスでセキュアなサービスを提供してくれる事が、クラウドの大きな特徴の１つのなので、当たり前なのかもしれないが・・・）


## 試しにやってみたい事
今回は、上記のイメージ図で言う、VPCに紐づかないサービスとして最も有名な **"Lambda"**　**"S3"** を使ってみることにする。一応、それぞれの説明は以下の様な感じになる。

**■Lambda**
　イベントの発生に応じてプログラムを実行する環境を提供するクラウドコンピューティングサービス。(wikipedia引用)
　（GCPで言う、Cloud Functionみたいなものと予想。裏側でプログラム実行用のコンテナが立ち上がるイメージだったので、VPCに属するサービスな気もしたが、実際はずっと起動しっぱなしという訳ではなく、10～15分起動したら落ちるらしい。）
設定でVPCに接続することもできるみたいで、関数内の処理でインターネットにアクセスする場合は必須らしい。

**■S3**
　Webサービスのインタフェースを介してストレージを提供している。(wikipedia引用)
少しいじってみた感じだと、バケットのバージョニング機能があり、格納された全ドキュメントのバージョンを保存、取得、復元できるらしい。今回は無効にして作成すると思うが・・・



今回使ってみるサービスの基本的な説明は以上で、具体的には以下の機能を作成してみる。
####S3にアップロードされたファイルを、S3にコピーするLambdaを作って見る！ 

以下、具体的な作成の流れ。

１．**"S3"** のバケットを2つ作成する
２．**"Lambda"** の作成（関数ファイルを作成するとこまで）
３．**"Lambda"** の作成（実際の処理内容の作成）
４．テスト！


## １．**"S3"** のバケットを2つ作成する
**これは特に記載する程の手順はないのだが、設定は以下の様にしている。**
※バケットを2つ作成する意図は、トリガー用のバケットと実行処理としてファイルの投げ先のバケットを分けることで、無限Loopに陥るのを避けるため。

　・バケットのバージョニング　⇒　無効
　・タグ-オプション　⇒　特になし
　・デフォルトの暗号化　⇒　無効。
　　（この暗号化は、バケットに保存されたドキュメントを自動的に暗号化してくれるらしいが、自分達が生成した暗号鍵を使うことも可能な模様。）

**バケットの作成が完了したら、片方のバケット内にテストフォルダ(test_folder1)を１つ作成してみる。**

## ２．**"Lambda"** の作成（関数ファイルを作成するとこまで）

### **"Lambda"** の作成
作成の前に、起動タイミングとして何をトリガーにしたいか？どのプログラミング言語で処理を書くのか？ でテンプレートが異なるため、まずはそれを決める必要がある。（もちろん、０からコードを書いてもいいが、基本はテンプレートを使った方が楽そう。）
　※トリガーにできるイベント一覧は **[こちら](https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html#intro-core-components-event-sources)**


#### S3への格納をトリガーにするLambdaをPythonで作成する場合
Lambdaの【関数の作成】で、『設計図の使用』> 『s3-get-object-python』のテンプレートを選択する。
![Qiita-no049_img03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/041df9f2-2333-abb8-eaa3-e9d4f1bf580a.jpeg)
**基本的な情報の設定**
![Qiita-no049_img04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/61e0e3e8-25bc-0037-bd55-841d1d118ce8.jpeg)
**S3トリガーの設定**
![Qiita-no049_img05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/7b28dc4c-ca21-2933-ba96-0caf6c032e9a.jpeg)
**上記の様に入力した後、最下部の『関数の作成』をクリック**
※無事作成が完了した後、トリガーに設定したバケットのプロパティを見てみると、イベント通知に追加されている事が確認できる。
![Qiita-no049_img06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/45be9178-3f3b-3bff-51b8-12b795911c1d.jpeg)

## ３．**"Lambda"** の作成（実際の処理内容の作成）

### 実際のLambdaの処理を作成（まずは簡単なサンプルコード）
試しに`Hellowworld`を出力する最もシンプルな処理を実行しようとしたが、単体での実行のやり方が分からず、まさかのここで躓いた。
色々調べてみると、Lambda単体ではLambda関数の実行はできない様で、他のサービスリソースで処理を実行させるトリガーを設定しないといけないらしい。（今回の場合、対象S3バケットへのオブジェクトの作成。）

```py:lambda_function.py（HellorWorld-サンプルコード）
import json

def lambda_handler(event, context):
    # TODO implement
    test_str = 'Hello World'
    return  test_str
```

テスト実行は、テストイベントよりテスト内容を作成することで実行できる。（テンプレートもあるため、作りたいテストケースに合ったテンプレートがあれば、それをベースに作れる。）
![Qiita-no049_img07.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/151c46a3-bd52-9f21-eb46-cff0f46d0b16.jpeg)
例えば、単純にJSON形式でデータを渡せているかの簡単なテストをしたい場合とかは、上記の様なテストイベントを作る。
『テスト』ボタンをクリックすると、上図の青枠部分の結果表示される。（処理内容が "Hello World" なので、上図でそのテストができてるわけではないが・・・）

#### ※処理の中身を変えて、テストしようとした時に気付いた事
**開発の進め方的には、テストイベントをいくつか試して問題なかったらデプロイって流れが一般的かと思っていたのだけど、そもそもデプロイしないとコードの改修内容が反映されないっぽい。**
**（デプロイしなくても、コンソール上のコードソースに記入すればテストはできるのかなーっと思っていたのだけど、デプロイしないと改修後コードのテストができなかった。。。）**

### 本番のLambdaの処理を作成
具体的な処理は、トリガー用のバケットにファイルが入ってきたら、そのファイルをリネームしてもう一方のバケットコピーする処理。
![Qiita-no049_img08.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/f97ec5ff-d84e-d379-2f93-dc6920b848aa.jpeg)

以下のコードが、トリガー用S3から通知があった際の処理になるのだが、ロールの権限を特に変えてないと恐らくアクセス権エラーとなる。

```py:lambda_function.py（HellorWorld-サンプルコード）
import json
import urllib.parse
import boto3

print('Loading function')

s3 = boto3.client('s3')


def lambda_handler(event, context):
    #print("Received event: " + json.dumps(event, indent=2))

    # JSON形式で受け取ったデータより、バケット情報とkey情報の抜き出し
    from_bucket = event['Records'][0]['s3']['bucket']['name']
    from_key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    
    # コピー先のバケットとファイルパスを指定
    to_bucket = 'もう片方のバケット名'
    to_filepath = from_key.split('/')[1].split('.')[0] + '_copy.txt'
    
    # 各変数を出力
    print(from_bucket)
    print(from_key)
    print(to_bucket)
    print(to_filepath)
    
    try:
        # 上記で抜き出したバケット情報とkey情報よりアップロードファイルにアクセスし、コンテンツのタイプを出力する。
        response = s3.get_object(Bucket=from_bucket, Key=from_key)
        print("CONTENT TYPE: " + response['ContentType'])
        
        # 実際のコピーコマンド
        s3.copy_object(Bucket=to_bucket, Key=to_filepath, CopySource={'Bucket': from_bucket, 'Key': from_key})
    
    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
```

アクセス権限エラーのメッセージ ↓↓↓
![Qiita-no049_img09.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/77145ae6-2cf0-009c-d586-06e9eb6bc7ec.jpeg)

上記エラーが出ている場合は、Lambdaの実行ロールの権限を確認してみる。
IAMの該当ロールを開き、S3へのアクセス権ポリシーがなければ、『ポリシーをアタッチ』する。
![Qiita-no049_img10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/2a0d2d78-d500-43da-e87b-0938ee853790.jpeg)

**s3で検索して、付与したいレベルにチェックを付けて『ポリシーのアタッチ』をクリック**　※今回はFullAccessにする。
![Qiita-no049_img11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/48c5fbca-86c3-166d-c314-c61b3e39c2e5.jpeg)


**この状態で試しにトリガー用S3バケットの`test_folder1`に何らかのファイルをアップロードすると、もう片方のバケットにそれがコピーされるはず！**


## 補足
一応、Lambdaの同時実行数には制限があるみたいで、上限値は同一アカウントの同一リージョン内で1,000までらしい。
上限に達すると、それ以上の関数の呼び出しはスロットリングされるとのこと。