bdg.blog는 온프레미스로 쿠버네티스 위에서 동작하는 블로그 서비스입니다. bdg.blog는 ‘AWS Route 53’과 ‘Github’을 제외한 모든 기능을 온프레미스로 배포하고 있습니다. 이번 포스트에서는 쿠버네티스를 활용한 온프레미스 CI/CD 파이프라인의 구축 경험을 소개합니다.
- 개발 동기
- 비용 문제
- 공부 목적
- 디바이스 구성
- 클러스터 구성
- Kubernetes - K3s
- bdg.blog
- Harbor
- Minio
- Argo Events, Workflows, CD
- 형상 관리
- kustomization
- helm chart + kustomization
개발 동기
비용 문제
처음 블로그 서비스를 배포했을 때는 AWS를 사용했습니다. 컨테이너 없이 AWS 위에 서버를 실행해서 배포하는 구조로 간단한 구조였습니다. 하지만 가장 낮은 스팩으로 배포하더라도 데이터베이스 비용, 인스턴스 운영 비용이 발생한다는 단점이 있었습니다.
공부 목적
온프레미스에서 앱, DB, 스토리지 서버, CI/CD 파이프라인을 모두 구축하는 것은 상업적 관점에서 봤을 때 비효율적입니다. 하지만 클라우드에서 해결해주는 여러 문제를 직접 겪어 보면 시스템을 공부하는 데 큰 도움이 될 것으로 생각해 이 프로젝트를 시작했습니다.
디바이스 구성
2개의 노트북을 묶어 하나의 쿠버네티스 클러스터를 구성했습니다. 네트워크 스위치는 가정용 공유기를 사용하고 있으며, Public IP의 80 포트, 443 포트가 이 클러스터로 연결됩니다.
- 2개의 노트북으로 구성
- ubuntu 22.04
- k3s v1.25.7
비용 측면
위의 구성으로 24시간동안 항상 서버를 실행하고 있습니다. 혼자 사는데 필요한 생활 전력(냉장고, 에어컨, 각종 전자기기, 데스크탑 컴퓨터)을 포함하여 전기세를 한달에 2 ~ 3만원 정도 지출하고 있습니다. 생활 전기 비용이 포함되어 있기 때문에 명확한 비교는 힘들지만, 크게 부담되는 비용은 아닙니다. 만약 이 서비스를 모두 AWS에서 운영했을 때와 비교해도 비용면에서 충분히 효율적이라고 판단했습니다.
클러스터 구성
하나의 쿠버네티스 클러스터에 아래 앱들이 고유의 namespace를 가지고 운영되고 있습니다. 형상을 정의하는 yaml파일들은 github에 저장되며, 아래 링크에서 확인하실 수 있습니다.
- https://github.com/deagwon97/bdg-blog-v2/tree/main/deploy
Kubernetes - K3s
구축하려는 클러스터는 고성능 서버가 아닌 노트북 위에 설치해야 합니다. 실제 K8s와 호환되면서도 Edge Device에서 잘 동작하는 K3s로 클러스터를 구성하기로 했습니다. K3s는 containerd(컨테이너 런타임), flannel (CNI), traefik(Ingress Controller)를 기본 설정으로 사용합니다. 다른 옵션을 구성할 수도 있지만, 불필요하다고 판단하고 기본 설정을 그대로 사용했습니다.
bdg.blog
NextJs로 개발한 블로그 서비스입니다. 링크에 자세한 설명이 포함되어 있습니다.
Harbor
초기에는 AWS의 ECR를 사용했습니다. 비용 문제로인해서 Harbor로 이전하는 것을 선택했습니다.
Minio
Image File을 저장하기 위한 Object Storage 입니다. bdg.blog에서는 minio에 이미지 다운로드 및 업로드를 위한 presigned url을 발급하여 클라이언트로 전달합니다. 클라이언트는 발급받은 URL로 minio 서버에 파일을 바로 업로드합니다.
Argo Events, Workflows, CD
CI/CD 구축을 위해서 사용한 도구들 입니다. 처음에는 Jenkins를 사용했지만, 잦은 plugin 업데이트 문제, 네이티브로 쿠버네티스 컨테이너를 지원하지 않는 문제 등을 이유로 Argo로 이전했습니다. 링크(KubernetesArgoCD-Argo-WorkflowsEvents-도입기)에 자세히 정리해 두었습니다.
- 마주했던 문제점
단순히 공식문서에 나온 방법으로 CI 파이프라인을 구축하면, namespcae별로 구분되는 argo ci를 구성할 수 없었습니다. namespace 속에 argoci를 구축한 후, 다른 namespace에 독립적인 argoci를 설치하려고 했었습니다. 분명히 namespace가 분리되어 있음에도 불구하고, 두 argoci는 동시에 동작하지 못 했습니다. - 원인
argo events와 argo workflows의 설치 스크립트를 살펴보면 CustomResourceDefinition 을 생성하며 ClusterRole 을 정의합니다. 그리고 이 둘은 하나의 Cluster Scope에서 동작합니다. 만약 B 프로젝트를 위해서 ClusterRole 수정하면, 기존의 A 프로젝트는 정상적으로 동작하지 못하게 되는 것입니다. - 해결책
이 문제를 해결하기 위해 Cluster Scope에서 동작하는 CustomResourceDefinition 과 ClusterRole 는 따로 설치하고, namespace 범위에서 동작하는 자원들만 분리했더니 Argo Events, Argo CD 를 프로젝트단위로 설치할 수 있었습니다.
형상 관리
kustomization
프로젝트의 모든 앱은 kustomization 으로 관리하고 있습니다. 아래와 같이 자원 명세를 작성하고 kubectl apply -k
명령을 통해서 배포합니다. 또한 이 명세서는 모두 git과 argo cd를 통해서 관리합니다. argo cd는 git에 올라간 형상과 실제 클러스터 형상을 비교하고 차이점을 알려 줍니다. (자동으로 형상을 동기화할 수 있지만, 현재는 형상이 잘 유지되고 있는지 확인하는 용도로만 사용합니다.)
------- production 폴더 구조 -------
# 프로비저닝 순서에 따라서 yaml 파일에 순서를 부여했습니다.
production
|-- 00namespace.yaml
|-- 01dockerconfig.json# gitignore
|-- 01dockerconfig.json.example
|-- 01nextjs.env # gitignore
|-- 01nextjs.env.example
|-- 03serviceAccount.yaml
|-- 04deployment.yaml
|-- 05service.yaml
|-- 11issuer.yaml
|-- 12certificate.yaml
|-- 13middleWare.yaml
|-- 14ingressRouter.yaml
`-- kustomization.yaml
------- kustomization.yaml -------
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: bdg-blog
metadata:
name: arbitrary
## generate secret only first time
# secretGenerator:
# - name: bdg-blog
# envs:
# - 01nextjs.env
# - name: bdg-blog-regcred
# files:
# - .dockerconfigjson=01dockerconfig.json
# type: kubernetes.io/dockerconfigjson
resources:
- 00namespace.yaml
- 03serviceAccount.yaml
- 04deployment.yaml
- 05service.yaml
- 11issuer.yaml
- 12certificate.yaml
- 13middleWare.yaml
- 14ingressRouter.yaml
Helm Chart
쿠버네티스 패키지를 관리하는 방법은 kustomize 말고도 다양합니다. 일부 오픈 소스는 Helm Chart를 통해서 패키지를 배포하기도 합니다. 이 프로젝트에서 사용된 컨테이너 레지스트리 Harbor 또한 Helm Chart를 사용합니다.
하지만 Helm은 일부자원을 추가하는데 제약이 있습니다. 정의된 values.yaml 파일을 수정할 수만 있습니다. 만약 PersistentVoulme을 생성하거나, ingress, issuer와 같은 자원을 추가로 만들고 관리하기 위해서는 새로운 helm chart를 패키징해야 합니다.
저는 배포되는 Harbor의 Helm Chart를 그대로 사용하면서도, 원하는 자원들을 추가하기 위해 kustomize와 helm을 결합해서 사용했습니다.
이렇게 구성하면, 나중에 Harbor의 Helm Chart가 업데이트되더라도, 다시 Chart를 처음부터 작성하지 않고 해당 Chart의 버전만 업데이트할 수 있습니다. 또한, 형상이 모두 코드로 표현되기 때문에, ArgoCD를 통한 Continuous Deployment가 가능합니다.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: bdg-blog-harbor
metadata:
name: bdg-blog-harbor
generatorOptions:
disableNameSuffixHash: true
resources:
- 00namespace.yaml
- 01harbor-pv.yaml
- 02harbor-pvc.yaml
- 03issuer.yaml
- 04certificate.yaml
- 05ingress.yaml
helmCharts:
- includeCRDs: true
name: harbor
namespace: bdg-blog-harbor
releaseName: harbor
repo: https://helm.goharbor.io
valuesFile: 06harbor-helm-values.yaml
version: 1.11.1
주의할 점은 argocd config에서 enable-helm
을 설정해야 helmChart를 argocd에서 사용할 수 있습니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
data:
kustomize.buildOptions: --enable-helm
...
클러스터 구조 및 의존 관계
쿠버네티스 클러스터에는 다양한 서비스들이 동작하며, 네임스페이스를 통해 구분됩니다. 앱의 소스 코드 및 배포를 위한 명세 파일은 하나의 GitHub 레포지토리에서 관리됩니다.
- MariaDB, Redis, Minio, NextJS 로 구성된 블로그
- CI/CD tools: kaniko, harbor, argo workflows, argo events, argo cd
Continuous Integration - 검은색 화살표
소스코드를 git에 push하면서 발생하는 일련의 자동 배포 과정입니다. git은 argo events에 webhook을 보내고, argo events는 정해진 workflow를 실행하면서 kaniko로 컨테이너 이미지를 빌드합니다. 이후 빌드가 완료되면 이미지를 harbor repository에 push합니다. 마지막으로 kubectl restart rollout deployment bdg-blog -n bdg-blog
명령어를 실행하면서 기존의 컨테이너들을 교체하면서 소스코드가 서비스에 적용됩니다.
Continuous Delivery - 주황색 화살표
ArgoCD를 활용한 Continuous Delivery 과정입니다. bdg.blog의 클러스터 명세는 secret 및 config 설정을 제외한 모든 코드가 kustomization.yaml 파일에 작성되어 있고, 이 파일은 동일한 git repository에 저장됩니다. 클러스터 명세를 수정하여 git에 push하면, ArgoCD는 이 명세의 설정과 클러스터의 상태를 비교하여 변경사항을 반영하도록 구성했습니다.
Network Flow - 파란색 화살표
Ingress 컨트롤러인 traefik이 외부에서 들어오는 요청을 중계하는 과정을 보여줍니다. bdg.blog 외에도 harbor, minio, argo 모두 관리용 web ui를 제공합니다. 클라이언트가 요청한 도메인에 따라서 적절한 앱으로 요청이 라우팅 됩니다.
Reference
- https://k3s.io/
- https://goharbor.io/
- https://min.io/
- https://argoproj.github.io/
- https://kustomize.io/
- https://helm.sh/
- https://trstringer.com/helm-kustomize/