

Continuous Intergration Continoues Delivery
CI는 개발자를 위한 자동화 프로세스인 지속적 통합을 의미한다.
CD는 지속적인 서비스 제공 및 지속적 배포를 의미한다.

지속적 통합(CI)은 개발 팀이 코드를 지속적으로 통합하고, 이를 자동으로 테스트 하여 통합 버그를 최소화 하는 프로세스를 말한다.
- git push를 할때마다 자동으로 build & test 를 실행
- 자동화 배포를 통하여 빈번한 코드 통합이 가능하다.
- 자동 테스트로 코드가 충돌 되는 현상을 미리 발견할 수 있다.
지속적 배포(CD)는 지속적으로 통합된 코드를 자동으로 프로덕션 환경에 배포하는 프로세스다.
- 보다 빠르게 기능 개발과 버그 수정 사항이 사용자들에게 제공된다.
- Jenlkins
- GitHubActions
- AWS CodePipeline
... 등

Jenkins
1. 높은 커스터 마이징
2. 풍부한 플러그인
3. 좀 더 세밀한 파이프라인 제어 가능
4. 설치와 유지 보수 필요
5. 별도의 서버 리소스가 필요
6. 초기 러닝커브 곡선이 있다.

GitHubActions
1. gitHub 와 완벽한 통합
2. 서버 리소스와 유지보수 불필요
3. 비교적 간단한 설정
4. YAML 기반의 직관적인 문법
5. GitHub 종속성
6. 비교적 제한적 커스터마이징
혼자 진행 하게 되는 소규모 1인 프로젝트인 현재 GitHubActions 를 통해 구현 하는 것이 낫다고 판단 했다.
블루-그린

8080 포트로 연결된 컨테이너

8081 포트로 다른 버전의 컨테이너 생성

nginX.conf 수정 후 reload(업스트림 8081 수정

8080 컨테이너 제거
Rolling
카나리
가급적이면 비용 문제상 aws의 부수적인 서비스들은 이용하지 않는 방향으로 진행 했다. (elastic cache(redis), aws elastic load balancing, aws route53)
본 진행하는 프로젝트는 소규모 프로젝트로 비교적 설정이 간단하고 세밀한 workflow만 구현하면 되는 조건이라, jenkins가 아닌 githubactions으로 docker image 를 build 후 ec2 인스턴스에 배포 하기로 결정 했다. DB는 따로 ec2 에 올리지 않고 RDS를 이용 했다.SSL 설정은 route53을 이용하지 않고 가비아에서 도메인을 직접 구매 자체 DNS 서비스를 이용해 포팅 하기로 결정 했다. 인증서는 certbot을 이용해 Let's Encrypt 인증서를 받고 nginX 를 이용해 연동 해주었다. 수정이 잦은 conf 파일을 포함해 docker-compose.yml 파일은 workflow.yml 에 포함시켜 배포 했다. 다루기 민감한 값들과 application.yml은 base64로 인코딩 후 Repository secrets 에 담아줬다.
application.yml 수정이 생길 때 마다 github secrets 에 수동으로 수정하는 작업을 거쳐야 하는데 추후 이 과정도 수정이 필요해 보인다. code Deploy를 추가하지 않고 단일 컨테이너 CI/CD 파이프라인이다. 빌드 후 2분 가량 멈추는 시간이 발생한다. 즉 무중단 배포는 아니다. 추후 기존 활용하고 있는 nginX의 로드밸런싱 기능과 codeDeploy 를 추가해 blue/green 배포 전략을 활용할 예정이다. master dev productuon 등 실무환경 처럼 branch를 별도로 나누지 않고 master branch로 push 시에 파이프라인이 진행되게 설정 했다.
ec2 인스턴스에 443 포트(ssl), 80 포트(nginx), 8080 포트를 열고 인증서 발급 과정, ec2 생성과 rds 연결, IAM 생성 및 권한 설정, 도메인 구입 과정과 DNS 연결 과정, docker hub 연결 등의 과정들은 다루지 않을 예정.
workflow.yml
# 워크플로우의 이름 설정
name: Java CI/CD with Gradle
# 워크플로우 실행 조건 설정
# master 브랜치에 push나 pull request가 발생할 때 실행
on:
push:
branches:
- master
pull_request:
branches:
- master
# GitHub Actions의 기본 권한 설정
# contents: read는 저장소 내용을 읽을 수 있는 권한만 부여
permissions:
contents: read
# 전역 환경 변수 설정
# Docker 이미지명, API 도메인, 이메일 등 워크플로우에서 사용할 변수들
env:
DOCKER_IMAGE: ${{ secrets.DOCKER_USERNAME }}/spring
API_DOMAIN: api.togerun.shop
EMAIL: ${{ secrets.EMAIL }}
# 실제 작업(job) 정의
jobs:
# build-and-deploy라는 이름의 작업 정의
build-and-deploy:
runs-on: ubuntu-latest # Ubuntu 최신 버전에서 실행
permissions:
contents: read # 이 작업의 권한 설정
# 작업의 각 단계(step) 정의
steps:
# Step 1: GitHub 저장소 코드를 가져오기
- name: Checkout Repository
uses: actions/checkout@v4 # GitHub 제공 액션 사용
# Step 2: Java 개발 환경 구성
# JDK 17 설치 및 설정
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17' # Java 버전 지정
distribution: 'temurin' # JDK 배포판 지정
# Step 3: application.yml 파일 생성
# GitHub Secrets에 저장된 설정 파일을 디코딩하여 생성
- name: Create and verify application.yml
run: |
mkdir -p ./src/main/resources
cd ./src/main/resources
echo "${{ secrets.APPLICATION_YML }}" | base64 -d > application.yml
echo "Created application.yml:"
cat application.yml
# Step 4: Gradle 설정
# Gradle 빌드 도구 설정
- name: Setup Gradle
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5
# Step 5: Gradle 실행 권한 부여
# gradlew 파일에 실행 권한 부여
- name: Grant Execute Permission For Gradlew
run: chmod +x gradlew
# Step 6: Gradle을 사용하여 프로젝트 빌드
- name: Build with Gradle
run: ./gradlew build --info
# Step 7: Docker 빌드 컨텍스트 생성
# JAR 파일과 Dockerfile을 준비
- name: Create build context
run: |
mkdir -p docker-build
EXEC_JAR=$(find build/libs/ -name "*.jar" -not -name "*plain.jar" -type f)
if [ -z "$EXEC_JAR" ]; then
echo "Error: No executable JAR file found"
exit 1
fi
echo "Found executable JAR: $EXEC_JAR"
cp "$EXEC_JAR" docker-build/app.jar
cp Dockerfile docker-build/
# Step 8: DockerHub 로그인
# Docker Hub 인증 처리
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Step 9: Docker 이미지 빌드 및 푸시
# 애플리케이션 Docker 이미지 생성 및 Docker Hub에 업로드
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: docker-build
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/spring:latest
# Step 10: EC2 인스턴스에 배포
# SSH를 통해 EC2 인스턴스에 접속하여 배포 수행
- name: Deploy to EC2
uses: appleboy/ssh-action@master
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
PUBLIC_IP_V4: ${{ secrets.PUBLIC_IP_V4 }}
EMAIL: ${{ secrets.EMAIL }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
with:
host: ${{ secrets.HOST_DEV }} # EC2 호스트 주소
username: ${{ secrets.USERNAME }} # EC2 접속 사용자명
key: ${{ secrets.PRIVATE_KEY }} # EC2 접속 키
port: 22 # SSH 포트
envs: DOCKER_USERNAME,PUBLIC_IP_V4,EMAIL,REDIS_PASSWORD # 환경변수 전달
script: |
# SSL 인증서 디렉토리 구성
# Let's Encrypt SSL 인증서 관련 디렉토리 생성
mkdir -p certbot/conf
mkdir -p certbot/www
mkdir -p ssl
# docker-compose.yml 파일 생성
# 컨테이너 구성 정의: nginx, certbot, spring-boot, redis
cat << EOF > docker-compose.yml
# ... [docker-compose.yml 내용] ...
EOF
# nginx.conf 파일 생성
# Nginx 웹 서버 설정: SSL, CORS, 프록시 등 설정
cat << EOF > nginx.conf
# ... [nginx.conf 내용] ...
EOF
# 기존 Docker 컨테이너 정리
# 이전 컨테이너 종료 및 이미지 정리
docker-compose down --remove-orphans
docker image prune -f
# Docker 네트워크 생성
# 컨테이너 간 통신을 위한 네트워크 구성
docker network create ubuntu_this_network || true
# 최신 이미지 배포 및 서비스 시작
# 새 버전 배포 및 컨테이너 실행
docker-compose pull
docker-compose up -d
# Step 11: 배포 시간 기록
# 배포 완료 시간 기록 (한국 시간 기준)
- name: Get Current Time
uses: 1466587594/get-current-time@v2
id: current-time
with:
format: YYYY-MM-DDTHH:mm:ss
utcOffset: "+09:00"
# 배포 시간 출력
- name: Print Current Time
run: echo "Current Time=${{steps.current-time.outputs.formattedTime}}"
shell: bash
각 단계의 수행 역할 정리
- 빌드 준비 단계(Step1-3)
- 소스코드 체크 아웃
- JDK 설정
- 설정 파일 생성
- 빌드 단계 (Step4-7)
- Gradle 설정
- 프로젝트 빌드
- Docker 빌드 준비
- 컨테이너화 단계 (Step 8-9)
- DockerHub 로그인
- image 빌드 푸시
- 배포 단계 (Step 10)
- EC2 접속
- 인프라 구성
- 컨테이너 배포
- 완료 단계 (Step 11)
- 배포 시간 기록

참고 : https://velog.io/@leejungho9/CICD-%EB%9E%80,
https://minsu20.tistory.com/27
https://wikidocs.net/197261