S3ファイルをまとめてダウンロードできる環境を作る

やりたいこと
----------
* S3ファイルをまとめてダウンロードしたい
    * AWS CLIを使えば解決
        * その環境がない人用の環境構築

仕様
----
* AWS Lambda: Python 2.7
* Lambdaを手動で実行したらS3に圧縮して保存
    * 対象の絞り込み（bucket、key、拡張子）と出力先はLambda内に記述
        * 何パターンか必要な場合、テストイベントの設定のテンプレートに情報を保存し、そちらを使うようようにすると楽です
            * [S3ファイルをまとめてダウンロードできる環境をテストイベントの設定を利用して作る](https://qiita.com/HorieH/items/36f61636f1b6ff9a784c)
    * 想定外の大量ファイルを指定できないよう最大1000件まで(そもそも処理に使うlist_objectsの最大が1000件)
        * 1000件から先はlist_objectsのMarkerを設定して続きから処理する（名前の昇順で取得される）
* 手動が嫌な場合は下記のような方法もあります
    * S3 notifications
        * 指定したS3の場所にファイルが追加、削除などされたら実行
    * CloudWatch Events
        * cronの時間指定で実行
 

Lambdaの処理時間
------------

|ファイル数|時間(ms)|
|----:|----:|
|10|2600|
|100|17100|
|1000|177500|

※平均25KBの画像ファイル
上記の結果からLambdaのタイムアウトをを３秒→３分に設定しました

AWS料金
------
* 月数十回なのでLambdaは無料範囲内
* S3のダウンロード料金で数十円程度

Lambda
-------

### 関数の作成
  * 名前: s3downloads
  * ランタイム: Python 2.7
  * ロール: テンプレートから新しいロールを作成
  * ロール名: Lambda_s3downloads
  * ポリシーテンプレート: 空欄（下記のIAMロールで追加）

### IAM ロール
  * Lambda_s3downloadsロールを選択
  * ポリシーのアタッチから「AmazonS3FullAccess」を追加する
    * ※対象のS3を限定的にする場合はカスタムしてください

### Lambdaのその他の設定
  * タイムアウト３秒から3分に変更
  * index.jsをlambda_function.pyにRename
  * コードの内容は下記のPythonコードを貼り付け
  * テストイベントは今回は使用しないのでデフォルトのまま保存して実行します
 
Pythonのコード
------------

```python:lambda_function.py
import boto3
import tarfile
import os

s3_client = boto3.client('s3')

in_bucket = 's3inbucket'
in_prefix = 'hoge/neko/'
in_extension = '.jpg'
in_max = 1000
in_marker = ''

out_bucket = 's3outbucket'
out_dir = 'out/'
out_filenmae = 'archive'


def lambda_handler(event, context):
    response = s3_client.list_objects(Bucket=in_bucket, Prefix=in_prefix, MaxKeys=in_max, Marker=in_marker)
    if 'Contents' not in response:
        return

    s3_files = [c['Key'] for c in response['Contents'] if c['Key'].endswith(in_extension)]

    if s3_files:
        get_s3_files(s3_files)
        create_archive()
        return s3_files


def get_s3_files(s3_files):
    for s3_file in s3_files:
        basename = os.path.basename(s3_file)
        tmp_file = os.path.join('/tmp/', basename)
        s3_client.download_file(in_bucket, s3_file, tmp_file)


def create_archive():
    out_file = os.path.join(out_dir, out_filenmae + '.tar.gz')
    archive_file = os.path.join('/tmp/', out_filenmae + '.tar.gz')

    archive = tarfile.open(archive_file, mode='w:gz')
    archive.add('/tmp/', arcname=out_filenmae)
    archive.close()

    s3_client.upload_file(archive_file, out_bucket, out_file)
    print('output ' + out_bucket + ':' + out_file)
```

### 変数の設定

|変数|説明|
|---|---|
|in_bucket|対象のバケット|
|in_prefix|対象のバケットのキー|
|in_extension|対象の拡張子(空の場合は全て)|
|in_max|最大取得数(0〜1000)|
|in_marker|取得数を超えた場合、最後のキー(フルパス)をここに記述することで続きからの処理になる。※対象のフルパス一覧はテスト結果の詳細に出力されます|
|out_bucket|出力先のバケット|
|out_dir|出力先のディレクトリ|
|out_filenmae|圧縮ファイル名|

### Prefixで指定した階層だけを対象にしたい場合
下記のように「Delimiter='/'」を追加する

```python
response = s3_client.list_objects(Bucket=in_bucket, Prefix=in_prefix, Delimiter='/', MaxKeys=in_max, Marker=in_marker)
```

毎回はまるところ
------------

コメントに日本語が含まれているとエラー・・・


### 2019/08/13 追記
先頭に下記を記述することで日本語コメントをつけることができるようになる（emoseiさん教えていただきありがとうございます）

```
## coding: UTF-8
```


応用
----
[S3ファイルをまとめてダウンロードできる環境をテストイベントの設定を利用して作る](https://qiita.com/HorieH/items/36f61636f1b6ff9a784c)


参考
----
[Boto 3 で Amazon S3 上の key を取得する方法、実装例、注意点](https://qiita.com/ikai/items/38c52e0c459792a4da46)