본문 바로가기
Docker + CI,CD

로컬 개발에서 AWS 운영 서비스까지

by JuNo_12 2025. 6. 23.

순서대로만... 따라하시면... 될겁니다...

 

학습 목표

  1. 컨테이너화의 필요성과 Docker 활용법
  2. 클라우드 인프라스트럭처의 구성 요소
  3. CI/CD 파이프라인의 설계와 구현
  4. 운영 환경에서의 서비스 배포 전략
 

선수 지식

  • Spring Boot 기반 웹 애플리케이션 개발
  • Git을 통한 버전 관리
  • 리눅스 기본 명령어
  • 네트워킹 기초 (포트, IP 개념)
 

Chapter 1: 현재 상황 분석

1.1 로컬 개발 환경의 특성과 한계

현재 상황: 개발자의 로컬 머신에서만 실행되는 Spring Boot 애플리케이션|

로컬 환경의 구성요소:

  • Spring Boot 애플리케이션 (localhost:8080)
  • 로컬 데이터베이스 (MySQL/H2)
  • 개발자 PC의 특정 환경 (Java 버전, OS, 설정 등)

핵심 문제점:

  1. 접근성 제약: 개발자 PC에서만 접근 가능
  2. 가용성 문제: PC 종료 시 서비스 중단
  3. 환경 의존성: "내 컴퓨터에서는 됩니다" 문제
  4. 확장성 부족: 사용자 증가 시 대응 불가

 

 

1.2 목표: 전체 배포 로드맵 이해

최종 목표: 코드 푸시만으로 자동 배포되는 24시간 운영 서비스

4단계 로드맵:

1단계: 로컬 개발 (현재 상황)

  • Spring Boot 애플리케이션 완성
  • 로컬 DB 연동
  • localhost:8080에서 정상 동작

2단계: 컨테이너화

  • Dockerfile 작성으로 환경 독립성 확보
  • Docker 이미지 빌드
  • 컨테이너로 실행하여 환경 문제 해결

3단계: AWS 인프라 구축

  • EC2 인스턴스 (실제 서버)
  • RDS 데이터베이스 (관리형 DB)
  • 보안 그룹 설정 (네트워크 보안)

4단계: CI/CD 자동화

  • GitHub Actions으로 자동 배포
  • 코드 푸시 → 자동 테스트 → 자동 배포
  • 완전 자동화된 개발 workflow

각 단계별 핵심 질문:

  1. "왜 로컬에서 잘 되는데 도커로 만들어야 할까?"
  2. "AWS가 정확히 어떤 역할을 하는 거지?"
  3. "CI/CD가 없어도 배포는 되는 거 아닌가?"
  4. "테스트 코드와 CI는 어떤 관계인가?"

Chapter 2: 컨테이너화 (Containerization)

2.1 왜 컨테이너화가 필요한가?

환경 의존성 문제:

개발 환경 vs 운영 환경 불일치:

 

Docker의 해결책: 컨테이너는 애플리케이션과 모든 의존성을 하나의 패키지로 묶어서, 어떤 환경에서든 동일하게 실행되도록 보장합니다.

Docker 컨테이너에 포함되는 것들:

  • Java 17 Runtime (내장)
  • Spring Boot 애플리케이션 (내장)
  • 필요한 라이브러리 (내장)
  • 설정 파일 (내장)
  • 환경 변수 (내장)

 

2.2 Dockerfile 작성 및 이해

Dockerfile의 구조: 레이어 기반 시스템

# 1. 베이스 이미지 설정 (OS + Java Runtime)
FROM openjdk:17-jre-slim

# 2. 메타데이터 추가
LABEL maintainer="student@university.edu"
LABEL version="1.0"

# 3. 작업 디렉토리 설정
WORKDIR /app

# 4. 애플리케이션 파일 복사
COPY build/libs/*.jar app.jar

# 5. 포트 노출 (문서화 목적)
EXPOSE 8080

# 6. 시작 명령어 정의
ENTRYPOINT ["java", "-jar", "app.jar"]

 

각 명령어 설명:

  • FROM: 기본이 되는 운영체제와 런타임 설정
  • WORKDIR: 컨테이너 내부의 작업 디렉토리
  • COPY: 로컬 파일을 컨테이너로 복사
  • EXPOSE: 사용할 포트 번호 명시
  • ENTRYPOINT: 컨테이너 시작 시 실행할 명령어

 

레이어 시스템 이해: Docker는 각 명령어마다 새로운 레이어를 생성합니다:

  • Layer 1: Ubuntu + Java 17 Runtime
  • Layer 2: 메타데이터 + 작업 디렉토리
  • Layer 3: Spring Boot JAR 파일
  • Layer 4: 포트 설정 + 시작 명령어

 

실습: 빌드 과정

# Step 1: 애플리케이션 빌드
./gradlew clean build

# Step 2: Docker 이미지 생성
docker build -t my-spring-app .

# Step 3: 컨테이너 실행
docker run -p 8080:8080 my-spring-app

# Step 4: 테스트
curl http://localhost:8080/health

 

 

2.3 Docker Compose를 통한 Multi-container 환경

왜 Docker Compose가 필요한가? 실제 애플리케이션은 보통 여러 서비스로 구성됩니다:

  • 애플리케이션 서버
  • 데이터베이스
  • 캐시 서버 (Redis)
  • 등등...

docker-compose.yml 작성:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/myapp
    depends_on:
      - db
    networks:
      - app-network

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: myapp
      MYSQL_USER: appuser
      MYSQL_PASSWORD: apppassword
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - app-network

volumes:
  db_data:

networks:
  app-network:
    driver: bridge

 

Docker Compose의 장점:

  1. 서비스 간 네트워킹: 컨테이너들이 서로 통신 가능
  2. 의존성 관리: depends_on으로 시작 순서 제어
  3. 환경 변수 관리: 각 서비스별 설정 분리
  4. 볼륨 관리: 데이터 영속성 보장

 

실행 명령어:

# 모든 서비스 시작
docker-compose up -d

# 로그 확인
docker-compose logs

# 서비스 중지 및 삭제
docker-compose down

Chapter 3: AWS 클라우드 인프라스트럭처

3.1 Docker Hub vs AWS: 역할 차이 이해

나의 오해: "Docker Hub에 올리면 배포 완료 아닌가?"

 

Docker Hub = 창고 (저장소):

  • 이미지를 저장만 함
  • 실제로 실행되지 않음
  • 다운로드만 가능

AWS = 실제 컴퓨터 (실행 환경):

    • 이미지를 다운받아서 실행
    • 24시간 가동되는 서버
    • 외부에서 접근 가능

전체 배포 과정:

1. 로컬에서 이미지 빌드
   ↓
2. Docker Hub에 업로드 (저장)
   ↓
3. AWS EC2에서 이미지 다운로드
   ↓
4. EC2에서 컨테이너 실행 (실제 서비스)
   ↓
5. 외부 사용자들이 접속 가능

 

 

3.2 AWS 아키텍처 설계

목표 아키텍처:

Internet
    │
    ▼
┌──────────────────────────────────────┐
│            AWS Cloud                 │
│  ┌─────────────────────────────────┐ │
│  │         EC2 Instance            │ │  ← 애플리케이션 서버
│  │    Docker Container 실행        │ │
│  └─────────────────────────────────┘ │
│  ┌─────────────────────────────────┐ │
│  │           RDS MySQL             │ │  ← 데이터베이스
│  └─────────────────────────────────┘ │
│  ┌─────────────────────────────────┐ │
│  │            S3 Bucket            │ │  ← 파일 저장소
│  └─────────────────────────────────┘ │
└──────────────────────────────────────┘

 

각 서비스의 역할:

EC2 (Elastic Compute Cloud):

  • 가상 서버 역할
  • Docker 컨테이너 실행 환경
  • 24시간 가동
  • 외부 접근 가능한 Public IP

RDS (Relational Database Service):

  • 관리형 데이터베이스 서비스
  • MySQL 8.0 엔진 사용
  • 자동 백업, 보안 패치
  • EC2에서만 접근 가능 (보안)

S3 (Simple Storage Service):

  • 파일 저장소
  • 프로필 이미지, 정적 파일 저장
  • 전세계 어디서든 접근 가능
  • 무제한 용량

 

3.3 AWS 서비스 설정 실습

3.3.1 EC2 인스턴스 생성:

인스턴스 설정:

  • AMI: Ubuntu Server 22.04 LTS
  • 인스턴스 타입: t3.micro (1 vCPU, 1GB RAM)
  • 스토리지: 8GB gp3
  • 키 페어: 새로 생성 (.pem 파일 다운로드)

 

보안 그룹 설정:

Inbound Rules:
- SSH (22): My IP (SSH 접속용)
- HTTP (80): 0.0.0.0/0 (웹 트래픽)
- Custom TCP (8080): 0.0.0.0/0 (Spring Boot)

 

탄력적 IP 할당:

  • EC2 재시작 시에도 IP 고정
  • 도메인 연결 시 필요

 

3.3.2 RDS 설정:

데이터베이스 설정:

  • 엔진: MySQL 8.0
  • 템플릿: 프리 티어
  • 인스턴스 클래스: db.t3.micro
  • 스토리지: 20GB gp2

네트워크 보안:

RDS Security Group:
- Type: MySQL/Aurora (3306)
- Source: EC2 Security Group ID
- Description: MySQL access from EC2

 

3.3.3 S3 버킷 생성:

  • 버킷 이름: unique-bucket-name-12345
  • 리전: ap-northeast-2 (서울)
  • 퍼블릭 액세스: 읽기만 허용

 

 

3.4 수동 배포 테스트

EC2에 SSH 접속:

# pem 키 권한 설정
chmod 400 my-key.pem

# EC2 접속
ssh -i my-key.pem ec2-user@[EC2-PUBLIC-IP]

 

EC2에서 Docker 설치:

# Docker 설치
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user

# 재접속 후 Docker 테스트
docker --version

 

애플리케이션 배포:

# Docker Hub에서 이미지 다운로드
docker pull your-username/my-spring-app:latest

# 컨테이너 실행
docker run -d --name my-app -p 80:8080 \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://[RDS-ENDPOINT]:3306/myapp \
  -e SPRING_DATASOURCE_USERNAME=appuser \
  -e SPRING_DATASOURCE_PASSWORD=apppassword \
  your-username/my-spring-app:latest

# 실행 확인
docker ps
curl localhost:80/health

 

외부 접속 테스트:

  • 브라우저에서 http://[EC2-PUBLIC-IP] 접속
  • API 테스트: http://[EC2-PUBLIC-IP]/api/test

Chapter 4: CI/CD 파이프라인

4.1 CI/CD와 테스트 코드의 관계

4.2 GitHub Actions 워크플로우 구조

GitHub Actions의 구조:

Workflow (전체 자동화 과정)
├── Job 1: 테스트
│   ├── Step 1: 코드 다운로드
│   ├── Step 2: Java 설치
│   └── Step 3: 테스트 실행
└── Job 2: 배포 (테스트 성공 시에만)
    ├── Step 1: 도커 빌드
    ├── Step 2: AWS 업로드
    └── Step 3: 서비스 재시작

 

워크플로우 실행 과정:

1단계: 트리거 감지

  • 개발자가 git push origin main 실행
  • GitHub이 main 브랜치 변경 감지
  • .github/workflows/deploy.yml 파일 실행

2단계: 가상머신 생성

  • GitHub이 ubuntu-latest 가상머신 생성
  • 완전히 깨끗한 환경에서 시작

3단계: 테스트 Job 실행 (2-5분 소요)

  • 코드 다운로드 (checkout)
  • Java 17 설치
  • 의존성 다운로드
  • 테스트 실행 (./gradlew test)

4단계: 배포 Job 실행 (3-10분 소요, 테스트 성공 시에만)

  • Docker 이미지 빌드
  • AWS에 업로드
  • EC2에서 컨테이너 재시작

5단계: 완료 또는 실패

  • 성공: 새 버전이 자동 배포됨
  • 실패: 즉시 중단, 오류 알림 발송

 

4.3 GitHub Actions 설정 실습

파일 구조:

프로젝트 루트/
├── .github/
│   └── workflows/
│       └── deploy.yml
├── Dockerfile
├── docker-compose.yml
└── src/

 

deploy.yml 작성:

name: Spring Boot CI/CD

on:
  push:
    branches: [main]  # main 브랜치 푸시 시 실행

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
        
    - name: Run tests
      run: ./gradlew test
      
    - name: Build application
      run: ./gradlew build

  deploy:
    needs: test  # 테스트 성공 후에만 실행
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        
    - name: Build application
      run: ./gradlew build
      
    - name: Build Docker image
      run: docker build -t ${{ secrets.DOCKER_USERNAME }}/my-app:latest .
      
    - name: Login to Docker Hub
      run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
      
    - name: Push Docker image
      run: docker push ${{ secrets.DOCKER_USERNAME }}/my-app:latest
      
    - name: Deploy to EC2
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ${{ secrets.EC2_USER }}
        key: ${{ secrets.EC2_PRIVATE_KEY }}
        script: |
          docker pull ${{ secrets.DOCKER_USERNAME }}/my-app:latest
          docker stop my-app || true
          docker rm my-app || true
          docker run -d --name my-app -p 80:8080 \
            -e SPRING_DATASOURCE_URL=${{ secrets.DB_URL }} \
            -e SPRING_DATASOURCE_USERNAME=${{ secrets.DB_USERNAME }} \
            -e SPRING_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }} \
            ${{ secrets.DOCKER_USERNAME }}/my-app:latest

 

GitHub Secrets 설정: Repository Settings → Secrets and variables → Actions에서 다음 값들 설정:

DOCKER_USERNAME: Docker Hub 사용자명
DOCKER_PASSWORD: Docker Hub 비밀번호
EC2_HOST: EC2 퍼블릭 IP
EC2_USER: ec2-user
EC2_PRIVATE_KEY: .pem 파일 내용 전체
DB_URL: jdbc:mysql://[RDS-ENDPOINT]:3306/myapp
DB_USERNAME: appuser
DB_PASSWORD: apppassword

Chapter 5: 종합 실습 가이드

5.1 단계별 체크리스트

Phase 1: 로컬 개발 완료

  • Spring Boot 애플리케이션 개발 완료
  • 로컬에서 정상 실행 확인 (localhost:8080)
  • 테스트 코드 작성 및 통과
  • 데이터베이스 연동 확인
  • ./gradlew build 성공

Phase 2: 컨테이너화

  • Dockerfile 작성
  • docker build -t my-app . 성공
  • docker run으로 로컬 테스트
  • docker-compose.yml 작성 (선택)
  • 컨테이너 환경에서 정상 동작 확인

Phase 3: AWS 인프라 구축

  • AWS 계정 생성
  • EC2 인스턴스 생성 (t3.micro, Ubuntu)
  • 보안 그룹 설정 (SSH, HTTP, 8080)
  • 탄력적 IP 할당
  • RDS MySQL 인스턴스 생성
  • S3 버킷 생성
  • IAM 사용자 및 정책 설정

Phase 4: 수동 배포 테스트

  • EC2에 Docker 설치
  • 로컬 이미지를 Docker Hub에 푸시
  • EC2에서 이미지 다운로드
  • 컨테이너 실행 및 포트 연결
  • 외부에서 접속 테스트
  • RDS 연결 확인
  • Health Check API 구현

Phase 5: CI/CD 자동화

  • .github/workflows/deploy.yml 생성
  • GitHub Repository Secrets 설정
  • 워크플로우 테스트 (테스트 → 빌드 → 배포)
  • 푸시 시 자동 배포 확인

Phase 6: 운영 준비 완료

  • 도메인 연결 (선택)
  • HTTPS 인증서 설정 (선택)
  • 모니터링 설정
  • 로그 관리 시스템
  • 성능 테스트
  • 서비스 런칭!

 

 

5.2 자주 발생하는 문제와 해결책

Docker 관련 문제:

문제: "컨테이너가 즉시 종료됨" 해결:

  1. docker logs [컨테이너명]으로 로그 확인
  2. 보통 JAR 파일 경로 오류: COPY build/libs/*.jar app.jar 경로 재확인
  3. 포트 충돌: docker run -p 8081:8080 다른 포트 시도

 

AWS 연결 문제:

문제: "EC2에 SSH 접속 안됨" 해결:

  1. 보안 그룹에서 SSH(22) 포트 오픈 확인
  2. pem 키 파일 권한: chmod 400 key.pem
  3. 올바른 사용자명 사용: ssh -i key.pem ec2-user@퍼블릭IP

 

데이터베이스 연결 문제:

문제: "RDS 연결 실패" 해결:

  1. RDS 보안 그룹에서 MySQL(3306) 포트 오픈
  2. Source를 EC2 보안 그룹 ID로 설정
  3. application.yml에서 RDS 엔드포인트 URL 확인

 

네트워크 문제:

문제: "외부에서 접속 안됨" 해결:

  1. 보안 그룹에서 HTTP(80), Custom(8080) 포트 오픈
  2. Source: 0.0.0.0/0 (전체 허용)
  3. EC2 내부에서 curl localhost:8080 테스트 먼저

 

 

5.3 핵심 명령어 요약

로컬 개발:

# 빌드 및 테스트
./gradlew clean build test

# Docker 이미지 빌드 및 실행
docker build -t my-app .
docker run -p 8080:8080 my-app

 

Docker Hub 업로드:

# 태그 및 푸시
docker tag my-app username/my-app:latest
docker push username/my-app:latest

 

AWS 배포:

# EC2 접속
ssh -i key.pem ec2-user@server-ip

# 배포
docker pull username/my-app:latest
docker stop my-app || true
docker rm my-app || true
docker run -d --name my-app -p 80:8080 username/my-app:latest

 

디버깅 명령어:

# Docker 디버깅
docker logs my-app
docker exec -it my-app bash
docker ps -a

# 네트워크 디버깅
curl localhost:8080/health
netstat -tlnp | grep 8080
ping rds-endpoint

# 시스템 모니터링
top
free -h
df -h

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Docker + CI,CD' 카테고리의 다른 글

AWS 배포 순서 (파일 & 작업 순서)  (0) 2025.06.23