6.2K Views
November 14, 25
スライド概要
JJUG CCC 2025 Fallの資料です。
Java、Spring、IntelliJ IDEA
開発と運用を楽にする Spring Boot on AWS テクニック集 JJUG CCC 2025 Fall 2025年11月15日 多田真敏 1
このセッションについて ◆Spring BootアプリケーションをAWS上で開発・運用する際に、 僕が実際に使っているテクニックをご紹介します。 ◆理想とかけ離れている部分もありますが、 いろいろと試行錯誤した結果です。 ◆僕の考慮が漏れていそうな部分については、 ぜひ優しくフィードバックいただければ幸いです。 2
必要な前提知識 ◆このセッションを理解するには、以下の前提知識が必要です ◆Spring Bootの利用経験 ◆AWSの利用経験 3
前提知識を学べる資料 https://gihyo.jp/book/2023/9 78-4-297-13613-0 https://www.sbcr.jp/ product/4815626044/ 4
環境 ◆JDK 25 ◆AWS LambdaのみJDK 21 バージョンが異なる場合、 この資料の説明が正しくない 可能性があります ◆Spring Boot 4.0.0-RC2 ◆Amazon ECS ◆AWS Lambda 5
自己紹介 ◆多田真敏(@suke_masa) ◆JJUG・JSUGスタッフ ◆クレジットカード会社で 社内システムの内製化+AWS化 ◆OSSドキュメントの和訳 ◆Thymeleaf・Resilience4j ◆ゴジラ、北海道日本ハムファイターズ、 DB.スターマン、櫻坂46 6
目次 ① テストのテクニック ② デプロイのテクニック ③ 耐障害性のテクニック ④ その他のテクニック ⑤ おまけ:Spring Boot 4の話 7
目次 ① テストのテクニック ② デプロイのテクニック ③ 耐障害性のテクニック ④ その他のテクニック ⑤ おまけ:Spring Boot 4の話 8
ローカルでのテストや開発 ◆何も考えずにIDEから起動できるのが理想 ◆起動するために多数の設定が必要だと、なかなか開発に入れない ◆必要な手順は git clone → IDEで起動 のみにしたい しかし ◆ローカルで起動するときの壁 ① DBなど必要なミドルウェアはどうするか? ② 外部システムとの連携部分をどうするか? ③ AWSサービス利用部分はどうするか? 9
①DBなどのミドルウェアをどうするか? ◆Docker Composeで まとめて起動! ◆しかし、docker compose up コマンドを手動で打つの? services: postgresql: container_name: postgres-sample image: postgres:15.10 ports: - "5432:5432" environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=password - POSTGRES_DB=sample 10
spring-boot-docker-compose ◆アプリの起動時に、自動でdocker compose upしてくれる ◆spring.docker.compose.skip.in-tests=false でテスト時も有効に ◆アプリやテストの終了時はdocker compose stopしてくれる ◆application.propertiesに接続情報の記述不要 ◆spring.datasource.* 不要! <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-docker-compose</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> リファレンス -> https://docs.spring.io/spring-boot/reference/features/dev-services.html#features.dev-services.docker-compose 11
②外部システムとの連携部分をどうするか? ◆外部システムは本番環境でしか接続できないことも多い ◆そのままだと、ローカルで実行できない 12
モック+プロファイルで切り替え public interface ExternalClient { ExternalResponse execute(...);} @Profile("local") ローカル用モック → localプロファイルを指定 @Component public class LocalExternalClient implements ExternalClient { @Override public ExternalResponse execute(...) { // モックだと分かるようにログを出す+固定値を返すなど仮の処理を行う}} @Profile("production") 本番用クラス → productionプロファイルを指定 @Component public class ProductionExternalClient implements ExternalClient { @Override public ExternalResponse execute(...) { // 外部システムへのアクセスなど、本当の処理を行う}} 13
プロファイルの指定 ◆application.propertiesに spring.profiles.active=local → 何も指定せずに起動したらlocalプロファイルが指定される ◆AWS上では環境変数SPRING_PROFILES_ACTIVE=productionで productionプロファイルを指定 → プロファイルがlocalのBeanは使われなくなる 14
③AWSサービス利用部分はどうするか? ◆S3とのファイルアップロード/ダウンロード ◆KMSでの暗号化/復号 ◆Secrets Managerからのシークレット取得 ◆・・・など 15
LocalStack ◆ローカルでAWSサービスを再現できる ◆Docker Hubでコンテナイメージも提供されている ◆https://hub.docker.com/r/localstack/localstack 16
LocalStackをDocker Composeで localstack: container_name: localstack-sample image: localstack/localstack:4.7 ports: - "4566:4566" # LocalStack Gateway - "4510-4559:4510-4559" # external services port range volumes: - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" - "/var/run/docker.sock:/var/run/docker.sock" environment: - EAGER_SERVICE_LOADING=1 アクセスキーを設定 - AWS_ACCESS_KEY_ID=test - AWS_SECRET_ACCESS_KEY=test - AWS_DEFAULT_REGION=ap-northeast-1 - DISABLE_CORS_CHECKS=1 - DEBUG=1 - PERSISTENCE=0 17
AWS SDKのBean定義
@Profile("local") @Configuration
LocalStackを向くように設定
public class LocalAwsConfig {
@Bean public S3Client s3Client() {
return S3Client.builder()
.endpointOverride(URI.create("http://localhost:4566"))
.credentialProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")))
.region(Region.AP_NORTHEAST_1)
.forcePathStyle(true)
環境変数で指定したアクセスキーなど
.build();}}
@Profile("production") @Configuration
public class ProductionAwsConfig {
@Bean public S3Client s3Client() {
return S3Client.builder().build();}}
18
テストのテクニック:まとめ ◆DBなどのミドルウェアは、 spring-boot-docker-composeで自動起動 ◆外部システムとの連携部分は、 localプロファイルでモックが動くようにする ◆AWSサービスのテストはLocalStackで行う 19
目次 ① テストのテクニック ② デプロイのテクニック ③ 耐障害性のテクニック ④ その他のテクニック ⑤ おまけ:Spring Boot 4の話 20
今回の対象AWSサービス ① Amazon ECS ◆コンテナ実行環境 ◆Webアプリのような永続的に動くものと、 バッチアプリのようなスケジュールドタスクの両方に対応 ② AWS Lambda ◆「サーバーレス」とか「FaaS」とか言われるやつ ◆2025年11月2日現在、Java 8・11・17・21に対応 21
①Amazon ECS ◆Webアプリや、15分以上かかるバッチ処理に利用しています ◆15分以内のバッチはLambdaにしてます ◆アプリをデプロイするにはコンテナ化が必要 22
ECSの基本用語 ◆タスク ◆アプリやサイドカーなど、 1つ以上のコンテナの集合 ◆ECSにおけるコンテナ管理の最小単位 ◆サービス タスク タスク アプリ コンテナ アプリ コンテナ サイドカー コンテナ サイドカー コンテナ ◆1つのロードバランサーと、 1つ以上のタスクの集合 ◆タスクの数をキープする役割も担う ◆クラスター ◆1つ以上のサービスの集合 ロード バランサー サービス クラスター 23
ecspresso ◆ECSのサービスとタスクをデプロイするコマンド ◆https://github.com/kayac/ecspresso ◆読み方は「エスプレッソ」、作者は@fujiwaraさん ◆クラスター・ロードバランサー・IAMロールなどは他の手段で 作成 ◆僕のチームではTerraformを使ってます ◆Terraformとの連携機能あり ◆ecspresso deployコマンドでデプロイ可能 24
なぜecspressoでデプロイするのか? ◆Terraformでもデプロイ可能だけど・・・ ◆関係ないインフラリソースもデプロイしてしまう可能性 ◆インフラのデプロイ時に、アプリが想定外のデグレード ◆デプロイにTerraformの知識(=インフラの知識)が必要になる ◆ecspressoなら! ◆アプリのタスク・サービスに限定してデプロイ可能! ◆インフラデプロイ時のデグレードが起こらない! ◆コマンドを実行するだけなら、Terraformの知識不要! 25
ecspresso.yml ◆ecspresso全体の設定ファイル region: ap-northeast-1 cluster: クラスター名(他の手段で作成済みのもの) service: 作成するサービス名 service_definition: サービス定義ファイル名(後述) task_definition: タスク定義ファイル名(後述) plugins: - name: tfstate config: url: s3://バケット名/tfstateファイル名 26
サービス定義ファイル
{
ECSのサービス定義JSON
そのまま
"serviceName": "web-app-service",
"loadBalancers": [
{
"targetGroupArn": "{{ tfstate `aws_lb_target_group.web_app.arn` }}",
"containerName": "web-app",
"containerPort": 8080
Terraformで作成した
}
リソースを参照可能
],
"desiredCount": 2,
"launchType": "FARGATE",
"platformVersion": "LATEST",
"platformFamily": "LINUX",
...
}
27
タスク定義ファイル
{
ECSのタスク定義JSON
そのまま
"family": "web-app-task-definition",
"cpu": "512",
"memory": "1024",
"containerDefinitions": [
{
"name": "web-app",
"image": "{{ tfstate `aws_ecr_repository.web_app.repository_url` }}:{{ must_env `TAG` }}",
"user": "webappuser",
"portMappings": [
{
"containerPort": 8080,
環境変数も参照可能
"hostPort": 8080,
"protocol": "tcp"
}
],
...c
28
タスク定義の注意点 ◆セキュリティのため、ルートファイルシステムを変更不可能に する ◆組み込みTomcatが起動時にフォルダを作成するための ボリュームは、別途作成が必要 29
タスク定義ファイル
"volumes": [
Ephemeral Volume
{
を作成
"name": "web-app-ephemeral-volume"
}
],
"containerDefinitions": [
{
ルートファイルシステムを
...
変更不可能に
"readonlyRootFilesystem": true,
"mountPoints": [
{
"containerPath": "/tmp",
"sourceVolume": "web-app-ephemeral-volume",
Ephemeral Volumeを
"readOnly": false
/tmpにマウント
}
]
}
],
30
Dockerfile # ベースイメージ FROM amazoncorretto:21-alpine3.22 ベースイメージに 脆弱性が無いかは要確認 # ユーザーやグループの管理に必要なパッケージをインストール RUN apk add --no-cache shadow # アプリ用ディレクトリを作成 RUN mkdir /app アプリ用のユーザーを作成 # ログイン不要のユーザーを作成 RUN groupadd -r webappgroup && useradd -r -s /usr/sbin/nologin -g webappgroup webappuser WORKDIR /app 31
Dockerfile(続き) # ボリュームを設定 VOLUME /tmp 組み込みTomcatが使うフォルダをVOLUMEに指定 (これができないので、Buildpacksは使っていません) # /appと/tmpの所有権を変更 RUN chown -R webappuser:webappgroup /app \ && chown -R webappuser:webappgroup /tmp # 作成したユーザーで実行 USER webappuser rootユーザーで実行されないよう、 作成したユーザーを指定 COPY ./target/web-app.jar app.jar ENTRYPOINT ["java","-jar","app.jar"] 32
デプロイ いま動いているアプリのバージョンがすぐ分かる →そのバージョンのソースコードをすぐ確認可能 # Gitコミットハッシュをタグにする export TAG=$(git rev-parse --short HEAD) # コンテナイメージをビルド docker image build --platform linux/arm64 --load -t web-app:${TAG} -f Dockerfile . # ECRにログイン aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS \ --password-stdin 1234567890123.dkr.ecr.ap-northeast-1.amazonaws.com # イメージにタグ付け docker image tag web-app:${TAG} \ 1234567890123.dkr.ecr.ap-northeast-1.amazonaws.com/web-app:${TAG} # ECRにイメージをプッシュ docker image push 1234567890123.dkr.ecr.ap-northeast-1.amazonaws.com/web-app:${TAG} # ECSにデプロイ ecspresso deploy これだけでタスクとサービスをデプロイ可能! 33
ECSのまとめ ブログにも書いてます ◆ecspressoでデプロイする ◆ルートファイルシステムを 変更不可能にする ◆組み込みTomcatが使う フォルダはボリュームとして作成 ◆イメージのタグは Gitコミットハッシュにする https://zenn.dev/masatoshi_tada/ articles/1e899786462567 34
②AWS Lambda ◆15分以内のバッチ処理や、AWS関連のイベント処理に 利用しています ◆CloudWatch Logsのサブスクリプションフィルター、 SQSから受け取ったメッセージの処理など 35
JavaでLambda関数を作る ◆aws-lambda-java-coreを依存性に追加 ◆RequestHandlerインタフェース実装クラスを作成 ◆これがLambda関数の処理の起点になる ◆maven-shade-pluginでシングルJARを作成し、デプロイ 36
コード例
RequestHandlerを実装
public class SampleFunction implements RequestHandler<CloudWatchLogsEvent, String> {
private static final Logger logger = LoggerFactory.getLogger(SampleFunction.class);
@Override
public String handleRequest(CloudWatchLogsEvent event, Context context) {
try (MDC.MDCCloseable m1 = MDC.putCloseable(
"aws_request_id", context.getAwsRequestId())) {
AWS Request IDを
// 関数の処理
ログのMDCに入れておく
} catch (Exception e) {
→ログが検索しやすくなる
logger.error("関数が異常終了しました。", e);
throw new RuntimeException("関数が異常終了しました。", e);
}}}
37
lambroll ◆AWS Lambdaのコードをデプロイするコマンド ◆https://github.com/fujiwara/lambroll ◆読み方は「ラムロール」、作者はecspressoと同じ@fujiwaraさん ◆IAMロールなどは他の手段で作成 ◆僕のチームではTerraformを使ってます ◆Terraformとの連携機能あり ◆lambroll deployコマンドでデプロイ可能 38
なぜlambrollでデプロイするのか? ◆Terraformでもデプロイ可能だけど・・・ ◆関係ないインフラリソースもデプロイしてしまう可能性 ◆インフラのデプロイ時に、アプリが想定外のデグレード ◆デプロイにTerraformの知識(=インフラの知識)が必要になる ◆lambrollなら! ◆アプリのタスク・サービスに限定してデプロイ可能! ◆インフラデプロイ時のデグレードが起こらない! ◆コマンドを実行するだけなら、Terraformの知識不要! 39
function.json
◆lambrollの設定ファイル
{
Terraformで作成した
リソースを参照可能
"FunctionName": "sample-function",
"Handler": "com.example.SampleFunction",
"Role": "{{ tfstate `aws_iam_role.sample_function.arn` }}",
"Runtime": "java21",
"Code": {
"S3Bucket": "sample-lambda-code",
JARを配置するS3の
"S3Key": "sample-function.jar"
バケット名+キー名
},
"Architectures": [
"arm64"
],
...
40
デプロイ mvn clean package -Dmaven.test.skip=true これだけで関数をデプロイ可能! lambroll deploy \ (S3へのアップロードもやってくれる) --function="function.json" \ --tfstate="s3://バケット名/tfstateファイル名" \ --src="./target/app.jar" \ --publish \ --alias="latest" エイリアスも設定可能 41
Lambdaのまとめ ◆lambrollでデプロイする ◆@fujiwaraさんありがとう! 42
目次 ① テストのテクニック ② デプロイのテクニック ③ 耐障害性のテクニック ④ その他のテクニック ⑤ おまけ:Spring Boot 4の話 43
2つの耐障害性 ① 自システムに多少の障害があっても処理を継続する ② 連携先システムに多少の障害があっても処理を継続する 44
①自システムの障害対策 ◆複数AZに負荷分散させる ap-northeast-1a ap-northeast-1c アプリ アプリ ◆1つのAZで障害が起きても、 残りのAZで処理を継続可能 ロード バランサー 45
負荷分散の問題点 ◆セッションはメモリで作られる → 前リクエストと違う方に分散されると、そこにセッションが 無い Aさん アプリ ①リクエスト ②レスポンス Aさんの セッション ロード バランサー アプリ ③リクエスト セッション が無い 46
Spring Sessionで解決! ◆セッションを外部ストレージに保存 → どこに負荷分散されてもOK! アプリ Aさん 外部ストレージ Spring Session ロード バランサー Aさんの セッション アプリ Spring Session 47
外部ストレージの種類 ◆RDB: spring-session-jdbc ◆変わらずSpringチーム主導 ◆Redis: spring-session-redis ◆変わらずSpringチーム主導 ◆MongoDB: mongodb-spring-session? ◆MongoDBチーム主導に変更 ◆Hazelcast: hazelcast-spring-session ◆Hazelcastチーム主導に変更 48
Spring Sessionの使い方 ◆RDBの場合、spring-session-jdbcを追加するだけ! <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-jdbc</artifactId> </dependency> 49
Spring Sessionの注意点 ◆ネットワークおよび 外部ストレージへの アクセスにより、 レスポンスが少し遅くなる ◆詳細は右の資料参照 https://www.docswell.com/s/MasatoshiTada/ K1XE12-spring-session 50
②連携先システムの障害対策 ◆連携先システムからエラーが返ってきた ◆別インスタンスにアクセスすれば成功するかも ◆連携先システムやAWSサービスにアクセスしたら タイムアウトになった ◆AWSの瞬間的なネットワーク不具合の影響かも 少し時間を置いてもう一度試せば 成功するかも? 51
Spring Retry ◆リトライを行うライブラリ ◆以前は独立したライブラリだった ◆7.0からSpring Framework本体に取り込まれた ◆パッケージ名 org.springframework.retry → org.springframework.core.retry 52
リトライで解決するもの/しないもの ◆解決するものの例 ◆解決しないものの例 ◆接続タイムアウト ◆SQL文法エラー ◆500 Internal Server Error ◆認証エラー ◆429 Too Many Requests ◆404 Not Found リトライで問題が本当に解決するかどうかは しっかり検討しましょう 53
リトライで考えるべきこと ◆呼び出し元をどれだけ待たせられるか? ◆再試行や待ち時間の分、呼び出し元を待たせることになる ◆リトライ間の待ち時間(バックオフ)をどうするか? ◆固定時間、ランダム、2倍ずつ、など ◆最大リトライ回数を何回にするか? ◆回数が増えるほど呼び出し元を待たせることになる 54
RetryTemplateでリトライ
RetryTemplate retryTemplate = RetryTemplate.builder()
.fixedBackoff(Duration.ofSeconds(2)) // リトライ間隔
.maxAttempts(3) // 最大リトライ回数
.retryOn(List.of( // リトライ対象例外
HogeException.class, FugaException.class))
.build();
retryTemplate.execute(context -> {
// ここにリトライしたい処理を書く
});
55
アノテーション+AOPでリトライ @EnableResilientMethods @Configuration public class AppConfig { ... } // リトライしたいメソッドに付加 @Retryable( delay = 2, // 初回リトライ間隔 multiplier = 1.0, // リトライ間隔を1.0倍ずつ(=一定間隔) maxRetries = 3, // 最大リトライ回数 includes = { // リトライ対象例外 HogeException.class, FugaException.class}, ) public void doSomething() { ... } 56
耐障害性のテクニック:まとめ ◆負荷分散によるセッションの問題はSpring Sessionで解決 ◆リトライで解決できる問題はSpring Retryで解決 57
目次 ① テストのテクニック ② デプロイのテクニック ③ 耐障害性のテクニック ④ その他のテクニック ⑤ おまけ:Spring Boot 4の話 58
構造化ログの活用 ◆ログはJSONにする ◆アプリケーションのバージョンを 判別するために、Gitコミットハッシュを ログに含める https://zenn.dev/masatoshi_tada /articles/7c943497d9b76b 59
Microsoft Entra IDとの連携 ◆ALBにEntra IDを設定 ◆Spring Securityでは AbstractPreAuthenticated ProcessingFilter を利用 https://docswell.com/s/MasatoshiTada /K1R8JP-high-level-spring-security 60
@SpringBootTest ◆テストはすべてコレ ◆@WebMvcTest・@JdbcTestなどは使わない ◆Auto Configurationの微妙な違いでハマる ◆起動時間短縮効果は小さい ◆テストの時間を短縮したいなら@shindo_ryoさんの資料を 読みましょう ◆https://speakerdeck.com/rshindo/spring-fest-2020 61
目次 ① テストのテクニック ② デプロイのテクニック ③ 耐障害性のテクニック ④ その他のテクニック ⑤ おまけ:Spring Boot 4の話 62
Spring Boot 4.0 ◆2025年11月20日リリース予定 ◆11月15日現在、4.0.0-RC2がリリースされている ◆変更点いっぱい → マイグレーションガイドを必ず読んでください ◆特に影響が大きそうな変更点を紹介します 63
ライブラリの構成変更+リネーム ◆spring-boot-starter-web → spring-boot-starter-webmvcにリネーム ◆spring-boot-starter-aop → spring-boot-starter-aspectjにリネーム ◆spring-boot-starter-test → spring-boot-starter-webmvc-testなどに細分化 単にバージョン番号を書き換えるだけでは対応できない → start.spring.ioやマイグレーションガイドを活用して、 ライブラリを書き換えていく 64
RestTemplateの非推奨化予定 ◆Spring Framework 7.1(2026年予定)で非推奨化 → 8.0(時期未定)で削除予定 ◆https://spring.io/blog/2025/09/30/the-state-of-http-clients-inspring#the-future-of-http-clients-in-spring ◆今のうちにRestClientに移行しましょう 65
Jackson 2 → 3 ◆ライブラリのグループIDとパッケージ名が com.fasterxml.jackson → tools.jackson に変更 ◆ただしjackson-annotationsのみ変更無し 違和感アリアリだけど、 これで正しい ◆出力されるJSONに非互換性がありそうな気がするので、 しっかりテストしましょう! 66
もっと詳しいことは槙さんのセッションで! ◆Room G+H 13:15〜 67
ご清聴ありがとうございました! ◆よいSpring & AWSライフを! 68