Database-Driven Kubernetes 자동화가 실제로 어떻게 동작하는지 알아보기

안녕하세요, 이전 글에서 Lynq를 만든 이유와 해결하려는 문제에 대해 이야기했는데, 정작 어떻게 동작하는지는 자세히 다루지 못했습니다. 이번 글에서 그 부분을 보충해보려고 합니다. (만약 문제가 있는 경우 말씀 부탁드립니다!)

직접 따라해보고 싶으시다면 Killercoda 시나리오를 준비해두었습니다. 약 10분 정도면 전체 흐름을 체험해볼 수 있습니다: https://killercoda.com/lynq-operator/course/killercoda/lynq-quickstart

기본 개념

먼저 전체적인 구조를 설명드리겠습니다. 크게 세 가지 구성요소가 있습니다.

  1. LynqHub - 데이터베이스에 연결하여 변경사항을 감시합니다

  2. LynqForm - 생성할 Kubernetes 리소스를 정의하는 템플릿입니다

  3. LynqNode - 실제 인스턴스로, 데이터베이스 행 하나당 템플릿 하나씩 생성됩니다

동작 흐름은 단순합니다. 데이터베이스에 행이 추가되면 Hub가 이를 감지하고 LynqNode를 생성합니다. Node 컨트롤러가 템플릿을 렌더링하고 리소스를 적용합니다.

행이 삭제되거나 비활성화되면 자동으로 정리됩니다.

데이터베이스 연결하기

먼저 LynqHub를 생성합니다. 데이터가 어디에 있는지 Lynq에게 알려주는 역할입니다.

apiVersion: operator.lynq.sh/v1
kind: LynqHub
metadata:
  name: my-saas-hub
spec:
  source:
    type: mysql
    mysql:
      host: mysql.default.svc.cluster.local
      port: 3306
      database: nodes
      table: node_data
      username: node_reader
      passwordRef:
        name: mysql-secret
        key: password
  syncInterval: 30s
  valueMappings:
    uid: node_id
    activate: is_active
  extraValueMappings:
    planId: subscription_plan
    region: deployment_region

valueMappings 섹션이 중요합니다. 어떤 컬럼을 사용할지 Lynq에게 알려주는 부분입니다.

  • uid는 각 노드의 고유 식별자입니다

  • activate는 리소스 생성 여부를 제어하는 boolean 값입니다

extraValueMappings에는 필요한 커스텀 필드를 추가할 수 있습니다. 여기서 정의한 값들은 템플릿에서 변수로 사용됩니다.

Hub는 syncInterval 주기로 데이터베이스를 폴링하여 변경사항을 동기화합니다. 행의 activate가 true면 리소스를 생성하고, false로 바뀌면 정리를 시작합니다.

리소스 템플릿 정의하기

다음은 LynqForm입니다. 데이터베이스 행당 어떤 리소스를 생성할지 정의하는 청사진입니다.

apiVersion: operator.lynq.sh/v1
kind: LynqForm
metadata:
  name: web-app
spec:
  hubId: my-saas-hub
  deployments:
    - id: app
      nameTemplate: "{{ .uid }}-app"
      labelsTemplate:
        app: "{{ .uid }}"
        plan: "{{ .planId | default \"basic\" }}"
      spec:
        apiVersion: apps/v1
        kind: Deployment
        spec:
          replicas: 2
          template:
            spec:
              containers:
                - name: app
                  image: "{{ .deployImage | default \"nginx:latest\" }}"
  services:
    - id: svc
      nameTemplate: "{{ .uid }}-svc"
      dependIds: ["app"]
      spec:
        apiVersion: v1
        kind: Service
        # ...

템플릿은 Go의 text/template 문법에 Sprig 함수 라이브러리를 사용합니다. 200개 이상의 함수를 바로 사용할 수 있습니다. 변수는 Hub의 매핑을 통해 데이터베이스 컬럼에서 가져옵니다.

자주 사용하는 패턴을 몇 가지 소개드리면:

  • {{ .uid }} - 고유한 이름 생성

  • {{ .planId | default "basic" }} - 컬럼이 null일 수 있을 때 기본값 지정

  • {{ .uid | trunc63 }} - Kubernetes 이름 길이 제한(63자) 준수

정책(Policy) 활용하기

여기서부터 흥미로워집니다. 모든 리소스가 같은 방식으로 동작할 필요는 없습니다.

템플릿의 각 리소스는 개별 정책을 가질 수 있습니다:

deployments:
  - id: app
    creationPolicy: WhenNeeded
    deletionPolicy: Delete
    conflictPolicy: Stuck
    patchStrategy: apply

creationPolicy는 리소스 생성 및 업데이트 시점을 제어합니다.

WhenNeeded(기본값)는 Lynq가 지속적으로 동기화한다는 의미입니다. 누군가 리소스를 수동으로 삭제해도 다시 생성됩니다. 템플릿을 업데이트하면 변경사항이 적용됩니다. 대부분의 경우 이 설정을 사용하면 됩니다.

Once는 한 번 생성하고 다시는 건드리지 않습니다. 초기 설정 시에만 실행되어야 하는 init job이나 마이그레이션 스크립트에 적합합니다.

jobs:
  - id: init-job
    creationPolicy: Once
    nameTemplate: "{{ .uid }}-init"
    spec:
      apiVersion: batch/v1
      kind: Job
      spec:
        template:
          spec:
            containers:
              - name: init
                command: ["sh", "-c", "echo 'one-time setup'"]
            restartPolicy: Never

deletionPolicy는 LynqNode가 삭제되거나 행이 사라질 때 어떻게 처리할지 제어합니다.

Delete(기본값)는 리소스를 정리합니다. Lynq가 ownerReference를 설정하므로 Kubernetes 가비지 컬렉션이 자동으로 처리합니다.

Retain은 리소스를 유지합니다. ownerReference 대신 라벨로 추적하며, 삭제 시 orphaned로 표시만 해두어 나중에 찾을 수 있게 합니다.

PersistentVolumeClaim처럼 데이터 손실이 우려되는 리소스에는 Retain을 사용하세요:

persistentVolumeClaims:
  - id: data-pvc
    deletionPolicy: Retain
    nameTemplate: "{{ .uid }}-data"

conflictPolicy는 다른 소유자가 관리하는 리소스가 이미 존재할 때 어떻게 처리할지 결정합니다.

Stuck(기본값)은 보수적인 방식입니다. 충돌이 발생하면 reconciliation을 멈추고 이벤트를 발생시킵니다. 안전하지만 수동 개입이 필요합니다.

Force는 Server-Side Apply의 force=true 옵션으로 소유권을 가져옵니다. 다른 시스템에서 마이그레이션하거나 Lynq가 단일 진실 공급원(single source of truth)이어야 할 때 유용합니다.

의존성으로 순서 제어하기

리소스가 특정 순서로 생성되어야 할 때가 있습니다. Deployment는 ConfigMap이 먼저 필요하고, Service는 Deployment를 기다려야 합니다.

이럴 때 dependIds를 사용합니다:

secrets:
  - id: db-creds
    nameTemplate: "{{ .uid }}-creds"

deployments:
  - id: db
    dependIds: ["db-creds"]
    waitForReady: true
    
  - id: app
    dependIds: ["db"]
    waitForReady: true

Lynq는 의존성으로 DAG(Directed Acyclic Graph)를 구성하고 위상 정렬 순서로 리소스를 적용합니다. 실수로 순환 의존성을 만들면 즉시 에러가 발생합니다.

waitForReady: true 플래그가 중요합니다. 이 옵션 없이 dependIds만 사용하면 생성 순서만 보장됩니다. 이 옵션을 켜면 의존하는 리소스가 실제로 Ready 상태가 될 때까지 기다립니다.

skipOnDependencyFailure(기본값 true)도 있습니다. 의존성이 실패하면 종속 리소스도 건너뜁니다. 반대 동작이 필요한 경우도 있습니다:

jobs:
  - id: cleanup-job
    dependIds: ["main-app"]
    skipOnDependencyFailure: false  # main-app이 실패해도 실행

전체 흐름 정리

전체 흐름을 정리하면 다음과 같습니다:

  1. Hub 컨트롤러가 syncInterval마다 데이터베이스를 폴링합니다

  2. 각 활성 행에 대해 LynqNode CR을 생성하거나 업데이트합니다

  3. Node 컨트롤러가 LynqNode를 처리합니다

  4. 행 데이터로 모든 템플릿을 렌더링합니다

  5. 의존성 그래프를 구성하고 리소스를 정렬합니다

  6. Server-Side Apply로 각 리소스를 순서대로 적용합니다

  7. 설정된 경우 Readiness를 기다립니다

  8. 생성된 리소스로 LynqNode 상태를 업데이트합니다

행이 비활성화되거나 삭제되면:

  1. Hub 컨트롤러가 변경을 감지합니다

  2. LynqNode CR이 삭제됩니다

  3. Finalizer가 각 리소스의 deletionPolicy에 따라 정리를 실행합니다

  4. Delete 정책인 리소스는 제거됩니다

  5. Retain 정책인 리소스는 orphan 라벨이 추가됩니다

다음 명령어로 동작을 확인할 수 있습니다:

kubectl get lynqnodes -w
kubectl describe lynqnode <name>

상태에서 현재 상황을 정확히 파악할 수 있습니다:

status:
  desiredResources: 5
  readyResources: 5
  failedResources: 0
  appliedResources:
    - "Deployment/default/acme-app@app"
    - "Service/default/acme-svc@svc"

직접 해보기

이해하는 가장 좋은 방법은 직접 실행해보는 것입니다. Killercoda 시나리오에서 전체 과정을 체험해볼 수 있습니다:

https://killercoda.com/lynq-operator/course/killercoda/lynq-quickstart

약 10분 정도 소요됩니다. MySQL 설정, 오퍼레이터 배포, Hub와 템플릿 생성, 그리고 행을 insert/update/delete하면서 리소스가 어떻게 반응하는지 확인할 수 있습니다.

어떤 상황에 적합한가

이 패턴이 잘 맞는 경우는 다음과 같습니다:

  • 데이터베이스에 이미 비즈니스 데이터가 있는 경우 (사용자, 조직, 테넌트 등)

  • commit-sync-reconcile 루프가 아닌 빠른 프로비저닝이 필요한 경우

  • 같은 리소스를 다른 값으로 여러 번 복제해야 하는 경우

  • 인스턴스 버전 관리보다 템플릿 버전 관리가 중요한 경우

모든 상황에 적합한 것은 아닙니다. 소수의 고유한 환경을 관리한다면 기존 IaC로 충분할 수 있습니다. 하지만 데이터베이스 레코드 기반으로 같은 패턴을 수백 번 복제해야 한다면, 이 접근 방식을 고려해볼 만합니다.


문서: https://lynq.sh GitHub: https://github.com/k8s-lynq/lynq

궁금한 점이 있으시면 댓글로 남겨주세요.

1 Like

여담으로, 이 운영 방식을 뭐라고 불러야 할지 모르겠는데 좋은 이름이 있을까요?

GitOps 의 불편을 해소하고자 만든건데 DataOps는 이미 다른 의미로 사용되는것 같고, DBOps 라고 해야할지… TemplateOps 라고 해야할지 애매하네요 ㅎㅎ

2 Likes

DB 분야라면 @myoungsig.youn 윤명식 상무님이죠! :slight_smile: 윤상무님! 위의 내용에 DB 전문가로써 한마디 말씀?!?!? ^^

1 Like

집단지성이 필요한 때인가요? 운영방식 작명 컨테스트를 열어야 겠습니다 ㅎㅎ
저는 TemplateOps는 기능 보다 구현 스타일을 강조하시려면 좋을 것 같고,
RecordOps ? 데이터 레코드 기반으로 운영된다라는 느낌을 강조하려면 이것도 괜찮을 것 같네요.

나중에는 Bro들의 오픈소스 프로젝트 작명소를 열어도 되겠어요 :slight_smile:

1 Like

주말 내내 고민해봤는데 제안해주신 RecordOps 가 제일 적절하고 더 나은 이름을 찾을수가 없네요.. ㅎㅎ

지금 지원중인 데이터 소스는 MySQL 뿐이지만 다른 관계형 데이터베이스/비관계형 데이터베이스 뿐만 아니라 outgoing webhook 까지 계획에 포함되어 있기 때문에 RecordOps 가 제일 추상적이면서 Lynq 의 동작이 머리속에 잘 그려지는 명칭인것 같습니다.

조만간 소개 페이지를 업데이트 하면서 추천해주신 명칭을 사용해보겠습니다 :slight_smile:
감사합니다

1 Like

@myoungsig.youn 윤명식 상무님 의견? ^^