먼저, 로컬에서 도커 파일을 통해 도커 허브에 이미지를 푸시하고, 해당 이미지를 EC2 환경에서 컨테이너로 실행시키는 방법을 알아본 후,
Github Actions와 Docker를 통해 EC2에 자동으로 배포되도록 CI/CD를 구축하는 과정을 살펴보겠습니다.
CI/CD가 궁금하다면 EC2 배포는 건너뛰고 CI/CD 부분만 보아도 괜찮지만, 도커에 익숙하지 않다면 처음부터 해보는 것이 이해하는 데 도움이 될 것이라고 생각합니다.
EC2에 배포
도커 배포 플로우

1. Docker File 작성
스프링 부트 프로젝트 안에 Dockerfile을 작성해줍니다.
# open jdk 17 버전의 환경을 구성
FROM openjdk:17
# 빌드 시 전달할 수 있는 인수를 정의
# build가 되는 시점에 JAR_FILE이라는 변수 명에 build/libs/*.jar 선언
ARG JAR_FILE=build/libs/*.jar
# JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar
# 컨테이너가 시작될 때 실행할 명령
ENTRYPOINT ["java", "-jar", "/app.jar"]
※ 주의

plain.jar 파일이 함께 생기지 않도록 build.gradle에 enabled = false 설정을 해줍니다.
jar 파일의 경로를 설정할 때, *.jar로 해놓았기 때문에 우리가 사용할 한 개만 있어야 하기 때문입니다.
2. gradle build
빌드하여 .jar 파일을 생성합니다.
./gradlew build -x test
/build/libs에 .jar 파일이 생성되었습니다.

3. DockerHub Repository 생성
Docker Hub에 들어가 Repository를 생성해줍니다.
visibility는 pubilc으로 설정합니다.
4. Docker Image Build
태그를 지정해서 도커 빌드를 실행합니다.
docker build -t (도커 허브 ID)/(Repository 이름) .
로컬에서 진행 시 위와 같은 에러가 발생하여, 저는 도커 데스크탑을 실행시키고 빌드했습니다.
ERROR: error during connect: this error may indicate that the docker daemon is not running: Get "http://%2F%2F.%2Fpipe%2Fdocker_engine/_ping": open //./pipe/docker_engine: The system cannot find the file specified.
5. DockerHub Push
docker push (도커 허브 ID)/(Repository 이름)
도커 허브로 푸시를 진행하고자 했으나, 아래와 같은 에러가 발생하였습니다.

로그인 명령어를 통해 로그인을 한 후 다시 명령어를 입력해줍니다.
docker login
성공적으로 푸시되었습니다.


중간 점검

현재, 왼쪽 부분까지 완료된 상태입니다.
이어서 도커 허브에 있는 이미지를 EC2에서 실행시키도록 하겠습니다.
EC2 세팅
우분투 환경에서는 아래 명령어들을 사용합니다.
# 패키지 업데이트
sudo apt-get update -y
# 기존에 있던 도커 삭제
# sudo apt-get remove docker docker-engine docker.io -y
# 도커 설치
sudo apt-get install docker.io -y
# docker 서비스 실행
sudo service docker start
# /var/run/docker.sock 파일의 권한을 666으로 변경하여 그룹 내 다른 사용자도 접근 가능하게 변경
sudo chmod 666 /var/run/docker.sock
# ubuntu 유저를 docker 그룹에 추가
sudo usermod -a -G docker ubuntu
기존에 설치되어 있던 도커가 있다면 삭제하고 다시 설치해주어도 좋습니다.
리눅스 환경에서는 아래 명령어들을 사용합니다.
# 패키지 업데이트
sudo yum update -y
# docker 설치
sudo yum install docker -y
# docker 서비스 실행
sudo service docker start
# ec2-user를 docker 그룹에 추가
sudo usermod -a -G docker ec2-user
이 글에서는 우분투 환경을 사용합니다.
sudo usermod -a -G docker ubuntu
위 명령어를 실행시키면, ubuntu 사용자로 docker 명령어를 입력할 때마다 sudo를 붙이지 않아도 됩니다.
6. Docker Image Pull & Docker Run
sudo docker pull (도커 허브 ID)/(Repository 이름)
sudo docker run -p 8080:8080 (도커 허브 ID)/(Repository 이름)
도커 허브에서 이미지를 pull 받고, 실행시켜줍니다.
Github Actions + Docker CI/CD
이번에는 깃허브 액션과 도커를 사용하여 CI/CD를 구축하는 과정을 살펴보겠습니다.
1. EC2 JDK 설치
푸시한 버전이 제대로 적용되었는지 확인하려면 EC2에서 직접 코드를 열어볼 수 있어야 합니다.
우리가 사용하는 도커는 .jar 파일을 컨테이너로 실행시킨 것이기에 JDK가 필요 없지만, 결국 .jar 파일의 압축을 풀고 소스코드를 들여다보기 위해서는 JDK를 설치해야 합니다.
sudo apt update
# jdk 17버전 설치
sudo apt-get install openjdk-17-jdk
# java 버전 확인
java -version
2. deploy.yml 파일
name: Java CI with Gradle
on:
push:
branches: [ "main" ]
permissions:
contents: read
jobs:
# Spring Boot 애플리케이션을 빌드하여 도커 허브에 푸시하는 과정
build-docker-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# 1. Java 17 세팅
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# 2. gradle caching - 빌드 시간 향상
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# 3. create application-database.yml
- name: create application.yml file
run: |
mkdir ./src/main/resources/database
touch ./src/main/resources/database/application-database.yml
echo "${{ secrets.DATABASE_YML }}" >> src/main/resources/database/application-database.yml
# 4. 빌드 권한 부여
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash
# 5. Spring Boot 애플리케이션 빌드
- name: Build with Gradle
uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee
with:
arguments: clean build -x test
# 6. Docker 이미지 빌드
- name: docker image build
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }} .
# 7. DockerHub 로그인
- name: docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# 8. Docker Hub 이미지 푸시
- name: docker Hub push
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}
run-docker-image-on-ec2:
needs: build-docker-image
runs-on: ubuntu-latest
steps:
# deploy
- name: Deploy-EC2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }} # EC2 퍼블릭 IPv4 DNS
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }}
script: |
sudo rm -rf app
sudo mkdir app
sudo docker stop ${{ secrets.PROJECT_NAME }} || true
sudo docker rm ${{ secrets.PROJECT_NAME }} || true
sudo docker rmi ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}
sudo docker run --name ${{ secrets.PROJECT_NAME }} -d -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}
sudo docker cp ${{ secrets.PROJECT_NAME }}:app.jar app/app.jar
cd app
# 압축 풀기
sudo jar xvf app.jar
sudo docker image prune -f
★ Gradle 캐싱
CI/CD 진행 과정 중 gradle build이 시간이 가장 오래 걸립니다.
어떻게 하면 더욱 빠르게 빌드를 할 수 있을까 고민하던 중, gradle 캐싱을 적용하기로 했습니다.
# gradle caching - 빌드 시간 향상
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
Gradle 캐싱은 GitHub Actions 워크플로우에서 빌드 시간을 단축하기 위한 설정입니다.
- path: 캐시할 파일이나 디렉토리의 경로를 지정합니다. 이는 캐시의 저장과 복원에 사용되는 runner 내 파일 경로입니다.
- ~/.gradle/caches: Gradle 캐시 디렉토리입니다. Gradle이 다운로드한 종속성과 빌드 아티팩트가 저장됩니다.
- ~/.gradle/wrapper: Gradle wrapper 디렉토리입니다. Gradle wrapper 스크립트 및 Gradle 배포 파일이 포함됩니다.
- key : 캐시의 고유 키를 정의합니다. 이는 캐시를 저장하고, 식별, 사용 및 복원하는 데 사용되는 키입니다.
- restore-keys : 캐시를 찾을 때 사용할 키의 접두사를 정의합니다. 캐시가 key로 지정한 키와 정확히 일치하지 않더라도 이 접두사를 사용하여 유사한 키를 가진 캐시를 복원하려고 시도합니다.
- ${{ runner.os }}-gradle-: 운영 체제와 Gradle 관련 캐시의 접두사입니다. 예를 들어, ubuntu-latest-gradle-와 같이 구성될 수 있습니다.
Gradle 캐싱이 빌드 시간을 단축시켜주는 이유
1. 종속성 다운로드 시간 단축
- Gradle 프로젝트는 빌드할 때 종속성을 외부 저장소에서 다운로드합니다. Gradle 캐시는 이 종속성들을 로컬에 저장하여, 다음 빌드 시 동일한 종속성을 다시 다운로드할 필요가 없도록 합니다.
2. 빌드 아티팩트 캐싱
- Gradle은 빌드 과정에서 생성된 아티팩트 (예: 컴파일된 클래스 파일, JAR 파일 등)를 캐시합니다. 동일한 소스 코드에 대해 다시 빌드할 때, Gradle은 이전 빌드에서 생성된 아티팩트를 재사용할 수 있습니다.
3. Gradle Wrapper 다운로드 시간 단축
- Gradle Wrapper는 Gradle 빌드 도구의 특정 버전을 포함합니다. Gradle 캐시를 사용하면, Gradle Wrapper가 이미 다운로드되어 로컬에 저장된 상태이므로, 빌드 시마다 Gradle 배포 파일을 다시 다운로드할 필요가 없습니다.
3. Github Secrets 변수 설정

- DB 설정 외에도 S3, JWT 등 다양한 설정 파일을 추가할 수 있습니다.
- 도커 허브에 로그인하기 위한 USERNAME과 PASSWORD, REPOSITORY 명을 넣어 주었습니다.
- EC2에 연결하기 위해 HOST 주소와 SSH 연결을 위한 KEY를 넣어 주었습니다.
- PROJECT NAME 또한 변수로 설정하였습니다.
★★★
도커 명령어
# 도커 컨테이너 내부 파일 확인
sudo docker exec -it <container_id_or_name> /bin/bash
# 도커 로그 출력
sudo docker logs <container_id_or_name>
# 실시간 로그 출력
sudo docker logs -f <container_id_or_name>
# 컨테이너 내부 app.jar 파일을 ec2의 app/app.jar로 복사
sudo docker cp <container_id_or_name>:app.jar app/app.jar
# 현재 Docker 데몬에서 실행 중이거나 중지된 모든 컨테이너의 ID를 출력
docker ps -qa
sudo docker stop $(docker ps -qa) && docker rm $(docker ps -qa)
# 지정된 프로젝트 이름의 컨테이너를 삭제
# 만약 컨테이너가 존재하지 않아서 오류가 발생하더라도, 스크립트가 계속 실행되도록 한다
sudo docker rm ${{ secrets.PROJECT_NAME }} || true
# 도커 허브에서 가져온 특정 이미지를 삭제
# 특정 이미지를 직접 삭제하지만, 만약 그 이미지가 다른 컨테이너에서 사용 중이면 삭제되지 않는다
sudo docker rmi ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}
# 더 이상 사용되지 않는 모든 이미지를 정리
sudo docker image prune -f
참고)
https://chb2005.tistory.com/191