OCI(Oracle Cloud Infrasturucture)で構築するマイクロサービス

# はじめに

この記事は[Oracle Cloud Infrastructure #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/oci)の24日目の記事です。

12/17-12/18の2日間に渡って開催されたOracle Developer Daysの2日目にCloud Nativeセッションを担当させて頂きました。

この記事では、上記セッションで使用した簡単なデモアプリケーションについて、その解説と補足をしていきたいと思います。


# デモのソースコードについて

使用したデモについては、[このレポジトリ](https://github.com/oracle-japan/oracle-developer-days-2020-cloudnative-demo)にありますので、ご自由にお使い頂ければと思います。

セッション資料は[こちら](https://www.oracle.com/jp/a/otn/docs/oracle-graalvm-helidon.pdf)です。
動画についても[こちら](https://www.youtube.com/watch?v=BRD3HtC4Qsk&feature=emb_title)にアップロードされています。

# デモアプリケーションの構成

![Greenshot 2020-12-20 22.50.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40392/ed24827b-e90e-1ec4-2c8d-3b3f25d4f7b2.png)

上図はセッション資料にも含まれている図です。

[Nuxt.js](https://ja.nuxtjs.org/)で実装したフロントエンドアプリケーションとHelidon(後述)アプリケーションをGraalVM(後述)でNative Image化(後述)した2種類のバックエンドAPIから構築したアプリケーションです。全体がショボすぎてマイクロサービスなのかというのはありますが、一応サービスごとにコンポーネントが分かれているということで許してください...

フロントエンドへはロードバランサー経由でアクセスします。

フロントエンドからバックエンドAPIへのアクセスはClusterIPを利用します。
ここで、勘が良い方はお気づきかもしれませんが、フロントエンドはいわゆるSSR(サーバサイドレンダリング)で構築しています。

ランタイム環境はKubernetesを利用しています。(ここでは、Oracle Cloud Infrastructureで利用可能なマネージドKubernetesサービスを使っています)

# デモアプリケーションで利用している技術スタック

このデモでは、Oracle Cloud Infrastructureで利用可能な以下のサービスやプロダクトを利用して実装しています。

* [GraalVM](https://www.graalvm.org/)：オープンソースのランタイムエンジンです。JavaやScalaなどのJVM上で動作する言語以外にもJavaScriptやPythonなども動作させることがでできます。後ほど紹介しますが、Javaのバイトコードを事前にコンパイルするNative Imageという仕組みも利用することができます。今回のデモアプリケーションでは、HelidonアプリケージョンをNative Imageとしてビルドして稼働させています。また、今回は、Nuxt.jsで実装したフロントエンドアプリケーションもGraalVMで動作させています。

* [Helidon](https://helidon.io/)：非常に軽量なオープンソースのアプリケーションフレームワークです。マイクロサービス・アーキテクチャを導入する際に非常に相性が良いオープンソースのJavaのアプリケーション・フレームワークとなっています。今回のデモアプリケーションでは、バックエンドアプリケーションをHelidonで実装しています。

* [Oracle Container Engine for Kubernetes(OKE)](https://docs.cloud.oracle.com/ja-jp/iaas/Content/ContEng/Concepts/contengoverview.htm)：Oracle Cloud Infrastructureで提供されているマネージドのKubernetesサービスです。今回のデモアプリケーションでは、アプリケーションのランタイム環境としてOKEを利用しています。


# デモの構成

このデモは、以下の構成になっています。

```
.
├── devdays-demo-backend_kohaku
├── devdays-demo-backend_trendword
├── devdays-demo-frontend
├── k8s_manifest
```

それぞれのディレクトリについて見ていきます。
ソースコードまで見ていくとキリがないので、今回はDockerfileとmanifestを中心に見ていきます。

## Dockerfile

まずはそれぞれのアプリケーションのDockerfileを見ていきます。

### devdays-demo-backend_kohaku

```Dockerfile

# 1st stage, build the app
FROM helidon/jdk11-graalvm-maven:20.1.0 as build
```

今回はNative Imageとしてビルドするためにhelidon公式で公開されているベース・イメージを利用します。
このイメージには、HelidonをビルドするためのJDKの他にGraalVMとmavenコマンドが梱包されています。

```Dockerfile
WORKDIR /helidon

# Create a first layer to cache the "Maven World" in the local repository.
# Incremental docker builds will always resume after that, unless you update
# the pom
ADD pom.xml .
RUN mvn package -Pnative-image -Dnative.image.skip -Dmaven.test.skip -Declipselink.weave.skip
```

作業ディレクトリ`WORKDIR`を作成し、ローカルからコンテナに`pom.xml`をコピーします。
その後、mavenコマンドでビルドしていきます。
ここでは、`pom.xml`に定義されている内容を元にライブラリや依存関係などをローカルにキャッシュするためのプロセスです。
このプロセスを一度実行すると、以降のビルドでは`pom.xml`が更新されるまではこのプロセスはスキップされます。
また、実際にNative Imageはビルドされません。

```Dockerfile
# Do the Maven build!
# Incremental docker builds will resume here when you change sources
ADD src src
RUN mvn package -Pnative-image -Dnative.image.buildStatic -DskipTests

RUN echo "done!"
```
ここではじめてソースコードをローカルからコピーし、ビルドを行っていきます。
`-Pnative-image -Dnative.image.buildStatic`オプションを指定することで、Native Imageとしてビルドされます。

遅くなりましたが、ここでNative Imageについて紹介しておきます。
上記で紹介したセッション資料にも記載している絵を利用して説明します。
![Greenshot 2020-12-20 15.04.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40392/c16e3bb7-9639-13c1-f203-324fac59da50.png)

Native Imageとは、Javaコード(JVMベースの言語)を事前コンパイルし、スタンドアローンで実行可能な形にコンパイルするしたものです。

具体的には

* 依存関係にあるアプリケーションクラス群
* 実行時に利用するJDKクラス群(ランタイム環境)
* 静的にリンクされたJDKのネイティブコード

などを含んでいます。

メリットとしては、

* ランタイム起動時間の短縮
* メモリフットプリントの極小化
* セキュリティの向上(ランタイム環境の隔離)

などがあげられます。

ビルドしたNative Imageはシングルバイナリとして実行することが可能です。

```Dockerfile
# 2nd stage, build the runtime image
FROM scratch
WORKDIR /helidon

# Copy the binary built in the 1st stage
COPY --from=build /helidon/target/backend-kohaku-app .

ENTRYPOINT ["./backend-kohaku-app"]

EXPOSE 8081
```

ここからはマルチステージビルドを利用して、非常に軽量なベース・イメージ`scratch`に先ほどビルドしたNative Imageをコピーします。

最後にNative Imageを実行します。

ランタイム環境も含めたバイナリなので、JDKがベース・イメージに不要ですし、`java -jar`などのコマンドも付与することなく実行できます。

最後の行の`EXPOSE 8081`はローカル環境で動作確認をするために付与したものです。

### devdays-demo-backend_trendword

こちらも`devdays-demo-backend_kohaku`と全く同じDockerfileになっています。

```Dockerfile
# 1st stage, build the app
FROM helidon/jdk11-graalvm-maven:20.1.0 as build

WORKDIR /helidon

# Create a first layer to cache the "Maven World" in the local repository.
# Incremental docker builds will always resume after that, unless you update
# the pom
ADD pom.xml .
RUN mvn package -Pnative-image -Dnative.image.skip -Dmaven.test.skip -Declipselink.weave.skip

# Do the Maven build!
# Incremental docker builds will resume here when you change sources
ADD src src
RUN mvn package -Pnative-image -Dnative.image.buildStatic -DskipTests

RUN echo "done!"

# 2nd stage, build the runtime image
FROM scratch
WORKDIR /helidon

# Copy the binary built in the 1st stage
COPY --from=build /helidon/target/backend-trendword-app .

ENTRYPOINT ["./backend-trendword-app"]
```

異なるのは、Native Imageのファイル名だけになります。

### devdays-demo-frontend
同様にフロントエンドアプリケーションもDockerfileを見ていきます。

```Dockerfile
FROM oracle/graalvm-ce:latest

# GraalVMのnpmバイナリをPATHに追加
ENV PATH $PATH:$JAVA_HOME/bin/npm

# ディレクトリ作成
WORKDIR /app
```

冒頭でお話しした通り、今回はNuxt.jsのアプリケーションをGraalVMで動作させてきます。
GraalVMにはgraaljsというECMAScript2020互換の仕組みが実装されているので、これを利用します。
ちなみにgraaljsは[こちら](https://github.com/oracle/graaljs)に詳細があります。
ベース・イメージにGraalVM CE(Community Edition)を使用していますが、この中にnpmバイナリが梱包されています。
このnpmバイナリにパスを通すことで、以降のNuxt.jsアプリケーションのビルドに利用していきます。

```Dockerfile
# パッケージをコピー
COPY package*.json ./

# npm モジュールをインストール
RUN npm install --quiet

# 成果物コピー
COPY . .

# このコマンドをしないといけないとwarnが出たので
RUN npm rebuild

# 本当はいらないが開発環境でvue-cliを使っていたのでそこに含まれているパッケージを使っているようでwarnが出たので入れる
RUN npm install vue-cli -g

# ビルド
RUN npm run build

# なくても良い
ENV HOST 0.0.0.0
# なくても良い
EXPOSE 3000

# 起動
CMD ["npm", "run", "start"]
```
ここから先は、GraalVMに梱包されているnpmバイナリでビルドしていくだけです。

## manifest

続いて、各アプリケーションのmanifestを見ていきます。

### backend_kohaku.yaml
要所をピックアップしていきます。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-kohaku-app
  labels:
    app: backend-kohaku
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend-kohaku
```
今回レプリカ数は2個で冗長化しています。

```yaml
    spec:
      containers:
      - name: backend-kohaku
        image: phx.ocir.io/orasejapan/devdays2020/backend-kohaku-app
        ports:
        - containerPort: 8081
```
コンテナイメージは、Oracle Cloud Infrastructure Registry(OCIR)に登録しています。
アプリケーションは8081ポートで公開します。

```yaml
      - name: h2kohaku
        image: oscarfonts/h2
        env:
        - name: H2_OPTIONS
          value: "-ifNotExists"
        ports:
        - containerPort: 81
        - containerPort: 1521
```
今回は、サイドカーとしてもう一つのコンテナをデータソース(データベース)として利用します。
サイドカーパターンについては[こちら](https://docs.microsoft.com/ja-jp/azure/architecture/patterns/sidecar)を参考にしてください。

サイドカーに対してはlocalhostでアクセスできます。

次にアプリケーションに対するServiceの設定です。
フロントエンドのサービスからバックエンドアプリケーションをルーティングするための設定です。

```yaml
apiVersion: v1
kind: Service
metadata:
  name: backend-kohaku-svc
spec:
  type: ClusterIP 
  ports:
  - port: 8081
    targetPort: 8081
  selector:
    app: backend-kohaku
```
ポートについては、アプリケーション側で利用している8081ポートをそのまま流しています。
今回はClusterIPタイプを利用しています。
今回はフロントエンドをSSR(サーバサイドレンダリング)で実装しているので、バックエンドをサーバサイド(ブラウザではなく)で呼び出しているので、ClusterIPとしました。

### backend_trendword.yaml
ほぼ`backend_kohaku.yaml`と同様です。異なるのはポート番号のみです。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-trendword-app
  labels:
    app: backend-trendword
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend-trendword
  template:
    metadata:
      labels:
        app: backend-trendword
    spec:
      containers:
      - name: backend-trendword
        image: phx.ocir.io/orasejapan/devdays2020/backend-trendword-app 
        ports:
        - containerPort: 8082
      - name: h2trendword
        image: oscarfonts/h2
        env:
        - name: H2_OPTIONS
          value: "-ifNotExists"
        ports:
        - containerPort: 81
        - containerPort: 1521
---
apiVersion: v1
kind: Service
metadata:
  name: backend-trendword-svc
spec:
  type: ClusterIP 
  ports:
  - port: 8082
    targetPort: 8082
  selector:
    app: backend-trendword
```

### frontend_app.yaml

最後にフロントエンドアプリケーションのmanifestを見ていきたいと思います。

こちらも要所要所で解説していきます。

```yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-app
  labels:
    app: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
```

フロントエンドアプリケーションも、バックエンドアプリケーションと同様に2個のレプリカで冗長化しています。

```yaml
    spec:
      containers:
      - name: frontend
        image: phx.ocir.io/orasejapan/devdays2020/frontend-app
        ports:
        - containerPort: 3000
        env:
        - name: HOST
          value: "0.0.0.0"
```
コンテナイメージはバックエンド同様にOCIRにpushしています。

コンテナポートは3000ポートで公開しています。

```yaml
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: LoadBalancer
  ports:
  - port: 3000
  selector:
    app: frontend
```
こちらはインターネット側からのルーティングとしてLoadBalancerを利用しています。
ポート番号はアプリケーションと同様に3000番ポートで流しています。

以上がmanifestの紹介になります。

# まとめ

今回はOracle Cloud Infrastructureで利用できる各種テクノロジーでマイクロサービスを実装してみました。


もちろん、マイクロサービス自体は今回解説したものだけで簡単に実装できてしまうものではありません。

ただ、今回利用したような技術スタックを利用していけば、少しでも効率的に実装や運用を行うことができるのではないかと思います。

## 最後に...

今回ご紹介したGraalVM(Community Edition)とHelidonはオープンソースで公開されています。
積極的に開発が進められているプロダクトになりますので、ぜひお試しいただければと思います！

ちなみにGraalVM Enterprise EditionはOracle Cloud Infrastructureで無償でご利用いただけます！