Amazon LightsailにRails環境を構築する

Amazon Lightsail自体はリリース当初に良さそうだと思っていたのですが、完全に忘れており、今更ながらAmazon Lightsailデビューしました。

Amazon LightsailはAWSが提供するVPSサービスです。
私はEC2, EKS, RDS, Elasticache, Route53といったAWSのサービスを中心にインフラを構築することが多いのですが、予算の少ないプロジェクトのステージング環境や規模の小さなシステムのインフラはConoHa VPSを利用していました。
ConoHa VPSを利用していた理由は一般的なVPSと同等の価格でロードバランサー、セキュリティグループ、ローカルネットワーク、オブジェクトストレージ等が利用でき、小さいながらもそれなりの環境を安くで作りやすかったためです。

とはいえConoHa VPSに不満がなかった訳ではありません。
ロードバランサーはL4相当なのでACMのようなSSL証明書の自動更新はできず、Webサーバ側でLet’s Encryptで証明書を作成する必要がありましたし、セキュリティグループはカスタムしたいとなるとWeb UIで操作できないため、cURLで設定していましたし、オブジェクトストレージも同様でcURLで操作していました。(価格を考えれば求めすぎ感はありますが)

Amazon Lightsailは通常のVPSとしての機能に加えてコンテナ、マネージドデータベース(MySQL Community Editionベースなのが気になりますがバックアップが楽)、ロードバランサー(SSL証明書自動更新可)、CDNといったサービスが提供されています。
ロードバランサーでWebサーバを冗長化してマネージドデータベースを高可用性プランで作成すれば、キャッシュやセッション管理にElasticache(Multi AZ)とリソース配信にS3、CloudFrontだけ利用すればかなり安価にSPOFの無いシステムが作れます。
今回はとあるサービスのステージング環境として導入しましたが、今後も利用機会がありそうなので環境構築のメモです。

Webサーバ構築

最初にAmazon LightsailでホームのインスタンスからWebサーバを作成します。
今回はコンテナは利用しません。

  1. リージョンは「東京(ap-northeast-1)」を選択して適宜AZを変更
  2. インスタンスイメージはOSのみの「Amazon Linux 2」を選択
  3. SSHキーペアを作成、ダウンロードして選択
  4. インスタンスプランで適切なサイズのインスタンスを選択
  5. リソース名を適当に付けてインスタンスを作成

インスタンスが出来たらsshでログインしてnginxをインストールします。

$ ssh -i ~/.ssh/xxxx.cer -l ec2-user xx.xx.xx.xx
$ sudo su -
# amazon-linux-extras install -y nginx1

nginxのサービスを起動します。

# systemctl start nginx

ブラウザでPublic IPにアクセスしてnginxのデフォルトページを確認します。
必要であればホーム -> ネットワーキングから静的IPを作成してインスタンスにアタッチしておきます。

ロードバランサー追加

ホーム -> ネットワーキングからロードバランサーの作成。

  1. リージョンは「東京(ap-northeast-1)」を選択
  2. リソース名をつけてロードバランサーの作成
  3. ターゲットインスタンスで先程作成したインスタンスをアタッチ

DNSゾーンの作成

ホーム -> ネットワーキングからDNSゾーンの作成。
本番と同一の場合はDNSゾーンは作成せずに、ロードバランサーのDNS名でCNAMEのレコードを作成する。

  1. ドメインを入力してDNSゾーンの作成
  2. ネームサーバをLightsail指定の値に変更
  3. Aレコードで解決先に上記で作成したロードバランサーを指定して作成

暫く待ってから設定したドメイン名でWebサーバへのアクセスを確認する。

SSL証明書の設定

ホーム -> ネットワーキングから追加したロードバランサーの詳細へ。

  1. インバウンドトラフィックを開いて証明書の作成
  2. ドメインやリソース名を入力して作成
  3. 表示されたCNAMEをDNSに登録(登録後は認証まで暫く待つ)
  4. HTTPSで作成した証明書を選択

再度暫く待ち、HTTPSでアクセスしてSSL証明書を確認する。

マネージドデータベースの作成

ホーム -> データベースからマネージドデータベースの作成。

  1. リージョンは「東京(ap-northeast-1)」を選択して適宜AZを変更
  2. データベースに「MySQL」で適切なバージョンを選択
  3. データベースプラン(標準・高可用性)を選択
  4. インスタンスタイプを選択
  5. リソース名を入力してデータベースの作成

キャッシュサーバの追加

ElasticacheはLightsailのインスタンスに比べて高価なのでSPOFを許容できる環境の場合はRedisサーバ用のインスタンスをWebサーバと同様の手順で作成する。(Redisが不要な場合は作成する必要ありません)
インスタンスを作成したらインスタンス詳細から6379ポートを開放し、sshでログインしてRedisをインストールします。(epelもamazon-linux-extrasも古いので公式から落としてきます)

$ ssh -i ~/.ssh/xxxx.cer -l ec2-user xx.xx.xx.xx
$ sudo su -
# yum install -y clang jemalloc
# cd /usr/local/src/
# wget -c https://download.redis.io/releases/redis-6.2.1.tar.gz
# tar zxvf redis-6.2.1.tar.gz
# cd redis-6.2.1
# CC=clang make && make install

makeに失敗して再実行する際はmake distcleanしてから再実行します。
インストール完了したら設定していきます。

# mkdir -p /usr/local/redis/dump
# cp -pi redis.conf /usr/local/redis/.
# vi /usr/local/redis/redis.conf

設定は環境にあわせて行う。

75c75,76
< bind 127.0.0.1 -::1
---
> # bind 127.0.0.1 -::1
> bind * -::*
279c280
< pidfile /var/run/redis_6379.pid
---
> pidfile /var/run/redis.pid
444c445
< dir ./
---
> dir /usr/local/redis/dump

カーネルパラメータの調整

# sysctl vm.overcommit_memory=1
# sysctl net.core.somaxconn=1024

sysctl.confにも書いておく

vm.overcommit_memory = 1  # 追加
net.core.somaxconn = 1024 # 追加

サービスユニットの追加

[Unit]
Description=redis

[Service]
Type=simple
ExecStart=/usr/local/bin/redis-server /usr/local/redis/redis.conf
ExecStop=/usr/local/bin/redis-cli shutdown
User=redis
Group=redis
SyslogIdentifier=redis

[Install]
WantedBy=multi-user.target

Redisを起動する

# systemctl daemon-reload
# systemctl start redis
# systemctl enable redis

WebサーバにRailsの環境を整える

WebサーバにRubyをインストールします。

# yum install git gcc gcc-c++ readline-devel openssl-devel libyaml-devel zlib-devel
# cd /usr/local/src/
# wget -c https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.2.tar.gz
# tar zxvf ruby-2.7.2.tar.gz
# cd ruby-2.7.2
# ./configure
# make && make install
# gem update --system
# gem update bundler

次にnodejsをインストール。

# curl -fsSL https://rpm.nodesource.com/setup_14.x | bash -
# yum install -y nodejs
# npm install -g yarn

MySQL Clientインストール(Percona-Server-client)

# yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm
# yum install Percona-Server-client-57 Percona-Server-devel-57

Railsアプリ毎に必要な起動準備をする

  • Railsアプリの配置
  • データベースのユーザ・パスワード・エンドポイントなどを作成したマネージドデータベースの情報に変更
  • Redisのアクセス情報を上記で作成したインスタンスのPrivate IPに変更
  • master.keyの配置
$ cd /path/to/rails_app
$ bundle install --deployment --without development test
$ yarn install
$ RAILS_ENV=staging bundle exec rails db:create db:migrate db:seed
$ RAILS_ENV=staging bundle exec rails assets:precompile

Pumaのサービスユニットを作成

[Unit]
Description=puma
After=network-online.target

[Service]
Type=simple
Environment=RAILS_ENV=staging
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WorkingDirectory=/var/www/app
User=root
Group=root
UMask=0002
PIDFile=/var/www/app/tmp/pids/puma.pid
ExecStart=/usr/local/bin/bundle exec puma -C /var/www/app/config/puma.rb -e staging
ExecReload=/usr/local/bin/bundle exec pumactl -S /var/www/app/tmp/pids/puma.state -F /var/www/app/config/puma.rb phased-restart
ExecStop=/usr/local/bin/bundle exec pumactl -S /var/www/app/tmp/pids/puma.state -F /var/www/app/config/puma.rb stop
RestartSec=15
Restart=on-failure
SyslogIdentifier=puma

[Install]
WantedBy=multi-user.target

Pumaを起動する。

# systemctl daemon-reload
# systemctl start puma

nginxの設定変更

user nginx;
worker_processes auto;
worker_rlimit_nofile 8192;

error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    multi_accept off;
    worker_connections 1024;
}

http {
    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"';
    log_format ltsv "time:$time_iso8601"
                    "\thost:$remote_addr"
                    "\txff:$http_x_forwarded_for"
                    "\tmethod:$request_method"
                    "\tpath:$request_uri"
                    "\tstatus:$status"
                    "\tua:$http_user_agent"
                    "\treq_size:$request_length"
                    "\treq_time:$request_time"
                    "\tres_size:$bytes_sent"
                    "\tbody_size:$body_bytes_sent"
                    "\tapp_time:$upstream_response_time";

    sendfile on;
    server_tokens off;
    keepalive_timeout 10;
    index index.html index.htm;
    error_page 500 502 503 504 /50x.html;

    include /etc/nginx/conf.d/*.conf;
}
upstream backend-puma {
  server unix:/var/www/app/tmp/sockets/puma.sock;
}

server {
  listen 80 default_server;
  server_name example.com;
  root /var/www/app/public;
  client_max_body_size 30m;

  access_log /var/log/nginx/app/access.log ltsv;
  error_log /var/log/nginx/app/error.log info;

  location / {
    try_files $uri @proxy;
  }

  location /health {
    try_files $uri @proxy;
  }

  location @proxy {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://backend-puma;
  }
}

nginx再起動

# mkdir /var/log/nginx/app
# systemctl restart nginx