社内イベントアプリを作成しました【第二弾】

※本項は私自身の見解であり、必ずしもIBMの立場、戦略、意見を代表するものではありません

4月に作成した新入社員向けアプリに引き続き、今度は全社向けの公式アプリとしてイベントアプリを作成しました.

今回は Web アプリになります.
フロントエンドは Vue.js、バックエンドは Spring Boot で、 IBM Cloud の Cloud Foundry 上で稼働させました.
デザインもアグレッシブなものを採用したので、弊社らしからぬ(?)遊び心溢れるものに仕上がったと思います.

今回はこのアプリの製作について書いてみます.

 

プロジェクト体制

デザイン、フロントエンド、バックエンドチームに分かれての総勢14名の開発チームでした.
日々の実務と平行しての有志の活動だったこと、メンバーのスキルレベルがまちまちだったこともあり、人数は多いながらそれなりに厳しい開発となりました.

私はPM兼バックエンド、クラウドを担当し、最後の方は遅延を巻き取るためにフロントエンドも少しですが触りました.

ほぼ全員が入社1,2年目の若手でしたが、フロントエンドにシニアの方がお一人、クラウド・アーキテクトに3年目の先輩に参加いただきました.このお二人のおかげでフロント・バックエンドともにアーキテクチャが充実したものとなりました.

 

スケジュール

7月末にプロジェクト発足、10月初めにリリースというスケジュールでした.

個人的に重きを置きたかったのは初期の2週間の技術検証期間でした.この期間でどれだけスキルレベルを上げれるかが鍵だと思っていました.

残念ながら検証期間の走り出しは目標に達しないもので、スイッチが入ったのは実装開始3週目くらいだったかなと思います.完全に締め切り駆動ですね(笑)
初めの段階から充実したサポートや適切なトラッキングをすべきだったなというのは反省です.

 

クラウド・アーキテクチャ

Cloud Foundry ランタイムをメインに据えた PaaS 構成です.

前段の Cloud Foundry ランタイムは Nginx のランタイムとなっており、Vue.js で実装したフロントエンドのアプリを載せています.

後段の Cloud Foundry ランタイムは Java のランタイムとなっており、Spring Boot で実装したバックエンドのアプリを載せています.

DB は RDB, NoSQL, 非構造DB をデータの特性に分けて使い分けています.
それぞれ、Compose for PostgreSQL, Cloudant, Object Storage を使用しました.

認証機能は、App ID + Spring Security を用いて SAML 2.0 の SSO サインインを実現しています.

Cloud Foundry の両ランタイムは、Continuous Delivery を用いて GitHub Enterprise および Slack と連携させて CI/CD を実現しました.

参考までに、今回用いた IBM Cloud のサービスと AWS のサービスの対応関係を以下に記します.
(公式見解ではありませんので、あくまでご参考程度にお願いします)

  • Cloud Foundry … Elastic Beanstalk
  • Compose for PostgreSQL … RDS for PostgreSQL
  • Cloudant … Couchbase
  • Object Storage … S3
  • App ID … Cognito
  • Continuous Delivery … CodePipeline

アーキテクチャの不足点

理想的には、以下のようなアーキテクチャを目指したかったです.

ポイントは、各ランタイムの前段に認証用の Gateway を配置することです.
これにより、フロント/バックエンドともに保護され、また、Functions 等の新たなランタイムも導入可能になります.
(実際に実装したアーキテクチャでは、バックエンドの API のみ保護されており、フロントエンドは保護されておりません)

加えて、NoSQL(Cloudant) や RDB(Compose for PostgreSQL) の前段にキューイング機能を配置できると、過剰なトランザクションを抑止できて、より安定性の高いシステムに出来たかと思います.

 

CI/CD

IBM Cloud の Continuous Delivery を用いて CI/CD を実現しました.

GitHub Enterprise の master/dev ブランチにプッシュされたのをトリガーに IBM Cloud 上で maven/npm build し、Cloud Foundry にデプロイを行いました.

Continuous Delivery のバージョン周りで苦しんだのは以下の記事で書いています.


工夫は必要ですが、比較的最新のパッケージ(Spring Boot 2.X, Node 10.X)が対象でも、CI/CD が実現できました.

 

分析

Kibana を使って分析していました.
実際に分析結果をUI/UXの改善に役立てました.

リリース当初の分析結果です.
右上の円グラフを見ると、/senryu エンドポイント(=川柳画面)への流入が低いことが分かります.

こちらを改善すべく、川柳画面への導線を増やしたり、画面遷移のポイントとなる要素を強調しました.その結果、/senryu エンドポイントへの流入を増やすことができました.

 

フロントエンド

フロントエンドチームの Super Developer が記事を書いてくれました!

 

バックエンド

Spring Boot 2.0 で API を実装しました.

使用したライブラリは以下になります.

Spring

  • spring-boot-starter-web
  • spring-boot-starter-data-jpa
  • spring-boot-configuration-processor
  • spring-boot-starter-security
  • spring-security-oauth2-autoconfigure

DB

  • postgresql
  • ibm-cos-java-sdk
  • cloudant-spring-boot-starter

その他

  • lombok
  • springfox-swagger2
  • springfox-swagger-ui
  • swagger-core

個人的におすすめなのが、Cloudant の Spring 用 SDK です.
非常に使い勝手が良いものになっています.

 

バックエンドアプリのアーキテクチャは以下のようになっています.

一般的(?)な DDD(Domain-driven design) のようなパッケージ構成にしてみました.

ドメイン層とインフラ層を IRepository と ImplRepository で区切ることで、例えば Cloudant を Couchbase に、Compose for PostgreSQL を RDS for PostgreSQL にという変更も比較的容易になります(ちなみに Object Storage は S3 と互換なので SDK のプロパティでアクセス先を変更可能です).

また Service 内で Converter を用い、Entity -> Response の変換を行っています.
今回のアプリはフロントエンド、バックエンドともに新規開発だったため、I/F をフロントエンドに寄せて調整しました.その結果、DB 上のデータモデルと API レスポンスのデータモデルの間に変換処理が必要になりました.

一例になりますが、セッションデータは下記のような変換処理を行っています.

val response = sessions.stream()
        .collect(Collectors.groupingBy(e -> e.getTimeSlot().getTimeSlotText()))
        .entrySet()
        .stream()
        .map(e -> new SessionResponse(e.getKey(), e.getValue()
                .stream()
                .map(new SessionConverter(favorites, likes, comments))
                .sorted(Comparator.comparing(Session::getSessionId))
                .collect(Collectors.toList())))
.collect(Collectors.toList());
public class SessionConverter implements Function {

  @Override
  public Session apply(SessionEntity entity) {

      return new Session(entity.getSessionId(),
              entity.getTitle(),
              entity.getSessionCategory() == null ? null : entity.getSessionCategory().getSessionCategory(),
              entity.getKeyword(),
              entity.getSpeakers().stream().map(speakers -> speakers.getSpeakerName()).collect(Collectors.toList()),
              entity.getRoom(),
              entity.getOverview(),
              entity.getLikeCount(),
              entity.getCommentCount(),
              "https://app.mybluemix.net/api/image/room/" + entity.getRoomImagePath(),
              entity.getInformationUrl().stream().map(e -> new InformationUrl(e.getTitle(), e.getUrl())).collect(Collectors.toList()),
              favorites.stream().map(e -> e.getFavoriteSessionKey()).anyMatch(e -> e.getSessionId().equals(entity.getSessionId())),
              likes.stream().anyMatch(e -> e.getSessionId().equals(entity.getSessionId())),
              comments.stream().anyMatch(e -> e.getSessionId().equals(entity.getSessionId())));
  }
}

 

比較的整った構成になったかなと思います(ここは結構満足しています!).
相談にのってくれた @Sh00ne に感謝.

 

イベント当日

最終的に537人の方にログインいただけました.
当日の分析結果は以下のような感じでした.

  • 左上の円グラフ: 綺麗に3等分に振り分けされています.
  • 右上の円グラフ: /sessions エンドポイントがよく使われています.
  • 左下の棒グラフ: セッション間の休憩時間に利用が増えています.
  • 右下の円グラフ: データ上は障害はありませんでした.

データ上、イベント当日らしい使われ方が見て取れて、それなりに使っていただけたのではないかと思います.可視化すると具体的に使用状況が分かるので良いですね.

 

感想

今回のアプリ製作は、振り返ると『初めてとこだわり』だったなと思います.

初めての Spring Boot, Vue.js、比較的大人数の有志の開発.
紆余曲折ありましたが最後まで作り切れたのは大きな収穫です(まだイベント当日が残ってるけど障害なく終えたいですね笑).

また、入社して1年半が経とうということもあり、以前よりも業務を意識した、細部へのこだわりを見せれたのではないかと思います.
実際開発をしていて難しいことがあると「有志の社内アプリだし、ここは妥協していいか…」という気持ちになりがちなのですが、今回は比較的よく戦えました.CI/CD や Cloud Foundry 周りは情報が少なく特に苦労しましたが、なんとか本番業務に持っていける土台(監視など、本番業務には他にも色々必要)くらいまではいけたと思っています.

今後の予定は、Java で書いた Spring Boot を Kotlin でリプレイスしてみる.
また、Continuous Delivery での CI/CD をもう深めてみようと思います.

CI/CD は Advent Calendar に書く予定なのでよろしければご覧ください.

 

以上です.

PS. Spring Boot 完全に理解した