大量のメールを送信する為にEC2インスタンスを自動で起動・停止する

## 背景
大量のメールを短時間で送信し切る為に、送信時にハイスペックな専用のbatchサーバを起動し、並列数を多くして送信処理を行なう必要がありました。
これまではサーバの起動を手動で行なっていたのですが、メールの一斉送信の頻度が増えたので、SQSとLambdaを使って一斉送信時に自動でサーバを起動し、送信後にはサーバが停止されるようにしました。

## 構成

![mail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/804583c4-fa5b-eab9-d155-374ed31631f8.png)


1. 一斉送信メールが設定されるとkickerが検知して、SQSにEC2起動のキューを投げます
2. SQSにキューが投げられるとlambdaの関数が実行され、一斉送信専用batchサーバが起動します
3. batchサーバは起動と同時に一斉送信メールの設定を読み込みメール送信処理を開始します
4. メール送信処理が完了すると、SQSにEC2停止のキューを投げます
5. SQSにキューが投げられるとlambdaの関数が実行され、一斉送信専用batchサーバが停止します

## SQSの設定

新しいキューの作成から、キューを作成します。

EC2起動用のキュー
<img width="1512" alt="スクリーンショット 2019-08-17 17.12.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/ed04a798-1311-2319-a606-4dcdc650ee08.png">

EC2停止用のキュー
<img width="1520" alt="スクリーンショット 2019-08-17 17.13.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/c10be148-c91a-c25d-0df2-fb0a91c9df83.png">


## lambdaの設定

まずlambda用の実行ロールを定義します。
ロールの作成で、Lambdaを選択します。

<img width="1052" alt="スクリーンショット 2019-08-17 16.43.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/391fe432-833c-c178-dd63-6c30afbe7746.png">

ポリシーにはAmazonSQSFullAccessとLambdaEC2Controlを指定して作成します。

<img width="1045" alt="スクリーンショット 2019-08-17 16.47.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/794a8fbb-7e8a-1df5-0a07-dc8d379dba28.png">

LambdaEC2ControlはEC2の起動・停止を定義したポリシーです。
以下の設定で作成しておきます。

<img width="1235" alt="スクリーンショット 2019-08-17 16.31.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/1aa18741-aed2-e8e1-46c6-b150d6bfa99d.png">

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:Start*",
                "ec2:Stop*"
            ],
            "Resource": "*"
        }
    ]
}
```

次に関数の作成からEC2を起動させるlambda関数を作成します。
ランタイムにはpython3.7を選択し、実行ロールには先ほど作成したlambda-control-instanceを選択します。

![screencapture-ap-northeast-1-console-aws-amazon-lambda-home-2019-08-17-16_59_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/82ba5799-a4f8-8b34-2aee-1c536377b922.png)




lambdaの作成後、EC2起動用のSQSをトリガーに設定します。
最終的に以下のようになります。

<img width="1381" alt="スクリーンショット 2019-08-17 15.40.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/a1d5abff-ef2f-59d3-6106-691420f14975.png">


最後にインスタンスを起動させるlambda関数を設定します。

<img width="1383" alt="スクリーンショット 2019-08-17 15.47.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/1f4fe5ce-78b3-ee97-73e4-019c9c58c39c.png">

```
import boto3
import os

def lambda_handler(event, context):
    ec2 = boto3.client('ec2', region_name='ap-northeast-1')
    ec2.start_instances(InstanceIds=[os.environ['instance_id']])
    print('Instance ' + os.environ['instance_id'] + ' Started')
```


一斉送信用のbatchサーバのインスタンスIDを、環境変数として指定します。

<img width="1383" alt="スクリーンショット 2019-08-17 15.47.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11783/5a04178e-7ab7-4672-893a-ba4f6e7531a3.png">

これでEC2起動用のlambda関数作成は完了です。
同様の手順でEC2停止用のlambda関数も作成しておきます。

EC2停止用のlambdaでは以下の関数を設定します。

```
import boto3
import os

def lambda_handler(event, context):
    ec2 = boto3.client('ec2', region_name='ap-northeast-1')
    ec2.stop_instances(InstanceIds=[os.environ['instance_id']])
    print('Instance ' + os.environ['instance_id'] + ' Started')
```


## 今後の課題

### 排他制御
今後メール一斉送信の頻度がさらに増えた場合、メール送信処理中にkickerが起動して意図しない動きになってしまうので、排他制御の処理を追加する必要があります。

### サーバレス構成に向けて

SQS + lambda + S3 + SESでサーバレスなメール配信システムを作ることができます。
それぞれの役割は以下の通りです。

SQS ... メールの送信処理待ちキュー
Lambda ... SQSからキューを受け取って、SES経由でメールを送る
S3 ... メールのペイロードを入れておく
SES ... メール送信

この構成の課題は、送信したメールのデータを更新したい場合、別でデータを管理する必要が出てくる点です。
例えば、HTMLメールを開封したタイミングでダミーピクセスのアクセスがサーバに飛んで、送信データのテーブルを更新する様なパターンです。

ちなみに、LambdaからMySQLにアクセスするのはコネクションプールの関係からアンチパターンらしいので、切り分ける場合でもDynamoDBとかに入れる必要があります。
ただし、最近はAurora Serverlessが出てきたのでLambdaからMySQLを使えるかもしれません。