
오늘은 깃허브 액션으로 CI/CD 를 구축해보는 것을 정리해볼려고 한다.
CI/CD라는 말은 많이 들어봤는데 정확하게 무슨 뜻인지 잘 몰라 이참에 추가로 정리해볼려고 한다.
CI/CD 는 지속적 통합(Continuous Integration)과 지속적 배포(Continuous Deployment or Continuous Delivery)를 의미한다.
지속적 통합(CI)은 개발자들이 여러 개발자가 작업한 코드 변경 사항을 정기적으로 병합하고 자동으로 빌드 및 테스트하는 프로세스를 의미한다.
지속적 배포(CD)는 지속적으로 통합된 코드를 자동으로 프로덕션 환경에 배포하는 프로세스
지속적 제공(Continuous Delivery)
지속적 배포(Continuous Deployment)
코드 변경 사항이 테스트 및 승인(approve)을 거쳐 자동으로 프로덕션 환경에 배포(merge to main)
새로운 기능과 버그 수정 사항이 실제 사용자에게 빠르게 제공
사용자 피드백을 수집하고 제품을 개선하는 속도를 향상 가능

- GitHub Actions는 GitHub에서 제공하는 CI/CD(지속적 통합 및 지속적 배포) 플랫폼
- 소프트웨어 개발 워크플로우를 자동화할 수 있는 도구
- 코드 푸시, 풀 리퀘스트, 스케줄링 등 다양한 이벤트에 따라 빌드, 테스트, 배포 작업을 자동으로 실행 가능
- GitHub Actions는 YAML 파일로 정의되며, GitHub 저장소와 긴밀히 통합되어 있어 별도의 외부 CI/CD 도구 없이도 간편하게 사용가능
나는 스프링부트 프로젝트를 도커로 빌드하고 -> Ec2로 배포하는 작업을 해보았다.
일단 도커 이미지 파일을 작성한다.도커 이미지 파일은 최상단 루트 폴더에 작성하면 된다.

FROM openjdk:17-jdk-slim
ARG JAR_FILE=build/libs/your-app.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

레포지토리에서 Action을 누른후 자신에게 맞는 걸 선택하면 된다.나는 Java with Gradle을 선택했다.
그리고 CI/CD 파이프라인을 구축한다.
파이프라인의 트리거 (Trigger)
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
push 트리거: main 브랜치에 코드가 푸시되면 파이프라인 실행pull_request 트리거: main 브랜치로 향하는 Pull Request가 열리거나 업데이트될 때 실행Job 정의
jobs:
ci-cd:
runs-on: ubuntu-latest
ci-cd Job: 파이프라인의 작업 단위를 정의runs-on: GitHub Actions는 Ubuntu 환경에서 실행CI 단계 (Continuous Integration)
소스 코드 체크아웃
- uses: actions/checkout@v4
JDK 설정
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
Gradle 설정 및 빌드
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4.0.0
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle Wrapper
env: ... # 환경 변수 설정
run: ./gradlew build -x test
CD 단계 (Continuous Deployment)
Docker 이미지 생성
- name: Build Docker Image
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/gunpo .
Docker Hub에 업로드
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Push Docker Image to Docker Hub
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/gunpo
EC2 서버에 배포
- name: Deploy to EC2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ec2-user
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
# Docker 이미지 가져오기
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/gunpo:latest
# 기존 컨테이너 중지 및 삭제
if [ $(sudo docker ps -a -q -f name=gunpo) ]; then
sudo docker stop gunpo
sudo docker rm gunpo
fi
환경 변수 설정 및 실행
echo "SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }}" > .env
sudo docker run -d -p 80:8080 \
--name gunpo \
--env-file .env \
${{ secrets.DOCKERHUB_USERNAME }}/gunpo:latest
Redis 컨테이너 실행
sudo docker run -d --name Gunporedis -p 6379:6379 redis:latest
Docker 리소스 정리
sudo docker system prune -f
최종 Gradle.YAML
name: CI/CD using GitHub Actions & Docker
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
ci-cd:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# 리포지토리 체크아웃
- uses: actions/checkout@v4
# JDK 17 설정
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
# Gradle 설정
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4.0.0
# gradlew 실행 권한 부여
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Gradle 빌드
- name: Build with Gradle Wrapper
env:
SPRING_DATASOURCE_URL: ${{ secrets.SPRING_DATASOURCE_URL }}
SPRING_DATASOURCE_USERNAME: ${{ secrets.SPRING_DATASOURCE_USERNAME }}
SPRING_DATASOURCE_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }}
SERVICE_KEY: ${{ secrets.SERVICE_KEY }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
SPRING_DATA_REDIS_HOST: ${{ secrets.EC2_HOST }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
SPRING_MAIL_USERNAME: ${{ secrets.SPRING_MAIL_USERNAME }}
SPRING_MAIL_PASSWORD: ${{ secrets.SPRING_MAIL_PASSWORD }}
UPLOAD_DIR: ${{ secrets.UPLOAD_DIR }}
GYEONGGI_CURRENCY_DATA_KEY: ${{ secrets.GYEONGGI_CURRENCY_DATA_KEY }}
KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }}
KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }}
NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }}
NAVER_CLIENT_SECRET: ${{ secrets.NAVER_CLIENT_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
run: ./gradlew build -x test
# Docker 이미지 빌드
- name: Build Docker Image
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/gunpo .
# Docker 로그인
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Docker 이미지 푸시
- name: Push Docker Image to Docker Hub
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/gunpo
# EC2에 배포
- name: Deploy to EC2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ec2-user
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
# 최신 Docker 이미지 가져오기
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/gunpo:latest
# 기존 컨테이너 중지 및 삭제
if [ $(sudo docker ps -a -q -f name=gunpo) ]; then
sudo docker stop gunpo
sudo docker rm gunpo
fi
if [ $(sudo docker ps -a -q -f name=Gunporedis) ]; then
sudo docker stop Gunporedis
sudo docker rm Gunporedis
fi
# .env 파일 생성
echo "SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }}" > .env
echo "SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }}" >> .env
echo "SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }}" >> .env
echo "SERVICE_KEY=${{ secrets.SERVICE_KEY }}" >> .env
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
echo "SPRING_DATA_REDIS_HOST=${{ secrets.EC2_HOST }}" >> .env
echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env
echo "SPRING_MAIL_USERNAME=${{ secrets.SPRING_MAIL_USERNAME }}" >> .env
echo "SPRING_MAIL_PASSWORD=${{ secrets.SPRING_MAIL_PASSWORD }}" >> .env
echo "UPLOAD_DIR=${{ secrets.UPLOAD_DIR }}" >> .env
echo "GYEONGGI_CURRENCY_DATA_KEY=${{ secrets.GYEONGGI_CURRENCY_DATA_KEY }}" >> .env
echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
echo "KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }}" >> .env
echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env
echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env
echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env
# Redis 컨테이너 실행
sudo docker run -d --name Gunporedis -p 6379:6379 redis:latest
# 애플리케이션 컨테이너 실행
sudo docker run -d -p 80:8080 \
--name gunpo \
--env-file .env \
${{ secrets.DOCKERHUB_USERNAME }}/gunpo:latest
# 사용하지 않는 Docker 리소스 정리
sudo docker system prune -f
참고