🎖️

Next.jsをEC2にデプロイする方法とCI/CDの設定

2024/06/25に公開

はじめに

Next.js (App Router) + Cloudfront + S3のデプロイする方法はこちらです。
https://zenn.dev/nenenemo/articles/a3a4b29af76df2

EC2にデプロイした後、ドメイン経由でACMを利用してインターネットからELBへの通信をHTTPSで行う設定については別の記事にしています。
https://zenn.dev/nenenemo/articles/17898e5b1150c0

EC2の作成

インスタンス名(for-next.js)を入力、

キーペアの作成

キーペアとは、EC2インスタンスに安全にアクセスするための認証です。SSHを通じてインスタンスにアクセスする予定がある場合、キーペアを設定してください。

SSH(Secure Shell)

ネットワークを通じて他のコンピュータにリモートログインし、安全な通信を行うためのプロトコルです。インターネット上でデータを暗号化して送受信することで、セキュリティを確保しながら、サーバーや他のリモートシステムにアクセスすることが可能になっています。
https://wa3.i-3-i.info/word11722.html

新しいキーペアの作成を選択、

キーペア名(for-next.js)を入力したら、キーペアを作成を押してください。

ネットワーク設定 > 編集

わかりやすいようにセキュリティグループ名(for-next.js)と説明を変更しておきます。

インスタンスを起動を押してEC2インスタンスを作成してください。

ダウンロードされたキーは、~/.sshディレクトリに移動させておいてください。

EC2インスタンスはVPC内でのみ起動されます。VPCはデフォルトのものを選択し、インスタンスを起動を選択してください。

EC2インスタンスにSSH接続

このままローカルから接続すると、下記のような権限のエラーが表示されます。

パーミッションを表示して確認してください。

ls -l ~/.ssh/for-next.js.pem

下記が表示されました。(黒塗りはユーザー名)
この状態では、ファイルの所有者は読み書きが可能ですが、他のユーザーも読み取りが可能となっています。

パーミッションの見方

-

この先頭のダッシュは通常のファイルであることを意味します。ディレクトリであれば、d、シンボリックリンクの場合はlが表示されます。

rw-

最初の三文字はファイルの所有者(この場合は ユーザー名)のアクセス権を示します。rwは読み書き(readとwriteの頭文字)が可能であることを意味し、-は実行権限がないことを示します。

r--

次の三文字はファイルのグループ(この場合はstaff)のメンバーのアクセス権を示します。rは読み取り可能であることを意味し、--は書き込みと実行ができないことを示します。

r--

最後の三文字はその他のユーザーのアクセス権を示します。ここでもrは読み取り可能であることを意味し、--は書き込みと実行ができないことを示します。

1

ファイルのハードリンク数です。通常のファイルでは 1が一般的です。

ユーザー名

ファイルの所有者のユーザー名

staff

ファイルが属するグループ名

1678

ファイルのサイズ(バイト単位)

6 24 12:22

ファイルの最後の変更日時(月、日、時刻)

パーミッションを600(所有者のみ読み書き可能)に変更してください。

chmod 600 ~/.ssh/for-next.js.pem

権限が正しく変更されているのがわかると思います。

ssh -i キーペアのパス ec2-user@パブリックIPアドレス

パブリックIPアドレスはパブリック IPv4 アドレスまたは、自動的に割り当てられた IP アドレスで確認してください。

今回は下記を実行します。

ssh -i ~/.ssh/for-next.js.pem ec2-user@パブリックIPアドレス

接続の承認が必要なので、yesと入力してください。

Are you sure you want to continue connecting (yes/no/[fingerprint])? 

fingerprint

SSH公開鍵の短縮されたハッシュ値です。

下記のように表示されると、SSH接続ができています。

セットアップ

Next.jsはNode.js上で動作するJavaScriptフレームワークなので、Next.jsアプリケーションをEC2インスタンス上で実行するにはインストールが必要です。

まずは、EC2インスタンスのOSをアップデートしてください。

sudo yum update -y

Node.js をインストールします。NVM(Node Version Manager)を使用すると便利です。
https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/setting-up-node-on-ec2-instance.html
https://github.com/nvm-sh/nvm

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

.bashrcに記載した内容を反映してください。正しく設定された環境でNode.jsのインストールやその他の操作が行えるようになります。

source ~/.bashrc

nvmを使用してNode.jsの最新のLTSバージョンをインストールしてください。

nvm install --lts

gitのインストール

sudo yum -y install git

リポジトリのクローン

アプリケーションのリポジトリをSSH経由でEC2インスタンスにクローンする場合、SSHキーの生成が必要です。4096はビット数です。

ssh-keygen -t rsa -b 4096

下記が聞かれますが、全てEnterで問題ないです。

Enter file in which to save the key (/home/ec2-user/.ssh/id_rsa):

生成されたSSH秘密鍵(私鍵)を保存するファイルパスを入力する

Enter passphrase (empty for no passphrase):

秘密鍵に対して設定するパスフレーズ(パスワード)を入力する。パスフレーズを設定しない場合は、空のままEnterキーを押してください。

Enter same passphrase again:

先ほど入力したパスフレーズを再度入力する

生成されたSSHキーの内容を確認してコピーしてください。ssh-rsaから始めるものです。

cat /home/ec2-user/.ssh/id_rsa.pub

setting > SSH and GPG keysでNew SSH keyを選択、

Title(ec2-for-next.js)とkey(コピーしたSSHキー)を入力したらAdd SSH keyを押して登録してください。

これでクローンできるようになっています。

git clone <SSH用のクローンURL>

接続の承認が必要なので、yesと入力してください。

Are you sure you want to continue connecting (yes/no/[fingerprint])? 

リバースプロキシ

クライアントとサーバー(アプリケーション)の間に立ち、ポートの変換やセキュリティ機能、負荷分散などの役割を果たします。そのため、ポートの違いを橋渡しする重要なコンポーネントとして利用されます。

リバースプロキシの設定

Next.jsアプリケーションは通常、3000番ポートで起動し、リクエストを受け付けますが、本番環境ではポート番号を変更(80番ポート(HTTP)または443番ポート(HTTPS))してを使用して公開されます。そのため、ユーザーがブラウザを通じてアクセスする際には、これらのポート(80、443)を介して通信することが一般的です。

このポートの違いの橋渡し(リバースプロキシ)をするために、Nginxなどのリバースプロキシが必要になります。リバースプロキシは、80番や443番ポートで受け取ったリクエストを、バックエンドで稼働している3000番ポートのNext.jsアプリケーションに転送します

ルートディレクトリで、Nginxをインストールしてください。

sudo yum install nginx -y

Next.jsアプリケーションのポートに転送されるようにNginxの設定を変更します。

sudo nano /etc/nginx/nginx.conf

server {}の内に下記を追加してください。

location / {
    proxy_pass http://localhost:3000;
}

/etc/nginx/nginx.confの内容を確認

cat /etc/nginx/nginx.conf

以下の内容になっているはずです。

/etc/nginx/nginx.conf
# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        error_page 404 /404.html;
        location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }
        location / {
            proxy_pass http://localhost:3000;
        }
    }

# Settings for a TLS enabled server.
#
#    server {
#        listen       443 ssl http2;
#        listen       [::]:443 ssl http2;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        ssl_certificate "/etc/pki/nginx/server.crt";
#        ssl_certificate_key "/etc/pki/nginx/private/server.key";
#        ssl_session_cache shared:SSL:1m;
#        ssl_session_timeout  10m;
#        ssl_ciphers PROFILE=SYSTEM;
#        ssl_prefer_server_ciphers on;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        error_page 404 /404.html;
#        location = /404.html {
#        }
#
#        error_page 500 502 503 504 /50x.html;
#        location = /50x.html {
#        }
#    }

}

再起動して変更を適用します。

sudo systemctl restart nginx

下記コマンドでどのポートが開いているか確認することができます。

netstat -tlnt

セキュリティグループの設定

インバウンドルールを下記のように4つ追加してください。

0.0.0/0

IPv4アドレスの範囲で、インターネット上のすべてのIPv4アドレスを指します。
たとえば、AWSのセキュリティグループでインバウンドルールを設定する際に、0.0.0.0/0をソースとして指定すると、全世界のIPv4アドレスからのアクセスを受け入れることになります。

::/0

IPv6アドレスの範囲で、インターネット上のすべてのIPv6アドレスを指します。
セキュリティグループやファイアウォールの設定で::/0を使用すると、全世界のIPv6アドレスからのアクセスを許可することになります。

環境変数の設定

環境変数を使用している場合は忘れずに.envを作成してください。

アプリケーションのディレクトリに移動して、

cd リポジトリ名

.envの内容を入力してください。

touch .env

ローカルの.envをコピペしてください。

nano .env

アプリケーションをビルド

アプリケーションのディレクトリで、依存関係のインストール

npm install

Next.jsアプリケーションをビルドします。

npm run build

サーバーを起動してください。

npm run start

パブリックIPアドレス(http://パブリックIPアドレス/ )にアクセスして画面が表示されるか確認してください。

PM2を使用してアプリケーションを永続的に起動させる

現在のままでは、SSH接続でサーバーを立ち上げている時のみしかブラウザにプロジェクトが表示されません。

試しにnpm run startで起動しているサーバーを停止してください。パブリックIPアドレス(http://パブリックIPアドレス/ )に再度アクセスすると、下記のように表示されると思います。

PM2を使用してアプリケーションを永続的に起動するようにしてください。
https://pm2.keymetrics.io/

ルートディレクトリに移動し、

cd ~/

ルートディレクトリでpm2をインストールしてください。

npm install -g pm2

アプリケーションのディレクトリに移動して、

cd リポジトリ名

アプリケーションを--nameフラグを使用してアプリケーションに任意の名前を付けて起動してください。
下記はPM2を使用してNode.jsのパッケージマネージャであるnpmを通じてNext.jsアプリケーションを起動しています。

pm2 start npm --name "nextjs-app" -- start

再度パブリックIPアドレス(http://パブリックIPアドレス/ )に再度アクセスすると、画面が表示されると思います。

-- start

package.jsonのscripts > "start":に記述されている内容を実行しています。

停止する
"nextjs-app"はidでも構いません。

pm2 stop "nextjs-app"

停止させると、アクセスしてもこのような表示になっていると思います。

アプリケーションのステータス確認

pm2 status "nextjs-app"

アプリケーションを一覧表示

pm2 list

ログ確認

pm2 status "nextjs-app"

特定のアプリケーションプロセスを削除する

pm2 delete 1

修正後のコードをEC2インスタンスに反映させる

ssh -i ~/.ssh/for-next.js.pem ec2-user@パブリックIPアドレス

アプリケーションのディレクトリに移動して、

cd リポジトリ名

最新のコードをpullしてください。

git pull

再度Next.jsアプリケーションをビルドします。

npm run build

既存のアプリケーションプロセスを削除してください。

pm2 delete nextjs-app

再度、アプリケーションを起動してください。

pm2 start npm --name "nextjs-app" -- start

パブリックIPアドレス(http://パブリックIPアドレス/ )にアクセスして、修正後の内容が表示されいるか確認してください。

CI/CD

コードが変更されるたびに毎回手動で修正後のコードをEC2インスタンスに反映させるのは大変なので、CI/CDの設定を行います。

settings > secret > actionsを選択

New Repository secretを選択して下記の4つを登録してください。

EC2_HOST: EC2インスタンスのパブリックIPまたはDNS名
EC2_USER: SSH接続に使用するユーザー名(デフォルトはec2-userです。)
EC2_SSH_KEY: EC2インスタンスへのSSH接続に使用する秘密鍵
EC2_PORT: SSH接続に使用するポート(通常は22です。)

EC2_SSH_KEYは下記で確認してください。

cat ~/.ssh/for-next.js.pem

下記の形式のものです。

-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----

ルートディレクトリに.github/workflowsディレクトリを作成し、その中にdeploy.ymlを作成します。

mkdir -p .github/workflows && touch .github/workflows/deploy.yml

deploy.ymlの内容を下記に変更してください。

deploy.yml
name: Deploy to EC2

on:
  push:
    branches:
      - main # mainブランチにプッシュされたときをトリガーとする

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Deploy to EC2
        uses: appleboy/ssh-action@v0.1.5
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          port: ${{ secrets.EC2_PORT }}
          script: |
            cd <>
            git pull
            npm run build
            pm2 delete nextjs-app
            pm2 start npm --name "nextjs-app" -- start

appleboy/ssh-action@v0.1.5

GitHub Actions で使用されるアクションの一つで、SSHを使ってリモートサーバーに接続し、指定したコマンドやスクリプトを実行するために設計されています。

このアクションを利用することで、GitHubのワークフローから直接、サーバー上での作業を自動化できるようになります。
https://github.com/appleboy/ssh-action

GitHub ActionsのVSCode拡張

VSCodeを使用していると、下記が表示されたのでGitHub Actionsの拡張をインストールします。

リポジトリのActionsを選択してください。

インストール後にサインインしてください。

Allowを選択してください。

VSCodeを再起動してください。GitHub Actionsのアイコンを選択すると、

Workflow実行履歴の確認などができて便利です。

Actions secrets and variables(GitHub Actionsのシークレットと変数)もエディターで編集することができます。

denied (publickey,gssapi-keyex,gssapi-with-mic).

SSH接続でEC2に接続しようとした際に表示されました。
私の場合、コマンドをタイプミスしていました。

以下はGithub Actionsで表示されたエラーです。

2024/06/26 03:10:51 dial tcp :: i/o timeout

これはSSH接続の試みがタイムアウトにより失敗したことを意味します。

2024/06/26 03:42:47 ssh.ParsePrivateKey: ssh: no key found

SSH秘密鍵を解析しようとした際に、有効なキーフォーマットが見つからなかった。

「ssh: handshake failed: ssh: unable to authenticate, attempted methods [none], no supported methods remain」

SSH接続時に認証が失敗し、利用可能な認証方法がないことを示しています。

GitHub Actionsで使用している秘密鍵(EC2_SSH_KEY)がEC2インスタンスに登録された公開鍵と一致しているか確認してください。

could not parse *** as int value for flag port,p: strconv.ParseInt: parsing "***": invalid syntax

文字列を整数値として解釈しようとした際に、構文が無効であるために解析できなかったことを示しています。
今回の場合はportとあるのでポート番号に誤りがあります。

終わりに

何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉

Discussion