ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

Lombokを利用した開発環境でGradleからJavadocを出力する

エンジニアの友野です。久しぶりのポストですが小ネタです。

日頃、開発時はIDE上でJavadocを参照していたので気にしていませんでしたが、ふと、現状整理するために俯瞰した全体のJavadocドキュメントが欲しくなりました。Lombokコンパイル時にコード生成するため、Javadoc生成時にはLombokが生成するコードがドキュメント化されません。ちなみに、onMethodのように生成したコードにアノテーション付与などするオプションではそもそもJavadoc生成時にビルドエラーになりました。

Lombokにはアノテーションからコード生成するdelombokという機能があります。つまり、delombokしてからJavadocを生成すればLombokが生成したコードのJavadoc化ができるはず。ということで試してみました。

環境

本記事のサンプルコードは以下の環境で動作確認しています。

JVM: OpenJDK Runtime Environment Corretto-11.0.12.7.2 (build 11.0.12+7-LTS)
Gradle: 6.6.1
Lombok: 1.18.12

プラグインの存在

まず初めに、gradle-lombokというプラグインが存在します。このプラグインは以下の機能を提供しており、これを使えばやりたいことは簡単にできます。

it adds the Lombok dependency to the classpath
it simplifies the Eclipse IDE installation
it offers support for delomboking
(訳)
Lombokの依存関係をクラスパスに追加
Eclipse IDEへのインストールの簡易化
delombokのサポート

しかし、以下のいくつかの理由で、このプラグインを参考に自分で書いてみることにしました。

  • すでにLombokは依存関係に追加済み
  • 開発で使用しているIDEIntellij
  • delombok以外の機能は使わなさそう
  • プラグインのコードを見るとシンプルなJavaExecタスクである
  • このプラグインは開発停止中、不具合対応のみ行う状態である

delombokタスクの実装

JavaExecタスクということは、通常のjavaコマンドによる実行をスクリプトに置き換えればうまくいきます。javaコマンドでdelombokを実行するのは以下の通りです(公式サイトから引用)。srcがdelombok対象のディレクトリ、src-delombokedがdelombok結果を格納するディレクトリです。

$ java -jar lombok.jar delombok src -d src-delomboked

これをJavaExecタスクにすると以下のように書けます。lombok.jarはすでにコンパイルのクラスパスに含まれるので、mainを直接指定しています。argsでdelombokを文字列で渡すことで、上記コマンドラインと同じ結果が得られます。
今回はプロダクションコードのJavadocだけで良いので、ソースにはsrc/main/javaを指定しました。

task delombok(type: JavaExec, dependsOn: compileJava) {
    ext.outputDir = file("$buildDir/delombok")

    classpath = sourceSets.main.compileClasspath + sourceSets.main.output.generatedSourcesDirs
    main = 'lombok.launch.Main'
    args('delombok', 'src/main/java', '-d', outputDir)

    doFirst {
        outputDir.deleteDir()
    }
}

JPAメタクラスコンパイルの依存に含んでいるので、compileJavaタスクに依存させてannotationProcesserの出力先も含めていますが、Javadoc出力のためだけであれば、これがなくともdelombokタスクでエラーが出るだけなので動作には問題ありません(気持ちの問題だけ)。

Javadocタスクの設定

次にJavadocを出力する設定です。javaプラグインをインポートしていればタスク定義はすでにあるので、設定だけ追加します。

apply plugin: 'java'

/* JavadocでJava8以降のタグを出力するためのオプション */
def tagOptions = ["apiNote:a:API Note:", "implSpec:a:Implementation Requirements:", "implNote:a:Implementation Note:"]
/* Javadoc生成時のクラスパス */
def javadocClasspath = sourceSets.main.compileClasspath + sourceSets.main.output.generatedSourcesDirs + files("$buildDir/delombok")

javadoc {
    dependsOn([compileJava, delombok])
    classpath = javadocClasspath
    source = delombok.outputDir
    options.tags = tagOptions
    failOnError = false
}

実行は以下の通りです。

$ ./gradlew javadoc

これでdelombokしたソースからJavadocを出力できました!

今回はjavadocの設定で例示しましたが、特定のクラス群だけフィルタしたい場合などは別のJavadocタスクを定義して、sourceに渡す対象を変えてあげれば可能です。

それでは良いJavadocライフを!

transformプロパティを用いたアニメーション改善

こんにちは、id:j-horikawaです。

昨今のUIはリッチ化が進みインタラクティブなデザインを実現するためアニメーションが多く使われています。

アニメーションを多用すると、コンポーネントのアニメーションがカクつく、もたつくなど処理が重くなりがちです。

今回はアニメーションのパフォーマンスに関する1つの改善方法としてDOMのレンダリングに絞った対応策を探ってみましょう。

今回改善したいアニメーション

f:id:j-horikawa:20210907121241g:plain

要素が左から右へ400px移動するだけのシンプルな実装なので実際に処理が重くなることはないのですが、処理が重くなりアニメーションがカクついているという前提でお話を進めます。

HTML

<div class="animation-cpu">CPU</div>

CSS

.animation-cpu {
  animation-name: position-element;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-timing-function: ease;
  background-color: gray;
  color: white;
  width: 100px;
  height: 100px;
  position: relative;
  left: 0;
  top: 0;
}

@keyframes position-element {
  100% {
    left: 400px;
  }
}

問題箇所の調査

Chromeをお使いの場合、DevToolsのRenderingタブにあるPaint flashingを有効にしてみましょう。

Renderingタブがない場合は下ペインの左にある「︙」からRenderingを選択すると表示されます。

f:id:j-horikawa:20210907121441p:plain

Paint flashingを有効にするとページ上で再描画が発生したエリアが緑色でハイライトされ、DOMのリペイントが行われていることがわかります。

f:id:j-horikawa:20210907121540g:plain

このリペイントは要素の位置が変更されるたびにCPUが使用されペイント処理が走るため、使用箇所が多くなるとCPUの負荷が高くなっていきます。

リペイントによる処理の負荷を軽減させる

今回の左から右へ要素が移動する処理、つまりright, left, top, bottomによる位置の移動にはCPUが使用されるので、GPUを使用する transformプロパティに変更します。

HTML

<div class="animation-gpu">GPU</div>

CSS

.animation-gpu {
  animation-name: transform-css;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-timing-function: ease;
  background-color: gray;
  color: white;
  width: 100px;
  height: 100px;
}

@keyframes transform-css {
    0% { transform: translate(0px); }
    100% { transform: translate(400px); }
}

要素が左から右へ400px移動する処理を transform に置き換えました。

Paint flashingを有効にした状態で比較してみると transformを使用した要素がリペイントされていないことが分かります。

f:id:j-horikawa:20210907121632g:plain

対象ブラウザの使用率によるバランスの良い設計が必要

GPUを使った処理が早いとは言え今回ご紹介した改善方法はアニメーションの描画に関する速度改善です。

GPUを使ったハードウェア・アクセラレーションを行うとメモリの使用率が高くなり端末への負荷が高くなります、改善によって新しいボトルネックが生まれないか慎重に確認を行いながらパフォーマンス改善を行うと良いでしょう。

最後に

ContractSはエンジニア・デザイナーを募集しています。 共にプロダクト作りを通して、社会・組織を良くしていきましょう! 興味がある方はぜひこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

SpringBoot+Slackアプリでメッセージ通知を実装してみる

こんにちは、id:c-terashimaです。
技術書典11で無料配布している「Holmes Tech Book」ですが、多くの方にダウンロードしていただいております!ありがとうございます!!
ドメイン駆動設計やmiroアプリの作成などバラエティに富んだ内容になっていますので、ぜひダウンロードしていただけると幸いです。

techbookfest.org

さて、今回はJava(SpringBoot)からSlackへメッセージ送信を試してみました。

Slackアプリを作る

Slackとアプリのやり取りを行うためのアプリを作成します。作成するSlackアプリからDMが届くことを想定しています。

  1. SlackAPIのアプリページに移動して、「Create New App」をクリックします f:id:c-terashima:20210719152014p:plain
  2. 「App Name」に作成するアプリ名、「Pick a workspace to develop your app in:」に使用するワークスペースを選択し、「Create App」ボタンをクリックします f:id:c-terashima:20210719152450p:plain
  3. 「OAuth&Permissions」にアプリに許可させたい権限を付与させます。今回は「chat:write」を付与します f:id:c-terashima:20210719162232p:plain
  4. 「Redirect URLs」に認証時に呼び出されるエンドポイントを指定します。後述で作成する「/slack/oauth」を指定します。SSL化されたエンドポイントを指定する必要があるのでご注意ください。(画像はダミー) f:id:c-terashima:20210720094057p:plain
  5. 「App Home」に移動して「Display Name」と「Default username」を入力します f:id:c-terashima:20210719163418p:plain
  6. 「OAuth&Permissions」の設定が完了したら、作ったアプリをワークスペースにインストールします f:id:c-terashima:20210719162650p:plain
  7. 「Basic Information」に移動して「Client ID」と「Client Secret」をメモします。この2つはホームズクラウドなどのアプリケーションからメッセージを書き込む際に必要となります

アプリとの認証

サンプルは以下の環境で動作確認をしております。

  • Java8
  • SpringBoot 2.3

Slack SDK

build.gradledependencies に以下を追加してSlack SDKをインストールします。

dependencies {
    implementation 'com.slack.api:slack-api-client:1.7.1'
}

アプリケーションとの認証

Slackユーザとアプリケーションユーザの紐付けを行います。まずは認証用のボタンを表示します。

<a href="https://slack.com/oauth/v2/authorize?client_id=「Client ID」&scope=chat:write&state=「任意のデータ」">
  <img alt="Add to Slack"
    height="40"
    width="139"
    src="https://platform.slack-edge.com/img/add_to_slack.png"
    srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" />
</a>

client_id には作成したSlackアプリの「Client ID」を指定します。
state はSlackからアプリケーションの認証エンドポイントが実行された際に設定される値になります。認証用エンドポイントの呼び元がこのアプリによって実行されたのかをチェックするのに利用します。

次にSlackから呼ばれるエンドポイントを作成します。

@RestController
@RequiredArgsConstructor
@Slf4j
public class SlackController {
    private static final String STATE = "state_id";
    private final String slackClientId = "client_id";
    private final String slackClientSecret = "client_secret";

    @GetMapping("/slack/oauth")
    @SneakyThrows
    public ResponseEntity<Void> oauth(@RequestParam("code") String code, @RequestParam("state") String state) {
        // stateの確認
        if (!Objects.equals(state, STATE)) {
            log.error("stateが一致しません。{}", state);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        try (Slack slack = Slack.getInstance()) {
            // Slackアプリとの認証
            OAuthV2AccessResponse response = slack.methods()
                    .oauthV2Access(req -> req.code(code).clientId(slackClientId).clientSecret(slackClientSecret));

            if (!response.isOk()) {
                log.error("認証に失敗しました。{}", response.getError());
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
            }

            // SlackユーザID、Slackアプリのトークンを取得
            // メッセージを送信するのに必要になるため、DBかファイルに保存しておこう
            String slackUserId = response.getAuthedUser().getId();
            String slackAccessToken = response.getAccessToken();
        }

        // 認証後のリダイレクト先を指定する
        HttpHeaders headers = new HttpHeaders();
        UriComponentsBuilder builder =
                UriComponentsBuilder.fromUri(URI.create("http://localhost:8080/slack"));
        headers.setLocation(URI.create(builder.toUriString()));

        return new ResponseEntity<>(headers, HttpStatus.MOVED_PERMANENTLY);
    }
}

「Add to Slack」ボタンをクリックすると次の画面が表示されますので、「許可をする」ボタンをクリックすることで上で作ったエンドポイントが呼び出されます。
f:id:c-terashima:20210719184234p:plain

Slackへ通知

通知するメッセージはJSON形式で指定します。(Block Kit Builder)https://app.slack.com/block-kit-builder/T3HBJHYRHを利用するとかんたんに作ることができます。
以下のメッセージを送ってみたいと思います。
f:id:c-terashima:20210719185834p:plain

認証を行ったユーザにメッセージを送るソースは次になります。

private String slackAccessToken = "accessToken";
private String slackUserId = "userId";

@SneakyThrows
public void postApprovalRequestMessage() {
    try (Slack slack = Slack.getInstance()) {
        MethodsClient client = slack.methods(slackAccessToken);

        ConversationsOpenResponse openResponse =
                client.conversationsOpen(req -> req.users(Arrays.asList(slackUserId)));
        if (!openResponse.isOk()) {
            throw new Exception("DMを開くことができませんでした");
        }

        String message =
                "[\n" +
                "    {\n" +
                "        \"type\": \"section\",\n" +
                "        \"text\": {\n" +
                "            \"type\": \"mrkdwn\",\n" +
                "            \"text\": \"こんにちは!メッセージを送るよ!!\"\n" +
                "        }\n" +
                "    }\n" +
                "]\n";


        ChatPostMessageResponse messageResponse = client.chatPostMessage(req ->
                req.channel(openResponse.getChannel().getId())
                .blocksAsString(message)
        );
        if (!messageResponse.isOk()) {
            throw new Exception("DMを送ることができませんでした." + messageResponse.getError());
        }

        ConversationsCloseResponse closeResponse = client.conversationsClose(req ->
                req.channel(openResponse.getChannel().getId()));
        if (!closeResponse.isOk()) {
            throw new Exception("DMを閉じることができませんでした");
        }
    }
}

実行するとメッセージが!!
f:id:c-terashima:20210719191910p:plain

まとめ

Slackは情報も豊富でかんたんにDMを送ることが出来ました。
Block Kitのテンプレートを見ていただくとボタンやテキストボックスをメッセージで送ることができますので、ぜひ色々試していただければ楽しいと思います。
参考にさせていただいた資料は下記に載せておきます。

参考資料


Holmesではエンジニア・デザイナーを募集しております。ご興味がある方はこちらからご連絡ください。

lab.holmescloud.com

lab.holmescloud.com

#技術書典 11に参加します!

こんにちは、id:c-terashimaです。

今週末に開催される技術書典11にHolmes開発部の有志メンバーで参加させていただきます!

techbookfest.org

DDDやmiroアプリ作成、E2EテストにPM視点と盛りだくさんとなっております。
価格は無料を予定しておりますので、ぜひお手に取っていただきホームズクラウドの開発裏話を楽しんでいただけたらと思います。
乞うご期待!

techbookfest.org

Holmesのとあるスクラムチームのとある一週間

Holmesでエンジニアをしている三澤です。

Holmesではアジャイル開発を採用しており、開発部には複数のスクラムチームが存在しています。 私はその中のWakaruチームに所属しているのですが、日々、ホームズクラウドが分かりやすいものになるよう改善を続けています。

今回はWakaruチームの一週間の活動を通して、開発メンバーに焦点を当てて、どのようにスクラム開発を行っているのか、その雰囲気をお伝えしたいと思います。

前提として、Holmesの各スクラムチームはコンポーネントチームではなく、フィーチャーチームとして構成されています。つまり、開発の企画からリリースまでを一貫して行う機能を持ったチームになっています。具体的に言うと、Epic作成→PBI作成→デザイン→実装→テスト→リリース(※一部Opsチームに依頼)をチーム内の作業で完結させています。

  • チーム構成とスプリントについて
    • 役割
    • スプリントについて
    • 主なスクラムイベント
  • 一週間の流れ
    • 水曜日(午後)
      • 15:30〜18:00 スプリントプランニング
    • 木曜日
    • 金曜日
    • 月曜日
      • 各メンバーの個別作業
    • 火曜日
      • 11:00〜12:00 PBIのリファインメント
      • 各メンバーの個別作業
    • 水曜日(午前)
      • 9:30〜10:30 スプリントレビュー
      • 10:30〜12:00 スプリントレトロスペクティブ
  • まとめ
続きを読む

TestCafeによるPage Object Patternの実装

こんにちは。Holmesでエンジニアをしている山本です。

以前TestCafeについて調べた際、TestCafeでもPage Object Patternが利用できることが分かりました。

E2Eテストとしての導入を検討しているため、簡単にではありますが、TestCafeによるPage Object Patternの実装を試してみました。

実行環境

名前 バージョン
OS Windows 10 Pro 64bit バージョン2004
Node.js 14.16.1
Yarn 1.22.5
TestCafe 1.14.0
TypeScript 3.9.7

Page Object Patternの概要

テスト対象となるページへのインターフェイスとなるオブジェクトまたはクラスを作成し、テストケースのコードと分離する手法です。

UIをページオブジェクトで隠蔽し、操作をメソッドとしてテストケースに提供することで、UIに変更があってもページオブジェクトの変更のみで対応できるようになります。

Seleniumを用いたテスト自動化において、コードの重複を減らしてメンテナビリティを高めるためのデザインパターンとして広まりました。

JavaSeleniumラッパーであるSelenideで推奨されており、GroovyのSeleniumラッパーであるGebでもファーストクラスとしてサポートされています

近年人気のCypressのブログでは、Page Object Patternの問題点を指摘されていますが、少なくともテストを都度スクリプトとして記述するよりも、メンテナンス性は高くなると思います。

TestCafeにおけるPage Object Patternの実装

ガイドページに、Page Modelとして実装例が記載されています。

今回は、弊社プロダクトであるホームズクラウドの、2021年4月末時点でのログイン画面を対象として、ページオブジェクトおよびテストケースを実装していきます。

TestCafeではJavaScriptおよびTypeScriptでテストを実装できますが、今回は型の指定が可能なTypeScriptを利用します。

実装対象画面

ログイン画面の初期表示は、ログインID入力欄とソーシャルログイン用のリンクが表示されます。

f:id:h-yamamoto_holmescloud:20210506193738p:plain
ログイン画面の初期表示

ログインIDを入力して「次へ」ボタンをクリックすると、パスワード入力欄が表示されます。

f:id:h-yamamoto_holmescloud:20210506195835p:plain
ログイン画面のパスワード入力欄

パスワードを入力し、「ログイン」ボタンをクリックすると、ログインIDとパスワードが正しければ、ダッシュボード画面に遷移します。

また、ログインの導線以外に、パスワードリセット画面へのリンクが表示されます。

ページオブジェクトの実装

まず、 pages/login.ts として、以下のファイルを作成します。なお、今回はソーシャルログインに関する要素を除外しております。

import { Selector, t } from 'testcafe';

export default class LoginPage {
  private url = 'http://localhost:8080/login';

  private loginIdInput = Selector('#username');
  private nextButton = Selector('.footer button[type=button]');

  private passwordInput = Selector('#password');
  private loginButton = Selector('.footer button[type=submit]');

  private alert = Selector('.alert');
  private passwordResetLink = Selector('a[href="/reset"]');

  async open(): Promise<void> {
    await t.navigateTo(this.url);
  }

  async toPasswordReset(): Promise<void> {
    await t.click(this.passwordResetLink.filterVisible());
  }

  async typeLoginId(loginId: string): Promise<void> {
    await t.typeText(this.loginIdInput, loginId).click(this.nextButton);
  }

  async doLogin(loginId: string, password: string): Promise<void> {
    await this.typeLoginId(loginId);
    await t.typeText(this.passwordInput, password).click(this.loginButton);
  }

  async hasError(): Promise<boolean> {
    return await this.alert.exists;
  }
}

実装例ではコンストラクタ内でSelectorを設定していましたが、TestCafeのSelectorは遅延評価されるため、privateなフィールドとして初期化しています。

UIの各要素を隠蔽し、操作をメソッドとして提供しています。また、初期ページのためURLが必要ですが、それもページオブジェクトに持たせています。

TestCafeのテストにはTestControllerが不可欠なため、当初はSelenium系のページオブジェクトを作る場合と同様、コンストラクタ引数でTestControllerを引数として受けてやる必要があるかと思ったのですが、TestCafeの場合、TestControllerをimportするだけで、適切なコンテキストが解決されるため、非常にシンプルになりました。

テストケースの実装

作成したページオブジェクトを用いて、実際のテストケースを記述していきます。TestCafe実行時に指定するファイルはこちらになります。

今回は、 features/login.ts として以下のファイルを記述します。

import { t } from 'testcafe';
import LoginPage from '../pages/login';

fixture `ログイン画面`;

// 実際は、IDやパスワードは外部ファイルから読み込みます
const loginId = 'test@example.com';
const password = 'test-password';

test('ログインするとダッシュボードが開く', async (t) => {
  const loginPage = new LoginPage();
  await loginPage.open();
  await loginPage.doLogin(loginId, password);

  const location = await t.eval(() => window.location);
  await t.expect(location.pathname).eql('/ws/dashboard');
});

test('ログインID入力前にパスワードを忘れた場合のリンクをクリックすると、パスワードリセットが開く', async (t) => {
  const loginPage = new LoginPage();
  await loginPage.open();
  await loginPage.toPasswordReset();

  const location = await t.eval(() => window.location);
  await t.expect(location.pathname).eql('/reset');
});

test('ログインID入力後にパスワードを忘れた場合のリンクをクリックすると、パスワードリセットが開く', async (t) => {
  const loginPage = new LoginPage();
  await loginPage.open();
  await loginPage.typeLoginId(loginId);
  await loginPage.toPasswordReset();

  const location = await t.eval(() => window.location);
  await t.expect(location.pathname).eql('/reset');
});

test('存在しないログインIDを入力すると、エラーメッセージが表示される', async (t) => {
  const loginPage = new LoginPage();
  await loginPage.open();
  await loginPage.typeLoginId('notfound-loginId@example.com');

  await t.expect(await loginPage.hasError()).ok();
});

test('パスワードを間違えていると、エラーメッセージが表示される', async (t) => {
  const loginPage = new LoginPage();
  await loginPage.open();
  await loginPage.doLogin(loginId, 'invalid-password');

  await t.expect(await loginPage.hasError()).ok();
});

実際のテストケースの流れを記述していきます。入力する値などもこちらで制御し、ページオブジェクトに渡すようにします。

今後の実装予定

今回はログイン画面のテストの例に対象を絞ったため、パスワードリセットやログイン後のページ遷移先ではURLを確認しただけですが、ページ遷移を伴うメソッドでは、遷移先のページオブジェクトを生成し、それを返すように実装していきます。

今回の例でいうと、 pages/reset.tsPasswordResetPagepages/ws/dashboard.tsDashboardPage を追加し、 LoginPage#toPasswordResetLoginPage#doLogin の戻り値としてそれぞれ返すイメージです。該当部分だけ抽出すると、以下のようになります。

export default class LoginPage {

  async toPasswordReset(): Promise<PasswordResetPage> {
    await t.click(this.passwordResetLink.filterVisible());
    return new PasswordResetPage();
  }

  async doLogin(loginId: string, password: string): Promise<DashboardPage> {
    await this.typeLoginId(loginId);
    await t.typeText(this.passwordInput, password).click(this.loginButton);
    return new DashboardPage();
  }

}

また、ログイン後の画面には、ヘッダやフレームといった共通コンポーネントが存在します。

それらもページオブジェクトとして実装し、継承やCompositeパターンを用いることで、各ページオブジェクトでのUIや操作を共通化できます。

class Header {
  ...
}

class DashboardPage extends Header {
  ...
}

感想

Page Object Pattern自体は歴史もあり、枯れたパターンであるため、特に違和感なく実装できました。

ただ、TestCafeの場合、どうしても処理の実行に asyncawait が必要になるため、以下の2点が気になりました。

  1. ページオブジェクトを生成したタイミングでのURL遷移が難しい
    • コンストラクタに async を付与できないため、今回はページオブジェクト側でURL遷移を行わせるメソッドを用意しました
    • static なファクトリーメソッドを作成し、それに async を付与することは可能ですが、やや煩雑になります
  2. Promise で値を返す必要があるため、メソッドチェーンが書きにくく、1行ずつ await して実行していく記述になりがち
    • TestController のように、 this | Promise<any> を返すようにすればできるかもしれませんが、テスト記述としては実装難度が高そうです

とはいえ、 async/await の頻出はJavaScriptベースのE2Eテストツールでは仕方のないことかと思います。また、E2Eテストはメンテナンスが難しいため、書きやすさよりも読みやすさを重視すべきですが、どちらのケースでも行単位で処理を実行していくことになるため、結果的にテストケースの可読性が上がることにもつながるかと思いました。

参考ページ

最後に

Holmesではエンジニア・デザイナーを募集しております。ご興味がある方はこちらからご連絡ください。

lab.holmescloud.com

lab.holmescloud.com

SpringBootAdminでかんたん見える化

こんにちは、サーバサイドエンジニアをしているid:c-terashimaです。
ホームズクラウドはSpringBootをメインフレームワークとして採用して複数のAPIサービスが稼働しています。
これからも増えていくが予想されるAPIサービスを管理運用していく上で、以下の課題がありました。

  • 障害発生時のログ調査権限がSREチームメンバーのみで運用負荷が高い
  • JVMが適切に稼働しているかリアルタイムに確認したい
  • どのバージョンのアプリケーションがデプロイ・稼働しているのか確認したい
  • サーバが増減しても低コストでかんたんに運用したい

これらを解決するのにSpringBootAdminが適していそうなので導入を検討していきます。

SpringBootAdminとは

SpringBootAdminはSpringBootで作られたアプリケーションの管理者向けのGUIツールです。
以下の情報を可視化することができます。

  • 各種メトリクスの表示
    • JVMとメモリ
    • データソース
    • キャッシュ
  • ビルド番号の表示
  • ログファイルのリアルタイム表示とダウンロード
  • ログ出力レベルの変更
  • JVMパラメータや環境設定プロパティの表示
  • SpringBoot構成プロパティの表示(application.yml)
  • スレッドダンプの表示
  • http-tracesの表示
  • endpointsの表示
  • スケジュールされたタスクの表示(SpringBatchやTaskschedulerなど)
  • SpringSessionの表示や削除
  • Flywayなどのマイグレーション状況の表示
  • heapdumpのダウンロード
  • イベントの通知(メール、Slackなど)

SpringBootAdminサーバの作成

導入は非常にシンプルで公式に書いてあるとおりに設定すればOKです。
新規にSpringBootアプリケーションを作成し、bild.gradleにSpringBootAdmin Serverの依存を追加し@EnableAdminServerアノテーションを付与するだけです。
application.ymlを作成して任意のポートを指定しておきます。server.port=9000

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("de.codecentric:spring-boot-admin-starter-server")
}
@SpringBootApplication
@EnableAdminServer
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

SpringBootAdminクライアントの設定

監視したアプリケーションにSpringBoot Actuatorを依存に追加し、application.ymlでエンドポイントを制御してあげるだけで設定はOKです。
非常にかんたんですね!

// 抜粋
dependencies {
    implementation "org.springframework.boot:spring-boot-starter-actuator"
    implementation "de.codecentric:spring-boot-admin-starter-client:2.3.1"
}

management以下は監視する情報を制御する設定になります。
Spring Boot Admin Reference Guideで監視したい設定を指定してください。
以下はすべてを公開する設定になっています。

spring:
  application.name: アプリケーションの名前     // SpringBootAdminサーバに表示させるアプリケーション名
  boot.admin.client:
    url: http://localhost:9000

management:
  server:
    port: 9001
  endpoints:
    web:
      exposure:
        include: "*"  
  endpoint:
    health:
      show-details: ALWAYS

スクリーンショット

http://localhost:9000にブラウザで接続すると管理コンソール画面が表示され、クライアントの情報が可視化されたと思います!

f:id:c-terashima:20210511185957p:plain f:id:c-terashima:20210511190016p:plain f:id:c-terashima:20210511190037p:plain f:id:c-terashima:20210511190050p:plain

まとめ

非常にかんたんな設定だけで構築することができ、たくさんの情報を可視化することができました。
その反面、セキュリティをしっかりしておかないと重大な事故につながる危険性もあり、監視する項目はしっかりと見極めないといけないことがわかりました。
皆様の導入の参考になれば幸いです。


Holmesでは現在エンジニアを募集しています。 興味がある方は是非こちらからご連絡ください!

lab.holmescloud.com