Codeit/프로젝트

[Monew] 트러블슈팅 - CICD

leejunkim 2025. 9. 16. 20:26

1. CI/CD pipeline에서 region mismatch

  • ecr로 올릴때는 public registry라서 제공되는 지역 us-east-1로 AWS CLI 로그인을 해야한다
  • ecs로 올릴때는 프로젝트의 지역으로 AWS CLI 로그인을 해야한다 (ap-northeast-2)
    • Github Secrets로부터 가져옴
deploy-to-ecs:
    runs-on: ubuntu-latest
    needs: build-and-push
    steps:
      # AWS 자격 증명 설정
      - name: AWS CLI 설정
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

2. Task Definition 시작 오류 - CannotPullImageManifestError

 

에러명: CannotPullImageManifestError: Error response from daemon: invalid reference format

jobs:
  # ----------------------------------------------------------------
  # JOB 1: Docker 이미지를 빌드하고 ECR에 푸시
  # ----------------------------------------------------------------
  build-and-push-to-ecr:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      id-token: write

    env:
      REPO_URI: ${{ secrets.ECR_REPOSITORY_URI }}

    outputs:
      image_tag: ${{ steps.prep.outputs.image_tag }} # <<<< 문제점
      
  ....
  
    # ============== docker 빌드하고 ecr로 푸쉬 ==============
    # 이미지 태그 구성 - 커밋 해시 7자리 + latest
    - name: Set Image Tag
      id: set-tag
      run: echo "tag=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT

 

  • 알고보니 그냥 복붙 에러였는데, steps.prep이 아니라 밑에 Set Image Tag 부분에 steps.set-tag을 사용해야 했었다.
jobs:
  # ----------------------------------------------------------------
  # JOB 1: Docker 이미지를 빌드하고 ECR에 푸시
  # ----------------------------------------------------------------
  build-and-push-to-ecr:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      id-token: write

    env:
      REPO_URI: ${{ secrets.ECR_REPOSITORY_URI }}

    outputs:
      image_tag: ${{ steps.set-tag.outputs.image_tag }} # <<<< set-tag 으로 수정
      
  ....
  
    # ============== docker 빌드하고 ecr로 푸쉬 ==============
    # 이미지 태그 구성 - 커밋 해시 7자리 + latest
    - name: Set Image Tag
      id: set-tag
      run: echo "tag=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT

 

  • 이제 deploy-to-ecs job이 실행될 때, set-tag id를 가진 step의 output을 제대로 가져올 수 있게 됐다

3. (오류는 아니지만) docker build 최적화

기존 코드:

      # ============== docker 빌드하고 ecr로 푸쉬 ==============
      # 이미지 태그 구성 - 커밋 해시 7자리 + latest
      - name: Set Image Tag
        id: set-tag
        run: echo "tag=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT

      # Docker 빌드 (dockerfile 사용)
      - name: Build Docker image
        run: |
          docker build -t "${{ env.REPO_URI }}:${{ steps.set-tag.outputs.tag }}" -t "${{ env.REPO_URI }}:latest" .

      # ecr로 푸쉬
      - name: Push to ECR
        run: |
          docker push "${{ env.REPO_URI }}:${{ steps.set-tag.outputs.tag }}"
          docker push "${{ env.REPO_URI }}:latest"
  • 캐싱이 제대로 되지 않는 상태였다..!
    • docker build -> docker push를 해버려서 매번 빌드할떄 시간이 오래 걸렸었다.

 

수정 후:

      # ============== docker 빌드하고 ecr로 푸쉬 ==============

      # docker Buildx 설정 (캐싱을 위해 필요)
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      # 이미지 태그 구성 - 커밋 해시 7자리 + latest
      - name: Set Image Tag
        id: set-tag
        run: echo "image_tag=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT

      # Docker 빌드 (dockerfile 사용)
      - name: Build and push Docker image with ECR cache
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REPO_URI }}:${{ steps.set-tag.outputs.image_tag }}
            ${{ env.REPO_URI }}:latest
          cache-from: type=registry,ref=${{ env.REPO_URI }}:buildcache
          cache-to: type=registry,ref=${{ env.REPO_URI }}:buildcache,mode=max
          platforms: linux/amd64

buildx를 사용하게 되면 이렇게 buildcache가 추가된다.

 

4. EC2 public 주소 접속 안됨 1 - MaxMetaspaceSize

모든게 정상적으로 되지만, EC2가 왠지 모르게 접속이 안됐었다..

그래서 로그를 확인해봤다:

 

2025-09-12T02:41:06.164Z WARN 1 --- [MoNew] [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Metaspace

 

결론은 Metaspace out of memory error이여서, Spring Boot가 JVM 메모리가 부족하다는 오류였다.

그래서 ecs가 reference하는 (S3에 올린) .env 파일에 MaxMetaspaceSize를 64에서 128로 올려주었다.

 

전:

# Spring Configuration
SPRING_PROFILES_ACTIVE=prod

...

# JVM Configuration
JAVA_TOOL_OPTIONS=-Xmx384m -Xms256m -XX:MaxMetaspaceSize=64m -XX:+UseSerialGC

 

후:

# JVM Configuration
JAVA_TOOL_OPTIONS=-Xmx384m -Xms256m -XX:MaxMetaspaceSize=128m -XX:+UseSerialGC

 

 

5. EC2 public 주소 접속 안됨 2 - MongoDB 연동 오류

 

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'batchConfig' defined in URL [jar:nested:/app/app.jar/!BOOT-INF/classes/!/com/sprint/team2/monew/domain/article/batch/config/BatchConfig.class]: Unsatisfied dependency

...

[org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryDependentConfiguration.class]: Unsatisfied dependency expressed through method 'mongoTemplate' parameter 0: Error creating bean with name 'mongoDatabaseFactory' defined in class path resource [org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryConfiguration.class]: Failed to instantiate [org.springframework.data.mongodb.core.MongoDatabaseFactorySupport]: Factory method 'mongoDatabaseFactory' threw exception with message: Database name must not be empty

 

장황하지만 뜯어보면 그냥 mongodb db 이름 설정을 까먹고 안적어주어서 생긴 오류다.

 

application-prod.yml에 이렇게 추가해주면 된다:

  data:
    mongodb:
      uri: ${MONGODB_URL}
      database: ${MONGODB_DB}

 

여기까지 하고... 드디어 베포가 돼었다...!

 


이제부터는 배포 후 생긴 오류들도 정리해보려고 한다.

 

1. MongoDB Atlas 커넥션 오류

 

로그인 페이지에 회원가입할때 MongoDB 쪽 오류가 났다....!

  • 새로운 유저가 생성될 때 UserActivity를 만드는데 이건 mongodb 를 사용하기 떄문이다

 

{

    "code": "DataAccessResourceFailureException",

    "details": {},

    "exceptionType": "DataAccessResourceFailureException",

    "message": "Timed out while waiting for a server that matches WritableServerSelector. Client view of cluster state is {type=REPLICA_SET, servers=[{address=ac-fwt058t-shard-00-02.4sizz77.mongodb.net:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketWriteException: Exception sending message}, caused by {javax.net.ssl.SSLException: Received fatal alert: internal_error}}, {address=ac-fwt058t-shard-00-01.4sizz77.mongodb.net:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketWriteException: Exception sending message}, caused by {javax.net.ssl.SSLException: Received fatal alert: internal_error}}, {address=ac-fwt058t-shard-00-00.4sizz77.mongodb.net:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketWriteException: Exception sending message}, caused by {javax.net.ssl.SSLException: Received fatal alert: internal_error}}]",

    "status": 500,

    "timestamp": "2025-09-12T05:18:02.410287676Z"

}

 

알고보니 MongoDB Atlas에서 커넥션을 허용 안해줘서 생긴 문제인 듯 하다.

 

Mongodb 로그인 > Security > Network Access에서 배포된 EC2링크를 여기에 추가해주었다.

 

추가했더니 이제 회원가입은 됐다~~

 

2. 활동 내역 페이지 먹통 - 태스크 실행 안되는 오류

 

잘 되나 싶었더니.. 처음에는 코드 상의 문제인 줄 알고 굉장히 해멨는데, 아무리 서비스 코드를 뜯어봤자 고칠 게 없어 보였다. 더 나아가 로컬에서는 잘만 됐기 때문에, 배포할때 어디 문제가 있던게 분명했다. 

AWS 콘솔을 보니까 태스크가 무한 대기중이였고 새롭게 배포가 안되는 상황이였다. 

 

태스크 무한 대기 중으로 마지막에는 timeout이 떴다 ㅋㅋ

 

더 자세히 알아보니까, ECS 클러스터의 컴퓨팅 리소스가 부족했었기 때문이었다.

  • ECS의 서비스의 기본 배포 방식은 롤링 업데이트다 -> 서비스 중단을 막기 위해 기존 버전의 태스크를 중단하기 전에 새 버젼의 태스크를 먼저 실행시킨다. 
  • 그래서 배포가 진행되는 짧은 순간에는 기존 태스크 + 신규 태스크 2개가 동시에 실행된다!
  • 결론적으로 원래는 1개의 태스크만 운영하더라고 배포 시점에는 일지적으로 2개의 리소스가 필요하지만, 현재 free tier는 t3.micro를 사용하고 있어서 새 태스크가 실행될 공간이 없어서 배포가 멈췄던 것이다.

기본적으로 이렇게 세팅이 되어있다:

  • 아까 언급했던 대로 rolling update의 기본값으로 최대 실행 작업 비율은 200다
    • 동시에 2개를 일지적으로 허용해야 해서 200이다

솔루션:

cd.yml 파일을 이렇게 수정했다

      # 디버깅 스텝
      - name: ECS_TASK_DEFINITION 확인
        run: |
          if [ -z "${{ secrets.ECS_TASK_DEFINITION }}" ]; then
            echo "::error:: The ECS_TASK_DEFINITION secret is empty or not set."
            exit 1
          else
            echo "ECS_TASK_DEFINITION secret is present."
            echo "Length: ${#ECS_TASK_DEFINITION}"
            # Show first few characters (safely masked by GitHub)
            echo "Value starts with: ${ECS_TASK_DEFINITION:0:10}..."
          fi

      # 기존 ECS 태스크 정의 다운로드
      - name: 기존 ECS 태스크 정의 다운로드
        run: aws ecs describe-task-definition --task-definition "${{ secrets.ECS_TASK_DEFINITION }}" --query taskDefinition > task-def.json

      # 기존 태스크 중지
      - name: 서비스 중지 (Desired Count를 0으로 설정)
        run: |
          aws ecs update-service \
            --cluster ${{ secrets.ECS_CLUSTER }} \
            --service ${{ secrets.ECS_SERVICE }} \
            --desired-count 0

      # 서비스 중지 대기
      - name: 서비스 중지 대기
        run: |
          aws ecs wait services-stable \
            --cluster ${{ secrets.ECS_CLUSTER }} \
            --service ${{ secrets.ECS_SERVICE }}

      # 새 ECR 이미지로 태스크 정의 업데이트
      - name: 새 ECR 이미지로 태스크 정의 업데이트
        id: render-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-def.json
          container-name: ${{ secrets.ECS_CONTAINER_NAME }}
          image: ${{ secrets.ECR_REPOSITORY_URI }}:${{ needs.build-and-push-to-ecr.outputs.image_tag }}

      # 새 태스크 정의 등록
      - name: 새 태스크 정의 등록
        id: register-task-def
        run: |
          task_def_arn=$(aws ecs register-task-definition --cli-input-json file://${{ steps.render-task-def.outputs.task-definition }} --query 'taskDefinition.taskDefinitionArn' --output text)
          echo "task_definition_arn=$task_def_arn" >> $GITHUB_OUTPUT

      # 서비스 업데이트 및 재시작
      - name: 서비스 업데이트 및 재시작
        run: |
          aws ecs update-service \
            --cluster ${{ secrets.ECS_CLUSTER }} \
            --service ${{ secrets.ECS_SERVICE }} \
            --task-definition ${{ steps.register-task-def.outputs.task_definition_arn }} \
            --desired-count 1 \
            --force-new-deployment

      # 새 배포 안정화 대기
      - name: 새 배포 안정화 대기
        run: |
          aws ecs wait services-stable \
            --cluster ${{ secrets.ECS_CLUSTER }} \
            --service ${{ secrets.ECS_SERVICE }}

 

  1. 기존 서비스 강제 중지: aws ecs update-service --desired-count 0 명령어를 실행하여 현재 실행 중인 태스크를 0으로 만든다 -> 의도적으로 잠깐의 서비스 다운타임이 발생하지만, 클러스터의 모든 리소스를 확보할 수 있다
  2. 기존 태스크가 완전히 중지될 때까지 대기
  3. 새 버전으로 서비스 업데이트 + 재시작: 클러스터 리소스가 완전히 비워진 상태에서, 새로운 도커 이미지가 적용된 태스크 정의로 서비스를 업데이트하고 실행할 태스크 수를 다시 1로 설정한다
  4. 새 서비스 안정화 대기: 새로운 태스크가 완전히 실행되고 안정적인 상태가 될 때까지 기다린 후 배포를 종료한다

드디어 활동 내역에 가보면 페이지 먹통이 사라지고 서비스는 태스크 1개를 잘 실행중인 것을 볼 수 있다!!