こんにちは。最近、ピストのチェーンを 和泉チエン TOUGH GUARD に替えて、ご機嫌な原口です。
ホスティング事業部の MRE(Messaging Reliability Engineering *ペパボの造語です)というチームで、 SRE ような取り組みを、DNS やメールなどのメッセージングサービスに対して実施しています。
今回は、弊社のホスティングサービスで提供しているメールシステムについてご紹介いたします。
メールシステム構成
弊社のホスティングサービスで提供しているメールシステムは、運用効率化やメールサーバー リプレイス時のダウンタイム削減のため、リバースプロキシを導入しています。
このリバースプロキシについては、過去、dovecot や Courier-IMAP などを利用していましたが、 現在は Nginx に変更しています。メールシステムで Nginx を利用している例は少ないと思うので、 簡単にご紹介させていただきます。
ngx_mail_core_module
Nginx を HTTP のリバースプロキシとして利用することは一般的ですが、MAIL についても 標準でモジュール ngx_mail_core_module が提供されています。
Nginx の設定
POPの設定であれば、以下のような感じです。
mail {
# mailのproxy機能を使う
proxy on;
# 認証サーバーのURI
auth_http http://127.0.0.1:9000/api/auth/mail/;
# 認証情報(XCLIENT)をバックエンドに渡す
xclient on;
pop3_auth plain cram-md5;
server {
listen 110;
protocol pop3;
}
}
Nginx が POPプロトコルをしゃべり、メールクライアントと認証情報のやりとりを行います。
その後、その認証情報をまとめて auth_http
で指定した認証サーバーに送り、
認証サーバーから、認証成功の status や upstream が返されると、認証以降の通信がプロキシされます。
認証サーバー(HTTP authentication server)
弊社では、認証サーバーを Go で書いており、以下のような感じで、リクエストとレスポンスを 取り扱っています。
認証情報を HTTP のヘッダーで受け取り、認証結果もヘッダーで返します(なぜ、ヘッダーで やりとりしないといけないのか、不満はありますが…)。ヘッダーの詳細については、 ngx_mail_auth_http_module をご参照ください。
// リクエストを受け取る処理
func NewAuthRequest(r *http.Request) *AuthRequest {
loginAttempt, _ := strconv.Atoi(r.Header.Get("Auth-Login-Attempt"))
a := &AuthRequest{
SessionID: xid.New().String(),
Method: r.Header.Get("Auth-Method"),
Protocol: r.Header.Get("Auth-Protocol"),
AuthUser: r.Header.Get("Auth-User"),
Pass: r.Header.Get("Auth-Pass"),
Salt: r.Header.Get("Auth-Salt"),
LoginAttempt: loginAttempt,
Client: client{
IP: net.ParseIP(r.Header.Get("Client-Ip")),
Host: r.Header.Get("Client-Host"),
},
}
if r.Header.Get("Auth-Ssl") == "on" {
a.SSLCipher = r.Header.Get("Auth-Ssl-Cipher")
a.SSLProtocol = r.Header.Get("Auth-Ssl-Protocol")
}
return a
}
// レスポンスを組み立てる処理
func (a *AuthResponse) SetHeaders(w http.ResponseWriter) {
w.Header().Set("Auth-Status", a.Status)
if a.Status == AuthSuccess {
w.Header().Set("Auth-User", a.Address)
w.Header().Set("Auth-Server", a.Server)
w.Header().Set("Auth-Port", strconv.Itoa(a.Port))
if a.Pass != "" {
w.Header().Set("Auth-Pass", a.Pass)
}
} else if a.Status == AuthFailure {
if a.Wait > 0 {
w.Header().Set("Auth-Wait", strconv.Itoa(a.Wait))
}
} else if a.Status == AuthError {
w.Header().Set("Auth-Error-Code", a.ErrorCode)
}
}
Nginx の選定理由
リバースプロキシを Nginx に変更した理由は、やはり、Nginx の使用に慣れており、信頼もあったことが 大きかったと思います。またリソース面でもイベント駆動型の Nginx は優秀でした。
2021年8月、弊社サービスのロリポップ!において、POP・IMAP が長時間使用できなくなる 電気通信事故 を 起こしてしまいました(ユーザーの皆様にはご迷惑をおかけし、本当に申し訳ございませんでした)。
この時の原因は、Nginx 以前に使用していた Courier-IMAP のプロセス増加によるメモリ枯渇でした。 現在の Nginx では、半分以下のメモリ使用量で、余裕がある状態でプロキシができています。
まとめ
メールシステムにおいて、Nginx をリバースプロキシとして事例は少ないと思い、紹介させていただきました。 何百万というメールアドレスをホスティングしている環境で利用しても、軽々とさばく Nginx には、 本当に助かっています。
追伸
珍しい構成と思っていたのに、ChatGPT からは一般的と言われてしまいました…