CI/CD는 약어로, 몇 가지의 다른 의미를 가지고 있다. CI/CD의 "CI"는 개발자를 위한 자동화 프로세스인 지속적인 통합(Continuous Integration)을 의미한다.
CI를 성공적으로 구현할 경우 애플리케이션에 대한 새로운 코드 변경 사항이 정기적으로 빌드 및 테스트되어 공유 리포지토리에 통합되므로 여러 명의 개발자가 동시에 애플리케이션 개발과 관련된 코드 작업을 할 경우 서로 충돌할 수 있는 문제를 해결할 수 있다.
CI/CD의 "CD"는 지속적인 서비스 제공(Continuous Delivery) 및/또는 지속적인 배포(Continuous Deployment)를 의미하며 이 두 용어는 상호 교환적으로 사용된다. 두 가지 의미 모두 파이프라인의 추가 단계에 대한 자동화를 뜻하지만 때로는 얼마나 많은 자동화가 이루어지고 있는지를 설명하기 위해 별도로 사용되기도 한다.
.github/workflows 디렉토리에 필요한 Actions 파일들을 yaml 형식으로 작성.github/workflows 폴더 아래에 YAML 파일이 위치gradle.yml 의 코드를 살펴보자!! (우리 프로젝트는 Monolithic Architecture에서 MSA(MicroService Architecture)로 아키텍쳐 구성을 시도하여 아래 파일을 각 프로젝트마다 적용하였다.)gradle.yml
name: Java CI with Gradle
on:
push:
branches: [ "main" ] # main branch로 push 할 때
pull_request:
branches: [ "main" ] # main branch로 pull_request 할 때
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
# resources/application.yml 파일에 환경변수로 적용된 값들을 넣어주기 위함.
- name: Set yml files
uses: microsoft/variable-substitution@v1
with:
files: ./src/main/resources/application.yml
env:
spring.datasource.url: ${{ secrets.RDS_HOST }}
spring.datasource.username: ${{ secrets.RDS_USERNAME }}
spring.datasource.password: ${{ secrets.RDS_PASSWORD }}
jwt.key: ${{ secrets.JWT_KEY }}
# gradlew 실행 권한 부여
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# test를 제외하고 build
- name: Build with Gradle
run: ./gradlew clean build -x test
# Docker 이미지 빌드
- name: docker image build
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/user .
# DockerHub 로그인
- name: docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Docker Hub 이미지 푸시
- name: docker Hub push
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/user
#CD
- name: Deploy
uses: appleboy/ssh-action@v1.0.3
with:
# ec2에 ssh로 접속하여 script 실행
host: ${{ secrets.SSH_HOST }}
username: ubuntu
key: ${{ secrets.SSH_KEY }}
port: 22
script: |
# 기존에 'docker compose up'으로 실행중인 docker container 종료
docker compose down
# 위에서 새로 빌드한 후 docker hub에 push한 docker image pull
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/user
# 새로 pull 받은 docker image 포함하여 docker compose up
# --detach는 백그라운드로 실행함.
docker compose up --detach
- CI(Continuous Integration)
# resources/application.yml 파일에 환경변수로 적용된 값들을 넣어주기 위함.
- name: Set yml files
uses: microsoft/variable-substitution@v1
with:
files: ./src/main/resources/application.yml
env:
spring.datasource.url: ${{ secrets.RDS_HOST }}
spring.datasource.username: ${{ secrets.RDS_USERNAME }}
spring.datasource.password: ${{ secrets.RDS_PASSWORD }}
jwt.key: ${{ secrets.JWT_KEY }}
먼저 files에 application.yml의 경로를 작성한다.
application.properties 는 제대로 인식하지 못하는 문제가 생겨 yml파일로 변경했다.${{ }}안의 변수들은 github secret에서 환경변수 값을 셋팅해줘야 한다.

이 환경변수 값들은 한번 저장하면 수정, 삭제만 가능하며 조회는 불가능하다.
이렇게 셋팅된 값들이application.yml의 환경변수로 되어있는 부분에 적용된다.
application.yml
spring:
application:
name: dollar-user
datasource:
url: ${rds.host}
username: ${rds.username}
password: ${rds.password}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
show_sql: true
format_sql: true
use_sql_comments: true
jwt:
key: ${jwt.secret.key}
server:
port: 8082
# Docker 이미지 빌드
- name: docker image build
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/user .
# DockerHub 로그인
- name: docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Docker Hub 이미지 푸시
- name: docker Hub push
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/user
Docker Image로 빌드하기 전 Dockerfile을 프로젝트에 만들어줘야 한다.
프로젝트 최상단 레벨에서 Dockerfile생성
# Dockerfile
# jdk17 Image Start
FROM openjdk:17
# 인자 설정 부분과 jar 파일 복제 부분 합쳐서 진행해도 무방
COPY build/libs/*.jar app.jar
# 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]
build/libs 내의 모든jar파일을 app.jar 라는 이름으로 복사docker container가 실행될 때마다 app.jar 실행Dockerfile를 바탕으로 Docker Image 생성
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/user .
위 명령어를 실행하면 ${{ secrets.DOCKERHUB_USERNAME }}/user라는 이름의 Docker Image가 생성된다.
이후 gradle.yml파일에 작성된 내용대로 docker hub 에 login한 후
${{ secrets.DOCKERHUB_USERNAME }}/user라는 Repository로 push
- CD(Continuous Delivery)
❗ EC2에 Docker 및 Docker compose 설치가 필요하다.
- name: Deploy
uses: appleboy/ssh-action@v1.0.3
with:
# ec2에 ssh로 접속하여 script 실행
host: ${{ secrets.SSH_HOST }}
username: ubuntu
key: ${{ secrets.SSH_KEY }}
port: 22
먼저 ec2에 ssh 접속을 위해 우리의 ec2 인스턴스의 public Ipv4 DNS 주소를
SSH_HOST에 셋팅해준다.

인스턴스 생성할 때 같이 생성 또는 설정한 키 페어를 SSH_KEY에 셋팅해줘야 한다.
docker compose를 하기 위해서는 먼저 ec2 쪽에 docker-compose.yml파일을 작성해야 한다.
services:
front-service:
image: ssuminn/front:latest
ports:
- "8081:8081"
networks:
- app_network
user-service:
image: ssuminn/user:latest
ports:
- "8082:8082"
networks:
- app_network
product-service:
image: ssuminn/product:latest
ports:
- "8083:8083"
networks:
- app_network
order-service:
image: ssuminn/order:latest
ports:
- "8084:8084"
networks:
- app_network
review-service:
image: ssuminn/review:latest
ports:
- "8085:8085"
networks:
- app_network
networks:
app_network:
driver: bridge
docker-compose.yml파일을 기반으로 docker compose up을 실행할 수 있다.
- name: Deploy
uses: appleboy/ssh-action@v1.0.3
with:
# ec2에 ssh로 접속하여 script 실행
host: ${{ secrets.SSH_HOST }}
username: ubuntu
key: ${{ secrets.SSH_KEY }}
port: 22
script: |
docker compose down
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/user
docker compose up --detach
이후 script에는 ec2에서 실행할 명령어를 순서대로 작성해준다.
DOCKERHUB_USERNAME/user라는 이름의 Repository를 pull 받는다. 실행중인 ec2 인스턴스에서 docker compose up이 제대로 작동되는지 확인하기 위해 docker ps로 확인
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8d0d984ee9d0 ssuminn/user:latest "java -jar app.jar" 6 hours ago Up 6 hours 0.0.0.0:8082->8082/tcp, :::8082->8082/tcp ubuntu-user-service-1
209875fb8d33 ssuminn/front:latest "java -jar app.jar" 6 hours ago Up 6 hours 0.0.0.0:8081->8081/tcp, :::8081->8081/tcp ubuntu-front-service-1
31f3f22ea155 ssuminn/review:latest "java -jar app.jar" 6 hours ago Up 6 hours 0.0.0.0:8085->8085/tcp, :::8085->8085/tcp ubuntu-review-service-1
b44989ca02ae ssuminn/product:latest "java -jar app.jar" 6 hours ago Up 6 hours 0.0.0.0:8083->8083/tcp, :::8083->8083/tcp ubuntu-product-service-1
d16b5f42f859 ssuminn/order:latest "java -jar app.jar" 6 hours ago Up 6 hours 0.0.0.0:8084->8084/tcp, :::8084->8084/tcp ubuntu-order-service-1
docker compose up --detach docker compose를 백그라운드로 실행하기 때문에 로그 확인은 docker compose logs로 확인가능하다.

gradle.yml의 실행 과정을 다시 한번 정리해보자.
1. main branch로 push 또는 pull request가 발생한다.
2. application.yml에 환경변수를 셋팅한다.
3. gradlew 실행 권한을 부여한 후 테스트를 제외하고 build.
4. docker image를 build
5. docker hub에 login
6. docker hub에 image push
7. ec2 인스턴스에 ssh 접속
8. docker compose down
9. docker hub에 새롭게 push된 image pull
10. 새로운 image로 docker compose up
Dockerfile
# Dockerfile
# jdk17 Image Start
FROM --platform=linux/arm64 openjdk:17
# 인자 설정 - JAR_File
ARG JAR_FILE=build/libs/*.jar
# jar 파일 복제
COPY ${JAR_FILE} app.jar
# 인자 설정 부분과 jar 파일 복제 부분 합쳐서 진행해도 무방
#COPY build/libs/*.jar app.jar
# 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]
name: Java CI with Gradle
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
#CI
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Set yml files
uses: microsoft/variable-substitution@v1
with:
files: ./src/main/resources/application.yml
env:
spring.datasource.url: ${{ secrets.RDS_HOST }}
spring.datasource.username: ${{ secrets.RDS_USERNAME }}
spring.datasource.password: ${{ secrets.RDS_PASSWORD }}
spring.data.redis.host: ${{ secrets.REDIS_HOST }}
jwt.key: ${{ secrets.JWT_KEY }}
kakao.api.admin-key: ${{ secrets.KAKAO_ADMIN_KEY }}
loadbalancer.user: ${{ secrets.LOADBALANCER_USER }}
loadbalancer.product: ${{ secrets.LOADBALANCER_PRODUCT }}
aws.access.key: ${{secrets.AWS_ACCESS_KEY_ID}}
aws.secret.key: ${{secrets.AWS_SECRET_ACCESS_KEY}}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew clean build -x test
# Docker 이미지 빌드
- name: docker image build
run: docker buildx build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/order .
# DockerHub 로그인
- name: docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Docker Hub 이미지 푸시
- name: docker Hub push
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/order
#CD
- name: Deploy
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ubuntu
key: ${{ secrets.SSH_KEY }}
port: 22
script: |
sudo docker stop $(sudo docker ps -q --filter ancestor=jw059/order)
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/order
sudo docker run --platform linux/amd64 -d -e destination=${{ secrets.DESTINATION }} -p 8084:8084 ${{ secrets.DOCKERHUB_USERNAME }}/order
sudo docker container prune -f
sudo docker image prune -f