[JAWS-UG CLI] Lambda:#19 Lambda関数の作成 (inbound-ses-spam-filter: Python版)


前提条件
========


Lambdaへの権限
--------------

Lambdaに対してフル権限があること。


AWS CLI
-------

以下のバージョンで動作確認済

* AWS CLI 1.10.63

```bash:コマンド:
aws --version
```

```text:結果(例):
      aws-cli/1.10.63 Python/2.7.11 Darwin/15.6.0 botocore/1.4.53
```

バージョンが古い場合は最新版に更新しましょう。

```bash:コマンド:
sudo -H pip install -U awscli
```


IAM Role
--------

'inbound-ses-spam-filter'ロールが存在すること。


0. 準備
=======


0.1. リージョンの決定
---------------------

```bash:変数の設定
export AWS_DEFAULT_REGION='us-west-2'
```


0.2. 変数の確認
---------------

プロファイルが想定のものになっていることを確認します。

```bash:変数の確認:
aws configure list
```

```text:結果(例):
            Name                    Value             Type    Location
            ----                    -----             ----    --------
         profile       lambdaFull-prjz-mbp13        env    AWS_DEFAULT_PROFILE
      access_key     ****************XXXX shared-credentials-file
      secret_key     ****************XXXX shared-credentials-file
          region        us-west-2        env    AWS_DEFAULT_REGION
```


0.3. IAM Roleの指定
-------------------

```bash:変数の設定:
IAM_ROLE_NAME='inbound-ses-spam-filter'
```

```bash:コマンド:
aws iam get-role \
         --role-name ${IAM_ROLE_NAME}
```

```json:結果(例):
      {
          "Role": {
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Action": "sts:AssumeRole",
                        "Principal": {
                            "Service": "lambda.amazonaws.com"
                        },
                        "Effect": "Allow",
                        "Sid": ""
                    }
                ]
            },
            "RoleId": "AROAXXXXXXXXXXXXXXXXX",
            "CreateDate": "2016-09-10T01:23:45Z",
            "RoleName": "inbound-ses-spam-filter",
            "Path": "/",
            "Arn": "arn:aws:iam::XXXXXXXXXXXX:role/inbound-ses-spam-filter"
          }
      }
```


1. 事前作業
===========


1.1. AWS IDの取得
-----------------

```bash:変数の設定:
AWS_ID=$( \
        aws sts get-caller-identity \
          --query 'Account' \
          --output text \
) && echo ${AWS_ID}
```

```text:結果(例)
      XXXXXXXXXXXX
```


1.1. IAM RoleのARN取得
----------------------

```bash:コマンド:
IAM_ROLE_ARN=$( \
        aws iam get-role \
          --role-name ${IAM_ROLE_NAME} \
          --query 'Role.Arn' \
          --output text \
) \
        && echo ${IAM_ROLE_ARN}
```

```text:結果(例):
      arn:aws:iam::XXXXXXXXXXXX:role/inbound-ses-spam-filter
```


1.2. SESで利用するドメインの指定
--------------------------------

```bash:変数の設定:
SES_IDENTITY='<SESで利用するドメイン名>'
```


1.3. Lambda関数名の決定
-----------------------

```bash:変数の設定:
LAMBDA_FUNC_NAME="inbound-ses-spam-filter-python-$( date '+%Y%m%d' )" \
        && echo ${LAMBDA_FUNC_NAME}
```

同名のLambda関数の不存在確認

```bash:コマンド:
aws lambda get-function \
        --function-name ${LAMBDA_FUNC_NAME}
```

```text:結果(例):
      A client error (ResourceNotFoundException) occurred when calling the GetFunction operation: Function not found: arn:aws:lambda:us-west-2:XXXXXXXXXXXX:function:inbound-ses-spam-filter-python-20160912
```


1.4. Lambda関数
---------------

```bash:変数の設定:
FILE_LAMBDA_FUNC="${LAMBDA_FUNC_NAME}.py"
PY_FUNC_NAME='lambda_handler'
```

```bash:変数の確認:
cat << ETX

          FILE_LAMBDA_FUNC: ${FILE_LAMBDA_FUNC}
          PY_FUNC_NAME:     ${PY_FUNC_NAME}
          SES_IDENTITY:     ${SES_IDENTITY}

ETX
```

```bash:コマンド:
cat << EOF > ${FILE_LAMBDA_FUNC}
from __future__ import print_function
from datetime import datetime
import json
import boto3


def print_with_timestamp(*args):
    print(datetime.utcnow().isoformat(), *args)


def ${PY_FUNC_NAME}(event, context):
    print_with_timestamp('Starting - inbound-ses-spam-filter')

    ses_notification = event['Records'][0]['ses']
    message_id = ses_notification['mail']['messageId']
    receipt = ses_notification['receipt']

    print_with_timestamp('Processing message:', message_id)

    # Check if any spam check failed
    if (receipt['spfVerdict']['status'] == 'FAIL' or
            receipt['dkimVerdict']['status'] == 'FAIL' or
            receipt['spamVerdict']['status'] == 'FAIL' or
            receipt['virusVerdict']['status'] == 'FAIL'):

        send_bounce_params = {
            'OriginalMessageId': message_id,
            'BounceSender': 'mailer-daemon@${SES_IDENTITY}',
            'MessageDsn': {
                'ReportingMta': 'dns; ${SES_IDENTITY}',
                'ArrivalDate': datetime.now().isoformat()
            },
            'BouncedRecipientInfoList': []
        }

        for recipient in receipt['recipients']:
            send_bounce_params['BouncedRecipientInfoList'].append({
                'Recipient': recipient,
                'BounceType': 'ContentRejected'
            })

        print_with_timestamp('Bouncing message with parameters:')
        print_with_timestamp(json.dumps(send_bounce_params))

        try:
            ses_client = boto3.client('ses')
            bounceResponse = ses_client.send_bounce(**send_bounce_params)
            print_with_timestamp('Bounce for message ', message_id, ' sent, bounce message ID: ', bounceResponse['MessageId'])
            return {'disposition': 'stop_rule_set'}
        except Exception as e:
            print_with_timestamp(e)
            print_with_timestamp('An error occurred while sending bounce for message: ', message_id)
            raise e
    else:
        print_with_timestamp('Accepting message:', message_id)
EOF

cat ${FILE_LAMBDA_FUNC}
```

```bash:コマンド:
zip ${LAMBDA_FUNC_NAME}.zip ${FILE_LAMBDA_FUNC}
```

```text:結果(例):
      adding: inbound-ses-spam-filter-python-20160912.py (deflated 43%)
```


2. Lambda関数の作成
===================

```bash:変数の設定:
LAMBDA_FUNC_DESC='A simple email filter for protection against spam and viruses as well as DKIM and SPF failures.'
LAMBDA_RUNTIME='python2.7'
LAMBDA_HANDLER="${LAMBDA_FUNC_NAME}.${PY_FUNC_NAME}"
FILE_LAMBDA_ZIP="${LAMBDA_FUNC_NAME}.zip"
```

```bash:変数の確認:
cat << ETX

        LAMBDA_FUNC_NAME:  ${LAMBDA_FUNC_NAME}
        LAMBDA_FUNC_DESC: "${LAMBDA_FUNC_DESC}"
        LAMBDA_RUNTIME:    ${LAMBDA_RUNTIME}
        FILE_LAMBDA_ZIP    ${FILE_LAMBDA_ZIP}
        IAM_ROLE_ARN:      ${IAM_ROLE_ARN}
        LAMBDA_HANDLER:    ${LAMBDA_HANDLER}

ETX
```

```bash:コマンド:
aws lambda create-function \
        --function-name ${LAMBDA_FUNC_NAME} \
        --description "${LAMBDA_FUNC_DESC}" \
        --zip-file fileb://${FILE_LAMBDA_ZIP} \
        --runtime ${LAMBDA_RUNTIME} \
        --role ${IAM_ROLE_ARN} \
        --handler ${LAMBDA_HANDLER}
```

```json:結果(例):
      {
        "CodeSha256": "tRyAvhC8oug6kZtrpkSUtm79lqNmjrQP9K1oY88MxEo=",
        "FunctionName": "inbound-ses-spam-filter-python-20160912",
        "CodeSize": 970,
        "MemorySize": 128,
        "FunctionArn": "arn:aws:lambda:us-west-2:XXXXXXXXXXXX:function:inbound-ses-spam-filter-python-20160912",
        "Version": "$LATEST",
        "Role": "arn:aws:iam::XXXXXXXXXXXX:role/inbound-ses-spam-filter",
        "Timeout": 10,
        "LastModified": "2016-09-10T01:23:45.678+0000",
        "Handler": "inbound-ses-spam-filter-python-20160912.lambda_handler",
        "Runtime": "python2.7",
        "Description": "A simple email filter for protection against spam and viruses as well as DKIM and SPF failures."
      }
```

```bash:コマンド:
aws lambda get-function \
        --function-name ${LAMBDA_FUNC_NAME}
```

```json:結果(例):
      {
        "Code": {
          "RepositoryType": "S3",
          "Location": "https://awslambda-ap-ne-1-tasks.s3-ap-northeast-1.amazonaws.com/snapshots/XXXXXXXXXXXX/HelloWorld-2979ba79-b08f-495d-9ee6-46397c95ba13?x-amz-security-token=AQoDYXdzEDoa8AMR6t8h66eOXhN3%2Fx7XpuRxvf7pVn7IuWV4cEmwx0CtZT6yxCJ1%2BWmigYXqGoyQHuBYOWnxbhmwEcTg839qMuhSu1fk0fXpXf0oJOLkhKMudNqhdElyFQpzyT6Q8GDfhAsfbX9wvwCDTty4imxz7MczF%2FQl6tgvTYdip08ap5fAyrknZGV1%2B1Ggnp5w6JOjydYxuUsWwhoxoEWzi7SoVTmpRQQA91c4VW9lNotOAHACFxo6klzDPM8mxR9RJl66WxFugL0wQJyLUpmtjS9XoArD86sEWWiIccMpV2BQipTPQlzL%2F1Hoy%2BDF6QUxyPUihlDjPBoJTISTP8W1wxmzW%2BLbilAfFQRPY7CFjzR0k%2FA%2FIX5x9iyz52Pu1Q0ASTw1l%2Fq%2Fo3pRbvzWR79QS%2BpxXrwbYzoQHKiK62DSTsQo5tqKPsiDCYzrPxbq8lm7pNBPG%2FsxjePRWBVJeRl08WxEjSjoRRwBOPX5mz1BCUoUBPGG5tEENp87A%2FCdDgibFWM5DdYhwtaYPY7FTmi8DvqjQHL9jOmP8YuVteBTBcv8nFW6UbErPjwwn79FKG1u5M9HoTWUqUMBByz6D4tTRSEw6iJU7XdCujFnhnHe5V8imZ1KGI7fDWpciJhrhml0wnKPCK%2Fe9lK1P2kO7ldSWc7zn5hcIOD2tbEF&AWSAccessKeyId=ASIAJFVALOKV5SJVYPPA&Expires=1445825978&Signature=bvwu1Ny34LgTmZeOO3q4sn7x3Fg%3D"
        },
        "Configuration": {
          "Version": "$LATEST",
          "CodeSha256": "tRyAvhC8oug6kZtrpkSUtm79lqNmjrQP9K1oY88MxEo=",
          "FunctionName": "inbound-ses-spam-filter-python-20160912",
          "MemorySize": 128,
          "CodeSize": 350,
          "FunctionArn": "arn:aws:lambda:us-west-2:XXXXXXXXXXXX:function:inbound-ses-spam-filter-python-20160912",
          "Handler": "inbound-ses-spam-filter-python-20160912.lambda_handler",
          "Role": "arn:aws:iam::XXXXXXXXXXXX:role/inbound-ses-spam-filter",
          "Timeout": 10,
          "LastModified": "2016-09-10T01:23:45.678+0000",
          "Runtime": "python2.7",
          "Description": "A simple email filter for protection against spam and viruses as well as DKIM and SPF failures."
        }
      }
```


3. Lambda関数の動作確認
=======================


3.1. サンプルデータの作成
-------------------------

```bash:変数の設定:
FILE_INPUT="${LAMBDA_FUNC_NAME}-data.json" \
        && echo ${FILE_INPUT}
```

```bash:変数の確認:
cat << ETX

        FILE_INPUT:         ${FILE_INPUT}
        AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION}

ETX
```

```bash:サンプルデータ:
cat << EOF > ${FILE_INPUT}
{
        "Records": [
          {
            "eventVersion": "1.0",
            "ses": {
              "mail": {
                "commonHeaders": {
                  "from": [
                    "Jane Doe <janedoe@example.com>"
                  ],
                  "to": [
                    "johndoe@example.com"
                  ],
                  "returnPath": "janedoe@example.com",
                  "messageId": "<0123456789example.com>",
                  "date": "Wed, 7 Oct 2015 12:34:56 -0700",
                  "subject": "Test Subject"
                },
                "source": "janedoe@example.com",
                "timestamp": "1970-01-01T00:00:00.000Z",
                "destination": [
                  "johndoe@example.com"
                ],
                "headers": [
                  {
                    "name": "Return-Path",
                    "value": "<janedoe@example.com>"
                  },
                  {
                    "name": "Received",
                    "value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.${AWS_DEFAULT_REGION}.amazonaws.com with SMTP id o3vrnil0e2ic28trm7dfhrc2v0cnbeccl4nbp0g1 for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)"
                  },
                  {
                    "name": "DKIM-Signature",
                    "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=example; h=mime-version:from:date:message-id:subject:to:content-type; bh=jX3F0bCAI7sIbkHyy3mLYO28ieDQz2R0P8HwQkklFj4=; b=sQwJ+LMe9RjkesGu+vqU56asvMhrLRRYrWCbVt6WJulueecwfEwRf9JVWgkBTKiL6m2hr70xDbPWDhtLdLO+jB3hzjVnXwK3pYIOHw3vxG6NtJ6o61XSUwjEsp9tdyxQjZf2HNYee873832l3K1EeSXKzxYk9Pwqcpi3dMC74ct9GukjIevf1H46hm1L2d9VYTL0LGZGHOAyMnHmEGB8ZExWbI+k6khpurTQQ4sp4PZPRlgHtnj3Zzv7nmpTo7dtPG5z5S9J+L+Ba7dixT0jn3HuhaJ9b+VThboo4YfsX9PMNhWWxGjVksSFOcGluPO7QutCPyoY4gbxtwkN9W69HA=="
                  },
                  {
                    "name": "MIME-Version",
                    "value": "1.0"
                  },
                  {
                    "name": "From",
                    "value": "Jane Doe <janedoe@example.com>"
                  },
                  {
                    "name": "Date",
                    "value": "Wed, 7 Oct 2015 12:34:56 -0700"
                  },
                  {
                    "name": "Message-ID",
                    "value": "<0123456789example.com>"
                  },
                  {
                    "name": "Subject",
                    "value": "Test Subject"
                  },
                  {
                    "name": "To",
                    "value": "johndoe@example.com"
                  },
                  {
                    "name": "Content-Type",
                    "value": "text/plain; charset=UTF-8"
                  }
                ],
                "headersTruncated": false,
                "messageId": "o3vrnil0e2ic28trm7dfhrc2v0clambda4nbp0g1"
              },
              "receipt": {
                "recipients": [
                  "johndoe@example.com"
                ],
                "timestamp": "1970-01-01T00:00:00.000Z",
                "spamVerdict": {
                  "status": "PASS"
                },
                "dkimVerdict": {
                  "status": "PASS"
                },
                "processingTimeMillis": 574,
                "action": {
                  "type": "Lambda",
                  "invocationType": "Event",
                  "functionArn": "arn:aws:lambda:${AWS_DEFAULT_REGION}:012345678912:function:Example"
                },
                "spfVerdict": {
                  "status": "PASS"
                },
                "virusVerdict": {
                  "status": "PASS"
                }
              }
            },
            "eventSource": "aws:ses"
          }
        ]
}
EOF

cat ${FILE_INPUT}
```

JSONファイルを作成したら、フォーマットが壊れてないか必ず確認します。

```bash:コマンド:
jsonlint -q ${FILE_INPUT}
```

エラーが出力されなければOKです。


3.2. lambda関数の手動実行
-------------------------

```bash:変数の設定:
FILE_OUTPUT_LAMBDA="${LAMBDA_FUNC_NAME}-out.txt"
FILE_LOG_LAMBDA="${LAMBDA_FUNC_NAME}-$(date +%Y%m%d%H%M%S).log"
```

```bash:変数の確認:
cat << ETX

        LAMBDA_FUNC_NAME:   ${LAMBDA_FUNC_NAME}
        FILE_INPUT:         ${FILE_INPUT}
        FILE_OUTPUT_LAMBDA: ${FILE_OUTPUT_LAMBDA}
        FILE_LOG_LAMBDA:    ${FILE_LOG_LAMBDA}

ETX
```

```bash:コマンド:
aws lambda invoke \
        --function-name ${LAMBDA_FUNC_NAME} \
        --log-type Tail \
        --payload file://${FILE_INPUT} \
        ${FILE_OUTPUT_LAMBDA} \
        > ${FILE_LOG_LAMBDA}
```

```bash:コマンド:
cat ${FILE_LOG_LAMBDA} \
        | jp.py 'StatusCode'
```

```text:結果(例):
      200
```


3.3. lambda関数の実行結果の確認
-------------------------------

```bash:コマンド:
cat ${FILE_OUTPUT_LAMBDA}
```

```text:結果(例):
      null
```


3.4. lambda関数のログの確認
---------------------------

```bash:コマンド:
cat ${FILE_LOG_LAMBDA} \
        | jp.py 'LogResult' \
        | sed 's/"//' \
        | base64 --decode
```

```text:結果(例):
      START RequestId: c1d2232a-75a0-11e6-a600-25217ddb12d8 Version: $LATEST
      2016-09-10T08:46:42.782421 Starting - inbound-ses-spam-filter
      2016-09-10T08:46:42.782481 Processing message: o3vrnil0e2ic28trm7dfhrc2v0clambda4nbp0g1
      2016-09-10T08:46:42.782547 Accepting message: o3vrnil0e2ic28trm7dfhrc2v0clambda4nbp0g1
      END RequestId: c1d2232a-75a0-11e6-a600-25217ddb12d8
      REPORT RequestId: c1d2232a-75a0-11e6-a600-25217ddb12d8      Duration: 0.63 ms       Billed Duration: 100 ms         Memory Size: 128 MB     Max Memory Used: 43 MB
```


4. permission: SES
==================

```bash:変数の設定
LAMBDA_STAT_ID='AllowSES'
LAMBDA_PERMIT_ACTION='lambda:InvokeFunction'
LAMBDA_PERMIT_PRINCIPAL='ses.amazonaws.com'
SOURCE_AWS_ID=${AWS_ID}
```

```bash:変数の確認:
cat << ETX

        LAMBDA_FUNC_NAME:        ${LAMBDA_FUNC_NAME}
        LAMBDA_STAT_ID:          ${LAMBDA_STAT_ID}
        LAMBDA_PERMIT_ACTION:    ${LAMBDA_PERMIT_ACTION}
        LAMBDA_PERMIT_PRINCIPAL: ${LAMBDA_PERMIT_PRINCIPAL}
        SOURCE_AWS_ID:           ${SOURCE_AWS_ID}

ETX
```

```bash:コマンド:
aws lambda add-permission \
        --function-name ${LAMBDA_FUNC_NAME} \
        --statement-id ${LAMBDA_STAT_ID} \
        --action ${LAMBDA_PERMIT_ACTION} \
        --principal ${LAMBDA_PERMIT_PRINCIPAL} \
        --source-account ${SOURCE_AWS_ID}
```

```json:結果(例):
      {
        "Statement": "{"Condition":{"StringEquals":{"AWS:SourceAccount":"XXXXXXXXXXXX"}},"Action":["lambda:InvokeFunction"],"Resource":"arn:aws:lambda:us-west-2:XXXXXXXXXXXX:function:inbound-ses-spam-filter-python-20160912","Effect":"Allow","Principal":{"Service":"ses.amazonaws.com"},"Sid":"AllowSES"}"
      }
```

```bash:コマンド:
aws lambda get-policy \
        --function-name ${LAMBDA_FUNC_NAME} \
        | sed 's/\\//g' | sed 's/\"{/{/' | sed 's/\"$//' 
```

```json:結果(例):
      {
        "Policy": {"Version":"2012-10-17","Statement":[{"Condition":{"StringEquals":{"AWS:SourceAccount":"XXXXXXXXXXXX"}},"Action":"lambda:InvokeFunction","Resource":"arn:aws:lambda:us-west-2:XXXXXXXXXXXX:function:inbound-ses-spam-filter-python-20160912","Effect":"Allow","Principal":{"Service":"ses.amazonaws.com"},"Sid":"AllowSES"}],"Id":"default"}
      }
```


完了
====