Elixirマイクロサービス②：Umbrellaでサービス移動が容易かつAPI地獄にならないマイクロサービス／デマイクロサービス

この記事は、[Elixir Advent Calendar 2023 シリーズ11](https://qiita.com/advent-calendar/2023/elixir) の18日目です

[piacere](https://twitter.com/piacere_ex) です、ご覧いただいてありがとございます :bow:

[前回は、マイクロサービスの現状と「マイクロサービスパターン」に触れた後、Elixirがマイクロサービス構築に有利な面の紹介](https://qiita.com/piacerex/items/e298a8c0f131f2368ce1) をしましたが、今回は、Elixirマイクロサービス化（デマイクロサービス化も）の有力な機能の1つである「Unmbrella」について実践交えて解説します
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/5bd17822-d35a-026d-5204-8855b1873964.png)](https://speakerdeck.com/piacerex/elixirdemaikurosabisupatan-qian-bian-aimltoetuzikonpiyuteingumo?slide=39)

あと、このコラムが、面白かったり、役に立ったら、![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/907a8695-4d83-1b5c-8052-d0ce52de5f66.png) をお願いします :bow:

# 事前準備：モノリシックからスタートする

マイクロサービス／デマイクロサービスの経過をより実感しやすくするため、最初は、モノリシックな構成からスタートすることとしましょう

ブログとそのコメント、それらから参照される画像ストアの3つのサービスを持たせます（本来であればログインユーザーで投稿者やコメント者を管理／認証するところですが今回は割愛）

モノリシックなPhoenix PJを作成し、Phoenix起動します

```bash
mkdir apps
cd apps
mix phx.new basic
cd basic
mix ecto.create
cd apps/basic_web
mix phx.gen.live Pictures Picture pictures image:string
mix phx.gen.live Posts Post posts title:string body:text picture_id:integer
mix phx.gen.live Posts Comment comments post_id:integer body:text
mix ecto.migrate
cd ../..
iex -S mix phx.server
```

下記コード修正も行ってください

```diff_elixir:apps/basic/lib/posts/post.ex
…
defmodule Basic.Posts.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :title, :string
    field :body, :string
    field :picture_id, :integer
+   has_many :pictures, Basic.Posts.Comment

    timestamps()
  end
…
```

```diff_elixir:apps/basic/lib/posts/comment.ex
defmodule Basic.Posts.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :body, :string
    field :post_id, :integer
+   has_many :posts, Basic.Posts.Post

    timestamps()
  end
…
```

```diff_elixir:apps/basic_web/router.ex
defmodule BasicWeb.Router do
…
  scope "/", BasicWeb do
    pipe_through :browser

+   live "/pictures", PictureLive.Index, :index
+   live "/pictures/new", PictureLive.Index, :new
+   live "/pictures/:id/edit", PictureLive.Index, :edit
+   live "/pictures/:id", PictureLive.Show, :show
+   live "/pictures/:id/show/edit", PictureLive.Show, :edit
+
+   live "/posts", PostLive.Index, :index
+   live "/posts/new", PostLive.Index, :new
+   live "/posts/:id/edit", PostLive.Index, :edit
+   live "/posts/:id", PostLive.Show, :show
+   live "/posts/:id/show/edit", PostLive.Show, :edit
+
+   live "/comments", CommentLive.Index, :index
+   live "/comments/new", CommentLive.Index, :new
+   live "/comments/:id/edit", CommentLive.Index, :edit
+   live "/comments/:id", CommentLive.Show, :show
+   live "/comments/:id/show/edit", CommentLive.Show, :edit

    get "/", PageController, :home
…
```

ここまでだと、通常PJと同様で、ブラウザでトップ／Scaffold先が普通に見れます（各Scaffold先に適当なサンプルデータを数件入れておいてください）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/62d3c2ad-e282-3b25-66ab-594a3ab8aad6.png)

なお、フロント側の表示（`posts` 配下に `comments` が並んだり、`picture` が並ぶ）は、本来のアプリケーションであれば必須ですが、今回はマイクロサービス解説のみに限定します

# Umbrellaでマイクロサービス化

Umbrella PJを作成し、その配下にモノリシックから切り離したいサービスを移動し、動作を確認してから、配下Phoenix PJ単位で別のPCやVM（Dockerなど）へサービス移動できます

別PC／VMが用意できない方は、同じPC内の別フォルダで体験いただいてもOKです（ただし、ポート番号が重ならない工夫が要ります）

サービス移動元でUmbrella PJを作成します

```bash
mix new a --umbrella
cd a/app
```

Umbrella構成だと、`apps` フォルダ配下にPJフォルダを配置するので、`apps` フォルダ配下に上記で作成したPhoenix PJをコピーします

`deps` はUmbrella PJ直下で複数PJが共有する形となります

なお、`apps` フォルダ配下で複数PJを作成できますが、その際は、`config/` フォルダ配下のファイルに各PJ設定が追記書き込みされていきます（これにより、esbuildやtailwind、logger、json_library、import_configが重複し、エラーとなるので手動調整が必要ですが、これは別の機会に扱い、今回は割愛）

あと、今後のScaffoldもUmbrella PJ直下では無く、`apps` フォルダ配下PJで実行します

なお複数Phoenixが配下にいても、Umbrella PJ直下からの起動は、全てのPhoenixを一斉に起動してくれます

では、2台目のPC／VM用のUmbrella PJを作りましょう

```bash:【2台目のPC／VM】
mix new b --umbrella
cd b/apps
mix phx.new basic
cd basic
mix ecto.create
```

この2台目からDB接続先を1台目のPCのIPアドレスに指定しておきましょう

```diff_elixir:config/dev.exs【2台目のPC／VM】
import Config

# Configure your database
config :basic_2, Basic2.Repo,
  username: "postgres",
  password: "postgres",
- hostname: "localhost",
+ hostname: "172.24.82.141",
  database: "basic_dev",
```

同一PC内であれば、Phoenixのポート番号を変更しておきましょう

```diff_elixir:config/dev.exs【2台目のPC／VM相当】
import Config
…
config :basic_2, Basic2Web.Endpoint,
  # Binding to loopback ipv4 address prevents access from other machines.
  # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
- http: [ip: {127, 0, 0, 1}, port: 4000],
+ http: [ip: {127, 0, 0, 1}, port: 4001],
…
```

Scaffoldをした上で、Phoenix起動します

```bash:【2台目のPC／VM】
mix phx.gen.live Memos Memo memos title:string body:text
mix ecto.migrate
cd ../..
iex -S mix phx.server
```

```diff_elixir:apps/basic_2/lib/basic_2_web/router.ex【2台目のPC／VM】
defmodule Basic2Web.Router do
…
  scope "/", Basic2Web do
    pipe_through :browser

+   live "/memos", MemoLive.Index, :index
+   live "/memos/new", MemoLive.Index, :new
+   live "/memos/:id/edit", MemoLive.Index, :edit
+   live "/memos/:id", MemoLive.Show, :show
+   live "/memos/:id/show/edit", MemoLive.Show, :edit

    get "/", PageController, :home
…
```

ブラウザで `http://localhost:4001` を確認しておきます（別PCやDockerであれば4000番のままでOK）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/19de2811-fddf-bf44-ec51-5d2cfdcbde1f.png)

では、このPJ配下に、序盤で作成した `pictures` をサービス移動するので、`basic_web/live/picture_live` と `` を持っていきましょう

同一PC内であれば、下記コマンドです

```bash:1台目のPC／VM
cd ~
mkdir serive_b/basic_umbrella/apps/basic_web/lib/basic_web/live/
mv a/basic_umbrella/apps/basic_web/lib/basic_web/live/picture_live/ basic_b_umbrella/apps/basic_b_web/live/
```

routerを各々、変更します

```diff_elixir:apps/basic_web/lib/basic_web/router.ex（1台目のPC／VM）
defmodule BasicWeb.Router do
…
  scope "/", BasicWeb do
    pipe_through :browser

-   live "/pictures", PictureLive.Index, :index
-   live "/pictures/new", PictureLive.Index, :new
-   live "/pictures/:id/edit", PictureLive.Index, :edit
-   live "/pictures/:id", PictureLive.Show, :show
-   live "/pictures/:id/show/edit", PictureLive.Show, :edit
-
+   live "/posts", PostLive.Index, :index
+   live "/posts/new", PostLive.Index, :new
+   live "/posts/:id/edit", PostLive.Index, :edit
+   live "/posts/:id", PostLive.Show, :show
+   live "/posts/:id/show/edit", PostLive.Show, :edit
+
+   live "/comments", CommentLive.Index, :index
+   live "/comments/new", CommentLive.Index, :new
+   live "/comments/:id/edit", CommentLive.Index, :edit
+   live "/comments/:id", CommentLive.Show, :show
+   live "/comments/:id/show/edit", CommentLive.Show, :edit

    get "/", PageController, :home
…
```

```diff_elixir:apps/basic_web/lib/basic_web/router.ex（2台目のPC／VM）
defmodule BasicWeb.Router do
…
  scope "/", BasicWeb do
    pipe_through :browser

    live "/memos", MemoLive.Index, :index
    live "/memos/new", MemoLive.Index, :new
    live "/memos/:id/edit", MemoLive.Index, :edit
    live "/memos/:id", MemoLive.Show, :show
    live "/memos/:id/show/edit", MemoLive.Show, :edit

+   live "/pictures", PictureLive.Index, :index
+   live "/pictures/new", PictureLive.Index, :new
+   live "/pictures/:id/edit", PictureLive.Index, :edit
+   live "/pictures/:id", PictureLive.Show, :show
+   live "/pictures/:id/show/edit", PictureLive.Show, :edit

    get "/", PageController, :home
…
```

あとは、この切り離したPhoenix PJを、移動先のUmbrella PJ配下に移動します

実は、Elixir Umbrellaにおけるサービス移動は、たったこれだけです :stuck_out_tongue_winking_eye:

<!--
準備は整ったので、1台目の方のPhoenixを、下記でiex起動し直します（`@` の後ろはPCのIPアドレスを記入）

```bash:1台目のPC／VM
iex --name service_a@172.24.82.141 --cookie services -S mix phx.server
```

`basic_a` の方のPJを下記でiex起動し直します

```bash:2台目のPC／VM
iex --name b@172.24.82.141 --cookie services -S mix phx.server
```

1台目のPCとの通信を確認後（`:pong` は通信成功、`:pang` は通信失敗）、接続します

```diff_elixir:2台目のPC／VM
iex> Node.ping(:"service_a@172.24.82.141")
:pong
iex> Node.connect(:"service_a@172.24.82.141")
true
iex> Node.list
[:"service_a@172.24.82.141"]
```

1台目のPCからも2台目のPCが見れます

```diff_elixir:1台目のPC／VM
iex> Node.list
[:"b@172.24.82.141"]
```
-->

# Umbrellaでデマイクロサービス化

Phoenix PJごと、元のUmbrella配下に戻すだけです

# この方式のメリット／デメリット

ここで注目したいのは、サービスを移動したにも関わらず、APIは1つも作っていないし、DB移動／移行も伴っていないという観点です

純粋な開発効率向上としては、このパターンで充分なケースも多くあるのですが、「マイクロサービス」と言うと、どうしても「それら全てを移動しなければならない」という固定観念があるようです

しかし、それは実は「技術スタックの自由度とエンジニアにかかる負荷を天秤にかけている」、もっと言えば「開発経済性をちゃんと計算した上で技術選定できているのか？」という疑問が残る領域だったりします

ただし、この方式には、下記のデメリットも存在します

- DBが移動していないことで、チームのマイクロ化は厳密には成されていない
  - いわゆる「逆コンウェイの法則」は実現していない
- 同一DBに対してEctoサーバが2箇所のため、トランザクションの不整合があり得る
  - 通常のマイクロサービスであれば、APIサーバ毎にトランザクションは閉じる
    - もっともXAトランザクション的なことにそもそも不利なので程度問題かも？
- 分散されていないことに伴う性能劣化もあり得る

ここをカバーする仕組みは、続編で扱いたいと思います

とは言え、もっとも重要なのは、「サービス分割による開発効率向上」、マイクロサービスパターンで言うところの2つの「Decompose by XXXXXXX」を、API作成／DB移行不要で、Elixirはいとも簡単に叶えてしまうという点です

# コンテナを使わずマイクロサービスを構成するメリット

- 仮想化層を含まないため、性能劣化が無く、メモリ利用がヘルシー

# 終わりに

Elixirにおけるマイクロサービス／デマイクロサービスが、いかにサービス移動がカンタンで、更にAPI Hellに陥らないかが実感できたでしょうか？

これは、LiveViewを使えば、SPA開発でAPI Hellに陥らない優位性のマイクロサービス版みたいなものです

次回は、このDB移動を伴うパターンについて、これまたElixirならではのテクニックについて解説します

# p.s.このコラムが、面白かったり、役に立ったら…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/907a8695-4d83-1b5c-8052-d0ce52de5f66.png) にて、どうぞ応援よろしくお願いします :bow: