Pub/Subを使ってWebSocketサーバをスケールアウトさせる(クラウド版)

前回の記事の続き。前回はオンプレ(自分が管理するVM)でWebSocketサーバをスケールアウトする方法を書いたが、本稿はクラウド(AWS)のマネージドサービスを使った方法について書く。

記事の内容

  • WebSocketを使ったWebアプリケーションをAWS上に構築する。
  • AMI作成にPackerを利用し、AWS構築にはTerraformを使う。

前提

  • AWSのルートアカウント持っている。
  • インターネットで名前解決できるドメインを持っていて、Route53で管理している。
    • ドメインはトップレベルドメイン(.comや.netとったドメインの末尾)にこだわらなければ、お名前.comムームードメインで安価に購入できる。

システムアーキテクチャ

ロードバランサーにはApplication Load Balancer (ALB) を利用する。
ALBの後段にはアプリケーションサーバのEC2を二つ配置する。ALBでSSL終端するため、ALB-EC2間はHTTP/WebSocketで通信する。
EC2間の情報連携用のPub/SubにはAmazon ElastiCache for Redisを利用する。
認証局により証明されたSSL証明書を発行するためRoute53を使ってDNS認証する。

システムアーキテクチャ

Packer用IAMユーザ作成

IAMグループ作成

  • グループ名に「packer」と記入し、「次のステップ」をクリック
  • 「AmazonEC2FullAccess」ポリシーを選択し、「次のステップ」をクリック
  • 「グループの作成」をクリック

IAMユーザ作成

  • ユーザー名に「packer」と記入し、アクセスの種類「プログラムによるアクセス」を選択し、「次のステップ:アクセス権限」をクリック
  • グループ「packer」を選択し、「次のステップ:タグ」をクリック
  • キー「Name」、値「packer」を記入し、「次のステップ:確認」をクリック
  • 「ユーザーの作成」をクリック
  • 「アクセスキーID」と「シークレットアクセスキー」をメモしておく。

PackerでEC2のマシンイメージ(AMI)作成

Packerインストール

以下は執筆時点のコマンド。最新のインストール手順についてはこちらを参照。

# curl -O https://releases.hashicorp.com/packer/1.4.2/packer_1.4.2_linux_amd64.zip
# unzip packer_1.4.2_linux_amd64.zip
# mv packer /usr/local/sbin/
# packer version
Packer v1.4.2

認証情報

認証情報を環境変数に書き込む。
IAMユーザ作成時にメモしたpackerユーザのアクセスキーIDとシークレットアクセスキーを環境変数に書き込む。

export AWS_ACCESS_KEY_ID=xxxx
export AWS_SECRET_ACCESS_KEY=xxxx

AMI構成情報を作成

以下のjsonファイルを作成する。
DockerとDocker Composeのインストール、ソースコードのダウンロード、Dockerイメージの作成をAMIの中で済ませておく。

{
    "variables": {
        "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
        "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
        "region":         "ap-northeast-1"
    },
    "builders": [
        {
            "access_key": "{{user `aws_access_key`}}",
            "ami_name": "packer-linux-aws-demo-{{timestamp}}",
            "instance_type": "t2.micro",
            "region": "{{user `region`}}",
            "secret_key": "{{user `aws_secret_key`}}",
            "source_ami_filter": {
              "filters": {
                  "name": "amzn2-ami-hvm-*-x86_64-gp2"
              },
              "owners": ["137112412989"],
              "most_recent": true
            },
            "ssh_username": "ec2-user",
            "type": "amazon-ebs"
        }
    ],
    "provisioners": [{
        "type": "shell",
        "inline": [
            "sleep 30",
            "sudo yum -y install docker git",
            "sudo systemctl start docker",
            "sudo systemctl enable docker",
            "sudo curl -L \"https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose",
            "sudo chmod +x /usr/local/bin/docker-compose",
            "git clone https://github.com/tetsis/simple-chat.git",
            "sudo cp -r simple-chat /root/",
            "sudo /usr/local/bin/docker-compose -f /root/simple-chat/docker-compose.yml build"
        ]
    }]
}

packer validate コマンドでjsonファイルの構文チェックをする。

# packer validate app-server.json
Template validated successfully.

Template validated successfully. が表示されればOK。

packer build コマンドでAMIを作成する。
最終行の ami-... をメモしておく。(ここでは xxxx としているが、実際は英数字の文字列)

# packer build app-server.json
...
==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
ap-northeast-1: ami-xxxx

EC2インスタンス用ロールを作成

EC2インスタンス内でawsコマンドを実行するため、ロールを作成する。

  • 「このロールを使用するサービスを選択」で「EC2」を選択し、「次のステップ:アクセス権限」をクリック
  • 「AmazonEC2ReadOnlyAccess」ポリシーを選択し、「次のステップ:タグ」をクリック
  • キー「Name」、値「ec2-role」を記入し、「次のステップ:確認」をクリック
  • ロール名「ec2-role」を記入し、「ロールの作成」をクリック

TerraformでAWSリソース作成

Terraform用IAMグループとIAMユーザ作成

Packer用にIAMグループとIAMユーザを作ったのと同様にTerraform用にも作成する。
作り方はPackerの時と同じなので詳細については省略する。
Packerとの違いはIAMグループにアタッチするポリシーで、今回の例では以下の5つのポリシーをアタッチする。

  • AmazonEC2FullAccess
  • AmazonElastiCacheFullAccess
  • AmazonRoute53FullAccess
  • AWSCertificateManagerFullAccess
  • IAMFullAccess

Terraformインストール

以下は執筆時点のコマンド。最新のインストール手順についてはこちらを参照。

# curl -O https://releases.hashicorp.com/terraform/0.12.5/terraform_0.12.3_linux_amd64.zip
# unzip terraform_0.12.3_linux_amd64.zip
# mv terraform /usr/local/sbin/
# terraform version
Terraform v0.12.3

AWSリソース作成

Terraformの.tfファイルはこちらを参照。
公開したくない情報は環境変数で設定してから terraform plan で正常性を確認して、 terraform apply でリソース作成を実行する。
(環境変数は ~/.bashrc 等に書いておいても良い)

# export TF_VAR_access_key=xxxx
# export TF_VAR_secret_key=xxxx
# export TF_VAR_allow_ip=x.x.x.x/32
# export TF_VAR_ami=ami-xxxx (Packerで作成したAMI)
# export TF_VAR_app_fqdn=simple-chat.xx.xx
# export TF_VAR_zone_id=xxxx (自分がホストしているドメインのZONE ID)
# cd /dir/to/terraform
# ls
aws_acm.tf  aws_alb.tf  aws_ec2.tf  aws_elasticache.tf  aws_r53.tf  aws_sg.tf  aws.tf  aws_tg.tf  aws_variables.tf  aws_vpc.tf terraform.tfvars
# terraform plan
...
Plan: 25 to add, 0 to change, 0 to destroy.
...
# terrafrom apply
...
  Enter a value: yes
...
(リソース作成。しばらく待つ)
Apply complete! Resources: 25 added, 0 changed, 0 destroyed. (「Apply complete!」が表示されたらリソース作成は正常に完了している。)

(参考)EC2初期処理用スクリプト

TerraformのEC2インスタンス作成ファイルで以下のような記述があるが、このuser_dataの作成方法について書いておく。

    user_data = <<EOF
IyE...
EOF

user_dataはEC2インスタンス起動時の処理をシェルスクリプト形式で書いたものをbase64エンコードしたものである。
シェルスクリプトの中身はEC2インスタンスを起動してからでないと決まらないパラメータをタグから取得してアプリケーションの設定ファイルに書き込む、という操作を行っている。
参考のためbase64変換前のシェルスクリプトを記載する。

# !/bin/sh

aws="/usr/bin/aws --region ap-northeast-1"
logger="logger -t $0"

get_instance_id()
{
    instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
}

get_redis_endpoint()
{
    redis_endpoint=$(${aws} ec2 describe-instances \
                  --instance-id ${instance_id} \
                  --query 'Reservations[].Instances[].Tags[?Key==`Redis_endpoint`].Value' \
                  --output text)
}

set_redis_endpoint()
{
    sed -i -e "s/REDIS_HOST.*$/REDIS_HOST: '${redis_endpoint}'/g" docker-compose.yml
}

${logger} "start $0"
sleep 60

${logger} "get info"
get_instance_id
get_redis_endpoint

${logger} "set environment"
cd /root/simple-chat
set_redis_endpoint

${logger} "run app"
docker-compose up -d

${logger} "finished $0"

exit 0

確認

Webブラウザで https://(環境変数TF_VAR_app_fqdnに設定したFQDN) にアクセスする。
simple-chatの画面が表示され、テキストボックスに入力した結果がテキストエリアに表示されたら正常に動作している。

後始末

検証が終わったら忘れずにリソースを削除する。

# terraform destroy
...
  Enter a value: yes
...