21
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ElixirAdvent Calendar 2021

Day 20

[Elixir] libclusterを使ってKubernetes上でErlangクラスタを形成する

Posted at

私は普段、インフラ周りの仕事をすることがよくあるのですが、最近初めてKubernetes(k8s)上でElixir/PhoenixアプリのErlangクラスタを形成(クラスタリング)する機会がありました。libcluster というライブラリを使えば簡単にクラスタリングできますが、公式ドキュメントの説明だけだとk8sの設定が足りずうまくいきませんでした…

そこで、今回は公式ドキュメントを補足しつつ、libclusterを使ってk8s上でElixir/Phoenixアプリをクラスタリングする方法を解説します。

解説の前に

以下については本題ではないため、説明を省略します。

  • k8sでWebアプリを動かすためのPodやDeployment、Serviceといった各種用語
  • kubectlコマンドの使い方
  • mix release 周りのこと

k8s上で動かすElixir/Phoenixアプリを用意する

クラスタリングできていることを視覚的に確認できるように、Phoenix LiveViewで作った簡単なアプリをk8s上で動かすことにします。

匿名で短い文章を投稿できるアプリです。あるセッションでの投稿/更新/削除が別のセッションにも反映されるようになっており、以下のデモ動画のように動きます。

仕組みの説明は省略しますが、興味がある方は以下のスライドを見てみてください。

minikubeを使ってローカルにk8sクラスタを作る

k8sクラスタを作る方法は何でもいいんですが、ローカルで気軽に試せるように今回は minikube を使います。

$ minikube start

アプリを動かすためにPostgreSQLが必要なので、Helm でさくっと入れちゃいましょう。諸々の簡略化のため、postgresユーザのパスワードを固定して起動します。

$ helm repo add bitnami https://charts.bitnami.com/bitnami
$ helm install db --set auth.postgresPassword=postgres bitnami/postgresql

k8sにアプリをデプロイする

本題ではないので詳しい説明は省略しますが、今回は Skaffold を使ってデプロイします。

$ skaffold run

デプロイが完了したら、DBマイグレーションを忘れずに実行してください。クラスタリングできていることを確認するためにPodが2つ動くようにしていますが、どちらのPodで実行しても構いません。

$ kubectl exec {pod_name_1_or_2} -it -- bin/migrate

DBマイグレーションを実行したら、Terminalを2つ起動してそれぞれのPodで動くアプリにブラウザからアクセスできるようにします。

Terminal 1
$ kubectl port-forward {pod_name_1} 4000:4000
Terminal 2
$ kubectl port-forward {pod_name_2} 4001:4000

ブラウザでタブを2つ開いてそれぞれ http://localhost:4000/micropostshttp://localhost:4001/microposts にアクセスして操作すると、デモ動画と同様に投稿/更新/削除がお互いに同期されるようになっているはずです :clap:

何をやっているのか

とりあえずk8s上でクラスタリングできましたが、ここからは具体的にどう対応するとクラスタリングできるのか解説していきます。

libclusterを導入する

まずはlibclusterをインストールします。

mix.exs
  defp deps do
    [
+     {:libcluster, "~> 3.3"},
      {:phoenix, "~> 1.6.2"},
$ mix deps.get

次に、libclusterの設定を追加します。設定内容は 公式ドキュメントの設定例 とほぼ同じですが、 kubernetes_node_basenamekubernetes_selector のみ今回のアプリ用に変更しています(変更内容については後述)。

config/runtime.exs
  # For this example you need include a HTTP client required by Swoosh API client.
  # Swoosh supports Hackney and Finch out of the box:
  #
  #     config :swoosh, :api_client, Swoosh.ApiClient.Hackney
  #
  # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
+
+  config :libcluster,
+    topologies: [
+      k8s_example: [
+        strategy: Cluster.Strategy.Kubernetes,
+        config: [
+          mode: :ip,
+          kubernetes_node_basename: "sample",
+          kubernetes_selector: "app=microposts-sample",
+          kubernetes_namespace: "default",
+          polling_interval: 10_000]]]

そして、libclusterのスーパーバイザを追加すると、設定に沿って他のErlangノードを探して接続してくれるようになります。

lib/sample/application.ex
  def start(_type, _args) do
+   topologies = Application.get_env(:libcluster, :topologies, [])
+
    children = [
+     {Cluster.Supervisor, [topologies, [name: Sample.ClusterSupervisor]]},
      # Start the Ecto repository
      Sample.Repo,

Erlangノード名を設定する

libclusterの設定に mode: :ip という部分がありますが、このモードではPodのIPアドレスで他ノードと接続するため、ノード名が sample@{IPアドレス} というフォーマットになるように rel/env.sh.eex を追加します。ノード名に含まれる sample はlibclusterの設定における kubernetes_node_basename と同じ値である必要があります。

rel/env.sh.eex
export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=sample@${POD_IP}

また、Pod内で POD_IP という環境変数でIPアドレスを参照できるように、以下の指定が必要です。

k8s/deployment.yaml
    spec:
      containers:
        - name: microposts-sample
          image: microposts-sample
          env:
            - name: DATABASE_URL
              value: ecto://postgres:postgres@db-postgresql/postgres
            - name: SECRET_KEY_BASE
              value: mysecretmysecretmysecretmysecretmysecretmysecretmysecretmysecret
+           # For libcluster
+           - name: POD_IP
+             valueFrom:
+               fieldRef:
+                 fieldPath: status.podIP
          ports:
            - containerPort: 4000

k8sに必要なリソースを追加する

libclusterのデフォルト設定では、 kubernetes_ip_lookup_mode という項目が :endpoints になっています。これは、他PodのIPアドレスを探すためにk8sのEndpointリソースを参照するというモードです。

EndpointリソースはServiceリソースを作成すると自動的に作成されるので、まずはServiceリソースを追加します。

k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: microposts-sample
  labels:
    app: microposts-sample # ①
spec:
  type: ClusterIP
  ports:
    - name: http
      protocol: TCP
      port: 4000
      targetPort: 4000
  selector:
    app: microposts-sample # ②

libclusterの設定に kubernetes_selector: "app=microposts-sample" という部分がありますが、これはlibclusterがEndpointリソースを参照する際のセレクタで、①と②の指定が必要です。

また、PodにはデフォルトではEndpointリソースを参照する権限がないため、RoleリソースとRoleBindingリソースを作成して権限を付与します。説明の簡略化のためデフォルトのServiceAccountに権限を付与しますが、別途ServiceAccountを作成してPodに指定しても構いません。

k8s/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: endpoints-reader
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-endpoints
  namespace: default
subjects:
  - kind: ServiceAccount
    name: default
    namespace: default
roleRef:
  kind: Role
  name: endpoints-reader
  apiGroup: rbac.authorization.k8s.io

ここまでの設定で、k8s上でクラスタリングできるようになりました :tada:

なお、以下のようなコマンドを実行して他ノードの一覧が得られれば、わざわざ画面操作しなくてもクラスタリングできていることを確認できます :sparkles:

$ kubectl exec {pod_name_1_or_2} -it -- bin/sample rpc "IO.inspect(Node.list)"
実行結果(例)
[:"sample@172.17.0.7"]

最後に

他の言語で複数サーバのセッションやWebSocket接続の管理をする際はRedisを使うことが多いですが、ElixirであればクラスタリングすることでRedisを使わず簡単に複数台構成を実現できます。libclusterや関連ライブラリでk8s以外でもクラスタリングする機能が提供されているので、各自の環境に合うものを探してみてください。

また、ローカルでしか動かしていなかったElixir/Phoenixアプリをk8s上で動かしつつクラスタリングするための変更点がまとまっているプルリクエストを作っているので、mix releaseを含めすべての変更点を知りたい方はそちらも見てみてください :muscle:

21
18
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?