쿠버네티스에 cicd 파이프라인을 구축했다. 상당히 많이 헤맸다. 헤맸던 이유는 머리속에 cicd 가 이뤄지는 구조를 분명히 그리지 못해서 그런거 같다. cici 의 대상에 대해서 분명히 설정하고, 어떤 도구들이 어떤 대상을 바라보고 있는지 확실하게 인지하고 있었으면 조금 더 명쾌하게 해결이 되었을 것 같다.
사실 그렇게 복잡한 구조도 아닌데, 퇴근후에 지친 상태로 프로젝트를 진행하면 빨리 끝내고 쉬고 싶은 생각이 들고 머리도 잘 안돌아가서 바보 같이 대충대충 빨리 하게된다... 지금에서야 배포 파이프라인의 구조가 명확하게 머리속에서 그려지기 때문에 문제가 없는데 어쩐지 다른 공부를 계속하다보면 휘발될지도 모르겠다는 생각에 부랴부랴 구축 과정과 파이프라인 구조를 정리 해둔다.
1. Kubernetes 에서 CICD 대상
1) 쿠버네티스 메니페스토
첫번째로, 쿠버네티스의 메니페스토들을 추적관리 해야한다. 생각보다 쿠버네티스 클러스터에는 많은 변경점들이 생겼다. 새로운 차트를 추가할 수도 있고, 프록시의 라우팅 규칙을 변경할 일도 있다. (아마 개발단계라서...) 운영 단계에서는 Deployment 의 레플리카를 늘리고 줄이기, 보안 설정 변경 등의 업데이트가 진행 될 수 있을 것이다.
그런데 이런 변경사항이 생길 때마다 kubectl 명령어를 쳐서 업데이트를 하는 것은 많이 귀찮은 일 일 뿐더러, 실수할 여지도 있다. 헬름 차트를 쓴다면 upgrade 명령어로 쉽게 변경지점을 반영할 수 있을지 모르겠다. 그러나 여전히 오케스트레이션 관리라는 측면에서 모든 리소스를 cli 로 지켜 본다는 것은 많이 피곤한 일 것이다.
이러한 이유로 쿠버네티스 상의 어플리케이션을 쉽게 관리해주고 배포도 자동화 해주는 도구들이 있다. 나같은 경우는 회사에서 다뤄본 경험이 있어서 다른 선택지를 고민하지 않고 Argocd 를 선택했다. (Flux 라는 도구도 있다) Argocd 는 쿠버네티스 메니페스토가 있는 Git 리포지토리를 보고있다가, 해당 리포지토리내에서 메니페스토(.yml) 에 변경이 생기면 그걸 Sync 하는 방식으로 지속적인 통합 및 배포를 해준다. 또한 배포되어있는 쿠버네티스의 다양한 오브젝트를 시각화해서 보여주고, 직접 매니페스토를 변경 적용하거나, 컨테이너 내부 로그를 보여주는 등의 기능도 제공한다.
이를 위해서는 Argocd 에 추적관리 대상이 되는 리포지토리와 메니페스토의 경로를 설정해줘야한다. Jenkins 와 마찬가지로 private 깃허브를 추적관리 하기 위해서는 Credential 설정을 해줘야하고, 해당 리포지토리에서 어떤 리소스를 관리할지도 정해줘야한다. 또, 변경지점이 생겼을 때는 바로 동기화를 해줄 건지 수동으로 동기화를 해줄 것인지도 설정해줘야하고... 등등 신경쓸 것이 좀 있다.
몇번인가 클러스터를 날려먹기도 하고, 뭔가 마음에 안들어 삭제했다가 다시 깔기도 했는데, 이런건 코드로 관리가 안되나 했었는데, Argocd 에 위 방식으로 등록을 하니까 Argocd 네임스페이스에 Application 이라는 커스텀 리소스가 생성되는걸 확인했다. 그렇다면 역으로 해당 커스텀 리소스를 정의해서 배포하면 저걸 일일이 등록하지 않아도 등록 된다는 뜻.
나는 comppi-dev / comppi-prod / comppi-gateway 세가지 어플리케이션을 정의해두어서 번거롭게 위의 과정을 거치지 않고 배포를 할 수 있게 하였다. 그냥 단일한 yml 메니페스토로 정의해도 되는데, 혹시 다른 패키징을 할 수도 있지 않을까 싶어서 헬름차트로 등록했다.
저렇게 argo 에서 쓰일 여러개의 어플리케이션들을 따로 헬름차트로 정의해두고 Jenkins 로 자동화 배포를 하고 있다. 후술하겠지만 Jenkins 파이프라인 스크립트들도 전부 코드로 관리하고 있다. (사진에서 보이는 Jenkinsfile 들이 그 코드)
어플리케이션 종류 및 배포/삭제 옵션을 선택하고 빌드를 누르면 자동적으로 Argocd 에 어플리케이션이 등록이 되면서 쿠버네티스 리소스들이 생성이 되고 Argocd 가 추적관리를 시작한다.
2) 컨테이너 이미지
사실 메니페스토보다 훨씬 더 자주 수정되는 것이 어플리케이션 코드일 것이다. 만약 수정된 어플리케이션 코드가 깃허브 푸쉬되고, 다른 cicd 툴을 통해 어플리케이션 컨테이너가 도커 리포지토리에 추가되었다고 해보자. 그러면 Argocd 가 추가된 컨테이너 이미지를 감지해서 배포된 디플로이먼트에 파드를 새로운 컨테이너로 변경해줄까?
아쉽지만 Argocd 는 메니페스토가 담긴 깃허브만을 바라보고 있기 때문에 컨테이너가 바뀌든 어쩌든 이건 Argocd 의 관심사가 아니다. Argocd 는 메니페스토, 즉 쿠버네티스 리소스만 관리한다. 때문에 다른 방법을 써야한다.
직관적으로 떠오르는 방법은 메니페스토의 컨테이너 테그 부분을 새롭게 생성된 컨테이너의 태그로 수정하여, Argocd 가 동기화 하도록 할 수 있다. 그런데 이건 자동화 배포라고 하기엔 부족한 부분이 있다. 컨테이너 이미지를 추적관리하여 지속적인 통합/배포를 수행 할 수 없는 걸까?
여기서 필요한게 Argocd Image updater 다. (https://argocd-image-updater.readthedocs.io/en/stable/) Argocd Image Updater 는 Argocd 와는 다르게 도커 이미지 리포지토리를 바라보고 있다. 도커 이미지 리포지토리를 바라보고 있다가, 컨테이너 이미지에 수정된 사항이 생기면 메니페스토를 직접 수정해서 컨테이너에 적용을 해준다.
Argocd Image Updater 가 컨테이너 이미지를 추적 관리 하기위해서는 직접 Argocd 어플리케이션 CRD 에 어노테이션을 달아서 어떤 이미지를 추적해야하는지, 또 어떤 규칙대로 업데이트를 해야하는지를 설정해줘야한다. 컨테이너 이미지가 변경이 생겨도 사용자가 정해준 규칙에 벗어난 이미지 태그를 달고 있다면 이미지 업데이트가 일어나지 않는다.
comppi-dev 의 Application.yaml 에서 이미지 업데이트 규칙을 설정하는 부분
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: comppi-dev
annotations:
argocd-image-updater.argoproj.io/image-list: >-
discovery-eureka=utopiandrmer/comppi-discovery-eureka,
api-gateway=utopiandrmer/comppi-api-gateway,
auth-service=utopiandrmer/comppi-auth-service,
board-service=utopiandrmer/comppi-board-service,
my-plant=utopiandrmer/comppi-my-plant,
encyclopedia=utopiandrmer/comppi-encyclo-service,
scraper=utopiandrmer/comppi-scraper,
mysql=utopiandrmer/comppi-mysql-dev,
nginx=utopiandrmer/comppi-nginx
argocd-image-updater.argoproj.io/discovery-eureka.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/discovery-eureka.update-strategy: newest-build
argocd-image-updater.argoproj.io/discovery-eureka.helm.image-name: discovery-eureka.image.name
argocd-image-updater.argoproj.io/discovery-eureka.helm.image-tag: discovery-eureka.image.tag
argocd-image-updater.argoproj.io/api-gateway.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/api-gateway.update-strategy: newest-build
argocd-image-updater.argoproj.io/api-gateway.helm.image-name: api-gateway.image.name
argocd-image-updater.argoproj.io/api-gateway.helm.image-tag: api-gateway.image.tag
argocd-image-updater.argoproj.io/auth-service.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/auth-service.update-strategy: newest-build
argocd-image-updater.argoproj.io/auth-service.helm.image-name: auth-service.image.name
argocd-image-updater.argoproj.io/auth-service.helm.image-tag: auth-service.image.tag
argocd-image-updater.argoproj.io/board-service.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/board-service.update-strategy: newest-build
argocd-image-updater.argoproj.io/board-service.helm.image-name: board-service.image.name
argocd-image-updater.argoproj.io/board-service.helm.image-tag: board-service.image.tag
argocd-image-updater.argoproj.io/my-plant.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/my-plant.update-strategy: newest-build
argocd-image-updater.argoproj.io/my-plant.helm.image-name: my-plant.image.name
argocd-image-updater.argoproj.io/my-plant.helm.image-tag: my-plant.image.tag
argocd-image-updater.argoproj.io/encyclopedia.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/encyclopedia.update-strategy: newest-build
argocd-image-updater.argoproj.io/encyclopedia.helm.image-name: encyclopedia.image.name
argocd-image-updater.argoproj.io/encyclopedia.helm.image-tag: encyclopedia.image.tag
argocd-image-updater.argoproj.io/scraper.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/scraper.update-strategy: newest-build
argocd-image-updater.argoproj.io/scraper.helm.image-name: scraper.image.name
argocd-image-updater.argoproj.io/scraper.helm.image-tag: scraper.image.tag
argocd-image-updater.argoproj.io/mysql.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/mysql.update-strategy: newest-build
argocd-image-updater.argoproj.io/mysql.helm.image-name: mysql.mysql-dev.image.name
argocd-image-updater.argoproj.io/mysql.helm.image-tag: mysql.mysql-dev.image.tag
argocd-image-updater.argoproj.io/nginx.allow-tags: regexp:^dev
argocd-image-updater.argoproj.io/nginx.update-strategy: newest-build
argocd-image-updater.argoproj.io/nginx.helm.image-name: nginx.image.name
argocd-image-updater.argoproj.io/nginx.helm.image-tag: nginx.image.tag
위와 같이 어플리케이션 CRD 에 어노테이션을 달아주는 방식으로 설정할 수 있다. 위의 경우엔 정규식 ^dev 를 만족하는 태그(dev로 시작하는) 중 가장 최근 생성된(newest-build) 태그로 업데이트를 한다. 이외에도 다양한 업데이트 전략이 있다.
공식 문서 (https://argocd-image-updater.readthedocs.io/en/stable/)
나는 총 9개의 컨테이너 이미지를 추적하도록 설정하였고, 각각의 이미지마다 업데이트 규칙을 설정해야하니 어노테이션이 꽤 복잡하고 길게 나타난다. 내 생각에는 새로운 리소스를 통해서 정의될 수 있도록 해야할 것 같다. 아마 정식버전 (v1) 이 출시되면 분명 별도의 CRD 가 나오지 않을까 기대해본다.
prod 환경에는 이러한 자동 업데이트 방식은 위험할 것 같아서 dev 에만 적용해두었다. prod 는 blue-green 방식으로 업데이트를 진행하게 될 것 같은데 그땐 어떤식으로 배포를 할지 고민해봐야겠다.
2. Jenkins
쿠버네티스의 컨테이너 이미지나 메니페스토의 통합/배포는 최종적으로 Argocd 를 통해 이루어지기 때문에 Argocd 만 쓰고 끝내려고 했는데, Jenkins 의 역할도 아주 중요하기 간략하게나마 설명을 남겨두지 않을 수가 없다.
Jenkins 은 기본적으로 정의된 pipelin script 를 통해 해야할 작업을 순차적으로 수행한다.
그런데 script 를 직접 입력하지 않고 SCM (Source Code Management) 를 통해 script 를 받아오는 방식으로도 설정할 수 있는데, 이를 통해 Jenkins 의 스크립트를 유지, 관리할 수 있다.
나는
- Argocd 를 클러스터에 배포하는 스크립트 --> application-deployment
- Argocd 에 어플리케이션을 배포하는 스크립트 --> argocd-installation
- 어플리케이션 컨테이너 이미지를 빌드하는 스크립트 --> application-image-build
- 그외 DB 나 다른 모듈을 빌드하는 스크립트 --> container-build
를 각각 작성하여 Jenkins pipeline 을 관리하였다. 특히 어플리케이션 이미지 빌드의 경우 도커허브 리포지토리에 푸쉬를 수행하고 Argocd 의 이미지 업데이트를 통해 배포까지 한번에 수행하게 된다.
3. CICD 흐름도
최종적으로 위와같은 흐름대로 CICD 가 진행된다. 이렇게 되면 개발자에게 남은 것은 소스코드를 SCM(Github) 에 push 하는 일과 Jenkins 에서 어플리케이션 컨테이너 이미지를 빌드하는 일이다. 사실 Jenkins 에서 Github 리포지토리에 웹훅을 걸어서 Build 과정도 자동화가 가능한데, 이부분은 현재 리포지토리 공개설정이 Private 여서 돈을 내야하기도 하고 과한 자동화같아서 일부러 수동으로 남겨두었다. (현재 백엔드 팀원 간 코드 리뷰는 못하고 있어서 그냥 냅다 푸쉬 박아버리는 상황이라...)
4. 추후 숙제
상기 CICD 는 dev 환경에서 적용되는 사항이다. prod 환경에서는 신중하게 배포를 진행해야 하기 때문에 자동적으로 배포를 진행할 수는 없을 것 같다. prod 에서는 rolling / blue-green / canary 같이 실질적으로 배포가 이뤄지는 방식이 훨씬 중요해진다. Argo cd 에서도 이러한 배포방식을 지원하기 위해서 rollouts 라는 도구도 있는 걸로 알고 있다.
지금은 쿠버네티스에서 rediness probe / liveness probe 만 적용되어있는 날 것 그 자체인데, 쿠버네티스의 다양한 리소스를 통해서 배포 방식에 대해서 고민하고 개선할 필요가 있다.