ECS, ECR, 도커를 이용한 CICD 성공 - 항해플러스

codeing999·2023년 7월 17일
0

NestJS

목록 보기
7/9

성공하기 전 도커파일과 깃헙액션 파일에서 어떤 문제가 있었고 어떻게 해결해나갔는지 정리해봄

  • 기존 Dockerfile
ENTRYPOINT ["top", "-b"]

# 베이스 이미지 선택
FROM node:18-alpine

WORKDIR /src

# 앱 종속성 설치
COPY package.json ./
RUN npm install

# 앱 소스 코드 복사
COPY . .

# 포트 노출
EXPOSE 3000

# mysql 서버가 실행되기 전에 앱이 실행되는것을 막아주는 스크립트래요. 이게 문제였던건 아닌것 같지만 사용했습니당
# ADD https://github.com/vishnubob/wait-for-it/raw/master/wait-for-it.sh /wait-for-it.sh
# RUN chmod +x /wait-for-it.sh

# 앱 실행 명령
CMD ["npm", "run", "start:seed"]

깃헙액션은 성공했으나 응답이 없는 상태

  • 이전에도 도커파일, 깃헙액션, aws 설정, 환경변수들 엄청 수정해가며 수많은 실패를 계속 하다가 깃헙액션은 성공했다. 깃헙액션이 성공했어도 서버가 계속 죽었고 어떤 원인 때문에 죽는지에 대한 에러 같은 걸 볼 수 있는 것도 없고, 깃헙액션은 성공했으니 aws에서 로드밸런서나 ecs 쪽 설정이 뭐가 잘못된건지 계속 건들여보며 해매고 있었다.

  • 그러다가 다시 도커파일과 깃헙액션 쪽을 열심히 훑어봤는데, 우리 도커파일에는

RUN npm run build

이 빌드가 없다는 걸 발견해서 일단 추가해보았다. 그랬더니 깃헙액션이 실패하기 시작했고 이 빌드 단계에서 실패하는 에러로그가 나오게 되었다.

  • 일단 우리가 빌드를 하지 않았어도 그동안 깃헙액션에 성공했던 이유는 도커파일에서 우리의 앱 실행 명령어가
npm run start:seed

이거였기 때문이었다.

우리의 start:seed는

"start:seed": "ts-node src/seeder.ts && nest start --watch",

이거 였는데 이건 명시적으로 dist 폴더를 사용하는 실행이 아니어서, 빌드를 하지 않은 상태에서 dist 폴더를 생성하지 않고 실행에도 깃헙액션 자체는 에러 없이 성공해버린 것 같았다.

여러 곳을 찾아봤을 때 배포환경에서의 실행 명령어는 "node dist/main" 이것을 사용하는 게 정석인 것으로 보였고 그래서 우리의 이 실행을 하는 명령어인

"start:prod": "cross-env NODE_ENV=production node dist/main",

이것으로 바꿔 넣었다. 이제 이러면 빌드를 하지 않으면 에러가 나며 실행이 안될 것이다.

추가로 수정한 것

  • ENV NODE_ENV production
    배포 환경으로 실행되도록 하는 이 부분도 도커파일에 없어서 추가

  • .env 파일이 gitignore이기 때문에 환경변수가 없어서 서버가 죽는게 아닌가 싶어서 배포환경에 .env 파일을 생성하는 깃헙액션도 추가하였다.

name: Deploy to Amazon ECR
on:
  push:
    branches:
      - main
env:
  AWS_REGION: ap-northeast-3
  ECR_REGISTRY: 123456789027.dkr.ecr.ap-northeast-2.amazonaws.com/
  ECR_REPOSITORY: repository-nestjs
  DB_HOST: ${{ secrets.DB_HOST }}
  DB_PORT: ${{ secrets.DB_PORT }}
  DB_USER: ${{ secrets.DB_USER }}
  DB_PW: ${{ secrets.DB_PW }}
  DB_SCHEMA: ${{ secrets.DB_SCHEMA }}
  JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - 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: ${{ env.AWS_REGION }}
      - name: Create .env.production file
        run: |
          echo "DB_HOST=${{ secrets.DB_HOST }}" >> .env.production
          echo "DB_PORT=${{ secrets.DB_PORT }}" >> .env.production
          echo "DB_USER=${{ secrets.DB_USER }}" >> .env.production
          echo "DB_PW=${{ secrets.DB_PW }}" >> .env.production
          echo "DB_SCHEMA=${{ secrets.DB_SCHEMA }}" >> .env.production
          echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> .env.production
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
      - name: Fill in the new image ID in the Amazon ECS task definition
        id: setting-task-definition
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: container-nestjs
          image: ${{ steps.build-image.outputs.image }}
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.setting-task-definition.outputs.task-definition }}
          cluster: cluster123 # 여러분의 ECS 클러스터 아이디
          service: service123 # 여러분의 ECS Fargate 서비스 아이디
          wait-for-service-stability: false

cicd.yml

"- name: Create .env.production file"
이 job을 저 사이에 끼워넣었다.

빌드 실패 해결

  • 이제 빌드를 하다가 난 에러들이 뭔지 살펴봤는데, nest 명령어를 쓸 수 없다고 에러가 났다. 이것은 nestjs/cli를 설치하지 않아서 nest 명령어를 쓸 수 없단 에러였는데 우리는 package.json에
"@nestjs/cli": "^10.0.3",

이게 설치되어 있긴했다. 근데 대신에 devDependency여서 에러가 난 것인 것 같았다.
검색해보니 npm install -g @nestjs/cli 이렇게 -g 옵션을 붙여서 설치하도록 하란게 나왔다.
그래서

RUN npm install -g @nestjs/cli && npm install

인스톨하는 부분에서 nestjs/cli만 저렇게 명시적으로 -g를 붙여서 설치하게 수정했다.

이렇게하고나서 이제 빌드 시에 이 에러는 넘어갔고 그 다음에 또다른 에러가 났다.

seeder파일과 test.ts파일들에서 난 에러였다. 이것은 우리가 seeder와 jest를 devDependency로 설치해놓고서는, 빌드 시에는 제외를 시키지 않았기 때문에 난 에러였다. 배포 환경에서의 빌드에는 저 파일들이 포함되면 안되는 것이었다.

  • 그래서 seeder파일과 test.ts 파일들을 빌드에서 제외하도록 .dockerignore에 다음을 추가하였다.

처음엔

*.test.ts
*.seeder.ts

이렇게 추가했는데 저 파일들이 제대로 제외되지가 않아서 여전히 빌드에서 에러가 났다.
검색해보니 모든 경로의 저 접미사로 끝나는 파일들을 제외하려면 앞에 "**/"를 붙여줘여 했다.

참고로

*/*.test.ts

이렇게 경로에 * 한개만 붙이면 루트에서 한 depth만 들어가서 있는 파일들하고만 매칭시킨다고 한다. 두개를 써줘야 루트 디렉토리를 포함한 모든 경로와 매칭.

**/*.test.ts
**/*.seeder.ts
**/seeder.ts

그래서 이렇게 했고 앞에 .이 없는 그냥 seeder.ts 파일도 있어서 이것도 추가.

그리고, 저 접미사로 끝나지 않으면서도 seeder 패키지의 데코레이터를 사용하는 엔티티 파일들이 존재하여서 빌드에서 또 에러가 났는데 해당 엔티티에서 일단 seeder 패키지 관련 코드를 주석 처리하여 해결하였다.

이렇게까지 하니 깃헙액션이 다시 성공하였다.
변경 후 Dockerfile은 다음과 같다.

# FROM ubuntu:latest
# LABEL authors="kjjdsa"

# ENTRYPOINT ["top", "-b"]

# 베이스 이미지 선택
FROM node:18

ENV NODE_ENV production

WORKDIR /src


# 앱 종속성 설치
COPY package.json ./
RUN npm install -g @nestjs/cli && npm install


# 앱 소스 코드 복사
COPY . .

RUN npm run build

# 포트 노출
EXPOSE 3000

# mysql 서버가 실행되기 전에 앱이 실행되는것을 막아주는 스크립트래요. 이게 문제였던건 아닌것 같지만 사용했습니당
# ADD https://github.com/vishnubob/wait-for-it/raw/master/wait-for-it.sh /wait-for-it.sh
# RUN chmod +x /wait-for-it.sh

# 앱 실행 명령
CMD ["npm", "run", "start:prod"]

추가로 잘못되어있던 것들

나는 빌드에서 제외시키기 위한 파일들을 .dockerignore에서 제외시켰는데, 이것도 틀린 것은 아니지만 이건 해당 도커파일을 사용하여 빌드를 할 때에만 제외시키는 것이다. 도커를 이용하지 않을 경우도 있을 거고, 기본적으로 빌드에서 제외시키는 부분은 tsconfig.build.json에 작성해야 맞는 것이다.

근데 우리의 tsconfig.build.json과 tsconfig.json을 보면 제외시키는 부분이

"exclude": ["./src/__test__"]

 "exclude": [
  "**/*spec.ts",
  "*.seeder.ts",
  "*.test.ts"
]

이렇게 인데 이것들 자체가 제대로 우리의 seeder와 test 파일들을 제외시키지 못하도록 잘 못 작성되어 있었다.

일단 모든 경로에서 제외시키려면 "**/"를 앞에 붙여야하며,
우리의 유닛 테스트 폴더는 __test__가 아니라 __tests__
여서 이부분도 오타였다.

"exclude": ["./src/__tests__"]

 "exclude": [
  "**/*spec.ts",
  "**/*.seeder.ts",
  "**/*.test.ts"
]

이렇게 고쳐주어야 원래 의도에 맞다.

profile
코딩 공부 ing..

0개의 댓글