Spring Bootのプロフィール:dev、test、prodの設定
1. 概要
アプリケーションは通常、複数の環境で実行されます。開発マシンはH2データベースを使用します。テストサーバーは独自のPostgreSQLを使用します。本番環境は実際のクラスタと実際のSMTPを使用します。デプロイのたびに設定ファイルを手動で編集していると、やがて開発設定を誤って本番環境にプッシュしてしまうでしょう。
Spring Bootはこの問題をプロフィールで解決します。この記事を読み終わったら、プロフィールに応じて設定ファイルを分離する方法、@Profileでbeanのオン/オフを切り替える方法、複数の方法でプロフィールを有効化する方法、@ActiveProfilesでテストを書く方法、そしてよくあるエラーを回避する方法を知ることができます。
2. プロフィールとは?
プロフィールは名前付きのラベルで、例えばdev、test、prodです。Springはこのラベルを使って2つのことを決定します:
- どの設定が読み込まれるか:
application-dev.ymlはプロフィールdevがアクティブな場合にのみ読み込まれます。 - どのbeanが作成されるか:
@Profile("prod")が付いたbeanはプロフィールprodがアクティブな場合にのみ登録されます。
プロフィールが有効化されていない場合、Springはデフォルトという名前のプロフィールを使用します。@Profileが付いていないbeanは、どのプロフィールがアクティブであるかに関わらず、常に作成されます。
この記事の例では、基本的なweb依存関係のみが必要です:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
3. プロフィール別の設定ファイル
3.1. 複数のファイルに分割する
命名規則はapplication-{profile}.yml(または.properties)です。ファイルapplication.ymlは共通の設定を含み、常に読み込まれます:
# application.yml
spring:
application:
name: shop-service
profiles:
default: dev
app:
mail:
from: no-reply@shop.vn
host: localhost
プロフィール別のファイルは異なる部分のみを記述します:
# application-dev.yml
server:
port: 8080
logging:
level:
root: DEBUG
# application-test.yml
server:
port: 0
app:
mail:
host: fake-smtp
# application-prod.yml
server:
port: 80
logging:
level:
root: WARN
app:
mail:
host: ${MAIL_HOST}
説明:
spring.profiles.default: devとは、プロフィールを有効化しない場合、Springはdefaultではなくdevを使用することを意味します。開発マシンはパラメータなしで直ぐに実行できます。- プロフィール
prodがアクティブな場合、Springは最初にapplication.ymlを読み込み、次にapplication-prod.ymlを読み込みます。プロフィールファイル内の値は共通の値をオーバーライドします。したがって、app.mail.hostは環境変数MAIL_HOSTの値になり、app.mail.fromはno-reply@shop.vnのままです。 testのserver.port: 0は、Springがランダムなポートを選択するようにして、並列実行時の競合を回避します。
3.2. 1つのYAMLファイルに統合する
ファイルを減らしたい場合は、---で区切られたYAML複数ドキュメントを使用できます:
# application.yml
app:
mail:
host: localhost
---
spring:
config:
activate:
on-profile: prod
app:
mail:
host: smtp.shop.vn
2番目のドキュメントはprodがアクティブな場合にのみ有効です。この方法は各プロフィールの違いが少ない場合にコンパクトです。設定が長い場合、ファイルを分割する方が読みやすくレビューしやすいです。
注意:
.propertiesファイルでは、#---行を使って---の代わりにドキュメントを分割します。
4. プロフィールの有効化
多くの方法があります。優先順位はSpring Bootの一般的なルールに従います:コマンドライン引数 > 環境変数 > 設定ファイル。
コマンドライン引数:
java -jar shop-service.jar --spring.profiles.active=prod
JVMシステムプロパティ:
java -Dspring.profiles.active=prod -jar shop-service.jar
環境変数は、DockerおよびKubernetesで最も一般的な方法です:
export SPRING_PROFILES_ACTIVE=prod
java -jar shop-service.jar
Mavenプラグインで実行する場合:
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
コードで指定する、特定のプロフィールを常に追加したい場合に便利です:
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ShopApplication.class);
app.setAdditionalProfiles("metrics");
app.run(args);
}
}
setAdditionalProfilesはプロフィールをリストに追加し、外部から有効化されたプロフィールを置き換えません。--spring.profiles.active=prodで実行すると、prodとmetricsの両方がアクティブになります。
カンマで複数のプロフィールを同時に有効化できます:--spring.profiles.active=prod,eu。2つのファイルが同じキーを定義している場合、後ろのプロフィールが優先されます。この例では、application-eu.ymlがapplication-prod.ymlをオーバーライドします。
アプリケーションが起動すると、ログにどのプロフィールが実行されているかが表示されます:
INFO c.e.shop.ShopApplication : The following 1 profile is active: "prod"
プロフィールがなく、spring.profiles.defaultも設定されていない場合、以下が表示されます:
INFO c.e.shop.ShopApplication : No active profile set, falling back to 1 default profile: "default"
デプロイ時には常にこのログを確認してください。これはサーバーが誤ったプロフィールで実行されていることを検出する最速の方法です。
5. @Profileを使ってbeanのオン/オフを切り替える
異なる設定はほんの一部に過ぎません。時には全く異なる実装が必要な場合があります。例えば、devはメールをコンソールに出力するだけ、testはメモリに保存して検証、prodはSMTPで送信する場合です。
共通インタフェースを定義します:
public interface NotificationService {
void send(String to, String message);
}
dev用の実装:
@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
System.out.println("[DEV] 送信先 " + to + ": " + message);
}
}
test用の実装で、メッセージをassertするために保存します:
@Service
@Profile("test")
public class InMemoryNotificationService implements NotificationService {
private final List<String> sentMessages = new CopyOnWriteArrayList<>();
@Override
public void send(String to, String message) {
sentMessages.add(to + "|" + message);
}
public List<String> getSentMessages() {
return List.copyOf(sentMessages);
}
}
prod用の実装:
@Service
@Profile("prod")
public class SmtpNotificationService implements NotificationService {
private final String host;
public SmtpNotificationService(@Value("${app.mail.host}") String host) {
this.host = host;
}
@Override
public void send(String to, String message) {
// 実際にはJavaMailSenderを使用します。ここでは説明のため省略
System.out.println("[SMTP " + host + "] 送信先 " + to);
}
}
使用側のクラスは、どの環境にいるかを知る必要がありません:
@Service
public class OrderService {
private final NotificationService notificationService;
public OrderService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void placeOrder(String customerEmail, long orderId) {
notificationService.send(customerEmail, "ご注文 #" + orderId + " が作成されました");
}
}
同時に、3つのNotificationService beanのうち1つだけが作成されます。Springは適切なbeanをOrderServiceにインジェクトします。プロフィールdevで実行してplaceOrder("an@shop.vn", 42)を呼び出すと、以下が得られます:
[DEV] 送信先 an@shop.vn: ご注文 #42 が作成されました
5.1. プロフィール式
@Profileはロジック式をサポートしています:
@Configuration
public class SecurityConfig {
@Bean
@Profile("!prod")
public CommandLineRunner seedDemoData() {
return args -> System.out.println("デモデータを読み込み中...");
}
@Bean
@Profile("dev | test")
public CommandLineRunner relaxedCors() {
return args -> System.out.println("CORSをすべてのオリジンで有効化");
}
}
!prod:prodを除くすべてのプロフィールでbeanを作成します。dev | test:devまたはtestがアクティブな場合に作成します。「かつ」を表す&もあります、例えばprod & eu。
@Profileは@Component/@Configurationクラスまたは個々の@Beanメソッドに付けることができます。@Configurationに付けると、内部のすべてのbeanが同じ条件の対象になります。
5.2. コードでプロフィールを確認する
時には、ロジックで分岐する必要があります。Environmentを使用してください:
@Component
public class StartupBanner implements CommandLineRunner {
private final Environment environment;
public StartupBanner(Environment environment) {
this.environment = environment;
}
@Override
public void run(String... args) {
if (environment.acceptsProfiles(Profiles.of("prod"))) {
System.out.println("本番環境で実行中");
}
}
}
この方法は控えめに使用してください。コードがif (prod)で満ちている場合は、@Profileを持つ別のbeanに分割する必要があります。
6. プロフィールグループ
システムが大きくなるにつれて、設定を細かく分割したい場合があります。例えば、proddbはデータベース用、prodmqはメッセージブローカー用です。プロフィールグループにより、1つの名前でクラスタ全体を有効化できます:
# application.yml
spring:
profiles:
group:
prod: proddb, prodmq
dev: devdb
これで--spring.profiles.active=prodを実行すると、prod、proddb、prodmqがアクティブになります。Springは順番にapplication-prod.yml、application-proddb.yml、application-prodmq.ymlを読み込みます。オペレータは1つの名前を覚えるだけで済みます。
7. @ActiveProfilesでテストを書く
テストでは、@ActiveProfilesを使ってプロフィールを選択します。以下のテストは2つのことを確認します:正しいbeanがインジェクトされること、およびapplication-test.ymlからの設定が有効であること。
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceProfileTest {
@Autowired
private OrderService orderService;
@Autowired
private NotificationService notificationService;
@Value("${app.mail.host}")
private String mailHost;
@Test
void shouldUseInMemoryNotificationInTestProfile() {
assertThat(notificationService).isInstanceOf(InMemoryNotificationService.class);
}
@Test
void shouldRecordNotificationWhenOrderPlaced() {
orderService.placeOrder("binh@shop.vn", 7);
var inMemory = (InMemoryNotificationService) notificationService;
assertThat(inMemory.getSentMessages())
.contains("binh@shop.vn|ご注文 #7 が作成されました");
}
@Test
void shouldOverrideMailHostFromTestProfile() {
assertThat(mailHost).isEqualTo("fake-smtp");
}
}
説明:
@ActiveProfiles("test")は、spring.profiles.defaultが何であれ、このクラスのコンテキストに対してプロフィールtestをアクティブ化します。- 最初のテストは、Springが実際のメール送信ではなく
InMemoryNotificationServiceを選択することを確認します。 - 2番目のテストは、メモリ内beanを使用してメッセージの内容を確認し、モックは必要ありません。
- 3番目のテストは、
application-test.ymlからのapp.mail.hostがapplication.ymlのlocalhost値をオーバーライドしていることを示しています。
注意: プロフィールの組み合わせが異なるたびに、別々のSpringコンテキストが作成されます。各テストクラスが異なるプロフィール群を使用する場合、コンテキストキャッシュが再利用されず、テストスイートが大幅に低下します。テスト内のプロフィール組み合わせの数を最小限に保ってください。
8. よくあるエラー
プロフィール別ファイルにspring.profiles.activeを設定する。 Spring Boot 2.4以降、application-dev.yml内(またはプロフィール固有のドキュメント内)にspring.profiles.activeまたはspring.profiles.includeを宣言するとアプリケーションが起動しなくなります:
InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from
location 'class path resource [application-dev.yml]' is invalid in a profile specific resource
代わりにプロフィールグループ(セクション6)を使用してください。
application.ymlにspring.profiles.active=devをハードコードする。 このファイルはjarにパッケージ化されます。サーバーで誰かが環境変数の設定を忘れると、prodが開発設定で実行されます。ローカル環境にはspring.profiles.defaultを使用し、サーバーではプロフィールを明示的に有効化してください。
application-prod.ymlに秘密を置く。 データベースのパスワードやAPIキーはGitに置くべきではありません。${DB_PASSWORD}のようなプレースホルダーを使用し、環境変数またはシークレットマネージャーで値を提供してください。
プロフィールに一致するbeanがない。 stagingプロフィールで実行しているが、dev、test、prodの実装しかない場合、アプリケーションはエラーを報告します:
Parameter 0 of constructor in OrderService required a bean of type
'NotificationService' that could not be found.
フォールバックbeanを@Profile("!prod")で検討するか、新しいプロフィールの実装を追加してください。
フィーチャーフラグにプロフィールを乱用する。 プロフィールは環境を説明するのに適しています。環境に関係なく機能をオン/オフにしたい場合、@ConditionalOnPropertyの方が通常明確です:
@Bean
@ConditionalOnProperty(name = "app.feature.loyalty-points", havingValue = "true")
public LoyaltyService loyaltyService() {
return new LoyaltyService();
}
prod-with-loyaltyやdev-no-cacheのようなプロフィールがある場合、それはプロパティに切り替える時期のサインです。
9. まとめ
プロフィールは、コードを変更せずに環境ごとに設定と動作を分離するのに役立ちます。application-{profile}.ymlを使って設定をオーバーライドする方法、@Profileで実装を選択する方法、プロフィールグループで複数のプロフィールをグループ化する方法、@ActiveProfilesでテストする方法を学びました。
覚えておくべき原則がいくつかあります:共通設定をapplication.ymlに置き、プロフィールファイルには異なる部分のみ記述してください。サーバーでプロフィールを環境変数またはコマンドライン引数で有効化してください。Gitに秘密を入れないでください。そして、デプロイのたびにThe following profiles are activeログを常に確認してください。