CI/CD를 깊게 다룬 계기
과거 프로젝트에서는 CI/CD가 너무 어렵게 느껴져서, 제대로 이해하지 못한 채 GPT가 제공한 워크플로우를 그대로 복사·붙여넣기 하며 구축했다.
하지만 이번 프로젝트에서 다시 CI/CD를 설정하려고 하니, 머릿속에 남아 있는 게 거의 없었고 여전히 어렵게 느껴졌다. “또 그냥 복사해서 붙이면 되겠지”라고 넘어가면 결국 같은 상황이 반복될 것 같았다.
그래서 이번에는 CI/CD를 한 번 제대로 해부해보기로 했다. 왜 이런 스텝이 필요한지, 각 설정이 어떤 흐름으로 이어지는지, 실제로 어떤 문제가 생길 수 있는지까지 깊게 분석했다. 그리고 그 과정을 기술 블로그로 정리해두면, 다음에 CI/CD를 다시 구축해야 할 때 이 글을 기반으로 빠르게 복기하면서 더 단단하게 이해할 수 있다고 판단했다.
이 시리즈에서 다루는 범위
- 1편: CI/CD 전체 설계 & 워크플로우 큰그림 (이번 글)
- 2편: CI 빌드 파트 깊게 (JDK 17, Gradle Caching, Gradle Wrapper, build -x test, 캐시 동작 원리) + 트러블슈팅
- 3편: Docker 이미지 전략 (login/metadata/latest+sha/푸시, Docker 캐시 vs Gradle 캐시, Dockerfile)
- 4편: 운영 배포/보안 (SSM send-command, deploy.sh 전체, application.yml 시크릿 주입 이슈, .env 전략)
전체 흐름
내 CI/CD 흐름은 다음과 같다.
- GitHub 이벤트 발생
- Gradle 빌드
- Docker 이미지 빌드
- DockerHub push
- GitHub Actions → AWS SSM send-command
- EC2가 deploy.sh 실행 → docker pull → docker run
- GitHub Actions가 코드를 빌드하고 → Docker 이미지를 만든 뒤 DockerHub에 올리고 → AWS SSM으로 EC2에 “배포 스크립트 실행” 명령을 내려서 → EC2가 최신 이미지를 pull 받아 컨테이너를 갈아끼우는 방식이다.
“왜 build와 deploy를 나눴는가?”
워크플로우는 job이 두 개로 나뉜다.
- build:
- 소스코드 checkout
- JDK 17 설치
- Gradle 캐시 복원
- Gradle 빌드 (테스트 제외)
- Docker Buildx 준비
- (main push 또는 수동 실행일 때만) DockerHub 로그인/태그 생성/이미지 빌드&푸시
- deploy:
- (main push 또는 수동 실행일 때만) AWS Credentials 설정
- SSM send-command로 EC2에 deploy.sh 실행
이 구조의 핵심은 “책임 분리”다.
- build는 “아티팩트(이미지) 생산”이 책임
- deploy는 “운영 서버 교체/재기동”이 책임
그래서 deploy는 needs: build로 빌드가 성공해야만 실행되게 묶었다.
“어떤 이벤트에서 언제 무엇이 실행되는가?” (트리거/조건 전략)
트리거(on)
- push : main, dev
- pull_request : main, dev
- workflow_dispatch : 수동 실행
즉, dev에서도 PR에서도 기본적인 build 흐름은 돈다.
하지만 중요한 차이는 “이미지 푸시와 배포는 main에서만”이다.
조건(if)
- DockerHub 로그인/메타데이터/이미지 빌드&푸시는
- main 브랜치 push거나
- workflow_dispatch일 때만 실행
- deploy job 자체도
- main 브랜치 push거나
- workflow_dispatch일 때만 실행
이렇게 한 이유는 단순하다.
- dev / PR 단계에서는 “검증(build)”까지만 하고 운영 배포는 막는다.
- 운영 배포는 main에 머지된 결과(또는 수동 실행)만 대상으로 한다.
워크플로우 (CI/CD)
아래는 실제 워크플로우 YAML이다.
name: oneco CI/CD
on:
push:
branches: ["main", "dev"]
pull_request:
branches: ["main", "dev"]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# --- 레포지토리 파일들 checkout ---
- name: repository checkout
uses: actions/checkout@v3
# --- JDK 17 설정 ---
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# --- gradle 파일들 캐싱 ---
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# --- gradle wrapper 파일에 실행 권한을 부여 ---
- name: Grant execute permission for gradlew
run: chmod +x gradlew
working-directory: oneco
# --- Gradle 빌드 액션을 이용해서 프로젝트 빌드 ---
- name: Build with Gradle
run: ./gradlew build -x test
working-directory: oneco
# --- Docker Buildx 설정 (캐시 사용하려면 필요) ---
- name: Set up Docker Builder
uses: docker/setup-buildx-action@v3
# --- 메인 브랜치 push 시에만 이미지 빌드/푸시 ---
- name: Login to Docker Hub
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|| github.event_name == 'workflow_dispatch'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# --- 메인 브랜치 push 시에만 메타데이터 생성 ---
- name: Docker metadata
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|| github.event_name == 'workflow_dispatch'
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/oneco
tags: |
type=raw,value=latest
type=sha
# --- 도커 이미지 빌드&푸시 ---
- name: Build and push Docker image
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|| github.event_name == 'workflow_dispatch'
uses: docker/build-push-action@v6
with:
context: ./oneco
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy to EC2 with SSM
needs: build # build 작업이 성공해야 실행
runs-on: ubuntu-latest
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|| github.event_name == 'workflow_dispatch'
steps:
# --- 1. AWS 자격 증명 설정 ---
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
# --- 2. AWS SSM을 통해 EC2의 deploy.sh 스크립트 실행 ---
- name: Run deploy script on EC2 via SSM
run: |
aws ssm send-command \
--instance-ids "${{ secrets.EC2_INSTANCE_ID }}" \
--document-name "AWS-RunShellScript" \
--comment "Running deploy.sh script" \
--parameters 'commands=["/bin/bash /home/ssm-user/deploy.sh ${{ secrets.DOCKERHUB_USERNAME }}"]'
설계 포인트: “SSM 배포”라는 선택
보통 EC2 배포는 SSH로 붙어서 docker pull하고 docker run을 돌린다.
하지만 나는 GitHub Actions에서 EC2로 직접 SSH 접속하지 않고, SSM을 통해 원격 명령을 실행한다.
즉, 배포의 관점에서 GitHub Actions가 하는 일은:
- EC2에 직접 접속해서 작업하는 게 아니라
- AWS에게 “저 인스턴스에서 이 명령 실행해줘”라고 요청한다.
이 구조 덕분에:
- SSH 키 관리/포트 개방을 최소화할 수 있고
- 배포 작업을 스크립트(deploy.sh)로 표준화할 수 있다.
(SSM send-command 옵션과 deploy.sh 내부 동작은 4편에서 자세히 다룬다.)
보안 관점: “Secrets를 어디까지 흘려보내는가?”
이번 1편에서는 큰그림만 잡는다.
지금 쓰는 GitHub Secrets는:
- AWS 세션 잡기
- DockerHub 로그인
- SSM으로 “배포 스크립트 실행” 요청
이 과정에서 Runner(일회용 VM)에서 잠깐 쓰이고 끝나면 사라진다.
하지만 “application.yml을 시크릿으로 만들어 이미지에 포함”시키면 성격이 달라진다.
그건 시크릿이 Docker 이미지 레이어에 “영구 저장”될 수 있다.
이 부분은 4편에서 더 자세히 다룬다.
- “GitHub Secrets를 쓴다는 사실”이 문제가 아니라
- “그걸 application.yml로 만들어서 Docker 이미지에 같이 올린다”가 문제
[CI/CD 1편] GitHub Actions + DockerHub + SSM으로 EC2 자동 배포 CI/CD 큰그림
CI/CD를 깊게 다룬 계기과거 프로젝트에서는 CI/CD가 너무 어렵게 느껴져서, 제대로 이해하지 못한 채 GPT가 제공한 워크플로우를 그대로 복사·붙여넣기 하며 구축했다. 하지만 이번 프로젝트에서
gimini.tistory.com
[CI/CD 2편] CI 빌드 파트 완전 해부: JDK 17 세팅 → Gradle 캐시 → Gradle Wrapper → build -x test ( + 트러블
이 글은 1편에서 잡은 큰그림 중, GitHub Actions의 build job을 “Runner 내부에서 실제로 어떤 일이 벌어지는지” 기준으로 풀어낸다.이번 2편에서 다루는 범위는 아래와 같다.Set up JDK 17Gradle Caching (actio
gimini.tistory.com
[CI/CD 3편] Docker 이미지 빌드/푸시 전략 완전 해부: Login → metadata(latest+sha) → buildx 캐시 → DockerHub
[CI/CD 3편] Docker 이미지 빌드/푸시 전략 완전 해부: Login → metadata(latest+sha) → buildx 캐시 → DockerHub push
gimini.tistory.com
[CI/CD 4편] SSH 없이 배포하기: GitHub Actions → AWS Credentials → SSM send-command → EC2 deploy.sh, 그리고 시
[CI/CD 4편] SSH 없이 배포하기: GitHub Actions → AWS Credentials → SSM send-command → EC2 deploy.sh, 그리고 시크릿 주입
gimini.tistory.com