Posted on

Antikythera on Kubernetes - 2 クラスター作成

まずは, docker-compose で3台固定で Erlang クラスター作成した後、k8s 上で複数 Pod 構成の Erlang クラスター用の実装に置き換え, クラスタリングしてみます。

docker-composeでErlangクラスターを作る

Antikythera instanceをセットアップする

Antikythera instance example に従ってAntikythera instanceをセットアップします。

git clone https://github.com/access-company/antikythera.git
git clone https://github.com/access-company/antikythera_instance_example.git
git clone https://github.com/access-company/testgear.git

cd antikythera_instance_example
vim mix.exs # [git: "/home/kmt/Documents/antikythera" に
mix deps.get && mix deps.get && mix compile

cd ../testgear
export ANTIKYTHERA_INSTANCE_DEP='{:antikythera_instance_example, [git: "/home/kmt/Documents/antikythera_instance_example"]}'

unlink .tool-versions
cp ../antikythera/.tool-versions .

mix deps.get && mix deps.get

sudo vim /etc/hosts # 127.0.0.1 testgear.localhost
iex -S mix

2024-04-27T05:55:13.693+00:00 [error] module=FileSystem.Backends.FSInotify `inotify-tools` is needed to run `file_system` for your system, check https://github.com/rvoicilas/inotify-tools/wiki for more information about how to install it. If it's already installed but not be found, appoint executable file with `config.exs` or `FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE` env.
docker pull rtvu/docker-elixir
docker build rtvu/docker-elixir --build-arg ERLANG_VERSION=24.3.4.13 --build-arg ELIXIR_VERSION=1.13.4-otp-24

asdfを使ってerlangとelixirのベースイメージを作る

ベースイメージを作ったあと dc run <container> sh で中に入ってdc build --progress=plain で出力を確認しながらインクリメンタルにDockerfileを作るのがおすすめです。以下注意点

Erlang + ElixirのDockerfileが作れたらイメージをDocker Hubにプッシュしましょう

https://github.com/wakame-tech/antikythera_k8s/blob/feature/dockerize/Dockerfile

FROM vborja/asdf-ubuntu:latest

ARG ERLANG_VERSION
ARG ELIXIR_VERSION

USER root

# https://github.com/asdf-vm/asdf-erlang#before-asdf-install
RUN \
    apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y \
    build-essential \
    autoconf \
    m4 \
    libncurses5-dev \
    libwxgtk3.0-gtk3-dev \
    libwxgtk-webview3.0-gtk3-dev \
    libgl1-mesa-dev \
    libglu1-mesa-dev \
    libpng-dev \
    libssh-dev \
    unixodbc-dev \
    xsltproc \
    fop \
    libxml2-utils \
    libncurses-dev

USER asdf

RUN asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
RUN export KERL_CONFIGURE_OPTIONS="--without-javac"
ENV ERLANG_VERSION $ERLANG_VERSION

RUN asdf install erlang $ERLANG_VERSION
RUN asdf global erlang $ERLANG_VERSION

RUN asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
ENV ELIXIR_VERSION $ELIXIR_VERSION
RUN asdf install elixir $ELIXIR_VERSION
RUN asdf global elixir $ELIXIR_VERSION

RUN mix local.hex --force && mix local.rebar --force
docker build --tag=wakametech/asdf-erlang-elixir:latest ./docker/ --build-arg ERLANG_VERSION=24.3.4.13 --build-arg ELIXIR_VERSION=1.13.4-otp-24
docker push wakametech/asdf-erlang-elixir:latest

a9aのDockerイメージを作る

今作ったイメージをベースイメージとしてAntikythera instanceが動くDockerイメージを作ります。

  • ビルド時に必要な依存
    • inotify-tools
    • libexpat1-dev: fast_xmlのために必要らしい

リリースの作成

1からリリースを作るのは大変そう & 理解していないので AntikytheraLocal.RunningEnvironment を使い回す。

  • mix antikythera_local.start <gears> でa9a本体の起動とgearの配置
    • たまにスペックの問題?で wait_until_directories_are_created() が失敗する
    • daemonで起動するのでコンテナが終了しないように tty: true にしておく
    • deps/antikythera/tmp/local/release_per_node がリリースディレクトリになる
    • ログ確認 tail -f deps/antikythera/tmp/local/release_per_node/tmp/log/erlang.log.1
  • 起動スクリプト
    • a9a_k8sをclone, ビルド, gearをclone, ビルド
    • http://localhost:8082/healthcheck (=> :8080) が返ってくるようになりました
git clone -b feature/dockerize https://github.com/wakame-tech/antikythera_k8s.git .
mix deps.get && mix deps.get

export ANTIKYTHERA_INSTANCE_DEP="{:antikythera_k8s, [github: \"wakame-tech/antikythera_k8s\"]}"
mkdir gears
git clone https://github.com/access-company/testgear.git

remote_consoleに接続できない

  • AntikytheraLocal.NodeName.get()antikythera@$(hostname -f).local
    • ドットが含まれている時は .local をつけない
    • ホスト名はドットが含まれていなければならない
      • ** System running to use fully qualified hostnames **
      • ** Hostname antikythera-k8s-b9cdc79dd-dgzf6 is illegal **
    • hostname -f3820709c2d5a (コンテナID) でドットが含まれていない
      • -> compose.yamlに services.app.hostname=a9a-k8s.local を設定
MIX_ENV=prod \
RELEASE_NODE="antikythera@$(hostname -f)" \
RELEASE_COOKIE="local" \
rel_local_erlang-24/antikythera_k8s/bin/antikythera_k8s remote

docker-composeでerlangクラスターを作る

3台からなるerlangクラスターをdocker-composeで作ってみます。

  • ホスト名: antikythera-k8s-{1,2,3}.local
  • IPアドレス: 192.168.100.{11,12,13}

IPアドレスの固定は <service>.networks.<network>.ipv4_address で, extra_hosts を書くとコンテナ内の /etc/hosts に追加されるらしいです

services:
  app1:
    hostname: antikythera-k8s-1.local
    networks:
      app_net:
        ipv4_address: 192.168.100.11
    extra_hosts:
      - "antikythera-k8s-2.local:192.168.100.12"
      - "antikythera-k8s-3.local:192.168.100.13"
  app2:
    hostname: antikythera-k8s-2.local
    networks:
      app_net:
        ipv4_address: 192.168.100.12
    extra_hosts:
      - "antikythera-k8s-1.local:192.168.100.11"
      - "antikythera-k8s-3.local:192.168.100.13"
  app3:
    hostname: antikythera-k8s-3.local
    networks:
      app_net:
        ipv4_address: 192.168.100.13
    extra_hosts:
      - "antikythera-k8s-1.local:192.168.100.11"
      - "antikythera-k8s-2.local:192.168.100.12"
networks:
  app_net:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.100.0/24

iexで確認

$ dc up --build -d
# それぞれで
$ dc exec app1 bash
asdf@antikythera-k8s-1:/app$ iex --name antikythera@$(hostname -f) --cookie cookie
iex --name antikythera@$(hostname -i) --cookie cookie
> Node.ping(:"node@antikythera-k8s-2.local")
:pong
> Node.list()
[:"antikythera@antikythera-k8s-2.local", :"antikythera@antikythera-k8s-3.local"]

AntikytheraEal.ClusterConfiguration の実装

  • EAL: Environment Abstraction Layerらしい
    • クラスターの設定は AntikytheraEal.ClusterConfiguration.Behaviour でインターフェースになっていてデフォルト実装は AntikytheraEal.ClusterConfiguration.StandAlone でした
    • running_host()Node.self() しか返さない
    • config.exsの erl_impl_modules.cluster_configuration で差し替えられるみたい
    • 固定でホストを返す実装 AntikytheraK8s.ClusterConfigurationStandAlone を参考に書きます
use Croma
alias Croma.Result, as: R

defmodule AntikytheraK8s.ClusterConfiguration do
  @behaviour AntikytheraEal.ClusterConfiguration.Behaviour

  @impl true
  defun running_hosts() :: R.t(%{String.t() => boolean}) do
    {:ok,
     %{
       "antikythera-k8s-1.local" => true,
       "antikythera-k8s-2.local" => true,
       "antikythera-k8s-3.local" => true
     }}
  end

  @impl true
  defun(zone_of_this_host() :: String.t(), do: "zone")

  @impl true
  defun(health_check_grace_period_in_seconds() :: non_neg_integer, do: 30)
end

動作確認

config :antikythera,
  antikythera_instance_name: :antikythera_k8s,
  eal_impl_modules: [
    cluster_configuration: AntikytheraK8s.ClusterConfiguration
  ]
asdf@antikythera-k8s-1:/app$ ./remote.sh
iex(antikythera@antikythera-k8s-1.local)1> RaftFleet.active_nodes
%{
  "zone" => [:"antikythera@antikythera-k8s-3.local",
   :"antikythera@antikythera-k8s-1.local",
   :"antikythera@antikythera-k8s-2.local"]
}

k8sでerlangクラスターを作る

再デプロイの手順

  • antikythera-k8sイメージを作成
    • docker build --tag=wakametech/antikythera-k8s:latest . --no-cache
    • docker push wakametech/antikythera-k8s:latest
  • manifestを書く, replicasは3
    • 更新: k apply -f k8s/manifest.yaml
    • リデプロイ: kubectl rollout restart deploy antikythera-k8s
      • 1分位でPodが入れ替わる
  • コンテナにアタッチする k get pod, k exec -it <POD_NAME> bash
    • ホスト名はPod名と同じ, /etc/hosts には自身しかない

修正中は上記のDockerfile更新->push->再デプロイ->アタッチを無限に繰り返す...

# イメージを作成
docker build --tag=wakametech/antikythera-k8s:latest . --no-cache
# イメージをプッシュ
docker push wakametech/antikythera-k8s:latest
# 再デプロイ
kubectl rollout restart deploy antikythera-k8s
# 1分位でPodが入れ替わる, 数分でantikythera_k8sも起動する
k get pod -w
# コンテナにアタッチ
k exec -it `k get pod -o json | jq ' .items[] | select(.metadata.labels.app == "antikythera-k8s") | .metadata.name' | jq -sr '.[0]'` bash
NAME                                  READY   STATUS    RESTARTS        AGE
antikythera-k8s-85cf9994bc-5mvvr      1/1     Running   3 (109s ago)    16m
antikythera-k8s-85cf9994bc-lgjvc      1/1     Running   2 (66s ago)     11m
antikythera-k8s-85cf9994bc-mj8rx      1/1     Running   1 (4m54s ago)   11m

なんかRestartを繰り返す

ClusterConfiguration を実装する

Pod内から kubectl を叩けばホスト名の一覧を取得できそう

ClusterConfigurationの実装
defmodule AntikytheraK8s.ClusterConfiguration do
  @behaviour AntikytheraEal.ClusterConfiguration.Behaviour

  defun get_pod_ip_and_statuses(label_app :: String.t()) :: Map.t(String.t(), boolean()) do
    {res, 0} =
      System.cmd("kubectl", [
        "get",
        "pods",
        "-o",
        "json"
      ])

    Jason.decode!(res)
    |> Map.get("items")
    |> Enum.filter(fn item ->
      item["kind"] == "Pod" && item["metadata"]["labels"]["app"] == label_app
    end)
    |> Enum.map(fn item -> {item["status"]["podIP"], item["status"]["phase"] == "Running"} end)
    |> Map.new()
  end

  @impl true
  defun running_hosts() :: R.t(%{String.t() => boolean}) do
    hosts_with_status = get_pod_ip_and_statuses("antikythera-k8s")

    {:ok, hosts_with_status}
  end
end
asdf@antikythera-k8s-784d7bd9f8-9987c:/app$ MIX_ENV=prod \
  RELEASE_NODE="antikythera@$(hostname -i)" \
  RELEASE_COOKIE="local" \
  rel_local_erlang-24/antikythera_k8s/bin/antikythera_k8s remote
Erlang/OTP 24 [erts-12.3.2.13] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]

Interactive Elixir (1.13.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(antikythera@192.168.1.153)1> Node.list
[:"antikythera@192.168.1.30", :"antikythera@192.168.1.94"]
iex(antikythera@192.168.1.153)2> RaftFleet.active_nodes
%{
  "zone" => [:"antikythera@192.168.1.153", :"antikythera@192.168.1.30",
   :"antikythera@192.168.1.94"]
}

🙌🙌🙌

ホスト名でもつなぎたい

TODO: PodのDNSについて