나의 삽질 내역
처음 졸업 프로젝트를 위한 CI/CD를 구축하려고 했을 때 고민을 많이 했었다. 일단 졸업 프로젝트를 위한 API 개발이 활발해지면서 새로운 백엔드 서버를 배포해야 하는 일이 한 손가락을 넘어가며 CI/CD는 반드시 필요한 상황이었다. 그런데 어떤 방식으로 CI/CD를 구축할지가 고민이었는데, 내가 기존에 해본 적 없는 방법을 도전해보고 싶다는 생각이 들었었기 때문이다. 그렇게 많은 삽질을 하고 결론적으로는 GitHub Action으로 회귀하게 된 내역은 다음과 같다.
1. Jenkins 사용
우선 젠킨스는 조금 고민하다가 보류했다. 예전에 인턴할 때 AWS EC2에 젠킨스를 구축했던 경험이 있었는데, 그때 프리티어로 지원되는 t2.micro
의 스팩으로는 젠킨스가 돌아가지 않아서 스팩을 더 높여야 했던 기억이 났기 때문이다. 난 가난하고 학교에서는 AWS 사용 비용을 지원하지 않기 때문에 젠킨스를 사용할 수 없다. 그리고 관리적인 측면에서도 젠킨스로 CI/CD를 구축하면 내가 관리해야 하는 엔드포인트가 백엔드 서버 뿐만 아니라 젠킨스 서버도 생기게되는 셈인데, 이게 과연 적절한가?라는 생각이 들었었다.
2. AWS CodeDeploy
두 번째로 고려했던 방법은 AWS CodeBuild
였다. 나는 Spring Boot 프로젝트를 도커로 빌드해서 레포지토리에 push하고, 이 도커 이미지를 배포한 후 run하는 파이프라인을 만들고 싶었다. 그래서 GitHub Action으로 빌드한 후, Amazon Elastic Container Registry(ECR)
에 push하고, CodeDeploy
로 배포하려고 했었다. 실제로 ECR에 업로드하는 파이프라인까지는 구축했고 CodeDeploy를 만드는데, 알고보니 CodeDeploy는 빌드 파일을 S3에서 가져오는 거고 ECR에서 가져오는 방법은 없었다. 그럼 ECR은 어떻게 배포를 하냐면 ECS
이라는 컨테이너 오케스트레이션 서비스를 사용해 배포한다고 한다.
그러니까 GitHub Action(또는 AWS CodeBuild) → AWS S3 → AWS CodeDeploy
파이프라인을 구축하거나, GitHub Action → AWS ECR → AWS ECS
를 구축해야 한다. 근데 또 여기서 AWS CodeDeploy는 AWS Systems Manager를 사용해 EC2에 미리 CodeDeploy agent를 설치해놔야 한다고 한다. 이쯤 되니 오버 엔지니어링이라는 생각이 계속 들어서 결국 이 방식을 포기했다. 물론 구축하려면 얼마든지 할 수야 있지만, 이런 파이프라인이 나중에 다른 프로젝트를 할 때도 쉽게 재현 가능할까?라는 질문에는 아니라는 생각이 강하게 들었다.
GitHub Action으로 CI/CD
사설이 길었는데, 그래서 결론적으로는 다른 오픈 소스 및 클라우드 서비스 없이 그냥 GitHub Action만 사용해서 Spring Boot 백엔드 CI/CD를 구축했다. 방법은 다음과 같다.
1. Docker Hub에서 퍼블릭 레포지토리 만들기
일단 도커 허브에 가입을 안했다면 가입한다. 그 후 내 백엔드 도커 이미지가 업로드될 퍼블릭 레포지토리를 하나 만든다.
2. GitHub Secret 만들기
먼저 백엔드 개발 서버용 EC2를 하나 만든다(참고로 이 글과 똑같이 구축할 사람이라면 Amazon Linux로 만드는 것 추천). 이 EC2의 public IP
(private IP만 있는 EC2로 만들면 절대 안된다), username
(기본적으로 ec2-user), 그리고 pem key
를 깃허브 시크릿에 등록해야 한다.
깃허브 시크릿은 레포지토리의 Setting → Secrets and variables → Actions
로 이동해서 Repository secrets
를 만든다.
그 후 docker hub에 내가 만든 레포지토리로 push 해야 하므로, docker hub username이랑 password도 시크릿으로 등록해야 한다.
추가적으로 완전 중요한 점은, 아마 다들 중요한 정보를 담고 있는 application.yml
파일은 깃허브에 올라가지 않게 만들었을 것이다(그렇게 하지 않았다면 지금이라도 그렇게 하시길...) 그런데 이 파일이 없으면 당연한 일이지만 GitHub 측에서 파일의 정보를 바탕으로 빌드하는 게 안된다. 그래서 application.yml 파일의 전체 코드를 다 GitHub Secret에 등록해야 한다. 그리고 dev-deploy.yml 파일에서 gradlew build
를 하기 전에 깃허브 시크릿 값으로 application.yml 파일을 생성하는 job을 만들어주면 된다. 그럼 퍼블릭 레포지토리에서도 안전하게 application.yml 파일을 사용해 빌드할 수 있다.
정리하자면, 다음 목록을 Repository secrets를 만들어주면 된다.
DOCKER_PASSWORD #Docker Hub 계정의 비밀번호
DOCKER_USERNAME #Docker Hub 계정의 이메일
HOST_DEV #EC2의 public ip
KEY_DEV #EC2의 pem key
PROPERTIES_DEV #application.yml 파일 내용
USER_DEV #EC2의 계정(기본적으로 ec2-user)
3. GitHub Action YML 파일 만들기
프로젝트 루트에 ./github/workflows
디렉토리를 만들고(반드시 이 디렉토리여야 한다), dev-deploy.yml
을 만들었다. 이 GitHub Action은 개발용 CI/CD이고, 프로덕트용은 따로 만들려고 한다. 그래서 dev 브랜치에 push 될 때마다 작동하도록 만들었다(즉, dev 브랜치 하위에 feat 브랜치들을 만들어서 각 기능을 만들고, 기능 하나가 개발 완료되여 dev로 merge하면 그 순간 dev에 push가 일어나서 CI/CD가 작동하는 구조). 그림으로 보면 이런 구조이다.
파일 내용은 다음과 같다. 각각의 step이 한 단계씩 실행되는 구조인데, JDK 17 설정 → application-dev.yml 파일 생성 → build → Docker Hub에 이미지 push → EC2에 소스 코드 복사 → EC2에서 deploy.sh 실행 → EC2에서 기존 컨테이너 중지하고 새로운 docker 컨테이너 run 흐름으로 이루어진다.
name: Deploy to Dev
## dev에 push 일어날 때마다(즉, 하위 디렉토리에서 merge 될 때마다) 빌드
on:
push:
branches: [ "dev" ]
jobs:
build:
## checkout 후 자바 17 버전으로 설정
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
## gradlew에 권한 부여
- name: Grant execute permission for gradlew
run: chmod +x gradlew
## Copy properties files
- name: Make application-dev.yml
run: |
touch ./src/main/resources/application-dev.yml
echo "$PROPERTIES_DEV" > ./src/main/resources/application-dev.yml
# Make env file
env:
PROPERTIES_DEV: ${{ secrets.PROPERTIES_DEV }}
## gradle build
- name: Build with Gradle
run: ./gradlew clean build
## docker metadata(namespace/repository)
- name: Docker meta
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
with:
images: jeonhaeseung/ness-server-dev
## 멀티-플랫폼 빌드 도구 Buildx 사용
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
## DockerHub에 로그인
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
## 위에서 설정한 테그를 참고해 push
- name: Docker build & push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile.dev
platforms: linux/amd64
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
## 원격에 접속 및 디렉토리 생성
- name: create remote directory
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST_DEV }}
username: ${{ secrets.USER_DEV }}
key: ${{ secrets.KEY_DEV }}
script: mkdir -p ./dev
## 소스 코드 복사 붙여넣기
- name: copy source via ssh key
uses: burnett01/rsync-deployments@4.1
with:
switches: -avzr --delete
remote_path: ./dev
remote_host: ${{ secrets.HOST_DEV }}
remote_user: ${{ secrets.USER_DEV }}
remote_key: ${{ secrets.KEY_DEV }}
## EC2에 배포(CD)
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST_DEV }}
username: ${{ secrets.USER_DEV }}
key: ${{ secrets.KEY_DEV }}
script: |
sh ./dev/config/scripts/deploy.sh
sudo docker stop $(sudo docker ps -a -q)
sudo docker rm $(sudo docker ps -a -q)
sudo docker rmi $(sudo docker images -q)
docker pull jeonhaeseung/ness-server-dev:dev
docker run -d --name backend-server -p 8080:8080 --restart unless-stopped jeonhaeseung/ness-server-dev:dev
4. config.sh 파일 만들기
이건 그냥 부수적인 건데, 나는 EC2에 docker 설치하는 과정까지도 자동화하고 싶었다. 나중에 이 코드를 재활용할 때도 EC2만 만들고 GitHub Secret에 EC2 내용만 적어주면 docker 설치부터 docker hub에서 이미지 pull 및 run까지 되었으면 했다. 그래서 docker가 설치되어 있지 않으면 자동으로 이를 설치하는 shell 스크립트를 작성해서 실행시키기기로 했다.
프로젝트의 아무 경로나 deploy.sh
(파일명은 마음대로)를 만들어주고, 다음과 같은 코드를 쓴다.
#!/bin/bash
# Installing docker engine if not exists
if ! type docker > /dev/null
then
echo "docker does not exist"
echo "Start installing docker"
sudo yum update -y
sudo yum install docker -y
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -a -G docker $(whoami)
fi
위 스크립트는 shell 환경에서 if ! type docker > /dev/null
코드를 통해서 docker가 설치되어 있는지 검사하고, 없으면 docker를 설치한다. 참고로 난 Amazon Linux를 쓰기 때문에 docker 설치하는 명령어가 위와 같다. Amazon Linux의 정말 좋은 점은 docker 설치가 너무 너무 간편하다는 거다. 예전에 Ubuntu 환경에서 docker 설치를 처음 해보다가 많은 삽질을 겪은 사람으로써 정말 신세계이다.
결과
다음과 같이 프로젝트 구조를 만들어주면 된다.
project-root/
│
├─ .github/
│ └─ workflows/
│ └─ dev-deploy.yml <-- 개발용 CI/CD
│ └─ prod-deploy.yml <-- 프로덕션용 CI/CD
│
├─ src/
│ └─ main/
│ └─ java/
│ └─ resources/
│ └─ application.yml <-- 애플리케이션 설정 파일(깃허브에는 올라가지 않음)
│
├─ Dockerfile <-- Spring Boot 애플리케이션을 위한 Dockerfile
└─ ...
dev 브랜치에 push를 했더니 다음과 같이 GitHub Action이 실행되었다. fail되면 X 표시가 뜨고 성공하면 체크 표시가 뜬다.
Docker Hub에 성공적으로 이미지가 올라간 것을 볼 수 있다. postman으로 EC2에 API call을 했을 때도 정상적으로 답변이 왔다.
깃허브 레포지토리
위의 CI/CD가 구현된 졸업 프로젝트의 깃허브 레포지토리는 다음과 같다. 소스 코드로 바로 보고 싶은 사람들은 여기서 보면 된다.
깃허브 레포지토리 링크