GitHub Actionsを使ってNuxt3のSSRなプロジェクトをVPSデプロイしてPM2で動かす + LINEで完了通知
Nuxt3でポートフォリオを作り直したので自動ビルドとデプロイのためにやったこと録です。
ざっくり
- Nuxt3プロジェクトをmainブランチにpushしたら勝手にビルドしてVPS上にデプロイしてくれてWebが更新されるよ
- デプロイ完了したらLINEで通知がくるから更新確認やりやすいかもね(デフォルトだとコケたときだけGitHubからメールがくるよね)
もう少しざっくり
- GitHub Actionsを使って、ランナー内でNuxt3プロジェクトをビルドしてから、出力されたファイル(
.output
ディレクトリ)だけをrsync
を使ってVPSに同期させるよ - プライベートなリポジトリでも月2000分のランナーが無料で使えるし個人利用なら余裕だからどんどん使っちゃおう
- VPS内ではビルドしないしJenkinsを置いとく必要もないから非力なサーバでも大丈夫だよ
- Nuxt3はPM2で起動させてsystemctlで永続化までさせるよ
- テストは嫌いだからやってない(CIとはいえないかもだけど許して)
まずはpush
ある程度できたプロジェクトはさっさとリモートpushしておきましょう。
GitHub側からSSHでVPSに接続する用のユーザを作成
pushのたびにGitHubがSSHでVPSにつなぐようにしたいので、それ専用のユーザーとSSHキーを予め作成しておきましょう。
ログインシェルはなんとなくセキュリティ意識してrbash
にしておいてみます。(鍵もれない限り大丈夫なので普通のシェルでも全然よい)
やってもやらなくてもいいですが気になる場合はrsync
だけが実行可能なようにコマンド制限しておくとなおさらよさそうです。
今回はgithubというユーザーを作成しました。
git-shell
というものもありますが、こちらはVPS側からSSH経由でリポジトリをcloneしたりする用のユーザーに設定されるものであり、今回のようにデプロイ時にrsync
などのコマンドを動かす場合にこれを設定してしまうとうまくいかないので注意です。(以下参考)
SSHキーなどを登録
プロジェクト設定 → Secrets → Actions から次の5つほどのシークレットを登録します。
キー名 | なかみ |
---|---|
SSH_HOST | VPSのホスト名 (IPなど) |
SSH_KEY | SSHの秘密鍵 |
SSH_PATH | デプロイしたいパス (接続ユーザのホームディレクトリ以下になる) ここでは "ukojp" としてみます |
SSH_PORT | SSHポート番号 (22番以外の場合) |
SSH_USER | SSHユーザ名、ここではさっき作ったユーザ "github" |
自動化を実現するWorkflowの考え方
- YAMLで記述される
-
on
にはActionsの起動条件を書く -
jobs
でジョブ(1つの自動化操作)を定義する - ジョブ内ではさらに
steps
において細かな1つ1つの手順を定義する- 1つのstepは、
name
で定義される名前とuses
またはrun
で定義される実際の操作に相当する宣言の組み合わせを最小単位とする -
uses
は一連の動きがパッケージ化されたGitHubの公開リポジトリを指定する -
run
はコマンド(CUI操作)を記述する
- 1つのstepは、
気持ち的には「build」と「deploy」の2つのJobsに分けたいところですが、Jobが別になるとランナー(仮想環境)も別になってしまいます。つまりビルドをしたJobで生成されたファイル(Nuxt3においては.output
)は、デプロイ用の別Jobへは持ち越されないわけです。よって、1つのJob内の別Stepとしてビルドとデプロイの動作を設定します。
今回は以下の2つの記事を参考に作っていきます。
いずれもHugoのプロジェクト向けなので、Nuxt3向けにアレンジしてみます。
今回のミニマムWorkflow
プロジェクトのActions → set up a workflow yourself をクリックして以下のように定義してみます。
name: Nuxt3 CI/CD
on:
push:
branches: [ "main" ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 16.x ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Create ssh key file on runner filesystem
run: echo "$SSH_KEY" > ${{ runner.temp }}/key && chmod 600 ${{ runner.temp }}/key
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
- name: Deploy with rsync
run: rsync -ahv --delete -e "ssh -i ${{ runner.temp }}/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${SSH_PORT}" ${GITHUB_WORKSPACE}/.output ${SSH_USER}@${SSH_HOST}:${SSH_PATH}
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: ${{ secrets.SSH_PORT }}
SSH_PATH: ${{ secrets.SSH_PATH }}
ランナー上でビルドされ、.output
フォルダが生成されます。このときの生成位置はランナー上のワークスペースとなるので、以下のコマンドでrsync over SSH
してVPSへデプロイします。rsyncは若干沼っぽいみたいなのでここでは解説しません。
rsync -ahv --delete -e "ssh -i ${{ runner.temp }}/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${SSH_PORT}" ${GITHUB_WORKSPACE}/.output ${SSH_USER}@${SSH_HOST}:${SSH_PATH}
VPS側にビルド後ファイルが配置されているか確認
SSH_USER=github
でSSH_PATH=ukojp
という条件で動かして、うまくいってりゃ以下のようなディレクトリ配置になります。
/home/github/ukojp % ls -al
合計 20
drwxr-xr-x 4 github github 4096 12月 19 04:59 ./
drwxr-xr-x 8 github github 4096 12月 19 05:24 ../
drwxr-xr-x 4 github github 4096 12月 19 05:34 .output/
PM2を設定する
まさかnode
で動かすわけなんてないので定番のPM2を使います。
設定ファイルは以下のように定義します。
複数のNode.jsプロジェクトを動かす場合であってもこの1つの設定ファイルで全て管理できるので、できるだけグローバルな位置に置いておくことにします。
module.exports = {
apps: [
{
name: 'ukojp(今回の管理したいプロジェクト名)',
cwd: '/home/github/ukojp',
exec_mode: 'cluster',
instances: 'max',
script: './.output/server/index.mjs',
watch: [ './' ], // または true
watch_options: {
followSymlinks: true,
usePolling: true,
interval: 10000,
binaryInterval: 10000,
},
env: {
PORT: ポート番号(Nuxtはデフォルト3000),
NODE_ENV: 'production',
}
}
]
}
ポイントとしてはwatch
オプションを定義しておくところです。こうすることで自動デプロイされてファイルが更新されたときにPM2が自動リロードしてくれます。MacOS以外はfseventsが使えないのでusePolling
オプション等を使ってポーリング監視させないとダメなようです(ドキュメント斜め読みなので間違ってたらすみません)。
さらにデーモン化する
PM2をsystemctlの管理下におくことにします。
以下のファイルを作成してからsystemctl start pm2
を実行することで永続化することができます。
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target
[Service]
Type=forking
User=github
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PM2_HOME=/home/github/.pm2
PIDFile=/home/github/.pm2/pm2.pid
ExecStart=/usr/bin/pm2 start /var/www/ecosystem.config.js
ExecReload=/usr/bin/pm2 reload /var/www/ecosystem.config.js
ExecStop=/usr/bin/pm2 kill
[Install]
WantedBy=multi-user.target
ここでは実行をgithub
ユーザに設定することで、このユーザからPM2にリロードコマンドを送ることが可能になります(デフォルトはroot
なのでsudoしなきゃいけなくて面倒)。
実際、自分の環境ではPM2のwatchがうまく行かずデプロイ後にエラーが起きたりしていたので、Workflowに以下のようなstepを追記してPM2をリロードさせることで対処しました。
- name: Reload PM2 process
run: ssh -i ${{ runner.temp }}/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${SSH_PORT} ${SSH_USER}@${SSH_HOST} "pm2 reload ukojp"
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: ${{ secrets.SSH_PORT }}
自分の環境ではこのあとnginxのリバースプロキシを使って外部公開してますがここでは割愛します。
Workflow完了をLINEで通知する
LINE Notifyにログインします。
マイページ下部の「Generate token」ボタンをクリックし、通知名と送りたい相手(1 on 1で自分宛て)を選んでトークンを生成します。
生成できたトークンはSSH情報と同様にリポジトリのシークレットに格納しておきましょう。
マーケットプレイスに、LINE NotifyのActionがあったのでこちらを使ってみます。
Workflowに以下を追記します。
- name: LINE Notify
uses: snow-actions/line-notify@v1.0.0
with:
access_token: ${{ secrets.LINE_NOTIFY_TOKEN }}
message: ワークフロー実行が完了しました
実行すると終了通知がLINE Notifyアカウント経由で飛んできます。
途中のコマンド結果をランナー内のどこかに保存しておいて、最後にその結果を飛ばすなどができれば、デプロイついでにVPSの負荷状態を見れたりもできるかもしれません。
Discussion