Amazon S3 と AWS Lambda を使って PPAP を回避します。

みなさん、PPAP をご存知ですか？
以前、流行した、ﾍﾟﾝﾊﾟｲﾅﾎﾟｰｱｯﾎﾟｰﾍﾟﾝ ではなく...

>PasswordつきZIP暗号化ファイルを送ります
>Passwordを送ります
>Aん号化（暗号化）
>Protocol

のことです。
https://www.jaipa.or.jp/event/isp_mtg/asahikawa_190912-13/190913-3.pdf

つまり、社外に添付ファイルを送る際に使われるプロトコルです。
このプロトコルにはいろいろと問題があるといわれています。
例えば、パスワードを送り忘れたら受信者はいつまでたっても開けません。
受信者はパスワード入りメールがくるまで待ってないといけません。
そして、そもそも、メールでパスワードを送ってよいのかしら。。。

そんな PPAP を使わないで済む仕組みを Amazon S3 と AWS Lambda で作りました。

## 予備知識
Amazon S3 には事前署名 URL というものを生成する機能があります。
これを使うことで、期間を指定して(執筆時点で最大7日)、URLを知ってる人だけが HTTPS でファイルを取得できます。
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/ShareObjectPreSignedURL.html

## 本記事のようなものを作ると嬉しい点
事前署名 URL を作るには、aws cli の presign コマンドを使うことが多いです。
その場合以下のステップになりますが、最初の S3 への格納だけでいい！というラクチンな運用になります。

1. S3 にファイルを格納
2. S3 のファイルパスを確認(s3://<bucket-name>/<objectname>)
3. EC2 へログイン（設定済みなら手元のパソコンでもOK)
4. aws presign コマンドを実行する

## レシピ
### 登場人物
- Amazon S3
 - 送りたいファイルを格納する場所
 - ファイルが格納されたことを AWS Lambda に通知
- Amazon SNS
 - 処理が完了したら指定メールアドレスに情報を送信。
 - その情報を社外へ送信するメールにコピペする。
- AWS IAM
 - アクセスを制御するサービス。
 - 種類に応じて後述のとおり事前署名 URL の有効期限の最大長が決まる
- AWS SSM
 - パラメータストア機能を使って、IAM のシークレットアクセスキーを保管
- AWS Lambda
 - Amazon S3 にファイルが格納されたことを検知して、処理実施



### 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/d2a063fa-6be0-0706-a02c-b232fbd91027.png)


### Amazon S3 バケットを作る
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/01e945fc-41a1-3e02-3295-25bf69b0ad29.png)

必要に応じて、デフォルト暗号化など各種設定を有効に！

### Amazon SNS の設定
#### SNS トピックの作成
Amazon SNS のトピックをまず作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/1c656686-3035-c502-55c0-29714892dae2.png)
必要に応じて、暗号化やアクセスポリシーなどの設定をしてくださいね！

#### サブスクリプションの設定
1. 作成した SNS トピックの詳細を開き、**サブスクリプションの作成**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/e42ec267-4b8b-6785-bee7-cf52a205ae5f.png)


2. プロトコルを**Eメール**、エンドポイントに処理結果を伝える**メールアドレス**(例：自分のアドレス)を指定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/7a569b54-e573-77ed-7987-af896d07271f.png)

3. 指定したメールアドレスに以下のようなメールが着弾するので、**Confirm subscription**リンクをクリックする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/dedbc99b-f40f-3b1b-b0e3-8287386390dc.png)


4. ブラウザが開き以下のような画面が表示されれば設定完了
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/813fcb04-55eb-4376-6c61-7e34c92ebdc4.png)
※ click here to unsubscribe と記載されているリンクはクリックしないでください。せっかく登録したメールアドレスの設定が解除されます。


### 認証情報ごとの事前署名 URL の最大有効期限
事前署名 URL は作成時に有効期限が決められます。この期限は、作成時に使った認証情報によって最大値が決まります。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/presigned-url-s3-bucket-expiration/

>- AWS Identity and Access Management (IAM) インスタンスプロファイル: 最大 6 時間有効
>- AWS Security Token Service (STS): 最大 36 時間有効 (AWS アカウントユーザーや IAM ユーザーの認証情報など、永続的認証情報を使用して署名した場合)
>- IAM ユーザー: 最大 7 日間有効 (AWS 署名バージョン 4 を使用した場合)


お好みの情報を使えばよいですが、使い勝手を考えると IAM ユーザー の情報を使うのがよろしいかと存じます。
※例えば、STS で36時間とした場合でも1日半しかもたないため、土日を挟む恐れがあるとか、受信側が開くのが遅れたりすると取得できないといったことになります。

というわけで、本記事では IAM ユーザー を利用することとします。

### IAM ユーザーの設定
1. IAM ユーザーの作成画面を呼び出し、ユーザー名を指定するとともに、**アクセスの種類**で**プログラムによるアクセス**にチェック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/1b56e524-e257-5c38-8a40-5c6c84870c50.png)
**次のステップ:アクセス権限** ボタンをクリックして進む

2. **グループの作成**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/10f2da24-9847-7251-259c-49d57ba3c0d0.png)

3. グループ名を任意の名称で指定し、**ポリシーの作成**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/5cd17665-8d1e-2b48-a6fe-8ca1594ef689.png)

4. **JSON**タブを選択し、後述のJSON(バケット名:your-bucket-nameを適宜置き換え)をコピペし、**ポリシーの確認**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/bdfc921e-0362-dcfc-a87c-9fe604836212.png)

```json:policy-create-preSign
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::your-bucket-name/*"
        }
    ]
}
```

5．ポリシー名として**名前**に任意の名称を指定し、**ポリシーの作成**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/b072e9ce-ad4d-2a96-9b06-9d4fd47c313e.png)

6．IAMグループの作成画面(ブラウザタブなど)に戻り、**更新**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/ae3a369f-30cd-9cac-ddf0-1c105655ed12.png)

7．作成したポリシーを選択して、**グループの作成**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/8c837ede-4cfc-6a0d-e5ac-1f6f0464cd00.png)

8．IAMユーザー作成の画面に戻ってくるので、**次のステップ:タグ**ボタンをクリック
9．タグの指定は任意。**次のステップ:確認**ボタンをクリック
10．内容を確認して、問題なければ**ユーザーの作成**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/202386db-3708-6f8b-545c-10ab7ec19ee2.png)

11．アクセスキーIDとシークレットアクセスキーを控える
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/5e5b0f5e-0f7a-4882-806c-d95b3dda00cd.png)
シークレットアクセスキーは**表示**リンクをクリックすることで表示される。
保存しておきたい場合は**.csvのダウンロード**ボタンをクリック


### 認証情報を AWS Systems Manager パラメータストアに格納する

1. AWS Systems Manager の**パラメータストア**を開き、**パラメータの作成**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/888dac7e-b438-741f-a9ae-3e933a7c1dce.png)

2. 名前、安全な文字列、値に控えておいたシークレットアクセスキーを指定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/05d1fdba-4130-db47-e693-d0beb7f92b2d.png)
名前は IAM ユーザー名としました。アクセスキーIDでもよいかもしれません。お好みの方法で。。

3. **パラメータの作成**ボタンをクリック
4. **必要に応じて**、ほかのユーザーがこのパラメータへアクセスできないように、 IAM ポリシーの見直しなどを行ってください。

### AWS Lambda の設定
#### Lambda 関数の設定
以下のサンプルコードを参考に Lambda 関数を設定・保存する。
エラーハンドリングなどは適宜入れてください。

```python:lambda_function.py
import json
import boto3
import os
from datetime import datetime, timezone, timedelta
import urllib.parse

JST = timezone(timedelta(hours=+9), 'JST')

iam=boto3.client('iam')
sns=boto3.client('sns')
ssm=boto3.client('ssm')

userName=os.environ['USERNAME']
# 指定した値*1日の秒数(60*60*24)
expire=int(os.environ['EXPIRE'])*60*60*24
topic_arn=os.environ['TOPIC_ARN']

def lambda_handler(event, context):
    # PUTされたファイルがゼロバイトの場合は処理終了
    size=event['Records'][0]['s3']['object']['size']
    if size==0:
        print("対象外のイベント")
        return
    
    # S3 Put イベントの中身を取得    
    bucketName=event['Records'][0]['s3']['bucket']['name']
    objectKey=urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'])
    
    accessKeyId = get_access_key(userName)
    
    if accessKeyId == 'nothing':
        print('AccessKeyがありません')
        msg('指定しているIAM ユーザーにAccessKeyが存在しません。')
        send_email(msg)
        return
    
    secretAccessKey = get_parameter(userName)

    s3 = boto3.client('s3', aws_access_key_id=accessKeyId, aws_secret_access_key=secretAccessKey)
    
    request = s3.generate_presigned_url(
        ClientMethod = 'get_object',
        Params = {
            'Bucket' : bucketName,
            'Key' : objectKey
        },
        ExpiresIn = expire,
        HttpMethod = 'GET'
    )
    
    expireEpoch = request[-10:]
    expireTime = datetime.fromtimestamp(int(expireEpoch), JST)
    
    msg = []
    msg.append("★事前署名URL対象ファイル：\r\n"+objectKey)
    msg.append("\r\n★有効期限(日本時間):\r\n"+str(expireTime))
    msg.append("\r\n★事前署名URL：\r\n"+request)

    send_email(msg)

    return

def get_access_key(userName):
    # Status が Active で作成日時が一番新しいアクセスキーを返す
    
    request = iam.list_access_keys(
        UserName=userName,
    )
    
    createDate =  datetime(2000, 1, 1, 0, 0, 0)
    accessKeyId = "nothing"
    
    for i in request['AccessKeyMetadata']:
        if i['Status'] != 'Active':
            return 'noAccessKey'
        date = str(i['CreateDate'])[0:19]
        date = datetime.strptime(date, "%Y-%m-%d %H:%M:%S")

        if createDate <= date:
            createDate = date
            accessKeyId = i['AccessKeyId']
    
    return accessKeyId


def send_email(msg):
    # SNS 経由でメールを送信する。
    request = {
        'TopicArn': topic_arn,
        'Message': '\r\n'.join(msg),
        'Subject': 'S3 PreSignUrl for you'
    }

    response = sns.publish(**request)
    
    return response

def get_parameter(userName):
    response = ssm.get_parameter(
        Name=userName,
        WithDecryption=True
    )
    
    return response['Parameter']['Value']
```

#### 環境変数
- USERNAME
 - 作成した IAM ユーザー名を指定 
- EXPIRE
 - 有効期限を日数で指定(MAX 7日)
- TOPIC_ARN
 - 作成した SNS トピックの ARN を指定 

#### トリガー の設定
1. Lambda 関数の Designer に表示辞されている**+ トリガーを追加**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/95a45ec8-63f1-bac1-2806-20c19887908c.png)

2. トリガーの選択で **S3** を選択する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/8d91d173-6acb-c716-b82d-66b34310c00f.png)

3. バケットやイベントタイプといった設定を行う
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/2725177b-7504-229a-0553-1bdf9fb952ce.png)

4. トリガーの有効化にチェックが入っていることを確認して、**追加**ボタンをクリック


## 試してみよう
1. S3 にファイル格納するファイルを作る
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/4158a7a4-9f73-fdfa-0c09-544f07b3ce01.png)

2. test.txt ファイルを S3 にドラッグアンドドロップして、**アップロード**ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/6e41ffbe-dcc3-f8e4-501f-23b97aa95157.png)

3. ちょっと待つ
4. メールボックスを確認！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/f758f022-9862-2553-647c-ca5b1bb99f07.png)

5. 本文を確認！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/42dbd135-6636-ea65-fc1e-5a1239c79646.png)

6. リンクをクリック！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/99408f7e-e86a-b390-024b-e6a38c358bab.png)

7. あとはこのURLを、ファイル送付したい先方宛てメールにコピペすればOK！

### ちなみに、期限が切れると・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/9ad6f944-6b3d-0841-d8b1-d3e99706fbd9.png)


## この後は
AWS Transfer for SFTP や Amazon API Gateway などと組み合わせて、 Amazon S3 へのファイルアップロードを簡略化していくとさらに使い勝手があがります！

## ちなみに
こんな仕組みが使えるようになったらいいなぁ、と
妄想しながら作りました。お察しください。

## 参考資料
AWS Lambda を使ってS3の署名付きURLを自動的に発行する
https://qiita.com/aquaviter/items/ae32b24ccef491bf9e7d