Skip to main content

デプロイ(Rails)

ベータ版のリリースはほとんど本番と同じ様にリリースする。 実際のベータ版と本番の使い分けはプロジェクト毎に異なるだろう。 大規模なサービスであるほど、本番にリリースする前にベータ版にリリースを行い、動作確認を行ってから本番環境にリリースをすることが多い。 しかし、ベータ版と本番の両方の環境を維持するということは、運用費用が2倍になることとと同じなので、実装している技術によるだろう。 例えば、AWSではほとんど運用費用がかからないLamda + Dynamodbの構成では2つの環境を維持することは容易いが、EC2 + RDBの場合は運用費が嵩んでしまう。

このドキュメントではRails + Next.jsの構成になるわけで、ベータ版環境と本番環境2つを同時に維持するのは難しい。 そのため、このドキュメントでは継続的なベータ版環境とプロダクション環境の運用を紹介するのではなく、 1度ベータ版で本番環境同様にリリースを行い、それを確認したら本番をリリースする方法を紹介する。

次のモジュールで本番環境の構築が完成したら、運用費用がかかるベータ版環境は破棄して構わない。

EC2サーバを作成

Ruby on Railsをベータ版環境で動かすにはEC2が必要である。 EC2はAWSのサービスの1つで、仮想サーバーを構築できるものである。サーバーのOSはLinuxからWindowsまで幅広く用意されている。 EC2はAWS環境でうまく動くようにチューニングされたAmazonLinuxというディストリビューションがあり、特に理由がない限りはこれを利用するのがベターだ。

早速、AWSコンソールにログインし、EC2マネジメントコンソールに移動して、サーバを立ててみる。 「インスタンスを起動」を押す。

ec2 console

サーバの名前を決める。ベータ版用のサーバなので「プロジェクト名-beta」とかにしておく。

ec2-name

「アプリケーションおよび OS イメージ 」の選択は上でも指摘したように、Amazon Linuxが良い。 最新のAmazonLinuxは「Amazon Linux 2 AMI」になっている。アーキテクチャは64ビットのx86にしておく。

amazon linux

「インスタンスタイプ」はデフォルトで「t2.micro」である。これはサーバーのメモリや処理能力の強さを選べる。 Ruby on Railsはこのt2.microで十分稼働するレベルである。これよりも運用費用が安いt2.nanoというのがあるが、 こちらは試していないけど、メモリが半分なので厳しそうではある。

t2.microのまま次に移動する。

instance

キーペア (ログイン)は初回の場合はsshする鍵を作成する。もしも、1度でも作ってあれば、「選択」のフィールドから選ぶ。 初回の場合は右の「新しいキーペアの作成」を押す。

キー作成ウィンドウが現れる。キーペアの名前は今後全てのec2サーバで利用するのでわかりやすく「ec2-ssh-key」とかにしておく。それ以外の項目はデフォルトで良い。「キーペアを作成」ボタンをおす。

new key

pemファイルがダウンロードされる。鍵を使ったsshは実際にec2サーバが起動したら紹介する。

コンソール画面では作成したキーを選んでおく。

select key

「Network settings」はec2の諸々のアクセスを制御の項目である。 サーバーはインターネットに接続しているため、もちろん設定によっては外部からもアクセスが可能になる。 Webページをインターネットで配信しようと思えばポート80番を世界に向けて公開するわけだる。 また、サーバにアクセスするときのsshはポート22番を開ける必要がある。

もちろん、無防備に開けてしまうと、ハッカーの攻撃対象になりうるので、そういったアクセス制御を行うのがこの項目である。EC2はデフォルトでは全てのアクセスを遮断している。この今の画面では新しくそういったアクセス制御をするグループを作成し、sshを許可しようとしている。

すでに、過去に1度でもsshのグループを作ったことがあれば、「既存のセキュリティグループを選択する」にチェックを入れて選ぶと良い。

そうでない場合は、「セキュリティグループを作成する」にチェックを入れておき、「からの SSH トラフィックを許可する」にチェックを言えれて、プルダウンメニューの「自分のIP」を選ぶ。デフォルトでは「任意の場所0.0.0.0/0」になっている。これはネットのどこからでもアクセスが可能という結構危険な設定なので、必ず「自分のIP」にしておく。

「自分のIP」はあなたが今利用しているインターネットの現在のIPからのアクセスを許可するもので、テザリングや家の固定回線であったとしても、コロコロ変わってしまう。なので、ec2が起動した後、sshでサーバにログインできないなと思ったら、大体IPが変わっているので、その都度、sshのセキュリティグループのIPを変更してあげる必要がある。

とりあえず、この様な感じで良い。

network setting

次に、名称を「launch-wizard-1」から「ssh」に変更する。 右上の「編集」ボタンを押し「セキュリティグループ名 」を「ssh」にし、「説明」を「ssh created xxxx-xx-xxxx:xx:xx.xxx」にする。

ssh setup

「ストレージを設定」は8GBのgp2で良い。ec2サーバはRailsのプログラムファイルしかおかないので容量は少なくて良い。 データベースはRDSという別のサービスを利用するし、アップロードされる画像はS3におかれる。

storage

「高度な詳細」はいじる必要はない。 「概要」を確認して、「インスタンスを起動」 をおす。

start

インスタンスの一覧に戻り、しばらく待っていると、ステータスが「実行中」になる。これでいつでもec2を利用できる。

running

無事サーバが起動した。

EC2サーバの停止と起動

info

EC2の課金モデルは実行中の時間に応じて発生する。利用していないときは止める。

止め方は、「実行中」のインスタンスを選択し、「インスタンスを停止」を選ぶ。

stop

danger

プルダウンメニューの「インスタンスを終了」はec2サーバを削除するボタンなので絶対に押してはならない。 筆者も間違えて消してしまったことがある。 英語では「terminate」なんだけど日本語では「終了」と翻訳がバカ。

起動は、同じように、チェックを入れて、メニューから「インスタンスを起動」を押す

launch

EC2は起動すると、IPv4のアドレスが割り振られるが、これは固定ではない。サーバを再起動するたびに、空いているランダムなアドレスが割り振られる。なのでちょっとsshするときにアドレス指定が不便ではある。

ip

ec2へsshログイン

EC2作成時にダウンロードしたec2-ssh-key.pemがダウンロードフォルダにあると思う。 それをMac PCのホームディレクトリの.sshフォルダに移動させる。 もしも、.sshフォルダがない場合は、mkdir ~/.sshで作成する。

mv ~/Downloads/ec2-ssh-key.pem ~/.ssh

sshキーの権限を変更する。

chmod 400 ~/.ssh/ec2-ssh-key.pem

ec2のパブリックIPアドレスを確認する。 awsコンソールのec2のページで目的のec2を選択すると、下のところにIPアドレスが表示される。 表示されるIPアドレスは2種類あり、パブリックとプライベートがあるので、間違えないようにする。

なお、パブリックIPアドレスはec2サーバを再起動するたびに自動的に別のアドレスに割り振られるので注意する。

ipaddress

パブリックIPアドレスをコピーしたらターミナルに戻り、下のコマンドでサーバーにsshする。

ssh -i ~/.ssh/ec2-ssh-key.pem ec2-user@ここにコピーしたIPアドレス

初めてのIPアドレスにアクセスするときは、同意が求められる。 yesとタイプして、エンターをおす。

frist-ssh

問題なくログインできるとこのような画面になる。

sshed

もしも、いつまで経ってもログインできなかったり、コネクションが拒否された場合は大体2つの理由がある。

1つは上でも指摘したように、パブリックIPアドレスが間違っている場合がある。AWSコーンソールで確認をする。

もう1つはあなたのパソコンのIPアドレスが変わった場合である。 作成したセキュリティグループのsshはアクセス元のIPを厳格に指定しているので、少しでも変わればアクセスできない。

アクセス元のIPアドレスを変更する場合は、立ち上げたec2を選択し、セキュリティタブを選ぶ。 その中のセキュリティグループのsshを選ぶ。

select group

移動さきの、「インバウンドルールの編集」ボタンをおす。

edit-inbound

「カスタム」を押して、マイIPを選択、「ルールを保存」を押す。

change-ip

再び、sshコマンドを実行すれば、アクセスできる。

Linuxサーバのセットアップ

サーバにアクセスできたので、Railsサーバの構築をしていく。

最初にサーバの状態を最新にする。

sudo yum update -y

そしたら、最低限必要なコマンドをインストールする。

gccはrbenvのインストールの時に使い、gitは諸々のパッケージのインストールの時に使い、mysql-develはrailsのmysql2 gemで利用する。

sudo yum -y install gcc git

ruby

rubyのインストールは普通はyum経由で入れるのだが、amazon linux2のyumで利用できるrubyのバージョンが3.0.4と古いため、 Macで行ったのと同じように、rbenvを入れて、3.1.1を入れる。

curlでインストーラーを持ってきて、実行する。

curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash

rbenvへのパスを通す。

echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile

rbenv の初期設定をする。

~/.rbenv/bin/rbenv init

sshログイン時にrbenvをinitする。

echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

.bash_profileを読み込めば、rbenvコマンドが利用できる。

source ~/.bash_profile
rbenv install --list

ruby3.1.1をインストールする。

rbenv install 3.1.1

3.1.1をグローバルに利用する。

rbenv global 3.1.1

rbenvを再読み込みし、versionを確認。

rbenv rehash
ruby --version

こんな感じのログが出たらよし。

ruby 3.1.1p18 (2022-02-18 revision 53f5fc4236) [x86_64-linux]

mysql

EC2では直接Mysqlサーバを建てるわけではないが、コマンドが利用できるようにはしておきたい。 しかし、mysqlはいくつかの問題を抱えており、利用は簡単ではない。

mysqlはライセンス問題が発生してからその後継としてMariadbが標準となっている。 そのため、Amazon Linux2では最初からMariadbの利用devel系がインストールされている。

yumでもmysqlはすでにサポート外になっており、手動でパッケージを加えてインストールする必要がある。 さらに、普通にインストールするとmysqlサーバまでインストールしてしまうので、そうならないような手順が必要になる。

まずは、インストールされているmariadb関連のパッケージを表示する。

yum list installed | grep mariadb

mariadb-devel.x86_64 1:5.5.68-1.amzn2 @amzn2-core
mariadb-libs.x86_64 1:5.5.68-1.amzn2 installed

競合する可能性があるので、すでにインストールされているmariadbを取り除く。

sudo yum remove mariadb-devel mariadb-libs 

mysql8.0のリポジトリを追加する。

sudo yum localinstall -y https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm

mysql80-community-release-el7-3.noarchの中にmysql5.7も入っている。 mysql5.7のリポジトリを無効化し、mysql8.0のリポジトリを有効化する。

sudo yum-config-manager --disable mysql57-community
sudo yum-config-manager --enable mysql80-community

MySQLのGPGキーの有効期限切れのためインストールに失敗するので新しい鍵をインポートする。

sudo rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022

mysql-community-clientをインストールする。 これだとサーバはインストールされない。あと、gemで使うmysql-develもインストールする。

sudo yum install -y mysql-community-client mysql-devel

nginx

nginxはwebサーバである。Railsはアプリケーションサーバであるが、web向けには作られていない。そのため、一度インターネットからのアクセスはnginxが受けて、 それをプロキシさせて、Railsサーバーにつなぐ。こうすることでセキュリティ的にも強くなり、webのログなども取りやすくなる。

このコマンドからnginxをインストールする。

sudo amazon-linux-extras install -y nginx1

これえEC2サーバに必要なアプリケーションがインストールされた。 ひとまず、これでec2は置いておいて、次はRDSを作成する。

RDSサーバの作成

RDSはデータベースサーバー専用のサーバを構築できるサービスである。 RDSでMysqlサーバーを立てるメリットはいくつもあり、1つは料金がec2で立てるよりも割安であること。 また、linuxの不具合からサーバーがクラッシュするなどのリスクを回避できたり、 スナップショットと言って、一定時間でデータベースを丸ごとバックアップする機能があったりと、 データを守ることに特化しており、AWSでMysqlを利用するなら絶対にRDSで建てた方が良い。

info

RDSはMysqlデーターベースサーバーを作成するので、サービス毎に1つのRDSを立てる必要はない。 1度RDSを作ってしまえば、その中ではMysqlが動いており、そこにいくつもデーターベースを作成できる。 複数のサービスを作る場合も同一のRDS内のMysqlにデータベースを作れば良い。 つまり、よほど大規模でデータベースサーバー自体を切り分ける必要がない限りは、この工程は1度だけで良い。

AWSコンソールのRDS画面に移動する。「データベースの作成」を押す。

rds-console

上から、「データベース作成方法を選択」はデフォルトの「標準作成」にしておく。

「エンジンのオプション」

「Mysql」を選ぶ。

database

「テンプレート」

テンプレートは料金プランに影響するので、かなり重要である。 選択肢は実質「本番稼働用」と「開発/テスト」の2つである。 それぞれチェックを入れ替えると、その下の項目の構成が自動的に変化する。

caution

対外向けのサービスは「本番稼働用」を選んだほうが良い気がするが、 料金がバカ高くなるので必ず「開発/テスト」を選ぶ。

構成による価格の変化はこちらの記事が言及しているので参照。

https://zenn.dev/yuta28/articles/rds-strage-attention

どのようなサービスを運用していくとしても、基本的には最小構成最小コストで作ってゆき、 規模が大きくなったら構成を拡大していく方がお得である。

template

「可用性と耐久性」

これは、サーバーを物理的に異なる地域にまたがって、編成することで、地震や停電などにある程度スケールして対応できるようにするかどうかである。 どんなサービスも最初は最小構成で良いので、「単一の DB インスタンス」を選ぶ。

solo

「設定」

ここではデータベースの名前やログインユーザーの名前、パスワードを設定する。

「DB インスタンス識別子」はサービス名とかにするのではなく、汎用的な名前が良いだろう。

「マスターユーザー名」はある程度複雑にしておくとよい。

「パスワード」は自分で指定することもできるが、どうせならAmazonに作ってもらった方が強固だと思うので「パスワードの自動生成」にチェックを入れておく。

database-info

「インスタンスの設定」

ここでは、データベースサーバーのスペックを決める。 膨大な情報を捌けるサーバーから小規模なスペックまで幅広く選べる。

RDSは構成を豪華にすればするほど時間当たりの費用がかかる。 スタートアップでサービスを出す場合は最小構成で良い。 もしも、ユーザーが殺到して、データベースの処理がパンクしたら、後からスペックをあげることもできる。

「バースト可能クラス」にチェックを入れて、「db.t3.micro」を選ぶ。

instance type

このサイトでは、ざっくり費用を計算でき、上のような構成だと、月々2600円程度である。

https://aws-rough.cc/rds/

rds cost

「ストレージ」

「ストレージタイプ」は最も費用が安い汎用SSDにする。

「ストレージ割り当て」は最小の5GBで良い。データベースに収める情報は文字情報のみなので、1GBすら相当な文字数収めないと達成しないので、最小で良い。

それ以外はデフォルトのままで良い。

storage type

「接続」

接続はRDSサーバにアクセス可能な範囲を定義する。

まず、「Virtual Private Cloud 」を選ぶ。これはAWSクラウド内に仮想的な範囲を作り、その範囲が適応されていないものからのアクセスを遮断する機能である。 クラウドだからといって、あなたの立ち上げた全てのEC2がRDSにアクセスできてしまうのは万が一のことを考えると危険である。 なので、AWSではアクセスできる範囲をVPCで指定する。

VPCは作っても良いのだが、default VPCというのがあらかじめ存在していると思うので、それを利用する。

「パブリックアクセス情報」は必ず「なし」を選ぶ。そうで無い場合、インターネットからのアクセスがありうる設定になる。

「既存の VPC セキュリティグループ」ではdefaultが選ばれているようにする。

connection

「データベース認証」

これは「パスワード認証」で良い。

「概算月間コスト」

ここでは、現在の構成で1ヶ月運用した時の概算が出ている。 私の場合はこんな風に表示された。

インスタンス   ストレージ   合計
18.98 USD 0.69 USD 19.67 USD

最後に「データベースの作成」ボタンを押す。

ボタンを押すと、こんな感じのメッセージが表示される。

rds auth

danger

パスワードは自動生成にしたので、上の青いバーナーの「認証情報の詳細を表示」からパスワードを確認する必要がある。 この情報はこの時にしか表示できないので、必ずコピーして保存しておく。

auth info

パスワードが保存できたら、しばらく待ってステータスが「バックアップ中」 から「利用可能」になったら立ち上げ成功。

backup

RDSの停止

このドキュメントはベータ版に向けて書いているので、RDSは24時間起動しておく必要はない。

もしも、利用していない場合は止めた方が利用金額は小さくできる。

RDSコンソールで目的のインスタンスをチェックして、「アクション」 から「停止」を選ぶ。

stop rds

スナップショットの作成の有無を聞かれる。 スナップショットとはRDSサーバーの丸々コピーをバックアップするかという意味である。 単純に止めるだけが目的なら必要はないので、なしを選ぶ。

snap

danger

上のアラートにもあるように、RDSの仕様上止められる日数は最大で7日間だけである。 なので、1週間後には自動的に起動して、課金が発生するので注意する。 もしも、長期で利用しない場合は、バックアップをとって、RDSを削除するなど手間がかかる。

VPCの設定とアクセス

Mysqlサーバー作成時にdefaultのVPCを設定した。 これは上でも説明したように、awsクラウド上のどの管轄からアクセス可能にするかを区分するものである。 同じVPCに属しているインスタンスはアクセスが可能なようにできる。

VPCをインスタンスに割り当てる時は、VPCを直接割り振るのではなく、 セキュリティグループに割り合ってて、それをインスタンスに割り当てる。

上で利用しているVPCは「default VPC」という名称でセキュリティグループの「default」に割り振られている。 確認するにはec2コンソールのセキュリティーグループに移動する。

default

RDSに割り振られたdefault VPCをec2に割り振るにはこの「default」セキュリティーグループを割り振れば良い。

ec2一覧からインスタンスを選び、「アクション」から「セキュリティ」、「セキュリティーグループを変更」を選ぶ。

select security

検索欄みたいなところを、クリックすると、候補が出てくるので、defaultを選ぶ。 選んだら、横の「セキュリティーグループを追加」をクリックする。

add

下の「保存」ボタンを押して割り当てが完了する。

こうすることで、RDSとこのEC2インスタンスは同一のdefault VPCに配置され、ec2からRDSのmysqlサーバーにアクセスができる。

ec2にsshログインして、mysqlへのログインを試みる。

rdsのエンドポイントを知る必要があるので、webのRDSコンソールに移動して、対象のrdsをクリックし、詳細画面でエンドポイントを確認する。

rds endpoint

mysqlコマンドでアクセスする。

mysql -u rds作成した名前 -p -h rdsエンドポイント

login

unicorn

Railsサーバを立ち上げる時、rails6ではpumaが使われている。 かつてはwebrickというが使われており、それとは別にpassengerとunicornがある。 どれもRailsのアプリケーションサーバである。

それぞれのアプリケーションサーバには特色があり、詳しい性能比較はしていないが、あまり優劣はないらしい。

本番サーバーでもpumaで運用することも最近はあるらしいが、今までunicornを利用していたので、本番はunicornを使う。

Railsプロジェクトで、Gemfileを開いき、unicornを追加。

unicornはproductionでしか利用しないので、このような括りにしておく。

Gemfile
group :production do
gem "unicorn", "~> 6.1"
end

unicornのファイルを作成し、設定を記述する。

touch config/unicorn.rb

下のコードのapp_nameとなっているのはRailsプロジェクトのフォルダ名に変更する。 例えば、フォルダ名がtest-appならlisten "/usr/share/nginx/test-app/tmp/test-app_unicorn.sock", :backlog => 64にする。

config/unicorn.rb
rails_root = File.expand_path('../..', __FILE__)

worker_processes 1
working_directory rails_root
preload_app true
timeout 30

listen "/usr/share/nginx/app_name/tmp/app_name_unicorn.sock", :backlog => 64
pid "/usr/share/nginx/app_name/tmp/app_name_unicorn.pid"

stderr_path "#{rails_root}/log/unicorn.stderr.log"
stdout_path "#{rails_root}/log/unicorn.stdout.log"

before_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.pid
begin
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
Process.kill(sig, File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
sleep 1
end

unicornの実行ファイルを配置する。

touch lib/tasks/unicorn.rake

以下を記述する。 同じように、app_nameはrailsのフォルダ名に変更する。

/lib/tasks/unicorn.rake
# Unicorn signal handling see: http://unicorn.bogomips.org/SIGNALS.html
namespace :unicorn do
def unicorn_kill(signal)
pid = File.read("/usr/share/nginx/app_name/tmp/app_name_unicorn.pid").to_i
Process.kill(signal, pid)
rescue Errno::ENOENT, Errno::ESRCH
STDERR.puts "Unicorn doesn't seem to be running"
end

desc 'Start unicorn'
task :start do
sh "bundle exec unicorn -c #{Rails.root}/config/unicorn.rb -E production -D"
end

desc 'Quick shutdown'
task :shutdown do
unicorn_kill :INT
end

desc 'Graceful shutdown'
task :stop do
unicorn_kill :QUIT
end

desc 'Graceful restart (Application code can be loaded)'
task :graceful do
unicorn_kill :USR2
end

desc 'Reopen all logs owned by the master and all workers'
task :reopen do
unicorn_kill :USR1
end

desc 'Increment the number of worker processes by one'
task :increment do
unicorn_kill :TTIN
end

desc 'Decrement the number of worker processes by one'
task :decrement do
unicorn_kill :TTOU
end
end

コミットする。

git add .
git commit -m "Add unicorn and configuration"

Railsのデプロイ

Railsのデプロイは必要最低限のファイルのみをサーバに送る方法をとる。 かつてはPythonのFabricというlinuxコマンドのラッパーみたいなライブラリを利用していた。 Fabricはpython2までの対応で、レガシーとなってしまい、残念ながらサポートを終わっている。

単純にRailsフォルダをec2のlinuxに送るだけならば、linuxコマンドのrsyncで良い。 rsyncを直で叩くことの問題は、例外やsshキーの指定などをその都度打ち込まなくてはならないことだ。 pythonにはrsyncコマンドのラッパーがあり、そちらが便利などでそれを利用する。

python3のpipでsysrsyncをインストール。

pip3 install sysrsync

もう1つサーバ側でコマンドを実行できるparamikoをインストールする。

pip3 install paramiko

次にRailsプロジェクトのルートにdeploy.pyファイルを作成する。

Railsルート
touch deploy.py

以下を書き込む。

import sys
import sysrsync
import paramiko
from os import environ


IP_ADDRESS = environ['IP_ADDRESS']
USER_NAME = environ["USER_NAME"]
KEY_FILENAME =environ["KEY_FILENAME"]

sysrsync.run(source='../test-app/',
destination='/usr/share/nginx/test-app/',
destination_ssh=USER_NAME+'@'+IP_ADDRESS,
options=['-a','--rsync-path="sudo rsync"'],
exclusions=['.git','.gitignore','.envrc','.DS_Store','log','tmp','vendor/bundle','public/uploads','README.md', "deploy.py"],
private_key=KEY_FILENAME)

print("Project was synced")

# sshクライアントの作成
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
client.load_system_host_keys()


# 上記で設定したIPアドレス、ユーザー名、キーファイルを渡す
client.connect(IP_ADDRESS,
username=USER_NAME,
key_filename=KEY_FILENAME,
timeout=5.0)

print("SSH was connected")
try:
client.exec_command('sudo chown -R ec2-user:nginx /usr/share/nginx/test-app')
print("Change the rails project owers")

except Exception as ee:
sys.stderr.write(str(ee) + "\n")

# ssh接続断
client.close()
del client
print("Deployed!")

上のコードはIP_ADDRESSUSER_NAMEKEY_FILENAMEの環境変数がある。 Railsプロジェクトルートの.envrcを開いて、値を設定する。

.envrc
export IP_ADDRESS="xxx.xxx.xx.xxx"
export USER_NAME="ec2-user"
export KEY_FILENAME='/Users/Macのホームディレクトリ名/.ssh/ec2-ssh-key.pem'

IP_ADDRESSの値はAWSコンソールのec2ページでインスタンスをチェックし、パブリックIPアドレスを貼り付ける。

check public ip

USER_NAMEは共通のec2-userになる。 KEY_FILENAMEはec2にsshする鍵の場所を絶対パスで指定する。pythonのコードで利用するparamikoは相対パスの解決ができなかったので、必ず絶対パス宣言する。

保存したら、direnv allowを実行して環境変数を読み込んでおく。

準備ができたので、コマンドからデプロイする。

% python3 deploy.py

Project was synced
SSH was connected
Change the rails project owers
Deployed!

このような結果が出たらデプロイ成功である。 もしも、デプロイができない場合は、チェックする項目が2つある。 1つはdirenvのパスやIPアドレスが間違っていたり、direnv allowがされていない可能性がある。 もう1つはAWSのセキュリティグループでSSHのインバウンドIPアドレスが変更されてアクセスできない場合である。 この場合はec2のコンソールからsshセキュリティーグループでマイIPに変更してあげる。

デプロイ先のAmazon Linux 2にはこのような構造の場所にデプロイされる。 この場所はnginxの公開ディレクトリになる。

[ec2-user@ip- ~]$ cd /usr/share/nginx/
[ec2-user@ip- nginx]$ ls
html test-app
[ec2-user@ip- nginx]$ cd test-app/
[ec2-user@ip- test-app]$ ls
Gemfile Gemfile.lock Rakefile app bin config config.ru db lib public vendor

デプロイファイルをコミットする。

git add .
git commit -m "Create a deploy file"

本番環境のセットアップ

まずは、ローカル環境で本番用のデータベースセットアップを行う。 本番でのデータベースの設定が初期設定ではRDSの利用を想定していないので、hostを加える必要がある。 config/database.ymlを開いて、ファイルの一番下にあるproductionリージョンを変更する。

なお、環境変数の名称は機械的に決まっている。このチュートリアルではtest-appというプロジェクト名にしたのでTEST_APPが接頭辞としてつく。 config/database.ymlの中を見れば、命名規則が分かると思うので、それに準じて名前をつける。別にこの名前があってないと動かないというわけではない。

config/database.yml
production:
<<: *default
database: test_app_production
username: <%= ENV['TEST_APP_DATABASE_USER'] %>
password: <%= ENV['TEST_APP_DATABASE_PASSWORD'] %>
host: <%= ENV['TEST_APP_DATABASE_HOST'] %>

コミット。

git add .
git commit -m "Set production env variables"

これで一度、デプロイする。

python3 deploy.py

ここからサーバーに移動してセットアップを行う。

ssh -i ~/.ssh/ec2-ssh-key.pem ec2-user@xxx.xxx.xxx.xxx

データベース用の環境変数を~/.bash_profileに書き込む。

  • TEST_APP_DATABASE_USERはRDS作成時に指定したマスターユーザー名である。
  • TEST_APP_DATABASE_PASSWORDはRDS作成時に生成されたパスワードである。
  • TEST_APP_DATABASE_HOSTはRDSのwebコンソールで確認できるRDSのエンドポイントである。
~/.bash_profile
export TEST_APP_DATABASE_USER="xxxxx"
export TEST_APP_DATABASE_PASSWORD="xxxxxxxx"
export TEST_APP_DATABASE_HOST="xxx.xxx.ap-northeast-1.rds.amazonaws.com"

保存したら、環境変数を読み込む。

source ~/.bash_profile

Railsプロジェクトに移動する。

cd /usr/share/nginx/test-app

gemをインストールする。 この時、本番環境で利用するもののみをインストールする。

bundle install --without test development doc

データベースを作成する。 .bash_profileに書かれた環境変数を元に、config/database.yml が正しく値を読み込み、ec2からrdsにアクセスができれば、 本番用のデーターベースがRDSに作られる。

bundle exec rake db:create RAILS_ENV=production

このメッセージが表示されたらec2からrdsにアクセスができてデータベースが作成されたことになる。

Created database 'xxxxx_production'

そしたら、本番サーバーにマイグレーションを通す。

bundle exec rake db:migrate RAILS_ENV=production

unicornサーバーを起動する。

bundle exec rake unicorn:start

psコマンドでunicornサーバが起動しているかを確認する。

ps aux | grep unicorn

Railsサーバが起動したので、次にnginxのwebサーバを起動させる。

まだ、サーバにはRaisサーバへインターネットからのアクセスをプロキシ(飛ばす)させる設定がないのでそこから作成していく。

nginxの設定ファイルは/etc/nginxの中にある。デフォルトでは/etc/nginx/nginx.confが読み込まれ、そこに記述されている設定でwebサーバが起動する。 デフォルトの設定では/usr/share/nginx/htmlがサーバーのエンドポイントになっている。また、/etc/nginx/nginx.confには/etc/nginx/conf.d以下の.confがいるを読み込む記述がある。 設定を上書きするには/etc/nginx/conf.dの中にRailsようの設定ファイルを配置すれば良い。

sudo touch /etc/nginx/conf.d/rails.conf

下の記述の3箇所のapp_nameはrailsのプロジェと名に適宜変更する。 server_nameの値は本来はS3で獲得したドメインを割り振るが、なんでも動くは動くのでalice.example.com にしている。後で、正しいものに変更する。

/etc/nginx/conf.d/rails.conf
upstream default_unicorn_server {
# This is the socket we configured in unicorn.rb
server unix:/usr/share/nginx/test-app/tmp/app_name_unicorn.sock
fail_timeout=0;
}

server {
listen 80;
client_max_body_size 4G;
server_name alice.example.com;

keepalive_timeout 5;
root /usr/share/nginx/app_name/public;

location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://default_unicorn_server;
break;
}
}


error_page 500 502 503 504 /500.html;
location = /500.html {
root /usr/share/nginx/app_name/public;
}

}

保存したら、nginxサーバを起動する。

sudo systemctl start nginx

RailsサーバとNginx webサーバが起動したので完了まで後少し。 残りは、webサーバポート80番プライベートにアクセスできるようにする。

ポート80のセキュリティグループ

ec2にsshのポート22番にIPアドレスフィルタを加えてアクセスしたように、webサーバー80番にもIPアドレス制限を加えてアクセスできるようにする。

AWSコンソールのec2の画面に移動して、左のメニューから「セキュリティグループ」を選ぶ。画面の「セキュリティーグループを作成」を押す。

sec

-「セキュリティグループ名」は分かりやすくprivate-webにする。

  • 「説明」は適当に「private web 80 port access」

「インバウンドルール」でルールを追加し、HTTPを選び、マイIPを選ぶ。「セキュリティグループを作成」をおす。

new one

完成したら、awsコンソールのec2インスタンスの画面に戻り、起動しているインスタンスを選び、「アクション」=>「セキュリティ」=>「セキュリティグループの変更」を選ぶ。

attach

テキストフィールドをクリックして、作成したprivate-webを選ぶ。横の「セキュリティーグループを追加」ボタンを押す。一番下のオレンジ色の「保存」ボタンを押す。

private-access

これで、あなたのPCからサーバにアクセスできる。試しにこのURLにアクセスしてみる。

http://ec2のパブリックIPアドレス/posts

何もpostsを投稿していないので空のjsonしか返ってこないがちゃんとアクセスできることがわかる。

web

Route53でドメイン割り振り

IPアドレスに直接アクセスするのはテストだけで、ベータではRoute53で取得したドメインにサブドメインbeta-api.を割り当てる。

EC2にRoute53のドメインを割り当てる場合、多くのサイトはElastic IPを取得し、それをEC2に割り当ててから、Route53のドメインを割り当てることをお勧めしている。 Elastic IPとは固定IPアドレスのことである。EC2のパブリックIPアドレスは再起動するたびに自動的に空いているアドレスが割り振られる。 そのため、ベータ版のように節約のため、止めたり起動したりする場合、その都度IPアドレスが変更されてしまう。 Elastic IPはそういった場合に固定のIPをEC2に割り振ることで対処できるが、料金プランに少し難がある。 Elastic IPの料金プランはちょっと特殊で以下の通りである。

  • Elastic IP アドレスが EC2 インスタンスに関連付けられている。
  • Elastic IP アドレスに関連付けられているインスタンスが実行中である。
  • インスタンスには、1 つの Elastic IP アドレスしかアタッチされていない。
  • Elastic IP アドレスが、アタッチされているネットワークインターフェイスに関連付けられている。

すべての Amazon EC2 インスタンスが終了されているにも関わらず、Elastic IP アドレスの料金が請求されているのはなぜですか?

まとめると、固定IPアドレスを取得したけど、割り当てず、サーバも起動していない場合は料金が発生するということである。

これは普段は実行しないベータ版とは相性が悪いので、ベータではElastic IPを取得せずに、Route53のドメインを割り振る。 固定IPでは無いので、EC2を再起動するたびに、Route53で別のIPを指定しなくちゃいけないが、 この次の章で紹介するSSL化でうまくやるので、ここでは動作テストも含めて、愚直に進める。

EC2を起動しておき、ec2のパブリックIPアドレスを控えておく。

Route53のコンソールに移動して、ホステッドゾーンから取得してあるドメインをクリック。

hosted zone

新しくレコードを作成する。

new record

レコード名にはbeta-apiをつけて、「値」はEC2からコピーしたパブリックIPアドレスを割り振る。

入力したら保存を押す。

input form

保存後は、DNSの処理が終わるまで待つ。長くて1分くらいまつ。

その間に、サーバのnginxのrails.confのserver_nameを今割り振ったものに変更する。

/etc/nginx/conf.d/rails.conf
 ...省略...

server_name route53で割り振ったドメイン; // 例) server_name beta-api.example.com;

再起動して

sudo systemctl restart nginx

Route53のドメインが反映されたくらい待ったら、APIサーバのURLを叩くhttp://割り振ったドメイン/posts

空の配列が返ってきたら成功している。

empty return

info

ベータ版でec2を止める場合は、Route53で割り振ったAレコードも削除しておくと良い。 仮に放置して、別のサーバに飛ぶだけだが、事故もあり得るので、削除しておくのがベター。

サーバのSSL化

上のAPIのエンドポイントはhttpである。これは平文でインターネットの中をパッケットが飛ぶことになり、個人情報、パスワードのやりとりを行う場合は利用できない。 そのため、SSLで暗号化を行うことになる。これを行うとURLがhttpsになり、安全な通信が可能になる。

EC2を利用したSSL通信は大きく分けて2つの方法がある。

  1. ACMで取得した証明書をALBに割り振りec2に割り振る
  2. EC2のcertbotで内部でSSLを管理する

1つ目は最もオーソドックスで、AWS上のec2にsslを割り振るならこのやり方になる。 唯一のデメリットはやや割高になることだ。

2つ目は無料のssl照明をamazon linuxの中で自分で取得し割り振る方法である。 最近はcertbotも便利になり管理も簡単だ。

ここでは、2つ目の方法を紹介する。

ec2のコンソールに移動し、新しく世界中からのhttpを許可するセキュリティグループを作成し、ec2に割り振る。

新規セキュリティグループ作成画面で、名前を「http anywhere」とし、インバウンドルールでhttpを選び、ソースでAnywhere-IPV4を選ぶ。

http anywhere

作成して、ec2インスタンスの画面に移動し、稼働しているrailsのec2にチェックを入れて、アクションからセキュリティーグループの変更を選ぶ。

add

http anywhereを加えて、保存ボタンを押す。

groups

サーバーにsshしてcertbotの準備をする。

標準のyumにはcertbotのパッケージがないので、追加する。

sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm

有効化する。

sudo yum-config-manager --enable epel

certbotとpython製のnginxで簡単に認証ができるコマンドをインストールする。

sudo yum install certbot python2-certbot-nginx -y

certbotを利用して認証する。 この時、いくつか確認することがある。 1つ目は/etc/nginx/conf.d/rails.confのファイルにserver_name:に証明書を取得したいドメイン名が記述されているかである。 certbotはここの値を見て、証明書の認証を行うので、ここが適当な値だと、目的の証明書が取得できない。

次に、グローバルな場所からrailsのAPIにアクセスできるかである。上のセキュリティグループの設定で、http anywhereを割り振ったので、 どこからでもアクセスできるようになっていると思うが、一応確認しておく。certbotはインターネットからアクセスして認証を行うので、 ここが塞がっていると認証が通らない。

準備ができたので、 certbotを実行して証明書を取得する メールの登録は必須ではなく、期限切れやセキュリティの警告がくるだけである。 メールは登録すると公開されるので、特に理由がない限りは無登録で良い。

sudo certbot --nginx --register-unsafely-without-email

certbotがnginxのconfigを見て、どのドメインの証明書を取得するか聞かれるので、数字で答える。

Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 1

後は自動的にcertbotが/etc/nginx/conf.d/rails.confを書き換えて、認証を行ってくれる。

早速アクセスしてみたいが、https 443のポートが空いていないので、http anywhereを編集してhttpsを追加する。

http anywhere

そしたらhttpsにアクセスする。

https://beta-api.xxx.com/postsちゃんとjsonが取得できたら完了。

httpのポートも開けておくと、自動的にhttpsにリダイレクトしてくれるので良い。 この環境はベータ版なので、常にhttpsとhttpを世界に開いておく必要はない。 あくまで、証明書の取得の時に開いておけば良いので、それ以外時はec2からこのセキュリティグループを外して、安全にしておいた方が良い。

本番でのメールのセットアップ

httpsでのアクセスが可能になったので、ベータ版での運用前にメールの設定をしておく。 Railsのdevelopment環境下ではいくつか設定を行ったが、本番ではまだ準備をしていない。

SESメールを利用するIAM認証キーは開発環境でも本番環境でも同じキーを利用するので、特に準備をすることはない。

最初にするのは認証メールを送る時に、認証を行うエンドポイントをどこにするかである。

開発環境ではhttp://localhost:3001にしてある。本番ではこれをhttps://beta-api.xxx.comのような形式にする。

/config/environments/production.rbの本番用設定ファイルを開き、mailer周りの設定を書いてある場所に追加する。 hostの値は自分が上で指定したドメイン名にする。

/config/environments/production.rb
config.action_mailer.default_url_options = { host: 'beta-api.xxx.com', protocol: "https" }

もう1つ、メールのメソッドはsesを使うことを明記する。

/config/environments/production.rb
config.action_mailer.delivery_method = :ses

デプロイファイルを書き換えて、継続的にデプロイできる様にする。

今までは、ディレクトリをsyncして、権限を帰るだけだったが、権限を変えた後、bundle installしてunicornをリスタートする。

デバッグのprint("ssh was connected")からtry catchの中を変更する。

deploy.py
print("SSH was connected")

def remote_debug(stdobj):
str_out = ''
for line in stdobj:
str_out += line
print(str_out)

try:
client.exec_command('sudo chown -R ec2-user:nginx /usr/share/nginx/test-app')
print("Change the rails project owers")
stdin, stdout, stderr = client.exec_command('source /home/ec2-user/.bash_profile; cd /usr/share/nginx/test-app; bundle install --path vendor/bundle --without test development doc')
remote_debug(stdout)
remote_debug(stderr)
stdin.close()
print("bundle install gems")

stdin, stdout, stderr = client.exec_command('source /home/ec2-user/.bash_profile; cd /usr/share/nginx/test-app; bundle exec rake unicorn:graceful')
remote_debug(stdout)
remote_debug(stderr)
stdin.close()
print("Restart unicorn server")

except Exception as ee:
sys.stderr.write(str(ee) + "\n")

remote_debug関数を作り、そこにssh先のコマンドで出力される内容とエラー内容を表示する。 paramikoの仕様上この引数受け取りを行わないと、bundle installコマンドの実行みたいに時間がかかるコマンドを待機してくれずエラーが出る。

bundle installのオプションも実行すると必要はないと言われるが、sync時にbundleの設定が削除されているのか本番用にインストールするのではなく、開発環境用にインストールされてしまうので必要。

unicorn:gracefulはunicornのリスタートコマンドで、サーバを止めずにリスタートする。unicornがスタートしていない場合は失敗するので、その場合はsshして手動でstartしてあげる。

デプロイして。

python3 deploy.py

コミットする。

git add .
git commit -m "Setup production email and change the deploy file"

メールの送信と認証がうまくいくか試す。

クライアントがまだリリースできていないので、ローカルの開発環境で行ったように、curlコマンドを利用して認証してみる。

confirm_success_urlはNext.jsが稼働していないので、APIサーバの適当なエンドポイントにしている。

curl -X POST https://beta-api.xxx.com/auth -d '[email]=alice@example.com&[password]=123456&[password_confirmation]=123456&[confirm_success_url]=https://beta-api.xxx.com/posts'

200が返って来れば、メールが送られている。

production-mail

もしも、うまくいかない場合は、問題はさまざまあるだろうが、サーバーのlog/produciton.log でエラーを確認するとよい。

送られてきたメールをクリックして、リダイレクトされればうまく認証系も機能している。

https://beta-api.xxx.com/posts?account_confirmation_success=true