AWS EC2와 Github Actions로 NEXT.JS 배포하기 (3)

dev_hyun·2023년 8월 26일
1

1. Github Secret 등록

깃허브에 AWS 시크릿 키와 시크릿 ACCESS 키를 등록해줘야 한다.
git에서 Settings에 들어가면 저장소 키를 등록할 수 있다.
우리는 여기에 등록이 필요하다.

ACCESS_KEY와 SECRET_ACCESS_KEY 는 IAM 사용자의 키이다.

IAM > 사용자에 들어가 전에 사용자를 만들때 나온 .csv 파일을 확인해 Github에 등록해준다.

키를 발급하지 못했다면 여기 문서에서 확인이 가능하다.
AWS 문서

secret 을 등록하는 이유는 github action에서 변수에서 사용하기 위해 필요하다.

2. Github Action 세팅하기

2-1 deploy.yml 생성

다음은 배포를 위한 deploy.yml이다.

name: Deploy
on:
  push:
    branches:
      - develop

env:
  S3_BUCKET_NAME: til-web
  CODE_DEPLOY_APPLICATION_NAME: til_web
  CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: til_fe_dev
jobs:
  build:
    environment: develop
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source code.
        uses: actions/checkout@v3

      - name: Check Node v
        run: node -v

      - name: Generate Environment Variables File
        run: |
          echo "NEXT_PUBLIC_MODE=$NEXT_PUBLIC_MODE" >> .env.development
          echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL" >> .env.development
          echo "NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID" >> .env.development
          echo "NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI=$NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI" >> .env.development
          echo "NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_PW=$NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_PW" >> .env.development
        env:
          NEXT_PUBLIC_MODE: ${{ secrets.NEXT_PUBLIC_MODE }}
          NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
          NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID }}
          NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI: ${{ secrets.NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI }}
          NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_PW: ${{ secrets.NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_PW }}

      - name: Cache node modules
        uses: actions/cache@v3
        id: cache
        with:
          # node_modules라는 폴더를 검사하여
          path: node_modules
          # 아래 키값으로 cache가 돼있는지 확인합니다.
          key: npm-packages-${{ hashFiles('**/package-lock.json') }}

      - name: Install Dependencies
        if: steps.cache.outputs.cache-hit != 'true'
        run: npm install

      - name: Build
        run: |
          npm run build:dev

      - name: zip create
        run: |
          zip -qq -r --symlinks  ./til-dev.zip .
        shell: bash

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

      - name: Upload to S3
        run: |
          aws s3 cp --region ap-northeast-2 ./til-dev.zip s3://$S3_BUCKET_NAME/til-dev.zip

      - name: Deploy For Development
        run: |
          aws deploy create-deployment \
          --deployment-config-name CodeDeployDefault.AllAtOnce \
          --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
          --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
          --s3-location bucket=$S3_BUCKET_NAME,key=til-dev.zip,bundleType=zip

다음은 전체 yml 코드이며 dev 서버와 prod 서버가 분리 되어있어서 yml 또한 분리되어 있는데 자세한 내용은 저장소를 살펴보는게 더 좋을 것 같다.

코드를 하나씩 보며 설명하겠다.

name: Deploy
on:
  push:
    branches:
      - develop

여기는 이제 develop 브랜치에 push 가 이뤄졌을 때 동작하기 위한 코드이다.

env:
  S3_BUCKET_NAME: til-web
  CODE_DEPLOY_APPLICATION_NAME: til_web
  CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: til_fe_dev

env 를 설정해 yml 내에서 변수를 사용하는 것이다. 자주 쓰이는 것들을 위로 선언해놨다.

jobs:
  build:
    environment: develop
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source code.  # 깃허브 레포지토리 체크.
        uses: actions/checkout@v3

      - name: Check Node v # 노드 버전 확인
        run: node -v

      - name: Generate Environment Variables File # 환경 변수 세팅
        run: |
          echo "NEXT_PUBLIC_MODE=$NEXT_PUBLIC_MODE" >> .env.development
          echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL" >> .env.development
          echo "NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID" >> .env.development
          echo "NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI=$NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI" >> .env.development
          echo "NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_PW=$NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_PW" >> .env.development
        env:
          NEXT_PUBLIC_MODE: ${{ secrets.NEXT_PUBLIC_MODE }}
          NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
          NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID }}
          NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI: ${{ secrets.NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI }}
          NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_PW: ${{ secrets.NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_PW }}

이제 가장 중요한 github action의 step들이다.

  • Git 저장소를 확인
  • 노드 버전을 확인
  • EC2 인스턴스내의 저장소에서 사용될 환경변수를 생성
jobs:
  ...
    steps:
      ...
      - name: Cache node modules  # 설치된 의존성 파일 설치.
        uses: actions/cache@v3
        id: cache
        with:
          # node_modules라는 폴더를 검사하여
          path: node_modules
          # 아래 키값으로 cache가 되있는지 확인합니다.
          key: npm-packages-${{ hashFiles('**/package-lock.json') }}

      - name: Install Dependencies # 캐시된게 없다면 npm install
        if: steps.cache.outputs.cache-hit != 'true'
        run: npm install

      - name: Build # next 프로젝트 빌드
        run: |
          npm run build:dev

      - name: zip create  # S3에 올릴 zip 파일 생성. (빌드한 프로젝트를 압축)
        run: |
          zip -qq -r --symlinks  ./til-dev.zip .
        shell: bash
  • npm 의존성 패키지를 cache 되어있다면 캐쉬된것 설치, 또는 npm install
  • next 프로젝트 빌드
  • 빌드된 파일 zip 파일로 압축

이제 npm i를 통해 패키지들을 설치하고 빌드후 zip 파일을 생성하는 과정입니다.

jobs:
  ...
    steps:
      ...
      - name: Configure AWS credentials # AWS 인증절차
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2

      - name: Upload to S3 # ZIP 파일을 S3에 업로드
        run: |
          aws s3 cp --region ap-northeast-2 ./til-dev.zip s3://$S3_BUCKET_NAME/til-dev.zip

      - name: Deploy For Development # S3에 올라간 zip 파일을 CodeDeploy를 통해 가져온다.
        run: |
          aws deploy create-deployment \ 
          --deployment-config-name CodeDeployDefault.AllAtOnce \ # Code Deploy에서 설정했던 Deploy 방식.
          --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
          --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \ # AWS의 Code Deploy 애플리케이션의 배포 그룹 이름.
          --s3-location bucket=$S3_BUCKET_NAME,key=til-dev.zip,bundleType=zip  # Code Deploy가 S3에서 프로젝트를 찾을 수 있도록 경로 지정. (bucket=버킷 이름/test-build.zip)

(주석은 설명을 위한 것일 뿐 실행되지 않습니다. 시도를 위한 코드는 가장 처음의 코드를 봐주세요.)

  • Github에 등록한 Secret 키를 통해 AWS 를 인증한다.
  • 전에 생성한 Zip 파일을 S3에 업로드한다.
  • S3에 올라간 파일들을 CodeDeploy를 사용해 가져온다.

appspec.yml 생성

AWS에서 CodeDeploy가 동작하면 appspec.yml을 바라보기 때문에
EC2에서 실행할 단계를 설정해줘야 한다.

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/deploy
    overwrite: yes
permissions:
  - object: /home/ubuntu/deploy
    owner: ubuntu
    group: ubuntu
    mode: 755
hooks:
  ApplicationStart:
    - location: deploy.sh
      timeout: 500
      runas: ubuntu
  • EC2 인스턴스에서 /home/ubuntu/deploy 폴더에 프로젝트를 저장한다.
  • 그 뒤 deploy.sh를 실행한다.

deploy.sh 는 다음과 같다.

#!/bin/bash

REPOSITORY=/home/ubuntu/deploy
REPOSITORY_PROD=/home/ubuntu/deploy

echo "DEPLOYMENT_GROUP_NAME: ${DEPLOYMENT_GROUP_NAME}"

if [ "${DEPLOYMENT_GROUP_NAME}" == "til_fe_prod" ]; then
  echo "운영 서버 배포"
  cd "${REPOSITORY_PROD}"
  
  # production 환경인 경우에 대한 처리
  sudo npm install
  pm2 describe til-product > /dev/null
  if [ $? -eq 0 ]; then
	  # 실행 중인 경우
	  echo "til-product 프로세스가 실행 중입니다."
	  sudo npm run pm2:reload:prod
  else
  	# 실행 중이 아닌 경우
  	echo "til-product 프로세스가 실행되지 않았습니다."
	  sudo npm run pm2:start:prod
  fi
elif [ "${DEPLOYMENT_GROUP_NAME}" == "til_fe_dev" ]; then
  echo "개발 서버 배포"
  cd "${REPOSITORY}"
  sudo npm install
  pm2 describe til-dev > /dev/null
  if [ $? -eq 0 ]; then
	  # 실행 중인 경우
	  echo "til-dev 프로세스가 실행 중입니다."
	  sudo npm run pm2:reload:dev
  else
  	# 실행 중이 아닌 경우
  	echo "til-dev 프로세스가 실행되지 않았습니다."
	  sudo npm run pm2:start:dev
  fi
fi

나는 서버를 pm2로 설정했기 때문에 pm2를 실행하는 코드가 들어있다.
개발서버와 실서버 모두 deploy.sh를 바라보기에 분기처리를 해놨다.

배포되는 그룹에 따라 실서버인지 개발서버인지 구분을 했고 pm2 서버 명령어가 다르다.
pm2 가 실행되어 있으면 재실행>= 실행되어 있지 않으면 start를 해줬다.

pm2 설정은 다음과 같다.
나는 여기 블로그를 참고를 많이 했다.

module.exports = {
  apps: [
    {
      /* 개발 환경용 서버 */
      name: 'til-dev', // pm2 이름
      cwd: './', // 경로
      script: 'npm',
      args: 'run start:dev', // package.json 명령어 실행
      instances: 1, // 단일 쓰레드
      autorestart: false,
      watch: false,
      env: {
        Server_PORT: 3000,
        NODE_ENV: 'development',
      },
    },
    {
      /* 배포 환경용 서버 */
      name: 'til-product',
      cwd: './',
      script: 'npm',
      args: 'run start:prod',
      instances: -1, // 클러스터 모드
      autorestart: false,
      watch: false,
      // wait_ready: true,
      env: {
        Server_PORT: 3000,
        NODE_ENV: 'production',
      },
    },
  ],
};

여기도 마찬가지로 개발, 운영서버를 나눠서 분리해뒀고, pm2를 실행할때 각각의 옵션들을 설정해줬다.

이렇게 작업이 완료되었으면 develop 브랜치에 push 할 때 github action에서 다음과 같이 동작할 것이다.

actions에서 각 steps에 맞는 작업들이 이뤄진다.

AWS Code Deploy에서도 배포가 잘 되는 것을 볼 수 있다.

마무리

next.js를 Vercel을 사용하지 않고, aws를 하나 하나 세팅해보았는데,
대학교 때 혼자 배포해본 경험말고 정말 오랜만에 다시 해보았다.

느낀 점은 CI/CD 구성은 나에게 정말 어려운 작업이였다.. Vercel로 하는 것은 정말 편해보였는데.. 왜 Vercel로 배포하는지 알 것 같다 🫡🫡🫡

그래도 하나하나 모두 세팅해보면서 자동 배포 세팅해보는 경험을 해서 다행이다.
기록도 해두었으니 다음에 한번 더 할 기회가 생긴다면 참고할 때 도움이 될 것 같다.

profile
하다보면 안되는 것이 없다고 생각하는 3년차 프론트엔드 개발자입니다.

0개의 댓글