이 글은 인프런의 지식 공유자 박재성님의 강의를 듣고 개인적으로 정리하는 글임을 알립니다.
CI/CD는 소프트웨어 개발에서 중요한 개념 중 하나로, “Continuous Integration(지속적 통합)“과 “Continuous Deployment(지속적 배포)” 또는 “Continuous Delivery(지속적 전달)“를 뜻한다.
이 개념은 개발자가 더 효율적으로 코드를 작성하고, 빠르고 안정적으로 사용자에게 소프트웨어를 제공하는 것을 목표로 한다.
GitHub Actions는 자동화된 워크플로우를 지원하는 강력한 도구로, 저장소의 빌드, 테스트, 배포 등의 작업을 자동화할 수 있다.
즉, GitHub Actions는 CI/CD 과정에서 빌드, 테스트, 배포에 대한 로직을 실행시키는 서버(컴퓨터)의 역할을 한다.
개발자가 코드를 작성 후 커밋 & 푸시를 하는 순간 GitHub Actions는 빌드 및 테스트를 하고 EC2에 배포까지 자동화 할 수 있다.
물론 테스트 코드에서 오류가 난다면 배포가 중단된다.(서비스가 중단되는 것은 아니다.)
기본 문법
GitHub Actions에서 가장 중요한 파일은 deploy.yml인데, 이 파일의 위치는 git으로 관리되는 최상위 루트의 .github/workflows/ 폴더에 저장되어 있어야 한다.
-> 최상위루트/.github/workflows/deploy.yml
# Workflow의 이름
# Workflow : 하나의 yml 파일을 하나의 Workflow라고 부른다.
name: Github Actions 실행시켜보기
# Event : 실행되는 시점을 설정
# main이라는 브랜치에 push 될 때 아래 Workflow를 실행
on:
push:
branches:
- main
# 하나의 Workflow는 1개 이상의 Job으로 구성된다.
# 여러 Job은 기본적으로 병렬적으로 수행된다.
jobs:
# Job을 식별하기 위한 id
My-Deploy-Job:
# Github Actions를 실행시킬 서버 종류 선택
runs-on: ubuntu-latest
# Step : 특정 작업을 수행하는 가장 작은 단위
# Job은 여러 Step들로 구성되어 있다.
steps:
- name: Hello World 찍기 # Step에 이름 붙이는 기능
run: echo "Hello World" # 실행시킬 명령어 작성
- name: 여러 명령어 문장 작성하기
run: |
echo "Good"
echo "Morning"
# 참고: https://docs.github.com/en/actions/learn-github-actions/variables
- name: Github Actions 자체에 저장되어 있는 변수 사용해보기
run: |
echo $GITHUB_SHA
echo $GITHUB_REPOSITORY
- name: Github Actions Secret 변수 사용해보기
run: |
echo ${{ secrets.MY_NAME }}
echo ${{ secrets.MY_HOBBY }}
시크릿 변수는 아래와 같이 지정할 수 있다.
스프링부트 배포 자동화
EC2에서 빌드하는 방식
이 방식은 깃허브에 저장된 소스코드를 PULL 하고 EC2에서 빌드하는 방식이다.
빌드는 컴퓨터의 자원을 많이 소모하는 작업이다.
장점
- git pull을 활용해서 변경된 부분의 프로젝트 코드에 대해서만 업데이트 하기 때문에 CI/CD 속도가 빠르다.
- CI/CD 툴로 Github Actions만 사용하기 때문에 인프라 구조가 복잡하지 않고 간단하다.
단점
- 빌드 작업을 EC2에서 직접 진행하기 때문에 운영하고 있는 서버의 성능에 영향을 미칠 수 있다.
- Github 계정 정보가 해당 EC2에 저장되기 때문에 개인 프로젝트 또는 믿을만한 사람들과 같이 진행하는 토이 프로젝트에서만 사용해야 한다.
이 방법은 주로 개인 프로젝트에서 CI/CD를 심플하고 빠르게 적용시키고 싶을 때 적용한다.
name: Deploy To EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: SSH로 EC2에 접속하기
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }} # EC2의 주소
username: ${{ secrets.EC2_USERNAME }} # EC2 접속 username
key: ${{ secrets.EC2_PRIVATE_KEY }} # EC2접속을 위한 pem 파일 내부 정보
script_stop: true # 아래 script 중 실패하는 명령이 하나라도 있으면 실패로 처리
script: |
cd /home/ubuntu/instagram-server # 여기 경로는 자신의 EC2에 맞는 경로로 재작성하기
git pull origin main
./gradlew clean build
sudo fuser -k -n tcp 8080 || true # || true를 붙인 이유는 8080에 종료시킬 프로세스가 없더라도 실패로 처리하지 않기 위해서이다.
# jar 파일을 실행시키는 명령어이다. 그리고 발생하는 로그들을 ./output.log 파일에 남기는 명령어이다.
nohup java -jar build/libs/*SNAPSHOT.jar > ./output.log 2>&1 &
이렇게 설정하면 output.log를 통해서 로그 데이터를 읽을 수 있다.
민감한 값을 따로 application.yml로 분리하는 경우가 많다. 민감한 값이기에 .gitignore에 추가해서 application.yml가 버전관리 되지 않게 세팅한다.
.gitignore
...
application.yml
deploy.yml 수정
name: Deploy To EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: SSH로 EC2에 접속하기
uses: appleboy/ssh-action@v1.0.3
env:
APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }}
with:
host: ${{ secrets.EC2_HOST }} # EC2의 주소
username: ${{ secrets.EC2_USERNAME }} # EC2 접속 username
key: ${{ secrets.EC2_PRIVATE_KEY }} # EC2의 Key 파일의 내부 텍스트
envs: APPLICATION_PROPERTIES
script_stop: true # 아래 script 중 실패하는 명령이 하나라도 있으면 실패로 처리
script: |
cd /home/ubuntu/instagram-server # 여기 경로는 자신의 EC2에 맞는 경로로 재작성하기
rm -rf src/main/resources/application.yml
git pull origin main
echo "$APPLICATION_PROPERTIES" > src/main/resources/application.yml
./gradlew clean build
sudo fuser -k -n tcp 8080 || true # || true를 붙인 이유는 8080에 종료시킬 프로세스가 없더라도 실패로 처리하지 않기 위해서이다.
nohup java -jar build/libs/*SNAPSHOT.jar > ./output.log 2>&1 &
이렇게 하면 application.yml이 git에 추적되지 않고, 배포를 자동화 할 수 있다.
GitHub Actions에서 빌드하는 방식
이 방식은 빌드를 GitHub Actions에서 하는 방식이다.
장점
- 빌드 작업을 Github Actions에서 하기 때문에 운영하고 있는 서버의 성능에 영향을 거의 주지 않는다.
- CI/CD 툴로 Github Actions만 사용하기 때문에 인프라 구조가 복잡하지 않고 간단하다.
단점
- 무중단 배포를 구현하거나 여러 EC2 인스턴스에 배포를 해야 하는 상황이라면, 직접 Github Actions에 스크립트를 작성해서 구현해야 한다.
- 꽤 복잡하다.
name: Deploy To EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Github Repository 파일 불러오기
uses: actions/checkout@v4
- name: JDK 17버전 설치
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: application.yml 파일 만들기
run: echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./src/main/resources/application.yml
- name: 테스트 및 빌드하기
run: ./gradlew clean build
- name: 빌드된 파일 이름 변경하기
run: mv ./build/libs/*SNAPSHOT.jar ./project.jar
- name: SCP로 EC2에 빌드된 파일 전송하기
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: project.jar
target: /home/ubuntu/instagram-server/tobe
- name: SSH로 EC2에 접속하기
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script_stop: true
script: |
rm -rf /home/ubuntu/instagram-server/current
mkdir /home/ubuntu/instagram-server/current
mv /home/ubuntu/instagram-server/tobe/project.jar /home/ubuntu/instagram-server/current/project.jar
cd /home/ubuntu/instagram-server/current
sudo fuser -k -n tcp 8080 || true
nohup java -jar project.jar > ./output.log 2>&1 &
rm -rf /home/ubuntu/instagram-server/tobe
테스트 및 빌드
- ./gradlew clean build 명령어를 사용해 Gradle로 프로젝트를 빌드하고, 테스트한다.
빌드된 파일 이름 변경
- 빌드된 JAR 파일의 이름에 SNAPSHOT이 포함된 경우, 이 파일을 project.jar로 이름을 변경한다.
SCP로 EC2에 빌드된 파일 전송
- appleboy/scp-action@v0.1.7 액션을 사용해 EC2 인스턴스로 project.jar 파일을 전송한다.
- host, username, key는 GitHub Secrets에 저장된 값들을 사용하여 SSH 접속을 설정한다.
- source는 전송할 파일(project.jar), target은 EC2 인스턴스의 경로이다.
SSH로 EC2에 접속하여 명령어 실행
appleboy/ssh-action@v1.0.3 액션을 사용해 SSH로 EC2에 접속한 후, 다음 작업을 수행한다
- 현재 실행 중인 서버 파일 삭제: /home/ubuntu/instagram-server/current 디렉터리를 제거하고 새로 생성한다.
- 파일 이동: SCP로 전송된 파일을 /tobe 디렉터리에서 /current 디렉터리로 이동시킨다.
- 현재 실행 중인 프로세스 종료: 현재 8080 포트에서 실행 중인 프로세스를 강제로 종료한다.
- 새로 배포한 파일 실행: nohup 명령어를 사용해 백그라운드에서 project.jar을 실행한다.
- 배포 완료 후 불필요한 파일 삭제: /tobe 디렉터리를 삭제한다.
Docker를 이용한 컨테이너 기반 CI/CD(중단 후 배포)
이 방식은 Docker 이미지를 빌드하고 Docker Hub에 푸시한 후, 원격 EC2 서버에서 Docker Compose를 이용해 배포하는 방식이다.
deploy.yml
name: CI/CD for test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Create application.yml from secrets
run: echo "${{ secrets.R2R_APPLICATION_YML }}" > ./SpringBoot/src/main/resources/application.yml
- name: Build with Gradle
run: ./gradlew clean build
working-directory: ./SpringBoot # SpringBoot 디렉터리에서 빌드 실행
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
# SpringBoot 애플리케이션 이미지 빌드 및 푸시
- name: Build and push test image
run: |
docker build -t <도커 허브 계정>/test ./SpringBoot/
docker push <도커 허브 계정>/test
# MySQL 이미지 빌드 및 푸시
- name: Build and push MySQL image
run: |
docker build -t <도커 허브 계정>/test-db ./mysql/
docker push <도커 허브 계정>/test-db
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: SSH to EC2 and Deploy
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.R2R_EC2_HOST }}
username: ${{ secrets.R2R_EC2_USERNAME }}
key: ${{ secrets.R2R_EC2_PRIVATE_KEY }}
script: |
cd /home/ubuntu/test
docker compose down
docker compose pull
docker compose up -d
compose.yml
services:
<서비스 이름>:
image: <도커 허브 계정>/<이미지 이름>
platform: linux/amd64
ports:
- 80:8080
depends_on:
<앱이름-db>:
condition: service_healthy
networks:
- <네트워크 이름>
<앱이름-db>:
image: <도커 허브 계정>/<이미지 이름>
platform: linux/amd64
environment:
MYSQL_ROOT_PASSWORD: <루트 패스워드>
MYSQL_DATABASE: <데이터베이스 이름>
MYSQL_USER: <유저 이름>
MYSQL_PASSWORD: <유저 패스워드>
volumes:
- <볼륨 이름>:/var/lib/mysql
ports:
- 3306:3306
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
retries: 10
networks:
- <네트워크 이름>
networks:
<네트워크 이름>:
volumes:
<볼륨 이름>: