ClusterIP 서비스에 externarlIPs 를 지정하면, 추가적인 IP 로도 ClusterIP 서비스로의 접근이 가능하게 된다. spec.type: ExternalIP 로 지정하는게 아닌, ClusterIP 로 지정하고, spec.externalIPs 를 지정한다. 단, externalIPs 에는 트래픽을 보내고자 하는 node 의 IP 를 지정해야 한다. externalIPs 를 지정하면, 쿠버네티스 클러스터 외부에서도 클러스터 내부 node IP 로 통신이 가능하며, 파드에 대한 요청도 분산되어 처리된다.

필드 설정값
spec.externalIPs 외부로 노출할 쿠버네티스 노드 IP 주소
spec.ports[].port ClusterIP 서비스에서 수신할 포트 번호
spec.ports[].targetPort 목적지 파드 포트 번호

 

서비스를 생성하기 위해서 먼저 node 의 IP 를 조회한다.

$ kubectl get nodes -o custom-columns='NAME:{.metadata.name},IP:{.status.addresses[?(@.type=="InternalIP")].address}'
NAME                    IP
desktop-control-plane   172.18.0.6,fc00:f853:ccd:e793::6
desktop-worker          172.18.0.4
desktop-worker2         172.18.0.3

 

아래 매니페스트를 사용하여 externalIPs 를 지정한 ClusterIP 서비스를 생성하자. externalIPs 에는 위에서 조회한 노드의 IP 중 클러스터 외부에서 접속 가능하게 할 IP를 기재한다.

apiVersion: v1
kind: Service
metadata:
  name: sample-externalip
spec:
  type: ClusterIP
  externalIPs:
    - 172.18.0.3 # node 의 IP
    - 172.18.0.4 # node 의 IP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

 

생성한 서비스를 조회해보자

$ kubectl get services
# sample-externalip 서비스의 EXTERNAL-IP 에 지정한 node IP 들이 출력됨을 확인한다.
NAME                TYPE        CLUSTER-IP     EXTERNAL-IP             PORT(S)    AGE
kubernetes          ClusterIP   10.96.0.1      <none>                  443/TCP    98d
sample-externalip   ClusterIP   10.96.37.106   172.18.0.3,172.18.0.4   8080/TCP   14s

$ kubectl describe service sample-externalip
Name:                     sample-externalip
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=sample-app
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.37.106
IPs:                      10.96.37.106
External IPs:             172.18.0.3,172.18.0.4
Port:                     http-port  8080/TCP
TargetPort:               80/TCP
Endpoints:                10.244.2.118:80,10.244.2.117:80,10.244.1.133:80
Session Affinity:         None
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster
Events:                   <none>

 

클러스터 내부 파드에서 dig 를 이용해서 클러스터 내부 DNS 에서 반환하는 서비스의 IP 주소를 확인해보면, 위에서 생성한 서비스의 IP 주소임을 확인할 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig sample-externalip.default.svc.cluster.local
...
;; QUESTION SECTION:
;sample-externalip.default.svc.cluster.local. IN	A

;; ANSWER SECTION:
sample-externalip.default.svc.cluster.local. 30	IN A 10.96.37.106
...
pod "testpod" deleted

 

이렇게 서비스를 생성하면 지정한 노드의 iptables 규칙에 kube-proxy 가 아래와 같은 규칙을 추가한다.(일부만 표시)

...
# 파드 IP 가 직접 등록되어있다.
Chain KUBE-SEP-LNFMA4JXKHGYU5GA (1 references)
    0     0 KUBE-MARK-MASQ  0    --  *      *       10.244.2.118         0.0.0.0/0            /* default/sample-externalip:http-port */
Chain KUBE-SEP-PIHNOKMVC3JPXE5V (1 references)
    0     0 KUBE-MARK-MASQ  0    --  *      *       10.244.2.117         0.0.0.0/0            /* default/sample-externalip:http-port */
...
Chain KUBE-SERVICES (2 references)
...
# ClusterIP 서비스 IP/Port 와 설정된 ExternalIPs(즉, 노드 IP)가 등록되어있다.
    0     0 KUBE-SVC-PCX4Y6UD55K35SPY  6    --  *      *       0.0.0.0/0            10.96.37.106         /* default/sample-externalip:http-port cluster IP */ tcp dpt:8080
    0     0 KUBE-EXT-PCX4Y6UD55K35SPY  6    --  *      *       0.0.0.0/0            172.18.0.3           /* default/sample-externalip:http-port external IP */ tcp dpt:8080
    0     0 KUBE-EXT-PCX4Y6UD55K35SPY  6    --  *      *       0.0.0.0/0            172.18.0.4           /* default/sample-externalip:http-port external IP */ tcp dpt:8080
...
# 파드로 로드밸런싱하는 방식이 지정되어있다.
Chain KUBE-SVC-PCX4Y6UD55K35SPY (2 references)
    0     0 KUBE-MARK-MASQ  6    --  *      *      !10.244.0.0/16        10.96.37.106         /* default/sample-externalip:http-port cluster IP */ tcp dpt:8080
    0     0 KUBE-SEP-HNM32ADNEKRGXVEK  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/sample-externalip:http-port -> 10.244.1.133:80 */ statistic mode random probability 0.33333333349
    0     0 KUBE-SEP-PIHNOKMVC3JPXE5V  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/sample-externalip:http-port -> 10.244.2.117:80 */ statistic mode random probability 0.50000000000
    0     0 KUBE-SEP-LNFMA4JXKHGYU5GA  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/sample-externalip:http-port -> 10.244.2.118:80 */
...

 

이 iptables 규칙은 해당 노드의 리눅스 커널이 "직접" 참조하며, 이를 바탕으로 패킷을 필터링하고 NAT 를 직접 수행한다. kube-proxy 파드가 NAT를 수행하는 것이 "아님"에 유의하자. kube-proxy 는 서비스 설정에 따라 노드에 iptables/IPVS 규칙을 생성할 뿐이다.

 

참고로, 각 노드에 실행중인 kube-proxy 파드는 아래 커맨드로 확인 가능하다.

# kube-proxy 파드 확인
$ kubectl get pods -n kube-system -o wide -l k8s-app=kube-proxy
NAME               READY   STATUS    RESTARTS      AGE    IP           NODE                    NOMINATED NODE   READINESS GATES
kube-proxy-6pn2g   1/1     Running   1 (52d ago)   126d   172.18.0.4   desktop-worker          <none>           <none>
kube-proxy-c8r4l   1/1     Running   1 (52d ago)   126d   172.18.0.3   desktop-worker2         <none>           <none>
kube-proxy-l52jn   1/1     Running   0             126d   172.18.0.3   desktop-control-plane   <none>           <none>

 

참고로, 노드의 iptables 를 아래 커맨드로 조회할 수 있다.

# 특정 노드의 kube-proxy 파드에 접속
$ kubectl -n kube-system exec -it kube-proxy-xxxxx -- sh

# 그 안에서 iptables 조회
$ iptables -t nat -L -n -v | grep KUBE

 

노드에 해당 포트로 패킷이 인입되면 노드의 리눅스 커널에서 iptables 에 명시된 NAT 규칙에 따라 목적지 Pod 로 NAT 를 수행한다. ClusterIP 서비스가 런타임(패킷이 들어오는 시점)에 직접 NAT 를 수행하는 것이 아니라는 점에 유의해야 한다. ClusterIP는 로드밸런싱 로직을 정의할 뿐이다. 즉, 실제 NAT는 커널이 수행하지만, "어느 파드로 NAT할지 결정하는" 서비스 로직을 ClusterIP 설정을 기반으로 kube-proxy 가 미리 ClusterIP 체인 로직으로 만들어둔다.(iptables 모드. selector에 매칭되는 각 파드가 대상임.)

 

정리하면, 아래와 같다.

[1] 외부 클라이언트 → 172.18.0.3:8080
       ↓
[2] 노드 172.18.0.3의 kube-proxy가 생성한 iptables/IPVS 규칙이 매칭됨
(노드 리눅스 커널 네트워크 계층에서 수행됨)
       ↓
[3] 해당 규칙이 패킷을 ClusterIP의 externalIPs 로 NAT 수행
(노드 리눅스 커널 네트워크 계층에서 수행됨)
       ↓
[4] ClusterIP 규칙에 따라 파드 중 하나(예: 10.244.1.133:80)로 로드밸런싱
(노드 리눅스 커널 네트워크 계층에서 수행됨)
       ↓
[5] 파드 내 컨테이너 프로세스가 80포트에서 응답

 

단, 무조건 클러스터 외부에서 접속할 수 있는건 아니고, 그 노드에 도달할 수 있는 외부 네트워크(예: 같은 사설망. LAN, VPC 등)에 한해서 접속가능하다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

서비스에 할당 가능한 IP 대역은 kube-apiserver 의 --service-cluster-ip-range 으로 지정되어있다.

아래와 같은 방법으로 조회한다.

$ kubectl get pods -n kube-system -l component=kube-apiserver -o name | xargs kubectl get -n kube-system -o yaml | grep service-cluster-ip-range
   - --service-cluster-ip-range=10.96.0.0/16

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

클러스터 내부에서 가상 IP를 할당받고, 각 노드에 파드로 떠있는 kube-proxy 를 통해서 파드와 통신하는 내부용 로드 밸런서이다.

쿠버네티스 API 에 접속 가능하게 하기 위해 클러스터 생성시 기본적으로 기동되는 Kubernetes 서비스도 ClusterIP 이다.

kube-proxy 는 각 노드에 파드로 떠있으며, kube-system 네임스페이스에서 조회 가능하다.

$ kubectl get pods -o wide -n kube-system | grep proxy
kube-proxy-6pn2g                                1/1     Running       1 (45d ago)   119d   172.18.0.4   desktop-worker          <none>           <none>
kube-proxy-c8r4l                                1/1     Running       1 (45d ago)   119d   172.18.0.3   desktop-worker2         <none>           <none>
kube-proxy-l52jn                                1/1     Running       0             119d   172.18.0.3   desktop-control-plane   <none>           <none>

 

1. ClusterIP 생성

아래와 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Service
metadata:
  name: sample-clusterip
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

 

spec.ports[].port 는 ClusterIP 에서 수신하는 포트를 지정하고, spec.ports[].targetPort 는 목적지 파드의 포트번호를 지정한다.

 

이제 클러스터 내부 파드에서 방금 생성한 서비스명인 sample-clusterip 를 도메인명으로 호출하면, 서비스가 바라보고 있는 파드들(app=sample-app 라벨을 가지고 있는 파드들)로 로드밸런싱 된다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://sample-clusterip:8080
Host=sample-clusterip  Path=/  From=sample-deployment-75c768d5fb-9qdjm  ClientIP=10.244.1.158  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://sample-clusterip:8080
Host=sample-clusterip  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.1.159  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://sample-clusterip:8080
Host=sample-clusterip  Path=/  From=sample-deployment-75c768d5fb-2lkwp  ClientIP=10.244.1.163  XFF=
pod "testpod" deleted

 

2. ClusterIP 의 서비스 디스커버리

한가지 의문이 생긴다. 파드에서 sample-clusterip 도메인으로 호출했을 뿐인데 어떻게 도메인을 해석하고 목적지 파드까지 도달하게 되는 걸까? 아래와 같은 순서로 해석된다.

 

1) 파드 내에서 질의

파드 내에서 http://sample-clusterip:8080 질의

 

2) 파드 내 DNS 레코드 해석

파드 내 /etc/resolv.conf DNS 레코드에 따라 suffix 를 붙여 kube-dns 로 질의한다.

/etc/resolv.conf 는 아래 커맨드로 조회할 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

이에 따라 10.96.0.10 DNS(kube-dns) 에서 해석이 가능 할 때까지 search 에 지정된 suffix 를 붙여 아래 순서대로 질의한다.

sample-clusterip.default.svc.cluster.local
sample-clusterip.svc.cluster.local
sample-clusterip.cluster.local
sample-clusterip

 

3) kube-dns 의 블록 매칭

kube-dns 에서는 coredns configMap 의 설정에 따라 kubernetes cluster.local 블록에 매칭되어 쿠버네티스 API 서버인 kube-apiserver 에 질의한다.
해당 설정은 아래 커맨드로 조회 할 수 있다.

$ kubectl get configmaps coredns -n kube-system -o yaml
...
kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
...

 

 

4) kube-apiserver 에서 네임스페이스와 서비스 명칭으로 서비스 IP 를 조회

쿠버네티스가 자동 생성하는 서비스의 DNS 이름 규칙에 따라 해당하는 서비스의 IP 를 조회한다.

규칙은 아래와 같다.

{서비스명}.{네임스페이스명}.svc.cluster.local

 

5) kube-dns 에서 이를 A 레코드로 응답하여 해석됨

실제로 dig 명령어를 통해 10.96.0.10 DNS 에서 도메인을 어떻게 해석하는지 질의 가능하다.

 

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig @10.96.0.10 sample-clusterip.default.svc.cluster.local
;; ANSWER SECTION:
sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60

 

10.96.82.60 서비스가 응답함을 의미한다.

 

6) 해당 서비스를 호출하여 각 파드로 로드밸런싱 됨

해석된 서비스 IP 인 10.96.82.60:8080 으로 호출한다.

 

3. ClusterIP 의 가상 IP 직접 지정

일반적으로 ClusterIP 를 생성할 때 IP 를 지정하지 않기 때문에 할당 가능한 대역 내에서 자동 할당된다. 하지만, 직접 IP 로 서비스를 참조하고자 하는 상황이 있을 수 있다.

  • DNS 캐시나 방화벽 정책을 고정 IP로 관리할 때(IP 기반 ACL)
  • 테스트 환경에서 동일한 IP 를 사용해야 할 때

직접 IP 를 지정하기 위해서는 spec.clusterIP 에 지정할 IP 를 설정한다.

apiVersion: v1
kind: Service
metadata:
  name: clusterip-vip
spec:
  type: ClusterIP
  clusterIP: 10.96.82.70
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

 

아래 커맨드로 직접 지정한 IP 가 설정되었는지 확인 가능하다.

$ kubectl get services clusterip-vip
NAME               TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
clusterip-vip      ClusterIP   10.96.82.70   <none>        8080/TCP   13m

 

다만, 지정하는 IP 는 아래와 같은 조건이 있으므로 주의하여 사용한다.

Service CIDR 대역 내 IP 로 지정해야 함 예: 10.96.0.0/12 범위 안이어야 함(kube-apiserver의 --service-cluster-ip-range 옵션으로 설정되는 범위)
이미 다른 Service 가 사용중이지 않아야 함 IP 충돌 발생 시 생성 실패 (The Service "X" is invalid: spec.clusterIP: Invalid value: "10.96.100.50": provided IP is already allocated)
한번 IP 를 지정하여 생성하면 변경할 수 없음(immutable) IP 를 변경하려면 삭제 후 재생성 해야 함

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

먼저, 클러스터 내부 DNS 가 무엇인지 알아야 한다.

클러스터 내부 DNS 는 쿠버네티스 파드에서 질의한 도메인이 최우선적으로 바라보는 쿠버네티스 DNS 이다. 도메인이 클러스터 내부 도메인 규칙을 준수한다면 외부 DNS 에 질의하지 않고 클러스터 내부에서 도메인 해석을 끝낸다. 클러스터 내부 DNS 는 kube-system 네임스페이스에 속한 kube-dns 라는 서비스가 그 실체이다.(ClusterIP 타입)

아래의 커맨드로 조회할 수 있다.

$ kubectl get services -n kube-system
# kube-dns 서비스의 CLUSTER-IP 확인. 이 주소가 클러스터 내부 DNS 서버 주소이다.

 

쿠버네티스에서 서비스 디스커버리란, 특정 서비스명으로부터 그 서비스의 엔드포인트 정보를 판별하는 것이다. 특정 조건의 대상이 되는 파드를 판별하는 것이라고 생각해도 좋다. 서비스 디스커버리를 수행하는 방법은 크게 아래 세가지가 있다.

  • 환경변수를 이용
  • DNS A 레코드를 이용
  • DNS SRV 레코드를 이용

1. 환경변수를 이용한 서비스 디스커버리

서비스와 연결된 Pod 에 대해 아래 커멘드처럼 env 를 조회하면, kubernetes 에서 관리하는 환경변수가 설정되어 있음을 알 수 있다. 이 환경변수 값들을 가지고 서비스 디스커버리를 수행하는 것이다. 파드 템플릿의 spec.enableServiceLinks: false 로 설정시에는 환경변수 추가가 비활성화된다.(기본값 true)

$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- env | grep -i kubernetes
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443

 

2. DNS A 레코드를 이용한 서비스 디스커버리

쿠버네티스에서 서비스 엔드포인트에 접속하기 위해서는 당연히 서비스에 할당된 IP 주소를 사용할 수 있지만, 클러스터 내부 DNS 에 자동 등록된 IP-DNS 레코드명을 사용하는 것이 관리 측면에서 바람직하다. IP 주소는 서비스를 생성할 때 마다 변경되기 때문에 미리 알 수 없기 때문이다. DNS명으로 주소를 사용하면 서비스 재생성에 따른 IP 주소 변경에 신경 쓸 필요가 없어진다.

ClusterIP 를 생성하면, 서비스의 name 값이 자동으로 DNS명으로 등록된다. 아래의 커맨드로 ClusterIP 서비스로 호출을 해보면 IP로 직접 호출하는것과 서비스 name 으로 호출하는 것이 동일한 것을 알 수 있다.

# 서비스의 IP로 호출
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://10.96.82.60:8080
Host=10.96.82.60  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.1.135  XFF=
pod "testpod" deleted

# 서비스의 name 으로 호출(DNS명으로 호출)
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://sample-clusterip:8080
Host=sample-clusterip  Path=/  From=sample-deployment-75c768d5fb-9qdjm  ClientIP=10.244.1.134  XFF=
pod "testpod" deleted

 

실제로 등록된 FQDN 형식은 "{서비스명}.{네임스페이스명}.svc.cluster.local" 이다. 이를 확인하려면 클러스터 내의 컨테이너에서 dig(Domain Information Groper) 명령어를 사용해보면 된다. 아래 커맨드를 실행하여 클러스터 내부 DNS 에 등록된 레코드를 확인해보자. sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60 레코드가 등록된 것을 알 수 있다.

# dig 명령어로 DNS 정보 확인
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig sample-clusterip.default.svc.cluster.local
If you don't see a command prompt, try pressing enter.
...
;; QUESTION SECTION:
;sample-clusterip.default.svc.cluster.local. IN A

;; ANSWER SECTION:
sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60
...
pod "testpod" deleted

 

QUESTION SECTION 에 출력된

;sample-clusterip.default.svc.cluster.local. IN A

내용을 해석하면 아래와 같다.

  • ; → dig에서 질의한 도메인 이름을 표시할 때 붙는 기호
  • sample-clusterip.default.svc.cluster.local. → 실제 DNS 서버에 요청한 도메인 전체 이름(FQDN)
  • IN → 인터넷 클래스
  • A → IPv4 주소를 요청했다는 뜻

ANSWER SECTION 에 출력된

sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60

내용을 해석하면 아래와 같다.

 

  • sample-clusterip.default.svc.cluster.local. → 응답한 도메인 이름
  • 30 → TTL(Time To Live): DNS 결과를 캐시해둘 수 있는 시간(초 단위)
  • IN → 인터넷 클래스
  • A → IPv4 주소를 요청했다는 뜻
  • 10.96.82.60 → 해당 서비스의 ClusterIP 주소

 

즉, 쿠버네티스 DNS(CoreDNS)가 sample-clusterip라는 Service를 찾아서, 그 서비스의 클러스터 내부 IP(10.96.82.60)를 응답했다는 뜻이다.

 

아래의 커맨드로 컨테이너 내의 DNS 설정파일을 확인하면, ndots:5 와 search: default.svc.cluster.local, svc.cluster.local, cluster.local 값이 명시되어 있다. 즉, 질의된 도메인의 dot 갯수가 5개 미만이면 질의된 도메인의 suffix 로 search 에 명시된 도메인들을 하나씩 붙여 질의해보는 것이다. 모든 질의에 실패하게 되면 최초 질의된 도메인으로 질의하게 된다.

예를 들어, sample-clusterip 도메인으로 질의하면 dot 이 5개 미만이라 FQDN 으로 인식하지 않기 때문에 아래 순서대로 질의에 성공할때까지 질의하게 된다.

  1. sample-clusterip.default.svc.cluster.local
  2. sample-clusterip.svc.cluster.local
  3. sample-clusterip.cluster.local
  4. sample-clusterip

예를 들어, test1.test2.sample.clusterip.com 도메인으로 질의하면 dot 이 5개 미만이라 마찬가지로 아래 순서대로 성공할때까지 질의함

  1. test1.test2.sample.clusterip.com.default.svc.cluster.local
  2. test1.test2.sample.clusterip.com.svc.cluster.local
  3. test1.test2.sample.clusterip.com.cluster.local
  4. test1.test2.sample.clusterip.com
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
pod "testpod" deleted

 

 

출력 결과를 보면 알 수 있듯이 default 네임스페이스임을 명시하고 있다. 만약 test 네임스페이스라면 default 부분이 test 로 되어있을 것이다. 같은 네임스페이스에 속한 도메인으로 질의할 때는 네임스페이스를 생략하여도 되지만, 다른 네임스페이스로 질의할 때는 명시해야 하는 것이다.

 

아래 커맨드를 이용하면 역방향으로 IP -> 도메인명을 질의할 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig -x 10.96.82.60
...
;; QUESTION SECTION:
;60.82.96.10.in-addr.arpa.	IN	PTR

;; ANSWER SECTION:
60.82.96.10.in-addr.arpa. 30	IN	PTR	sample-clusterip.default.svc.cluster.local.
...
pod "testpod" deleted

 

3. DNS SRV 레코드를 이용한 서비스 디스커버리

SRV 레코드란 특정 서비스가 어떤 호스트와 포트에서 제공되는지를 나타내는 DNS 레코드이다. 즉, SRV 레코드를 조회하면 특정 서비스가 어떤 포트에서 서비스되고 있는지 알 수 있다. 쿠버네티스에서 자동으로 생성하는 SRV 레코드 규칙은 다음과 같다.(port 명과 protocol 에는 접두어 "_" 가 포함됨)

 

"_{서비스 포트명}._{프로토콜}.{서비스명}.{네임스페이스명}.svc.cluster.local"

 

예를 들어 default 네임스페이스에서 아래와 같은 매니페스트로 생성된 서비스는

"_http-port.tcp.sample-clusterip.default.svc.cluster.local" 으로 SRV 레코드가 생성된다.

apiVersion: v1
kind: Service
metadata:
  name: sample-clusterip
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

 

아래 커맨드로 컨테이너 내부에서 SRV 레코드를 조회할 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig _http-port._tcp.sample-clusterip.default.svc.cluster.local SRV
...
;; QUESTION SECTION:
;_http-port._tcp.sample-clusterip.default.svc.cluster.local. IN SRV

;; ANSWER SECTION:
_http-port._tcp.sample-clusterip.default.svc.cluster.local. 30 IN SRV 0 100 8080 sample-clusterip.default.svc.cluster.local.

;; ADDITIONAL SECTION:
sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60
...
pod "testpod" deleted

 

ANSWER SECTION 에 출력된

_http-port._tcp.sample-clusterip.default.svc.cluster.local. 30 IN SRV 0 100 8080 sample-clusterip.default.svc.cluster.local.

내용을 해석하면 아래와 같다.

이름 _http-port._tcp.sample-clusterip.default.svc.cluster.local. 질의한 서비스 이름
TTL 30 30초 동안 캐시 가능
클래스 IN 인터넷 클래스
타입 SRV SRV 레코드
Priority 0 우선순위 (낮을수록 우선)
Weight 100 동일 우선순위 내에서의 부하분산 비율
Port 8080 서비스가 제공되는 포트
Target sample-clusterip.default.svc.cluster.local. 실제 트래픽을 전달할 대상 호스트명(서비스의 DNS 이름)

 

즉, sample-clusterip.default.svc.cluster.local 서비스의 _http-port라는 HTTP 서비스가 TCP 8080 포트에서 제공되고 있다는 것을 DNS 레코드로부터 해석할 수 있다.


 

위와 같이 쿠버네티스 클러스터 내부 DNS 의 서비스 디스커버리 방식을 알아보았다. 그렇다면, 클러스터 내부 DNS 가 아닌 클러스터 외부 DNS 를 사용하려면 어떻게 해야 할까?

[Kubernetes] 파드 DNS 설정 에서 다루었는데, 파드 매니페스트의 spec.dnsPolicy: None 를 지정하고 spec.dnsConfig 필드에 외부 DNS 서버를 지정해주면 된다. 이러한 설정을 해주지 않으면 기본적으로 spec.dnsPolicy: ClusterFirst 으로 적용되어 클러스터 내부 DNS 에 우선 질의한다.(단, spec.dnsPolicy: None 지정시에도 searchs, options 설정에 따라 클러스터 내부 DNS 를 우선적으로 질의하도록 설정할 수 있다.) 클러스터 내부 DNS 에는 "*.cluster.local" 레코드가 지정되어 있으므로 이를 서비스 디스커버리에 우선적으로 사용하고, 조건에 부합하지 않는 도메인 질의시에는 외부 DNS 에 다시 질의하게 된다.

 

DNS 질의 시간을 단축하기 위하여 클러스터 내부 DNS 인 kube-dns 를 사용하기 전에 각 노드에 DNS 캐시 파드를 따로 두고, 이를 우선적으로 사용하는 구조도 있다.(Node Local DNS Cache)

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

1. Multi Port ClusterIP

하나의 ClusterIP 서비스에서 여러 Port 의 엔드포인트를 가지고, 각 Port마다 파드의 다른 Port 로 연결도 가능하다.

아래의 매니페스트처럼 spec.ports 배열필드에 여러 포트를 지정하면 된다. 여기서는 서비스의 8080 -> 파드의 80, 서비스의 8443 -> 파드의 443 포트로 연결된다.

apiVersion: v1
kind: Service
metadata:
  name: clusterip-multi
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
    - name: "https-port"
      protocol: "TCP"
      port: 8443
      targetPort: 443
  selector:
    app: sample-app

 

2. 포트 이름을 사용한 참조

Service 의 spec.ports[].targetPort 에 포트 번호를 지정하지 않고, 미리 정의해놓은 포트 이름으로 포트번호를 사용할 수도 있다. 먼저 서비스가 띄워질 파드를 정의할 때 spec.containers[].ports[] 에 포트 이름과 포트 번호를 지정하고, Service 에서는 targetPort 에 해당 포트 이름으로 설정해주면 된다. 이렇게 하면 Service 매니페스트에서는 동일한 변수명으로 포트를 지정하더라도, 실제 서비스가 띄워지는 Pod 마다 다른 포트로 연결해줄 수 있다.

 

아래의 매니페스트로 "http" 이름의 port 가 각각 80, 81 포트로 정의된 파드를 한개씩 띄우자

---
apiVersion: v1
kind: Pod
metadata:
  name: named-port-pod-80
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: amsy810/echo-nginx:v2.0
      ports:
        - name: http
          containerPort: 80
---
apiVersion: v1
kind: Pod
metadata:
  name: named-port-pod-81
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: amsy810/echo-nginx:v2.0
      env:
        - name: NGINX_PORT
          value: "81"
      ports:
        - name: http
          containerPort: 81

 

아래의 매니페스트로 "http" 이름을 통해 targetPort 를 지정한 Service 를 띄우자

apiVersion: v1
kind: Service
metadata:
  name: named-port-service
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: http
  selector:
    app: sample-app

 

아래의 커맨드로 서비스의 목적지 파드의 엔드포인트의 port 가 각각 80, 81 로 설정되었는지 확인할 수 있다.

# 파드의 IP/PORT 확인
$ kubectl get pods -l app=sample-app -o custom-columns="NAME:{metadata.name},IP:{status.podIP},PORT:{spec.containers[].ports[].containerPort}"
NAME                IP             PORT
named-port-pod-80   10.244.2.116   80
named-port-pod-81   10.244.1.132   81

# 서비스가 바라보는 파드의 port 확인
$ kubectl describe service named-port-service
Name:                     named-port-service
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=sample-app
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.134.44
IPs:                      10.96.134.44
Port:                     http-port  8080/TCP
TargetPort:               http/TCP
Endpoints:                10.244.1.132:81,10.244.2.116:80
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

쿠버네티스에서 네트워크를 담당하는 리소스는 서비스 API 카테고리에 포함된 리소스들이다. 서비스 API 리소스는 클러스터상의 컨테이너에 대한 엔드포인트를 제공하거나 레이블과 일치하는 컨테이너를 서비스 디스커버리에 포함하는데 사용된다. 사용자가 직접 사용할 수 있는 것은 아래와 같으며, L4 의 서비스들과 L7의 인그레스 리소스가 있다.

  • 서비스(L4)
    • ClusterIP
    • ExternalIP(ClusterIP 의 한 종류)
    • NodePort
    • LoadBalancer
    • Headless(None)
    • ExternalName
    • None-Selector
  • 인그레스(L7)

각 서비스를 설명하기 전에 여기서는 기본적인 쿠버네티스 클러스터의 네트워크 구성을 알아보자.

 

쿠버네티스 클러스터를 생성하면, 자동으로 각 노드 내에 파드간 통신을 위한 내부 가상 네트워크(파드 네트워크)가 자동으로 구성된다. 노드별로 사용할 네트워크 세그먼트는 쿠버네티스가 클러스터 전체에 할당된 네트워크 세그먼트를 자동으로 분할해 할당하므로, 사용자가 직접 설정할 필요는 없다.

 

(아래 내용은 위 그램을 참고하여 읽자)

1. 파드 내 컨테이너 간 통신

동일한 파드 내에 존재하는 컨테이너와 컨테이너 간 통신은 별도의 IP 없이 localhost 로 통신이 가능하다. 쿠버네티스에서는 각 파드에 IP 주소가 할당되고, 네트워크 통신을 주고 받는 최소 단위는 파드이기 때문이다.(같은 파드 내에 존재하는 컨테이너는 IP가 같고 동일한 네트워크를 공유함)

 

2. 노드 내 파드 간 통신

동일 노드 내에 존재하는 파드간 통신을 할 때는 노드 내에서 여러 파드 인터페이스를 연결하는 cni0 네트워크 브릿지를 통해서 통신한다. cni0 bridge 에는 각 파드의 가상 IP 와 그 파드와 virtual ethernet pair 로 이어져있는 네트워크 인터페이스 장치(veth0,1,2...)의 mac 주소가 ARP 테이블에 매핑되어 저장되어 있어서 어느 파드로 통신할 것인지 알 수 있다.

 

3. 노드 간 파드 간 통신

다른 노드에 존재하는 파드간 통신의 경우에는 오버레이 네트워크를 이용하게 된다. 오버레이 네트워크의 실제 구성은 사용하는 CNI(Container Network Interface) 플러그인에 따라 다르다.(예시: Flannel 은 VxLAN 또는 host-gw, UDP 등을 사용함) Flannel CNI 플러그인을 설치하면 flannel.1 인터페이스가 생성되어 노드의 eth0 인터페이스와 cni0 브릿지를 연결해주며, cni0 -> flannel.1 -> eth0 을 통해 패킷이 외부로 나갈 때에는 flannel.1 에서 패킷을 wrap 하여 header 에 출발지/목적지 노드IP를 추가하고, payload 에 출발지/목적지 파드IP를 추가하는 행위를 한다.(패킷 캡슐화) 노드 네트워크를 통해서 목적지 노드에 도착하면 역순으로 eth0 -> flannel.1 -> cni0 으로 패킷이 전달되며, flannel.1 에서 패킷이 unwrap 되어 어떤 파드로 전달될 것인지 해석된다. 이를 가상 네트워크 오버레이라고 한다. 오버레이는 물리적 네트워크 구성과 관계없이 다른 노드에 있는 파드들이 마치 같은 노드에 존재하는 것 처럼 통신할 수 있게 해주는 것이다. 물론, 오버레이는 가상 네트워크이기 때문에 실제 패킷은 노드 네트워크를 통해 이동하게 된다.

 

4. 서비스 동작 맛보기

서비스 리소스를 사용하지 않아도 위와 같은 경로를 통해서 파드 간 통신이 가능하지만, 서비스를 사용하면 여러 파드에 대한 로드밸런싱과 서비스 디스커버리를 좀 더 수월하게 구성할 수 있다. 파드를 사용할 때는 보통 디플로이먼트를 이용해서 관리하게 되는데, 파드가 재기동되면 파드의 IP가 바뀌게 된다. 로드밸런싱을 직접 구현하게 되면 매번 파드의 IP를 재조회하는 등의 구현을 해야 하지만, 서비스를 사용하게 되면 label selector 기준으로 자동으로 변경된 파드 IP로 로드밸런싱 되게 하는 등의 편리함이 있다.

 

먼저 아래 매니페스트를 apply 하여 Deployment 를 기동하여 3개의 파드를 기동해보자. amsy810/echo-nginx 이미지는 접속정보를 응답으로 내어주는 이미지이다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: amsy810/echo-nginx:v2.0
          ports:
            - containerPort: 80

 

띄워진 파드의 LABEL 과 IP 를 확인하자

$ kubectl get pods -l app=sample-app -o custom-columns="NAME:{metadata.name},LABELS:{metadata.labels}"
# pod template 이 완벽히 동일하므로 해시값도 동일하다.
NAME                                 LABELS
sample-deployment-79cd77b9d6-cr8vj   map[app:sample-app pod-template-hash:79cd77b9d6]
sample-deployment-79cd77b9d6-v7dzz   map[app:sample-app pod-template-hash:79cd77b9d6]
sample-deployment-79cd77b9d6-z6jt5   map[app:sample-app pod-template-hash:79cd77b9d6]

$ kubectl get pods -l app=sample-app -o custom-columns="NAME:{metadata.name},IP:{status.podIP}"
# 자동 할당된 파드의 VIP 확인
NAME                                 IP
sample-deployment-79cd77b9d6-cr8vj   10.244.2.111
sample-deployment-79cd77b9d6-v7dzz   10.244.1.106
sample-deployment-79cd77b9d6-z6jt5   10.244.1.107

 

아래 매니페스트를 이용하여 ClusterIP 타입의 Service 를 기동하자

apiVersion: v1
kind: Service
metadata:
  name: sample-clusterip
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

 

아래 커맨드를 이용하여 생성한 서비스 정보를 확인해보면, IP 값이 해당 서비스의 IP 이고, Port 가 해당 서비스의 port 이다. Endpoints 에 명시된 IP:Port 들이 해당 서비스를 통해서 로드밸런싱 되는 대상 Pod 들의 주소값이다. 위에서 조회했던 Pod 의 IP:Port 와 동일함을 알 수 있다. selector 조건이 일치하는 Pod 의 주소를 자동으로 가져온 것이다. 본 서비스는 ClusterIP 타입이므로, 서비스의 엔드포인트가 되는 IP는 클러스터 내부에서만 접근 가능한 가상 IP임에 유의하자.

$ kubectl describe service sample-clusterip
Name:                     sample-clusterip
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=sample-app
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.236.220
IPs:                      10.96.236.220
Port:                     http-port  8080/TCP
TargetPort:               80/TCP
Endpoints:                10.244.1.106:80,10.244.2.111:80,10.244.1.107:80
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

 

파드가 삭제된 후 다시 동일한 label 을 가진 파드가 기동되었을 때 서비스가 해당 파드들을 자동으로 인식하는지 알아보기 위해 아래 커맨드를 실행해보자

# 디플로이먼트를 삭제하여 파드를 모두 삭제한다.
$ kubectl delete -f sample-deployment.yaml

# 서비스의 Endpoints 를 확인하면, 비어진 상태가 됨을 알 수 있다.
$ kubectl describe service sample-clusterip
Name:                     sample-clusterip
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=sample-app
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.236.220
IPs:                      10.96.236.220
Port:                     http-port  8080/TCP
TargetPort:               80/TCP
Endpoints:
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

# 다시 디플로이먼트를 기동하여 다른 IP를 가진 파드를 생성한다.
$ kubectl apply -f sample-deployment.yaml
deployment.apps/sample-deployment created

# 아까와는 다른 IP를 가진 파드들이 생성되었음을 확인한다.
$ kubectl get pods -l app=sample-app -o custom-columns="NAME:{metadata.name},IP:{status.podIP}"
NAME                                 IP
sample-deployment-75c768d5fb-4pngm   10.244.1.108
sample-deployment-75c768d5fb-pl4nz   10.244.2.113
sample-deployment-75c768d5fb-qwbjr   10.244.2.114

# 새로운 파드들의 IP를 Endpoints 로 가지고 있음을 확인한다.
$ kubectl describe service sample-clusterip
Name:                     sample-clusterip
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=sample-app
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.236.220
IPs:                      10.96.236.220
Port:                     http-port  8080/TCP
TargetPort:               80/TCP
Endpoints:                10.244.1.108:80,10.244.2.113:80,10.244.2.114:80
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

 

이제 아래 커맨드를 이용해서 실제로 여러 파드로 균일하게 트래픽이 로드밸런싱 되는지 확인하자. ClusterIP 는 클러스터 내부에서만 접근이 가능한 가상 IP 를 사용하기 때문에 클러스터 내부에 호출용 파드를 하나 기동하고, 그 내부의 컨테이너에서 ClusterIP 로 요청을 보낸다. echo-nginx 컨테이너는 호출된 파드의 호스트명을 포함한 응답을 반환하므로 쉽게 확인할 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://10.96.236.220:8080
Host=10.96.236.220  Path=/  From=sample-deployment-75c768d5fb-4pngm  ClientIP=10.244.1.118  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://10.96.236.220:8080
Host=10.96.236.220  Path=/  From=sample-deployment-75c768d5fb-pl4nz  ClientIP=10.244.1.119  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://10.96.236.220:8080
Host=10.96.236.220  Path=/  From=sample-deployment-75c768d5fb-pl4nz  ClientIP=10.244.1.120  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://10.96.236.220:8080
Host=10.96.236.220  Path=/  From=sample-deployment-75c768d5fb-4pngm  ClientIP=10.244.1.121  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://10.96.236.220:8080
Host=10.96.236.220  Path=/  From=sample-deployment-75c768d5fb-pl4nz  ClientIP=10.244.1.122  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://10.96.236.220:8080
Host=10.96.236.220  Path=/  From=sample-deployment-75c768d5fb-qwbjr  ClientIP=10.244.1.124  XFF=
pod "testpod" deleted

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

[Kubernetes] 크론잡

Kubernetes 2025. 10. 7. 00:04

CronJob 은 리눅스 크론탭처럼 스케쥴된 일정 시각마다 Job 을 실행시키는 리소스이다. Job 의 또다른 형태로 보이지만, 실제로는 Deployment 와 ReplicaSet 의 관계처럼 CronJob 이 Job 을 관리하고, Job 이 Pod 를 관리하는 관계이다.

 

1. 크론잡 생성

apiVersion: batch/v1
kind: CronJob
metadata:
  name: sample-cronjob
spec:
  schedule: "*/1 * * * *"
  concurrencyPolicy: Allow
  startingDeadlineSeconds: 30
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 3
  suspend: false
  jobTemplate:
    spec:
      completions: 1
      parallelism: 1
      backoffLimit: 1
      template:
        spec:
          containers:
            - name: sleep-container
              image: centos:7
              command:
                - "sh"
                - "-c"
              args:
                # 약 50% 확률로 성공하는 명령어
                - "sleep 40; date +'%N' | cut -c 9 | egrep '[1|3|5|7|9]'"
          restartPolicy: Never

 

위의 매니페스트를 기반으로 50% 확률로 성공하는 Job 을 실행하는 CronJob 을 생성하고, 아래 커맨드로 CronJob, Job, Pod 를 조회해보자

 

$ kubectl get cronjobs.batch
$ 크론잡 생성 직후에는 아직 다음 시작 1분에 도달하지 않아서 크롭잡만 존재하고, ACTIVE 잡이 없다.
NAME             SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
sample-cronjob   */1 * * * *   <none>     False     0        <none>          5s
$ kubectl get jobs
No resources found in default namespace.

# 몇초가 지난 후 재실행하면 ACTIVE 상태로 변경되며, Job 도 조회된다.
$ kubectl get cronjobs.batch
NAME             SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
sample-cronjob   */1 * * * *   <none>     False     1        40s             52s
$ kubectl get jobs
NAME                      STATUS    COMPLETIONS   DURATION   AGE
sample-cronjob-29329291   Running   0/1           30s        30s

# 50퍼센트 확률로 실패했다.
$ kubectl get pods
NAME                            READY   STATUS   RESTARTS   AGE
sample-cronjob-29329291-wg8rp   0/1     Error    0          48s

# 몇분간 방치한 후 매분 생성된 Job 과 Pod 들을 조회한다.
$ kubectl get jobs
NAME                      STATUS     COMPLETIONS   DURATION   AGE
sample-cronjob-29329300   Failed     0/1           11m        11m
sample-cronjob-29329301   Failed     0/1           10m        10m
sample-cronjob-29329305   Complete   1/1           92s        6m39s
sample-cronjob-29329306   Complete   1/1           93s        5m39s
sample-cronjob-29329307   Complete   1/1           93s        4m39s
sample-cronjob-29329308   Complete   1/1           43s        3m39s
sample-cronjob-29329309   Failed     0/1           2m39s      2m39s
sample-cronjob-29329310   Complete   1/1           43s        99s
sample-cronjob-29329311   Running    0/1           39s        39s

$ kubectl get pods
# backoffLimit: 1 이므로, 실패한 경우 한번 더 Pod 를 생성하여 시도한다.
NAME                            READY   STATUS      RESTARTS   AGE
sample-cronjob-29329300-4gdrc   0/1     Error       0          11m
sample-cronjob-29329300-8ts96   0/1     Error       0          10m
sample-cronjob-29329301-6wsxr   0/1     Error       0          10m
sample-cronjob-29329301-ww9jj   0/1     Error       0          9m56s
sample-cronjob-29329306-96zcl   0/1     Completed   0          4m56s
sample-cronjob-29329306-p7nmp   0/1     Error       0          5m46s
sample-cronjob-29329307-45pnz   0/1     Error       0          4m46s
sample-cronjob-29329307-pr9ls   0/1     Completed   0          3m56s
sample-cronjob-29329308-cvc4k   0/1     Completed   0          3m46s
sample-cronjob-29329309-6nrc9   0/1     Error       0          2m46s
sample-cronjob-29329309-6qhs6   0/1     Error       0          116s
sample-cronjob-29329310-fwj22   0/1     Completed   0          106s
sample-cronjob-29329311-rlmls   0/1     Completed   0          46s

 

2. 크론잡 일시정지

spec.suspend: true 로 설정하여 적용하면 스케쥴링 대상에서 제외된다. 매니페스트를 변경 후 kubectl apply 를 하거나, 아래처럼 kubectl patch 를 하여 적용한다. 다시 suspend: false 로 적용하면 일시정지가 풀린다.

$ kubectl patch cronjob sample-cronjob -p '{"spec":{"suspend":true}}'

# SUSPEND: True 로 변경되며, 스케쥴링에서 제외된 상태이다.
$ kubectl get cronjobs
NAME             SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
sample-cronjob   */1 * * * *   <none>     True      1        52s             45m

 

3. 크론잡을 스케쥴링 이외의 시점에 실행

CronJob 으로부터 Job 을 생성하여 1회 자율적으로 실행할 수 있다.

$ kubectl create job job-from-cronjob --from cronjob/sample-cronjob

 

4. 이전 잡이 종료되지 않았을 경우 동시성 제어

일반적으로 크론잡은 작업 완료까지 소요되는 시간을 고려하여 충분한 시간 간격으로 스케쥴링을 해야한다. 이전에 수행된 잡이 다음 수행시점 이전에 종료되었다면 다음 수행시점에는 새로운 잡을 생성하여 실행하지만, 만약에 이전 잡이 아직 실행중이라면 새로운 잡 실행 방식에 대해서 아래와 같이 spec.concurrencyPolicy 필드에 지정할 수 있다.

spec.concurrencyPolicy 값 설명
Allow 새로운 잡을 생성하여 이전 잡과 동시에 실행(기본값)
Forbid 새로운 잡을 생성하지 않음(동시실행 제한)
Replace 이전 잡을 삭제하고 새로운 잡을 생성하여 실행

 

5. 잡 실행 시작 기한 설정

크론잡으로 스케쥴링한 시각에는 kubernetes master 가 잡을 생성한다. 그러므로 해당 시각에 마스터 노드의 시스템 리소스가 부족하거나 일시적으로 다운되어 시작시각에 정확히 시작하지 못하면, 얼마나 지연 시작을 허용하지를 spec.startingDeadlineSeconds 필드에 지정할 수 있다. 예를 들어 매시 정각에 실행되는 크론잡인 경우(schedule: "0 * * * *") 정각부터 5분이 경과되기 전까지 시작을 허용하고자 한다면, spec.startingDeadlineSeconds: 300 값을 지정해주면 된다. 미지정시 아무리 지연되더라도 잡을 실행하게 되어있다.

 

6. 크론잡 이력 저장

크론잡이 생성한 잡은 종료된 후에도 삭제되지 않고 남아있는데, 남길 갯수를 지정할 수 있다.

필드 설명
spec.successfulJobsHistoryLimit 성공한 잡 저장 개수(기본값 3)
spec.failedJobsHistoryLimit 실패한 잡 저장 개수(기본값 3)

 

잡이 남아있으므로, 잡이 관리하는 파드도 Completed(정상종료) 또는 Error(비정상종료) 상태로 남아있게 된다.(1번 항목에서 조회한 Pod 참고) 0으로 지정하면 잡은 종료 즉시 삭제된다. 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

Job 리소스는 실행이 완료되어도 그 Job 과 Pod 가 자동으로 삭제되지 않는다. 이를 정리하는 옵션이 바로 ttlSecondsAfterFinished 이다. ttlSecondsAfterFinished 를 원하는 만큼 초 단위로 지정하면, Job 실행이 완료되 이후 해당 초가 지나면 Job 과 Pod 가 삭제된다.

apiVersion: batch/v1
kind: Job
metadata:
  name: job-ttl
spec:
  ttlSecondsAfterFinished: 30
  completions: 1
  parallelism: 1
  backoffLimit: 10
  template:
    spec:
      containers:
        - name: sleep-container
          image: centos:7
          command: ["sleep"]
          args: ["60"]
      restartPolicy: Never

 

위의 매니페스트로 Job 을 실행하면 60초 동안 Pod 하나가 실행되고 완료된 후, 30초 후에 Job 과 Pod 가 삭제되는 것을 볼 수 있다.

$ kubectl apply -f sample-job-ttl.yaml

$ kubectl get jobs
NAME                  STATUS     COMPLETIONS   DURATION   AGE
job-ttl               Running    0/1           4s         4s

# 30초 동안 실행되고 종료되는 Pod
$ kubectl get pods
NAME                        READY   STATUS      RESTARTS   AGE
job-ttl-kcv8n               1/1     Running     0          7s

# Job 의 상태가 변경되는 것을 출력
$ kubectl get job job-ttl --watch --output-watch-events
EVENT      NAME      STATUS    COMPLETIONS   DURATION   AGE
ADDED      job-ttl   Running   0/1           36s        36s
MODIFIED   job-ttl   Running   0/1           62s        62s
MODIFIED   job-ttl   Running   0/1           63s        63s
MODIFIED   job-ttl   Complete   1/1           63s        63s
MODIFIED   job-ttl   Complete   1/1           63s        93s
DELETED    job-ttl   Complete   1/1           63s        93s # Job 이 삭제된

# Pod 삭제 확인
$ kubectl get pods
No resources found in default namespace.

 

'Kubernetes' 카테고리의 다른 글

[Kubernetes] 클러스터 네트워크 개괄  (0) 2025.10.12
[Kubernetes] 크론잡  (1) 2025.10.07
[Kubernetes] 잡 병렬 실행  (0) 2025.10.04
[Kubernetes] 잡  (0) 2025.10.04
[Kubernetes] 스테이트풀셋 삭제와 영구 볼륨 정리  (0) 2025.09.07
블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

Job 매니페스트에는 아래 세가지 주요 필드를 설정할 수 있다.

필드명 의미 비고
completions 성공 횟수 실행 중 변경 불가능
parallelism 병렬성 동시에 실행하고자 하는 잡 갯수. 실행 중 변경 가능
backoffLimit 실패 허용 횟수 0 지정시 실패를 허용하지 않음. 실행 중 변경 가능

 

이를 기반으로 전형적인 네 가지 워크로드를 Job 으로 실행해보자

 

1. 1회만 실행하는  태스크

completions 1
parallelism 1
backoffLimit 0

 

성공 유무와 관계없이(backoffLimit: 0) 1회만 실행된다.

 

2. N 개를 병렬로 M 번 성공시까지 실행하는 태스크

completions M
parallelism N
backoffLimit P

 

실패는 P번 까지 허용하며, 동시에 N개의 파드를 기동하여 잡을 실행하며, 성공 횟수가 M 번이 될 때 까지 실행한다. 주의할 점은, 남은 성공횟수가 N 이하인 경우 남은 성공횟수 만큼만의 파드만 기동한다는 점이다.

예를들어, completions: 5, parallelism: 3 을 지정하면 아래와 같은 순서로 진행된다.

  • 3개의 파드가 동시 실행된다.(병렬성 3)
  • 3개의 파드가 성공적으로 실행 완료된다.(목표 성공횟수 성공횟수 2/5)
  • 남은 성공 횟수가 2 이므로, 3개가 아닌 2개의 파드가 동시 실행된다.(즉, 동시 실행되는 파드의 수는 남은 성공횟수보다 클 수 없다.)

또한, parallelism 을 completions 이상으로 설정하더라도 마찬가지로 남은 성공횟수가 completions 가 되어버리기 때문에 결국에는 completions 갯수만큼의 파드가 기동된다.

 

3. N 개를 병렬로 실행하는 작업큐

위에서 말한 Task 잡은 일정 성공 횟수에 도달할 때 까지 파드 종료 -> 시작을 반복하며 진행한다. 하지만 성공 횟수가 정해지지 않고 메시지 큐에 쌓인 모든 Topic 을 처리할 때 까지 N 개의 병렬성을 가지고 N 개의 파드를 계속 유지하면서 작업을 처리해야 하는 경우가 있다. 이때는 아래처럼 설정한다.

completions 지정하지 않음
parallelism N
backoffLimit P

 

이렇게 Job 을 실행하면 N 개의 파드가 병렬로 실행된다. 그 중 하나라도 정상 종료되면 전체 Job 이 종료되게 되며 더이상의 추가 파드 실행은 없다. 당연히 이미 실행중인 파드는 자체적으로 작업이 종료될 때 까지 유지된다.

 

작업큐의 잡을 이용하려면 대상 작업을 쌓기 위한 메시지 큐를 따로 두어야 한다. Job 이 관리하는 파드 내의 애플리케이션에서는 현재 진행중인 작업이 종료되기 전에 그 메시지 큐로부터 반복하여 Topic 을 가져오는 처리를 구현해두어야 한다. Topic 이 존재하면 현재 작업이 종료되어도 파드를 성공 종료처리하지 않고, 바로 이어서 해당 작업을 진행한다. 이를 반복하다가 메시지 큐가 빈 것을 확인하면 그때서야 파드를 정상 종료 처리하도록 구현하여야 전체 병렬 작업들을 마무리할 수 있게 된다.

이렇게 메시지 큐가 빈 이후에 다시 새로운 Topic 이 등록되는 경우를 위해서는 나중에 따로 설명할 Cron Job 을 이용하여 Job 을 정기적으로 실행하여 병렬 작업큐가 다시 기동될 수 있도록 구현해둔다.

메시지 큐에 쌓인 메시지 양에 따라 Job 의 parallelism 을 동적으로 변경할 수 있도록 작성하여 오토 스케일링하는 것도 가능하다.

 

작업큐는 completions 를 지정하지 않는다고 하였는데, 이렇게 작성하면 COMPLETIONS 가 태스크와 다르게 표시된다.

# COMPLETIONS 표시 형식 확인
$ kubectl get jobs multi-workqueue-job
NAME                  STATUS    COMPLETIONS   DURATION   AGE
multi-workqueue-job   Running   0/1 of 3      6s         6s

# 실행중인 파드 확인
$ kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
multi-workqueue-job-6hdcg   1/1     Running   0          11s
multi-workqueue-job-jgxbt   1/1     Running   0          11s
multi-workqueue-job-tl4s2   1/1     Running   0          11s

# 파드 실행 완료 확인
$ kubectl get pods
NAME                        READY   STATUS      RESTARTS   AGE
multi-workqueue-job-6hdcg   0/1     Completed   0          21m
multi-workqueue-job-jgxbt   0/1     Completed   0          21m
multi-workqueue-job-tl4s2   0/1     Completed   0          21m

# COMPLETIONS 표시 형식 확인
$ kubectl get jobs multi-workqueue-job
NAME                  STATUS     COMPLETIONS   DURATION   AGE
multi-workqueue-job   Complete   3/1 of 3      33s        21m

 

4. 1개씩 실행하는 작업큐

병렬 작업큐에서 parallelism 만 1로 변경한 것으로, 병렬성이 없는 작업큐라고 보면 된다.

completions 지정하지 않음
parallelism N
backoffLimit P

 

parallelism 은 Job 실행중에 변경할 수 있으므로, N개의 병렬 작업큐로 전환할 수 있다. kubectl patch 를 사용하거나, 매니페스트 수정 후 apply 를 진행하면 된다.(이력관리를 위해서 apply 권장)

# 확인을 위해 parallelism: 1 로 변경
$ kubectl patch job multi-workqueue-job -p '{"spec": {"parallelism": 1}}'
job.batch/multi-workqueue-job patched

$ kubectl get jobs
NAME                  STATUS     COMPLETIONS   DURATION   AGE
multi-workqueue-job   Complete   3/1           33s        79m

$ kubectl patch job multi-workqueue-job -p '{"spec": {"parallelism": 2}}'
job.batch/multi-workqueue-job patched

$ kubectl get jobs
NAME                  STATUS     COMPLETIONS   DURATION   AGE
multi-workqueue-job   Complete   3/1 of 2      33s        80m

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

[Kubernetes] 잡

Kubernetes 2025. 10. 4. 13:54

Job은 일정 횟수의 배치작업 성공을 보장하기 위해 사용하는 리소스이다. 좀 더 자세히 말하면, n개의 파드에서 각각 컨테이너를 띄워 일련의 프로세스를 병렬로 수행하고, 프로세스가 종료되면 정상종료/비정상종료 여부에 따라서 성공 횟수를 집계하고, 원하는 성공 횟수가 될 때 까지 반복하는 것이다.

 

1. 잡 생성

apiVersion: batch/v1
kind: Job
metadata:
  name: sample-job
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 10
  template:
    spec:
      containers:
        - name: sleep-container
          image: centos:7
          command: ["sleep"]
          args: ["60"]
      restartPolicy: Never

 

위의 매니페스트로 잡을 생성한다.

 

2. 잡 동작

아래 커맨드를 이용하여 job 이 관리하는 pod 를 관찰하면, 60 초 후 pod 가 정상 종료되면 job dml completions 가 1/1 로 변경되는 것을 볼 수 있다.

$ kubectl get jobs
NAME         STATUS    COMPLETIONS   DURATION   AGE
sample-job   Running   0/1           4s         4s

$ kubectl get pods --watch
NAME               READY   STATUS    RESTARTS   AGE
sample-job-x5cp9   1/1     Running   0          10s
sample-job-x5cp9   0/1     Completed   0          62s # 60초 후
sample-job-x5cp9   0/1     Completed   0          63s
sample-job-x5cp9   0/1     Completed   0          64s

$ kubectl get jobs
NAME         STATUS     COMPLETIONS   DURATION   AGE
sample-job   Complete   1/1           64s        86s

 

3. restartPolicy

restartPolicy 는 job 을 수행하는 컨테이너 내의 프로세스가 job 수행을 완료하지 못하고 정지되었을 때, 파드를 새로 띄워 다시 job 을 진행할지(Never), 아니면 기존 파드를 재사용할지(OnFailure) 명시하는 필드이다.

restartPolicy Job 실행이 중간에 정지되었을 시 동작
Never Pod 를 재기동하지 않는 정책으로, Job 을 다시 수행하기 위해 새로운 Pod 를 기동한다.(기본값)
OnFailure 실패시 Pod 를 재기동하는 정책으로, Job 을 다시 수행하기 위해 기존 Pod 를 재기동한다.

 

아래와 같은 커맨드로 restartPolicy: Never 의 동작을 확인할 수 있다.

$ kubectl apply -f job-never-restart.yaml

$ kubectl get pods
NAME                      READY   STATUS      RESTARTS   AGE
job-never-restart-gfnx7   1/1     Running     0          8s

# 파드 내 프로세스 kill
$ kubectl exec -it job-never-restart-gfnx7 -- sh -c 'kill -9 `pgrep sleep`'

$ kubectl get pods
# 새로운 job-never-restart-xpgqb 파드가 기동됨
NAME                      READY   STATUS      RESTARTS   AGE
job-never-restart-gfnx7   0/1     Error       0          52s
job-never-restart-xpgqb   1/1     Running     0          3s

 

아래와 같은 커맨드로 restartPolicy: OnFailure 의 동작을 확인할 수 있다.

$ kubectl apply -f sample-job-onfailure-restart.yaml

$ kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
job-onfailure-restart-5rhgb   1/1     Running   0          8s

# 파드 내 프로세스 kill
$ kubectl exec -it job-onfailure-restart-5rhgb -- sh -c 'kill -9 `pgrep sleep`'

$ kubectl get pods
# job-onfailure-restart-5rhgb 파드가 restart 됨(RESTARTS: 1)
NAME                          READY   STATUS    RESTARTS     AGE
job-onfailure-restart-5rhgb   1/1     Running   1 (5s ago)   59s

 

파드가 기동하는 노드나 파드 IP 주소는 변경되지 않는다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,