솝트에서 이번에 간단한 협업 과제가 주어졌다.
백엔드는 6-8개 정도 엔드포인트를 가진 API를 작성해야 했다.
그래서 이번 과제를 통해 API 작성보다는
CI/CD를 찍먹하는 데 집중해봤다.
깃헙 액션 선택 이전에 레퍼런스를 찾으며
두 가지 CI 툴을 먼저 고려했었다.
가장 먼저 젠킨스.
많이 사용하기도(레퍼런스가 많기도) 하고 오픈소스라는 점도 매력적이었다.
하지만 CI 설정이 다소 세부적이라는 점과
젠킨스를 위한 별도의 서버가 필요하다는 점에서 제외되었다.
다음으로 트래비스 CI.
역시 많이 사용하는 툴이다.
젠킨스와 달리 서버가 별도로 필요하지 않으며
설정에 있어 선택지가 적어 편리하다고 한다.
그러나 오픈소스로 운영되던 서비스가 전면 유료화되면서
내가 사용할 일은 없어졌다...
1. 별도의 서버가 필요하지 않다.
2. 비용이 발생하지 않는다.
라는 니즈와 충분한 레퍼런스가 있을 만큼의 유명도를 가진 CI 툴은
Github Actions라는 결론이 섰다.
사실 이 부분은 크게 이유가 있지 않았다.
현재 내가 도커 인터넷 강의를 듣고 있어서 이번 기회를 통해 검증해보고 싶었다.
애플리케이션을 빌드하고, 관리하는 데에 도커가 가장 편리해보였다.
Dockerfile
을 통해 타겟 서버에 자동 빌드 및
버전 관리가 가능했다.
그리고 EC2 서버에서 각종 도커 명령어를 통해
애플리케이션 상태를 점검하기에도 용이해 보였다.
프로젝트 디렉토리 내에 Dockerfile
을 작성해야 한다.
이 역시도 애플리케이션 환경에 따라 상이하므로 참고만 하자!
# open jdk 11 버전의 환경을 구성
FROM openjdk:17
# 노출할 포트를 설정
EXPOSE 8081
# build가 되는 시점에 JAR_FILE이라는 변수 명에 build/libs/*.jar 선언
# build/libs - gradle로 빌드했을 때 jar 파일이 생성되는 경로
ARG JAR_FILE=./planfit/build/libs/*.jar
# JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar
# 운영 및 개발에서 사용되는 환경 설정을 분리
ENTRYPOINT ["nohup", "java", "-jar", "/app.jar", "&"]
참고로, 첫번째 버전에서는
멀티 스테이징을 통해 의존성 라이브러리를 캐싱하는 로직이 추가되어 있었다.
그러나 도커파일이 제대로 적용되지 않는 탓에
타겟 서버에 복붙해 특정 포트에서 실행하기
로 축소되었다.
EC2에 ssh로 접속하여
도커를 설치해야 한다.
터미널 명령어에 관한 것은 레퍼런스를 남겨두겠다.
https://velog.io/@jbro321/Docker-Ubuntu-22.04.3에-docker-설치
EC2에 올린 OS(아마 많은 경우 우분투)의 버전에 따라
명령어가 상이할 수 있으므로 확인하려 참고하자.
깃허브 리포지토리의 {루트}/.github/workflows
디렉토리에
github-actions.yml
파일을 생성해준다.
그리고 이 yml 파일 안에 배포와 관련한 내용을 작성해야 하는데,
이 부분은 서비스와 배포 환경에 따라 상이하니 다른 레퍼런스도 참고하길 바란다.
하단에는 내 github-actions.yml
에 대한 설명이기도 하면서,
내 서비스 및 배포 환경에 대한 설명이다.
# github repository actions 페이지에 나타날 배포의 식별자
name: CI/CD using github actions & docker
# event trigger
# main 브랜치에 push가 되었을 때 실행
on:
push:
branches: [ "main" ]
permissions:
contents: read
jobs:
CI-CD:
runs-on: ubuntu-latest
steps:
# JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 맞춰주는 것이 좋다)
# 다만, actions/checkout과 actions/setup-java의 버전에 따라 경고가 발생할 수 있음
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# 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-
# yml 파일 생성 - application.yml
- name: make application.yml
if: |
contains(github.ref, 'main')
run: |
mkdir ./planfit/src/main/resources # resources 폴더 생성
cd ./planfit/src/main/resources # resources 폴더로 이동
touch ./application.yml # application.yml 생성
echo "${{ secrets.YML }}" > ./application.yml # github actions에서 설정한 값을 application.yml 파일에 쓰기
shell: bash
# gradle build
# --project-dir 옵션으로 빌드에 필요한 파일들이 어디 있는지 명시
- name: Build with Gradle
run: |
./planfit/gradlew build --project-dir ./planfit -x test
# docker login
- name: Docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# docker build & push to production
# 당연하겠지만, 내 깃허브 프로젝트 안에 Dockerfile이 있어야 함
- name: Docker build & push to prod
if: contains(github.ref, 'main')
run: |
pwd
docker build -f ./planfit/Dockerfile -t ${{ secrets.DOCKERHUB_USERNAME }}/planfit .
docker push ${{ secrets.DOCKERHUB_USERNAME }}/planfit
# deploy to prod -> 원격 서버에 배포
# 현재 원격 서버에 이미 돌아가고 있는 이전 버전의 컨테이너를 중지 및 삭제
# 도커 허브에서 새로운 버전 이미지를 pull
# docker run을 통해 새 이미지의 컨테이너 실행
# 이전 버전의 이미지 삭제
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
if: contains(github.ref, 'main')
with:
host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
username: ubuntu
key: ${{ secrets.PEM_KEY }} # EC2 생성과 함께 발급한 pem 키
envs: GITHUB_SHA
script: |
sudo docker ps -q --filter ancestor=${{ secrets.DOCKERHUB_USERNAME }}/planfit | xargs -r sudo docker stop | xargs -r sudo docker rm
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/planfit
sudo docker run -d --name planfit_container -p 8081:8081 ${{ secrets.DOCKERHUB_USERNAME }}/planfit
sudo docker image prune -f
위 파일에서 자주 볼 수 있는 ${{ ~~~ }}
에 대해서는
하단 항목에서 설명하겠다!
하단에 위 파일을 작성하는 데
큰 도움을 주었던 출처를 인용하겠다!
https://velog.io/@leeeeeyeon/Github-Actions과-Docker을-활용한-CICD-구축
처음에는 위 출처에서 파일을 그대로 베껴와 바로 실행해보았고,
그렇게 30번을 더 배포를 시도했다...
그만큼 많은 시행착오가 있었고,
개중에는 백발백중 내 실수로 인한 것뿐이었다.
yml
파일을 보면 수많은 ./planfit
이 보일 것이다.
기본적인 작업 디렉토리가 리포지토리 루트 디렉토리이기 때문에
내 스프링부트 프로젝트 안의 파일을 참조하기 위해서는 ./planfit
으로 한 단계 들어가야 한다.
만약 해당 파일을 찾을 수 없다는 에러 로그를 찾을 경우에는
pwd
를 통해 현재 어느 경로에서 작업중인지를 꼭 확인하라.
레퍼런스를 그대로 따르다가는 사소한 실수를 맞이하게 된다.
내 레퍼런스에서는 dev
과 prod
두 개 환경에서 모두 배포하고 있었으며,
각각을 위한 application.yml
, Dockerfile
등이 있었다.
yml
파일 내 run
에 늘여진
명령어를 유심히 확인하여 쓸데없는 CI 실패를 방지하자!
이 케이스는 아직 나도 정확한 원인을 알지 못한다...
yml
파일 내 수많은 secrets 값 중에서
도커와 관련한 것들이 처음에 트러블을 일으켰다.
***/planfit
으로 된 이미지를 생성할 수 없다?는 식의 에러 로그였는데
처음에는 secrets 값을 잘못 참조해서 ***
라는 더미 값이 들어간 줄 알았다.
그러나 그러한 이유 때문은 아니었고,
secrets 값을 삭제하고 다른 이름으로 변경하여 다시 참조하니 해결되었다.
(이왜진?)
참고로, 나의 경우에는 ${{ DOCKER_USERNAME }}
이 참조되지 않아서
${{ DOCKERHUB_USERNAME }}
으로 변경했다.
(GITHUB
으로 시작하는 secrets 값을 생성하지 말라는 주의사항을 스쳤었는데,
나의 경우에도 예약어 문제가 아닐까?하고 회고해본다)
가장 어이가 없던 케이스인데,
나 같은 사람이 있을 것 같아서 꼭 알려야겠다.
EC2 서버에 ssh로 접속하기 위해 pem 키를 발급해 활용하는데,
github-actions.yml
에서도 내 EC2 인스턴스에 애플리케이션을 배포하기 위해 동일한 과정을 거친다.
이에 대한 로직은 name: Deploy to prod
항목에서 찾을 수 있으며,
이 때 with/key
에서 pem 키를 참조해야 한다.
그냥 GLI에서 더블 클릭해서는 값을 알 수 없고,
터미널 환경에서 vim
으로 조회해 복붙해야 한다.
위 사진은 pem 키 조회시 보이는 키의 prefix다.
나는 당연히 이런 쓸모없는 값은 빼고 올려야지라고 생각하고 제외 후 복붙했지만,
계속해서 ssh 접속이 거부당한다는 에러를 만났고,
긴 시간 고군분투 끝에 저 prefix를 포함하여 pem 키 값을 등록해야 한다는 것을 깨달았다.
내 리포지토리의 settings
메뉴로 이동하여
Security/Secrets and variables/Actions/Repository secrets
순서대로 쭉 이동해보자.
여기서 New repository secret
버튼을 클릭해
yml에서 참조할 비밀스런 값들의 이름과 값을 등록할 수 있다.
나의 경우엔 도커 허브에 등록된 이름과 비밀번호,
EC2 IP 주소와 ssh pem 키,
application.yml 전문을 secrets 값으로 등록했다.
(참고로, 소문자로 등록하더라도 결국은 모두 대문자로 저장되며
yml에서 참조할 때에도 대문자로 해야한다.)
github-actions.yml
을 등록하고 나면 내가 변경을 감지하기로 한 브랜치에
새 커밋이 푸쉬될 때마다 자동으로 배포가 진행된다.
그리고 진행되는 배포에 대한 로그 확인은 리포지토리의 Actions 메뉴에서 확인할 수 있다.
클릭을 통해 들어가보면 yml
내 name
항목 각각에 대한 진행 상황을 꽤 상세히 알려준다.
만약 배포가 실패했을 경우 들어가서 로그를 까보면
정확한 원인을 확인할 수 있으므로 참고하자!
CI/CD를 처음 건드려보면서 느낀 것은,
스프링 코드보다도 더 민감해서
아주 작은 실수를 용납하지 않을 뿐더러
곳곳이 추상화되어 있기 때문에
때로는 이게 왜 되지? 싶은 모먼트도 많았다.
그리고 여기에 다 쓰지 못한 (혹은 기억을 못하는)
트러블 슈팅이 정말 많아서 힘들었다...
그러나 한 번 세팅해두면 어차피 거쳐야 할 '배포'라는 과정을
너무나도 간편하게 자동화할 수 있다는 점은 커다란 장점으로 다가오는 것 같다.
끝으로, 솝트 친구가 이번 과제 때 이런 질문을 던진 적이 있다.
형은 왜 도커를 써?
순간 답을 할 수가 없었는데,
내 경험이 부족한 탓이겠다...
데브옵스 공부에 좀 더 매진하여
애플리케이션을 더더욱 비즈니스의 관점에서 개선시킬 수 있는 사람이 되어야지!