원클릭 homelab 구축 2 - 구조

Byeonghoon Yoo·2022년 8월 4일
1

homelab 구축

목록 보기
2/3
post-thumbnail

이 시리즈는 개인적으로 구축한 Kubenetes cluster의 구축 과정과 대략적인 구조를 소개하고, 어떻게 원클릭으로 클러스터를 구축했는지 간단하게 설명할 예정입니다. 구현 디테일은 레포지토리에 점진적으로 문서화할 예정이며, 이 시리즈에서는 멀리서 봤을 때의 구조와 그렇게 구현된 이유, 제가 고민했던 포인트들을 설명할 예정입니다.

이 포스트에서는 원클릭 구축을 설명하기 전에, 현재 클러스터가 어떠한 구조로 구성되어있는지 설명합니다. 추가로 1편에서 정의한 목적이 각 구성에 어떻게 녹여져있는지 설명할 예정입니다.

  • 1편: 목적과 사연
  • 2편: 구조
  • 3편: 원클릭/돌아보기

구조 오버뷰

우선 구조를 간단하게 살펴본 이후에 1편에서 정의한 목적을 어떻게 이루었는지 설명한다. 처음부터 어떤 서비스를 배포하는게 목적이 아니라 클러스터의 원클릭 구축이 가능하도록 자동화 하는것이 목적이라서 배포되어있는 워크로드가 많진 않다.

Kubernetes cluster

총 2개의 cluster를 운영중이다. 사진처럼 각각 backbone과 prod로 이름지었다. 사실 backbone cluster는 homelab이라는 이름에 걸맞게 실제로 집에 노드가 설치되어있지만, prod는 집에 있지 않고 클라우드를 사용중이다. 두 cluster 모두 k3s를 사용했다.

backbone cluster

총 2개의 물리 노드를 각각 master와 worker로 구성했다. HA (High Availability)로 구성하지 않았기 때문에 master가 죽으면 클러스터 전체를 사용할 수 없게 되지만, 그만큼 가용성이 크게 중요하지 않은 워크로드만을 목적으로한다. worker의 경우 master와 다르게 NAS 역할도 맡고있고 (host OS에 직접 설치), 이후에 설명할 예정이다.
모든 트래픽은 Wireguard를 VPN으로 사용해서 보안을 유지하며, public에 노출되는 유일한 포인트는 하나의 노드가 Wireguard 트래픽을 받기위한 UDP 포트이다.
대표적으로 아래의 워크로드가 포함되어있다.

  • ArgoCD
  • VPN 관리 웹
  • (예정) Data backup service
  • (예정) Image registry
  • (예정) Prometheus
  • (예정) prod에서 사용할 DB의 replica

prod cluster

총 3개의 물리 노드를 모두 master로 HA를 구성했다. public으로 노출되어있기 때문에 VPN 없이도 접근이 필요하거나 가용성이 중요한 워크로드를 올리고있다. 대표적으로 아래의 워크로들 포함한다.

  • Python 코드 포매팅 서버 - blackd
  • 광고 차단 DNS proxy - blocky
  • 각종 static web page
  • 노드의 남는 공간을 활용한 분산 파일시스템 - rook (ceph)

Cloudflare

총 2개의 도메인을 가지고있고, 클러스터에 배포한 서비스들을 도메인으로 연결하기위해 Cloudflare DNS table에 서비스를 등록하고있다. 당연히 DNS record 관리 또한 자동화 되어있고, 각 클러스터에 External-DNS가 설치되어있어서 클러스터의 워크로드에 변경이 있으면 자동으로 Cloudflare에 반영된다.
prod cluster에 public으로 올라간 서비스뿐만 아니라 backbone cluster의 VPN으로 감춰진 서비스 또한 DNS record로 올라가있다. 대표적으로 ArgoCD의 웹 페이지가 argocd.bhyoo.com으로 등록되어있는데, 해당 record의 IP가 private ip로 등록되어있기 때문에 VPN 내부일 때 접근을 편하게 만들 뿐 외부 접근은 여전히 막는다.

Cloudflare 대시보드에 DNS record가 등록된 모습

배포

클러스터 정보 (.kube/config) 없이도 각 클러스터에 배포할 수 있으려면 근본적으로 배포 단계에서 클러스터 의존성을 없애야한다. 즉, 클러스터에 직접 배포하지 않아야 한다. 이걸 구현하기 가장 좋은게 GitOps 패턴이라고 생각한다. 간단하게 설명하면 git에 원하는 클러스터의 상태를 적어놓으면, 클러스터가 git을 보고있다가 배포자가 원하는 상태에 맞게 자신의 상태를 바꾸는것이다. 이렇게하면 admin -> cluster 의존성이 admin -> git <- cluster로 바뀌면서 연결고리가 끊어진다. 때문에 isac322/homelab 자체가 내가 원하는 클러스터의 상태를 의미한다. 덕분에 조금만 손봐주면 이 자체가 초기화 스크립트의 역할을 할 수 있다. 클러스터가 있고 ArgoCD만 잘 설치해주면 비어있든 잘못 설정 되어있든 git에 적어놓은 나의 요구대로 클러스터가 상태를 스스로 바꾼다.

OCI Secret Store

원클릭 클러스터 구축과 GitOps에 방해되는 가장 골치아픈 녀석은 Secret이다.
배포할 워크로드 중 일부는 API key같은 secret을 필요로하기 때문에 내가 원하는 상태를 의미하는 git에 포함되어있어야한다. 하지만 github에 public하게 secret을 적어서 올려놓는 짓은 하면 안된다. 그럼 git에 안전하게 올리려고 sealed-secrets를 쓴다면? sealed-secrets은 복호화를위해 클러스터 내부의 etcd에 private key를 저장해 놓는다. 이게 뭐가 문제일까? 닭 달걀 문제에서 etcd의 private key를 닭으로, 암호화된 sealed secret을 달걀로 볼 수 있다. private key가 클러스터에 없으면 암호화된 sealed secret을 읽을 수 없기 때문이다. 예를 들어, 빈 클러스터에 ArgoCD를 설치하면 git에있는 암호화된 sealed secret을 클러스터 내부 ectd의 private key로 복호화 시도할것이다. 이 때 당연히 빈 클러스터의 etcd에는 git에 올라가있는 sealed-secret을 암호화했던 private key가 없다. 결국 순환 의존성이 생기면서 클러스터 재구축이 불가능해진다.
결국 secret을 상태이기 때문에 클러스터 재구축에 핵심적인 stateless에 맞지 않고, 외부 저장소를 써야한다. 그래서 외부 저장소 중 하나인 OCI Vault에 secret을 저장하고 클러스터에서 그 상태를 읽어가도록 만들었다.
그렇다면 클러스터는 어떻게 외부 서비스에 기록되어있는 secret을 내 pod이 사용할 수 있는 상태로 만들 수 있을까? 이 때 사용할 수 있는 옵션 중 하나는 External-Secrets이다. External-Secrets의 Operator에게 외부 secret 저장소에 접근할 수 있게 만들어주면, 그곳에 저장된 secret을 읽어서 Kubernetes의 Secret resource로 만들어준다.
또 다른 옵션으로는 그냥 git에 secret을 올리는것인데, 대신 git-crypt같은 기능을 이용해서 git pull을 받아도 key가 있어야 secret을 읽을 수 있도록 암호화하는것이다. 하지만 프로젝트 설명에 나와있는것처럼 단점들이 존재하기 때문에 선택하지 않았다.

FYI. 외부 secret 저장소로 유명한 HashiCorp Vault를 사용하면 ArgoCD에 결합해서 Secret resource에 annotation만 추가하는것 만으로도 External-Secrets과 동일한 목적을 더 쉽게 달성할 수 있다. 하지만 External-Secrets는 더 다양한 서비스를 지원하고 ArgoCD에 의존성이 없는것에 반해, 이 옵션은 Vault만 지원하고 ArgoCD에 의존적이기 때문에 선택하지 않았다.

Terraform

그렇다면 외부 secret 서비스는 어떻게 자동으로 만들고, 거기에 secret들은 누가 자동으로 채워줄까? 또 prod 클러스터처럼 cloud의 노드는 어떻게 자동으로 provisioning할까?
이 때 쓰이는게 Terraform이고, 인프라를 코드로 관리할 수 있도록 만들어준다. GitOps가 클러스터 상태를 git에 immutable하게 코드로 정의했다면, Terraform은 인프라의 상태를 immutable하게 코드로 정의할 수 있게 만들어준다. 때문에 "나는 OCI Vault가 필요하다", "나는 OCI Vault에 key는 A, value는 B인 secret을 정의한다", "prod 클러스터에 쓰일 cloud 노드가 필요하다"라고 코드에 정의하면 Terraform이 알아서 그러한 상태로 만들어준다.
그렇다면 Terraform은 무슨 권한으로 OCI에 접근해서 서비스를 만들고 Secret을 넣을까? 결국 Terraform에게도 provider에 접근할 수 있는 API key를 입력으로 줘야한다. 그 key 또한 git에 올려놓을 수 없기 때문에 Terraform Cloud가 존재한다. 여기에 OCI, AWS와 같은 Cloud provider의 API key를 기록해 놓으면 안전하게 보관하면서 provisioning도 해준다.
(어지럽지만) 그럼 Vault에 넣을 secret은 어디서 구해오냐는 질문을 할 수 있다. 이 값들은 상황에 따라 다르기 때문에 Secret의 의존성 관계를 설명하는 곳에서 처음부터 끝까지 설명하겠다.

CI (Github Actions)

CI는 backbone 클러스터안에 다른 CI 구현체를 배포시키는 옵션도 있지만, 무료로 제공되는 Github Actions를 사용했다. Github Actions가 이미 생태계가 잘 갖춰져 있는게 가장 큰 이유였다. homelab에서 CI의 역할은 크게 두가지인데, 첫번째로는 GitOps와 Terraform을 사용할 때 git은 곧 원하는 상태 그 자체이기 때문에, 그 상태가 올바른지 확인하는 lint이다. 두번째로는 Terraform 실행을 위함인데, ArgoCD는 클러스터에 컨트롤러가 항상 돌아가고있기 때문에 주기적으로 github의 상태를 확인한다. 하지만 Terraform은 컨트롤러가 돌아가는게 아니라 CLI로 실행하는 형태이기 때문에, CI에서 Terraform 코드 변경이 있는지 모니터링하다가 발견되면 Terraform CLI를 실행해서 인프라 상태를 동기화한다.


다음편에서는 이번편에서 설명한 클러스터를 어떻게 원클릭으로 구축하게 만들었는지와, 1편에서 정의한 목적을 달성했는지 측정해보겠습니다.

0개의 댓글