앞선 포스트에서는 배포를 위한 EC2,RDS 인스턴스와 S3 버킷 생성과 관련 설정까지 완료하였다.
그 후 프로젝트를 직접 배포하기 위해 git clone 방식을 우선 시도 한 후, jar 파일을 FTP를 사용하여 직접 전달하는 방법으로 배포를 완료하였다.
이번 포스트에서는 CI/CD 파이프라인을 구축하기 위해 Github Actions와 Docker를 활용 해볼 것이며 이를 위해 Docker를 사용해 프로젝트를 배포하는 방법을 확인 해볼 것이다.

도커를 활용한 배포 흐름은 간략하게 아래와 같다.
로컬 환경에 Docker를 설치한다.
OS별 Docker 설치 방법 참고

Docker Hub 에 docker image를 push할 리포지토리를 생성한다.
리포지토리 이름과 설명을 작성한 후 Public으로 생성한다. (Private의 경우 계정 당 1개까지만 무료로 생성이 가능하다.)
프로젝트의 최상위 경로 아래에 Dockerfile을 생성한다. (※ src 아래에 생성하지 않도록 주의)
Dockerfile 작성을 위해 다음과 같은 지시어가 사용된다.
| 지시어 | 설명 |
|---|---|
| FROM | 베이스 이미지 지정 |
| RUN | 이미지를 지정하면서 실행할 명령 지정 |
| ENTRYPOINT | 컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어) |
| EXPOSE | 컨테이너의 포트 지정 |
| ADD | 이미지 생성 시 파일 추가 |
| COPY | 이미지 생성시 파일 복사 |
| WORKDIR | 컨테이너 작업 디렉토리 지정 |
| MAINTAINER | 이미지 작성자 명시 |
| CMD | 컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어) |
| LABEL | 이미지의 라벨 지정 |
| ENV | 컨테이너의 환경 변수 지정 |
| VOLUME | 컨테이너의 볼륨 지정 |
| USER | 컨테이너의 사용자 지정 |
| ARG | 인자 설정 |
Dockerfile
# jdk 17(amazoncorretto:17) 환경으로 구성
FROM amazoncorretto:17
# 인자 설정 :: 변수명 JAR_FILE
# build/libs(빌드 시 jar파일 생성 경로) 하위의 모든 jar파일
ARG JAR_FILE=build/libs/*.jar
# Docker Image 생성 시 JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar
# 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]
./gradlew clean build
프로젝트 경로의 터미널에서 build하여 jar파일을 생성한다.
Docker Image를 생성해서 Docker Hub에 push 해줄 것이다.
만약 Docker에 로그인되어 있지 않다면 아래와 같은 방법으로 로그인 해주자.
docker login -u [username] # 이후 password 입력
# 이미지 생성(build) 명령어
# (태그 생략 시 기본 :latest로 지정)
docker build -t [도커 허브 ID]/[이미지명]:[태그명] [Dockerfile위치]
# 실제 작성
# . (현재 위치)
docker build -t dulledulle/dulle-dulle-springboot .
맥북의 경우 명령어에
--platform linux/amd64를 추가해줘야 한다.
생성된 이미지는
docker images명령어로 목록을 확인 할 수 있다.
# push 명령어
docker push [도커 허브 ID]/[이미지명]
# 실제 작성
docker push dulledulle/dulle-dulle-springboot

리포지토리의 Tags를 확인해보면 정상적으로 push 됐음을 확인 할 수 있다.
앞서 생성한 EC2 인스턴스의 서버(ubuntu)에서 Docker Hub에 push된 이미지를 push 한다.
# 도커 설치
sudo apt-get install docker.io -y
# docker 서비스 실행
sudo service docker start
# /var/run/docker.sock 파일 권한 변경, 그룹 내 다른 사용자도 접근 가능하게 변경
sudo chmod 666 /var/run/docker.sock
# ubuntu 유저를 docker 그룹에 추가
sudo usermod -a -G docker ubuntu
sudo docker pull [도커 허브 ID]/[이미지명]
sudo docker run -d -p [로컬 port]:[도커 port] [도커 허브 ID]/[이미지명]
이번에는 위의 과정 (build - docker image 생성 - docker hub로 push - 서버에서 pull - container 실행) 을 자동화하여 변경사항을 commit/push 할 시 서버에 배포까지 한번에 이루어지도록 CI/CD 파이프라인을 구축해보자.
이번 프로젝트의 CI/CD 파이프라인 구축에는 Github Actions를 활용할 것이다.
Github Actions란 Github 저장소를 기반으로 소프트웨어 개발 Workflow를 자동화 할 수 있는 도구다. 특정한 브랜치에 대한 이벤트(트리거)를 통해 설정된 Workflow를 따라 빌드, 테스트, 배포 등의 다양한 동작을 자동으로 실행되게 할 수 있다.
프로젝트 리포지토리의 Settings-Security-Secrets and Variables -Actions 경로에서 보안상의 이유로 Github에 올릴 수 없는 환경변수들을 작성해준다.

Secrets는 Name-Secret(Key-Value)의 형태로 변수 값들을 관리하며 Name은 임의로 작성해줄 수 있다.
프로젝트 리포지토리의 Actions-New Workflow 를 통해 workflow 파일 작성을 위한 양식을 선택할 수 있다.

'Java with Gradle' 을 선택하면 Java-Gradle 환경에서 사용할 수 있는 workflow 파일의 기본양식을 보여준다. 파일의 이름을 설정하여 생성해준 후 내용을 원하는 workflow에 맞춰 수정한다.


Workflow 파일을 생성한 뒤 프로젝트를 update하면 프로젝트 최상단에 .github - workflows - <설정한 파일 이름>.yml 이 생성되어있다.
이 파일을 아래와 같이 수정해주었다.
dulle-dulle-ci-setting.yml
# Github Actions에 표시되는 Workflow 이름
name: Dulle-Dulle CI/CD
# master 브랜치로 push, pull request가 발생하면 workflow를 실행
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
# workflow에서 실행할 동작
jobs:
CI:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# JDK 세팅
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
# 빌드 시간 향상을 위한 Gradle 캐싱
- name: Gradle Caching
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# application.properties 파일 생성
- name: Make application.properties
run: mkdir ./src/main/resources |
touch ./src/main/resources/application.properties
- name: Deliver application.properties
run: echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.properties
# Gradle 설정 및 Build
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Change gradlew permissions
run: chmod +x ./gradlew
- name: Build with Gradle Wrapper
run: ./gradlew clean build
# Docker 로그인
- name: Docker Login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Docker 이미지 생성 및 Push
- name: Docker build & Push
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot .
docker push ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
CD:
needs: CI
runs-on: ubuntu-latest
steps:
# EC2 접근 후 docker 이미지 pull & run
- name: Deploy to EC2 Server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.AWS_EC2_HOST }}
username: ${{ secrets.AWS_USERNAME }}
key: ${{ secrets.AWS_EC2_KEY }}
port: 22
script: |
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
sudo docker stop $(sudo docker ps -qa)
sudo docker rm $(sudo docker ps -qa)
sudo docker run -d -p 8080:8080 \
-v /etc/localtime:/etc/localtime:ro \
-e TZ=Asia/Seoul \
${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
sudo docker system prune -f
작성한 파일을 하나씩 확인해보자.
# Github Actions에 표시되는 Workflow 이름
name: Dulle-Dulle CI/CD
# master 브랜치로 push, pull request가 발생하면 workflow를 실행
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
job은 Workflow의 실행 단위이다. 각 job은 다시 step으로 순차적인 실행 단계가 구분된다.
이번 Workflow에서는 CI와 CD로 job을 나누어 작성하였다.
CI
CI:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# JDK 세팅
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
# 빌드 시간 향상을 위한 Gradle 캐싱
- name: Gradle Caching
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# application.properties 파일 생성
- name: Make application.properties
run: mkdir ./src/main/resources |
touch ./src/main/resources/application.properties
- name: Deliver application.properties
run: echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.properties
# Gradle 설정 및 Build
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Change gradlew permissions
run: chmod +x ./gradlew
- name: Build with Gradle Wrapper
run: ./gradlew clean build
# Docker 로그인
- name: Docker Login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Docker 이미지 생성 및 Push
- name: Docker build & Push
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot .
docker push ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
CD:
needs: CI
runs-on: ubuntu-latest
steps:
# EC2 접근 후 docker 이미지 pull & run
- name: Deploy to EC2 Server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.AWS_EC2_HOST }}
username: ${{ secrets.AWS_USERNAME }}
key: ${{ secrets.AWS_EC2_KEY }}
port: 22
script: |
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
sudo docker stop $(sudo docker ps -qa)
sudo docker rm $(sudo docker ps -qa)
sudo docker run -d -p 8080:8080 \
-v /etc/localtime:/etc/localtime:ro \
-e TZ=Asia/Seoul \
${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
sudo docker system prune -f
uses : 원격 접속을 위해 appleboy 를 사용한다.
script :
Docker Hub에서 최신 Image를 pull 한 후 실행 중인 컨테이너를 중지, 삭제한다.
타임존을 한국시간대로 설정하여 image를 run하여 컨테이너를 실행시킨다.
마지막으로 불필요한 오래된 image는 삭제한다.
추가(2024.06.10)
Docker Container의 Timezone 설정을 위해 run 명령어에 아래 옵션을 추가해주었다.... -v /etc/localtime:/etc/localtime:ro \ -e TZ=Asia/Seoul \ ...
Workflow 파일을 모두 작성 한 후 프로젝트를 push하여 정상적으로 동작하는지 확인해보았다.


배포까지 문제 없이 동작하였다.
Github Actions과 Docker을 활용한 CI/CD 구축
Docker + Github Actions로 SpringBoot CI/CD 구축하기
[Infra] Github Actions + Docker를 이용한 자동 배포
Docker에 대해 궁금했는데 유익한 포스팅 감사합니다^^