Elastic BeanstalkのAutoScale環境でWebSocketを使う(Sails.js 0.10.x)


どのAdventCalendarにしようか迷った挙句AWSにしてみた。

非常にややこしいタイトルですが、Sails.js(node.js)をElastic BeanstalkのAutoScale環境に載せた上で、WebSocketを使用する方法です。

以前投稿した記事でSails.js(node.jsのフレームワーク)を使用したリアルタイムWebを実現しましたが、今回はそのサービスを公開できる環境を整えます。要件としては、

  • HTTPSで通信すること
  • AutoScaleの設定をしてELB(ロードバランサ)を設定すること

の2点です。基本的にそんなに難しくないのですが、これがWebSocketと相性が悪く、素直にやるとうまくいきません。

1.Beanstalk環境を作成

まずはElastic Beanstalkの環境を作成。特に難しいことはやってないので作ったことのある人はスルー推奨。画像のない箇所はとりあえずNextで。

アプリ名を設定

動作環境を設定

今回はnode.jsを使います。また、Load balancingの設定を忘れずに。

VPCを設定

インスタンスの設定

サンプルのために財布を痛めたくないのでt2.microを設定。EC2 key pairは自分で作ったやつです。

VPC設定

こんな感じで。

動作確認

Greenになったら上のURLからページが表示されるか確認。Greenになってもしばらく繋がらないこともあるので気長に待ちましょう。

2.HTTPSを適用する

鍵を作る

いわゆるオレオレ証明書を作ります。

$ openssl genrsa -aes128 2048 > server.org
$ openssl rsa -in server.org > server.key
$ openssl req -new -key server.key > server.csr
$ openssl x509 -req -days 365 -in server.csr -signkey server.key > server.crt

Listenerの追加

ロードバランサがListenするサービスを設定します、EC2メニューでロードバランサを選択、ListenersタブでEdit。

HTTPSを追加、証明書を設定するために[SSL Certificate]を選択。

Private Keyに.keyファイルの中身、Publick Key Certificateに.pemファイルの中身をコピペ。今回は自己証明なのでChainは不要。

ELBのSecurity Groupの設定

Listenerの設定だけではダメでELBに設定されたSecurityGroupでもポートを開かないといけない。次はSecurityタブからSecurityGroupを選択。

Security Groupの設定ページに飛ぶので、InboundをEdit。

HTTPSを追加。

動作確認

ここまで設定できていたら動作確認。HTTPでもHTTPSでも繋がるはず。

3.HTTPをHTTPSへリダイレクトする

実際のサービスでは同じページをHTTPとHTTPSの両方で表示するということは稀で、ユーザ情報を扱うサービスならHTTPSは必須。この時単純にHTTPを禁止してしまうと間違ってHTTPでアクセスしたユーザにはページが表示されずに機会損失につながるので、HTTPでアクセスしてきたユーザはHTTPSへリダイレクトしてあげます。

AutoScale環境下での問題点

問題点というほどでもないんですけど、AutoScale環境(ELBを通した環境)での注意点。先ほどELBのListenerを設定しましたが、HTTPSで来た通信をELBでHTTPに変換しています。こうすることでEC2インスタンス個々にSSL証明書を設定する必要がなくなるのですが逆にEC2側からはすべてHTTPに見えるためプロトコルを判定できない。

じゃあどうするかというとELB上でHTTP/HTTPで来た通信のポート番号を変更してEC2に渡してあげる。EC2側ではポート番号からHTTPなのかHTTPSなのかを判定し、HTTPの場合はHTTPSになるようにリダイレクトしてあげます。

ELBのListenerの設定

先ほどのListener設定にて、HTTPアクセスを適当なポート(ここでは12345)にフォワードするように変更。

ELBのSecurity Group設定(Outbound)

ELBから12345ポートでEC2に接続できるよう設定。本気でサービス稼働を考える場合はDestinationにEC2 Security Groupを設定しましょう。

EC2のSecurity Group設定

EC2に設定されているSecurity Groupを探し...

先ほどELCで設定したポートのInboundを許可する。

ここまででAWS Console上での設定は完了。

nginxの設定

残りはEC2インスタンス側での設定。nginxの設定でリダイレクトします。今回はSSHでインスタンスに直接つなぎに行って設定していますが、これだとAutoScaleでインスタンスが再生成されたときに対応できない。本当は.ebextensionsに書かないと行けないのだけれども時間が無いのでいつか追記します...

EC2の以下の設定ファイルを変更。

/etc/nginx/nginx.conf
/etc/nginx/nginx.conf
http {

# Elastic Beanstalk Modification(EB_INCLUDE)
include /etc/nginx/conf.d/*.conf;
# End Modification

    port_in_redirect off;
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;

    keepalive_timeout  65;

    ############################ ここから追加
    server {
      listen 12345;
      server_name chatsample-env.elasticbeanstalk.com;
      rewrite ^(.*) https://chatsample-env.elasticbeanstalk.com$1 permanent;
    }
    ############################ ここまで追加
}

nginxを再起動

$ sudo service nginx restart

これでリダイレクトできます!

4.WebSocketを通す

ここまで設定をして、このアプリケーションをデプロイしてみると分かるのですが、WebSocketが動きません。何故かwebsocketのリクエストが502で返ってきていて、仕方なくpolingしています。まあpolingはうまく出来ているようなのでSocket.IOとしては動いているのですがpolingの限界か、とてもリアルタイムWebと言える代物ではないです。

というわけでこれを解消する方法について。

原因

Bad Gatewayになる主な原因はELB(ロードバランサ)。ちょっと見づらいパケットキャプチャですが、websocketのリクエストのConnectionがになってます。WebSocketのリクエストの場合は本来になっていないといけない。

どうやらELBにHTTP/HTTPS通信が来た場合、無駄な気をきかせてに書き換えるそう。というわけでHTTPSでやったような感じで対策しましょう。

ELBのListener設定

HTTPSと同じようにListenerを設定。HTTP/HTTPSは使えないがWebSocketもセキュアな通信をしたいのでSSL(Secure TCP)を選択。[Load Balancer Port]と[Instance Port]は何でもいいですが、後で使うので覚えておきましょう。今回は3000と11111を使う。

ELBのSecurity Group設定

Inboundに3000を追加

Outboundには11111を追加

EC2のSecurity Group設定

Inboundに1111を追加

nginxの設定

nginxにWebSocket用の設定を追加。

/etc/nginx/nginx.conf
http {

# Elastic Beanstalk Modification(EB_INCLUDE)
include /etc/nginx/conf.d/*.conf;
# End Modification

    port_in_redirect off;
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;

    keepalive_timeout  65;

    server {
      listen 12345;
      server_name chatsample-env.elasticbeanstalk.com;
      rewrite ^(.*) https://chatsample-env.elasticbeanstalk.com$1 permanent;
    }

######################### ここから追加
   server {
     listen 11111;

     location / {
      proxy_pass http://nodejs;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
     }
   }
########################## ここまで追加
}          

アプリケーションの修正

何度も言いますが今回はこのアプリケーションで動作確認をします。今回修正が必要なのは2点。

まずはWebSocketを自動で接続する設定を解除(というかこれ前の記事でもやらないといけない設定だった...)

/assets/js/dependencies/sails.io.js
    // Set a `sails` object that may be used for configuration before the
    // first socket connects (i.e. to prevent auto-connect)
    io.sails = {

      // Whether to automatically connect a socket and save it as `io.socket`.
      autoConnect: false,

そして自作のソケット接続箇所でURLを指定。この時にWebSocket用にELBに設定したポート(3000)を指定。

/assets/js/main.js
(function() {

  // Socket.IOに接続
  var socket = window.io.connect('https://' + location.host + ':3000');

これをデプロイしたら完了です!

動作確認

ウィンドウを2つ開いて動作確認します。ネットワークコンソールを見てみるとWebSocketのリクエストが通ってる!

そしてしっかりとリアルタイム通信できました!

残タスク

とりあえずWebSocketの接続確立までは出来ました。ただし、このままではAutoScaleで複数インスタンス立ち上がった時に、別インスタンスにアクセスしてしまうと動きません。

サービスがスケールするまではこの方法で複数インスタンスを一つのmongoインスタンスに接続し、セッションを一括管理する感じでも大丈夫です。