Service 의 spec.ports[].targetPort 에 포트 번호를 지정하지 않고, 미리 정의해놓은 포트 이름으로 포트번호를 사용할 수도 있다. 먼저 서비스가 띄워질 파드를 정의할 때 spec.containers[].ports[] 에 포트 이름과 포트 번호를 지정하고, Service 에서는 targetPort 에 해당 포트 이름으로 설정해주면 된다. 이렇게 하면 Service 매니페스트에서는 동일한 변수명으로 포트를 지정하더라도, 실제 서비스가 띄워지는 Pod 마다 다른 포트로 연결해줄 수 있다.
아래의 매니페스트로 "http" 이름의 port 가 각각 80, 81 포트로 정의된 파드를 한개씩 띄우자
쿠버네티스에서 네트워크를 담당하는 리소스는 서비스 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 이미지는 접속정보를 응답으로 내어주는 이미지이다.
$ 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
아래 커맨드를 이용하여 생성한 서비스 정보를 확인해보면, IP 값이 해당 서비스의 IP 이고, Port 가 해당 서비스의 port 이다. Endpoints 에 명시된 IP:Port 들이 해당 서비스를 통해서 로드밸런싱 되는 대상 Pod 들의 주소값이다. 위에서 조회했던 Pod 의 IP:Port 와 동일함을 알 수 있다. selector 조건이 일치하는 Pod 의 주소를 자동으로 가져온 것이다. 본 서비스는 ClusterIP 타입이므로, 서비스의 엔드포인트가 되는 IP는 클러스터 내부에서만 접근 가능한 가상 IP임에 유의하자.
파드가 삭제된 후 다시 동일한 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 컨테이너는 호출된 파드의 호스트명을 포함한 응답을 반환하므로 쉽게 확인할 수 있다.
CronJob 은 리눅스 크론탭처럼 스케쥴된 일정 시각마다 Job 을 실행시키는 리소스이다. Job 의 또다른 형태로 보이지만, 실제로는 Deployment 와 ReplicaSet 의 관계처럼 CronJob 이 Job 을 관리하고, Job 이 Pod 를 관리하는 관계이다.
위의 매니페스트를 기반으로 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
일반적으로 크론잡은 작업 완료까지 소요되는 시간을 고려하여 충분한 시간 간격으로 스케쥴링을 해야한다. 이전에 수행된 잡이 다음 수행시점 이전에 종료되었다면 다음 수행시점에는 새로운 잡을 생성하여 실행하지만, 만약에 이전 잡이 아직 실행중이라면 새로운 잡 실행 방식에 대해서 아래와 같이 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으로 지정하면 잡은 종료 즉시 삭제된다.