
개발 전 배포까지 염두하고 개발하기 위해서 CI/CD 기능 구현을 진행하였으며, 해당 기능은 Jenkins를 활용하여 연동하였습니다. 하지만, AWS 프리티어 수준의 CPU는 성능이 매우 낮아 별다른 구현이 없음에도 배포 시간이 2분 정도 소요되는 것을 확인했습니다. 이로 인해, 서버 외부에서 CI/CD 구축할 것이 나을 것이라고 판단하여 GitHub Actions를 활용한 CI/CD 환경 구축을 진행했습니다.
GitHub Actions를 활용하여 CI/CD 서버를 구축하는 과정에 대해서 정리한 포스팅입니다.

CI/CD 란, "지속적인 통합(Continous Integration)"과 "지속적인 배포(Continous Deplyment)"를 의미합니다.
지속적인 통합은 코드 변경사항이 발생 시마다 자동으로 빌드 → 테스트 → 통합(병합)을 수행합니다. 테스트 과정에서 conflict 처리 및 compile error를 처리할 수 있습니다.
지속적인 배포는 통합된 코드를 자동으로 운영 서버에 배포하는 프로세스를 말합니다. 이로 인해, 새로운 기능과 버그 수정 사항이 실제 서비스에 빠르게 반영되므로 사용자 경험 향상이 됩니다. 또한, 사용자 피드백을 수집하고 제품을 개선하는 속도를 향상시킬 수 있다는 장점이 있습니다.
GitHub 플랫폼에서 제공하는 지속 통합/배포 서비스를 의미합니다. 이를 사용하면 코드의 통합과 배포 프로세스를 자동화해줌으로써 개발 생산성을 높여줍니다.
또한, github actions에서는 build용 가상 머신인 github-hosted runners를 제공해줍니다. 가상 머신의 동작은 다음과 같습니다.
1. 레포지토리를 로컬에 복사합니다.
2. 테스트를 위한 프로그램을 설치합니다.
3. 코드를 검사하는 명령을 실행해줍니다.
가상 머신을 사용하기 위해서는 workflow에 대한 script를 작성하고 runs-on 기능을 사용하여 가상 머신의 유형(Unbuntu, Windows, MacOS)을 지정해줘야 합니다.
workflow
.github/workflows 내에 정의됩니다.Event
Jobs
Actions
Runner
env
${{env.등록한_환경변수명}}기존의 방식(AS-IS)의 문제점은 PR을 통해 코드 리뷰를 거친다고는 하지만, 분명히 놓치는 문제가 발생할 수 있습니다. 이로 인해 문제가 발생한 코드가 development 서버에 자동 통합 & 배포되면, 다시 PR을 작성해야 하므로 비용이 큽니다.
반면에, 개선 방식(TO-BE)으로 진행하게 되면 feature(기능구현) branch를 push할 때, build 및 테스트 코드를 진행하기 때문에 PR을 close 하기 전에 build 확인 및 테스트 코드 정상 동작 여부를 확이할 수 있기 때문에 좀 더 개발 비용이 적게 들어 빠르게 배포가 가능합니다.
따라서, TO-BE 방식의 흐름으로 workflow 스크립트를 작성하겠습니다.

root directory에 .github/workflows 를 생성 후 YAML 파일을 통해 script를 작성해줍니다.
우선, 기능 구현 → build & test 단계의 script부터 작성하겠습니다.
# workflow 이름 지정
name: CI/CD
# workflow trigger
on:
push:
branches:
- feature/*
env:
DOCKER_IMAGE_NAME: projectggb/back-end-dev:0.0.1
DOCKER_CONTAINER_NAME: ggb-back
jobs:
ci:
runs-on: ubuntu-latest
environment: feature
steps:
# Runner 에 repository 에 저장된 코드 복사
- name: Checkout code
uses: actions/checkout@v2
# SET UP JDK 17
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# APPLICATION-SECRET.YML 등록
- name: Create application-secret.yml
run: |
cat << EOF > src/main/resources/application-secret.yml
spring:
servlet:
multipart:
enabled: true
max-file-size: 20MB
max-request-size: 20MB
cloud:
aws:
credentials:
access-key: ${{ secrets.AWS_ACCESS_KEY }}
secret-key: ${{ secrets.AWS_SECRET_KEY }}
region:
static: ap-northeast-2
s3:
bucket: ${{ secrets.AWS_S3_BUCKET }}
EOF
# BUILD WITH GRADLE
- name: Build with gradle
run: ./gradlew clean build
event
feature/* 형태의 브렌치가 github에 push 시 정의한 jobs가 동작하게 됩니다.jobs
steps
작성 후 테스트 결과, 다음과 같이 feature/* 브렌치 github에 push 시 build & test가 정상적으로 완료되었습니다.

그 다음으로 개발(development) 서버에 대한 배포 workflow 입니다.
# workflow 이름 지정
name: CI/CD
# workflow trigger
on:
pull_request:
branches:
- develop
types:
- closed
env:
DOCKER_IMAGE_NAME: projectggb/back-end-dev:0.0.1
DOCKER_CONTAINER_NAME: ggb-back
jobs:
ci:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
environment: development
steps:
# Runner 에 repository 에 저장된 코드 복사
- name: Checkout code
uses: actions/checkout@v2
# SET UP JDK 17
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
# APPLICATION-SECRET.YML 등록
- name: Create application-secret.yml
run: |
cat << EOF > src/main/resources/application-secret.yml
spring:
servlet:
multipart:
enabled: true
max-file-size: 20MB
max-request-size: 20MB
cloud:
aws:
credentials:
access-key: ${{ secrets.AWS_ACCESS_KEY }}
secret-key: ${{ secrets.AWS_SECRET_KEY }}
region:
static: ap-northeast-2
s3:
bucket: ${{ secrets.AWS_S3_BUCKET }}
EOF
# BUILD WITH GRADLE
- name: Build with gradle
run: ./gradlew clean build
# DOCKER BUILDX 설정 - Docker 빌드 기능을 확장한 도구
- name: Set up docker buildx
uses: docker/setup-buildx-action@v1
# DOCKERHUB LOGIN
- name : Login to dockerhub
uses: docker/login-action@v1
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
password: ${{secrets.DOCKERHUB_TOKEN}}
# DOCKERHUB IMAGE BUILD AND PUSH
- name: Build and push docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{env.DOCKER_IMAGE_NAME}}
cd:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
environment: development
needs: ci
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Copy files via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST}}
username: ${{ secrets.EC2_USERNAME}}
key: ${{secrets.SSH_PRIVATE_KEY}}
script: |
docker pull ${{env.DOCKER_IMAGE_NAME}}
docker stop ${{env.DOCKER_CONTAINER_NAME}}
docker rm ${{env.DOCKER_CONTAINER_NAME}}
docker run -d --name ${{env.DOCKER_CONTAINER_NAME}} -p 8081:8080 ${{env.DOCKER_IMAGE_NAME}}
docker image prune -af
develop branch에 Pull Request가 close 된 경우, 실제 CI/CD 배포가 동작합니다.
ci
cd
작성 후 테스트 결과, 다음과 같이 develop 브렌치에 pull request 시 CI/CD가 정상적으로 완료되었습니다.


마지막으로, 운영(Production) 서버에 대한 배포 workflow 입니다.
# workflow 이름 지정
name: CI/CD
# workflow trigger
on:
pull_request:
branches:
- main
types:
- closed
env:
DOCKER_IMAGE_NAME: projectggb/back-end-prod:0.0.1
jobs:
ci:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
environment: production
steps:
# Runner 에 repository 에 저장된 코드 복사
- name: Checkout code
uses: actions/checkout@v2
# SET UP JDK 17
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# APPLICATION-SECRET.YML 등록
- name: Create application-secret.yml
run: |
mkdir -p src/main/resources
echo "${{secrets.SECRET_YML}}" | base64 --decode > src/main/resources/application-secret.yml
# BUILD WITH GRADLE
- name: Build with gradle
run: ./gradlew clean build
# DOCKER BUILDX 설정 - Docker 빌드 기능을 확장한 도구
- name: Set up docker buildx
uses: docker/setup-buildx-action@v1
# DOCKERHUB LOGIN
- name : Login to dockerhub
uses: docker/login-action@v1
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
password: ${{secrets.DOCKERHUB_TOKEN}}
# DOCKERHUB IMAGE BUILD AND PUSH
- name: Build and push docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{env.DOCKER_IMAGE_NAME}}
작성 후 테스트 결과, 다음과 같이 main 브렌치에 pull request 시 CI가 정상적으로 완료되었습니다.

actions/setup-java@v3 부터는 distribution 속성값을 필수로 지정해줘야 하지만, 하지않아서 발생한 오류.distribution: temurin 속성값 지정. # APPLICATION-SECRET.YML 등록
- name: Create application-secret.yml
run: |
cat << EOF > src/main/resources/application-secret.yml
spring:
servlet:
multipart:
enabled: true
max-file-size: 20MB
max-request-size: 20MB
cloud:
aws:
credentials:
access-key: ${{ secrets.AWS_ACCESS_KEY }}
secret-key: ${{ secrets.AWS_SECRET_KEY }}
region:
static: ap-northeast-2
s3:
bucket: ${{ secrets.AWS_S3_BUCKET }}
EOF
문제 상황 : Test 시 aws 관련 오류가 발생하면서, Build Fail
원인 : gradle build 및 test 실행 시 Spring Context 내 bean들을 전부 등록하는데, test 코드의 경우, ActiveProfiles="test" 설정으로 인해서 application-test.yml 파일을 참조하고 있기 때문에 TestService와 같은 클래스 bean 등록 시 필요한 속성값들을 읽어오지 못해 발생하는 오류였습니다.


해결 : application-test.yml 파일에도 관련 설정들을 추가해줍니다. 만약, 민감 정보가 포함되어 있는 경우, 해당 부분에는 임시값을 넣어줍니다. - 실제 테스트 코드를 작성한 상황이 아니기 때문에 bean만 등록시켜주도록만 해주면, 오류가 발생하지 않게 됩니다.
cd:
runs-on: ubuntu-latest
environment: development sudo usermod -aG docker $USER newgrp docker sudo systemctl restart docker위와 같이 GitHub Actions workflow 스크립트를 작성하여 실제 EC2 인스턴스에 서버를 CI/CD 방식으로 배포하였습니다. 이를 진행하면서 배포 wokflow에 대해서 다시 한번 고민하며 script를 작성할 수 있었습니다. 추가로 개선해야할 내용과 학습해야할 내용들을 정리해가며 포스팅해 나가겠습니다.