[CL] 4단계: Docker 컨테이너화 배포 - 100-hours-a-week/6-nemo-wiki GitHub Wiki

1. 개요 및 기존 방식의 한계

1.1. 과제 목표 및 구성

본 단계에서는 지금까지 경험한 수작업 기반 배포(빅뱅), 그리고 스크립트 기반 CI/CD 자동화까지 적용한 환경에서 직접 체감했던 운영상의 문제점을 바탕으로 근본적인 해결 방법(Docker 컨테이너화)을 모색하고 이를 바탕으로 설계하는 것을 목표로 한다.

1.2. 이전 단계에서의 문제점

현재까지 수작업 기반의 빅뱅 배포를 넘어, GitHub Actions 기반 CI/CD 자동화 파이프라인까지 구축했다.

결과적으로 개발자가 코드를 push 하면 브랜치에 따라 자동으로 빌드, 테스트, 배포 스크립트 실행, 서버 반영, 간단한 롤백과 헬스 체크까지 이어지는 초기 수준의 배포 자동화 체계를 갖출 수 있었다.

그러나 실질적으로 서비스를 운영하고 확장하는 과정에서 다음과 같은 구조적 한계와 리스크가 여전히 존재했다.

  1. 환경 일치 및 의존성 관리의 불안정성
    • 자동화 파이프라인 도입으로 반복 작업과 휴먼 에러는 눈에 띄게 감소했지만, 여전히 서버에는 Spring, Node, Python 등 여러 런타임, 패키지, 환경 등이 직접 설치되어 있었다.
    • 이에 따라 로컬과 서버 환경 간의 차이로 개발 환경에서는 문제 없던 코드가 운영 배포 후 예상하지 못한 실행 오류가 나타나는 경우가 빈번했다.
    • 실제로 FastAPI의 uvicorn 버전 충돌을 경험하였다.
  2. 단일 서버(Single Point of Failure, SPOF) 구조의 한계
    • FE, BE, AI, DB, Cache 등 모든 서비스가 한 서버에 설치되어 동작하는 구조서버 한 대의 장애가 곧 전체 서비스의 중단으로 직결되었다.
    • 개별 서비스 장애로 인해 전체 시스템이 영향을 받는 것 뿐만 아니라, 장애 원인 파악이나 복구에도 많은 시간이 소요될 것이라고 예상된다.
  3. 확장성과 자원 관리의 비효율
    • 서비스 트래픽이 증가하거나, 특정 기능에 부하가 집중될 때 서비스별로 자원을 유연하게 할당하거나 개별적으로 확장하는 것이 불가능했다.
    • 즉, 하나의 서버가 모든 서비스를 담당하고 공유하다보니 하나의 서비스가 과도하게 자원을 점유할 경우 다른 서비스까지 영향을 받을 가능성이 존재한다.
  4. 운영/협업/유지보수의 부담
    • 예를 들어 신규 서버를 세팅할 시, 각종 패키지, 런타임, 설정 파일을 매번 동일하게 설치하고 관리해야 하므로 설정 누락, 환경 불일치, 실수 등의 리스크의 무방비로 노출된다.
    • 즉, 서버 환경을 완벽하게 재현하기 어렵기 때문에 로컬, 개발, 운영 환경 간의 차이로 인한 문제가 발생할 것으로 예상된다.
    • 또한 운영 중 장애 복구나 롤백 또한 하나의 서버가 모든 서비스를 담당하고 있기 때문에 특정 서비스만 빠르게 복구하거나 특정 장애 지점만 파악해 대응하는 것이 현실적으로 불가능했다.
    • 여러 서비스가 한 서버에서 동작하다 보니, 각 서비스의 상태나 장애 여부, 자원 사용 현황을 일일이 명령어로 수동 확인해야 했고, 서비스가 늘어나거나 장애가 겹치면 모든 로그와 상태를 직접 추적하는 데 많은 시간이 소요되어, 신속한 파악이나 자동화된 모니터링·알림 연동에도 한계가 있다고 판단했다.
  5. 3-Tier 구조 도입의 필요성
    • 단일 서버에 모든 서비스가 집적된 모놀리식(Monolithic) 구조는 운영상의 위험이 누적되고 있었다.
    • 웹, 비즈니스, 데이터 및 캐시로 명확히 레이어를 분리하여 각 티어별 자원, 배포 장애 대응, 확장 전략을 독립적으로 설계하고 운영해야 여러 변수에 유연하게 대응할 수 있다는 점을 설계 과정에서 체감하였다.
    • 특히, AI 서버는 추후 AWS 이관 및 비용 최적화를 고려해 별도 VPC 또는 서브넷으로 분리하는 방안도 함께 설계하여, 서비스 특성에 맞는 아키텍처 유연성까지 확보하고자 했다.

위와 같은 문제들은 수동 배포 및 CI/CD 자동화만으로는 구조적으로 해소되지 않으며, 실제 서비스 운영의 신뢰성, 확장성, 장애 대응, 협업 효율성들을 고려했을 때, 기존 방식으로는 근본적인 해결이 불가능하다는 점을 설계 과정에서 체감할 수 있었다.

2. Docker 도입의 필요성 및 선택 배경

앞서 언급한 문제들을 해결하고자 현 단계에서 Docker 기반 컨테이너화를 다음과 같은 이유로 4단계의 핵심 전략으로 선택하였다.

본 프로젝트는 Docker 도입을 통해 다음과 같은 이점을 얻을 수 있다.

  1. 환경의 완전한 표준화 및 일관성
    • Docker 이미지는 모든 패키지, 런타임, 환경설정을 한 번에 캡슐화하여 로컬, 개발, 운영 환경 어디서든 동일한 실행 환경을 보장한다.
    • 실제로, 기존에는 Python, Node, Java 등 각종 패키지를 서버마다 수동 설치해야 했지만, 개발했던 환경 그대로 Docker 이미지로 만들어 컨테이너로 배포한 후에는, 실행 명령어 하나로 동일한 환경을 바로 구성할 수 있게 되었다.
  2. 서비스 분리 및 아키텍처 확장 용이성
    • 컨테이너 단위로 FE/BE/AI/DB/Cache 등을 독립 배포 및 운영이 가능해져 서비스별 장애 격리 및 확징이 쉬워졌다.
    • 3-Tier 구조로 자연스럽게 전환할 수 있었고, 각 티어를 별도 컨테이너, 인스턴스, 서브넷으로 독립 운영하는 기반을 마련했다.
    • AI 서버의 경우, 추후 AWS 이관 및 비용 최적화를 위해 별도의 VPC/서브넷 설계도 병행하였다.
  3. 운영 자동화 및 협업 효율 극대화
    • 컨테이너 이미지는 버전 관리, 이미지 태깅을 통해 쉽게 누가나 같은 환경을 즉시 재현할 수 있다.
    • 즉, docker-compose up 한 번으로 동일한 배포와 실행 환경이 자동으로 복원된다.
  4. 배포, 롤백, 장애 복구의 신속성
    • 수작업 및 스크립트 기반 배포에서는 각종 설정 충돌, 의존성 꼬임, 수동 복구에 많은 시간이 걸렸으나, Docker 환경에서는 이전 이미지로의 롤백과 컨테이너 재기동만으로 빠른 장애 복구 및 안전한 롤백이 가능해졌다.
  5. 유지보수, 모듈화, 확장 전략 최적화
    • 각 서비스는 독립적으로 빌드, 배포, 관리할 수 있어 특정 서비스만 업그레이드하거나 재배포하는 것도 간단하다.
    • 실제 트래픽 증가나 부하 분산 시, FE/BE/AI 컨테이너 개수만 조정하여 운영의 유연성과 비용 효율도 크게 증가될 것이라고 예상된다.

즉, Docker 컨테이너화는 기존 수작업 및 단일 서버 기반 운영 구조로는 해결할 수 없었던 환경 표준화, 서비스 분리, 자동화, 장애 격리, 확장성 등의 문제를 근본적으로 해결할 수 있었다.

3. Docker 컨테이너 구조 및 설계

3.1. 컨테이너화 대상 서비스 및 구조

서비스 전체를 Docker 컨테이너 단위로 완전히 분리하고, 실제 운영에 적합한 3-Tier 아키텍처를 설계한 내용은 다음과 같다.

  1. 컨테이너화 대상 및 주요 역할

    서비스 기술 스택 주요 역할 및 특징
    Frontend Next.js SSR 및 웹 인터페이스 / 컨테이너화로 환경차이 해소
    Backend Spring Boot REST API / 핵심 로직 / DB,Cache,AI 연동 담당 / 컨테이너로 격리
    Cache Redis 세션 및 캐시 처리, 빠른 데이터 접근, 공식 이미지 활용
    AI FastAPI 인공지능 처리 / 별도 VPC로 분리
    VectorDB Chroma 임베등/검색 등 AI 인덱싱용 데이터 저장 / AI 컨테이너와 나란히 운영
  2. GCP 기반 3-Tier + AI 분리 아키텍처 요약

    • Web Tier: ALB(Application Load Balancer)가 Next.js 컨테이너로 트래픽을 분배
    • App Tier: Spring Boot(복수 컨테이너 가능), Redis 컨테이너가 비즈니스 로직/캐시 처리
    • DB Tier: MySQL(Cloud SQL)이 프라이빗 서브넷에 위치, 데이터 안전성 및 성능 보장
    • AI/Vector Tier: 별도 VPC 내에 FastAPI와 ChromaDB 컨테이너 배치, Transit Gateway로 본 서비스와 연동
    • 보안: 각 티어별 VPC/서브넷 분리, 외부 접근 최소화(필요한 포트만 오픈)
    • 스토리지: DB/Chroma 데이터는 GCP Persistent Disk, 백업/로그 등은 GCS 활용

Final_Archieticture-v2_최종 drawio

  1. CI/CD 및 운영 자동화
    • 각 서비스(FE/BE/AI)는 별도의 레포로 관리
      • 빌드시 GCP Container Registry(GCR)에 이미지 PUSH
      • GCE 인스턴스는 GCR에서 최신 이미지 PULL하여 자동 배포
      • 각 서비스가 독립적으로 업데이트 및 배포가 가능하여 운영 효율과 장애 격리 극대화

3.2. 배포 전략 및 기술 명세

이번엔 Docker 컨테이너 기반의 이미지 빌드부터 관리, 배포, 환경변수 관리까지 기술적 전략을 상세히 설명한다. 각 서비스(FE, BE, DB, Cache, AI, VectorDB)별 이미지 빌드 전략, 태깅 및 관리 방법, 레지스트리 활용, 그리고 환경변수 관리 기준을 개발환경과 운영환경을 명확히 구분하여 정리하였다.

  1. 공통 운영 전략 (모든 서비스 동일)

    1. 이미지 태깅 전략
      • dev: 서비스명:dev-브랜치/날짜 (덮어쓰기, 최신 개발상태)
      • prod: 서비스명:prod-버전/해시 (릴리즈 단위, 고정/롤백 용이)
    2. 레지스트리 활용
      • GCR(Google Container Registry) 활용, dev는 퍼블릭/임시 가능, prod는 프라이빗 필수
      • 서비스별 네임스페이스로 이미지 충돌 방지 및 접근권한 분리
    3. 환경변수 및 비밀번호 관리
      • 개발: .env.dev, .env.development 파일 mount

      • 운영: .env.production 또는 Secret Manager, CI/CD 환경변수로 관리

        (민감정보는 Git 미포함, 빌드시 외부 주입)

  2. 서비스별 Dockerfile

    1. Frontend (Next.js)
      • 1단계: 의존성 설치 및 빌드
      • 2단계: 빌드 산출물만 실행 이미지로 복사 → 용량 최소화
    2. Backend (Spring Boot)
      • Gradle 빌드 및 멀티스테이지
        • 빌드 컨테이너에서 jar 생성 후, 경량 JDK이미지에 jar만 복사
    3. Cache/DB (Redis/MySQL)
      • MySQL(개발): 공식 이미지, 환경변수로 DB명/비밀번호/볼륨 관리
      • MySQL(운영): GCP CloudSQL로 연결정보만 주입
      • Redis: 공식 이미지 사용
    4. AI (FastAPI), Vector DB(Chroma)
      • FastAPI
        • AI 모델 경로, DB/Chroma 접속정보 등은 .env/Secret Manager로 관리
      • Chroma: 공식 이미지, 볼륨 마운트 및 AI와 내부 네트워크에서만 통신
  3. 네트워크/데이터/자원 할당 전략

    1. 네트워크 구조
      • Docker Compose의 custom 네트워크(bridge 모드)를 서비스별로 생성
        • ex) app-net , ai-net
        • FE ↔ BE, BE ↔ DB/Redis, AI ↔ Chroma 등 컨테이너 간 내부 DNS 기반으로 통신
    2. 데이터 볼륨 전략
      • MySQL/ChromaDB/Redis 등 데이터 저장 서비스는 반드시 볼륨 마운트 적용으로 데이터 유지

        # 예시
        volumes:
          - db-data:/var/lib/mysql
          - chroma-data:/chroma/.chroma
        
      • 운영환경(CloudSQL)

        • 서비스별 외부 DB에 연결만 하고, 컨테이너에는 데이터 저장X
        • (개발/테스트 환경에서는 Compose의 로컬 볼륨 활용)
    3. 자원 할당(리소스 제한)
      • Docker Compose에서 각 컨테이너별 CPU/Memory 제한 설정)

        • 부하나 장애 상황에서도 한 컨테이너가 서버 전체를 먹지 않도록 제어
        • 운영환경에서는 서버 사양의 70~80%를 넘지 않게 설정
        # 예시
        deploy:
          resources:
            limits:
              cpus: '1.0'
              memory: 2g
        
  4. CI/CD 및 운영 자동화

스크린샷_2025-04-26_오후_3 18 05

  1. CI: 코드 Push/PR → GitHub Actions에서 자동 빌드 및 테스트 실행
    • FE: Lint, npm test/build → Docker 이미지 빌드
    • BE: Gradle test/bootJar → Docker 이미지 빌드
    • AI: pytest → Docker 이미지 빌드
    • 성공 시, 각 서비스별로 GCR에 dev/prod 이미지 push
  2. CD
    • 개발환경(develop)
      • develop branch에 push/merge → Staging 서버에 자동 배포
      • 서버에서 docker-compose pull && docker-compose up -d 자동 실행
    • 운영환경
      • main branch에 push/merge → 운영 서버에 수동 승인 후 배포
      • 운영자 승인(environment.approval) → docker-compose pull && up -d로 반영
  3. 배포/롤백/검증
    • 배포 후 validate.sh로 Health Check (각 서비스 포트/API 상태 확인)

    • 장애/배포 실패 시, 직전 prod 이미지로 즉시 롤백

      (docker pull 서비스:prod-직전버전 + compose restart)

3.3. 도입 전후 비교 및 기대 효과

항목 도입 전 (수작업/스크립트/CI) 도입 후 (Docker 기반 컨테이너화) 효과/개선점
환경 표준화/일관성 개발/운영 환경, 패키지 버전마다 상이 Docker 이미지로 실행환경 완전 통일 환경 불일치, "작동 안 함" 이슈 해소
배포 및 자동화 수동 빌드/전송/실행 반복, 배포 이력 단절 CI/CD 자동화, 이미지 단위 배포/롤백 배포 시간 단축, 작업 효율 ↑
서비스 분리/격리 한 VM 내 포트만 구분, 장애시 전체 영향 FE/BE/AI/DB 완전 분리 컨테이너 운영 장애 전파 차단, 개별 장애 복구 용이
운영 효율/유지보수 신규 서비스 추가/환경 변경 시 반복 작업 필요 Compose 정의만 추가, 즉시 반영 확장성/유지보수성 대폭 개선
협업/온보딩 환경 설정/의존성 공유 어려움, 설치 필요 docker-compose up만으로 즉시 실행 환경 구축 시간 대폭 단축
배포/복구/롤백 롤백 수동(복잡), git과 배포 분리 이미지 태깅, docker pull로 즉시 롤백 장애 복구 속도↑, 배포 이력 자동 관리

결론적으로 Docker 기반 컨테이너화를 도입함으로써,

서비스 환경의 표준화와 배포 자동화, 장애 격리, 유지보수 효율, 생산성까지 모두 개선하였다.

이제는 환경 차이, 수동 배포, 장애 전파, 복잡한 롤백 등

이전 단계에서 반복되던 운영상의 문제들이 대부분 사라지고,

새로운 기능 추가·확장도 빠르고 안전하게 진행할 수 있는

탄탄한 운영/개발 기반을 갖추게 되었다.

4. 제출 산출물 및 추가자료

4.1. 아키텍처

Final_Archieticture-v2_최종 drawio

4.2. Dockerfile & docker-compose 파일

  1. Frontend

    # 1. Build Stage
    FROM node:18 AS builder
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    RUN npm run build
    
    # 2. Run Stage
    FROM node:18-slim
    WORKDIR /app
    COPY --from=builder /app/.next ./.next
    COPY --from=builder /app/node_modules ./node_modules
    COPY --from=builder /app/public ./public
    EXPOSE 3000
    CMD ["npm", "start"]
    
  2. Backend

    dockerfile
    복사편집
    # 1. Build Stage
    FROM gradle:8.5.0-jdk21 AS builder
    WORKDIR /build
    COPY . .
    RUN gradle bootJar
    
    # 2. Run Stage
    FROM openjdk:21-jdk-slim
    WORKDIR /app
    COPY --from=builder /build/build/libs/*.jar app.jar
    EXPOSE 8080
    CMD ["java", "-jar", "app.jar"]
    
    
  3. AI

    FROM python:3.10
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    COPY . .
    EXPOSE 5000
    CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"]
    
    
  4. docker-compose.yml

    • 개발 환경용 docker-compose.dev.yml

      # 모든 구성요소를 로컬 컨
      version: "3.8"
      
      services:
        frontend:
          build:
            context: ./frontend
          container_name: frontend
          ports:
            - "3000:3000"
          env_file:
            - ./.env.dev
          volumes:
            - ./frontend:/app
          depends_on:
            - backend
          networks:
            - app-network
      
        backend:
          build:
            context: ./backend
          container_name: backend
          ports:
            - "8080:8080"
          env_file:
            - ./.env.dev
          volumes:
            - ./backend:/app
          depends_on:
            - mysql
            - redis
          networks:
            - app-network
      
        ai:
          build:
            context: ./ai
          container_name: ai
          ports:
            - "5000:5000"
          env_file:
            - ./.env.dev
          volumes:
            - ./ai:/app
          depends_on:
            - chromadb
          networks:
            - app-network
      
        mysql:
          image: mysql:8.0
          container_name: mysql
          ports:
            - "3306:3306"
          environment:
            MYSQL_ROOT_PASSWORD: devpw
            MYSQL_DATABASE: devdb
            MYSQL_USER: devuser
            MYSQL_PASSWORD: devpw
          volumes:
            - mysql-data:/var/lib/mysql
          networks:
            - app-network
      
        redis:
          image: redis:7
          container_name: redis
          ports:
            - "6379:6379"
          volumes:
            - redis-data:/data
          networks:
            - app-network
      
        chromadb:
          image: chromadb/chroma:latest
          container_name: chromadb
          ports:
            - "8000:8000"
          volumes:
            - chroma-data:/chroma/.chroma
          networks:
            - app-network
      
      networks:
        app-network:
          driver: bridge
      
      volumes:
        mysql-data:
        redis-data:
        chroma-data:
      
      
    • 운영 환경용 docker-compose.prod.yml

      # MySQL은 CloudSQL 사용 나머지는 전부 컨테이너
      version: "3.8"
      
      services:
        frontend:
          image: gcr.io/myproject/frontend:prod-latest
          container_name: frontend
          ports:
            - "3000:3000"
          env_file:
            - ./.env.production
          networks:
            - app-network
          restart: always
      
        backend:
          image: gcr.io/myproject/backend:prod-latest
          container_name: backend
          ports:
            - "8080:8080"
          env_file:
            - ./.env.production
          networks:
            - app-network
          restart: always
      
        ai:
          image: gcr.io/myproject/ai:prod-latest
          container_name: ai
          ports:
            - "5000:5000"
          env_file:
            - ./.env.production
          networks:
            - app-network
          restart: always
      
        redis:
          image: redis:7
          container_name: redis
          ports:
            - "6379:6379"
          volumes:
            - redis-data:/data
          networks:
            - app-network
          restart: always
      
        chromadb:
          image: chromadb/chroma:latest
          container_name: chromadb
          ports:
            - "8000:8000"
          volumes:
            - chroma-data:/chroma/.chroma
          networks:
            - app-network
          restart: always
      
      networks:
        app-network:
          driver: bridge
      
      volumes:
        redis-data:
        chroma-data:
      
      

4.3. 기술 명세표

서비스 주요 역할 기술스택 배포 포트 볼륨/데이터 환경 변수/Secret
Frontend SSR 웹 FE Next.js, Node 18 3000 - .env.production
Backend REST API Spring Boot 3/JDK21 8080 - .env.production
AI AI/ML FastAPI, Python 3.10 5000 - .env.production
VectorDB 벡터 DB Chroma (공식) - chroma-data -
DB 데이터베이스 MySQL 8.0 3306 db-data env
Cache 세션/캐시 Redis 7.2 6379 redis-data -

4.4. 배포 운영 자동화 스크립트

  1. 배포 스크립트 (deploy.sh) - validate & rollback & discord 알림 내장

    #!/bin/bash
    cd /home/ubuntu/app
    
    echo "[배포] 컨테이너 이미지 최신화"
    docker-compose pull
    
    echo "[배포] 서비스 재배포"
    docker-compose up -d
    
    echo "[배포] 배포 후 헬스체크 시작"
    ./validate.sh
    VALIDATE_RESULT=$?
    
    DISCORD_WEBHOOK_URL="여기에_디스코드_웹훅_URL"
    
    if [ $VALIDATE_RESULT -eq 0 ]; then
      echo "[배포] 배포 성공"
      curl -H "Content-Type: application/json" \
        -X POST \
        -d '{"content":"✅ **[배포 성공]** 서비스가 정상적으로 배포되었습니다."}' \
        $DISCORD_WEBHOOK_URL
      exit 0
    else
      echo "[배포] 배포 실패, 롤백 시작"
      ./rollback.sh
      curl -H "Content-Type: application/json" \
        -X POST \
        -d '{"content":"🚨 **[배포 실패]** 서비스 배포에 실패하여 롤백을 실행하였습니다!"}' \
        $DISCORD_WEBHOOK_URL
      echo "[`date`] 배포 실패 및 롤백 실행" >> /home/ubuntu/app/deploy.log
      exit 1
    fi
    
  2. 롤백 스크립트 (rollback.sh)

    #!/bin/bash
    cd /home/ubuntu/app
    
    echo "[롤백] 이전 버전 이미지 Pull"
    docker pull gcr.io/your-project/backend:prod-previous
    
    echo "[롤백] 재시작"
    docker-compose down
    docker-compose up -d
    
    DISCORD_WEBHOOK_URL="여기에_디스코드_웹훅_URL"
    
    ./validate.sh
    if [ $? -eq 0 ]; then
      echo "[롤백] 롤백 성공"
      curl -H "Content-Type: application/json" \
        -X POST \
        -d '{"content":"🛠️ **[롤백 성공]** 이전 버전으로 정상 롤백되었습니다."}' \
        $DISCORD_WEBHOOK_URL
    else
      echo "[롤백] 롤백도 실패! 즉시 확인 필요"
      curl -H "Content-Type: application/json" \
        -X POST \
        -d '{"content":"🚨 **[긴급] 롤백도 실패했습니다! 즉시 확인 바랍니다.**"}' \
        $DISCORD_WEBHOOK_URL
    fi
    
  3. 헬스 체크 (validate.sh)

    #!/bin/bash
    # 각 주요 서비스 상태를 curl 등으로 확인
    
    FE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/healthz)
    BE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/healthz)
    AI_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/healthz)
    
    echo "FE: $FE_STATUS, BE: $BE_STATUS, AI: $AI_STATUS"
    
    if [ "$FE_STATUS" == "200" ] && [ "$BE_STATUS" == "200" ] && [ "$AI_STATUS" == "200" ]; then
      echo "[헬스체크] All services OK"
      exit 0
    else
      echo "[헬스체크] 서비스 이상"
      exit 1
    fi
    

4.5. CI/CD 워크플로우 (Spring Boot 예시)

  1. pr-ci.yml (PR 생성 시 Lint & Test만 수행)

    name: CI - PR Lint & Test
    
    on:
      pull_request:
        branches: [main, develop]
    
    jobs:
      pr-test:
        runs-on: ubuntu-22.04
        steps:
          - uses: actions/checkout@v3
    
          - name: Set up JDK 21
            uses: actions/setup-java@v3
            with:
              java-version: '21'
              distribution: temurin
    
          - name: Grant execute permission to Gradle
            run: chmod +x gradlew
    
          - name: Run Tests
            run: ./gradlew test
    
  2. ci.yml (main, develop 브랜치 push 시 Docker 이미지 빌드 & GCR 업로드, 스크립트 GCS 업로드)

    name: CI - Backend Docker Build & Push
    
    on:
      push:
        branches: [main, develop]
    
    jobs:
      build-and-push:
        runs-on: ubuntu-22.04
    
        steps:
          - uses: actions/checkout@v3
    
          - name: Set up JDK 21
            uses: actions/setup-java@v3
            with:
              java-version: '21'
              distribution: temurin
    
          - name: Grant execute permission to Gradle
            run: chmod +x gradlew
    
          - name: Build with Gradle
            run: ./gradlew clean bootJar
    
          - name: Authenticate to GCP
            uses: google-github-actions/auth@v1
            with:
              credentials_json: ${{ secrets.GCP_KEY_JSON }}
    
          - name: Configure Docker for GCR
            run: |
              gcloud auth configure-docker
    
          - name: Set image tag
            id: tag
            run: |
              if [ "${{ github.ref_name }}" == "main" ](/100-hours-a-week/6-nemo-wiki/wiki/-"${{-github.ref_name-}}"-==-"main"-); then
                echo "IMAGE_TAG=prod-${{ github.sha }}" >> $GITHUB_ENV
                echo "TARGET_ENV=prod" >> $GITHUB_ENV
              else
                echo "IMAGE_TAG=dev-${{ github.sha }}" >> $GITHUB_ENV
                echo "TARGET_ENV=dev" >> $GITHUB_ENV
              fi
    
          - name: Build Docker image
            run: |
              docker build -t gcr.io/${{ secrets.GCP_PROJECT_ID }}/be-spring:${{ env.IMAGE_TAG }} .
    
          - name: Push Docker image to GCR
            run: |
              docker push gcr.io/${{ secrets.GCP_PROJECT_ID }}/be-spring:${{ env.IMAGE_TAG }}
    
          - name: Upload deploy scripts to GCS
            run: |
              gsutil cp ./deploy/*.sh gs://${{ secrets.GCS_BUCKET }}/$TARGET_ENV/backend/
    
  3. cd-staging (develop 브랜치용 Staging 서버 배포, CI 성공 후 트리거)

    name: CD - Deploy to Staging (Develop)
    
    on:
      workflow_run:
        workflows: ["CI - Backend Docker Build & Push"]
        types:
          - completed
    
    jobs:
      deploy:
        if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'develop' }}
        runs-on: ubuntu-22.04
    
        steps:
          - name: Set image tag/env
            run: |
              echo "IMAGE_TAG=dev-${{ github.sha }}" >> $GITHUB_ENV
              echo "TARGET_ENV=dev" >> $GITHUB_ENV
    
          - name: Create SSH private key
            run: |
              echo "$SSH_PRIVATE_KEY" > private_key.pem
              chmod 600 private_key.pem
            env:
              SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
    
          - name: Deploy to Staging (pull image & download scripts & restart)
            run: |
              ssh -o StrictHostKeyChecking=no -i private_key.pem ${{ secrets.STAGING_SSH_USER }}@${{ secrets.STAGING_SSH_HOST }} "
                cd ~/deploy/backend &&
                gsutil cp gs://${{ secrets.GCS_BUCKET }}/dev/backend/*.sh ./ &&
                chmod +x *.sh &&
                ./deploy.sh
              "
    
  4. cd-prod.yml (main 브랜치 운영 서버 배포, CI 성공 후, 혹은 main push에 manual approval)

    name: CD - Deploy to Production (Manual)
    
    on:
      workflow_run:
        workflows: ["CI - Backend Docker Build & Push"]
        types:
          - completed
    
    jobs:
      deploy:
        if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' }}
        runs-on: ubuntu-22.04
        environment:
          name: production
          url: ${{ secrets.PROD_SERVER_URL }}
    
        steps:
          - name: Set image tag/env
            run: |
              echo "IMAGE_TAG=prod-${{ github.sha }}" >> $GITHUB_ENV
              echo "TARGET_ENV=prod" >> $GITHUB_ENV
    
          - name: Create SSH private key
            run: |
              echo "$SSH_PRIVATE_KEY" > private_key.pem
              chmod 600 private_key.pem
            env:
              SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
    
          - name: Deploy to Production (pull image & download scripts & restart)
            run: |
              ssh -o StrictHostKeyChecking=no -i private_key.pem ${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }} "
                cd ~/deploy/backend &&
                gsutil cp gs://${{ secrets.GCS_BUCKET }}/prod/backend/*.sh ./ &&
                chmod +x *.sh &&
                ./deploy.sh
              "
    

5. 결론 및 다음 단계 제안

5.1. Docker 도입의 의의 및 개선 효과

이번 단계에서 Docker 기반 컨테이너화 및 CI/CD 자동화를 도입함으로써

기존의 수작업/스크립트 기반 배포에서 발생하던 운영상 문제들을 크게 개선할 수 있었다.

  • 실행 환경 완전 표준화: 개발~운영 전 구간 환경/의존성 일치로 문제 해소
  • 서비스 분리 및 장애 격리: 각 서비스가 독립적으로 운영, 개별 복구/확장 용이
  • 배포/롤백 자동화: 배포→헬스체크→실패시 롤백까지 일관된 자동화, 운영 부담 대폭 감소
  • 협업 및 유지보수 향상: 신규 서비스/기능 추가도 Compose 정의만 추가해 즉시 배포 가능

이로써 서비스의 신뢰성, 안정성, 확장성, 생산성이 전반적으로 크게 향상되었음을 직접 체감하였다.

5.2. 이후 단계 제안

이제 다음 단계에서는 아래와 같은 고도화를 준비할 수 있다:

  1. Kubernetes 기반 오케스트레이션 도입
    • 컨테이너 수 증가/트래픽 확장 대응 → kubeadm/EKS/GKE 등 매니지드 K8s 도입
    • 자동화된 배포/스케일링/롤링 업데이트/장애 복구 등 본격적인 클라우드 네이티브 아키텍처 구축
  2. GitOps
    • ArgoCD, Helm 등 GitOps 도구로 모든 배포/설정 자동화
  3. 실시간 모니터링 및 알림 체계 강화
    • Prometheus, Grafana, Sentry 등 도입해 서비스/인프라 상태 실시간 모니터링
    • 장애·오류 발생 시 Slack/Discord 등으로 즉시 알림,
    • 로그/메트릭 데이터 수집·분석을 통한 지속적 개선
  4. 클라우드 최적화 및 비용 효율화
    • 하이브리드/멀티클라우드 운영(예: AI 서버 GCP, 서비스 AWS/EKS)
    • 비용 및 성능 최적화, 각 클라우드 특성/비용정책을 활용한 유연한 인프라 운영

이번 4단계에서 컨테이너 기반 자동화 배포의 기반을 다진 만큼, 향후 단계에서는 Kubernetes 기반의 오토스케일링 및 무장단 배포, 그리고 완전한 클라우드 네이티브 인프라로의 전환을 통해 더 높은 수준의 확장성, 신뢰성, 운영 자동화를 목표로 발전시켜 나가고자 한다.