CI가 없을 시 : 개발자들은 일명
머지데이
라는 날을 통해 모든 분기 소스코드를 병합해야 했다.
- 굉장히 많은 수작업이 동반되었고 결과적으로 많은 리소스를 낭비하게 됨
- 개발자들이 코드를 병합할 때 개발자의 실수로 에러가 발생하는 코드를 병합하는 과정이 반복이 된다면, 에러가 발생했을 때 어느 부분에서 발생했는지 디버깅하기 굉장히 어려움
CI : 어플리케이션의 새로운 코드 변경 사항이 정기적으로 빌드 및 테스트 되어 공유 레포지토리에 통합하는 것을 의미
CD가 없을 시 : 서버의 규모가 커져 수십, 수백대로 늘어났다고 가정을 해보았을 때 모든 서버에 일일이 접속해 배포 스크립트를 수동으로 실행시켜야 한다.
CD : 지속적인 서비스 제공 혹은 지속적인 배포를 의미
Continuous Delivery
는 공유 레포지토리로 자동으로 Release 하는 것Continuous Deployment
는 Production 레벨까지 자동으로 deploy 하는 것CI/CD의 필요성과 각각 무엇을 의미하는 것인지 알아보았다. Github Action을 이용한 CI/CD를 적용하는 방법을 찾아보니 대표적으로 S3 + AWS CodeDeploy를 이용한 방법과 Docker를 이용한 방법이 있었다. 내가 이번에 선택한 방법은 Docker인데, 그 이유는 CodeDeploy의 디버깅 어려움 + AWS EC2를 옮기거나 확장할 때 각 서버별 환경 설정을 맞춰주기가 번거롭다는 게시글 을 보았기 때문이다.
그럼 이제 본격적으로 내가 적용한 CI/CD 과정을 나열해 보겠다.
다음과 같이 배포, 로컬, 테스트.yml을 만들었고 application.yml을 간단히 작성하였다.
이때 build.gradle에서 submodule인 application-config를 읽어오기 위해 다음과 같이 설정을 해야했다.
Spring Boot Layer Index
이 부분을 확인해 보면 된다.최적화된 Docker 이미지를 더 쉽게 생성할 수 있도록 Spring Boot는 jar에 레이어 인덱스 파일을 추가하는 기능을 지원합니다. 이 파일은 레이어 목록과 그 안에 포함되어야 하는 jar의 부분을 제공합니다. 인덱스의 레이어 목록은 Docker/OCI 이미지에 레이어를 추가해야 하는 순서에 따라 정렬됩니다. 기본적으로 다음과 같은 레이어가 지원됩니다.
이 계층화는 애플리케이션 빌드 간에 변경될 가능성에 따라 코드를 분리하도록 설계되었습니다. 라이브러리 코드는 빌드 간에 변경될 가능성이 적으므로 자체 레이어에 배치하여 툴링이 캐시에서 레이어를 재사용할 수 있도록 합니다. 애플리케이션 코드는 빌드 간에 변경될 가능성이 더 높으므로 별도의 레이어에 격리됩니다.
즉 jar 파일은 여러 계층으로 분리되어 있는데, 애플리케이션 빌드 간에 변경될 가능성에 따라 애플리케이션의 일부를 분리하여 Docker 이미지 레이어를 더욱 효율적으로 만들 수 있다는 것이다.
FROM bellsoft/liberica-openjdk-alpine:17 as build
WORKDIR /workspace/app
# Copy the built JAR file
COPY build/libs/*.jar .
# Unpack the built application
RUN mkdir -p target/extracted
RUN java -Djarmode=layertools -jar *.jar extract --destination target/extracted
FROM bellsoft/liberica-openjdk-alpine:17
VOLUME /tmp
ARG EXTRACTED=/workspace/app/target/extracted
# Copy over the unpacked application
COPY --from=build ${EXTRACTED}/dependencies/ ./
COPY --from=build ${EXTRACTED}/spring-boot-loader/ ./
COPY --from=build ${EXTRACTED}/snapshot-dependencies/ ./
COPY --from=build ${EXTRACTED}/application/ ./
ENTRYPOINT ["java","-Dspring.profiles.active=dev","org.springframework.boot.loader.JarLauncher"]
jar {
enabled = false
}
RUN java -Djarmode=layertools -jar *.jar extract --destination target/extracted
이 명령어를 실행 해주어야 한다. jar 파일을 레이어 도구로 실행하고 jar파일에서 필요한 파일을 추출하여 target/extracted 디렉토리에 저장되게 한다.org.springframework.boot.loader.JarLauncher
가 아닌 org.springframework.boot.loader.launch.JarLauncher
로 해야했다.name: SpringBoot Application CI-CD with Gradle
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
checks: write
pull-requests: write
jobs:
spring-boot-ci-cd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/gradle-build-action@v3
- name: Build with Gradle
run: ./gradlew clean build -x test
- name: Test with Gradle
run: SPRING_PROFILES_ACTIVE=[test] ./gradlew test
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2.15.1
if: always()
with:
files: '**/build/test-results/test/TEST-*.xml'
- name: Set Version
id: set_current_date
run: echo "version=$(date '+%Y-%m-%d-%H-%M-%S')" >> $GITHUB_OUTPUT
- name: Docker Image Build
run: |
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/springboot:${{ steps.set_current_date.outputs.version }} .
docker tag ${{ secrets.DOCKERHUB_USERNAME }}/springboot:${{ steps.set_current_date.outputs.version }} ${{ secrets.DOCKERHUB_USERNAME }}/springboot:latest
- name: Docker Login
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Docker Hub Push
run: |
docker push ${{ secrets.DOCKERHUB_USERNAME }}/springboot:${{ steps.set_current_date.outputs.version }}
docker push ${{ secrets.DOCKERHUB_USERNAME }}/springboot:latest
- name: Docker Deploy In EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.AWS_EC2_HOST }}
username: ec2-user
key: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
script: |
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/springboot
docker stop
docker run --name springboot --rm -d -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/springboot
docker system prune -f
Local에서는 test가 잘 진행되었는데 github action에서는 Build With Gradle에서 자꾸 에러가 발생하였다. 찾아보니 테스트를 실행할 때 데이터베이스와 연결이 안되서 발생한다고 하는데 application-test.yml에서 처음에는 RDS(mysql) 주소로 연결을 했다. 하지만 문제는 마찬가지였다.(왜 그런건진 잘 모르겠다ㅠㅠ) 그런데 보통 test db를 설정할 때에는 인메모리 모드 H2 데이터베이스(테스트 코드가 실행될 때 설정한 데이터베이스가 메모리에 올라가고, 테스트가 종료되면 데이터베이스가 메모리에서 내려가게 된다.)를 사용하는 경우도 많아서 build.gradle에 runtimeOnly 'com.h2database:h2'
을 추가하고 다음과 같이 작성을 하였더니 해결하였다.
Docker Deploy In EC2 부분에서 key: ${{ secrets.AWS_EC2_PRIVATE_KEY }} 에는 EC2생성할 때 받은 .pem 키 값을 넣어야 한다. 키는
-----BEGIN RSA PRIVATE KEY-----
~XXXXX~
-----END RSA PRIVATE KEY-----
이렇게 구성이 되어있는데, 안에 있는 값만 넣지 말고 --- 이것 부터 끝까지 다 넣어야 한다.
예전부터 github action가지고 CI/CD를 해보고 싶었는데, 계속 미루고 미루었었다... 참여중인 '글또'커뮤니티에서 같은 조 백엔드 개발자 분이 Jenkins에서 Github Action으로 이전하여 CI/CD를 구축한 글을 보고 이번 기회에 나도 한번 해봐야겠다는 생각으로 해보았다. 향상된 도커 이미지도 작성해보고, 혼자 구축해보고 트러블 슈팅도 해보니 한 층 레벨업이 된 기분이다~
spring-boot-docker docs
Spring Layering Docker Images docs
github action workflow docs
docker image tag docs
github action + docker ci/cd 예시1
github action + docker ci/cd 예시2
H2 DB 타입
CI-CD란 무엇인가
CI-CD 유튜브1
CI-CD 유튜브2