LoadBalancer IP 주소 관리

LoadBalancer IP 주소 관리 (LB IPAM)

LB IPAM은 Cilium이 LoadBalancer 타입의 Service에 IP 주소를 할당할 수 있게 해주는 기능이에요. 이 기능은 보통 클라우드 제공자가 담당하지만, 프라이빗 클라우드 환경에서는 항상 사용할 수 있는 것이 아니에요. IP 풀을 만들고 Service에 IP를 할당하는 방법을 설명해요.

출처: LoadBalancer IP Address Management (LB IPAM)

본문

LB IPAM은 Cilium이 LoadBalancer 타입의 Service에 IP 주소를 할당할 수 있게 해주는 기능이에요. 이 기능은 보통 클라우드 제공자에게 맡겨지지만, 프라이빗 클라우드 환경에 배포할 때는 이러한 시설을 항상 사용할 수 있는 것은 아니에요.

LB IPAM은 Cilium BGP Control Plane 및 L2 Announcements / L2 Aware LB (Beta) 같은 기능과 함께 동작해요. LB IPAM은 Service 객체에 IP를 할당·배정하는 일을 담당하고, 다른 기능들이 이 IP의 로드 밸런싱 및/또는 광고를 담당해요.

Cilium BGP Control Plane을 사용해 LB IPAM이 할당한 IP 주소를 BGP로 광고하고, L2 Announcements / L2 Aware LB (Beta)로 로컬에 광고하세요.

LB IPAM은 항상 활성화되어 있지만 휴면 상태예요. 첫 번째 IP Pool이 클러스터에 추가되면 컨트롤러가 깨어나요.

풀 (Pools)

LB IPAM에는 관리자가 IP 할당에 사용할 수 있는 IP 범위를 Cilium에 알려주기 위해 만들 수 있는 IP Pool 개념이 있어요.

IPv4와 IPv6 범위를 모두 가진 기본 IP Pool은 다음과 같아요:

apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "blue-pool"
spec:
  blocks:
  - cidr: "10.0.10.0/24"
  - cidr: "2004::0/112"
  - start: "20.0.20.100"
    stop: "20.0.20.200"

Caution YAML 파일의 대시에 주의하세요: start와 stop 필드는 함께 단일 독립 블록을 정의하며 blocks 시퀀스의 한 항목으로 지정해야 해요.

풀을 클러스터에 추가하면 다음과 같이 보여요.

$ kubectl get ippools
NAME        DISABLED   CONFLICTING   IPS AVAILABLE   AGE
blue-pool   false      False         65892           2s

Warning IP 풀을 업데이트하면 IP 주소가 재배정되고 서비스 IP가 바뀔 수 있어요. GitHub issue 40358 참고.

CIDR, 범위 및 예약 IP (CIDRs, Ranges and reserved IPs)

IP 풀은 여러 IP 블록을 가질 수 있어요. 블록은 CIDR 표기(/)나 시작·종료 IP가 있는 범위 표기로 지정할 수 있어요. 위 Pools에 그림과 같아요.

라우팅 가능한 IP 범위를 지정하기 위해 CIDR을 사용할 때는 CIDR의 첫 번째와 마지막 IP를 할당하지 않기를 원할 수 있어요. 일반적으로 첫 번째 IP는 "네트워크 주소"이고 마지막 IP는 "브로드캐스트 주소"예요. 일부 네트워크에서 이 IP들은 사용할 수 없고 모든 네트워크 장비와 항상 잘 작동하지는 않아요. 기본적으로 LB-IPAM은 주어진 CIDR의 모든 IP를 사용해요.

CIDR의 첫 번째와 마지막 IP를 예약하려면 .spec.allowFirstLastIPs 필드를 No로 설정할 수 있어요.

이 옵션은 /32 및 /31 IPv4 CIDR과 /128 및 /127 IPv6 CIDR에 대해서는 무시돼요. 각각 IP가 1개 또는 2개만 있기 때문이에요.

이 설정은 .spec.blocks[].cidr로 지정된 블록에만 적용되며 .spec.blocks[].start와 .spec.blocks[].stop으로 지정된 블록에는 적용되지 않아요.

서비스 셀렉터 (Service Selectors)

IP Pool에는 선택적 .spec.serviceSelector 필드가 있어서, 관리자가 label selector를 사용해 어떤 서비스가 어떤 풀에서 IP를 받을 수 있는지 제한할 수 있어요. 서비스 셀렉터를 지정하지 않으면 풀은 어떤 서비스에도 할당해요.

apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "blue-pool"
spec:
  blocks:
  - cidr: "20.0.10.0/24"
  serviceSelector:
    matchExpressions:
      - {key: color, operator: In, values: [blue, cyan]}
apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "red-pool"
spec:
  blocks:
  - cidr: "20.0.10.0/24"
  serviceSelector:
    matchLabels:
      color: red

라벨이 아니라 .meta.name이나 .meta.namespace 같은 다른 메타데이터와 매칭하는 몇 가지 특수 목적 셀렉터 필드가 있어요.

Selector Field
io.kubernetes.service.namespace .meta.namespace
io.kubernetes.service.name .meta.name

예를 들어:

apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "blue-pool"
spec:
  blocks:
  - cidr: "20.0.10.0/24"
  serviceSelector:
    matchLabels:
      "io.kubernetes.service.namespace": "tenant-a"

충돌 (Conflicts)

IP Pool은 중첩 CIDR을 가질 수 없어요. 관리자가 중첩되는 풀을 만들면 소프트 오류가 발생해요. 마지막에 추가된 풀이 Conflicting으로 표시되고 그 풀에서 더 이상 할당이 일어나지 않아요. 따라서 관리자는 수정 후 항상 모든 풀의 상태를 확인해야 해요.

예를 들어 같은 CIDR을 가진 2개 풀(blue-pool과 red-pool)을 추가하면 다음을 볼 수 있어요:

$ kubectl get ippools
NAME        DISABLED   CONFLICTING   IPS AVAILABLE   AGE
blue-pool   false      False         254             25m
red-pool    false      True          254             11s

충돌의 이유는 status에 명시되며 다음과 같이 접근할 수 있어요.

$ kubectl get ippools/red-pool -o jsonpath='{.status.conditions[?(@.type=="cilium.io/PoolConflict")].message}'
Pool conflicts since CIDR '20.0.10.0/24' overlaps CIDR '20.0.10.0/24' from IP Pool 'blue-pool'

또는

$ kubectl describe ippools/red-pool
Name:         red-pool
#[...]
Status:
  Conditions:
    #[...]
        Last Transition Time:  2022-10-25T14:09:05Z
        Message:               Pool conflicts since CIDR '20.0.10.0/24' overlaps CIDR '20.0.10.0/24' from IP Pool 'blue-pool'
        Observed Generation:   1
        Reason:                cidr_overlap
        Status:                True
        Type:                  cilium.io/PoolConflict
    #[...]

풀 비활성화 (Disabling a Pool)

IP Pool은 비활성화할 수 있어요. 풀을 비활성화하면 LB IPAM이 그 풀에서 새 IP를 할당하지 않지만, 기존 할당은 제거하지 않아요. 이를 통해 관리자는 풀을 천천히 비우거나(drain) 미래 사용을 위해 예약할 수 있어요.

apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "blue-pool"
spec:
  blocks:
  - cidr: "20.0.10.0/24"
  disabled: true
$ kubectl get ippools
NAME        DISABLED   CONFLICTING   IPS AVAILABLE   AGE
blue-pool   true       False         254             41m

상태 (Status)

IP Pool의 status에는 사용된 IP와 사용 가능한 IP의 양을 모니터링하는 데 사용할 수 있는 추가 카운트가 포함돼요. 머신 파싱 가능한 출력은 다음과 같이 얻을 수 있어요.

$ kubectl get ippools -o jsonpath='{.items[*].status.conditions[?(@.type!="cilium.io/PoolConflict")]}' | jq
{
  "lastTransitionTime": "2022-10-25T14:08:55Z",
  "message": "254",
  "observedGeneration": 1,
  "reason": "noreason",
  "status": "Unknown",
  "type": "cilium.io/IPsTotal"
}
{
  "lastTransitionTime": "2022-10-25T14:08:55Z",
  "message": "254",
  "observedGeneration": 1,
  "reason": "noreason",
  "status": "Unknown",
  "type": "cilium.io/IPsAvailable"
}
{
  "lastTransitionTime": "2022-10-25T14:08:55Z",
  "message": "0",
  "observedGeneration": 1,
  "reason": "noreason",
  "status": "Unknown",
  "type": "cilium.io/IPsUsed"
}

또는 사람이 읽을 수 있는 출력:

$ kubectl describe ippools/blue-pool
Name:         blue-pool
Namespace:
Labels:       <none>
Annotations:  <none>
API Version:  cilium.io/v2
Kind:         CiliumLoadBalancerIPPool
#[...]
Status:
  Conditions:
    #[...]
    Last Transition Time:  2022-10-25T14:08:55Z
    Message:               254
    Observed Generation:   1
    Reason:                noreason
    Status:                Unknown
    Type:                  cilium.io/IPsTotal
    Last Transition Time:  2022-10-25T14:08:55Z
    Message:               254
    Observed Generation:   1
    Reason:                noreason
    Status:                Unknown
    Type:                  cilium.io/IPsAvailable
    Last Transition Time:  2022-10-25T14:08:55Z
    Message:               0
    Observed Generation:   1
    Reason:                noreason
    Status:                Unknown
    Type:                  cilium.io/IPsUsed

서비스 (Services)

.spec.type=LoadBalancer인 모든 서비스는 IP Pool의 서비스 셀렉터가 서비스와 매칭하기만 하면 어떤 풀에서도 IP를 받을 수 있어요.

간단한 서비스를 추가한다고 해 볼게요.

apiVersion: v1
kind: Service
metadata:
  name: service-red
  namespace: example
  labels:
    color: red
spec:
  type: LoadBalancer
  ports:
  - port: 1234

이 서비스는 다음과 같이 보여요.

$ kubectl -n example get svc
NAME          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service-red   LoadBalancer   10.96.192.212   <pending>     1234:30628/TCP   24s

ExternalIP 필드의 값이 <pending>이라는 것은 LB IP가 아직 할당되지 않았음을 의미해요. LB IPAM이 서비스에 IP를 할당하거나 배정하지 못하면 status의 서비스 조건(condition)을 업데이트해요.

서비스 조건은 다음과 같이 확인할 수 있어요:

$ kubectl -n example get svc/service-red -o jsonpath='{.status.conditions}' | jq
[
  {
    "lastTransitionTime": "2022-10-06T13:40:48Z",
    "message": "There are no enabled CiliumLoadBalancerIPPools that match this service",
    "reason": "no_pool",
    "status": "False",
    "type": "io.cilium/lb-ipam-request-satisfied"
  }
]

서비스 라벨을 위의 blue-pool과 매칭되도록 업데이트한 후 다음을 볼 수 있어요:

$ kubectl -n example get svc
NAME          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service-red   LoadBalancer   10.96.192.212   20.0.10.163   1234:30628/TCP   12m

$ kubectl -n example get svc/service-red -o jsonpath='{.status.conditions}' | jq
[
  {
    "lastTransitionTime": "2022-10-06T13:40:48Z",
    "message": "There are no enabled CiliumLoadBalancerIPPools that match this service",
    "reason": "no_pool",
    "status": "False",
    "type": "io.cilium/lb-ipam-request-satisfied"
  },
  {
    "lastTransitionTime": "2022-10-06T13:52:55Z",
    "message": "",
    "reason": "satisfied",
    "status": "True",
    "type": "io.cilium/lb-ipam-request-satisfied"
  }
]

IPv4 / IPv6 패밀리 및 정책 (IPv4 / IPv6 families + policy)

LB IPAM은 SingleStack 또는 DualStack 모드에서 IPv4 및/또는 IPv6를 지원해요. 서비스는 .spec.ipFamilyPolicy와 .spec.ipFamilies 필드를 사용해 요청하는 IP를 바꿀 수 있어요.

.spec.ipFamilyPolicy를 지정하지 않으면 SingleStack 모드로 간주돼요. SingleStack 모드에서 IPv4와 IPv6가 모두 활성화되어 있으면 IPv4 주소가 할당돼요.

.spec.ipFamilyPolicy가 PreferDualStack이면 LB IPAM은 클러스터에서 둘 다 활성화되어 있을 때 IPv4와 IPv6 주소를 모두 할당하려 시도해요. 클러스터에서 IPv4만 또는 IPv6만 활성화되어 있으면 서비스는 여전히 "satisfied"로 간주돼요.

.spec.ipFamilyPolicy가 RequireDualStack이면 LB IPAM은 IPv4와 IPv6 주소를 모두 할당하려 시도해요. 클러스터에서 IPv4 또는 IPv6가 비활성화되어 있으면 서비스는 "unsatisfied"로 간주돼요.

.spec.ipFamilies의 순서는 LB IPAM에 영향을 주지 않지만, LB IPAM이 처리하지 않는 클러스터 IP 할당에는 중요해요.

LoadBalancerClass

Kubernetes >= v1.24는 같은 클러스터에서 여러 로드 밸런서를 지원해요. 로드 밸런서 사이의 선택은 .spec.loadBalancerClass 필드로 해요. LB IPAM이 활성화되면 로드 밸런서 클래스가 설정되지 않은 서비스에 IP를 할당하고 배정해요.

LB IPAM은 IP 할당만 하며 자체적으로 로드 밸런싱 서비스를 제공하지 않아요. 따라서 사용자는 다음 Cilium 로드 밸런서 클래스 중 하나를 선택해야 해요. 이 모두는 할당에 LB IPAM을 사용해요 (기능이 활성화된 경우):

loadBalancerClass Feature
io.cilium/bgp-control-plane Cilium BGP Control Plane
io.cilium/l2-announcer L2 Announcements / L2 Aware LB (Beta)

.spec.loadBalancerClass가 Cilium의 LB IPAM이 처리하지 않는 클래스로 설정되면, Cilium의 LB IPAM은 해당 서비스를 완전히 무시하며 status에 어떤 조건도 설정하지 않아요.

기본적으로 .spec.loadBalancerClass 필드가 설정되지 않으면 Cilium의 LB IPAM은 구성된 풀에서 서비스에 IP를 할당할 수 있다고 가정해요. 이 동작이 바람직하지 않다면 Helm 차트 또는 ConfigMap에 다음 구성을 설정해서, 인식된 로드 밸런서 클래스가 있을 때만 구성된 풀에서 서비스에 IP를 할당하도록 LB-IPAM을 구성할 수 있어요:

Helm ConfigMap Helm Repository OCI Registry

helm upgrade cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --reuse-values \
   --set defaultLBServiceIPAM=none
helm upgrade cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --reuse-values \
   --set defaultLBServiceIPAM=none
default-lb-service-ipam: none

IP 요청 (Requesting IPs)

서비스는 특정 IP를 요청할 수 있어요. 기존 방식은 단일 IP 주소를 받는 .spec.loadBalancerIP를 사용하는 것이에요. 이 방식은 k8s v1.24에서 deprecated됐지만 향후 제거될 때까지 지원돼요.

특정 IP를 요청하는 새 방식은 Cilium LB IPAM의 경우 lbipam.cilium.io/ips 어노테이션을 사용하는 것이에요. 이 어노테이션은 쉼표로 구분된 IP 주소 목록을 받아 한 번에 여러 IP를 요청할 수 있게 해줘요.

IP Pool의 서비스 셀렉터는 여전히 적용돼요. 서비스가 풀의 셀렉터와 매칭하지 않으면 요청된 IP는 할당되거나 배정되지 않아요.

IP 풀의 첫 번째 또는 마지막 IP를 요청하도록 어노테이션을 구성하지 마세요. 이들은 각각 네트워크와 브로드캐스트 주소로 예약되어 있어요.

apiVersion: v1
kind: Service
metadata:
  name: service-blue
  namespace: example
  labels:
    color: blue
  annotations:
    "lbipam.cilium.io/ips": "20.0.10.100,20.0.10.200"
spec:
  type: LoadBalancer
  ports:
  - port: 1234
$ kubectl -n example get svc
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP               PORT(S)          AGE
service-blue   LoadBalancer   10.96.26.105   20.0.10.100,20.0.10.200   1234:30363/TCP   43s

키 공유 (Sharing Keys)

서비스는 다른 서비스와 같은 IP 또는 IP 집합을 공유할 수 있어요. 이는 서비스에 lbipam.cilium.io/sharing-key 어노테이션을 설정해 해요. 같은 sharing key 어노테이션을 가진 서비스는 같은 IP 또는 IP 집합을 공유해요. sharing key는 어떤 값이든 될 수 있는 문자열이에요.

apiVersion: v1
kind: Service
metadata:
  name: service-blue
  namespace: example
  labels:
    color: blue
  annotations:
    "lbipam.cilium.io/sharing-key": "1234"
spec:
  type: LoadBalancer
  ports:
  - port: 1234
apiVersion: v1
kind: Service
metadata:
  name: service-red
  namespace: example
  labels:
    color: red
  annotations:
    "lbipam.cilium.io/sharing-key": "1234"
spec:
  type: LoadBalancer
  ports:
  - port: 2345
$ kubectl -n example get svc
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP               PORT(S)          AGE
service-blue   LoadBalancer   10.96.26.105   20.0.10.100               1234:30363/TCP   43s
service-red    LoadBalancer   10.96.26.106   20.0.10.100               2345:30131/TCP   43s

서비스에 충돌하는 포트가 없으면 같은 IP가 할당돼요. 서비스에 충돌하는 포트가 있으면 서로 다른 IP가 할당되며, 이 IP들은 sharing key에 속한 IP 집합에 추가돼요. 서비스가 sharing key를 갖고 특정 IP도 요청하면 요청된 IP가 할당되고 그 sharing key에 속한 IP 집합에 추가돼요.

기본적으로 네임스페이스 간 IP 공유는 허용되지 않아요. 네임스페이스 간 공유를 허용하려면 lbipam.cilium.io/sharing-cross-namespace 어노테이션을 서비스가 공유될 수 있는 네임스페이스로 설정하세요. 값은 쉼표로 구분된 네임스페이스 목록이어야 해요. 어노테이션은 두 서비스 모두에 있어야 해요. *로 모든 네임스페이스를 허용할 수 있어요.

더 알아보기 (Learn more)