LoginSignup
16
4

More than 1 year has passed since last update.

AWS Copilot で Phoenix Framework の本番環境を構築する クラスタリング編

Last updated at Posted at 2022-06-08

はじめに

AWS Copilot で Phoenix Framework の本番環境を構築するシリーズです

実装したサンプルはこちらに格納しています

実装するシステム

今回はクラスタリングされていることを分かりやすくするため、
Phoenix Channels を用いたチャットシステムを実装します

Phoenix Channels を使うと、 Websocket によるリアルタイム通信を簡単に実装できます

こちらの記事を参考に実装しました

このシステムはチャットの内容を DB には書き込んでいないため、
今まさに送信されたメッセージだけをやりとりします

システムのデプロイ

クラスタリング前のコードはこちら

複数台構成の動作を見たいので、
copilot/lb-svc/manifest.ymlcountspot: 2 に変更します

  • 変更前
count: 1
  • 変更後
count:
  spot: 2

アプリケーション名を react-chat 、サービス名を lb-svc として初期化します

copilot init \
  --app react-chat \
  --name lb-svc \
  --type "Load Balanced Web Service"

test 環境を作ります

copilot env init \
  --name test \
  --profile <プロファイル名> \
  --default-config \
  --container-insights

環境変数 SECRET_KEY_BASE を設定します

export SECRET_KEY_BASE="<任意の値>"

デプロイします

copilot svc deploy \
  --name lb-svc \
  --env test

しばらくすると以下のように表示されます

✔ Deployed service lb-svc.
Recommended follow-up action:
  - You can access your service at http://xxx.ap-northeast-1.elb.amazonaws.com over the internet.

表示された URL にアクセスすると、以下のような画面が表示されます

スクリーンショット 2022-06-01 10.15.19.png

ユーザ名を入力して Join をクリックします

チャットにテキストを入力できるようになりました

スクリーンショット 2022-06-01 10.16.43.png

クラスタリングしていない場合の動作

まず、コンテナの状態を確認してみましょう

$ copilot svc status
Found only one deployed service lb-svc in environment test
Task Summary

  Running            ██████████  2/2 desired tasks are running
  Health             ██████████  2/2 passes HTTP health checks
  Capacity Provider  ▓▓▓▓▓▓▓▓▓▓  2/2 on Fargate Spot

Tasks

  ID        Status      Revision    Started At      Capacity      HTTP Health
  --        ------      --------    ----------      --------      -----------
  049724e2  RUNNING     45          12 minutes ago  FARGATE_SPOT  HEALTHY
  72c217d7  RUNNING     45          12 minutes ago  FARGATE_SPOT  HEALTHY

FARGATE_SPOT で 2台 のコンテナが動いています

それぞれのコンテナのログを見てみましょう

--tasks に表示されたタスク一覧の ID を指定します

--follow で control + C するまでログを追跡し続けます

$ copilot svc logs --tasks 049724e2 --follow
Found only one deployed service lb-svc in environment test

別のターミナルを開いてもう片方のコンテナのログを見ます

$ copilot svc logs --tasks 72c217d7 --follow
Found only one deployed service lb-svc in environment test

この状態でチャットの LEAVE ボタンを押し、改めて JOIN します

すると、以下のようなログがどちらか片方にだけ表示されます

copilot/lb-svc/049724e2f6 01:25:46.457 [info] CONNECTED TO ReactChatWeb.UserSocket in 50µs
copilot/lb-svc/049724e2f6   Transport: :websocket
copilot/lb-svc/049724e2f6   Serializer: Phoenix.Socket.V2.JSONSerializer
copilot/lb-svc/049724e2f6   Parameters: %{"token" => "undefined", "vsn" => "2.0.0"}
copilot/lb-svc/049724e2f6 01:25:46.490 [info] JOINED room:lobby in 16µs
copilot/lb-svc/049724e2f6   Parameters: %{"user_name" => "bbb"}

では、ブラウザの別タブでチャットシステムを開き、別のユーザ名で JOIN してみましょう

一回では同じコンテナに接続されるかもしれませんが、何度かやっているうちに別のコンテナのログに接続結果が表示されるはずです

copilot/lb-svc/72c217d7eb 01:28:18.451 [info] CONNECTED TO ReactChatWeb.UserSocket in 42µs
copilot/lb-svc/72c217d7eb   Transport: :websocket
copilot/lb-svc/72c217d7eb   Serializer: Phoenix.Socket.V2.JSONSerializer
copilot/lb-svc/72c217d7eb   Parameters: %{"token" => "undefined", "vsn" => "2.0.0"}
copilot/lb-svc/72c217d7eb 01:28:18.484 [info] JOINED room:lobby in 16µs
copilot/lb-svc/72c217d7eb   Parameters: %{"user_name" => "xxx"}

4つか5つくらい開くと、2グループのチャットになると思います

同じコンテナに接続したものはチャットを共有しますが、違うコンテナ間ではチャットが共有されません

スクリーンショット 2022-06-01 10.34.32.png

ブラウザからの接続はロードバランサーによって割り振られます

クライアントA と クライアントB が コンテナX に接続され、
クライアントC と クライアントD が コンテナY に接続されたとき、
それぞれのコンテナは別個に動いているため、情報が共有されていないのです

without_clustering.drawio.png

これはいけません

同じチャットに繋いでいるはずなのに、人によって違うチャットに入ってしまっています

DB を使って情報を共有する、という手段もありますが、
リアルタイム性や整合性を求める場合に問題があります

クラスタリングしていないことを別の方法でも確認しましょう

copilot svc exec --task-id <タスクID> で、
コンテナ内に入ることができます(超便利!)

$ copilot svc exec --task-id 049724e2
Found only one deployed service lb-svc in environment test
Execute `/bin/sh` in container lb-svc in task 049724e2f64e4197b6b67349b169876b.

Starting session with SessionId: ecs-execute-command-06b484046dfea7346
/app # 

コンテナ内で Phoenix のセッションに接続してみましょう

<mix release で生成された実行可能ファイル> remote で Phoenix のセッションに接続できます

$ ./bin/react_chat remote
Erlang/OTP 24 [erts-12.3.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit:no-native-stack]

Interactive Elixir (1.13.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(react_chat@ip-10-0-1-169)1> 

Node.list で接続している(クラスタリングしている)ノードを確認します

iex(react_chat@ip-10-0-1-169)1> Node.list
[]

空配列が返ってきました

何も設定していないので当然です

というわけで、クラスタリングしましょう

クラスタリング方法

ECS 上でクラスタリングするので、ちょっと特殊な設定をする必要があります

実装済のものがこちらになります

参考記事:

libcluster の導入

libcluster は、設定さえしておけば自動的にクラスタリングしてくれる優秀なライブラリです

react_chat/mix.exs の依存パッケージに libcluster を追加してください

  defp deps do
    [
      ...
      {:libcluster, "~> 3.3"}
    ]
  end

クラスタリング設定

react_chat/config/prod.exs に以下の設定を加えます

config :libcluster,
  topologies: [
    dns_poll_example: [
      strategy: Elixir.Cluster.Strategy.DNSPoll,
      config: [
        polling_interval: 5_000,
        query: "lb-svc.test.react-chat.local",
        node_basename: "react_chat"
      ]
    ]
  ]

strategy でどうやってクラスタリング先を特定するのか、を指定します

Elixir.Cluster.Strategy.DNSPoll の場合、 DNS の名前解決を利用します

query で指定している "lb-svc.test.react-chat.local" が肝です

これは "<サービス名>.<環境名>.<アプリケーション名>.local" になっています

このドメインは Copilot が自動的にプライベートDNSに登録した値です

スクリーンショット 2022-06-01 11.54.07.png

libcluster は DNS(Route53)に lb-svc.test.react-chat.local の名前解決をリクエストします

結果として、各コンテナの IP アドレスが全て取得できます

libcluster は node_basename に指定した値と各IPアドレスを使って、ノードに接続しに行きます

この例だと、以下の2つのノードをクラスタリングすることになります

  • react_chat@10.0.1.169
  • react_chat@10.0.0.117

ただし、デフォルトのリリースだと、
ノード名が react_chat@ip-10-0-1-169 という形式になってしまっているので、
次にノード名を変更します

ノード名の変更

ノード名は react_chat/rel/env.sh.eex を以下のように設定することで変更可能です

export PUBLIC_HOSTNAME=`curl ${ECS_CONTAINER_METADATA_URI}/task | jq -r ".Containers[0].Networks[0].IPv4Addresses[0]"`
export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=<%= @release.name %>@${PUBLIC_HOSTNAME}

rel/env.sh.eex は Erlang VM 実行時の環境変数を指定できます

ここで行っていることの意味を解説します

ECS のコンテナ上では ECS_CONTAINER_METADATA_URI という環境変数が設定されています

/app # printenv ECS_CONTAINER_METADATA_URI
http://169.254.170.2/v3/049724e2f64e4197b6b67349b169876b-3196447412

ここの /task にリクエストを投げて、 jq でパースしてみます

/app # curl ${ECS_CONTAINER_METADATA_URI}/task | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1518  100  1518    0     0   236k      0 --:--:-- --:--:-- --:--:--  247k
{
  "Cluster": "arn:aws:ecs:ap-northeast-1:xxx:cluster/react-chat-test-Cluster-fxvr1iv0DCPA",
  "TaskARN": "arn:aws:ecs:ap-northeast-1:xxx:task/react-chat-test-Cluster-fxvr1iv0DCPA/049724e2f64e4197b6b67349b169876b",
  "Family": "react-chat-test-lb-svc",
  "Revision": "45",
  "DesiredStatus": "RUNNING",
  "KnownStatus": "RUNNING",
  "Containers": [
    {
      "DockerId": "049724e2f64e4197b6b67349b169876b-3196447412",
      "Name": "lb-svc",
      "DockerName": "lb-svc",
      "Image": "xxx.dkr.ecr.ap-northeast-1.amazonaws.com/react-chat/lb-svc@sha256:bef4ecf131400c5a00b37aca5ca67aa29c029aa714c3bfb89aa5b76ef81aa8b4",
      "ImageID": "sha256:bef4ecf131400c5a00b37aca5ca67aa29c029aa714c3bfb89aa5b76ef81aa8b4",
      "Labels": {
        "com.amazonaws.ecs.cluster": "arn:aws:ecs:ap-northeast-1:xxx:cluster/react-chat-test-Cluster-fxvr1iv0DCPA",
        "com.amazonaws.ecs.container-name": "lb-svc",
        "com.amazonaws.ecs.task-arn": "arn:aws:ecs:ap-northeast-1:xxx:task/react-chat-test-Cluster-fxvr1iv0DCPA/049724e2f64e4197b6b67349b169876b",
        "com.amazonaws.ecs.task-definition-family": "react-chat-test-lb-svc",
        "com.amazonaws.ecs.task-definition-version": "45"
      },
      "DesiredStatus": "RUNNING",
      "KnownStatus": "RUNNING",
      "Limits": {
        "CPU": 2
      },
      "CreatedAt": "2022-06-01T00:31:34.964143832Z",
      "StartedAt": "2022-06-01T00:31:34.964143832Z",
      "Type": "NORMAL",
      "Networks": [
        {
          "NetworkMode": "awsvpc",
          "IPv4Addresses": [
            "10.0.1.169"
          ]
        }
      ]
    }
  ],
  "Limits": {
    "CPU": 0.25,
    "Memory": 512
  },
  "PullStartedAt": "2022-06-01T00:31:31.890734978Z",
  "PullStoppedAt": "2022-06-01T00:31:33.644375682Z",
  "AvailabilityZone": "ap-northeast-1c"
}

このように、コンテナの情報が取得できます

jq のクエリで .Containers[0].Networks[0].IPv4Addresses[0] としているので、
Containers の先頭の Networks の先頭の IPv4Addresses の先頭、
つまり 10.0.1.169 が取得できます

つまり、このコンテナの IP アドレスです

env.sh.eex を実行すると、
PUBLIC_HOSTNAME10.0.1.169 が設定され、
RELEASE_NODEreact_chat@10.0.1.169 になります

ただし、 hostname -i でも IP アドレスが取得できるので、以下のように指定した方がシンプルかもしれません

react_chat/rel/env.sh.eex

export PUBLIC_HOSTNAME=`hostname -i`
export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=<%= @release.name %>@${PUBLIC_HOSTNAME}

クラスターの監視

react_chat/lib/react_chat/application.ex を以下のように変更します

  @impl true
  def start(_type, _args) do
    topologies = Application.get_env(:libcluster, :topologies) || []

    children = [
      ...
      {Cluster.Supervisor, [topologies, [name: ReactChat.ClusterSupervisor]]}
    ]
    ...
  end

これによって、 Cluster.Supervisor が DNS に定期的にリクエストを送り、
クラスタリング対象の追加、削除を自動的に行ってくれます

ECS 上でコンテナの追加や変更を行えば自動的にクラスターが再構成されるため、
デプロイやオートスケーリング、一部コンテナの停止時でもシステムは止まらずに動き続けます

クラスタリングしている場合の動作

では、クラスタリングした状態のコードをデプロイしてみましょう

改めて、コードはこちら

コードを変更した状態でデプロイコマンドを実行します

copilot svc deploy \
  --name lb-svc \
  --env test

しばらくしてデプロイが完了したら、
表示された URL (前に表示されたものと同じ)にアクセスしてチャットに JOIN してみましょう

今度はいくつタブを開いて JOIN しても、
全てが同じチャットに入ります

スクリーンショット 2022-06-01 13.52.13.png

コンテナがクラスタリングされたことにより、
コンテナ間でメッセージが全て共有されるようになりました

with_clustering.drawio.png

クラスタリングされたことをコンテナ内で確認してみましょう

タスクは全て変更されているので、改めてステータスを表示します

$ copilot svc status                          
Found only one deployed service lb-svc in environment test
Task Summary

  Running            ██████████  2/2 desired tasks are running
  Health             ██████████  2/2 passes HTTP health checks
  Capacity Provider  ▓▓▓▓▓▓▓▓▓▓  2/2 on Fargate Spot

Stopped Tasks

  Reason                          Task Count  Sample Task IDs
  ------                          ----------  ---------------
  Essential container in task ex  2           68bdc0b7,997c30e7
  ited                                        

Tasks

  ID        Status      Revision    Started At      Capacity      HTTP Health
  --        ------      --------    ----------      --------      -----------
  15fd84db  RUNNING     46          28 minutes ago  FARGATE_SPOT  HEALTHY
  8664e620  RUNNING     46          28 minutes ago  FARGATE_SPOT  HEALTHY

表示されたタスクに接続します

$ copilot svc exec --task-id 15fd84db

コンテナ内で Phoenix セッションに接続します

/app # ./bin/react_chat remote

ノードの一覧を取得すると、もう片方のコンテナのノードが表示されました

iex(react_chat@10.0.1.144)1> Node.list
[:"react_chat@10.0.0.149"]

もう一台のコンテナで同じ操作をすると、同じように他方のコンテナと接続できています

iex(react_chat@10.0.0.149)1> Node.list
[:"react_chat@10.0.1.144"]

まとめ

libcluster の Elixir.Cluster.Strategy.DNSPoll を利用することにより、
ECR 内でのクラスタリングが実装できました

これでいつでも台数を増減したり、コンテナを入れ替えたりできます

16
4
0

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
16
4