GitHub Actions を使って Next.js × AWS EC2 を自動デプロイした話


※ この記事は **[K-Ruby #25](https://k-ruby.connpass.com/event/195281/)** のLT資料として書かれた記事です。

こんにちは！  
先日、[GMOペパボの東証一部上場が決まった](https://twitter.com/pepabo/status/1329677997171826689)ことで**「東証一部上場の Web 系企業に未経験転職した29歳」**になって怪しさに磨きがかかりました[よしこ @k2_yoshikouki](https://twitter.com/k2_yoshikouki) です。そろそろエンジニアになれる石売ります。

最近 [yoshikouki.net](http://yoshikouki.net) という個人サイトを作っている最中で、勉強も兼ねて以下の要件で作っています

- Infrastructure as Code で環境構築
    - Chef（ホスト内の実装について定義）
    - Terraform（各ホストの関係について定義）
- AWS を使用
    - EC2 に nginx （リバースプロキシ）を載せる
    - ECS, EKSなどのコンテナサービスは使わない
- フレームワークは Next.js (React) を使用
    - バックエンドは node.js
    （しばらくは Next.js のルーティングを使用したベタ書きかなあと考えている）
    - CI/CD は GitHub Actions を利用

苦節一週間の結果、GitHub Actions で自動デプロイ（Continuous Delivery）を実装できたのでその内容を紹介いたします。

**Ruby？ 知らない子ですね...**

# ゴール

GitHub Actions を使って AWS EC2 への自動デプロイを導入します

- テストがないのでCIの優先順位が低い

# 課題

1. AWS へのデプロイでいい感じにしてくれるサービスはECS, EKS関係が潮流で情報が豊富
    （しかし、今回のコンテナ不使用という仕様には合わない）

2. 候補のデプロイサービスは AWS CodeDeploy や Capistrano がある
    - CodeDeploy はAWS専用サービスかつ情報が少ない
    - Capistrano は Ruby環境が必要なので、node.js な今回の仕様でスマートじゃない

3. コードの移設は rsync なりscp なりでできるが、プロセスの起動 `next start` がややこしい
    - 色々試行しているとき、ワークフローで `ssh ~~~ "npm run start"` を実行して**CI/CDが一生終わらないバグを埋め込んだ**  
    （プロセスが待機状態になるため）
    - `nohup npm run start &` などで裏プロセスとして動かせたが、2度目以降のデプロイではプロセスが生きているのでエラーになる  
    -> 前回のプロセスを kill しないと行けない

# 対応策

- コード移設には rsync コマンドで対応
- Next.js のプロセス管理に PM2 を採用


### GitHub Actions ワークフロー

**[`./.github/workflows/main.yml`](https://github.com/yoshikouki/yoshikouki.net/blob/main/.github/workflows/main.yml)**

```yaml
name: Deploy to yoshiko.net

on:
  push:

env:
  ssh_key_path: ~/.ssh/yoshikouki.net.pem
  app_path: /var/www/

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Cache multiple paths
        uses: actions/cache@v2
        with:
          path: |
            ~/.npm
            **/node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
      - uses: actions/setup-node@v1
        with:
          node-version: '12.x'
      - run: npm install
      - run: npm build

  deploy:
    if: github.ref == 'refs/heads/main'
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Generate SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ${{ env.ssh_key_path }}
          chmod 400 ${{ env.ssh_key_path }}
          eval "$(ssh-agent -s)"
          ssh-add ${{ env.ssh_key_path }}
      - name: Deploy with "rsync" command
        run: |
          rsync -avL --progress --exclude ".git/" \
          -e "ssh -i ${{ env.ssh_key_path }} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \
          ./ \
          ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ env.app_path }}
      - name: Build and Start Next.js
        run: |
          ssh -i ${{ env.ssh_key_path }} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \
          ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \
          'cd ${{ env.app_path }} \
          && sudo npm install \
          && sudo npm install pm2 -g\
          && sudo npm run build \
          && pm2 startOrReload app.json --env production'
```

### PM2 のプロセス設定ファイル

**[`./app.json`](https://github.com/yoshikouki/yoshikouki.net/blob/main/app.json)**


```json
{
  "name" : "app",
  "script" : "./node_modules/next/dist/bin/next",
  "env" : {
    "NODE_ENV" : "development"
  },
  "env_production" : {
    "NODE_ENV" : "production"
  }
}
```

# PM2 is 何
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532774/15b235a1-818c-43db-b2ca-5a2a1a03911e.png)

- [公式ページ](https://pm2.keymetrics.io/)
    > **ADVANCED, PRODUCTION PROCESS MANAGER FOR NODE.JS**
    PM2 is a daemon process manager that will help you manage and keep your application online 24/7

    - "deamon process manager" が分からない方向けの良教材  
    [Linux リテラシ - 第4回 デーモン](https://rat.cis.k.hosei.ac.jp/article/rat/linuxliteracy/2005/daemon.html)

- node.js 向けのプロセス管理ツール
    - 色々できる
        1. [BEHAVIOR CONFIGURATION](https://pm2.keymetrics.io/docs/usage/application-declaration/)
        2. [SOURCE MAP SUPPORT](https://pm2.keymetrics.io/docs/usage/source-map-support)
        3. [CONTAINER INTEGRATION](https://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/)
        4. [WATCH & RELOAD](https://pm2.keymetrics.io/docs/usage/watch-and-restart/)
        5. [LOG MANAGEMENT](https://pm2.keymetrics.io/docs/usage/log-management/)
        6. [MONITORING](https://pm2.keymetrics.io/docs/usage/monitoring/)
        7. [MODULE SYSTEM](https://pm2.keymetrics.io/docs/advanced/pm2-module-system/)
        8. [MAX MEMORY RELOAD](https://pm2.keymetrics.io/docs/usage/monitoring/#max-memory-restart)
        9. [CLUSTER MODE](https://pm2.keymetrics.io/docs/usage/cluster-mode/)
        10. [HOT RELOAD](https://pm2.keymetrics.io/docs/usage/cluster-mode/#reload-without-downtime)
        11. [DEVELOPMENT WORKFLOW](https://pm2.keymetrics.io/docs/usage/pm2-development/)
        12. [STARTUP SCRIPTS](https://pm2.keymetrics.io/docs/usage/startup/)
        13. [DEPLOYMENT WORKFLOW](https://pm2.keymetrics.io/docs/usage/deployment/)
        14. [PAAS COMPATIBLE](https://pm2.keymetrics.io/docs/usage/use-pm2-with-cloud-providers/)
        15. [KEYMETRICS MONITORING](https://pm2.keymetrics.io/docs/usage/monitoring/#keymetrics-monitoring)
        16. [API](https://pm2.keymetrics.io/docs/usage/pm2-api/)


    - 今回は「Next のプロセスの起動か再読み込みをする」という動きをしてもらっている
    
- Deployment Workflow 機能もあるので、rsync でやっているコード移設や npm 処理周りも PM2 で管理できるがまだやっていない
    - PM2 deployment ドキュメント  
    [https://pm2.keymetrics.io/docs/usage/deployment/](https://pm2.keymetrics.io/docs/usage/deployment/)
    - [このPullRequest](https://github.com/yoshikouki/yoshikouki.net/pull/8)で実装中だが、発表に間に合わなかった   
      

# まとめ

GitHub Actions の便利アクションやコンテナ技術を使用することで、実際の処理やデプロイ先の環境を意識することなく簡単にCI/CDできることができますが、今回のこの実装で**「便利なラッパーが実際どういう処理をしているのか」**ということが学べました。

Capistrano のデフォルト挙動では、アップロードしたディレクトリへのシンボリックリンクを公開パスに配置するという頭良すぎる工夫を知ることもできて（なのでロールバックする場合は以前アップロードしたディレクトリにシンボリックリンクを張り直すだけ）、**「便利なライブラリをただ利用するだけでなく、その仕組がどうなっているのかまで理解すると応用できる」**というエンジニアとしての重要な気付きも得ることができたのでした。


P.S.
**[Ruby on Rails で最速CDする記事](https://qiita.com/yoshikouki/items/13d75b60c1f2d63ec10a)**も上げましたので、そちらもよろしくお願いします。本来このLTでやろうと思っていた内容でした（やれよ）