AWS環境をイチから構築し、Railsアプリをデプロイすることがゴール
プロジェクト名はテキトーにneon-cyber
とします。(GPTが提案してくれた)
要件
- フレームワークはお馴染みのRuby on Rails(7系)
- WEBサーバー
- Sidekiqのサーバー
- データベースはMySQLの8系
これをAWSで実現するとなる時には以下の構成となる
- EC2インスタンス(AMI用+踏み台サーバー)
- ロードバランサー(CodeDeployでBlue/Greenデプロイをしたいから)
- EC2インスタンス(WEBサーバー)
- EC2インスタンス(Sidekiqのサーバー)
- RDSインスタンス
- Redisインスタンス
と、簡単に言ってもAWSで設定しなければいけないことは多岐に渡るので順番に解説していく。
- VPC
- サブネット
- ルートテーブル
- セキュリティグループ
- インターネットゲートウェイ
- NATゲートウェイ
- エンドポイント(EC2がS3にアクセスしたい場合)
- RDS
- サブネットグループ
- インスタンス
- Redis
- サブネットグループ
- インスタンス
- SystemManager
- パラメーターストア
- IAM
- EC2
- インスタンス作成
- 起動テンプレート
- AMI
- ロードバランサー
- ターゲットグループ
- オートスケーリンググループ
VPC(ヴァーチャル・プライベート・クラウド)
順を追って説明していきます。
VPCの作成
まずは仮想環境を作ります。
名前は「neon-cyber-vpc」とし、IPv4 CIDRは172.33.0.0/16
とします。
サブネット
インターネット環境からだれでもDBインスタンスにアクセスできたりしたら嫌なので基本的に必要なもののみインターネット環境からアクセスできるような構成をとります。
- EC2インスタンス
- RDSインスタンス
- Redisインスタンス
を今回利用するためそれぞれに対してサブネットを作成します
サブネット名 | AZ(アベイラビリティゾーン) | 役割 | IPv4 CIDR |
---|---|---|---|
neon-cyber-ec2-private-subnet-1a | ap-northeast-1a | EC2のプライベートサブネット。ロードバランサーを使用するためもう一つ1cのサブネットも用意してマルチAZ化している | 172.33.1.128/25 |
neon-cyber-ec2-private-subnet-1c | ap-northeast-1c | EC2のプライベートサブネット。ロードバランサーを使用するためもう一つ1aのサブネットも用意してマルチAZ化している | 172.33.4.128/25 |
neon-cyber-ec2-public-subnet-1a | ap-northeast-1a | EC2のパブリックサブネット。ロードバランサーを使用するためもう一つ1cのサブネットも用意してマルチAZ化している | 172.33.1.0/25 |
neon-cyber-ec2-public-subnet-1c | ap-northeast-1c | EC2のパブリックサブネット。ロードバランサーを使用するためもう一つ1aのサブネットも用意してマルチAZ化している | 172.33.4.0/25 |
neon-cyber-rds-private-subnet-1a | ap-northeast-1a | RDSのプライベートサブネット。RDSの要件として最低3つのAZが必要なため | 172.33.2.0/25 |
neon-cyber-rds-private-subnet-1c | ap-northeast-1c | RDSのプライベートサブネット。RDSの要件として最低3つのAZが必要なため | 172.33.3.128/25 |
neon-cyber-rds-private-subnet-1d | ap-northeast-1d | RDSのプライベートサブネット。RDSの要件として最低3つのAZが必要なため | 172.33.2.128/25 |
neon-cyber-redis-private-subnet-1a | ap-northeast-1a | Redisのプライベートサブネット。ロードバランサーを使用するためもう一つ1cのサブネットも用意してマルチAZ化している | 172.33.3.0/25 |
neon-cyber-redis-private-subnet-1c | ap-northeast-1c | Redisのプライベートサブネット。ロードバランサーを使用するためもう一つ1aのサブネットも用意してマルチAZ化している | 172.33.5.0/25 |
IGW(インターネットゲートウェイ)
ブラウザやターミナルなどインターネット環境からVPC内にアクセスするにはVPCにIGWをアタッチする必要があります。
インターネット環境からサブネットに入るためにも必要ですし、サブネットからインターネット環境に出ていくためにもサブネットは必要になります。
NAT(ネットワークアドレストランスレーション)ゲートウェイ
NATゲートウェイはプライベートサブネット内のリソースがインターネットとやり取りするための中継役を果たし、セキュリティや通信の制御を強化します。
NATゲートウェイをパブリックサブネット内に作成します。プライベートサブネットがインターネット環境に出ていくにはパブリックサブネット内のNATゲートウェイを経由しインターネットとの通信が可能になります。
ここで一度サブネットの種類について整理しておきます。
プライベートサブネット
インターネット環境にでていくことはできる
パブリックサブネット
インターネット環境にでていくこともインターネット環境からもアクセスが可能
エンドポイント(EC2がS3にアクセスしたい場合)
サービス名はcom.amazonaws.ap-northeast-1.s3のタイプが「Gateway」
ルートテーブル
VPCのルートテーブルは、VPC内のトラフィックを案内する案内人のようなものです。パケットがどの方向に送られるかを示す目的地(CIDRブロック)と、その目的地に到達する方法(ターゲット)が書かれています。
名前 | 関連しているサブネット |
---|---|
neon-cyber-ec2-public-rt |
neon-cyber-ec2-public-subnet-1a neon-cyber-ec2-public-subnet-1c
|
neon-cyber-ec2-private-rt |
neon-cyber-ec2-private-subnet-1a neon-cyber-ec2-private-subnet-1c
|
neon-cyber-rds-rt |
neon-cyber-rds-private-subnet-1a neon-cyber-rds-private-subnet-1c neon-cyber-rds-private-subnet-1d
|
neon-cyber-redis-rt |
neon-cyber-redis-private-subnet-1a neon-cyber-redis-private-subnet-1c
|
今回作成する4つのルートテーブルのうち、ルートの追加を行います
neon-cyber-ec2-public-rt
送信先 | ターゲット |
---|---|
0.0.0.0/0 |
neon-cyber-igw |
igwを割り当てたことによりパブリックサブネット化できました。
neon-cyber-ec2-private-rt
送信先 | ターゲット |
---|---|
0.0.0.0/0 |
neon-cyber-ngw |
NATゲートウェイを割り当てたことによりプライベートサブネットがインターネット環境にアクセスできるようになりました。
neon-cyber-rds-rt
デフォルトのまま
neon-cyber-redis-rt
デフォルトのまま
セキュリティグループ
各インスタンスがどのようにネットワーク接続を行えるようにするかを定義します
以下作成するセキュリティグループの一覧です。
名前 | 役割 |
---|---|
neon-cyber-ec2-admin-sg | のちに作成する踏み台サーバー(EC2インスタンス)用 |
neon-cyber-private-sg-1a | プライベートサブネット内のインスタンス用(AZ:ap-northeast-1a) |
neon-cyber-private-sg-1c | プライベートサブネット内のインスタンス用(AZ:ap-northeast-1c) |
neon-cyber-alb-sg | ロードバランサー用 |
rds-sg | RDS用 |
redis-sg | Redis用 |
ec2-to-rds-sg | EC2インスタンスがRDSに接続する用 |
ec2-to-redis-sg | EC2インスタンスがRedisに接続する用 |
どういったトラフィックを受け付けて、どういったトラフィックの送信を許可するかをそれぞれ定義します。
neon-cyber-ec2-admin-sg
neon-cyber-private-sg-1a
neon-cyber-private-sg-1c
neon-cyber-alb-sg
今はHTTPしか許可していませんがのちにSSL証明書を当てたいので後で設定します。
ec2-to-rds-sg
rds-sgへのトラフィックの送信を許可します(次の工程で作るSGです)
rds-sg
thisbe-vpcのIPからのトラフィックと先ほど作ったec2-to-rds-sg
のセキュリティグループからのトラフィックを許可します
ec2-to-redis-sg
redis-sgへのトラフィックの送信を許可します(次の工程で作成するSG)
redis-sg
先ほど作成したec2-to-redis-sgからのトラフィックを許可します(カスタムTCPとSSH)
ふう、、これでVPC周りの設定が完了です。(やっと1章、、、)
VPCの作成についてはこちらの動画を参考にさせていただきました。
RDS
サブネットグループの作成
DBクラスタ・インスタンスの作成
エンジンのタイプはAurora (MySQL Compatible)
エンジンバージョンはMySQL Aurora 8系のデフォルトバージョン
パスワードはセルフマネージドにし、インスタンスタイプは一番安上がりなものにしました。(にしても高い...)
Elastic Cache(Redis)
サブネットグループの作成
DBクラスタ・インスタンスの作成
IAM
ユーザーの作成
「neon-cyber-ec2」というEC2インスタンス用のユーザーを作成します
アクセスキーの作成
アクセスキーとシークレットアクセスキーを生成しておく。(のちに環境変数にセットします)
ポリシーの作成
「neon-cyber-ec2-policy」というポリシーを作成します。
設定するポリシーは以下です。
ロールの作成
System Manager
パラメータストアの追加
アプリで利用する環境変数をここにセットしておきます。
- AWS_ACCESS_KEY_ID(neon-cyber-ec2のアクセスキー)
- AWS_SECRET_KEY(neon-cyber-ec2のシークレットアクセスキー)
- REDIS_URL(redis://(ElastiCacheのプライマリエンドポイント):(ポート番号))
- DATABASE_HOST(RDS Auroraのクラスタ(ライター)エンドポイント)
- DATABASE_USER(RDS AuroraのDBユーザー名)
- DATABASE_PASSWORD(RDS AuroraのDBパスワード)
EC2
つづいて一番大事なEC2の作成をしていきます。
先述したとおり、以下三つのインスタンスを作成します
- neon-cyber-base(踏み台サーバーとAMIイメージの役割)
- neon-cyber-web(webサーバーとしてのインスタンス)
- neon-cyber-worker(workerサーバーとしてのインスタンス)
インスタンス作成
neon-cyber-baseの作成
私はubuntuを選択しました。(前に使ったことがあったので)
この踏み台サーバーは踏み台の役割と AMIの役割しか持っていない(railsサーバーなどを起動しない)ためインスタンスタイプは最低限でいいと思います。
このサーバーはパブリックサブネット上に配置し、インターネット環境から入れるようにするため
パブリックIPアドレスの割り当ては有効化します。(web,workerサーバーを作成するときは無効化)
新しいキーペアも作成しましょう。
「neon-cyber-web-ec2-key」にしました。
踏み台サーバーにpemファイルを配置
$ scp -i ~/.ssh/neon-cyber-prod-web-ec2-key.pem ~/.ssh/neon-cyber-prod-web-ec2-key.pem ubuntu@52.195.207.191:~/
- @の後ろはパブリックサブネット上にあるEC2インスタンスのパブリックIPアドレス
ssh接続
$ ssh -i "~/.ssh/neon-cyber-prod-web-ec2-key.pem" ubuntu@52.195.207.191
- ユーザーは
root
ではなくubuntu
基本rootで作業するため
$ sudo su -
設定ファイルの修正
$ sudo vi /etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 60
$ sudo vi ~/.ssh/config
TCPKeepAlive yes
ServerAliveInterval 60
ServerAliveCountMax 120
aptコマンドのアップデート
$ sudo apt update
rbenvインストール
$ git clone https://github.com/rbenv/rbenv.git /opt/rbenv
rbenvがあるか確認
$ ls /opt/
パスを通す
$ echo 'export RBENV_ROOT="/opt/rbenv"' >> /etc/profile
$ echo 'export PATH="${RBENV_ROOT}/bin:${PATH}"' >> /etc/profile
$ echo 'eval "$(rbenv init -)"' >> /etc/profile
$ source /etc/profile
通っているか確認
$ echo $RBENV_ROOT
$ echo $PATH
ruby-buildをインストール
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
入っているか確認
$ ls $(rbenv root)/plugins/
必要なパッケージをインストール
- rbenvのインストール
- mysqlクライアント
- redisクライアント
- awsのCLI
- jsonを扱うパッケージ
- CodeDeployに必要なパッケージ
などが含まれている
$ apt install make zlib1g-dev gcc g++ libmysqlclient-dev libreadline-dev libffi-dev libyaml-dev mysql-client-core-8.0 awscli jq ruby-full wget redis-tools -y
必要なバージョンのrubyをインストール
$ rbenv install 3.2.3
# => Installed ruby-3.2.3 to /opt/rbenv/versions/3.2.3
$ rbenv global 3.2.3
インストールできているか確認
$ ruby -v
$ which ruby
# => /opt/rbenv/shims/ruby
bundler インストール
$ gem install bundler
$ bundler -v
# => Bundler version 2.5.7
$ which bundler
# => /opt/rbenv/shims/bundler
プロジェクトディレクトリの作成とシンボリックリンクの設定
$ mkdir -p /var/www/rails/neon-cyber
$ ln -s /var/www/rails/neon-cyber neon-cyber
gitからリポジトリをクローンしてみる(AWSでCodeDeployの設定ができていないため)
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX root@ip-172-33-1-117
The key's randomart image is:
+---[RSA 3072]----+
| |
| |
| |
| |
| |
| |
| |
| |
| |
+----[SHA256]-----+
$ cat ~/.ssh/id_rsa.pub
ssh-rsa XXXXXX...
出力された内容を元にgithubでSSHキーを登録
$ git clone git@github.com:username/NeonCyber.git .
$ cd backend
$ bundle install
インストールできているか確認
$ rails -v
# => Rails 7.1.3.2
$ which rails
# => /opt/rbenv/shims/rails
aws cliの設定
$ aws configure
AWS Access Key ID [None]: neon-cyber-ec2ユーザーのAccessKeyId
AWS Secret Access Key [None]: neon-cyber-ec2ユーザーのSecretAccessKey
Default region name [None]: ap-northeast-1
Default output format [None]: json
OS起動時にrailsサーバーが起動するように設定
#!/bin/bash
# ssmからパラメータを取得し.envに書き出す
sudo -i
echo 'get_parameters.sh////////////////////////////////////////////////////////////////////////'
# ssm parameter-store settings
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") \
&& INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") \
&& ZONE=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone)
REGION=$(echo $ZONE | sed 's/.$//')
FILENAME="/var/www/rails/neon-cyber/backend/.env"
NAME=$(aws --region ${REGION} ec2 describe-instances --instance-ids "${INSTANCE_ID}" --query 'Reservations[0].Instances[0].Tags[?Key==`Name`]|[0].Value')
NAME=$(echo ${NAME} | awk '{print substr($0, 2, length($0)-2)}')
echo "INSTANCE_ID: $INSTANCE_ID"
echo "ZONE: $ZONE"
echo "REGION: $REGION"
echo "FILENAME: $FILENAME"
echo "NAME: $NAME"
if [ "${NAME}" = "neon-cyber-web" ] || [ "${NAME}" = "neon-cyber-worker" ]; then
SSM_PARAMS=$(aws --region ${REGION} ssm get-parameters-by-path --path "/neon-cyber-production" --with-decryption)
for params in $(echo $SSM_PARAMS | jq -r --raw-output '.Parameters[] | .Name + "='\''" + .Value + "'\''"'); do
echo ${params#"/neon-cyber-production/"}
done > ${FILENAME}
elif [ "${NAME}" = "neon-cyber-web-test" ] || [ "${NAME}" = "neon-cyber-worker-test" ]; then
SSM_PARAMS=$(aws --region ${REGION} ssm get-parameters-by-path --path "/neon-cyber-test" --with-decryption)
for params in $(echo $SSM_PARAMS | jq -r --raw-output '.Parameters[] | .Name + "='\''" + .Value + "'\''"'); do
echo ${params#"/neon-cyber-test/"}
done > ${FILENAME}
fi
#!/bin/bash
chmod 755 /var/www/rails/neon-cyber/deployment_scripts/get_parameters.sh
# .envファイルを再読み込み
su -l -c '/var/www/rails/neon-cyber/deployment_scripts/get_parameters.sh'
echo 'start_application.sh////////////////////////////////////////////////////////////////////////'
export INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
NAME=$(aws ec2 describe-instances --instance-ids "${INSTANCE_ID}" --query 'Reservations[0].Instances[0].Tags[?Key==`Name`]|[0].Value')
NAME=$(echo ${NAME} | awk '{print substr($0, 2, length($0)-2)}')
if [ "${NAME}" = "neon-cyber-web" ]; then
echo "railsサーバーを起動します"
su -l -c 'cd /var/www/rails/neon-cyber/backend && echo "PROCESS_TYPE=\"web\"" >> .env && bundle exec rails s -e production -b 0.0.0.0 -p 80 -d'
elif [ "${NAME}" = "neon-cyber-web-test" ]; then
echo "railsサーバーを起動します"
su -l -c 'cd /var/www/rails/neon-cyber/backend && echo "PROCESS_TYPE=\"web\"" >> .env && bundle exec rails s -e development -b 0.0.0.0 -p 80 -d'
elif [ "${NAME}" = "neon-cyber-worker" ]; then
echo "sidekiqを起動します"
su -l -c 'cd /var/www/rails/neon-cyber/backend && echo "PROCESS_TYPE=\"worker\"" >> .env && bundle exec sidekiq -C config/sidekiq.yml -e production'
elif [ "${NAME}" = "neon-cyber-worker-test" ]; then
echo "sidekiqを起動します"
su -l -c 'cd /var/www/rails/neon-cyber/backend && echo "PROCESS_TYPE=\"worker\"" >> .env && bundle exec sidekiq -C config/sidekiq.yml -e development'
elif [ "${NAME}" = "neon-cyber-base" ]; then
echo "Baseインスタンスでデプロイが実行されました。アプリケーションの起動は手動で行ってください。"
else
echo "ec2インスタンスに適切なNameタグが付与されていないため、アプリケーションの起動に失敗しました。" 1>&2
fi
$ vi /etc/systemd/system/rails.service
[Unit]
Description=RailsApp
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/www/rails/neon-cyber/deployment_scripts
ExecStart=/var/www/rails/neon-cyber/deployment_scripts/start_application.sh
Restart=always
User=root
Group=root
TimeoutStartSec=300
[Install]
WantedBy=default.target
デーモンの再読込・登録・起動・ステータスの確認
$ systemctl daemon-reload
$ systemctl enable rails.service
$ systemctl start rails.service
$ systemctl status rails.service
MySQLの接続確認
$ mysql --version
$ mysql -h neon-cyber-production.cluster-ro-chueaqy2c9v6.ap-northeast-1.rds.amazonaws.com -P 3306 -u admin -p
show databases;
Redisの接続確認
$ redis-cli -h neon-cyber-production-001.miwzhw.0001.apne1.cache.amazonaws.com
(redisインスタンスのエンドポイント名から末尾のポートの記述を除いたもの)
> ping
PONG
と返ってくればOK
バックグラウンドで起動しているrailsサーバーを停止したい時
PIDを拾う
$ ps aux | grep puma
root 2230 0.0 28.2 757544 129096 ? Ssl 01:23 0:00 puma 6.4.2 (tcp://0.0.0.0:80) [backend]
プロセスをkillする
$ kill -9 2230
ターゲットグループ
エンドユーザーはロードバランサー経由でEC2のwebサーバーにアクセスするものとします。そのための準備を行います。
ロードバランサー
起動テンプレート
EC2のオートスケーリングを利用してインスタンスが自動で生成されるようにする場合、起動テンプレートという機能を使います。
イメージの作成
起動テンプレートを使うには元となるマシンイメージが必要になります。以下の手順でイメージを作成します。
起動テンプレートの作成
オートスケーリンググループの作成
Auto Scaing グループ名に「neon-cyber-web-auto-scaling-group」を入力
起動テンプレートから「neon-cyber-template」を選択
サブネットはneon-cyber-private-subnet-1a, neon-cyber--private-subnet-1c
「起動テンプレートを上書きする」ボタンをクリック
「手動でインスタンスタイプを追加する」にチェックを入れ任意のインスタンスタイプを設定
「既存のロードバランサーにアタッチする」にチェック
既存のロードバランサーターゲットグループで「neon-cyber-target-group」を選択
ヘルスチェックのタイプの「ELB」にチェックを入れる
ヘルスチェックの猶予期間を「30」を入力
Cloud Watch内でグループメトリクスの収集を有効化するにチェックし「次へ」ボタンをクリック
何も入力せず「次へ」ボタンをクリック
何も入力せず「次へ」ボタンをクリック
「タグを追加する」ボタンをクリックし以下の組み合わせを登録したあと「次へ」ボタンをクリック
キー:Name、値:neon-cyber-web、新しいインスタンスをタグ付けするにチェック
「Auto Scalingグループを作成する」ボタンをクリック
できがったAuto Scaling グループを一覧から選択し、高度な設定の「編集」ボタンをクリック
デフォルトのクールダウンに「30」を設定し「更新」ボタンをクリック
AutoScalingグループ(Worker用)の作成
(同上)
「ロードバランサーがありません」にチェック
(同上)
新しいインスタンスをタグ付けするにしないと意図した名前でインスタンスが再生成されなくなるため注意。)起動スクリプトはそのインスタンス名でwebサーバーかwokerサーバーか判別しているため)
これで完成です。
ロードバランサーのDNSにアクセスすることでwebサーバーにアクセスできるようになっていると思います!