LoginSignup
7
3

【無料】爆速でAWS EC2でNode.jsの自動デプロイ環境を作る

Last updated at Posted at 2023-09-12

Discord Botを無料で24/7(休まず常時稼働)させたいが、Vercelなどの無料サービスはその要件を満たさなかったので、AWSのEC2でやることにした。私と同じお金を使わずに何かをしたい人に向けてますので、感想等くださるとありがたいです。
特に誤った内容があると思われた方は、ぜひコメントをお願いいたします。

この記事でやること

  • 【大目的】「無料」で、Node.jsのサーバを「爆速」で作る。
    • GitHubリポジトリのNode.jsのアプリを自動でAWS EC2インスタンスにデプロイする環境を作る。
    • 本番環境のコードのブランチ(production)を作り、ソースコードがプッシュ(マージなど)されると、自動で最新のコードでNode.jsプロセスがスタートするようにする。
    • また、手動でデプロイ等の操作を実行することもでき、ここでアプリケーションの起動・停止を操作することもできる!!

※全部無料でやります!!! ←重要

想定読者

  • Git, GitHubは使える!
  • Node.jsは使える!
  • AWSは全くわからない!(丸わかりでもいい)
  • GitHub Actionsは使ったことない!(あってもいい)
  • めんどくさいことはいいからとにかく動かしたい!
  • AWS/GitHub Action/ssh などの学習もできたら嬉しい!

重要事項と免責

  • AWSアカウント作成後12ヶ月のみの無料利用枠を使う(Amazon EC2 750時間無料)。
  • EC2インスタンスを停止・再起動すると、パブリックIP/ホスト名が変更される(Elastic IPの課金をケチっている)。
    →EC2のパブリックIPが変わるたびにリポジトリの環境変数secrets.EC2_HOSTの値の再設定が必要。
  • セキュリティ上の問題はないはずだが万全ではない(不備に気づいた方はコメントで教えていただきたいです)。

構成

  1. GitHubリポジトリ
    • mainブランチとproductionブランチがあるものとする。
    • Github Actionsを設定して、SSH接続でAWS EC2インスタンスにソースコードをアップロードしてnpm startする
  2. AWS EC2インスタンス
    • イメージ: Amazon Linux 2023 AMI
    • インスタンスタイプ: t2.microタイプ

前提条件

  • AWSアカウント作成後12ヶ月間のユーザのみ、EC2インスタンスが月750時間無料で使える。そうでない方はその分課金されます。
  • この記事では、リポジトリ名は「sample-app」とする。以下のディレクトリ構造とする(まるっきり一緒じゃなくても読み替えれば大丈夫)。
    sample-app
    ├── .github
    │   └── workflows
    │       └── main.yml
    ├── dist
    │   └── (ビルド済みファイルがここに)
    ├── package-lock.json
    ├── package.json
    ├── src
    │   └── (ソースコードがここに)
    └── tsconfig.json
    

説明について

ここから、全く同じ手順を2回記述します。
1回目は手順のみで、2回目は解説付きでゆっくりやります。
とりあえず爆速でやっちゃいたい方は1回目の手順、解説付きで理解したい方は下の解説付き手順を読んでください。

作業手順 (解説なし。爆速。)

AWSのアカウントを作ろう編

  1. AWSのアカウント作成ページでアカウントを作成する。
  2. 機能を利用するには支払い方法の登録が必要なため、恐る恐る登録する。
  3. おしまい。

EC2インスタンスを作ろう編

  1. AWSにログインする。右上に「バージニア北部」などの地名が書いてあるので、好きなエリア(リージョン)を選択する。このサーバを稼働させたいエリア(おそらく東京か大阪)を選べば問題ない。
  2. 左上の検索欄で「EC2」と入力してEnter。EC2ダッシュボードページにアクセスする。
  3. 「インスタンスを起動」というオレンジのボタンを押下。インスタンス新規作成画面に遷移する。
  4. 「名前」の欄にはこのインスタンスの用途などがわかり識別できる名称をつけよう。
  5. 「アプリケーションおよび OS イメージ」の欄は、先頭の「Amazon Linux 2023 AMI」を選ぼう。
  6. 「インスタンスタイプ」は、「無料利用枠の対象」であるt2.microを選ぼう。
  7. 「キーペア (ログイン)」の、「新しいキーペアの作成」を押下。ポップアップが出てくる。
  8. 「キーペア名」を半角で任意に入力した後、「キーペアのタイプ: RSA」、「プライベートキーファイル形式: .pem」を選択して「キーペアを作成」を押下する。すると、「キーペア名.pem」というファイルがダウンロードされるので、これを無くさず漏らさず保管すること。あとで使う。
  9. 「キーペア (ログイン)」に今作成したキーを選択する。
  10. 「ファイアウォール (セキュリティグループ)」は、「セキュリティグループを作成する」を選択する(自分もよくわかってない)。
  11. 「からの SSH トラフィックを許可する」にチェックを入れておく。
  12. 「インターネットからの HTTPS トラフィックを許可」は、Webサーバなどで外部のリクエストを受け付ける必要があるならチェックを入れる。
  13. 「インスタンスを起動」のオレンジのボタンを押下。これでインスタンスが起動する。
  14. おしまい

EC2インスタンスのセットアップ編

以下の方法で、EC2インスタンスのシェルにアクセスしよう。そこで、必要なソフトのインストールなどを行う。
【Web上のコンソールで接続】

  1. EC2のインスタンスのページから、さっき起動したインスタンスのインスタンスIDのリンクを押下。「インスタンス概要」ページに飛ぶので、右上あたりにある「接続」を押下。インスタンス接続画面に遷移する。
  2. ユーザ名は未入力(ec2-user)のまま、オレンジの「接続」ボタンを押下。Webコンソールが起動する。

Voltaインストール

  1. EC2インスタンスのコンソールで以下のコマンドを実行してVoltaをインストール(不安な人は、Volta公式サイトのLinux Installationにあるインストールのコマンドをコピればよい)
    curl -sSLf https://get.volta.sh | bash
    
  2. ターミナルを再起動(セッション再接続)するとvoltaコマンドが使えるようになる。

リポジトリの「Actions secrets and variables」 の登録

この後作成していくGitHub Actionsで利用する秘匿変数を登録する。
GitHubリポジトリのページの「settings」タブから「Secrets and variables/Actions」を選択して、EC2へのアクセス情報と、インスタンスの作成時にダウンロードされた「キーペア名.pem」ファイルの内容(秘密鍵)を登録しよう。

Action secretsに登録するキー/バリューのイメージ
EC2_HOST: xxxxxxxx # EC2インスタンスのホスト名(パブリック IPv4 アドレス や パブリック IPv4 DNS など)
EC2_USER: ec2-user # EC2インスタンスにログインするユーザー名(この例ではec2-user)
PRIVATE_KEY: xxxxxxxx... # EC2インスタンスのauthorized_keysに登録した公開鍵と対応する秘密鍵
SAMPLE_ENV_A : xxxxxxxx # Node.jsアプリケーションで使いたい環境変数(npm start時にセットしたい環境変数)の例
SAMPLE_ENV_B : xxxxxxxx # Node.jsアプリケーションで使いたい環境変数(npm start時にセットしたい環境変数)の例
SAMPLE_ENV_C : xxxxxxxx # Node.jsアプリケーションで使いたい環境変数(npm start時にセットしたい環境変数)の例

GitHub Actionsで実行するデプロイのタスクを作成する

リポジトリに.github/workflows/main.ymlを作成しよう。ファイル名は自由だが、ディレクトリ名は決して間違えてはいけない。
(VSCodeで編集しているなら、GitHub Actionsという拡張機能をインストールすると良い。構文やプロパティにエラーがあると補完してくれる)

ymlファイルに、以下のコードをコピペしよう。

.github/workflows/main.yml
name: Node.js Deploy Pipeline

on:
  push:
    branches:
      - production
  workflow_dispatch:
    # 手動実行時は、選択したrunTypeによって実行するjobを変更する
    inputs:
      runType:
        type: choice
        required: true
        description: ''
        default: 'デプロイ'
        options:
          - 'デプロイ'
          - 'アプリケーション 再起動'
          - 'アプリケーション 停止'
jobs:
  # 現在動作しているNode.jsプロセス停止するjob。どの処理でも必ず最初に実行する。
  kill_process:
    runs-on: ubuntu-latest
    steps:
      - name: Update OpenSSL
        run: sudo apt-get update && sudo apt-get install -y openssl

      - name: Down Process
        env:
          HOST: ${{ secrets.EC2_HOST }}
          USER: ${{ secrets.EC2_USER }}
          PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
        run: |
          : # 秘密鍵ファイルを一時的に作成し、権限を最小のものにする(エラーを出さないため)
          echo "$PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem
          : # EC2インスタンスにsshログインして、"node"という名前のプロセスを全てkillする
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "pkill ^node$ &"
  # productionブランチのソースコードをビルドしてEC2インスタンスにアップロードし、Node.jsのアプリケーションを起動するjob。
  deploy_and_start_process:
    # productionブランチにpushされた時または手動実行メニューで「デプロイ」が選択された時に実行する。
    if: github.event_name == 'push' || github.event.inputs.runType == 'デプロイ'
    # kill_processが完了した後に実行するという指定。
    needs: kill_process
    runs-on: ubuntu-latest

    steps:
      - name: Update OpenSSL
        run: sudo apt-get update && sudo apt-get install -y openssl

      - name: Checkout code to production branch
        uses: actions/checkout@v4
        with:
          ref: production

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18.17.1

      - name: Install Dependencies and Build
        env:
          NODE_ENV: production
        run: |
          npm ci
          npm run build

      - name: Deploy to EC2
        env:
          HOST: ${{ secrets.EC2_HOST }}
          USER: ${{ secrets.EC2_USER }}
          PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
          SAMPLE_ENV_A: ${{ secrets.SAMPLE_ENV_A }}
          SAMPLE_ENV_B: ${{ secrets.SAMPLE_ENV_B }}
          SAMPLE_ENV_C: ${{ secrets.SAMPLE_ENV_C }}
        run: |
          echo "$PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem
          : # EC2インスタンスの/home/ec2-user/sample-appディレクトリにアプリケーションがあるので、まずは削除してからアップロードする。ログファイル用のディレクトリもなければ作成する。
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "rm -r -f /sample-app && mkdir /home/ec2-user/sample-app &"
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "mkdir -p /home/ec2-user/sample-app-log &"
          : # リポジトリ内の全てのファイルを一括で送信するのではなく、サブディレクトリ・ファイル単位で送信する。ここは適宜読み替えること。
          echo "put -r ./dist ./sample-app/dist" | sftp -i private_key.pem $USER@$HOST
          echo "put ./package.json ./sample-app/package.json" | sftp -i private_key.pem $USER@$HOST
          echo "put ./package-lock.json ./sample-app/package-lock.json" | sftp -i private_key.pem $USER@$HOST
          : # EC2インスタンスで対象のNode.jsバージョンをインストールし、アプリケーションの依存関係をインストールする。
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "volta install node@18.17.1 && npm ci --production --prefix /home/ec2-user/sample-app"
          : # Node.jsプロセスを起動するコマンドを実行。
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "NODE_ENV=production SAMPLE_ENV_A=$SAMPLE_ENV_A SAMPLE_ENV_B=$SAMPLE_ENV_B SAMPLE_ENV_C=$SAMPLE_ENV_C nohup npm run start --prefix /home/ec2-user/sample-app > /home/ec2-user/sample-app-log/nohup-$(date +%Y%m%d%H%M).log 2>&1 &"
  # EC2インスタンスにあるアプリケーションを起動するjob。
  just_start_process:
    # productionブランチにpushされた時または手動実行メニューで「アプリケーション 再起動」が選択された時のみ実行する。
    if: github.event.inputs.runType == 'アプリケーション 再起動'
    # kill_processが完了した後に実行するという指定。
    needs: kill_process
    runs-on: ubuntu-latest
    steps:
      - name: Update OpenSSL
        run: sudo apt-get update && sudo apt-get install -y openssl

      - name: Restart Application
        env:
          HOST: ${{ secrets.EC2_HOST }}
          USER: ${{ secrets.EC2_USER }}
          PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
          SAMPLE_ENV_A: ${{ secrets.SAMPLE_ENV_A }}
          SAMPLE_ENV_B: ${{ secrets.SAMPLE_ENV_B }}
          SAMPLE_ENV_C: ${{ secrets.SAMPLE_ENV_C }}
        # EC2インスタンスにsshログインして、アプリケーションを起動するコマンドを実行する。
        # コマンドの前で、このコマンドにしか反映されない環境変数を定義している。"変数名=値"の形式で羅列する。process.envで使われる値をここで環境変数としてセットする。
        # このssh接続が切れてもプロセスを継続して欲しいので、バックグラウンドで実行してもらうためコマンド末尾に"&"をつけ、nohubコマンドを使い実行する。
        # 出力ログを"/home/ec2-user/sample-app-log/nohup-YYYYmmdd.log"というファイルに保存するように設定している。
        run: |
          echo "$PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "NODE_ENV=production SAMPLE_ENV_A=$SAMPLE_ENV_A SAMPLE_ENV_B=$SAMPLE_ENV_B SAMPLE_ENV_C=$SAMPLE_ENV_C nohup npm run start --prefix /home/ec2-user/sample-app > /home/ec2-user/sample-app-log/nohup-$(date +%Y%m%d%H%M).log 2>&1 &"

このymlファイル内のsample-appを自身のリポジトリ名にしたり、環境変数名を変更したり追加したり、適宜自身のアプリに合わせて記述を調整してください。
ここまでで作業は完了。このコードをプッシュしたら、早速動作確認をしよう。

動作確認・使い方について

GitHubのリポジトリのページ上部の「Actions」タブを押下すると、GitHub Actionの実行状況に関するページが出てくる。この「All workflows」の欄に、実行した全てのActionの履歴と、実行中の状況が確認できる(削除・キャンセルもできる)。

また、「All workflows」の下の「Node.js Deploy Pipeline」を押下すると、手動実行(Run workflow)のトグルが出てくるので、これを押下してrunType(実行メニュー)を選び、手動実行することができる。

  • デプロイ...productionブランチにコードがプッシュされた時と同じ動作になる。アプリケーションを停止 → productionブランチのソースコードをEC2インスタンスにアップロード → パッケージのインストール → アプリケーションの起動が行われる。
  • アプリケーション 再起動...アプリケーションを停止 → GitHub Actions secrets の最新の環境変数でアプリケーションを再起動する。実行前からアプリケーションが停止していても問題なく動く(起動だけする)。
  • アプリケーション 停止...実行中のNode.jsアプリケーションを停止(kill)する。

それでは、手動実行でもよいし、productionブランチにマージするでもよいので、GitHub Actionをトリガーして、Actionsランナーの動きを観察してみよう。緑のチェックが入って正常終了したことを確認したら、大喜びしてください。10分以上経っても完了しない場合はおそらく失敗しているので、ランナーを中断して何が原因だったかを実行ログを見て判断してください。
(私はnode_modulesを全てそのまま転送しようとしていいて、実行を中断したことがあります)

作業(解説なし。)はここまで!

これで全て終わりです!Your Awsome Applicationは世界に羽ばたきました!!

作業手順 (解説あり。)

AWSのアカウントを作ろう編

AWSとは・・・?

Amazonが提供するクラウドサービス。インターネットに繋げてコンピュータをどうこうする(Webサイトを公開するなど、ホスト側の何か)には、パソコン代や回線代、電気代に場所代などの料金を払った上でセキュリティなどを意識してようやく使えるのだが、AWSはそれをAmazonがぜーんぶ代わりにやってあげます、その代わりジェフべゾスにお金払ってね、なサービス。コンピュータができることをほぼ全部提供しているので、覚えきれないほど多機能。
MicrosoftはAzure、GoogleはGCPという名前で同じようなサービスをしている。

  1. AWSのアカウント作成ページでアカウントを作成する。
  2. 機能を利用するには支払い方法の登録が必要なため、恐る恐る登録する。
  3. おしまい。

EC2インスタンスを作ろう編

(EC2インスタンスって何・・・?)

EC2はAWSの目玉機能の1つで、仮想環境で作られたLinuxとかWindows/MacOSなどのコンピュータをAWSのサーバ上に作らせてもらって、自由に使わせてもらえるサービス。インスタンスとは、作られた仮想マシンの実体のことを指す。クラス(設計図)とそのインスタンス(実体)のような関係。EC2の仮想マシンは、いくつか用意されてる設計図(イメージ)を使って作られるので、作られた仮想マシンのことをインスタンスと呼んでいる。

(リージョンって何・・・?)

AWSみたいなクラウドサービスをやるには、大量のサーバが必要。そのため、AWSは世界各地にでっかいデータセンタを建てている。そのデータセンタのエリアの括りをリージョンという。例えば東京リージョンを選択すると、東京にいくつかあるデータセンタのAWSサーバのどれかを利用することになる。そのため、「このデータは国内で管理してます」と謳うために東京リージョンを選んだり(法的な理由で必要な場合もある)、東京一帯が停電してもサービスを続行できるよう東京とサンパウロの2箇所でサーバを立てるといった活用法がある。ちなみに、具体的なデータセンタの場所は非公開にしているらしい。テロられないために。

  1. AWSにログインする。右上に「バージニア北部」などの地名が書いてあるので、好きなエリア(リージョン)を選択する。このサーバを稼働させたいエリア(おそらく東京か大阪)を選べば問題ない。
  2. 左上の検索欄で「EC2」と入力してEnter。EC2ダッシュボードページにアクセスする。
  3. 「インスタンスを起動」というオレンジのボタンを押下。インスタンス新規作成画面に遷移する。
  4. 「名前」の欄にはこのインスタンスの用途などがわかり識別できる名称をつけよう。
  5. 「アプリケーションおよび OS イメージ」の欄は、「無料利用枠の対象」かつ「Linux/Unix」ベースのOSを選択しよう。とりあえず先頭の「Amazon Linux 2023 AMI」を選べば間違いない。
  6. 「インスタンスタイプ」は、「無料利用枠の対象」であるt2.microを選ぼう。
  7. 「キーペア (ログイン)」の、「新しいキーペアの作成」を押下。ポップアップが出てくる。
  8. 「キーペア名」を半角で任意に入力した後、「キーペアのタイプ: RSA」、「プライベートキーファイル形式: .pem」を選択して「キーペアを作成」を押下する。すると、「キーペア名.pem」というファイルがダウンロードされるので、これを無くさず漏らさず保管すること。あとで使う。
  9. 「キーペア (ログイン)」に今作成したキーを選択する。
  10. 「ファイアウォール (セキュリティグループ)」は、「セキュリティグループを作成する」を選択する(自分もよくわかってない)。
  11. 「からの SSH トラフィックを許可する」にチェックを入れておく。
  12. 「インターネットからの HTTPS トラフィックを許可」は、Webサーバなどで外部のリクエストを受け付ける必要があるならチェックを入れる。
  13. 「インスタンスを起動」のオレンジのボタンを押下。これでインスタンスが起動する。
  14. おしまい

EC2インスタンスのセットアップ編

以下の方法のどちらかで、EC2インスタンスのシェルにアクセスしよう。そこで、必要なソフトのインストールなどを行う。
【Web上のコンソールで接続】

  1. EC2のインスタンスのページから、さっき起動したインスタンスの「インスタンスID」を押下。そのインスタンスの「インスタンス概要」ページに飛ぶので、右上あたりにある「接続」を押下。インスタンス接続画面に遷移する。
  2. ユーザ名は未入力(ec2-user)のまま、オレンジの「接続」ボタンを押下。Webコンソールが起動する。

【ローカルPCでSSH接続】

  1. EC2のインスタンスのページから、さっき起動したインスタンスのインスタンスIDのリンクを押下。「インスタンス概要」ページに遷移する。
  2. そこに書かれている「パブリック IPv4 アドレス」または「パブリック IPv4 DNS」をコピー。以下で「ホスト名」と記述された箇所で使う。
  3. 自分のローカルPCで、以下のコマンドを実行する。
    ssh -i {さっきDLした「キーペア名.pem」のパス} ec2-user@{ホスト名}
    
  4. 初回は「何やこの秘密鍵とホストの組み合わせ、ワシは知らんで!ホンマに接続するんか!?(英語)」で聞かれるので、yes(せやで)と入力しよう。EC2インスタンスのシェルが起動する。

Voltaインストール

(Voltaって何・・・?)

こちらに素晴らしいVoltaとは何かの解説記事があるので、ここでは説明しません。
これを今回使う理由は、コマンド一発でEC2インスタンスのNode.jsバージョンを切り替えられるようにしたいからです。
また、この記事にあるように、package.jsonにVoltaでNode.jsとnpm/yarnのバージョンを記憶しておくとよりよいでしょう。

  1. EC2インスタンスのコンソールで以下のコマンドを実行してVoltaをインストール(不安な人は、Volta公式サイトのLinux Installationにあるインストールのコマンドをコピればよい)
    curl -sSLf https://get.volta.sh | bash
    
  2. ターミナルを再起動(セッション再接続)するとvoltaコマンドが使えるようになる。

許可された公開鍵の登録(やらなあかんけどやらんでいい作業)

GitHub自動デプロイには、GitHub Actionのタスクランナー(処理を実行する専用のコンピュータ)からEC2インスタンスに対してsshで接続する必要がある。これはGitHub Actionのタスクランナーがクライアント側になるため、GitHub Action側に秘密鍵EC2側に公開鍵のセットをそれぞれ登録しなければいけない。

コンピュータが操作される側(sshでコマンドを受け取って実行する側)の場合、sshでは~User/.ssh/authorized_keysというテキストファイルに、利用される公開鍵・秘密鍵ペアの「公開鍵」の方が記述されている必要がある。
そのため、これを登録する必要があるのだが、今回はその必要はない。インスタンス作成時に、自動で登録されているからだ。
EC2では、インスタンス作成時に登録するキーペアの公開鍵が、自動でauthorized_keysに記載された状態で登録されている。だから、さっきの【ローカルPCでSSH接続】の手順で接続できたのだ。

本来なら、このファイルに公開鍵を記載する作業が必要だったが、今回はスキップ。

リポジトリの「Actions secrets and variables」 の登録

この後作成していくGitHub Actionsで利用する秘匿変数を登録する。
GitHubリポジトリのページの「settings」タブから「Secrets and variables/Actions」を選択して、EC2へのアクセス情報と、インスタンスの作成時にダウンロードされた「キーペア名.pem」の内容(秘密鍵)を登録しよう。

Action secretsに登録するキー/バリューのイメージ
EC2_HOST: xxxxxxxx # EC2インスタンスのホスト名(パブリック IPv4 アドレス や パブリック IPv4 DNS など)
EC2_USER: ec2-user # EC2インスタンスにログインするユーザー名(この例ではec2-user)
PRIVATE_KEY: xxxxxxxx... # EC2インスタンスのauthorized_keysに登録した公開鍵と対応する秘密鍵
SAMPLE_ENV_A : xxxxxxxx # Node.jsアプリケーションで使いたい環境変数(npm start時にセットしたい環境変数)の例
SAMPLE_ENV_B : xxxxxxxx # Node.jsアプリケーションで使いたい環境変数(npm start時にセットしたい環境変数)の例
SAMPLE_ENV_C : xxxxxxxx # Node.jsアプリケーションで使いたい環境変数(npm start時にセットしたい環境変数)の例

GitHub Actionsで実行するデプロイのタスクを作成する

それでは、GitHub Actionsを利用していこう。今回は、以下の3つのjobを作成する(job=実行する処理のまとまりのこと)。

A. アプリケーションを停止するjob
B. productionブランチのコードをビルドして、EC2インスタンスに転送。その後、アプリケーションを開始するjob
C. アプリケーションを開始するだけのjob

3つに分けることで、以下のように3種類の機能を作成できる。

  1. 現在起動しているアプリケーションを停止する - Aのみ実行
  2. アプリケーションを再起動 - A, Cを実行
  3. アプリケーションを最新のコードで再度デプロイ・起動する - A, Bを実行

GitHub Actionsは、手動実行できるように設定できる。その時に、アプリの起動・停止などの操作もできるようにすると都合がいいのでついでにやる。

リポジトリに.github/workflows/main.ymlを作成しよう。ファイル名は自由だが、ディレクトリ名は決して間違えてはいけない。 GitHub様が、このディレクトリにあるファイルをActions用のymlファイルだと認識なさるからだ。

そして、VSCodeで編集しているなら、ゼッテーにGitHub Actionsという拡張機能をインストールすること!!
構文やプロパティにエラーがあると補完してくれる。

ymlファイルに、このように記述しよう。

.github/workflows/main.yml
name: Node.js Deploy Pipeline

on:
  push:
    branches:
      - production
  workflow_dispatch:
    # 手動実行時は、選択したrunTypeによって実行するjobを変更する
    inputs:
      runType:
        type: choice
        required: true
        description: ''
        default: 'デプロイ'
        options:
          - 'デプロイ'
          - 'アプリケーション 再起動'
          - 'アプリケーション 停止'

name:には、このymlファイルで定義するタスクの名前を指定する。Actionsでの操作画面の表示に使われる。
on:では、このタスクを実行するトリガー(きっかけ)について定義する。
workflow_dispatch:と記述すると、Actionsタブでタスクを手動実行できるようになる。その下のプロパティは、手動実行時のオプション。

ymlファイルに、さらにこのように追記しよう。

.github/workflows/main.yml
jobs:
  # 現在動作しているNode.jsプロセス停止するjob。どの処理でも必ず最初に実行する。
  kill_process:
    runs-on: ubuntu-latest
    steps:
      - name: Update OpenSSL
        run: sudo apt-get update && sudo apt-get install -y openssl

      - name: Down Process
        env:
          HOST: ${{ secrets.EC2_HOST }}
          USER: ${{ secrets.EC2_USER }}
          PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
        run: |
          : # 秘密鍵ファイルを一時的に作成し、権限を最小のものにする(エラーを出さないため)
          echo "$PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem
          : # EC2インスタンスにsshログインして、"node"という名前のプロセスを全てkillする
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "pkill ^node$ &"

ここでは、jobの定義をしている。まずは1つ目のkill_processだ。これは、先ほどの説明にある「A. アプリケーションを停止するjob」の定義だ。

  • runs-on:では、どのOSでタスクを実行するかを定義できる。
  • steps:に、実行するjobの具体的な処理を記述する。GitHub Actions用に作られたライブラリを指定して実行することもできる。
  • steps:内のrun:では、それぞれのstepで実行するコマンドを記述する。1行に1コマンドと認識され、改行することで複数コマンドを定義する。このrun:ブロック内でのコメントアウトは: #
  • steps:内のenv:では、このstep内でセットされる環境変数を定義している。Linuxコマンド上では環境変数の呼び出しは$KEYという形で使われる。

Update OpenSSLのstepでは、GitHub Action実行時に作成され、このjobが実行されるインスタンス(ubuntu)のaptパッケージの全更新とOpenSSLのインストールを行う。
Down Processのstepでは、まずubuntu jobインスタンス内にEC2インスタンスに通信する時に使う秘密鍵ファイルを作成。その後、このファイルの権限を所有者のみ読み書き可能に変更する。ファイルの権限を最小に絞らないと、使うときにこんなエラーが出る。

It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "./private_key.pem": bad permissions

そして、最後の行がプロセスを停止するコマンドだ。sshコマンドで、EC2インスタンスに接続してコマンドを実行している。

  • -oStrictHostKeyChecking=noオプションは、シェル上で秘密鍵の確認入力を求めないようにするオプション。
  • -i private_key.pemオプションは、使用する秘密鍵ファイルのパスを指定している。
  • $USER@$HOSTでは、接続するユーザ名とホスト名を"@"でつないで定義している。
  • 最後の""の中に、EC2インスタンスで実行したいコマンドを記述する(pkill ^node$ &)。
    • pkillコマンドは、指定した名前のプロセスを全て強制終了する、というコマンド。引数でnodeという名前のプロセスを指定している。Node.jsのプロセス名はどれもnodeなので、多重起動していたとしてもこれで全て終了して、まっさらの状態にできる。
    • ^node$という記載は、正規表現でnodeとの完全一致を指定している。
    • コマンド末尾の&は、実行結果などは表示せずバックグラウンドで実行してね、という記述。

ymlファイルに、"deploy_and_start_process"のjobを以下のように追記しよう。

jobs:
  kill_process:
    # 省略

  # productionブランチのソースコードをビルドしてEC2インスタンスにアップロードし、Node.jsのアプリケーションを起動するjob。
  deploy_and_start_process:
    # productionブランチにpushされた時または手動実行メニューで「デプロイ」が選択された時に実行する。
    if: github.event_name == 'push' || github.event.inputs.runType == 'デプロイ'
    # kill_processが完了した後に実行するという指定。
    needs: kill_process
    runs-on: ubuntu-latest

    steps:
      - name: Update OpenSSL
        run: sudo apt-get update && sudo apt-get install -y openssl

      - name: Checkout code to production branch
        uses: actions/checkout@v4
        with:
          ref: production

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18.17.1

      - name: Install Dependencies and Build
        env:
          NODE_ENV: production
        run: |
          npm ci
          npm run build

      - name: Deploy to EC2
        env:
          HOST: ${{ secrets.EC2_HOST }}
          USER: ${{ secrets.EC2_USER }}
          PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
          SAMPLE_ENV_A: ${{ secrets.SAMPLE_ENV_A }}
          SAMPLE_ENV_B: ${{ secrets.SAMPLE_ENV_B }}
          SAMPLE_ENV_C: ${{ secrets.SAMPLE_ENV_C }}
        run: |
          echo "$PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem
          : # EC2インスタンスの/home/ec2-user/sample-appディレクトリにアプリケーションがあるので、まずは削除してからアップロードする。ログファイル用のディレクトリもなければ作成する。
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "rm -r -f /sample-app && mkdir /home/ec2-user/sample-app &"
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "mkdir -p /home/ec2-user/sample-app-log &"
          : # リポジトリ内の全てのファイルを一括で送信するのではなく、サブディレクトリ・ファイル単位で送信する。ここは適宜読み替えること。
          echo "put -r ./dist ./sample-app/dist" | sftp -i private_key.pem $USER@$HOST
          echo "put ./package.json ./sample-app/package.json" | sftp -i private_key.pem $USER@$HOST
          echo "put ./package-lock.json ./sample-app/package-lock.json" | sftp -i private_key.pem $USER@$HOST
          : # EC2インスタンスで対象のNode.jsバージョンをインストールし、アプリケーションの依存関係をインストールする。
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "volta install node@18.17.1 && npm ci --production --prefix /home/ec2-user/sample-app"
          : # Node.jsプロセスを起動するコマンドを実行。
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "NODE_ENV=production SAMPLE_ENV_A=$SAMPLE_ENV_A SAMPLE_ENV_B=$SAMPLE_ENV_B SAMPLE_ENV_C=$SAMPLE_ENV_C nohup npm run start --prefix /home/ec2-user/sample-app > /home/ec2-user/sample-app-log/nohup-$(date +%Y%m%d%H%M).log 2>&1 &"

この2つ目のdeploy_and_start_processjobは、先ほどの説明にある「B. productionブランチのコードをビルドして、EC2インスタンスに転送。その後、アプリケーションを開始するjob」の定義だ。

  • if:では、このjobを実行する条件を定義する。github.eventというこのymlで利用できる変数の中に、判断に必要な情報が入っている。
  • needs:では、このjobを実行するために必要なjobを定義している。先ほどのkill_processを実行した後にこのjobを実行したいので、ここにkill_processと記述する。
  • Checkout code to production branchのstepでは、Actions jobインスタンス内のリポジトリをproductionブランチにチェックアウトする。
  • uses:で、どのActionsライブラリを使うかを指定できる。ブランチをチェックアウトするためのライブラリactions/checkout@v4を指定する。それぞれのライブラリの使い方はドキュメントに書いてある。
  • Set up Node.jsのstepで、jobインスタンス内にNode.jsをインストールする。"actions/setup-node@v3"ライブラリを利用する。
  • Install Dependencies and Buildのstepで、npmパッケージをインストールしてソースコードをビルドする。ビルドしたコードは/distディレクトリに配置される。
  • Deploy to EC2のstepで、ソースコードをEC2インスタンスにアップロード & アプリケーションを開始している。
    • アプリケーションは、/home/ec2-user/sample-appディレクトリに配置する。まずは、このディレクトリを空にするため、rmコマンドでディレクトリごと削除してからのディレクトリを作っている。
    • 後述するログファイルの配置場所を作成する。/home/ec2-user/sample-app-logディレクトリを作成する。-pオプションで、このディレクトリが存在しないときだけ実行。
    • 次に、sftpコマンドでファイルを送信するのだが、これがやや複雑である。sftpは、sshでファイル転送を行うためのコマンド。これを実行すると、sftpコマンド内のシェルが起動し、その中でputgetなどの専用コマンドが実行できる。これを1行で行うため、echo {sftpシェルで実行するコマンド} | sftp -i 秘密鍵 user@hostという形式で記述する。putコマンドに-rオプションをつけることで、フォルダごと転送ができる。
    • 次に、sshでEC2インスタンスでVoltaコマンドを実行する。これは、リリース対象のNode.jsをEC2インスタンスにインストールするコマンドである。
    • その後、npmパッケージをインストールする。--productionフラグをつけないと、開発環境のみ必要なdevDependenciesのパッケージもインストールされてしまう。
    • 最後に、sshでEC2インスタンスでnpm run startを実行してアプリケーションを開始するのだが、これもかなり複雑である。
      • まず、このコマンド内でだけ使う環境変数をKEY=$KEYの形で登録する。これらは、node.jsプロセスの実行中に参照される。環境変数の登録は、実行したいコマンドの前に記述する。
      • そして、ssh接続が切れてもプロセスがずっと続いて欲しいので"nohup"コマンドを利用する。これで、シェルの切断時に発される"SIGHUP"シグナルを無視するようになる。また、>で繋いでシェルの出力をどこに変更するかを定義できる。これで、"nohup-YYYYmmdd.log"という現在日時の名前のログファイルにシェルの出力が保存されるようになる。この出力は、実行のたびにテキストファイルを上書きしてしまうため、実行日時のログファイル名にしている。
      • 末尾付近にある2>&1は、シェル出力がエラー出力の時も普通に出力してね、の記号。
      • 末尾の&は、このコマンドをバックグラウンドで実行してね、の合図。

最後に、ymlファイルに、"just_start_process"のjobを以下のように追記しよう。

jobs:
  kill_process:
    # 省略
  deploy_and_start_process:
    # 省略

  # EC2インスタンスにあるアプリケーションを起動するjob。
  just_start_process:
    # productionブランチにpushされた時または手動実行メニューで「アプリケーション 再起動」が選択された時のみ実行する。
    if: github.event.inputs.runType == 'アプリケーション 再起動'
    # kill_processが完了した後に実行するという指定。
    needs: kill_process
    runs-on: ubuntu-latest
    steps:
      - name: Update OpenSSL
        run: sudo apt-get update && sudo apt-get install -y openssl

      - name: Restart Application
        env:
          HOST: ${{ secrets.EC2_HOST }}
          USER: ${{ secrets.EC2_USER }}
          PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
          SAMPLE_ENV_A: ${{ secrets.SAMPLE_ENV_A }}
          SAMPLE_ENV_B: ${{ secrets.SAMPLE_ENV_B }}
          SAMPLE_ENV_C: ${{ secrets.SAMPLE_ENV_C }}
        # EC2インスタンスにsshログインして、アプリケーションを起動するコマンドを実行する。
        # コマンドの前で、このコマンドにしか反映されない環境変数を定義している。"変数名=値"の形式で羅列する。process.envで使われる値をここで環境変数としてセットする。
        # このssh接続が切れてもプロセスを継続して欲しいので、バックグラウンドで実行してもらうためコマンド末尾に"&"をつけ、nohubコマンドを使い実行する。
        # 出力ログを"/home/ec2-user/sample-app-log/nohup-YYYYmmdd.log"というファイルに保存するように設定している。
        run: |
          echo "$PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem
          ssh -o StrictHostKeyChecking=no -i private_key.pem $USER@$HOST "NODE_ENV=production SAMPLE_ENV_A=$SAMPLE_ENV_A SAMPLE_ENV_B=$SAMPLE_ENV_B SAMPLE_ENV_C=$SAMPLE_ENV_C nohup npm run start --prefix /home/ec2-user/sample-app > /home/ec2-user/sample-app-log/nohup-$(date +%Y%m%d%H%M).log 2>&1 &"

ここまででymlファイルの追記はおしまい。
このymlファイル内のsample-appを自身のリポジトリ名にしたり、環境変数名を変更したり追加したり、適宜自身のアプリに合わせて記述を調整してください。

動作確認・使い方について

GitHubのリポジトリのページ上部の「Actions」タブを押下すると、GitHub Actionの実行状況に関するページが出てくる。この「All workflows」の欄に、実行した全てのActionの履歴と、実行中の状況が確認できる(削除・キャンセルもできる)。

また、「All workflows」の下の「Node.js Deploy Pipeline」を押下すると、手動実行(Run workflow)のトグルが出てくるので、これを押下してrunType(実行メニュー)を選び、手動実行することができる。

  • デプロイ...productionブランチにコードがプッシュされた時と同じ動作になる。アプリケーションを停止 → productionブランチのソースコードをEC2インスタンスにアップロード → パッケージのインストール → アプリケーションの起動が行われる。
  • アプリケーション 再起動...アプリケーションを停止 → GitHub Actions secrets の最新の環境変数でアプリケーションを再起動する。実行前からアプリケーションが停止していても問題なく動く(起動だけする)。
  • アプリケーション 停止...実行中のNode.jsアプリケーションを停止(kill)する。

それでは、手動実行でもよいし、productionブランチにマージするでもよいので、GitHub Actionをトリガーして、Actionsランナーの動きを観察してみよう。緑のチェックが入って正常終了したことを確認したら、大喜びしてください。10分以上経っても完了しない場合はおそらく失敗しているので、ランナーを中断して何が原因だったかを実行ログを見て判断してください。
(私はnode_modulesを全てそのまま転送しようとしていいて、実行を中断したことがあります)

作業(解説あり。)はここまで!

これで全て終わりです!Your Awsome Applicationは世界に羽ばたきました!!

長い記事でしたが、最後までスクロールしてくださりありがとうございました!!!

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3