NestJS + Prisma + Docker + EC2 + GitHub Actionsで作るモダンなCI/CDパイプライン

2024/07/08に公開

1. はじめに

本記事では、NestJS、Prisma、Docker、Amazon EC2、GitHub Actions を使用して、CI/CD パイプラインを構築する方法を紹介します。初めてのデプロイということで問題アリアリだと思いますがどうぞ大目に見てください。

対象読者

  • NestJS と Prisma を使用したバックエンド開発者
  • Docker コンテナ化に興味がある開発者
  • AWS EC2 でのデプロイを検討している方
  • GitHub Actions を用いた CI/CD 構築を学びたい方

2. 環境設定

2.1 EC2 インスタンスの準備

まず、適切な EC2 インスタンスを選択します。本記事では、t3.micro(16GB)を使用します。以前は t2.micro(8GB)を使用していましたが、NestJS のビルド中にメモリ不足エラーが発生したため、アップグレードしました。

メモリを効率的に使用するために、ec2 コンテナに ssh で接続しスワップ領域を設定します:

sudo dd if=/dev/zero of=/swapfile bs=128M count=16
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# スワップ領域の確認
sudo swapon -s

# システム起動時の自動有効化設定
echo "/swapfile swap swap defaults 0 0" | sudo tee -a /etc/fstab

# メモリ状態の確認
free

2.2 Docker のインストール

EC2 インスタンスに Docker をインストールします。Ubuntu の場合、公式ドキュメントに従ってインストールを行います。

https://docs.docker.com/engine/install/ubuntu/

3. NestJS アプリケーションの Dockerfile 作成

3.1 基本的な Dockerfile

NestJS アプリケーションの Dockerfile を作成します。以下は基本的な構成です:

FROM node:18-alpine AS build

WORKDIR /usr/src/app

COPY package.json yarn.lock ./
RUN yarn install --production --frozen-lockfile

COPY . .
RUN yarn cache clean
RUN yarn build

FROM node:18-alpine AS production

COPY --from=build /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/dist ./dist

CMD [ "node", "dist/src/main.js" ]

参考資料

https://www.tomray.dev/nestjs-docker-production

3.2 Prisma を使用する際の注意点

Prisma を使用する場合、prisma generateの結果を適切に扱う必要があります。以下の変更を行います:

  1. schema.prismaファイルを修正し、生成されるクライアントの出力先を変更します:
generator client {
  provider      = "prisma-client-js"
  output        = "./generated/client"
  binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
}
  1. PrismaClient のインポート先を変更します:
import { PrismaClient } from 'prisma/generated/client';

3.3 最終的な Dockerfile

Prisma の設定を反映した最終的な Dockerfile は以下のようになります:

FROM node:18-alpine AS build

WORKDIR /usr/src/app

COPY package.json yarn.lock ./
RUN yarn install --production --frozen-lockfile

COPY . .
RUN yarn cache clean

ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG AWS_REGION
ARG FRONTEND_BASE_URL
ARG COGNITO_CLIENT_ID
ARG COGNITO_USER_POOL_ID
ARG DATABASE_URL
ARG OPENAI_API_KEY

RUN yarn build

FROM node:18-alpine AS production

COPY --from=build /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/dist ./dist
COPY prisma ./dist/prisma/

CMD [ "node", "dist/src/main.js" ]

4. GitHub Actions ワークフローの作成

4.1 ワークフローの基本構造

.github/workflows/ci-cd.ymlファイルを作成し、以下の内容を記述します:

name: CI/CD Pipeline

on:
  push:
    branches:
      - main

jobs:
  build-backend:
    name: Build and Deploy Backend
    runs-on: ubuntu-latest

    steps:
      # バックエンドのビルドとデプロイ手順

  build-frontend:
    name: Build and Deploy Frontend
    runs-on: ubuntu-latest
    needs: build-backend

    steps:
      # フロントエンドのビルドとデプロイ手順

4.2 バックエンドのビルドとデプロイ

バックエンドのビルドとデプロイには、以下の手順を含めます:

  1. コードのチェックアウト
  2. Docker Hub へのログイン
  3. Docker イメージのビルドとプッシュ
  4. EC2 インスタンスへの SSH 接続とデプロイ
steps:
  - name: Checkout
    uses: actions/checkout@v3

  - name: Login Docker Hub
    uses: docker/login-action@v3
    with:
      username: ${{secrets.DOCKER_HUB_USERNAME}}
      password: ${{secrets.DOCKER_HUB_ACCESS_TOKEN}}

  - name: Build and push Docker image
    uses: docker/build-push-action@v5
    with:
      context: ./backend
      push: true
      tags: ${{ secrets.DOCKER_HUB_USERNAME }}/myservice-backend:latest
      build-args: |
        AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION=${{ secrets.AWS_REGION }}
        FRONTEND_BASE_URL=${{ secrets.FRONTEND_BASE_URL }}
        COGNITO_CLIENT_ID=${{ secrets.COGNITO_CLIENT_ID }}
        COGNITO_USER_POOL_ID=${{ secrets.COGNITO_USER_POOL_ID }}
        DATABASE_URL=${{ secrets.DATABASE_URL }}
        OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}

  - name: Deploy via SSH
    uses: appleboy/ssh-action@v1.0.0
    with:
      host: ${{ secrets.EC2_HOST }}
      username: ${{ secrets.EC2_USER }}
      key: ${{ secrets.EC2_KEY }}
      script: |
        # デプロイスクリプト

4.3 (おまけ)フロントエンドのビルドとデプロイ(S3 + CloudFront)

フロントエンドのビルドとデプロイには、以下の手順を含めます:

  1. Node.js のセットアップ
  2. 依存関係のインストール
  3. フロントエンドのビルド
  4. AWS クレデンシャルの設定
  5. S3 へのデプロイ
  6. CloudFront の無効化
steps:
  - name: Checkout Code
    uses: actions/checkout@v4

  - name: Set up Node.js
    uses: actions/setup-node@v4
    with:
      node-version: '20'

  - name: Install Dependencies
    run: |
      cd frontend
      npm install

  - name: Build Frontend
    env:
      FRONTEND_BASE_URL: ${{ secrets.FRONTEND_BASE_URL }}
      BACKEND_BASE_URL: ${{ secrets.BACKEND_BASE_URL }}
    run: |
      cd frontend
      npm run build

  - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v1
    with:
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      aws-region: ${{ secrets.AWS_REGION }}

  - name: Deploy to S3
    run: |
      aws s3 sync frontend/dist s3://myservice-app --delete

  - name: Invalidate CloudFront
    run: |
      aws cloudfront create-invalidation --distribution-id <distribution-id> --paths "/*"

5. CI/CD パイプラインの解説

5.1 バックエンドのデプロイフロー

  1. コードが main ブランチにプッシュされると、ワークフローが開始されます。
  2. Docker イメージがビルドされ、Docker Hub にプッシュされます。
  3. EC2 インスタンスに接続し、最新の Docker イメージをプルして実行します。

5.2 フロントエンドのデプロイフロー

  1. バックエンドのデプロイが完了後、フロントエンドのビルドが開始されます。
  2. ビルドされたファイルは S3 バケットに同期されます。
  3. CloudFront のキャッシュが無効化され、最新のコンテンツが配信されます。

5.3 環境変数の取り扱い

機密情報は GitHub Secrets に保存し、ワークフロー内で環境変数として使用します。これにより、セキュアな方法で必要な情報を CI/CD プロセスに組み込むことができます。

6. トラブルシューティング

よくあるエラーとその解決方法:

  • Prisma 関連のエラー:prisma generateの実行結果が正しく含まれていることを確認してください。
  • メモリ不足エラー:EC2 インスタンスのスペックアップやスワップ領域の設定を検討してください。
  • Docker 関連のエラー:Dockerfile の構成を見直し、必要なファイルが正しくコピーされているか確認してください。

7. まとめ

本記事では、NestJS、Prisma、Docker、EC2、GitHub Actions を組み合わせた CI/CD パイプラインの構築方法を紹介しました。

初めてのデプロイということであまり詳しく書けず色々とツッコミどころが多いと思います。もし改善点であったり問題点がありましたらお教えください。ありがとうございました。

GitHubで編集を提案

Discussion