ASP.NET Core + Amazon SES な Web システムを App Runner でホスティングし、ネットワークパターンごとにメール送信してみた

2022.08.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

いわさです。

ASP.NET Core から App Runner にデプロイしたときに Amazon SES 経由での SMTP メール送信でタイムアウトが起きるという話を聞いて検証していました。

原因としては、App Runner のネットワーキングでアウトバウンドが正しく構成されていないカスタム VPC を使っていたためだったのですが、ASP.NET Core で App Runner にデプロイする さらに非推奨の SmtpClient ではなく MailKit を使う過程が、良いサンプルが見当たらずに色々やったのでせっかくですしブログにしておくことにしました。

Amazon SES 用意

Amazon SES の準備は割愛しますが、SMTP を使ってメール送信を行いたかったので、SMTP 用のユーザーを作成しています。

ASP.NET Core Web アプリ用意

ASP.NET Core Web アプリに SMTP 送信処理を実装します。
SmtpClient は互換性のために用意されているもので現在は非推奨ということを知ったので、公式ドキュメントで推奨されていたMailKitを使ってみました。

SmtpClient doesn't support many modern protocols. It is compat-only. It's great for one off emails from tools, but doesn't scale to modern requirements of the protocol.

SmtpClient クラス (System.Net.Mail) | Microsoft Docs より

% dotnet new web
テンプレート "ASP.NET Core Empty" が正常に作成されました。

作成後の操作を処理しています...
/Users/iwasa.takahito/work/hoge0813ses/hoge0813ses.csproj で ' dotnet restore ' を実行しています...
  Determining projects to restore...
  Restored /Users/iwasa.takahito/work/hoge0813ses/hoge0813ses.csproj (in 91 ms).
正常に復元されました。

% dotnet add package MailKit --version 3.3.0

  Determining projects to restore...
  Writing /var/folders/4d/nhd1bp3d161crsn900wjrprm0000gp/T/tmpJHMBXC.tmp
info : Adding PackageReference for package 'MailKit' into project '/Users/iwasa.takahito/work/hoge0813ses/hoge0813ses.csproj'.

:

info : Writing assets file to disk. Path: /Users/iwasa.takahito/work/hoge0813ses/obj/project.assets.json
log  : Restored /Users/iwasa.takahito/work/hoge0813ses/hoge0813ses.csproj (in 4.46 sec).

dotnet new web では Minimal API で初期化されますが、ここでは検証用なので適当なルーティングを追加してそのまま全ての処理を実装してしまいます。
※ デバッグが面倒になって catch で潰しててエラー処理だいぶサボってますがお許しを。

Program.cs

using MailKit.Net.Smtp;
using MimeKit;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/mail/{text}", async (string text) => 
{
    try
    {
        var message = new MimeMessage ();
        message.From.Add (new MailboxAddress ("fromhoge", "from@mail1.tak1wa.com"));
        message.To.Add (new MailboxAddress ("tohoge", "to@example.com"));
        message.Subject = "subject: " + text;
        message.Body = new TextPart ("plain") {
            Text = "body: " + text
        };

        using(var client = new SmtpClient()){
            await client.ConnectAsync("email-smtp.ap-northeast-1.amazonaws.com", 587, false);
            await client.AuthenticateAsync("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
            return await client.SendAsync(message);
        }
    }
    catch (System.Exception ex)
    {
        return ex.ToString();
    }
});

app.Run();

まずはdotnet runでローカル実行し、/mail/figaへ GET リクエストを送信してみましょう。

期待どおりメールが送信されましたね。
ここまででローカルホストの ASP.NET Core アプリから Amazon SES を使ってメール送信することが出来ました。

App Runner へデプロイ

さて、次は App Runner へアプリをデプロイします。
流れとしてはアプリをコンテナ化し、コンテナイメージをレジストリに登録、レジストのイメージを使って App Runner インスタンスを作成という流れになります。

そして .NET CLI の AWS デプロイツールを使うと、このあたりの流れをコマンド一発で実行することが出来ます。
AWS .NET deployment tool というやつで、以前記事にさせて頂いたことがあります。

基本的にはdotnet aws deployで済むはずなのですが、私が使っている M1 Max 環境だと、x64版の .NET SDK を追加でインストールし、コンテナイメージはbuildxplatform=linux/amd64でビルドし直しました。本題から逸れるので深堀りはしませんがもう少し調査して改めてエントリ書きたいですね。

% dotnet aws deploy
AWS .NET deployment tool for deploying .NET Core applications to AWS.
Project Home: https://github.com/aws/aws-dotnet-deploy

Configuring AWS Credentials using AWS SDK credential search.
Configuring AWS region using AWS SDK region search to ap-northeast-1.
Recommended Deployment Option
-----------------------------
1: ASP.NET Core App to AWS Elastic Beanstalk on Linux
This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Linux. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.

Additional Deployment Options
------------------------------
2: ASP.NET Core App to AWS Elastic Beanstalk on Windows
This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Windows. Recommended if you do not want to deploy your application as a container image.

3: ASP.NET Core App to Amazon ECS using AWS Fargate
This ASP.NET Core application will be deployed to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy your application as a container image on Linux.

4: ASP.NET Core App to AWS App Runner
This ASP.NET Core application will be built as a container image on Linux and deployed to AWS App Runner, a fully managed service for web applications and APIs. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy your web application as a Linux container image on a fully managed environment.

Choose deployment option (recommended default: 1)
4

Name the Cloud Application to deploy your project to
--------------------------------------------------------------------------------
Enter the name of the new CloudFormationStack stack (default hoge0813ses):

プラットフォーム指定

参考までにプラットフォーム指定版のエラー内容と対処法を残しておきます。
デプロイ時にイベントログでヘルスチェックの失敗が確認され、アプリケーションログを確認してみるとexec format errorが。

08-13-2022 08:42:21 PM [AppRunner] Health check on port '80' failed. Service is rolling back. Check your configured port number. For more information, read the application logs.
08-13-2022 08:36:03 PM [AppRunner] Performing health check on port '80'.
08-13-2022 08:35:53 PM [AppRunner] Provisioning instances and deploying image.
08-13-2022 08:35:40 PM [AppRunner] Successfully pulled image from ECR.
08-13-2022 08:32:23 PM [AppRunner] Service status is set to OPERATION_IN_PROGRESS.
08-13-2022 08:32:23 PM [AppRunner] Service creation started.
08-13-2022 08:41:15 PM standard_init_linux.go:228: exec user process caused: exec format error
08-13-2022 08:38:48 PM standard_init_linux.go:228: exec user process caused: exec format error
08-13-2022 08:36:21 PM standard_init_linux.go:228: exec user process caused: exec format error

M1 Mac のせいで Arm でビルドされてしまっているのでbuildxでプラットフォームを指定しています。

% docker buildx build --platform=linux/amd64 -t hoge0813ses:hogehoge .
[+] Building 145.1s (19/19) FINISHED                                                                                                   
 => [internal] load build definition from Dockerfile                                                                              0.0s
 => => transferring dockerfile: 704B                                                                                              0.0s
 => [internal] load .dockerignore                                                                                                 0.0s

:

 => => exporting layers                                                                                                           0.0s
 => => writing image sha256:28b343d1ce9f3441e7e94a8316ef2bfecedb96eae338fa5e582ee54d7b8593c1                                      0.0s
 => => naming to docker.io/library/hoge0813ses

% docker tag hoge0813ses:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/hoge0813ses:hogeho
ge
% docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/hoge0813ses:hogehoge
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/hoge0813ses]
6ce5c6ef6207: Pushed 
5f70bf18a086: Layer already exists 
ccb5d5ac00e0: Pushed 
a3616206dbdf: Pushed 
91961d2d529c: Pushed 
66ec52466d81: Pushed 
e985411fba9e: Pushed 
92a4e8a3140f: Pushed 
hogehoge: digest: sha256:8ea1746b8dcfbedc394b1e1c93fe4faecbb3d94ac5e9ab160660fe7495657bb1 size: 1995

dotnet aws deployでECRへの統合は出来ているので、そのまま新しいタグでイメージを作ってプッシュだけしてやりました。

App Runner 作成

App Runner は ECR のイメージを指定してサービス作成するだけなので簡単です。
まずはパブリックアクセスネットワークモードで作成してみます。

デプロイ後に問題なく Web サイトへアクセス出来ました。

また、メール送信用のパスへアクセスすると、メールが送信されることも確認することが出来ました。

ネットワークモード

App Runner はカスタムVPC を選択することで、独自で用意した VPC リソースへアクセス出来ます。
以下がかなり詳しく書かれています。
ざっくり言うと、間にいくつかコンポーネントは挟んでいますが通常のリクエスト/レスポンスはマネージド VPC 経由でアクセスし、コンテナアプリケーションからそれ以外でアウトバウンド通信を行う際はカスタム VPC 経由でアクセスします。

ということで、カスタム VPC で Amazon SES へのアクセスが出来ないもの、出来るものをいくつか試してみました。

VPC 外へのアウトバウンド不可

これはいわゆるプライベートサブネットで、NAT ゲートウェイへのルートもないので完全に VPC 外へはアクセス出来ないサブネットです。

この場合は以下のようにタイムアウトが発生しました。

NATゲートウェイあり

続いて、NAT ゲートウェイへのルートがあるサブネットの場合です。
VPC 外へのアクセスは全て NAT ゲートウェイを経由します。

この場合はメール送信に成功しました。
NAT ゲートウェイ経由で Amazon SES へアクセスしています。

VPC エンドポイントあり

続いて、NAT ゲートウェイが無い代わりに Amazon SES の VPC エンドポイントを作成しました。
Amazon SES へのアウトバウンドの経路さえ用意出来れば良いのでこれはおそらくいけるでしょう。

期待どおりメール送信出来ましたね。

さいごに

本日は、ASP.NET Core + Amazon SES な Web システムを App Runner でホスティングし、ネットワークパターンごとにメール送信してみました。

今回は色々な事情から ASP.NET Core から Amazon SES へアクセスするというものでしたが、App Runner からアウトバウンドするシステムであればカスタム VPC 周りの挙動は同じだと思いますので、カスタム VPC を使う際には、まず NAT ゲートウェイや VPC エンドポイントが必要なのかを検討してみてください。
試してないですが、アウトバウンドの経路が VPC ピアリングや Direct Connect / Site to Site VPN でも同じ考え方になると思います。