KB IT's Your Life의 마지막 프로젝트를 어제까지 마감하여 끝을 냈다.
프로젝트를 진행하며 CICD 를 플로우를 작성해서 배포를 하였다.
CICD를 구축한다면 나중에 마감일에 수정 삭제를 마감일까지 하게 되는데
빌드해서 .war 파일만들어서 배포환경의 tomcat에 .war파일 넣고~ 너무 불편하다...
이전에 다른 프로젝트( React + nodeJS 등등 ) 할때에도 CICD 구축의 편리성을 느꼈기 때문에
이번 프로젝트에도 구축하려고 하였다.
먼저 환경은
톰캣 ( Tomcat 9.0.94 ) |
![]() Spring Framework ( spring boot 아님!! ) |
![]() Java version 17 |
에서 개발을 진행 하였습니다.
CICD 프레임 워크로는
Github Action 을 사용하였습니다.
무료에 Github 에서 바로 설정 할 수 있어 편리합니다.
Deploy 환경은
AWS EC2 - ubuntu 환경에서 진행하였습니다.
먼저 제가 생각한 WorkFlow는
- 백엔드 코드를 작성하고 Github의 기능 branch에 Psuh
- Github 의 배포 브랜치로 생각한 mater 브랜치에 배포가 된다면 Github Action에서 이를 감지하고 수행
- github action에서 spring 프로젝트를 빌드, tomcat에 넣어 docker 이미지로 빌드
- Docker 이미지를 Docker Hub 에 push
- ec2에서 image를 pull받아 이미지를 받는다.
- 기존의 컨테이너에서 새로 받은 최신 프로젝트 이미지를 run 하여 container를 수행한다.
라는 플로우를 생각하여 작성하였습니다.
최종 CICD.yml
name: Spring Framework CI/CD with Gradle
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle
- name: application.properties 파일 생성
run: |
echo "application.properties 파일 생성"
echo "src/main/resources 폴더 생성"
mkdir -p src/main/resources
echo "${{ secrets.APPLICATION_PROPERTIES }}" > src/main/resources/application.properties
- name: Build with Gradle
run: |
echo "gradle 빌드 시작"
chmod +x ./gradlew
./gradlew build
echo "gradle 빌드 완료"
- name: 도커 로그인
run: |
echo "Docker 로그인 중..."
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: 도커 이미지 빌드
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} .
docker tag ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
- name: Docker Hub 에 Push
run: |
docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }}
docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
- name: DEPLOY
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_PUBLIC_IP }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
port: ${{ secrets.EC2_SSH_PORT }}
script: |
echo "도커 이미지 가져 오는중..."
docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
echo "실행중인 컨테이너 확인"
if [ "$(docker ps -q -f name=${{ secrets.DOCKER_CONTAINER_NAME }})" ]; then
echo "실행중이던 컨테이너 중지"
docker stop ${{ secrets.DOCKER_CONTAINER_NAME }}
echo "실행중이던 컨테이너 삭제"
docker rm ${{ secrets.DOCKER_CONTAINER_NAME }}
else
echo "실행중인 컨테이너 없음"
fi
echo "pull과정에서 생긴 none 태그 images 삭제"
docker rmi $(docker images -f "dangling=true" -q)
echo "새로운 컨테이너 실행"
docker run -d --name ${{ secrets.DOCKER_CONTAINER_NAME }} -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
최종 Dockerfile
# Tomcat 이미지를 기반으로 설정
FROM tomcat:9.0.94-jdk17-temurin
# 컨테이너 내부에서 작업 디렉토리 설정
WORKDIR /usr/local/tomcat/webapps
# 빌드된 WAR 파일을 컨테이너에 복사
COPY build/libs/kb_i_dle_backend-1.0-SNAPSHOT.war /usr/local/tomcat/webapps/ROOT.war
# 애플리케이션 포트 노출
EXPOSE 8080
# Tomcat 실행 명령
CMD ["catalina.sh", "run"]
먼저
name: Spring Framework CI/CD with Gradle
on:
push:
branches: [ master ]
- name은 이 workflow의 이름이다.
- 생각하는 이름을 작성
- on 은 어떤 이벤트때 이 스크립트를 실행 할지 정하면 되는데 mater branch에 merge되어 push되었을때를 하고 싶기 때문에 push 시 라고 작성 했다.
- 여러 이벤트가 존재하는데 https://docs.github.com/ko/actions/writing-workflows/workflow-syntax-for-github-actions#on 에서 배열 형태( [ ] ) filter ( /** ) 형태로 작성 할 수 있다는 것을 알았다.
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle
이제 steps는 단계 별로 나누어 작업들을 설정 할 수 있다.
- - name 으로 어떤 작업인지 작성해두고 작성해야 나중에 실행 화면에서 보기 편하기 때문에 -name을 작성하고 작성하는게 좋다.
- 그리고 use를 이용하여 Action 에서 사용할 수 있는 여러 Open Source, Tool 들을 사용해서 아래의 작업들을 수행한다.
- Checkout은 " 깃허브의 코드 저장소에 올려둔 코드를 CI 서버로 내려받은 후에 특정 브랜치로 전환하는 행위 " 라는데 쉽게 말하면 github code 들을 cloud 환경에 가져 간다고 생각하면된다. 따라서 action사용할때 첫 번째로, 필수이다.
- Setup JDK 17은 JDK 17 버전에서 개발했기 때문에 같은 환경을 위해 Action의 JDK 도 17로 맞추어 주었다.
- Cache Gradle 은 이제 가져간 코드를 gradle을 이용하여 build해야하는데 Build과정은 오래걸리는 작업 중 하나이다.
- 따라서 Cache를 이용하면 좀 더 빨리 Build할 수 있기에 사용하였다.
- name: application.properties 파일 생성
run: |
echo "application.properties 파일 생성"
echo "src/main/resources 폴더 생성"
mkdir -p src/main/resources
echo "${{ secrets.APPLICATION_PROPERTIES }}" > src/main/resources/application.properties
- name: Build with Gradle
run: |
echo "gradle 빌드 시작"
chmod +x ./gradlew
./gradlew build
echo "gradle 빌드 완료"
그런데 프로젝트의 환경 변수인 application.properties를 github에 올리지 않았었다!!!
따라서 action에서 빌드를 할려는데 이 파일이 없어 아마 안될 것이다.
그래서 action에서 제공하는 github의 secret을 이용해서 거기에 APPLICATION_PROPERTIES 라는 키값으로 저장해 둔뒤 이 파일을 src/main/resouces/application.properties라는 파일로 생성 해주었다.( 프로젝트와 같은 폴더 )
그뒤 gradle로 빌드를 하였 .war을 생성하였다.
Action
- name: 도커 로그인
run: |
echo "Docker 로그인 중..."
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: 도커 이미지 빌드
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} .
docker tag ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
- name: Docker Hub 에 Push
run: |
docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }}
docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
Dockerfile
# Tomcat 이미지를 기반으로 설정
FROM tomcat:9.0.94-jdk17-temurin
# 컨테이너 내부에서 작업 디렉토리 설정
WORKDIR /usr/local/tomcat/webapps
# 빌드된 WAR 파일을 컨테이너에 복사
COPY build/libs/kb_i_dle_backend-1.0-SNAPSHOT.war /usr/local/tomcat/webapps/ROOT.war
# 애플리케이션 포트 노출
EXPOSE 8080
# Tomcat 실행 명령
CMD ["catalina.sh", "run"]
- 그뒤 이제 Gradle로 빌드를 한 .war 파일을 가지고 Dockerfile을 수행한다!
- 먼저 Action의 컴퓨터에서 docker 이미지를 빌드 하면 Docker Hub에 올릴때 로그인이 되어 있어야 하기에 로그인을 해준다.
- 그리고 Dockerfile을 이용해 이미지를 빌드한다.
- Dockerfile은 우리가 Tomcat 9.0.94 버전을 사용했기에 DockerHub 에서 9.0.94 버전을 찾았고 jdk 17 버전이 여러개 있었는데 그중 아까 JDK 깔때 와 같은 temurin 버전을 사용했다.
- 그뒤 빌드한 war 파일을 Tomcat 이미지의 소스 안에 넣어준다.
- 이때 ROOT.war로 하는게 좋다!!! ROOT.war 파일로 해야 배포시 주소가 " 원격주소/ " 로 된다.
- 만약 ROOT 가 아니라 backend.war로 하면 " 원격주소/backend " 가 기본 주소가 된다.
- 그뒤 tomcat의 기본 port인 8080을 설정해주고
- tomcat 실행 명령어를 통해 컨테이너를 실행 시켜 줄 것이다.
- 이때 이미지를 두개 만드는데 ${{ github.sha }} 는 이 action을 사용할때 생성되는 랜덤 코드라 생각하면 된다.
- 코드로 만들어진 이미지와 latest 버전 이미지를 만드는데
- 이렇게 만들면 dockerhub에서 코드를 통해 이미지 버전 관리
- latest 를 통해 ec2 에서 이미지를 pull 받아 컨테이너를 실행할때 이미지 관리가 용이 할 것이다.
- 처음에는 그냥 github.sha 버전으로 실행했는데 ec2 에 이미지가 계속 쌓이는 문제를 해결하기 위해 이렇게 작성했다.
- 이렇게 만든 도커 이미지를 Docker hub에 Push한다.
- name: DEPLOY
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_PUBLIC_IP }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
port: ${{ secrets.EC2_SSH_PORT }}
script: |
echo "도커 이미지 가져 오는중..."
docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
echo "실행중인 컨테이너 확인"
if [ "$(docker ps -q -f name=${{ secrets.DOCKER_CONTAINER_NAME }})" ]; then
echo "실행중이던 컨테이너 중지"
docker stop ${{ secrets.DOCKER_CONTAINER_NAME }}
echo "실행중이던 컨테이너 삭제"
docker rm ${{ secrets.DOCKER_CONTAINER_NAME }}
else
echo "실행중인 컨테이너 없음"
fi
echo "pull과정에서 생긴 none 태그 images 삭제"
docker rmi $(docker images -f "dangling=true" -q)
echo "새로운 컨테이너 실행"
docker run -d --name ${{ secrets.DOCKER_CONTAINER_NAME }} -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
그리고 appleboy라는 오픈 소스를 이용해 ec2 에 접속한다.
action에서 run으로 ssh 접속 명령어를 작성해 접속해도 된다는데 나는 그렇게 하니 자꾸 오류나서
appleboy를 사용하니 바로 해결 되었다.
이렇게 ssh 로 ec2 에 접속해 ec2에서 명령어를 실행해 주었다.
- 이미지를 가져오는데 이때 미리 ec2에 docker hub 로그인을 해두어야한다.
- 그리고 이미지를 run하면 되는데 처음에는 괜찮지만 CICD를 계속하면 이미 컨테이너가 실행중이다.
- 그래서 실행중인지 확인하고 각 상황에 맞게 컨테이너를 삭제해준다.
- 또한 이미지를 pull하는 과정에서 none 태그 이미지가 생기게 되는데 이를 관리하기 위해
- none태그 이미지들을 삭제하여 ec2 용량을 관리하였다.
- 그뒤 컨테이너를 실행하였다.
아주 잘 된다!!!!