私は普段、インフラ周りの仕事をすることがよくあるのですが、最近初めて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で動くアプリにブラウザからアクセスできるようにします。
$ kubectl port-forward {pod_name_1} 4000:4000
$ kubectl port-forward {pod_name_2} 4001:4000
ブラウザでタブを2つ開いてそれぞれ http://localhost:4000/microposts と http://localhost:4001/microposts にアクセスして操作すると、デモ動画と同様に投稿/更新/削除がお互いに同期されるようになっているはずです
何をやっているのか
とりあえずk8s上でクラスタリングできましたが、ここからは具体的にどう対応するとクラスタリングできるのか解説していきます。
libclusterを導入する
まずはlibclusterをインストールします。
defp deps do
[
+ {:libcluster, "~> 3.3"},
{:phoenix, "~> 1.6.2"},
$ mix deps.get
次に、libclusterの設定を追加します。設定内容は 公式ドキュメントの設定例 とほぼ同じですが、 kubernetes_node_basename
と kubernetes_selector
のみ今回のアプリ用に変更しています(変更内容については後述)。
# 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ノードを探して接続してくれるようになります。
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
と同じ値である必要があります。
export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=sample@${POD_IP}
また、Pod内で POD_IP
という環境変数でIPアドレスを参照できるように、以下の指定が必要です。
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リソースを追加します。
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に指定しても構いません。
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上でクラスタリングできるようになりました
なお、以下のようなコマンドを実行して他ノードの一覧が得られれば、わざわざ画面操作しなくてもクラスタリングできていることを確認できます
$ 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を含めすべての変更点を知りたい方はそちらも見てみてください