Codeit/스프린트 과제

[sprint8] 심화: CI/CD 파이프라인 + 버그와 해결방법들

leejunkim 2025. 8. 28. 20:00

심화로 CICD 배포 파이프라인을 구현하는 과제를 진행했었다.

test.yml

name: CI Workflow

on:
  #  push:
  #    branches: [ "main", "leejun/sprint8" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: JDK 17 설정
        uses: actions/setup-java@v3
        with:
          java-version: 17
          distribution: 'temurin'

      - name: 테스트 권한
        run: chmod +x ./gradlew

      - name: 빌드와 테스트
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
        run: ./gradlew clean build

      - name: Upload coverage reports to Codecov
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          verbose: true

 

deploy.yml

name: CD Workflow - Build & Push to ECR

on:
  push:
    branches: [ "release" ]

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

    permissions:
      contents: read

    outputs:
      #      repo_uri: ${{ steps.prep.outputs.repo_uri }}
      image_tag: ${{ steps.prep.outputs.image_tag }}

    steps:
      # 소스 체크아웃
      - name: Checkout
        uses: actions/checkout@v3

      # AWS 자격 증명 설정 (secrets 사용)
      - 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: us-east-1

      # ECR 로그인 (계정 ID를 Secret에서 참조)
      - name: Login to Amazon ECR Public
        id: login-ecr-public
        uses: aws-actions/amazon-ecr-login@v2
        with:
          registry-type: public

      # (선택) 이미지 태그 구성 - 커밋 해시 7자리 +latest
      - name: Prepare tags
        id: prep
        run: |
          echo "IMAGE_TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV
          echo "REPO_URI=${{ secrets.ECR_REPOSITORY_URI }}" >> $GITHUB_ENV

          echo "IMAGE_TAG=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
      #          echo "REPO_URI=${{ secrets.ECR_REPOSITORY_URI }}" >> $GITHUB_OUTPUT

      # Docker 빌드
      - name: Docker build
        run: |
          docker build -t "$REPO_URI:$IMAGE_TAG" -t "$REPO_URI:latest" .

      # ECR 푸쉬
      - name: Docker push
        run: |
          docker push "$REPO_URI:$IMAGE_TAG"
          docker push "$REPO_URI:latest"
  # ----------------------------------------------------------------
  # JOB 2: 새 이미지를 ECS 서비스에 배포
  # ----------------------------------------------------------------
  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 }}

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

      # 디버깅용
      - name: Verify Image URI
        run: |
          echo "Final Image string being used is: ${{ secrets.ECR_REPOSITORY_URI }}:${{ needs.build-and-push.outputs.image_tag }}"
      #          echo "Final Image string being used is: ${{ needs.build-and-push.outputs.repo_uri }}:${{ needs.build-and-push.outputs.image_tag }}"

      # 새 ECR 이미지로 태스크 정의 업데이트
      - name: Fill in new image ID in task definition
        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.outputs.image_tag }}

      # 새 태스크 정의 등록 및 ECS 서비스 업데이트
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.render-task-def.outputs.task-definition }}
          service: ${{ secrets.ECS_SERVICE }}
          cluster: ${{ secrets.ECS_CLUSTER }}
          wait-for-service-stability: false

      # 배포 상태 확인
      - name: ECS 배포 상태 확인
        run: |
          aws ecs describe-services \
            --cluster ${{ secrets.ECS_CLUSTER }} \
            --services ${{ secrets.ECS_SERVICE }} \
            --query "services[0].deployments"

 

CodeCov (Jacoco를 이해 변경한 build.gradle)

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.0'
    id 'io.spring.dependency-management' version '1.1.6'
    id 'jacoco'
}

group = 'com.sprint.mission'
version = '1.2-M8'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

jacoco {
    toolVersion = "0.8.8" //추가: 버전 명시
}


configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // core spring boot dependencies
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4'

    // db driver
    runtimeOnly 'org.postgresql:postgresql'

    // annotation processors and utilities
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.mapstruct:mapstruct:1.6.3'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'

    // testing dependencies
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    testImplementation 'org.springframework:spring-test'

    // testing
    testImplementation 'com.h2database:h2'
    testRuntimeOnly 'com.h2database:h2'

    // gson for controller testing
    implementation 'com.google.code.gson:gson'

    // AWS
    implementation 'software.amazon.awssdk:s3:2.31.7'
}

tasks.named('test') {
    useJUnitPlatform()
    finalizedBy jacocoTestReport //추가: test가 수행될 때마다 jacocoTestReport 작업 수행
}

task testCoverage(type: Test) {
    group 'verification'
    description 'Runs the unit tests with coverage'

    dependsOn(':test',
            ':jacocoTestReport',
            ':jacocoTestCoverageVerification')

    tasks['jacocoTestReport'].mustRunAfter(tasks['test'])
    tasks['jacocoTestCoverageVerification'].mustRunAfter(tasks['jacocoTestReport'])
}

jacocoTestReport {
    dependsOn test // test 실행 전에 수행될 task
    reports { // 어떤 파일을 생성할지/생성하지 않을지
        xml.required = true
        html.required = true
        csv.required = false
        // 기본 경로 $buildDir/reports/jacoco/test

        // 리포트 타입마다 destination file 옵션으로 경로 변경 가능
        //  html.destination file("$buildDir/jacocoHtml")
        //  xml.destination file("$buildDir/jacoco.xml")
    }
}

jacocoTestCoverageVerification {
    violationRules {
        rule {
            element = 'CLASS'
            enabled = true

            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.6
            }
        }
    }
}

 

이렇게 jacoco를 위해 build.gradle를 수정해주었는데, 이부분은 어느 천사분이 티스토리에 정리해주신걸 가져왔다...감사합니다..


가장 골치가 아팠던..... 버그들을 정리하려고 한다...

 

 

1) CPU 가 계속 꽉차서 Graceful Shutdown을 계속 함

어디서 잘못됐는지 모르겠어서 디버깅을 많이 해봤는데.....일단 discodeit service > Health 쪽에 CPUUtilization Mazimum, MemoryUtilization Maximum이 엄청 높았었다. 지금은 오류를 해결해서 안뜨지만, 무슨 산처럼 스파이크가 굉장히 높았었다.

CloudWatch를 통해 로그 추적도 해봤지만, 거기서는 별다는 로그나 에러 없이 그냥 Graceful Shutdown 하길래 정말 뭔지 모르겠어서 한참을 이것저것 찾아봤었다. 왜 갑자기 저렇게 메모리가 없는걸까?

그래서 몇시간이 흐르고 성과가 없어서 절망하고 있었는데, 갑자기 내가 전에 실수로 용량이 큰 파일들을 git에 그대로 push해두어서 git 용량이 엄청 커졌던걸 기억했다 (좀 조심할껄 ㅠㅠ). 그래서 .dockerignore 만들고 .git을 통채로 올려봤더니 이제 CPU/메모리가 꽉차는 에러는 더이상 발생하지 않게 돼었다!

 

# Git folder and history
.git
.gitignore

# IDE files
.idea/
.vscode/
*.iml

# Build artifacts (if not needed)
target/
build/
*.log

# OS files
.DS_Store
Thumbs.db

# Documentation
README.md
*.md

# CI/CD files
.github/

# Large/unnecessary files
*.ser
*.dump
*.log

 

초반부터 미리미리 하는게 좋은데, 중간에 실수로 잊어버려서 이런 사달이 난게 아닌가 싶다;; (앞으로는 기억할 듯 하다)

 

2) ERROR: failed to build: invalid tag ":": invalid reference format

AWS console에 들어가서 Task를 보니까... 자꾸 이렇게 에러가 떴어서 당연히 어플리케이션 실행조차 안되고 있는 상황이였다.

그래서 뭐지..?? 하면서 Github Secret도 다시 해보고 deploy.yml을 계속 조금씩 수정해나가면서 push 를 여러번 했고..그냥 여러가지 시도를 해봤었는데 안됐었다.

 

한참 후에야 REPO_URI가 제대로 안넘어오고 그냥 빈칸으로 넘어온다는 것을 깨달았다.

 

이런식으로 밑에 Skip output 'repo_uri' since it may contain secret. <<< 이 문구를 내가 그냥 넘어갔었는데, 알고보니까 이게 가장 결정적 힌트였다.... (앞으로 아무리 사소해(?)보이는 것들도 넘어가지 말자 ㅠㅠ). 

 

그래서 task에 들어가보면 자꾸 그런 오류가 떴던것이다.

그래서 굳이 기존 방식 대로 안하고 secret.ECR_REPOSITORY_URI를 바로 사용하는 걸로 바꾸고 다시 push + deploy해봤더니 드디어 성공했다....

 

(맨 위에 올린 deploy.yaml파일을 보면 된다)

 

 

드이어 성공한 워크플로우는 저런 warning이 안뜬다는 것을 볼 수 있다.

 

3) 테스트가 자꾸 실패

이건 내 실수인데, 앞으로 조심할 겸 미래의 나를 위해서라도 그냥 블로그에 적어둔다.

 

일단, intellij에서는 됐지만 CICD 파이프라인에는 자꾸 테스트가 실패했었다고 떴었다.

그래서 처음엔 어떤 테스트가 실패했는지도 안알려줘서 이부분을 추가했다. 

 

test.yml

  - name: Show test results on failure
    if: failure()
    run: |
      echo "=== Looking for failed tests ==="
      # Show only failed test results
      find . -name "*.xml" -path "*/test-results/*" -exec grep -l 'failures="[^0]"\|errors="[^0]"' {} \; -exec echo "=== Failed in: {} ===" \; -exec cat {} \;

      echo "=== Summary of all tests ==="
      find . -name "*.xml" -path "*/test-results/*" -exec echo "File: {}" \; -exec grep -o 'name="[^"]*" tests="[^"]*" skipped="[^"]*" failures="[^"]*" errors="[^"]*"' {} \;

  - name: Upload test reports
    uses: actions/upload-artifact@v4
    if: always() # Upload even if tests fail
    with:
      name: test-reports
      path: |
        build/reports/tests/test/
        build/test-results/test/
      retention-days: 30

 

그러더니 내가 예상했던 테스트 (AWS 관련/연결부분)이 아니라 그냥 DiscodeitApplicationTests 의 테스트 하나가 실행이 안돼었었고, 나머지 테스트는 다 잘되어있었다...!

 

그래서 먼저 @ActiveProfiles("test")를 해주고 이렇게 구현했다:

DiscodeitApplicationTests.java

@ActiveProfiles("test")
@SpringBootTest
class DiscodeitApplicationTests {

  @Test
  void contextLoads() {
  }

}

 

 

application-test.yml

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driverClassName: org.h2.Driver
    username: sa
    password:

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    show-sql: true
    hibernate:
      ddl-auto: create-drop

 

compose.yml

services:
	service:
    	....	
    db:
		....
	volumes:
      # mounts the volume
      - db-data:/var/lib/postgresql/data
      - ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro

 

하지만 이렇게 하면 에러가 났었다. 그 이유는 스프링부트는 어플리케이션/테스트가 시작될 때, classpath의 루트 경로 (src/main/resources나 src/test/resources 폴더를 자동으로 스캔한다). 이때 schema.sql이 있을 경우 이 파일을 현재 설정된 데이터소스 (DataSource)에 해당 스크립트를 자동으로 실행하려고 시도하는 것이다.

 

테스트를 실행할때:

  1. application-test.는 설정애 따라 H2 인메로리 DB를 실행함
  2. 스프링 부트는 src/main/resources 폴더에서 schema.sql을 발견함
  3. 스프링 부트는 이 schema.sql 파일의 내용을 H2 데이터베이스에 실행하려고 함
  4. 하지만 그 schema.sql 파일은 PostgreSQL 문법으로 작성되어있고 H2는 이해 X
  5. 그 결과 JDBC에러 발생!

그래서 schema.sql을 클래스패스가 아닌 다른 경로에 넣어주면 문제는 해결이 된다.

  • 스프링 부트가 테스트를 시작할 때 더 이상 schema.sql 파일을 자동으로 찾지 못한다. (클래스 패스에 없으니까)
  • 스프링 부트가 파일을 실행하지 않으니, 테스트는 ddl-auto: create-drop 설정에 따라 @Entity 클래스를 기반으로 H2에 맞는 스키마를 정상적으로 생성하게 된다.
  • 동시에, docker-compose.yml은 ./db/schema.sql라는 명시적인 경로를 통해 파일을 잘 찾아서 PostgreSQL 컨테이너를 초기화하는 데 사용할 수 있다.

나는 따로 db 폴더를 만들고 schema.sql을 거기로 옮겨서 문제를 해결했다.

 

compose.yml

services:
	service:
    	....	
    db:
		....
	volumes:
      # mounts the volume
      - db-data:/var/lib/postgresql/data
      - ./db/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro

 

나 자신 수고했다..