SendGrid用のMailモックコンテナを作りました

SendGrid用のMailモックコンテナを作りました

SREチームの金澤(@yakitori009)です。

社内開発用にSendGrid用のMailモックコンテナを作りました。

開発環境の構成

検証AWS環境の構成

今回、その経緯と内容について書きたいと思います。

Mailモックコンテナについて

開発環境におけるメール送信テストで最も気を付けるべきことはメールの誤送信です。
メールの誤送信は、最も発生件数の多いセキュリティインシデントの1つで、

開発中のメール誤送信も対策が必要で、最低限、以下の処理を行っておく必要があるでしょう。

  • DBデータのマスク処理
  • メールアドレスを存在しないメールアドレスにマスクする
  • メール送信処理をモック化する
  • 実際のSMTPサーバーに飛ばさず、ダミーのSMTPサーバーに吸収させる

ダミーのSMTPサーバーとしては、MailTrap等のサービスが有名ですが、最近では、メールモックのDockerコンテナを使う事例も増えています。

代表的なメールモックコンテナとして、MailCatcherMaildevなどがあります。

SendGrid用のメールモックコンテナについて

MailCatcherMaildevは、SMTP送信用です。

SendGridは(SMTPサーバーとしての利用も可能ですが)主にWebAPIによるメール送信が一般的かと思われます。

しかしながら、このWeb APIをサポートしたメールモックコンテナは公式では用意されていません。
※SendGridでは、メールのテストを行うために、サブユーザー機能等を提供しています。

SendGridのメールモックコンテナを自作した背景

※公式ではありませんが、SendGrid用のメールモックコンテナが提供されています。

このコンテナ(yudppp/simple-sendgrid-mock-server)には大変お世話になっており、全社共通のツールとしてここ数年使わせていただいてました。

しかし、利用ケースの拡大に伴い、以下の要望が出てきました。

  • 添付ファイルの中身を確認したい
  • 50KB以上のファイルを添付したい(現状、エラーとなる)
  • リクエスト、レスポンスも実際のSendGirdのものと一致させたい
  • 各種エラーメッセージの対応
  • 厳密なパス対応(スラッシュが連続した場合、エラーとする等)

これらを解決させるために、自前で本格的なメールモックコンテナを作ることにしました。

自作SendGridメールモックコンテナの構成

私自身、バックエンドのAPI周りは開発イメージをしやすいのですが、フロントエンドにそこまで強いわけではなく、メールの表示画面については作ることを躊躇していました。(作ってもイケてるUIにならなそう。。)

今回、自前で作る部分を最低限に済ませたく、以下の構成で作ることにしました。

  • 自前で作るのは、SendGridのモックAPIのみ
  • このAPIの処理内で、SMTPでMailCatcherやMailDev等のSMTPメールモックに飛ばす
  • Dockerコンテナには自作のSendGrid APIモックと既存のSMTPメールモックをバンドルする

軽量コンテナにするための方針

Dockerコンテナは、なるべく軽量にしたいところです。
ゆえに、極力不要なものは組み込まない方針にしました。

※結果的に以下のミドルウェアは組み込まずに構築しました。

・WWWサーバー(nginx等)
 ・SMTPメールモックとSendGrid APIはそれぞれ別ポートで起動する
・Appサーバー(php-fpm、unicorn等)
 ・言語が提供するアプリケーションサーバーを利用
・SMTPサーバー(postfix等)
 ・SMTPメールモックのSMTP機能を利用する

SMTPメールモックの選定

SMTPメールモックは(今まで利用したことのある)以下の2つに絞りました。

MailCatcher
 ・代表的なSMTPメールモック
 ・SendGrid MailCatherみたいな名前にすると何をするものなのか一発で理解できる
 ・CC、BCC等がTOに表示されてしまう
 ・必要なミドルウェアが多い(ruby、sqlite、openssl等)
Maildev
 ・Node.jsのみで起動
 ・ISO-2022-JPの表示ができない(ソースを見ないと確認できない)

結果、MailDevを採用しました。

・SendGrid APIを利用する送信にISO-2022-JPを利用する機会がなかったこと
・インストールの手軽さ、軽量さ
・UIがイケてる

などが主な理由です。

※MailCatcherについては、CC、BCC表示の問題や、コンテナの軽量化の方針に向かなかったことが大きなネックになりました。

APIサーバーの言語選定

以下の言語を候補に入れていました。

・PHP
 ・一番慣れている言語
・Ruby
 ・MailCatcherが利用している(共用できるかも?)
・Node.js
 ・MailDevが利用している(共用できるかも?)
・Golang
 ・バイナリのみで動作可能(コンテナにGolangのインストール不要)

結果、Golangを採用しました。

Golangであれば、ビルドしたバイナリを配置するだけでよく、言語すらインストール不要になります。

※極力軽量なコンテナにする方針だったためこの選択は後悔していませんが、バイナリならではのデメリットが後に生じました。(後述のM1 Mac対応を参考)

採用したFW、ライブラリ

Golangにおいて、以下のFW、ライブラリを採用しました。

・echo
・apitest
・validator.v9

※このAPIサーバーの実装はGitHubで公開しています。

SendGrid API + MailDevコンテナの構築

※構築したコンテナはDockerHubで公開しています。

Dockerコンテナの構築にあたっては、2つのプログラムを同時に起動する必要がありました。

・自作のSendGrid APIサーバー
・MailDevサーバー

この2つを同時起動させるため、supervisordで制御しています。

DockerHubのイメージのビルドは、GitHubリポジトリと連動させており、masterにpushしたら自動的にDockerHubのビルドイメージが更新されるようにしています。

M1 Mac(arm64)対応について

最近はArm系のCPUを搭載したM1 Macが出回り始めたため、Dockerコンテナを構築する際は、linux/amd64に加え、linux/arm両方への対応を意識する必要がでてきました。

DockerHubに登録されている代表的なコンテナは、Intel系、Arm系を含め、複数のアーキテクチャに対応しているものが多いです。

ただし、M1 MacでもRosetta2やDocker for Macの互換性維持機能があり、linux/amd64でも動作できるものが多いです。

今回構築したコンテナについては、結果的にlinux/amd64でも動きました。
また、本家maildevコンテナも現状linux/amd64のみの提供となっており、一旦linux/amd64のみでサポートしております。

ただ、linux/arm64にも対応することで、以下のメリットがあります。

・互換性維持の処理がなくなり、パフォーマンスが良くなる
・AWS EC2のGraviton2インスタンス上でも動作できる

将来的にマルチアーキテクチャをサポートしたコンテナにしたいと考えています。
今回、(途中で挫折しましたが)linux/arm64にも対応するために行ったことを残しておきたいと思います。

マルチアーキテクチャに対応したDockerイメージの構築

x86_64とarm64両方に対応するコンテナイメージを構築するには、buildxという機能を使います。

以下のようにdockerコマンドを実行することで、linux/amd64、linux/arm64両方に対応したコンテナをビルド可能です。

$ docker buildx build --platform linux/amd64,linux/arm64 -t ykanazawa/sendgrid-maildev:latest --push .

Golangのバイナリのアーキテクチャ別にビルド

Golangでビルドを行う際に、x86_64、arm64のバイナリをそれぞれビルドしておきます。

x86_64

$ env GOOS=linux GOARCH=amd64 go build -o sendgrid-dev_x86_64 main.go

arm64

$ env GOOS=linux GOARCH=arm64 go build -o sendgrid-dev_aarch64 main.go

※PHPやRubyであれば、Dockerfile構築時に、それぞれのアーキテクチャのPHP、Rubyをインストールできるのですが、Golangの場合は原則、ビルドしたバイナリを配置する形になるので、マルチアーキテクチャのコンテナを構築する際はここら辺が面倒になってきます。(言語自体入れなくて良いというメリットと引き換えで生じるデメリット)

Dockerコンテナ構築時にアーキテクチャ別のGolangバイナリを配置

※ここがうまくいかなくて挫折しました。

以前、M1 Macに対応するためにで行っていた方法では、

RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
...
linux-headers-$(if [ $(uname -m) = "aarch64" ]; then echo arm64; else echo amd64; fi) \
...

のように、unameコマンドの結果を出力していました。

今回、Linuxのバイナリをコピーするために、

COPY sendgrid-dev_$(uname -a) /usr/local/bin/sendgrid-dev

とやってみたのですが失敗しました。
COPYコマンドではコマンドの実行結果を取得

環境変数を使い

ARCH=$(uname -a)
COPY sendgrid-dev_${ARCH} /usr/local/bin/sendgrid-dev

としてみましたが、これも失敗($ARCHに何も入っていなかった)

この方法で構築できないとなると、手順そのものを変える必要がありそうです。

・x86_64、arm64両方のバイナリをDockerコンテナの/tmpにコピーして、RUNコマンドで分岐する
・Docker build時にGolangバイナリをアーキテクチャに合わせてビルドする

などを検討する必要がありそう。
マルチアーキテクチャ対応については、今後の課題としたいと思います。

2021/08/20追記:
SendGrid Web APIのリポジトリのReleaseにGolangのバイナリをアーキテクチャ別にアップロードし、
SendGrid MaildevコンテナのリポジトリのDockerfileでアーキテクチャ判定してそれぞれcurlでダウンロードさせることで対応しました。

SendGrid Web APIのリポジトリのReleaseにビルド済バイナリをアーキテクチャ別にアップロード
https://github.com/yKanazawa/sendgrid-dev/releases/tag/v0.9.0

SendGrid MaildevコンテナのDockerfileを以下のように修正
https://github.com/yKanazawa/sendgrid-maildev/pull/1/files

RUN curl -L -o /usr/local/bin/sendgrid-dev 
https://github.com/yKanazawa/sendgrid-dev/releases/download/v0.9.0/sendgrid-dev_$(if [ $(uname -m) = "aarch64" ]; then echo aarch64; else echo x86_64; fi)

※$(if [ $(uname -m) = “aarch64” ];…の部分は、単純に$(uname -m)としたかったのですが、この記述だとDockerfileが通らず。

さいごに

今回、社内からの要望をきっかけに本格的なSendGridメールモックコンテナを構築するに至りました。
それなりに手間がかかりましたが、公式でまだこのようなモックコンテナが出てないことと、非公式でも決定版的なものが普及してなさそうなので、今後丁寧にメンテナンスしながら普及させていけたらと思います。

お知らせ

2021/8/25にスペースマーケットさんと合同勉強会を行います。

今回構築したSendGridメールモックコンテナについて話す予定です。
興味がある方は是非ご参加ください。