Unleash + Spring Bootでフィーチャーフラグを実装する
フィーチャーフラグとは
フィーチャーフラグは、フィーチャートグル、フィーチャースイッチ等とも呼ばれます。フィーチャーフラグはコードを変更せずにシステムの動作を変更するテクニックの総称です。フィーチャーフラグを使うことによって、
- 開発途上のコードをmainブランチにマージしても無効化して動作しないようにする事ができ、頻繁にmainブランチにマージして開発ブランチが長期間存在しないようにして、マージ時のコンフリクトを減らす事ができ、また
- フラグによってA/Bテストを実行する事もでき、
- 新しい機能やライブラリを導入した事による悪影響があった場合に、切り戻しのデプロイをせずに機能を無効化する事によって元に戻す事ができ、
- 社内ユーザやベータユーザにのみ新機能を公開する事ができ
るようになります。
Unleashはオープンソースのフィーチャーフラグ管理のプラットフォームです。本記事ではUnleashのサーバを構築して、Spring Bootからフィーチャーフラグの値を取得する実装方法を解説します。
UnleashをローカルPCでセットアップ
UnleashのサーバをローカルPCでセットアップします。(本記事での方法はローカルPC上での動作確認のための方法であり、デフォルトのパスワードを使用するなどのセキュリティ上の問題があります。本番環境で使用する場合は別の方法でセットアップするようにしてください。)
Unleashサーバの起動
UnleashのサーバはDockerイメージとして提供されているので、Dockerを使ってローカルPC上で起動させます。適切なディレクトリ上で、以下のようにUnleashサーバを起動します。
wget https://raw.githubusercontent.com/Unleash/unleash/refs/heads/main/docker-compose.yml
docker compose up -d
管理画面へのサインイン
ブラウザで http://localhost:4242
にアクセスします。以下のサインイン画面が表示されます。以下の認証情報を入力してSign in
ボタンをクリックします。
User name: admin
Password: unleash4all
フィーチャーフラグの作成
左上のメニューから Projects
を選択します。
Default
プロジェクトを選択します。
Create flag
ボタンをクリックします。
Feature flag name
、Description
にフラグの名前と説明を入力し Create feature flag
ボタンをクリックします。本記事ではタスク管理サービスで締め切り日時の管理機能の追加を例に解説するので名前を support-due-date-time
としました。
トークンの発行
Connect SDK
ボタンをクリックします。
Server side SDKs
のJava
を選択します。
Enviroment
を選択して、Next
ボタンをクリックします。今回はローカルPCで動かすだけなのでdevelopment
を選択しています。
接続のための設定が表示され、クライアントからの接続待ち状態になります。
Spring Bootからの接続テスト
Unleashサーバでの準備ができたので、サーバに接続できるかをテストします。
Spring Bootプロジェクトの作成
Spring Intializrで以下のように設定して GENERATE
ボタンをクリックします。
プロジェクトをダウンロードして適切なディレクトリで展開します。
pom.xmlに以下を追加します。テストが終わったらこの記述は削除します。
<dependency>
<groupId>io.getunleash</groupId>
<artifactId>unleash-client-java</artifactId>
<version>LATEST</version>
</dependency>
Spring BootのApplicationクラスに以下のように記述して動作確認します。テストが終わったら追加した喜寿るは削除します。
@SpringBootApplication
public class UnleashDemoApplication {
public static void main(String[] args) {
SpringApplication.run(UnleashDemoApplication.class, args);
// UnleashClientを初期化
UnleashConfig config = UnleashConfig.builder()
.appName("unleash-onboarding-java")
.instanceId("unleash-onboarding-instance")
.unleashAPI("http://localhost:4242/api/")
.apiKey("default:development.unleash-insecure-api-token") // in production use environment variable
.build();
Unleash unleash = new DefaultUnleash(config);
while (true) {
boolean featureEnabled = unleash.isEnabled("support-due-date-time");
System.out.println("Feature enabled: " + featureEnabled);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
Spring Bootのプロジェクトの直下のディレクトリで、以下のようにSpring Bootを起動してUnleashサーバに接続できるかをテストします。コンソールに Feature enabled: false
が繰り返し表示されれば成功です。
$ ./mvnw spring-boot:run
[INFO] Scanning for projects...
# 中略
[INFO] Attaching agents: []
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.0)
# 中略
2024-11-30T12:59:56.615+09:00 INFO 75693 --- [unleash-demo] [ main] i.g.repository.FeatureBackupHandlerFile : Unleash will try to load feature toggle states from temporary backup
Feature enabled: false
Feature enabled: false
Feature enabled: false
接続が成功するとUnleashの管理画面のダイアログのConnection status
がConnected
に変化します。
フィーチャーフラグを使ったAPIサービスの実装
タスク管理アプリのAPIサーバで、フィーチャーフラグが有効になるとレスポンスに締め切り日時のdueDateTime
フィールドが追加されるケースを考えてみます。
締め切り機能追加前の状態を実装
締め切りがサポートされる前のController、Service、Responseは以下の通りだったとします。
@RestController
@RequestMapping("/tasks")
public class TasksController {
private final GetTasksUsecase getTasksUsecase;
public TasksController(GetTasksUsecase getTasksUsecase) {
this.getTasksUsecase = getTasksUsecase;
}
@GetMapping
public TasksResponse getTasks() {
return new TasksResponse(getTasksUsecase.getTasks());
}
}
public interface GetTasksUsecase {
List<Task> getTasks();
}
@Service
public class GetTaskService implements GetTasksUsecase {
@Override
public List<Task> getTasks() {
// 説明を簡略化するためリポジトリは作成せずに固定のデータを返す実装にしています。
return List.of(
new Task("1", "task1", "description1"),
new Task("2", "task2", "description2"),
new Task("3", "task3", "description3")
);
}
}
@Data
public class TasksResponse {
private final List<Task> results;
}
@Data
public class Task {
private final String id;
private final String title;
private final String description;
}
上記を実装して、Spring Bootのプロジェクトのディレクトリ直下で、以下のようにSpring Bootを起動します。
./mvnw spring-boot:run
アプリケーションを起動後に、別のコンソール画面でタスク取得APIを呼び出すと以下のレスポンスが返ってきます。
$ curl -s http://localhost:8080/tasks | jq .
{
"results": [
{
"id": "1",
"title": "task1",
"description": "description1"
},
{
"id": "2",
"title": "task2",
"description": "description2"
},
{
"id": "3",
"title": "task3",
"description": "description3"
}
]
}
締め切り機能をフィーチャーフラグと共に実装
Spring Bootプロジェクトの構成の変更
まず、pom.xmlファイルでテスト接続の記述を以下のように書き換えます。
<dependency>
<groupId>io.getunleash</groupId>
<artifactId>springboot-unleash-starter</artifactId>
<version>1.1.0</version>
</dependency>
次に、Unleashサーバへの接続構成をapplication.yamlに記述します。
io:
getunleash:
app-name: unleash-onboarding-java
instance-id: unleash-onboarding-instance
environment: development
api-url: "http://localhost:4242/api"
api-token: "default:development.unleash-insecure-api-token"
締め切り機能の実装
準備ができたら、Controller、Service、Responseクラスを変更して締め切り機能を実装していきます。
ユースケースのインタフェースに、以下のようにUnleashのコンテキストやアノテーションを追加します。 @Toggle
のname
はUnleashの管理画面で登録したフィーチャーフラグの名前を指定します。alterBean
は後述の、フィーチャーフラグが有効になった時に使うServiceの名前です。
public interface GetTasksUsecase {
@Toggle(name = "support-due-date-time", alterBean = "getTasksServiceNew")
List<Task> getTasks(UnleashContext context);
}
Serviceはフィーチャーフラグの有効/無効の2つの状態に対応したものを実装します。
フラグが無効の場合用
@Service("getTaskServiceOld")
public class GetTaskService implements GetTasksUsecase {
@Override
public List<Task> getTasks(UnleashContext context) {
// 説明を簡略化するため固定のデータを返しています。
return List.of(
new Task("1", "task1", "description1"),
new Task("2", "task2", "description2"),
new Task("3", "task3", "description3")
);
}
}
フラグが有効の場合用
@Service("getTasksServiceNew")
public class GetTaskServiceDue implements GetTasksUsecase {
public List<Task> getTasks(UnleashContext context) {
// 説明を簡略化するため固定のデータを返しています。
return List.of(
new TaskDue("1", "task1", "description1", "2021-01-01T00:00:00Z"),
new TaskDue("2", "task2", "description2", "2021-01-02T00:00:00Z"),
new TaskDue("3", "task3", "description3", "2021-01-03T00:00:00Z")
);
}
}
ここで TaskDue
クラスは下記の通りです。
@Data
@EqualsAndHashCode(callSuper = true)
public class TaskDue extends Task {
private String dueDateTime;
public TaskDue(String id, String title, String description, String dueDateTime) {
super(id, title, description);
this.dueDateTime = dueDateTime;
}
}
Controllerでは、ユースケースに機能が無効になっている場合の Qualifier
を指定しています。またユースケース呼び出し時には、Unleashのコンテキストを渡すようにします。
@RestController
@RequestMapping("/tasks")
public class TasksController {
private final GetTasksUsecase getTasksUsecase;
public TasksController(@Qualifier("getTaskServiceOld") GetTasksUsecase getTasksUsecase) {
this.getTasksUsecase = getTasksUsecase;
}
@GetMapping
public TasksResponse getTasks() {
return new TasksResponse(getTasksUsecase.getTasks(UnleashContext.builder().build()));
}
}
この状態では、Unleashサーバ側でのフィーチャーフラグが有効になっていないので無効の場合のServiceクラスが呼ばれ、締め切りのレスポンスは返ってきません。
締め切りフィーチャーフラグの有効化
Unleashの管理画面で support-due-date-time
のフィーチャーフラグを develop
環境で有効化します。
有効化してしばらくすると締め切りの入ったレスポンスが返ってくるようになります。
$ curl -s http://localhost:8080/tasks | jq .
{
"results": [
{
"id": "1",
"title": "task1",
"description": "description1",
"dueDateTime": "2021-01-01T00:00:00Z"
},
{
"id": "2",
"title": "task2",
"description": "description2",
"dueDateTime": "2021-01-02T00:00:00Z"
},
{
"id": "3",
"title": "task3",
"description": "description3",
"dueDateTime": "2021-01-03T00:00:00Z"
}
]
}
Discussion