[무신사 스토어 watcher] CI/CD 도입기
이번 프로젝트에 CI/CD를 도입하면서 많은 고민과 시행착오를 겪었다. 왜 처음 결정한 구조에서 변경을 했는지, 어떻게 변경을 했는지 정리해보았다.
CI/CD를 도입한 이유
CI는 사전적인 의미로 지속적인 통합이다. 바꿔말하면 개발자가 코드를 작성하고 합치고 빌드하고 테스트하는 과정을 일컫는다. 이 과정이 수동으로 진행되면 코드를 작성하고 합칠 때마다 빌드와 테스트를 수동으로 진행해야 한다. 이런 귀찮은 과정을 github에 push 하기만 해도 전부 이뤄지게 자동화할 수 있다.
CD는 사전적 의미로 지속적인 배포이다. CI 과정 뒤에는 CD 과정이 필요하다. 배포 프로세스도 수동으로 작업할 필요 없이 CI와 연동하여 자동화시키면 서버를 배포하는 과정을 수동으로 하지 않아도 된다.
내가 CI/CD를 도입한 이유도 이와 같다. 수동으로 작업해야 하는부분을 최대한 자동화를 시켜 시간을 단축한다. 이는 관리해야 하는 서버가 확장되면 훨씬 더 큰 차이를 보일 것이다.
첫 번째, CI/CD 구조
CI도구로 github action을 선택한 이유
CI 도구를 선택할 때 3가지의 후보가 있었다. Travis CI, Github Action, Jenkins중 github action을 선택하게 되었는데 그 이유는 다음과 같다.
1. Jenkins
Jenkins는 굉장히 유명하고 오래되고 자료도 많은 CI 도구로 알고 있다. 하지만 설치형으로서 내가 가진 서버 자원에 Jenkins를 설치하고 사용했어야 했다. 아무래도 개인으로 진행하던 프로젝트라 여러 개의 서버를 운영하는데 과금의 부담이 있었다. 만약, 운영 중인 서버에 jenkins를 설치해서 사용한다면 빌드시마다 서버 자원을 사용하게 될 것이고 모니터링할 때 노이즈는 물론, 서버 성능도 문제가 될 수 있다고 판단했다. 그런 이유로 Jenkins는 후보에서 제외했다.
2. Travis CI
Travis CI는 이전에 이동욱님이 집필하신 "스프링 부트와 AWS로 혼자 구현하는 웹서비스 구현" 도서에서 처음 CI/CD 개념과 함께 사용해보게 되었고 이전 프로젝트에서 사용해본 적이 있다. Travis를 사용하면서 단점이라고 느꼈던 것은 속도다. 어떨 때는 push시 빌드가 바로 진행되다가도 몇 시간 뒤에 빌드가 실행되는 적도 많았다.
3. Github Action
Github Action은 일단 github repository에서 관리할 수 있다는 점이 매력적이었다. 또한, travis ci에 비해 빌드 속도가 빨랐다. 다른 장점으로 github action은 cron 표현식을 사용해서 작업을 스케쥴링할 수 있었다. 이 것을 활용하는 부분이 있었는데, 크롤링을 수행하는 .py 파일을 작업으로 등록시켜주면 주기적으로 크롤링 작업을 수행할 수 있었다. 이 작업은 웹 상으로 빌드가 이뤄져서 내가 별도의 크롤링 서버를 운영할 필요도 없었다. travis ci에서도 주기적인 작업을 할 수 있지만 daily, weekly, monthly를 기준만 설정할 수 있어 cron 표현식이 지원되는 github action보다 제약이 있었다.
CI/CD 과정
- github에 push 한다.
- github action에서는 빌드 및 테스트를 진행한다.
- 빌드 결과인 Jar파일을 AWS S3에 업로드한다.
- AWS CodeDeploy의 배포 그룹에 등록된 EC2로 Jar파일을 업로드한다.
- EC2에서 미리 작성한 배포 스크립트가 실행된다.
이 구조를 바꾸게된 이유
구조를 바꾸게 된 이유는 과금 문제로 인해 서버 확장에 제약이 있다는 점이 있었다. 나는 AWS의 ec2 프리티어를 사용 중이었다. 여기서 AWS ec2를 더 확장하면 과금이 발생했다. 그래서 다른 클라우드 사에서 제공하는 인스턴스를 같이 써야하는 상황이 됐고 AWS와 NCP를 동시에 사용하게 되었다. 하지만 NCP의 서버에는 기존의 배포 방식인 AWS CodeDeploy를 통해 배포할 수 없었고 새로운 배포 방식을 고민해야 했다.
두 번째, CI/CD 구조
CI/CD 과정
- github에 push 한다.
- github action에서는 빌드 및 테스트를 진행한다.
- 빌드 결과를 미리 작성한 Dockerfile을 사용해서 Image를 만들고 Docker hub로 push 한다.
- 각 서버에 scp를 사용하여 배포 스크립트를 전송한다.
- 각 서버에 ssh를 사용하여 docker를 사용하는 배포 스크립트를 실행한다.
프론트엔드 배포 과정
- github에 push 한다.
- github action에서는 빌드 및 테스트를 진행한다.
- scp를 사용해 빌드 결과인 dist폴더를 로드밸런서 서버로 전달한다.
이전 방식 대비 개선된 부분
1. 이전 배포 방식은 클라우드에서 제공하는 서비스를 사용했기 때문에 AWS에 종속 되었지만 docker + ssh조합으로 AWS, NCP 서버에 배포할 수 있었다.
2. 인스턴스를 생성하면 java를 비롯하여 실행하기 위한 환경을 설치해줘야 했었는데 docker를 사용하여 환경 설정 문제가 간단히 해결되었다.
서버를 확장 시나리오
새로 생성한 인스턴스에 대하여 아래의 과정을 통해 배포 설정을 했다.
- nginx설치
- docker, docker-compose 설치
- docker login
- sudo visudo (배포 스크립트를 sudo 권한으로 실행하기 위해)
- /etc/nginx/conf.d/service-url.inc 파일 생성(배포 스크립트 실행시 port 변경을 위해)
- 서버에 배포용 application properties파일 만들기
- git secretkey에 서버정보 추가
- github action deploy파일에 서버정보 추가
- 로드 밸런서 서버 nginx파일의 upstream에 아이피를 추가한다.
이 구조를 바꾸게된 이유
몇 가지 문제가 있었다.
- 증설 절차를 단순화했지만 여전히 설치하고 손으로 입력할게 많다.
- 서버를 추가/변경/삭제한다고 했을 때 github action의 workflow파일을 수정하지 않고서는 배포할 수가 없다. 기존에 release branch에 올라가 있던 배포 파일을 수정해야하는 문제가 있다.
- 서버가 늘어날 때마다 서버 정보를 secret key로 등록해야한다. 수정, 변경 시에도 매번 서버정보를 관리해야한다.
- 서버의 로그를 확인하거나 죽은 컨테이너를 다시 실행해야 할 때 일일이 서버에 접속해서 확인해줘야 한다.
세 번째, CI/CD 구조
CI/CD 과정
- github에 push 한다.
- github action에서는 빌드 및 테스트를 진행한다.
- 빌드 결과를 미리 작성한 Dockerfile을 사용해서 Image를 만들고 Docker hub로 push 한다.
- ssh를 사용해 원격 서버에 접속하고 kubectl을 사용하여 kubernetes의 deployment를 업데이트한다.
이전 방식 대비 개선된 부분
- 리소스 사용률에 따라 클라우드에서 노드/pod을 자동으로 스케일링할 수 있다.
- 서버가 확장/변경/축소될 때 기존의 배포파일을 수정할 필요가 없다.
- 서버가 확장/변경/축소될 때 이전에는 application.properties등 설정 파일을 서버마다 새로 만들어 줘야 했다. 이 부분은 클러스터의 configmap과 secret의 설정을 통해 반복하지 않고 재사용이 가능하다.
- 서버에 접속하지 않고도 컨테이너를 관리할 수 있다. liveness Probe, Readness Probe등으로 pod의 health check도 가능하다.
서버를 확장 시나리오
노드를 추가하는 상황에서 NCP는 웹 콘솔에서 수동으로 조절할 수도 있고 node pool을 사용해서 오토 스케일링도 지원했다. 인스턴스를 늘리는 것 이외에 pod의 수를 조절하는 것은 kubetctl 커맨드를 사용해서 pod의 레플리카 수를 조절할 수 있고 kubernetes에서 제공하는 hpa기능을 사용하면 리소스 사용에 따라 pod을 알아서 조절할 수 있다.
나는 kubernetes에서 제공하는 hpa기능과 클라우드에서 cluster autoscaler를 사용하여 트래픽에따라 자동으로 서버와 pod을 스케일링하도록 했다.
hpa를 사용하면 pod의 개수를 리소스 사용량에 따라 자동으로 조절할 수 있다. 그러기 위해서는 pod에 리소스 요청이 필요하다. 이게 없으면 hpa는 작동되지 않는다. 아래 스크립트를 사용하면 평균 cpu 80%이상일 경우 pod가 늘어나고 pod의 수는 최소1개에서 최대 10개까지 늘어난다.
kubectl --kubeconfig=$KUBE_CONFIG autoscale deployment watcher-api-dp --cpu-percent=80 --min=1 --max=10
kubectl --kubeconfig=$KUBE_CONFIG get hpa
1. 기존 노드 수가 2개로 할당되어있다.
2. 부하테스트를 통해 cpu사용률을 높였고 그에따라 pod개수가 자동으로 스케일링 된다.
3. pod의 스케쥴링 상태를 확인하면 리소스 부족으로 pending상태로 남아있다.
4. 요청 리소스가 서버 자원보다 많아짐에 따라 클라우드에서 노드를 자동으로 스케일링한다.
5. 새롭게 노드가 추가됨
6. pending 상태인 pod은 노드를 할당받아 running상태로 변했다.
7. 부하테스트를 종료하고 hpa의 상태변화를 지켜보자.
8. cpu사용률이 감소함에 따라 pod의 개수가 4 -> 1개로 감소하였다.
9. 시간이 지남에 따라 node도 사용률이 저조한 노드에 대하여 스케쥴링이 중단되고 노드가 감소한다.
앞에서 시도한 서버 확장 방식과 비교해보면 트래픽에 따라 자동으로 서버가 증설되고 새로 증설하는 서버에 대해서도 수작업 없이 대응하고있다.
마치며
이렇게 여러 변경 과정을 거쳐 Github Action, Kubernetes를 사용한 CI/CD를 구축했다. 이 과정에서 어떤 이유로 불편함이 느껴지고 어떻게 더 편하게 배포하고 관리할 수 있을까를 고민하면서 많이 배운 것 같다. docker와 k8s를 둘 다 처음 사용해서 배포해봤지만 편리함이 많이 느껴졌다. nginx도 만져보면서 전반적인 배포프로세스를 배운 것 같아 좋았다.
앞으로 더 공부해가면서 부족한 지식과 새롭게 직면하게 될 문제들에 대해 고민해보고 싶다.