Lumenを導入したEC2とDynamoDBとSNS

# はじめに
EC2にLumenを導入し、小さなwebアプリケーションを作成する機会がありました。
要件を満たすために、新たに作成するDBとのI/Oとメール送信を実装する必要があり、
DynamoDBとSimple Notification Service(SNS)を利用したので、その時に得た知見をまとめます。

- 触れる内容
 - LumenでDynamoDBとSNSを利用する方法
- 触れない内容
 - Lumenの導入と設定
 - webアプリケーションのインフラの設定、ミドルウェアのインストール
 - ローカル環境の構築方法

## 各種バージョン
- PHP 7.2.10
- Lumen 5.7.7
- aws/aws-sdk-php 3.82.0
- laravel-dynamodb 4.11.2

# LumenとDynamoDB

## laravel-dynamodb導入
ORMを使うためのライブラリlaravel-dynamodbを導入します。
まずはcomposerでインストールします。
`composer require baopham/dynamodb`
そしてbootstrap/app.phpに下記を追記します。
(この手順はLaravelの場合と異なるので注意が必要です。)

```php:bootstrap/app.php
// ...

$app = new Laravel\Lumen\Application(
    dirname(__DIR__)
);

$app->withFacades();

// Load dynamodb config file
$app->configure('dynamodb');

$app->withEloquent();

// ...

$app->register(BaoPham\DynamoDb\DynamoDbServiceProvider::class);
```

## 認証
config/dynamodb.phpを作成します。

```php:config/dynamodb.php
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default DynamoDb Connection Name
    |--------------------------------------------------------------------------
    |
    | Here you may specify which of the DynamoDb connections below you wish
    | to use as your default connection for all DynamoDb work.
    */

    'default' => env('DYNAMODB_CONNECTION', 'local'),

    /*
    |--------------------------------------------------------------------------
    | DynamoDb Connections
    |--------------------------------------------------------------------------
    |
    | Here are each of the DynamoDb connections setup for your application.
    |
    | Most of the connection's config will be fed directly to AwsClient
    | constructor http://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.AwsClient.html#___construct
    */

    'connections' => [
        'aws' => [
            'credentials' => [
                'key' => env('DYNAMODB_KEY'),
                'secret' => env('DYNAMODB_SECRET'),
                // If using as an assumed IAM role, you can also use the `token` parameter
                'token' => env('AWS_SESSION_TOKEN'),
            ],
            'region' => env('DYNAMODB_REGION'),
            // if true, it will use Laravel Log.
            // For advanced options, see http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/configuration.html
            'debug' => env('DYNAMODB_DEBUG'),
        ],
        'aws_iam_role' => [
            'region' => env('DYNAMODB_REGION'),
            'debug' => true,
        ],
        'local' => [
            'credentials' => [
                'key' => 'key',
                'secret' => 'secret',
            ],
            'region' => 'region',
            // see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html
            'endpoint' => env('DYNAMODB_LOCAL_ENDPOINT'),
            'debug' => true,
        ],
        'test' => [
            'credentials' => [
                'key' => 'key',
                'secret' => 'secret',
            ],
            'region' => 'region',
            'endpoint' => env('DYNAMODB_LOCAL_ENDPOINT'),
            'debug' => true,
        ],
    ],
];

```

今回はlocal環境とdev環境とtest環境とprod環境の4つの環境を準備する必要があったので、
それぞれのenvファイルを作成します。

```..env.local
DYNAMODB_CONNECTION=local
DYNAMODB_LOCAL_ENDPOINT=http://localhost:4569
```

LocalStackを使ってDynamoDBをエミュレートしていることを前提としています。
利用するポートは`4569`です。

```..env.dev、.env.test、.env.prod
DYNAMODB_CONNECTION=aws_iam_role
DYNAMODB_REGION=ap-northeast-1
```

dev,test,prod環境全てでIAMを使って認証させています。
IAMを使うと認証情報をソースコードに記述する必要がないので良いです。

## Modelの作成例
DynamoDbModelクラスを継承したModelクラスを作成します。

```php:Text.php
/**
 * textテーブルの操作
 *
 * Class Text
 * @package App\Model
 */
class Text extends DynamoDbModel
{
    /**
     * @var string テーブル名
     */
    protected $table = 'text';

    /**
     * @var string Hash Key
     */
    protected $primaryKey = 'text_id';

    /**
     * @var array カラム一覧
     */
    protected $fillable = [
        'text_id',
        'title',
        'created_user',
        'created_at',
    ];
}
```

## 利用してみた感想
今回はsave()とall()を使いました。特に問題はなかったです。
ただ、RDBではないDBに対してEloquent ORMを利用するのはどうなのだろうと思ってしまいます。
ライブラリの中身を詳しく読んでいないので、互換性の取り方まで把握していません。
気になる方は[ライブラリ](https://github.com/baopham/laravel-dynamodb)を読んでください。

# LumenとSNS
Simple Email Service(SES)を用いてメール送信する方法が最適だと考えました。
しかし、今回は色々事情があり東京リージョンを使う必要があり、SESには東京リージョンがないため使えませんでした。(2019-02-09)
よってSNSでメール送信しています。

PHPでSNSに特化したライブラリはなく、AWS SDKを使って実装します。

## 設定
bootstrap/app.phpに下記を追記します。

```php:bootstrap/app.php
// ...

// Load sns config file
$app->configure('sns');

// ...
```

config/sns.phpを作成します。

```php:config/sns.php
<?php

return [

    'default' => env('SNS_CONNECTION', 'dev'),

    'connections' => [
        'prod' => [
            'topic_arn' => env('SNS_TOPIC_ARN'),
            'options' => [
                'version' => env('SNS_VERSION'),
                'region' => env('SNS_REGION'),
            ]
        ],
        'test' => [
            'topic_arn' => env('SNS_TOPIC_ARN'),
            'options' => [
                'version' => env('SNS_VERSION'),
                'region' => env('SNS_REGION'),
            ]
        ],
        'dev' => [
            'topic_arn' => env('SNS_TOPIC_ARN'),
            'options' => [
                'version' => env('SNS_VERSION'),
                'region' => env('SNS_REGION'),
            ]
        ],
        'local' => [
            'topic_arn' => env('SNS_TOPIC_ARN'),
            'options' => [
                'version' => env('SNS_VERSION'),
                'region' => env('SNS_REGION'),
                'credentials' => [
                    'key' => 'key',
                    'secret' => 'secret',
                ],
                'endpoint' => env('SNS_LOCAL_ENDPOINT'),
            ]
        ]
    ],
];
```


## envファイル

```..env.local
SNS_CONNECTION=local
SNS_VERSION=latest
SNS_LOCAL_ENDPOINT=http://localhost:4575
SNS_TOPIC_ARN=arn:aws:sns:ap-northeast-1:hoge:local
SNS_REGION=ap-northeast-1
```
LocalStackを使ってSNSをエミュレートしていることを前提としています。
ローカルで作成済みのトピックのARNをSNS_TOPIC_ARNに記載します。
利用するポートは`4575`です。

```..env.dev、.env.test、.env.prod
SNS_CONNECTION=dev // test or prod
SNS_VERSION=latest
SNS_TOPIC_ARN=arn:aws:sns:ap-northeast-1:hoge:topic
SNS_REGION=ap-northeast-1
```
クラウド上で作成済みのトピックのARNをSNS_TOPIC_ARNに記載します。
SNS_CONNECTIONは各環境で変更。

## メール送信の実装例

```php:SnsMail.php
<?php
/**
 * SNSによるメール送信するためのclass
 *
 * Class SnsMail
 * @package App\Library
 */
class SnsMail
{
    /**
     * @var string AWSの環境
     */
    private $connection;

    /**
     * @var string SNSのtopic
     */
    private $topicArn;

    /**
     * @var SnsClient SNSのクライアント
     */
    private $snsClient;

    /**
     * SnsMail constructor.
     */
    public function __construct()
    {
        $connection = Config::get('sns.default');
        $this->connection = $connection;
        $this->topicArn = Config::get("sns.connections.{$connection}.topic_arn");
        $this->snsClient = new SnsClient(Config::get("sns.connections.{$connection}.options"));
    }

    /**
     * SNSを用いたメール送信
     *
     * @param string $subject
     * @param string $message
     * @return void
     */
    public function publish(string $subject, string $message): void
    {
        $subject = "{$subject}（{$this->connection}環境）";
        $this->snsClient->publish([
            'TopicArn' => $this->topicArn,
            'Subject'  => $subject,
            'Message'  => $message,
        ]);
    }
}

```

## 利用してみた感想
トピック作成時に飛ぶ承認メールの`Confirm subscription`のリンクを踏まないとメールは飛びません...
このリンクを踏む作業を忘れていて、1時間とられましたw

# 最後に
気が向いた時にLocalStackを使ったローカル環境の構築とCloudFormationを使ったインフラ構築の記事を書きます。