おはこんばんちは、人事労務フリーのエンジニアを担当している橋本です。この記事は、freee Developers Advent Calendar 2019の6日目となります。
以前はSREに所属しており、この記事ではそのときの知見をご紹介いたします。
アプリケーションからのメール配送
早速ですが、表題のとおりアプリケーションにおけるメール配送を考えていきます。
freeeではお客様へのメール配送にSendGridを利用していますが、アプリケーションからSendGridに送信するまでの方法はいくつか考えられます。freeeでは以下の方法を検討しました。
- SendGrid APIを用いてアプリケーションから直接SendGridへ送信する
- アプリケーションからPostfixに送信させ、SendGridにリレーする
最終的に後者の方法を採用することにしました。メリットとしては、SendGridまでの経路でなんらかの障害が発生した場合にPostfix側で再送処理を担保してくれることや、仮にメールサービスをAmazon SESなどに変更したい場合でもPostfixの設定を変更するのみでアプリケーション側は責任を負う必要がない点が挙げられます。
Kubernetes上におけるサービスとPostfixの構成
freeeのいくつかのサービスはKubernetes (Amazon EKS) 上で動作しており、サービスごとにクラスタを用意するシングルテナント方式を採用しています。詳細は「AWSのマネージドサービスを活かした Kubernetes 運用とAmazon EKS によるクラスタのシングルテナント戦略について - Speaker Deck」をご参照ください。
ここであるサービスについて、Kubernetes環境における各サービスのPodとPostfix Podに関する概略図は以下のようになります。PostfixはDaemonSetとして各ノードに1台ずつ配置します。また、Podは同一Node上のPostfixに送信し、PostfixがSendGridにリレーする形をとっています。これは、サービスのPodからPostfix Pod間のネットワーク障害などによる配送障害を抑える意図があります。
Kubernetesの設定などを書く
Dockerfile
Alpine LinuxベースのDockerイメージを作成する場合は、以下のようなDockerfileになります。
FROM alpine:3.10 apk add --update --no-cache \ ca-certificates \ cyrus-sasl-login \ cyrus-sasl-plain \ libsasl \ postfix==3.4.7-r0 \ rsyslog==8.1904.0-r1 \ && mkdir -p /var/spool/postfix/etc/
libsasl
やcyrus-sasl
はSASL認証に必要になります*1。Postfixのログはrsyslogによって標準出力に流しておきます*2。また、念のため現時点で利用可能な最新バージョンを明示しておきます。
DaemonSet
DaemonSetのYAML定義はおおよそ以下のようになります(説明に必要な箇所だけ明示しています)。sasl_passwdやPostfixの各種設定ファイルはConfigMapやSecretとして渡しておきます。
apiVersion: apps/v1 kind: DaemonSet metadata: name: postfix spec: template: spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet volumes: - name: postfix-templates # 複数のConfigMapやSecretsをまとめてファイルとしてvolumeMountsするため、 # projectdを定義すると楽 projected: sources: - configMap: # Postfixおよびrsyslogの設定ファイル name: postfix items: - key: main.cf path: main.cf mode: 0644 - key: master.cf path: master.cf mode: 0644 - key: rsyslog.conf path: rsyslog.conf mode: 0644 - secret: # PostfixのSendGridに必要な認証情報 name: postfix-sasl-passwd items: - key: sasl-passwd path: sasl_passwd mode: 0600 - name: postfix-entrypoint configMap: name: postfix-entrypoint items: - key: entrypoint.sh path: entrypoint.sh mode: 0755 - name: var-spool-postfix # メールキューをNodeに永続化する hostPath: path: /var/spool/somedir/postfix type: DirectoryOrCreate containers: - name: postfix image: ... # 前述のDocker Imageに向ける command: ["sh", "-c", "/opt/entrypoint/entrypoint.sh"] # 各種設定ファイルやentrypointスクリプトを所定のパスへ配置する volumeMounts: - name: postfix-templates mountPath: /etc/postfix/main.cf subPath: main.cf - name: postfix-templates mountPath: /etc/postfix/master.cf subPath: master.cf - name: postfix-templates mountPath: /etc/rsyslog.conf subPath: rsyslog.conf - name: postfix-templates mountPath: /etc/postfix/sasl_passwd subPath: sasl_passwd readOnly: true - name: postfix-entrypoint mountPath: /opt/entrypoint - name: var-spool-postfix # Node側の/var/spool/somedir/postfixにマウントされる mountPath: /var/spool/postfix ports: - name: smtp-auth containerPort: 587 hostPort: 587 protocol: TCP - name: smtp-alt containerPort: 2525 hostPort: 2525 protocol: TCP
ここで、Amazon EKSの場合は以下の点に注意します:
- Node側の
/var/spool/postfix
はすでにEKS側で確保済みのようでマウントできないため、/var/spool/somedir/postfix
を使用する(somrdir
以下のパスは任意のものを指定) - Node側のTCP 25番ポートも同様にEKS側で確保されており、
hostNetwork: true
の際に利用できないため、代替えとなるポート番号を指定する(この例の場合ではsmtp-alt
にて2525番を指定)
仕様として明示されていないので将来的に変更になる可能性がありますが、現時点ではこのような追加設定が必要となりました。
ConfigMapおよびSecret
起動スクリプトentrypoint.sh
およびPostfixの各種設定ファイルを記述します((entrypoint.sh
において、コンテナ環境でのpostfix start-fg
によるPID1での起動はPostfix 3.3以降で利用可能です))。main.cf
やsasl_passwd
の詳細はPostfixのドキュメントをご確認ください(sasl_passwdは認証情報を含むため、Secretとして定義します)。
apiVersion: v1 kind: ConfigMap metadata: name: postfix data: main.cf: | ... # master.cfにてsmtpdのポートを前述のsmtp-altに合わせる master.cf: | 2525 inet n - y - - smtpd ... # 以降よしなに設定 rsyslog.conf: | # mail.info, mail.errをそれぞれ標準出力・標準エラー出力に指定 # その他の設定はよしなに記述してください mail.info /dev/stdout mail.err /dev/stderr --- apiVersion: v1 kind: ConfigMap metadata: name: postfix-entrypoint data: entrypoint.sh: | postmap hash:/etc/postfix/sasl_passwd # ハッシュテーブルを更新 postalias hash:/etc/postfix/aliases if [ ! -d /var/spool/postfix/etc ]; then mkdir /var/spool/postfix/etc; fi cp /etc/resolv.conf /var/spool/postfix/etc/resolv.conf # postfixが/var/spool/postfixへchrootするため必要となる rsyslogd # rsyslogdデーモンを起動 exec postfix start-fg # PostfixをPID 1にてフォアグラウンドで起動させる --- apiVersion: v1 kind: Secret metadata: name: postfix-sasl-passwd type: Opaque data: sasl-passwd: ...
これらをkubectl apply -f
などすることで、Postfixが各Nodeに配置されリクエストを受け付けます。実際には、これらをHelm Chartとしてテンプレート化して各クラスタにインストールしています。
各サービスからPostfix Podを参照する
各サービスのPodから同一NodeのPostfixを参照する場合は、Downward APIを用いてNodeのIPを得ます。ここでDaemonSetにおいてhostNetwork: true
としたことで、当該Nodeへの2525番ポートのアクセスはPostfixに到達できることになります。
apiVersion: apps/v1 kind: Deployment metadata: name: some-app spec: template: spec: containers: - name: some-app-container image: ... env: - name: SMTP_HOST_IP valueFrom: fieldRef: fieldPath: status.hostIP - name: SMTP_HOST_PORT value: "2525" ...
ここでたとえばRailsのAction Mailer (config/initializers/mail.rb
) の設定は、上記のenv
をもとに以下のように設定されます(必要に応じてENVがnil
の場合のデフォルト値も設定してください):
ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.smtp_settings = { address: ENV['SMTP_HOST_IP'], port: ENV['SMTP_HOST_PORT'].to_i }
さいごに
Kubernetes環境におけるPostfixの運用例をご紹介しました。今回のは一例で、先日発表されたAmazon EKS on AWS Fargateではこの構成を利用できなかったり*3、ほかにもよいプラクティスがある気がするので、知見をお持ちの方はぜひお話を聞かせてください。
明日、12月7日の記事は二輪ライダーのid:ymizushiが担当いたします!
*1:SendGridのリレーの際に必要です。詳細はSendGridのドキュメントをご参照ください
*2:Postfix 3.4でコンテナ向けのロギング機能が強化され、syslogなしに標準出力が可能となったため、場合によっては不要になります。詳細はPostfix logging to file or stdoutをご参照ください
*3:詳細はリンク先のLimitationsをご確認ください