【AWS_SES】LocalStackのSESを利用して、ローカル環境(SpringBoot)で実装したメール送信機能の動作確認をする方法
はじめに
こちらの記事では、AWSのメール送信サービスであるSES(Simple Email Service)を本番環境で使用する想定で、「まずはローカル環境で実装した内容の動作確認やテストを実施したい」といった際に、必要となるリソースやコマンド・流れなどを記載しております。
この記事の対象者
・LocalStackについてよく知らないけど、ちょっと興味あるなという方
・Webアプリ開発にて、「メール送信機能」の実現に向けて選択肢を広げたい方
・AWSのSESをローカル環境で動かしてテストを行いたい方
開発環境
・Java 21
・SpringBoot 3.3.5
・macOS sonoma 14.6.1
AWSのSESって何者?
AWS SESは"はじめに"でも記載した通り、「Simple Email Service」の略で、Amazon Web Services(AWS)が提供するクラウドベースのメール送信サービスです。AWS SESを使うと、大量のメールを迅速かつ確実に送信することができます。
※詳細が気になる方は、下記公式サイトをご参照ください。
そもそもLocalStackとは?
LocalStackは、AWSのクラウドサービスをローカル環境で模倣するためのツールで、開発者が本番環境に依存せずにローカルでAWSサービスをテストできる環境を提供します。Amazon SES(Simple Email Service)もLocalStackでエミュレーション(※)されるAWSサービスの一つです。
※エミュレーション:
あるシステムや環境の動作を別のシステム上で模倣・再現することを指します。具体的には、本物のハードウェアやソフトウェアを使わずに、仮想的な環境を構築してその動作を真似ることで、特定の機能や動作をテストしたり、実行したりする技術です。スマホアプリを開発をする際に、PC上でスマホを表示させて動きを確認したりするのもその類です。
この記事内のプロジェクトのディレクトリ構成(例)
sample-localstack-ses(プロジェクト名)
│
├── src/main/java
│ │
│ └── com.example.demo
│ │
│ ├── config
│ │ │
│ │ └── AwsSesConfig.java
│ │
│ ├── controller
│ │ │
│ │ └── SampleController.java
│ │
│ ├── service
│ │ │
│ │ └── EmailService.java
│ │
│ └── SampleLocalstackSesApplication.java
│
├── src/main/resource
│ │
│ └── application.yml
│
....
│
├── docker-compose.yml
│
└── pom.xml
環境を整えて動作確認をするまでの流れ
1: LocalStackでエミュレーションするための「docker-compose.yml」を用意する
2: 1で用意した「docker-compose.yml」ファイルを元にdockerコンテナを起動する
3: awsコマンドで送信元メールと送信先メールの認証を行う(LocalStack環境用)
4: SpringBootで、今回必要なメール送信機能(API)の実装を行う
環境準備その1: LocalStackでエミュレーションするための「docker-compose.yml」を用意する
下記内容の「docker-compose.yml」ファイルをプロジェクト直下に作成
・docker-compose.yml
services:
localstack:
image: localstack/localstack:latest
container_name: localstack-ses # コンテナ名を設定
environment:
- SERVICES=ses # SESサービスを利用するための設定
- DEBUG=1 # デバッグモードを有効にする
ports:
- "4566:4566" # LocalStackのエンドポイント用ポート
環境準備その2: その1で用意した「docker-compose.yml」ファイルを元にdockerコンテナを起動する
・ターミナルを開きプロジェクト直下に移動し、下記コマンドを実行
※今回はオプションで「-d」をつけてデタッチドモードで実行し、バックグラウンドで動かす
docker-compose up -d
・以下のレスポンスが返ってくれば、コンテナの起動は完了
環境準備その3: awsコマンドで送信元メールと送信先メールの認証を行う(LocalStack環境用)
・python3コマンドが使用できない場合は、まずは下記コマンドでインストール(下記はmacOSの例)
brew install python
・下記コマンドを実行し、awslocalコマンドを利用できるようにする
pip install awscli-local
・仮想環境がアクティブではない場合、下記を実行し再度awslocalをインストール
source path/to/your/venv/bin/activate
・メールアドレスの認証処理
// 送信元メールアドレス
awslocal ses verify-email-identity --email-address sender@example.com
// 送信先メールアドレス
awslocal ses verify-email-identity --email-address recipient@example.com
環境準備その4: SpringBootで、今回必要なメール送信機能(API)の実装を行う
メール送信APIを作成
・Configクラス
package com.example.demo.config;
import java.net.URI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.ses.SesClient;
@Configuration
public class AwsSesConfig {
@Bean
public SesClient sesClient() {
return SesClient.builder()
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")))
.endpointOverride(URI.create("http://localhost:4566"))
.build();
}
}
・Controllerクラス
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.service.EmailService;
@RestController
public class SampleController {
private final EmailService emailService;
public SampleController(EmailService emailService) {
this.emailService = emailService;
}
@GetMapping("/send-email")
public String sendEmail(@RequestParam String toAddress) {
emailService.sendEmail(toAddress, "件名: テストメール", "これはテストメールです。");
return "メール送信完了!";
}
}
・Service
package com.example.demo.service;
import java.net.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.ses.SesClient;
import software.amazon.awssdk.services.ses.model.Body;
import software.amazon.awssdk.services.ses.model.Content;
import software.amazon.awssdk.services.ses.model.Destination;
import software.amazon.awssdk.services.ses.model.Message;
import software.amazon.awssdk.services.ses.model.SendEmailRequest;
@Service
public class EmailService {
private static final Logger logger = LoggerFactory.getLogger(EmailService.class);
SesClient sesClient = SesClient.builder()
.region(Region.US_EAST_1)
.endpointOverride(URI.create("http://localhost:4566")) // LocalStackのSESエンドポイント
.build();
public EmailService(SesClient sesClient) {
this.sesClient = sesClient;
}
public void sendEmail(String toAddress, String subject, String body) {
logger.info("メール送信準備: 宛先={}, 件名={}, 本文={}", toAddress, subject, body); // ここでメール内容をログ出力
SendEmailRequest emailRequest = SendEmailRequest.builder()
.destination(Destination.builder()
.toAddresses(toAddress)
.build())
.message(Message.builder()
.subject(Content.builder()
.data(subject) // そのままsubjectを使用
.charset("UTF-8") // 件名のエンコーディングを指定
.build())
.body(Body.builder()
.text(Content.builder()
.data(body) // そのままbodyを使用
.charset("UTF-8") // 本文のエンコーディングを指定
.build())
.build())
.build())
.source("sender@example.com") // 送信元のアドレスを指定
.build();
sesClient.sendEmail(emailRequest);
}
}
・pom.xml(依存関係)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- AWS SDK for SES -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>ses</artifactId>
<version>2.20.55</version>
</dependency>
<!-- Spring Cloud AWS (SES用) -->
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-ses</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
アプリを起動し、リンクもしくはPostmanでAPIを叩き実行
・アプリを起動(ポート番号はデフォルトの8080)
・リンクからAPIを実行しブラウザで確認したい方
※処理が正常終了すれば、「メール送信完了!」とブラウザに表示される
・Poatmanからメール送信APIにリクエスト送信(Get)したい方
※処理が正常終了すれば、ステータス:200と「メール送信完了!」の文字列が返却される
処理完了後にコンテナのログを確認する
・プロジェクト直下(docker-compose.ymlのある階層)で下記コマンドを実行
docker logs <コンテナ名>
// 今回の場合のコマンド例
docker logs localstack-ses
・キャプチャのように「AWS ses.SendEmail => 200」が出力されていればOK
コンテナ内にログインし、実際に送信されたメールの内容を確認する
・プロジェクト直下(docker-compose.ymlのある階層)で下記コマンドを実行
※ログインできるとコマンドを打つ場所が「#」になる
docker exec -it <コンテナ名> /bin/sh
// 今回の場合のコマンド例
docker exec -it localstack-ses /bin/sh
・メール内容が確認できるファイルが格納されている場所まで移動
cd /tmp/localstack/state/ses/
・最新のjsonファイルが一番上に表示されるようにオプションを加え(-lt)、下記コマンドを実行
ls -lt
・下記のような内容が返ってきたら、対象のjsonファイル名をコピペする
※今回の出力内容だと、jsonファイル名にあたるのはここ↓
「yiehwbsibsnqpono-dnrbtbky-yfkv-tcvn-zrre-pgrfossbedoq-djdviw.json」
-rw-r--r-- 1 root root 403 Nov 4 11:29 yiehwbsibsnqpono-dnrbtbky-yfkv-tcvn-zrre-pgrfossbedoq-djdviw.json
・取得したjsonファイル名を元に下記コマンドを実行する
cat <jsonファイル名> | python3 -c "import sys, json; print(json.dumps(json.loads(sys.stdin.read()), ensure_ascii=False, indent=2))"
// 今回の場合のコマンド例
cat yiehwbsibsnqpono-dnrbtbky-yfkv-tcvn-zrre-pgrfossbedoq-djdviw.json | python3 -c "import sys, json; print(json.dumps(json.loads(sys.stdin.read()), ensure_ascii=False, indent=2))"
・下記の内容が返ってくれば成功!!
※pythonコマンドで件名やメールの内容をデコードし、オプションでフォーマットも調整されてる状態
最後に
この記事を見たことで、読者の方自身の気づきであったり、「AWS SES」のLocalStack環境での動作確認やテストの工数削減に少しでも貢献できれば幸いです。最後までお読みいただきありがとうございました。
Discussion