テスト用のダミーSMTPサーバ smtp4dev をAmazon ECS/Fargateのサービスとして動かしてみた

テスト用のダミーSMTPサーバ「smtp4dev」をAWSのマネージドコンテナ環境、ECS/Fargateを使ってサーバーレスで動かします。
サーバーレスで動かすことで、EC2の管理が不要となり、コストも抑えることができます。
メール送信アプリケーションをAWSで開発するときの開発・テストが便利になると思います。

# はじめに
本記事はAWSやコンテナについて知識があり、使用経験があるかたを読者として想定しています。
個々のサービスや用語についての説明は割愛しますので、必要に応じてAWSの公式ドキュメントなどを参考に調べていただけるとありがたいです。

# 実現すること
AWSのプライベートネットワークでsmtp4devをECS/Fargateのサーバーレス環境で動かしテストメールを送信できるようにし、さらにデータの永続化を実現します。

## AWSでダミーのSMTPサーバを動かす
SMTPでのメール送信アプリケーションを開発・テストする際に、安全かつ効率的にテストメールを送信するためのダミーの送信先SMTPサーバーとして使用できるsmtp4devというアプリケーションがあります。

https://github.com/rnwood/smtp4dev

Windows、Linux、コンテナなどで動かすことができます。
メールアプリケーションの稼働確認として実際にどのようなメールが送られるのか確認できます。

## ECS/Fargateで動かす
smtp4devはEC2で起動するのは容易ですが、さりとてこのためだけにEC2の管理はしたくありません。
コンテナ版smtp4devをECSの単体タスクとして起動することもできますが、SMTPサーバとしてアプリケーションからの通信を受けることになるので、IPアドレスがタスク再起動のたびに変更されるのも使いづらいです。
ですのでコンテナ版をECS/Fargateの「サービス」として動かします。

Amazon ECSについてはこちら

https://aws.amazon.com/jp/ecs/

Fargateについてはこちら

https://aws.amazon.com/jp/fargate/

## インターネット疎通のない「プライベートネットワーク」で動かす
smtp4devのコンテナはDocker Hubにホストされているので、一般的にはインターネットを通じたコンテナイメージの取得が必要です。
しかし今回はセキュリティ関連の要件が厳しくインターネットへの直接疎通が制限された環境を想定し、直接のインターネット疎通のないプライベートネットワークで動かします。

## smtp4devのデータをAWSのストレージサービスで永続化する
コンテナのデータは揮発性です。コンテナ内にデータを持つと、smtp4devの設定やメールも再起動で消えてしまいます。
今回はECSで使用できるAWSのストレージサービスを使用して、smtp4devのデータを永続化します。

# 事前準備
必要なAWS環境を準備します。
使用リージョンはアイルランド(eu-west-1)リージョンです。
※サービス展開が早い傾向があるのでアイルランドを使用しています。別のリージョンでも手順は同じですので、ご自分の環境に合わせて適切なリージョンを選択ください。

## VPC/Subnetの作成
まずVPCを作成します。
今回はPublic3つ(-pub)、Private3つ(-prv)の6サブネットを作成しました。

publicサブネットには踏み台サーバを配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/ddaf3a21-f2a2-4e8a-bd38-048784d5ef26.png)

AWSのネットワークについてはこちら

https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/configure-subnets.html

またコンテナに付与するセキュリティグループも作成しておきます。

## VPC Endpointの作成
プライベートネットワークでECSタスクを動かすためには、コンテナのPullに使用するECRとS3のエンドポイント、コンテナのログをCloudwatch Logsに送るためのエンドポイントが必要です。

以下のVPC Endpointを作成します。
- com.amazonaws.eu-west-1.ecr.dkr(Interface)
- com.amazonaws.eu-west-1.ecr.api(Interface)
- com.amazonaws.eu-west-1.logs(Interface)
- com.amazonaws.eu-west-1.s3(Gateway)
を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/88a63af5-ea8c-4959-9df6-0e7a47bb32dd.png)

必要なVPC Endpointについてはこちら

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/vpc-endpoints.html

## ECSクラスターの作成
ECSのクラスターを作成しておきます。
クラスター名は`tstclstr01`としました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/8ab73906-c48a-4b54-a5af-e6ae4d203da9.png)

## Docker HUBへのECR Pull Through Cacheの設定
Docker Hubからのsmtp4devのコンテナImageの取得をプライベートサブネットで可能にするため、ECRでDocker HubへのPull Through Cacheを設定します。

Pull Through Cacheについてはこちら

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/pull-through-cache.html

ECRのコンソールから以下を選択して設定していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/5932cbbb-1de8-402a-87e7-476c7aeb19af.png)

Pull Through CacheのアップストリームとしてDocker Hubを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/21cc6844-935f-4354-8bee-7ac8def57924.png)

認証情報としてDocker HubのアカウントとAPIキーを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/b4e423c6-9cba-4b8b-a95c-396250de400c.png)

Pull Through Cacheの設定を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/aac973f9-feb7-4e43-8270-cd5b4ede4b95.png)

これで、プライベートサブネットから、ECRのVPCエンドポイント経由でDockerHubのイメージが取得できるようになります。

取得する時のコマンドは以下のようになります。
ECRのリポジトリURIにPull Through Cacheで設定したPrefixがついたものになります。
<code>docker pull xxxxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/<strong>docker-hub</strong>/{UpstreamRepositoryName}:{ImageTag}</code>


## ロードバランサーとターゲットグループ
smtp4devの起動ガイドにあるように、smtp4devのコンテナのSMTP(25)とHTTP(80)のポートに通信を行うように構成します。

https://github.com/rnwood/smtp4dev/wiki/Installation#how-to-run-smtp4dev-in-docker
>docker run --rm -it -p 5000:80 -p 2525:25 rnwood/smtp4dev

80ポートの通信はsmtp4devのWEB画面にアクセスして、smtp4devの設定やメールの確認のために使用します。

転送先が二つあるので、ターゲットグループも二つ必要なのですが、ECSのサービスはGUIで設定すると1つのサービスに一つのターゲットグループしか定義できません。
そこで、Cloudformationで一つのサービスに複数のターゲットグループを定義する方法をとります。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/register-multiple-targetgroups.html

あらかじめロードバランサーやターゲットグループを定義しておく必要があります。

まずロードバランサーを作成します。
SMTPの通信を受けるためNLBで作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/85c613bf-742c-4635-bcb5-fd78fc70ee49.png)

ターゲットグループをPort 25とPort 80で二つ作成します。
ターゲットタイプはIPで作成し、ターゲットは登録しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/c40f8a52-6e09-4771-adb8-82d051655fb4.png)

Port 25とPort 80のリスナーを作成し、ターゲットグループを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/eadd07f1-0041-490b-a66a-e2ef40bd463b.png)


## 設定や受信したメールの永続化
コンテナの場合、タスクが再起動するたびにコンテナのストレージは初期化されるので、受信したテストメールや設定は消えてしまいます。
smtp4devのInstallationガイドによると、`/smtp4dev`というパスに設定等が保存されるとあります。

https://github.com/rnwood/smtp4dev/wiki/Installation#how-to-run-smtp4dev-in-docker
>The folder /smtp4dev will be used for the database and auto-generated TLS certificate. You can mount a directory outside of the container here for peristent storage.

ECSの場合、外部ストレージとしてAWSのストレージサービスであるAmazon EFS(Elastic File system)が使用できます。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/efs-volumes.html

EFSについてはこちら

https://aws.amazon.com/jp/efs/

EFSを作成し、永続ストレージとしてコンテナの`/smtp4dev`にマウントします。

EFSを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/283b9c6c-8972-4af1-8119-aa1069220518.png)

一旦別のEC2からマウントしsmtp4devのデータを保存するパス`/smtpdata`を作成しておきます。
```shell-session
$ sudo mount -t efs -o tls fs-08d8e02b6fd668856:/ /mnt/efs
$ sudo mkdir /mnt/efs/smtpdata
```

## smtp4dev用タスク定義の作成
ECSのタスク定義を作成します。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definitions.html

タスク定義の名前は`dummy-smtp4dev-1`としました。
タスク定義では以下を設定します。

### 起動タイプ
タスクの起動タイプはFargateを指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/db068121-e37f-4d33-b56e-80c1d4a464e1.png)

### イメージURL
コンテナ名はsmtp4devとしました。
イメージURIは事前に作成したPull Through CacheのURLとsmtp4devのリポジトリを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/dbbaa82b-d923-4baf-b866-aa6a536f7df7.png)

### コンテナのポート
tcp 25、80を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/b4f22beb-0603-4f32-94e8-41fc7cd5752d.png)

### ボリューム
事前に作成したEFSの`/smtpdata`をコンテナ`smtp4dev`の`/smtp4dev`にマウントするように設定します。
ボリュームを定義します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/2379b489-b493-4bfb-875d-f474d7e47c3c.png)

ボリュームをコンテナにマウントします。コンテナパスに`/smtp4dev`を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/72419372-6a25-4d16-a8d2-9cfc1ff6cdea.png)

# ECSサービスのデプロイ

Cloudformationを使用してサービスを作成します。

## Cloudformationコード

サービスの作成には以下のCloudformationテンプレートを使用します。
`LoadBalancer`以下にコンテナとターゲットグループの組み合わせを複数記載することで、一つのサービスに対して複数のターゲットグループを設定できます。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: ecs multi tgt svc
Resources:
  service:
    Type: AWS::ECS::Service
    Properties:
      #ECSクラスター名の指定
      Cluster: tstclstr01
      #タスク数は1
      DesiredCount: 1
      #Fargate起動タイプを指定
      LaunchType: FARGATE
      #事前に作成したタスク定義を指定
      TaskDefinition: dummy-smtp4dev-1:1
      #作成するサービス名
      ServiceName: testsvc
      #スケジューラはレプリカを指定
      SchedulingStrategy: REPLICA
      LoadBalancers:
        #事前作成した80ポートと25ポートのターゲットグループのARNを指定
        - ContainerName: smtp4dev
          ContainerPort: 80
          TargetGroupArn: arn:aws:elasticloadbalancing:eu-west-1:xxxxxxxxxxxx:targetgroup/tgt-ecssvc-http/2b53f6a0ff81f3d4
        - ContainerName: smtp4dev
          ContainerPort: 25
          TargetGroupArn: arn:aws:elasticloadbalancing:eu-west-1:xxxxxxxxxxxx:targetgroup/tgt-ecssvc-smtp/139f95508c75ec76
      NetworkConfiguration:
        AwsvpcConfiguration:
          #プライベートネットワークなのでパブリックIPは付与しない。
          AssignPublicIp: DISABLED
          #事前作成したセキュリティグループとサブネットを指定
          SecurityGroups:
            - sg-01bb32c5719e2fbfd
          Subnets:
            - subnet-0b6129430274e59ed
            - subnet-0afecdc596823b4fa
            - subnet-043482be4aaa20168
```

## Cloudformation スタックの作成
マネジメントコンソールからCloudformation テンプレートをデプロイし、サービスを作成します。

以下のように複数ターゲットグループへの割り振りを行うNLBを持ったサービスが作成できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/742de550-fd61-4d82-a717-813f137582f4.png)

# 動作確認

## WEB設定画面へのアクセス
踏み台EC2のブラウザからNLBのアドレスにアクセスします。
以下のようなsmtp4devのUIが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/931aad01-5ae4-469e-922f-d643ed8dcbff.png)

## テストメールの送信
PowerShellの`Send-MailMessage`を使ってからnlbのDNS名にあててテストメールを送信します。

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/send-mailmessage?view=powershell-7.5

```powershell
PS C:\Users\Administrator> $ToADDR   = "to@example.com"
>> $FromADDR = "from@example.com"
>> $SMTPHost   = "testnlb-a8b32e9c0524cf93.elb.eu-west-1.amazonaws.com"
>> $PortNum  = "25"
>> $Subject  = "smtp4dev test"
>> $MailBody  = "test mail"
>>
>> Send-MailMessage -To $ToADDR -From $FromADDR -SmtpServer $SMTPHost -Port $PortNum -Subject "$Subject" -Body $MailBody  -Encoding UTF8
```

テストメールが送信されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/e40a7c29-0d51-410a-8853-55a1adfca124.png)


## EFSによるデータ永続化の確認
ECSのコンソールからタスクを強制的に終了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/e339c0f2-5172-4285-bd98-d6bf3534b743.png)

ECSのサービスによって別IDのタスクが起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/fb23dbc3-fbbb-4ed1-b7cf-3af26dcf3471.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/b8a4a145-3c0a-4198-98f0-69d4e424b9d3.png)


テストメールが残っていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/d68a4fdd-d1f5-4bc7-9fe1-bd85b0f608e5.png)

EFSにはこのようなファイルが作成されていました。
```shell-session
$ ls -l /mnt/efs/smtpdata/
total 4200
-rw-r--r--. 1 root root    1296 Mar 26 05:35 appsettings.json
-rw-r--r--. 1 root root  106496 Mar 28 04:58 database.db
-rw-r--r--. 1 root root   32768 Mar 28 05:08 database.db-shm
-rw-r--r--. 1 root root 4148872 Mar 28 05:08 database.db-wal
-rw-r--r--. 1 root root     748 Mar 26 05:11 selfsigned-certificate.cer
-rw-r--r--. 1 root root    2343 Mar 26 05:11 selfsigned-certificate.pfx
$ 
```

# まとめ
テスト用のダミーSMTPサーバ「smtp4dev」をAWSのマネージドコンテナ環境、ECS/Fargateを使ってサーバーレスで動かすことができました。

動かすために
- Cloudformationを使った複数ターゲットグループを持つECSサービスの作成
- プライベートサブネットでのDocker HUBのコンテナの起動
- EFSを使ったデータの永続化
を使用しました。

すべての手順が役立つとは限りませんが、この記事の一部でも参考になれば幸いです。