
Docker 기반 실행 구조와 CI/CD 워크플로우 구성
AWS 인프라, 콘솔 대신 Terraform으로 관리해보자
그동안 프로젝트를 진행하면서 필요한 리소스들은 AWS 콘솔에서 직접 만들어서 사용했다. EC2, S3, RDS, Redis와 같은 리소스를 하나씩 늘려가면서 작업했다.콘솔에서 필요한 순간에 바로 만들 수 있
zerozeroseven.tistory.com
이전 글과 같이 Terraform으로 인프라 구성을 정리한 뒤에는 애플리케이션 배포 흐름을 잡는 작업을 진행했다.
이번에는 Dockerfile 추가부터 GitHub Actions 기반 CI/CD 워크플로우 구성까지 정리해보려고 한다.
Dockerfile 추가
애플리케이션을 서버에서 일관된 방식으로 실행하려면 먼저 컨테이너 실행 구조가 필요했다.
로컬에서는 잘 되던 실행 방식이 서버에서는 달라지는 경우가 많았기 때문에, 실행 환경 자체를 이미지로 고정하는 것이 우선이라고 판단했다.
Spring Boot 애플리케이션 이미지는 멀티스테이지 빌드로 구성했다.
빌드 단계와 실행 단계를 분리하면 최종 이미지 크기를 줄일 수 있고, 실행 컨테이너에 불필요한 빌드 도구가 포함되지 않는 장점이 있다.
# =================
# 애플리케이션 빌드 단계
# =================
FROM gradle:8.14.3-jdk21 AS builder
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts .
COPY settings.gradle.kts .
COPY src src
RUN chmod +x ./gradlew
RUN ./gradlew bootJar --no-daemon
# =================
# 애플리케이션 실행 단계
# =================
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
특히 실행 이미지에 jre 기반 이미지를 사용하면서, 빌드와 실행 책임을 분리한 구조를 초기에 잡아둘 수 있었다.
실행 환경 분리
Dockerfile을 추가한 뒤에는 실행 환경을 분리했다.
처음에는 하나의 설정으로 관리해도 괜찮을 것 같았지만, dev/prod/test 환경이 섞이기 시작하면 설정 충돌이 생기기 쉬워서 실행 설정은 아래처럼 나눴다.
- application-local.yml
- application-dev.yml
- application-prod.yml
민감한 값은 코드에 직접 넣지 않고 .env로 분리해 관리했다. 이렇게 해두니 환경별 설정이 훨씬 명확해졌다.
docker-compose 구성
다음으로 docker-compose 설정을 추가했다.
당시에는 ECR 기반 이미지 배포 전 단계였기 때문에, EC2에서 직접 빌드하는 방식으로 구성했다.
services:
app-dev:
build:
context: .
dockerfile: Dockerfile
container_name: moongchijang-dev
env_file:
- .env.dev
ports:
- "8081:8080"
restart: unless-stopped
app-prod:
build:
context: .
dockerfile: Dockerfile
container_name: moongchijang-prod
env_file:
- .env.prod
ports:
- "8082:8080"
restart: unless-stopped
이 구조로 먼저 dev/prod 컨테이너 분리 실행을 확인했고, 실제로 EC2에서 빌드와 실행이 정상 동작하는지 점검했다.
GitHub Actions 배포 자동화
컨테이너 실행 흐름이 확인된 뒤에는 배포 자동화를 붙였다.
수동으로 EC2에 접속해서 명령어를 반복하는 방식은 초기 검증 단계에서는 가능했지만, 배포 빈도가 올라가면 실수 가능성과 시간 비용이 커진다.
그래서 GitHub Actions에서 dev/prod 워크플로우를 분리하고, 브랜치 기준으로 배포가 자동으로 연결되도록 구성했다.
Dev 배포 워크플로우
name: Deploy Dev
on:
push:
branches:
- develop
jobs:
deploy:
runs-on: ubuntu-latest
environment: dev
steps:
- name: Deploy to dev container on EC2
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
set -e
cd ${{ secrets.EC2_APP_DIR }}
git fetch origin
git checkout develop
git pull origin develop
docker compose up -d --build app-dev
dev는 빠른 반영이 중요해서 테스트 단계를 배포 workflow 안에 중복으로 두지 않고, 개발 브랜치 기준으로 즉시 반영되는 흐름을 우선했다.
Prod 배포 워크플로우
name: Deploy Prod
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Run tests
run: ./gradlew test --no-daemon
deploy:
runs-on: ubuntu-latest
needs: test
environment: production
steps:
- name: Deploy to prod container on EC2
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
set -e
cd ${{ secrets.EC2_APP_DIR }}
git fetch origin
git checkout main
git pull origin main
docker compose up -d --build app-prod
prod는 안정성을 우선해서 테스트 통과 후 배포하도록 구성했다.
이렇게 분리해두니 dev와 prod의 운영 목적 차이를 워크플로우 레벨에서 명확하게 가져갈 수 있었다.
마무리
이번 단계에서는 Docker 기반 실행 구조를 먼저 고정하고, GitHub Actions를 붙여 dev/prod 배포 흐름을 자동화하는 데 집중했다.
예전처럼 서버에 직접 접속해서 배포하던 방식에서, 브랜치 기준으로 자동 배포되는 흐름으로 바꿀 수 있었다. 아직 개선할 부분은 남아 있지만, 배포 과정 자체는 이전보다 훨씬 단순하고 관리하기 쉬워졌다.
다음 글에서는 이 구조를 바탕으로 Nginx 리버스 프록시 구성과 ECR 기반 이미지 배포 방식으로 확장한 과정을 정리해보려고 한다.
'Backend' 카테고리의 다른 글
| 배포 시간 14분에서 53초로: 빌드 구조 개선과 ARM64 정합성 해결 (0) | 2026.05.31 |
|---|---|
| 포트는 하나로, 배포는 더 명확하게: Nginx 리버스 프록시와 ECR 전환기 (0) | 2026.05.08 |
| AWS 인프라, 콘솔 대신 Terraform으로 관리해보자 (0) | 2026.04.16 |
| 콤마 하나 때문에 구글 로그인이 실패했던 이유 (3) | 2026.03.14 |