[Microservices]Hibernateを使ったHelidonマイクロサービスの作成とデプロイ パート1/ Building And Deploying A Helidon Microservice With Hibernate Part 1

原文はこちら


繰り返し述べていますが、ソリューションを選ぶ前にアプリケーションそれぞれが抱える独特の要件を評価すべきであり、そのためこのブログシリーズで紹介する私の意見はあなたの組織の選択とは異なってくるかもしれないことに留意してくださいね。あなたが自問すべき重要な問いは以下でしょう:

  • 私の仕事にとってマイクロサービスは適切なツールだろうか?
  • マイクロサービスは私の問題をメンテナンス可能な方法で解決するだろうか?
  • 我々はこのソリューションを実装するに十分な予算を持っているだろうか?
新しい考え方を導入するとき、後では解決しがたい問題を招き入れてしまうこともあるため、これらを自問してみることは重要です。

マイクロサービスパターン

コードに進む前にマイクロサービスでのデータ管理のいくつかのパターンを定義するところから始めましょう。マイクロサービスの場合の最もわかりやすいパターンは、共有データベースパターンとサービス毎のデータベース(あるいはスキーマ)パターンです。ここから紹介していきます。

共有データベース

モノリスでは、通常我々はデータを単一のリレーショナルデータベースに保持します。これによりJOINを使ったクエリを書いたり、ACIDトランザクションを使ってデータ一貫性を保証したりと、永続化とクエリにまつわる部分を簡単に済ますことができます。共有データベースのマイクロサービスパターンでは、複数のサービスが単一のデータベースを共有しており、それぞれがテーブルをJOINしながらクエリをしてデータを取得したり、トランザクションを使用して一貫性を保証できる信頼性のあるやり方でデータを更新したりします。このことにより、このパターンは新人開発者たちにも理解しやすいものになっていますが、一方でAPIが複雑になってくると課題も生まれてきます。カラムを追加したりデフォルト値を変更したりすると同じテーブルにアクセスしているサービスを壊してしまうかもしれないため、スキーマの変更は他のサービスの開発者たちと調整したうえで行わなければなりません。また、長時間に渡るトランザクションが共有テーブル上でロックを保持することにより、他のサービスをブロックしてしまうかもしれません。更に、このパターンでは全てのサービスがデータをトラディショナルなリレーショナルデータベースに保持する前提となっており、NoSQLやGraph DBでドキュメントを利用する可能性を排除してしまっています(ただし少し後に紹介するようなワークアラウンドはあります)。

サービス毎のデータベース(あるいはスキーマ)

サービス毎のデータベース(あるいはスキーマ)パターンは共有データベースパターンのいくつかの欠点に対処しています。サービスがそれぞれ自身のデータベースを持っており、これは要はデータベースがサービスの実装の一部であるということを意味しています。このやり方ではスキーマの変更は他のサービスには影響しないということになります。なお、サービスごとにデータベース・サーバーを持つ必要は必ずしもありません。多くの場合サービスごとに独立したテーブル(他のサービスからはアクセスを制限されたユーザー、パーミッションをそれぞれのテーブルに付与しておきます)というかたち、あるいはサービスごとにユニークなスキーマを割り振るというかたちで実現されます。サービス毎のデータベースを利用するということはすなわち、サービスそれぞれの必要に応じて最も適した種類のデータベースを使えるようになるということを意味します。もちろん、このパターンにはデメリットもあります。トランザクションの管理が難しくなります。参照整合性の保証は簡単ではなくなるでしょう。データをJOINしてのクエリは不可能ではないにしても難しいでしょう。

もうわかったからコード見せてよ!

ここまでこのシリーズではマイクロサービスとは何か、あなたはなぜそれを使うべきなのかもしれないのかについて長々と語ってきましたが、理論を理解するのに最良な方法はいつもコードを見ることですよね。ということでようやくコードを見ていくことにします。このシリーズでは、何回かに分けてソーシャルメディア形式のアプリケーションのためのAPIを作っていきます。この中ではクラウドの様々な機能を使うだけでなく、いくつかの異なったマイクロサービスパターンを活用でき、そこに付き物となる興味深い問題への対処方法を紹介していくことになります。

ユーザーサービスの構築

説明

このサービスではHelidon MPをHibernateとともに利用し、ユーザーをOracle ATPインスタンスの user テーブルに永続化します。始めるために、Helidon Mavenアーキタイプを使いサービスのためにいくつかのファイルと構成の土台を作っていきましょう。以下がコマンドです(お好みに応じてパスを修正してもいいし、そのまま使ってもいいです):
mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-mp \
-DarchetypeVersion=1.1.1 \
-DgroupId=codes.recursive \
-DartifactId=user-svc \
-Dpackage=codes.recursive.cnms.user
view rawhelidon-archetype.sh hosted with ❤ by GitHub
生成されたコードを見たり直したりする前に、このマイクロサービス用のスキーマユーザーを作成しておきましょう。可動しているATPインスタンスにadminユーザーとして接続し、次のクエリを実行する必要があります。このポストシリーズで紹介されているSQL Developer Webを使うと簡単にできます。準備ができたら、以下を実行してください(パスワードを強いものに修正するのをお忘れなく)。
CREATE USER usersvc IDENTIFIED BY "STRONGPASSW0RD";
GRANT create session TO usersvc;
GRANT create table TO usersvc;
GRANT create view TO usersvc;
GRANT create any trigger TO usersvc;
GRANT create any procedure TO usersvc;
GRANT create sequence TO usersvc;
GRANT create synonym TO usersvc;
GRANT CONNECT TO usersvc;
GRANT RESOURCE TO usersvc;
GRANT UNLIMITED TABLESPACE TO usersvc;
view rawcreate-usersvc.sql hosted with ❤ by GitHub
SQL Developer Webを使っているなら、adminユーザーで以下のコマンドを使い各スキーマが有効化されていることを確かめておく必要があります:
BEGIN
 ords_admin.enable_schema(
  p_enabled => TRUE,
  p_schema => 'SCHEMA-NAME',
  p_url_mapping_type => 'BASE_PATH',
  p_url_mapping_pattern => 'schema-alias',
  p_auto_rest_auth => NULL
 );
 commit;
END;
上記のコマンドでは、プレースホルダの値は以下のように置き換えてください:
  • SCHEMA-NAME はデータベーススキーマ名、全て大文字
  • schema-alias はスキーマ名のエイリアスで、SQL Developer Webを使ってアクセスする際にURLに使われます。Oracleはスキーマ名を暴露しないほうがよいというセキュリティ上の理由から、スキーマ名をそのまま使わないことを推奨しています。
ユーザーアクセスを有効化したら、ADMINユーザーから当該ユーザーにSQL Developer WebにアクセスするためのURLを提供しましょう。このURLはADMINユーザーがSQL Developer WebにアクセスするためのURLと同じものですが、ただし admin/ の部分はschema-alias/で置き換えてください。
これ以降、私達は usersvc ユーザーを使っていくので、一旦SQL Developer Web(その他のツールをお使いならそのツール)からログアウトして先程作成した新しいユーザー名とパスワード、および上記で説明した適切なURLでログインし直しましょう。 usersvc でログインしたら、以下を実行してマイクロサービス用のテーブルを作成します:

CREATE TABLE users(
"ID" VARCHAR2(32 BYTE) DEFAULT ON NULL SYS_GUID(), 
"FIRST_NAME" VARCHAR2(50 BYTE) COLLATE "USING_NLS_COMP" NOT NULL ENABLE, 
"LAST_NAME" VARCHAR2(50 BYTE) COLLATE "USING_NLS_COMP" NOT NULL ENABLE, 
"USERNAME" VARCHAR2(50 BYTE) COLLATE "USING_NLS_COMP" NOT NULL ENABLE, 
"CREATED_ON" TIMESTAMP (6) DEFAULT ON NULL CURRENT_TIMESTAMP
CONSTRAINT "USER_PK" PRIMARY KEY ("ID")
);
view rawcreate-user-tbl.sql hosted with ❤ by GitHub
次に、依存モジュールを取得しておく必要があります。プロジェクトルートに /build-resource というフォルダを作成し、更にその配下に /libsというサブディレクトリを作成してください。ATPインスタンスと接続するために必要となるOJDBCドライバーをプロジェクトで使えるように、以下のJARファイルを取得する必要があります:
  • ojdbc8.jar
  • oraclepki.jar
  • osdt_cert.jar
  • osdt_core.jar
Download the JARs from Oracle and place them in /build-resource/libs.  We also need to publish these to our local Maven repo so that when we run the application locally they'll be properly resolved. The following commands should help you out with that:
OracleからJARをダウンロードし、 /build-resource/libsに置きましょう。また、ローカルのMavenリポジトリにこれらをパブリッシュしておき、ローカルでアプリケーションを稼働させたときにこれらが適切にリゾルブされるようにしておきましょう。以下のコマンドでできます:
mvn install:install-file -Dfile=/path/to/ojdbc8.jar -DgroupId=com.oracle.jdbc -DartifactId=ojdbc8 -Dversion=18.3.0.0 -Dpackaging=jar
mvn install:install-file -Dfile=/path/to/oraclepki.jar -DgroupId=com.oracle.jdbc -DartifactId=oraclepki -Dversion=18.3.0.0 -Dpackaging=jar
mvn install:install-file -Dfile=/path/to/osdt_core.jar -DgroupId=com.oracle.jdbc -DartifactId=osdt_core -Dversion=18.3.0.0 -Dpackaging=jar
mvn install:install-file -Dfile=/path/to/osdt_cert.jar -DgroupId=com.oracle.jdbc -DartifactId=osdt_cert -Dversion=18.3.0.0 -Dpackaging=jar
次にpom.xmlファイルを修正し、JPAに必要な依存ライブラリであるHibernate、JacksonおよびOJDBCのJARを含むようにしておきます。次のエントリをdependenciesセクションに追加してください:
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.4.3.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.3.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>el-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>com.oracle.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>18.3.0.0</version>
</dependency>
<dependency>
<groupId>com.oracle.jdbc</groupId>
<artifactId>oraclepki</artifactId>
<version>18.3.0.0</version>
</dependency>
<dependency>
<groupId>com.oracle.jdbc</groupId>
<artifactId>osdt_core</artifactId>
<version>18.3.0.0</version>
</dependency>
<dependency>
<groupId>com.oracle.jdbc</groupId>
<artifactId>osdt_cert</artifactId>
<version>18.3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.28</version>
</dependency>
そしてHibernateの永続化に係る設定を追加する必要があります。
/src/main/resources/META-INFに persistence.xml という名前で以下のようにファイルを作成してください:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="UserPU">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.archive.autodetection" value="class" />
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle12cDialect" />
            <property name="hibernate.format_sql" value="true" />
            <property name="hbm2ddl.auto" value="validate" />
        </properties>
    </persistence-unit>
</persistence>
view rawpersistence.xml hosted with ❤ by GitHub
そのディレクトリに行ってから、 microprofile-config.properties を以下のように修正します:
# Application properties.
datasource.username=
datasource.password=
datasource.url=
# Microprofile server properties
server.port=8080
server.host=0.0.0.0
これで事前に必要な設定は全て終えたので、アプリケーションコードに進めます。生成された GreetApplication.java を探してください。それを修正してもいいですし、削除して新しく作るのでもいいんですが、結局の所以下のコードを含む UserApplication.java が必要になります:
package codes.recursive.cnms.user;
import java.util.Set;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import io.helidon.common.CollectionsHelper;
import org.glassfish.jersey.jackson.JacksonFeature;
/**
 * Simple Application that managers users.
 */
@ApplicationScoped
@ApplicationPath("/")
public class UserApplication extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        return CollectionsHelper.setOf(
                UserResource.class,
                JacksonFeature.class
        );
    }
}
view rawUserApplication.java hosted with ❤ by GitHub
Use the same process (modify or replace) for GreetingProvider to end up with a UserProvider that looks like so:
同様に GreetingProvider についても修正または削除して再作成して以下のUserProvider を作ります:
package codes.recursive.cnms.user;
import java.util.concurrent.atomic.AtomicReference;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
/**
 * Provider for user config.
 */
@ApplicationScoped
public class UserProvider {
    private final AtomicReference<String> dbUser = new AtomicReference<>();
    private final AtomicReference<String> dbPassword = new AtomicReference<>();
    private final AtomicReference<String> dbUrl = new AtomicReference<>();
    /**
     * Create a new user provider, reading the message from configuration.
     *
     * @param dbUser
     * @param dbPassword
     * @param dbUrl
     */
    @Inject
    public UserProvider(
            @ConfigProperty(name = "datasource.username") String dbUser,
            @ConfigProperty(name = "datasource.password") String dbPassword,
            @ConfigProperty(name = "datasource.url") String dbUrl
    ) {
        this.dbUser.set(dbUser);
        this.dbPassword.set(dbPassword);
        this.dbUrl.set(dbUrl);
    }
    String getDbUser() { return dbUser.get(); }
    String getDbPassword() { return dbPassword.get(); }
    String getDbUrl() { return dbUrl.get(); }
    void setDbUser(String dbUser) { this.dbUser.set(dbUser); }
    void setDbPassword(String dbPassword) { this.dbPassword.set(dbPassword); }
    void setDbUrl(String dbUrl) { this.dbUrl.set(dbUrl); }
}
view rawUserProvider.java hosted with ❤ by GitHub
ここで UserProvider クラスはアプリケーション稼働時には生成された設定値を格納します。ここまででマイクロサービスの実ロジックを書き始める準備ができました。このシリーズの次のポストでは、コードを深く掘り下げていき、サービスをOracle Cloud上にデプロイ、稼働させてみます。

[Microservices]マイクロサービスなんてかんたんだ…/Microservices Are Easy…


理屈のうえでは、マイクロサービスは簡単です。疎結合で、互いに独立して開発、テスト、そしてデプロイできるスケーラブルなサービス。すごい話ですよね。さらに、それぞれのサービスはそれぞれの都合に合った言語、また好きなフレームワークで開発を行っても、アーキテクチャの他の部分に影響は与えないときたものです。JavaサービスをNodeやGo、Rubyなどのサービスと合わせて使い、それぞれが別々のデータストアにデータを永続化するようなこともできます。たとえばJavaサービスではKey/Valueストアを使っているかもしれません。もしかするとGoサービスではトラディショナルなRDBMSに永続化していたり。マジで未来が来ています。

…と思っていた時期もありました

現実には、マイクロサービスは開発者たちに新しい課題を投げかけています。われわれの業界はどうも循環しているところがあって、5年とか10年とか前に解決した問題へのソリューションを再発明するようなことがあります。これは必ずしも悪いことではなく、古い問題に新しい答えを創造することで、何年かの間に得られた教訓を織り込むことができます。ソーシャルメディアの勃興と流行により、以前は交わることのなかった開発者コミュニティ間の境界がやや曖昧になり、アイディアが共有されるようにもなってきました。とは言うものの、マイクロサービスがあなたの抱えるモノリシックな問題を即座に解決してくれるようなものではないということを理解しておくのが重要です。マイクロサービスは古くからある問題を解決する新しいやり方を提供してくれますが、それなりの代償もあります。あなたは考え方を変える必要に迫られるでしょうし、これまで揺るぎない常識だと思っていた概念から距離を取る必要も出てきます。データベースでのCHECK制約を用いた参照整合性といったやり方はもう当てはまらないでしょう。プログラマーが最も早い時期に学んできた考え方のひとつであるDon't Repeat Yourself(DRY)はそれほど重要だとはみなされなくなります。
あらゆるものごとにはなんらかの代償があり、これはマイクロサービスに限った話ではありません。Object Relational Mapping(ORM)フレームワークが一般的になったときには、それまで持ち得ていたきめ細かな制御は一定程度失われました。オブジェクトのごく一部のプロパティを選択して取得するのはそれほど簡単ではなくなってしまいました(オブジェクト全部を取得するか何も取得しないか、です)。ORMを用いてオブジェクトグラフに複雑なクエリをかけるのは難しく、パフォーマンスに苦しむことになります。これはほとんどのORMシステムがリフレクションを使っており、もし実装がへぼかった場合、たちの悪いデータベースクエリを実行することになるからです。
一方で、マイクロサービスの人気には疑問を差し挟む余地はありません。お客様からはもしかしたらここ数年の他のいずれのトピックよりもマイクロサービスについて頻繁に質問をいただきます。マイクロサービスについて、カンファレンスでは多くの議論がなされ、ブログポストやビデオも日々公開されています。というわけで私も、パワフルでメンテナンスしやすく、高性能なマイクロサービスを作成するための耳寄りなテクニックを紹介するため、ブログポストのシリーズを投稿することにしました。
言うまでもなく、プロジェクトはそれぞれ異なっています。ひとつのプロジェクトでうまくいったことが、他のプロジェクトでもうまくいくとは限りません。前の会社でやったことが今の会社にとってベストなやり方であるとも限りません。それでいいんです。それこそがこの業界で働くことをエキサイティングにしてくれる一因です。もし全てのプロジェクトに適用できる簡単なソリューションなんてものが存在したら、本当にすぐに仕事は退屈になってしまうでしょうね。私のブログポストシリーズを読む際にはこのことを覚えておいてください。そして、どのアーキテクチャでやると決める前にも、常にビジネス要件の評価を確実に実施してくださいね。

マイクロサービスの定義とは

私が言っている「マイクロサービス」の意味するところをあなたが完全に理解している、なんていう風には思い込むべきではないでしょう。というのも、しばしば「みんな」がマイクロサービスについて知っているような気がしてしまうときもありますが、現実には開発者の中には依然、そのおおまかな概念を理解できていない方もいます。伝統的に、われわれはアプリケーションを今では「モノリス」として知られているパターンでデザインしてきました。つまり、ひとつのアプリケーションにすべてのサービス、ビジネスロジック、モデルとそして通常画面のコードが含まれているというものです。あなたはモノリスはいくぶんか時代遅れであり、「モダン」なソフトウェアの世界にもはや居場所はないのだといった話をしばしば読んだり、聞いたりするでしょう。端的に言ってそういった話は間違っています。アプリケーションが単一のコードベースであることがベストである場合は確実に存在しますし、あなたがある方法でアプリケーションを開発することを決めたことそれだけであなたのソリューションが他のものに劣っているのだと感じる必要なんてないってことです。しかし、ある場合においてはアプリケーションの要件が単一のコードベースにするには複雑にすぎるといったこともあります。もしかするとあなたのところにはいくつかの分散した、異なるスキルセットを持ったチームがあるかもしれません。あるいは他のサービスに比べ突出して利用されているサービスがあるかもしれません。異なる永続化モデルに間借りされるようなデータがあったりとか。(とりわけ)そうした条件では、開発者たちはマイクロサービスパターンを用いることで結合を疎にしたり、個々にサービスをスケールさせたり、管理したりしてきました。Martin Fowlerはマイクロサービスについて2014年のこの記事の中でそのようなことを述べています:
簡潔に言えば、マイクロサービスアーキテクチャスタイルは単一のアプリケーションを複数の小さなサービスのセットとして開発することだ。それらはそれぞれ自身のプロセスの中で可動し、しばしば用いられるHTTPリソースAPIのような軽量な仕組みでコミュニケーションする。これらのサービスはビジネス上の機能に関連づけられて開発され、完全に自動化されたデプロイ機構によって個々にデプロイが可能である。そこでは中央集権的なサービス管理は実に最小限しかなく、サービスはそれぞれ異なったプログラミング言語で書かれていてもいいし、異なったデータストレージを用いていてもよい。

簡単で、でも簡単じゃない…

最初に書いた通り、理屈の上ではとてもシンプルです。でもマイクロサービスの背景にある理屈を理解するところから、実際に実装するまでの道のりは難しいものかもしれませんし、データを管理しようとする段になるといろいろとややこしくなるかもしれません。このブログシリーズでは、小規模なマイクロサービスを作成してデプロイする方法―基盤からコーディングそしてデプロイメント―をご紹介したいと思います。また、マイクロサービスパターンを実装した場合のいくつかのデータ管理方法についても説明し実演していけたらと思います。