CI/CD는 개발자가 작성한 코드를 자동으로 빌드, 테스트, 배포하는 파이프라인을 의미합니다.
CI (Continuous Integration, 지속적 통합)
여러 개발자의 코드 변경 사항을 자주 메인 브랜치에 통합하고, 자동 빌드 + 테스트를 통해 문제를 빠르게 발견합니다.
CD (Continuous Deployment, 지속적 배포)
빌드와 테스트를 통과한 코드를 운영 서버에 자동 배포하는 과정입니다.
수동 배포 없이 안정적이고 빠른 릴리스를 가능하게 해줍니다.
✅ CI/CD를 도입하면?

push하면appspec.yml과 스크립트에 따라 자동 실행1. AWS EC2에 Tag 설정
EC2 인스턴스 설정에서 태그 관리를 누르고 태그를 확인
2. AWS EC2에 IAM 설정
역할 생성 -> EC2 선택 ->
AmazonS3FullAccess권한 추가 -> EC2 인스턴스에서 IAM 역할 수정
3. 서버에 CodeDeploy Agent 설치
#패키지 설치
sudo apt update -y
sudo apt install ruby wget -y
#CodeDeploy 에이전트 설치
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto
#에이전트 정상 동작 확인
sudo service codedeploy-agent status
4. AWS S3 생성
S3 버킷 생성(버킷 이름, AWS 리전, ACL 비활성화, 퍼블릭 엑세스 차단, 버킷 버전 관리 비활성화, 버킷 키 비활성화)
5. AWS CodeDeploy 전용 IAM 역할 생성
역할 생성 -> CodeDeploy 선택 ->
AWSCodeDeployRole권한 추가
6. AWS CodeDeploy Application 생성 및 배포 설정
CodeDeploy 애플리케이션 생성 -> 배포 그룹 생성 -> 서비스 역할에 CodeDeploy IAM 추가 -> 환경구성 EC2 인스턴스 -> 태그 그룹 -> 에이전트 한 번만 설치 -> 배포 설정 -> 로드 밸런서 비활성화
7. Github Actions의 사용자 권한 추가
IAM 사용자 생성 ->
AWSCodeDeployFullAccess,AmazonS3FullAccess권한 추가 -> 보안 자격 증명 탭에서 엑세스 키 만들기 -> 기타 -> Github Repository -> Settings -> Secrets and variables -> Actions -> New repository secret에 엑세스 키, 비밀 키 만들기
8. App Spec 작성
version: 0.0
os: linux
#배포 파일 설정
files:
- source: /
destination: /home/ubuntu/project
overwrite: yes
#이미 있을 경우 덮어쓰기
file_exists_behavior: OVERWRITE
#files 섹션에서 복사한 파일에 대한 권한 설정
permissions:
- object: /
pattern: "**"
owner: ubuntu
group: ubuntu
#배포 이후에 실행할 일련의 라이프사이클
hooks:
BeforeInstall:
- location: scripts/cleanup.sh
timeout: 60
runas: ubuntu
AfterInstall:
- location: scripts/stop.sh
timeout: 60
runas: ubuntu
ApplicationStart:
- location: scripts/start.sh
timeout: 60
runas: ubuntu
9. 배포 스크립트(cleanup.sh, start.sh, stop.sh) 작성 & build.gradle 파일 수정
1) scripts/cleanup.sh
#!/usr/bin/env bash
# 프로젝트 경로
PROJECT_ROOT="/home/ubuntu/ptufestival"
OLD_DIR="$PROJECT_ROOT/old"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
# old 디렉토리 없으면 생성
mkdir -p "$OLD_DIR"
# 기존 jar 백업
if [ -f "$PROJECT_ROOT/spring-webapp.jar" ]; then
echo "$TIME_NOW > 기존 JAR 백업 시작" >> $DEPLOY_LOG
mv "$PROJECT_ROOT/spring-webapp.jar" "$OLD_DIR/spring-webapp-$(date +%Y%m%d%H%M%S).jar"
echo "$TIME_NOW > spring-webapp.jar 백업 완료" >> $DEPLOY_LOG
else
echo "$TIME_NOW > 백업할 기존 JAR 없음" >> $DEPLOY_LOG
fi
# 오래된 로그 정리 (선택)
find "$PROJECT_ROOT" -name "*.log" -type f -mtime +14 -exec rm -f {} \;
echo "$TIME_NOW > 14일 초과 로그 파일 정리 완료" >> $DEPLOY_LOG
2) scripts/start.sh
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/project"
JAR_SOURCE=$(ls $PROJECT_ROOT/build/libs/*.jar | tail -n 1)
JAR_TARGET="$PROJECT_ROOT/spring-webapp.jar"
APP_LOG="$PROJECT_ROOT/app.log"
ERROR_LOG="$PROJECT_ROOT/error.log"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
PORT=443
echo "$TIME_NOW > 새 애플리케이션 배포 시작" >> $DEPLOY_LOG
if [ ! -f "$JAR_SOURCE" ]; then
echo "$TIME_NOW > JAR 파일이 존재하지 않아 복사 실패!" >> $DEPLOY_LOG
exit 1
fi
echo "$TIME_NOW > JAR 복사: $JAR_SOURCE -> $JAR_TARGET" >> $DEPLOY_LOG
cp "$JAR_SOURCE" "$JAR_TARGET"
chmod +x "$JAR_TARGET"
echo "$TIME_NOW > 실행 권한 부여 및 애플리케이션 실행" >> $DEPLOY_LOG
# 백그라운드 실행
sudo nohup java -Duser.timezone=Asia/Seoul -jar "$JAR_TARGET" > "$APP_LOG" 2> "$ERROR_LOG" &
sleep 5
# 포트 점유 상태 확인
if sudo lsof -i :$PORT -sTCP:LISTEN > /dev/null; then
echo "$TIME_NOW > 포트 $PORT 정상적으로 점유됨. 실행 성공 추정" >> $DEPLOY_LOG
sudo lsof -i :$PORT -sTCP:LISTEN >> $DEPLOY_LOG
else
echo "$TIME_NOW > 포트 $PORT 점유 안됨. 실행 실패 가능성 있음" >> $DEPLOY_LOG
tail -n 20 "$ERROR_LOG" >> $DEPLOY_LOG
fi
3) scripts/stop.sh
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/project"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
PORT=443
echo "$TIME_NOW > 실행 중인 애플리케이션 종료 시도" >> $DEPLOY_LOG
# 443 포트 LISTEN 중인 PID 탐색
CURRENT_PID=$(sudo lsof -i :$PORT -sTCP:LISTEN -t)
if [ -z "$CURRENT_PID" ]; then
echo "$TIME_NOW > 포트 $PORT LISTEN 중인 프로세스 없음" >> $DEPLOY_LOG
else
echo "$TIME_NOW > 포트 $PORT LISTEN 중인 PID $CURRENT_PID 종료 시도" >> $DEPLOY_LOG
sudo kill -15 "$CURRENT_PID"
sleep 3
if ps -p "$CURRENT_PID" > /dev/null; then
echo "$TIME_NOW > 정상 종료 실패, 강제 종료 시도" >> $DEPLOY_LOG
sudo kill -9 "$CURRENT_PID"
fi
fi
10. Github Actions Workflow 작성
project/.github/workflows/deploy.yml
name: Deploy to Amazon EC2
on:
push:
branches:
- main
env:
AWS_REGION: ap-northeast-2
PROJECT_NAME: project
S3_BUCKET_NAME: {secrets.S3_BUCKET_NAME}
CODE_DEPLOY_APPLICATION_NAME: your_code_deploy_name
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: your_code_group_name
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
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: Build with Gradle
run: ./gradlew clean build --stacktrace --info
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Upload to AWS S3
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source .
- name: Deploy to AWS EC2 from S3
run: |
aws deploy create-deployment \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
🚨 env와 secrets 만드는 부분은 본인에 맞게 수정하셔야 됩니다.
11. Github에서 push로 배포 확인
git push 후 Actions에서 잘 되는지 확인하기
project/
├── .github/
│ └── workflows/
│ └── deploy.yml # GitHub Actions 워크플로우
├── appspec.yml # CodeDeploy 배포 설정
├── scripts/
│ ├── start.sh # 애플리케이션 실행 스크립트
│ ├── stop.sh # 애플리케이션 종료 스크립트
│ └── cleanup.sh # 이전 버전 정리
├── build/libs/
│ └── spring-webapp.jar # 빌드 산출물
└── ...
chmod +x) 부여했는가?CI/CD의 장점은 개발자에게 신속한 배포, 운영자에게 안정성, 팀 전체에게는 생산성 향상을 가져다 줄 수 있습니다.
아직 학생이라 부족한 점이 있을 수 있습니다. 댓글과 피드백은 언제든지 환영입니다!