🏃

GitHub Actionsを使ってNuxt3のSSRなプロジェクトをVPSデプロイしてPM2で動かす + LINEで完了通知

2022/12/19に公開

https://uko.jp

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キーを予め作成しておきましょう。

https://weblabo.oscasierra.net/openssh-sshd-pubkey-auth/

ログインシェルはなんとなくセキュリティ意識してrbashにしておいてみます。(鍵もれない限り大丈夫なので普通のシェルでも全然よい)

https://orebibou.com/ja/home/201609/20160926_001/

やってもやらなくてもいいですが気になる場合はrsyncだけが実行可能なようにコマンド制限しておくとなおさらよさそうです。
今回はgithubというユーザーを作成しました。

git-shellというものもありますが、こちらはVPS側からSSH経由でリポジトリをcloneしたりする用のユーザーに設定されるものであり、今回のようにデプロイ時にrsyncなどのコマンドを動かす場合にこれを設定してしまうとうまくいかないので注意です。(以下参考)

https://www.nautilus-code.jp/articles/add_git_user_on_os

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操作)を記述する

気持ち的には「build」と「deploy」の2つのJobsに分けたいところですが、Jobが別になるとランナー(仮想環境)も別になってしまいます。つまりビルドをしたJobで生成されたファイル(Nuxt3においては.output)は、デプロイ用の別Jobへは持ち越されないわけです。よって、1つのJob内の別Stepとしてビルドとデプロイの動作を設定します。

https://zenn.dev/hsaki/articles/github-actions-component

今回は以下の2つの記事を参考に作っていきます。

https://qiita.com/kz_morita/items/690b367067666fddb562

https://maku.blog/p/un3gu8m/

いずれもHugoのプロジェクト向けなので、Nuxt3向けにアレンジしてみます。

今回のミニマムWorkflow

プロジェクトのActions → set up a workflow yourself をクリックして以下のように定義してみます。

.github/workflows/main.yml
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}

https://www.bioerrorlog.work/entry/github-actions-default-workspace

VPS側にビルド後ファイルが配置されているか確認

SSH_USER=githubSSH_PATH=ukojpという条件で動かして、うまくいってりゃ以下のようなディレクトリ配置になります。

/home/github/ukojp % ls -al
合計 20
drwxr-xr-x 4 github github 4096 1219 04:59 ./
drwxr-xr-x 8 github github 4096 1219 05:24 ../
drwxr-xr-x 4 github github 4096 1219 05:34 .output/

PM2を設定する

まさかnodeで動かすわけなんてないので定番のPM2を使います。

https://kazuhira-r.hatenablog.com/entry/2022/01/02/151132

設定ファイルは以下のように定義します。
複数のNode.jsプロジェクトを動かす場合であってもこの1つの設定ファイルで全て管理できるので、できるだけグローバルな位置に置いておくことにします。

/var/www/ecosystem.config.js
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オプション等を使ってポーリング監視させないとダメなようです(ドキュメント斜め読みなので間違ってたらすみません)。

https://pm2.keymetrics.io/docs/usage/watch-and-restart/

さらにデーモン化する

PM2をsystemctlの管理下におくことにします。
以下のファイルを作成してからsystemctl start pm2を実行することで永続化することができます。

/etc/systemd/system/pm2.service
[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をリロードさせることで対処しました。

.github/workflows/main.yml 末尾に追加
    - 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にログインします。

https://notify-bot.line.me/ja/

マイページ下部の「Generate token」ボタンをクリックし、通知名と送りたい相手(1 on 1で自分宛て)を選んでトークンを生成します。

生成できたトークンはSSH情報と同様にリポジトリのシークレットに格納しておきましょう。

https://github.com/marketplace/actions/line-notify

マーケットプレイスに、LINE NotifyのActionがあったのでこちらを使ってみます。
Workflowに以下を追記します。

.github/workflows/main.yml 末尾に追加
    - name: LINE Notify
      uses: snow-actions/line-notify@v1.0.0
      with:
        access_token: ${{ secrets.LINE_NOTIFY_TOKEN }}
        message: ワークフロー実行が完了しました

実行すると終了通知がLINE Notifyアカウント経由で飛んできます。
途中のコマンド結果をランナー内のどこかに保存しておいて、最後にその結果を飛ばすなどができれば、デプロイついでにVPSの負荷状態を見れたりもできるかもしれません。

Discussion