はじめに
業務で開発されているプロダクトのインフラ周りはAWSで構成されています。
僕はフロントエンドエンジニアなのですが、AWSの利用としては、主にコンソールでAPIのログを確認する程度です。そのため、インフラがAWSでどのように構成されているかという詳細を知らなくても、業務はできています。
しかしAWSについて知っていないことで業務中にプロダクトの構成について考える際、AWSのインフラ部分で「どうなっているのだろう?」と立ち止まることが何度かありました。
そんな時にAWSについて勉強していればもっとプロダクトについて理解が進むだろうし、インフラを構築するチームとの会話もスムーズになると思ったので勉強することにしました。
AWSを学習するために
フロントエンドエンジニアの場合、アプリケーションをデプロイする際にVercelを利用することが多いと思います。
Vercelで開発すると、「いい感じ」にデプロイ環境を構築することができますが、その「いい感じ」に構成してくれるインフラが、実際どうなっているのかを理解してみたいと思いました。
そこで、今回はVercel上に本番環境を持つ自作アプリケーションをAWSに移管することで、AWSの使い方と、Vercelが自動的に行ってくれるインフラの構成について学ぶことにしました。
この記事から学べること
- AWSでNext.jsのアプリケーションを立ち上げる過程
- Vercelで自動的に構築されていたDB環境をAWS上でも再現する方法
- Vercelで自動的に行われていたNext.jsとDBを連携させる方法
- Vercelで簡単にできたDBの中身を確認する方法
Vercelにデプロイしているアプリケーションの構成
アプリケーション本体はNext.jsで構成されています。DBはNeonと呼ばれるPostgreSQLデータベースサービスを使っていて、アプリケーション内での会員登録・ログインにNextAuth.jsを使っています。
このアプリケーションをVercelからAWSに移管していきます。
AWSの本を読む
AWSを使ってインフラ構築をするために、まずはAWSを知ることから始めました。
最初に「Amazon Web Services基礎からのネットワーク&サーバー構築改訂4版」を読みました。実際にAWSで簡単なサーバーを立ち上げるなど手を動かしながら勉強できたのでAWSを使ってみたい方にオススメです。
AWSのWebサーバー環境の構築
本を読んで浅く学んだので、実際に自分のアプリのためのAWS環境を作っていきます。
まずはWebサーバーの構築から始めました。構築イメージは以下です。Next.jsを立ち上げる環境を作っていきます。
VPCを作成する(紫の枠)
AWSのクラウドで自分専用のプライベートネットワーク環境であるVPC(Virtual Private Cloud)を作成します。
VPCができたら自分のネットワーク環境とインターネットの接続ができるように、インターネットゲートウェイ(紫のゲートのロゴ)と呼ばれる入り口を作成します。
パブリックサブネットを作成する(緑のエリア)
自分のVPCの環境の中に、インターネットからアクセスできるオープンな領域(サブネット)を作成します。
このパブリックサブネットがインターネットに接続できるように、インターネットゲートウェイを通したインターネット通信を許可する設定を行います。
逆に、同じサブネットでもインターネット接続を許可しない設定を行った場合、それはプライベートサブネットと呼ばれます。外部からのアクセスを遮断したいDBサーバーなどを配置したい時に使用します。(後半登場します。)
EC2インスタンスを作成する(オレンジの枠)
パブリックサブネットを作成したら、次にそのサブネット内にEC2というサービスを使って仮想サーバー(インスタンス)を構築します。
EC2インスタンスは無料枠のOSであるAmazon Linux 2023を使うことにしました。
このOSはデフォルトでストレージが8GiBなのですが、「ディスク容量不足」のエラーでNext.jsのビルドが失敗するリスクがあったため20GiBで設定しました。
(GiBはGBみたいな単位です。GBは10進数に対してGiBは2進数になります)
構築したwebサーバーでNext.jsアプリを立ち上げる
必要なものをインストールする
EC2インスタンスでNex.jsを立ち上げるために、以下をインストールしていきます。
- Git: v2.50.1
- Node: v20.19.4
- npm: v10.8.2
インストールの方法は、ローカルのPCのターミナルからSSH接続で対象のEC2インスタンスに接続して、CLIでコマンド入力してインストールしていきます。
Gitのインストール
sudo dnf install git -y
Nodeとnpmのインストール
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs
EC2インスタンスに使用するOSがAmazon Linux 2023の場合、そのままinstall -y nodejsするとインストールされるNode.jsが18系になってしまいます。
今回立ち上げる Next.jsアプリケーション では Node.jsのバージョン20系 が必要だったので、install -y nodejsの前に上記のcurlコマンドを実行しました。これにより、Node.js 20のインストールに必要なリポジトリをOSのシステムに追加しています。
Web資材を置くディレクトリを作成する
Webサーバーを配置するディレクトリはEC2インスタンスでは通常/var/www/htmlになります。
Apache や nginx のようなWebサーバーをインストールする場合、そのソフトウェアが自動的に/var/www/htmlのディレクトリを作成するようです。
しかし、このインスタンスにはまだそれらのWebサーバーをインストールしていないため、Next.jsのプロジェクトを配置するために、インスタンス内に手動で/var/www/htmlディレクトリを作成しました。
ここまでec2-userとしてSSH接続していたため、実行するコマンドの前にsudoをつけていました。しかし、 /var/www/html配下ではNext.jsを立ち上げるまでに何度もコマンドを実行することになります。
そのため、/var/www/html配下でsudoをつけなくても作業できるように、ec2-userにも必要な権限を付与しました。
sudo chown -R ec2-user:ec2-user /var/www/html/
これでsudoなしで実行できるようになりました。
作成したディレクトリ配下でGithubからリポジトリをクローンしてビルドします。
cd /var/www/html/
git clone https://github.com/username/your-nextjs-app.git
cd your-nextjs-app
npm install
npm run build
Next.jsをバックグラウンドで起動する
次にpm2というnpmパッケージを使ってNext.jsアプリケーションをバックグラウンドで起動できるようにしました。
通常、SSHでEC2インスタンスに接続し、npm run startのようなコマンドを実行すると、そのコマンドはSSHセッションに紐づいて動きます。そのため、SSH接続を切断すると、プロセスが終了し、アプリケーションも停止してしまいます。pm2は、アプリケーションをデーモン(バックグラウンドプロセス)として実行するため、SSH接続を切ってもサーバーが動き続けることを可能にします。
sudo npm install -g pm2
pm2 start npm --name "nextjs-app" -- start
このコマンドを実行したことで、バックグラウンドでnpm run startされたことになるので、SHH接続が切断されても改めてnpm run startコマンドを実行する必要はないです。
環境変数を移動させる
npm run startによりNext.jsアプリが立ち上がったので、EC2インスタンスのドメインにブラウザからアクセスしてみました。
アクセスしてみると以下のエラー文が表示されていました。
Application error: a server-side exception has occurred (see the server logs for more information).
Applicationエラーと表示されているのでどうやらアプリケーションへのアクセスはうまくいっているようです。そこで環境変数ファイルを配置していないことを思い出しました。
そこでscpコマンドを使ってローカルにある環境変数ファイルをインスタンス配下のNext.jsディレクトリに配置しました。
scp -i ssh-key.pem .env.production ec2-user@パブリックIPアドレス:/var/www/html/next-js-application/.env.production
環境変数ファイルを配置したので、一度pm2を停止して、再build → startします。
pm2 stop nextjs-app
npm run build
pm2 start npm --name "nextjs-app" -- start
これで3000番ポートにアプリケーションを立ち上げたので、ドメイン名に:3000を付けてブラウザからアクセスすると、無事に表示できるようになりました🎉
ただ、このままではドメインの後ろに:3000を追加しなければなりません。そこで、次に/(デフォルトの80番ポート)でアクセスできるようにするために、nginxをプロキシサーバーとして設定することにしました。
nginxサーバーをリバースプロキシに設定する
ドメイン名だけでアクセスできるようにするためには、プロキシサーバーを設定することが一般的です。今回は、Next.jsの前にプロキシサーバーとしてnginxサーバーを構築することにしました。
これにより、インターネットからアクセスがあった際、nginxを経由してからNext.jsアプリケーションにアクセスするという流れになります。
Next.jsを立ち上げている同じEC2インスタンス内にnginxのインストールするので、同じWebサーバー用のEC2インスタンスにSSHで接続し、以下のコマンドを実行します。
sudo dnf install -y nginx
インストールするとEC2インスタンスの/etc配下にnginxディレクトリが作成されます。このnginxディレクトリ内にnginx.confファイルがあるのでプロキシサーバーとして立ち上げます。nginxサーバーにアクセスがあった際に、Next.jsサーバーにリクエストを転送するように設定します。
# nginxの設定例
server {
# ウェブサイトへのアクセスを待機するポート(HTTP)
listen 80;
# ウェブサイトのドメイン名を指定
server_name example.com www.example.com;
# ルートへのリクエストをNext.jsサーバーに転送
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# ビルドされた静的ファイルを直接配信
location /_next/static/ {
alias /var/www/html/your-nextjs-app/.next/static/;
expires 30d; # ブラウザキャッシュの有効期限
add_header Cache-Control "public, immutable";
}
}
準備が整ったのでnginxサーバーを立ち上げます。立ち上げた後にenableコマンドでEC2インスタンスが再起動した際に、nginxサーバーも自動で起動させるように設定しました。
sudo systemctl restart nginx
sudo systemctl enable nginx
これでドメイン名/にアクセスすると3000番ポートで立ち上げているNext.jsサーバーにアクセスできるようになりました🎉
NextAuthでログインできない
アプリケーションにアクセスできたので動作確認をしていたのですが、VercelではできていたNextAuthによるログインが上手くいかなくなっていることが判明しました。
少し実装の詳細を説明しますと、ログインボタンを押下した時にNextAuthのsignin()関数を発火させていますが、遷移先のURLがlocalhost:3000/api/auth/signin/googleのようにドメイン名がlocalhostになってしまっていました。
Vercelの本番環境にデプロイした時には、特に設定は必要なかったはずだけどな🤔と考えていたのですが、調べてみたところ、NextAuthにはVercelの本番環境にデプロイした際の自動的な対応が入っていることが分かりました(参照先)。その箇所が以下のソースコードです。
const __NEXTAUTH: AuthClientConfig = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL).origin,
basePath: parseUrl(process.env.NEXTAUTH_URL).path,
baseUrlServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ??
process.env.NEXTAUTH_URL ??
process.env.VERCEL_URL
).origin,
basePathServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ?? process.env.NEXTAUTH_URL
).path,
_lastSync: 0,
_session: undefined,
_getSession: () => {},
}
ここでは環境変数NEXTAUTH_URLがなければVERCEL_URLを参照するようになっています。
ローカルでの動作するために、ローカル環境ではNEXTAUTH_URLはlocalhost:3000で設定していました。しかし、Vercelの本番環境では特にエラーにならかったので、NEXTAUTH_URLは設定していませんでした。
調べてみたところ、Vercelの本番環境では環境変数VERCEL_URLが自動で設定され、そこには本番環境のURLが入るようになっているようです。(参照先)
したがって、Vercel以外でNextAuthを使う場合には、本番環境の環境変数ファイルにNEXTAUTH_URLを設定する必要があります。
そこで、EC2インスタンスに上げている.env.productionファイルにNEXTAUTH_URLを追加したところ、上手く動作するようになりました👏
VercelのDBからAmazon RDSに変更する
DBの構築方法を決める
ここまでWebサーバー環境をVercelからAWSに移管することができたので、次にDBサーバー環境の移管を進めていきます。
Vercel側の本番環境では、Neonと呼ばれるPostgreSQLデータベースサービスを使っていました。そのDB構成をAWS上でも構築します。
調べてみるとAWS上でデータベースを構築する方法は2つあることが分かりました。
一つは EC2インスタンスを立ち上げてDBソフトウェアをインストールする方法、もう一つはAWSのデータベースサービスであるAmazon RDSを利用する方法です。Vercelからの移行であればAmazon RDSが推奨されていたので、今回はRDSを採用することにしました。
プライベートサブネットを作成する
通常のEC2インスタンスを立ち上げる際にはサブネットが1つで済むのに対し、RDSインスタンスを立ち上げる際には2つのサブネットが必須であると判明しました。
そこで、インターネットからアクセスできWebサーバーを管理しているパブリックサブネットとは別に、外部からのアクセスを制限するプライベートサブネットを2つ作成することにしました。
この2つのプライベートサブネットの中に、RDSインスタンスを1つ立ち上げ、その中でDBサーバーを管理します。RDSインスタンスは1つだけなので、通常は片方のサブネットにインスタンスが配置され、もう片方のサブネットは空の状態となります。
サブネットは1つだけで良いのではないかと思われるかもしれませんが、この空のサブネットは、万が一の障害やメンテナンスが発生した際に、RDSが新しいインスタンスを自動で立ち上げたり、リソースを移行したりするための予備スペースとして機能するために必要であるようです。
DBに接続するための環境変数を変更する
VercelでDBを使用する時にはいくつか環境変数が必要でしたが、ほとんどがVercel DBで独自に使用する変数だったため変更しました。
##Before
POSTGRES_URL="postgres://..."
POSTGRES_PRISMA_URL="postgres://..."
POSTGRES_URL_NO_SSL="postgres://..."
POSTGRES_URL_NON_POOLING="postgres://..."
POSTGRES_USER="xxxx"
POSTGRES_HOST="xxxx"
POSTGRES_PASSWORD="xxxx"
POSTGRES_DATABASE="verceldb"
##After
DATABASE_URL="postgresql://[RDSユーザー名]:[パスワード]@[RDSエンドポイント]:5432/[データベース名]"
POSTGRES_USER, POSTGRES_HOST, POSTGRES_PASSWORD, POSTGRES_DATABASE
→これらは、より簡潔なDATABASE_URLに集約されるため不要です。
POSTGRES_URL, POSTGRES_PRISMA_URL, POSTGRES_URL_NO_SSL, POSTGRES_URL_NON_POOLING
→これらは、Neonの接続に関連するもので、Amazon RDSでは不要になります。
環境変数を更新してNext.jsサーバーからDBサーバーへの接続準備が整ったので、リポジトリで管理しているマイグレーションファイルを使って、 RDS内にVercelで管理していたDBと同じテーブルやカラムの構造を反映させます。
npx prisma migrate deploy
npx prisma generate
これでDBサーバー環境をVercelからAWSに移管することができました🎉
踏み台サーバーを作成する
アプリをブラウザで使ってみて、DBにデータが問題なく保存されているようでしたが、念のためDBのテーブルを直接覗いてデータを確認したくなりました。
Vercelではプロダクトの設定ページからDBの中身を確認することができますが、Amazon RDSはAWSのコンソール画面(ブラウザの管理画面)からは直接参照できません。
そこで、DBの中身をローカル環境から確認できるように、Amazon RDSのDBサーバーに接続する方法を試みることにしました。
RDSのDBサーバーへの接続には、一般的に踏み台サーバー(Bastion Host)の構築が必要です。そこで、外部からアクセス可能なパブリックサブネット内に、SSH接続のみを許可するEC2インスタンス(踏み台サーバー)を設置することにしました。
踏み台サーバーを経由してAmazon RDSに接続するには、まずローカルPCと踏み台サーバーの間でSSHトンネルを確立します。このトンネルを利用することで、プライベートネットワーク内にあるDBサーバーへ安全にアクセスできるようになります。これは、セキュリティを確保しながらDBに接続するための一般的な手法です。
ssh -i ssh-key.pem -L 5433:{amazon RDSのエンドポイント}:5432 ec2-user@{踏み台サーバーのパブリックIPアドレス}
この設定により、ローカルPCの特定のポート(今回であれば5433)に接続すると、その接続がSSHトンネルを経由して踏み台サーバーに転送され、最終的にAmazon RDSのDBサーバーに到達します。
踏み台サーバーにアクセスした状態で、DBクライアントツールを使ってlocalhost:5433に接続すると、RDSのDBサーバーの中身を見ることができるようになります。
これでDBのテーブルを直接覗いてデータを確認できるようになりました🎉
これで自作アプリケーションをVercelからAWSに移管することができました!
最終的に以下のようなインフラ構成図になりました。
感想
最後に、自作アプリケーションをVercelからAWSへ移管した感想を述べます。総じて、AWSへ移管してみて改めてVercelの優れた開発体験の良さを実感しました。 今回のようなシンプルなインフラ構成であれば、やはりVercelが最適であると再認識できたことは大きな収穫だったと思います。
DB閲覧の容易さ
DBの中身を見るときに、今回は踏み台サーバー経由でDBクライアントを使って確認していましたが、VercelではWebアプリの管理画面で簡単に閲覧することができるので、便利だなと思いました。
環境変数変更が容易
本番環境の環境変数を変更した際にVercelの方が楽だなと思いました。AWS環境ではNext.jsの再ビルドが必要になるのに対し、Vercelでは管理画面内で値を変更するだけで反映されるため、運用が非常に楽だと感じました。
DBサーバーのセットアップ
VercelでDBを作成する際、サーバーの構築が簡単で、接続に必要な環境変数群も管理画面に表示されるものをコピペするだけで済む点も、接続の手間が大幅に省けて便利だと思いました。
ビルドとデプロイの安全性
QA環境がない場合、今回のAWSの構成では本番デプロイしてみないとビルドエラーの有無が確認できませんでした。Vercelではデプロイ前に自動でビルドエラーをチェックしてくれるため、安全性が高いと感じました。
セキュリティ設定の簡素化
EC2インスタンスなどAWSでは、サーバーごとにセキュリティグループを細かく設定する必要がありますが、Vercelではこれらのセキュリティ設定を適切に管理してくれているという点にも利便性を感じました。