프로젝트 구조
- 언어 및 프레임워크: Java, Spring Boot
- 어플리케이션 배포 Platform: Docker
- Source 관리: Git & Github(저장소)
- 배포 클라우드 및 리소스: AWS EC2
CI 작성기는 아래 글을 참고해주세요.
[Spring] Github Action과 Docker를 활용한 CI 작성기
프로젝트 구조- 언어 및 프레임워크: Java, Spring Boot- 어플리케이션 배포 Platform: Docker- Source 관리: Git & Github(저장소) 현재 멀티 모듈로 구성되어 있는 Api 서버 제작 프로젝트를 하고 있습니다. 어플
happiestlife.tistory.com
이번 CD에서는 CI때와 달라진 부분이 있다면 멀티 모듈 프로젝트에서 단일 모듈 프로젝트로 변경되었다는 점입니다.
멀티 모듈로 프로젝트를 진행하다보니, 설계가 굉장히 중요하였습니다. 모듈 간 의존성이 복잡하게 얽히면서 구현부를 계속 수정해야 했습니다. 그로 인해 개발에 너무 많은 시간이 들어서 멀티 모듈을 가졌을 때의 이점보다 단점이 더 커졌습니다.
결국, 단일 모듈 프로젝트로 수정했습니다.
CD 흐름도
이번에 만든 CD의 흐름도는 아래와 같습니다.

1. 개발자가 생성된 PR의 머지를 승인함으로써 Github Action이 실행됩니다.
2. 현재 브랜치의 소스를 기반으로 빌드를 실행합니다.
3~4. 빌드된 도커 이미지를 AWS ECR에 저장합니다.
5. appleboy/ssh-action을 활용하여 EC2에 SSH 접속합니다.
6. AWS ECR에 저장된 앱 도커 이미지를 받아와 기존 앱을 종료하고 새로운 앱을 실행시킵니다. (조금의 다운타임 발생)
위 순서도를 바탕으로 작성한 github action.yml은 아래와 같습니다. 자세한 사항은 주석으로 작성하였습니다.
(Dockerfile은 "CI 작성기"에서 작성한 내용과 크게 다르지 않습니다. 멀티 모듈 -> 단일 모듈로만 수정)
name: Backend CI/CD on PR Merged
on:
pull_request:
branches: [develop]
types: [closed]
# OIDC 토큰 요청을 위한 권한 설정
permissions:
id-token: write # OIDC 인증을 위해 JWT를 요청하는데 필요
contents: read # actions/checkout@v4에서 코드를 가져오는데 필요
jobs:
build-and-deploy:
# merge가 완료되었을 때에만 github action 실행
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
# job 레벨에서 환경 변수 설정
env:
IMAGE_TAG: ${{ secrets.IMAGE_TAG }}
ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPOSITORY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
steps:
# 빌드 및 배포 기반 branch로 checkout (이 소스 기준: develop)
- name: Checkout source code
uses: actions/checkout@v4
# 빌드에 사용할 JDK 설정
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
# AWS 리소스에 접근하기 위한 자격증명
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
# 담당할 IAM 역할의 ARN 지정
role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}
# 역할 세션 이름 지정
role-session-name: GitHubActionsSession
aws-region: ${{ secrets.AWS_REGION }}
mask-aws-account-id: 'true' # AWS 계정 ID 마스킹 처리
# ECR에 빌드된 도커 이미지를 올리기 위해 로그인 => registry를 가져오기 위함
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
# application를 도커 이미지로 빌드 및 ECR에 배포
- name: Build and push Docker image to ECR
# step 레벨에서 사용할 환경 변수 설정
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
DB_URL: ${{ secrets.DB_URL }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
GOOGLE_EMAIL: ${{ secrets.GOOGLE_EMAIL }}
GOOGLE_APP_PW: ${{ secrets.GOOGLE_APP_PW }}
run: |
# Dockerfile을 사용하여 이미지 빌드
# 도커 빌드 시 --secret 옵션을 사용해야 최종 이미지에 남지 않기 때문에 활용
# 자세한 사항은 CI 작성기 참고
echo "Building Docker image..."
docker build \
--secret id=db_url,env=DB_URL \
--secret id=db_username,env=DB_USERNAME \
--secret id=db_password,env=DB_PASSWORD \
--secret id=google_email,env=GOOGLE_EMAIL \
--secret id=google_app_pw,env=GOOGLE_APP_PW \
--platform linux/amd64 \
-t $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.IMAGE_TAG }} .
# 빌드된 도커 이미지를 ECR로 푸시
echo "Pushing Docker image to ECR..."
docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.IMAGE_TAG }}
echo "Build and push docker image completed."
# applebot/ssh-action을 활용하여 EC2에 SSH 접속
- name: Deploy to AWS EC2
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.AWS_EC2_HOST }}
username: ${{ secrets.AWS_EC2_USERNAME }}
key: ${{ secrets.AWS_EC2_SSH_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
script: |
echo "Starting deployment on EC2 instance"
# 환경 변수 설정 (ECR 주소, 리포지토리, 새 이미지 태그)
ECR_REGISTRY=${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY=${{ env.ECR_REPOSITORY }}
IMAGE_TAG=${{ env.IMAGE_TAG }}
DB_URL=${{ secrets.DB_URL }}
DB_USERNAME=${{ secrets.DB_USERNAME }}
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
GOOGLE_EMAIL=${{ secrets.GOOGLE_EMAIL }}
GOOGLE_APP_PW="${{ secrets.GOOGLE_APP_PW }}"
# 위에서 했던 ECR 로그인은 Github Action에서의 로그인입니다.
# 이 로그인은 EC2 내부에서의 로그인으로 각각 다릅니다.
echo "Logging in to ECR from EC2"
sudo aws ecr get-login-password --region ${{ env.AWS_REGION }} | sudo docker login --username AWS --password-stdin $ECR_REGISTRY
# 빌드 step에서 빌드된 도커 이미지를 ECR에서 가져옵니다.
echo "Pulling new Docker image from ECR"
sudo docker pull $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
# 현재 실행중이던 app을 종료시키고 기존 도커 이미지도 삭제합니다.
echo "Stopping and removing existing Docker container"
sudo docker stop $(sudo docker ps -aq) && sudo docker rm $(sudo docker ps -aq)
sudo docker rmi $(sudo docker images -q)
# 새로 빌드된 도커 이미지를 실행시켜 app을 배포합니다.
echo "Running new Docker container with the latest image"
sudo docker run -d --name store-manager \
-p 8080:8080 \
-e DB_URL="$DB_URL" \
-e DB_USERNAME="$DB_USERNAME" \
-e DB_PASSWORD="$DB_PASSWORD" \
-e GOOGLE_EMAIL="$GOOGLE_EMAIL" \
-e GOOGLE_APP_PW="$GOOGLE_APP_PW" \
$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "Deployment completed successfully"
Github Action step 중 눈여겨 볼 라이브러리는 appleboy의 ssh-action입니다. 만약 비밀키가 치환된 통신내용이 특정인에게 노출된다면 분명 보안상 문제가 될 것입니다.
그래서 ssh-action github에 보면 "passphrase: ${{ secrets.PASSPHRASE }}" 추가하라고 나와 있습니다.
이는 비밀키가 PASSPHRASE에 넣은 비밀키로 암호화되어 ssh-action 서버 / 컨테이너로 전송되기 때문에 통신내용이 노출되어도 비밀키 자체는 노출되지 않는 다는 것을 의미합니다.

** CD flow에서 appleboy/ssh-action을 선택한 이유
CICD가 주목적이 아닌 사이드 프로젝트에서 CICD 로직이 한 곳에 모여있으면 빠르게 구축 및 유지보수할 수 있겠다고 생각했습니다.
그 결과, appleboy/ssh-action을 활용하면 CD를 Github Action에서 모두 구축할 수 있겠다 판단했고 결정하게 되었습니다.
수정 포인트
제가 생각한 위 Github Action의 단점은 2가지와 개선점 1가지입니다.
1. 배포시 잠깐이지만 다운타임이 발생한다 (단점1)
현재 배포 로직은 실행중인 앱을 종료시키고 새로운 앱을 시작하는 형식이어서 잠깐이지만 다운 타임이 발생합니다.
이는 많은 사용자가 사용하는 어플리케이션에서 치명적인 결함임으로 반드시 blue/green 배포 방식으로 수정되어야 합니다.
2. appleboy/ssh-action의 보안성 (단점2)
appleboy님은 ssh-action이 동작할 때 자격증명과 관련된 어떤 정보도 저장되지 않는다고 하였습니다. 물론 이 분의 말을 전적으로 믿을 수도 있지만, 저는 appleboy님이 "개인"이라는 점에서 이 말의 신빙성이 조금 떨어질 수 있다고 생각합니다.

AWS CICD 서비스 등 기업이 제공하는 서비스는 내부 보안 규칙 및 법에 입각하여 자격증명 같은 정보들을 저정합니다. (물론 그 기업이 보안 정보를 넘기는 등의 범죄 행위를 할 수도 있지만 단체이기 때문에 개인보다는 어렵다고 생각합니다.)
반면 개인은 한사람의 생각에 따라 원한다면 보안 관련 정보를 저장하고 악용할 수 있다는 점에서 보안상 좋지 못하다고 생각합니다.
따라서 저는 실제 운영 서비스에서는 appleboy/ssh-action 라이브러리보다는 Github Action과 AWS CICD 서비스를 같이 사용하는 것이 더 안전하다고 생각합니다.
(appleboy님을 신용할 수 없다는 이야기가 절대 아닙니다. appleboy님 뿐만 아니라 어떤 개인의 라이브러리 / 서비스이든 개인의 불확실성으로 인해 보안적으로 안전하지 못할 수 있다는 점을 의미합니다. appleboy님은 위 라이브러리 뿐만 아니라 여러 오픈 소스 프로젝트에 기여했을만큼 훌륭하고 좋은 개발자입니다.)
3. 의존성 및 기본 Docker 설정을 맡는 Base Docker image와 실제 어플리케이션을 빌드 및 실행하는 App Docker Image 분리하기
현재는 빠르게 개발을 하기 위해서 의존성 다운로드 및 기본 Docker 설정과 어플리케이션 빌드 및 실행을 모두 하나에서 진행합니다.
FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY src ./src
# 보안을 위한 시크릿 파일을 컨테이너에 마운트
RUN --mount=type=secret,id=db_url \
--mount=type=secret,id=db_username \
--mount=type=secret,id=db_password \
--mount=type=secret,id=google_email \
--mount=type=secret,id=google_app_pw \
DB_URL=$(cat /run/secrets/db_url) \
DB_USERNAME=$(cat /run/secrets/db_username) \
DB_PASSWORD=$(cat /run/secrets/db_password) \
GOOGLE_EMAIL=$(cat /run/secrets/google_email) \
GOOGLE_APP_PW=$(cat /run/secrets/google_app_pw) \
./gradlew build
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
COPY --from=builder /app/build/libs/*-SNAPSHOT.jar app.jar
# 애플리케이션이 사용할 포트 명시
EXPOSE 8080
# 컨테이너가 시작될 때 애플리케이션 실행
ENTRYPOINT ["java", "-jar", "app.jar"]
하지만 의존성과 기본 Docker 설정은 처음에 세팅되면 잘 바뀌지 않습니다. 반면 application code는 수시로 변경됩니다.
만약 의존성 다운로드와 기본 Docker 설정이 30분씩 걸리는 큰 작업이라면, 이를 CICD에 항상 포함하기에는 큰 부담입니다.
따라서 의존성을 다운로드하고 기본 Docker 설정을 담당하는 Base Docker image를 만들고, App Docker image에서는 base image를 바탕으로 app을 빌드 및 실행하도록 하면 훨씬 더 효율적인 CICD가 완성될 것입니다.
긴 글 읽어주셔서 감사합니다. 정정이 필요한 부분이 있다면 언제든지 부탁드리겠습니다.
참고
https://docs.aws.amazon.com/ko_kr/IAM/latest/UserGuide/id_roles_providers_create_oidc.html
https://docs.aws.amazon.com/ko_kr/AmazonECR/latest/userguide/docker-push-ecr-image.html
https://docs.aws.amazon.com/ko_kr/AmazonECR/latest/userguide/common-errors-docker.html
https://github.com/appleboy/ssh-action?tab=readme-ov-file#-introduction
'Backend > Spring' 카테고리의 다른 글
| [Spring] Github Action과 Docker를 활용한 CI 작성기 (0) | 2025.07.18 |
|---|---|
| [Spring] Multi-module 프로젝트에서 테스트용 docker-compose 파일 공통화 (0) | 2025.03.11 |
| [Spring] 멀티 모듈에 Spring Rest Docs 적용기 (0) | 2025.03.03 |
| [Gradle] Built-in Task의 내부 실행 순서 (0) | 2025.03.03 |
| [Gradle] Gradle Wrapper 개념 정리 (0) | 2025.02.08 |