심화로 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)에 해당 스크립트를 자동으로 실행하려고 시도하는 것이다.
테스트를 실행할때:
- application-test.는 설정애 따라 H2 인메로리 DB를 실행함
- 스프링 부트는 src/main/resources 폴더에서 schema.sql을 발견함
- 스프링 부트는 이 schema.sql 파일의 내용을 H2 데이터베이스에 실행하려고 함
- 하지만 그 schema.sql 파일은 PostgreSQL 문법으로 작성되어있고 H2는 이해 X
- 그 결과 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
나 자신 수고했다..
'Codeit > 스프린트 과제' 카테고리의 다른 글
| [sprint8] RDS/EC2, ECR, ECS 배포 (4) | 2025.08.27 |
|---|---|
| [sprint8] S3 관련 코드 + S3Client와 Presigned Url (3) | 2025.08.25 |
| [sprint8] 어플리케이션 컨테이너화 - dockerfile, docker compose, docker volume (1) | 2025.08.22 |
| [sprint5] 과제에서 사용한 swagger 간단하게 정리 (0) | 2025.07.14 |
| [sprint4] IOException과 @Transactional (멘토님 피드백) (0) | 2025.07.14 |