Kubernetes – Service

기본개념

Pod은 존재하는 동안 생성과 소멸을 반복합니다. 생성 할 때마다 IP 정보와 이름이 바뀌어서 추적하는데 어려움이 있습니다. 따라서, 변화하는 pod에 접근하기 위한 interface가 필요했고, 이것이 바로 service입니다. ( 대부분의 예제는 Kubernetes in Action의 내용을 활용하였습니다. )

# replicaset.yaml
apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
  name: kubia
spec:
  replicas: 3
  selector:
    matchLabels:
      app: kubia
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia
# kubia.js
const http = require('http');
const os = require('os');
 
console.log("Kubia server starting...");
 
var handler = function(request, response) {
  console.log("Received request from " + request.connection.remoteAddress);
  response.writeHead(200);
  response.end("You've hit " + os.hostname() + "\n");
};
 
var www = http.createServer(handler);
www.listen(8080);

Service가 생성되면 ClusterIP라는 것이 할당됩니다. 이것은 Cluster 내부에서만 통신이 가능한 가상의 IP로, “type: ExternalName”으로 생성시에는 ClusterIP가 보이지 않습니다. ExternalName은 service를 특정 host에 mapping 시킵니다. ( ExternalName: https://kubernetes.io/ko/docs/concepts/services-networking/service/#externalname )

Service은 selector의 label을 통해서 endpoint가 될 pod을 선택합니다. 위의 예시에서는 “app: kubia” label을 갖는 pod을 선택하여 endpoint로 등록합니다. Endpoint란 service에 요청이 올 때, 요청을 처리할 pod에 대한 정보를 담고 있는 object입니다.

문제점

Curl로 service의 ClusterIP에 요청을 하면, 매번 요청 받는 pod이 바뀌는 것을 확인할 수 있습니다. 요청 받는 pod이 계속해서 바뀌기 때문에 service를 통해서 요청을 하는 경우 MongoDB 등과 같은 application을 사용할 때 문제가 발생할 수 있습니다. ( Curl로 ClusterIP에 요청하는 것은 pod 내부나 cluster에 소속된 node에 접속한 상태에서만 가능합니다. 앞서 말했듯이 ClusterIP는 cluster 내부에서만 통신이 가능한 가상의 IP이기 때문입니다. )

# Replicaset
apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
  name: mongo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mongo
  template:
    metadata:
      labels:
        app: mongo
    spec:
      containers:
      - name: mongo
        image: mongo
# Service
apiVersion: v1
kind: Service
metadata:
  name: mongo
spec:
  type: NodePort
  ports:
  - port: 27017
    targetPort: 27017
    nodePort: 30809
  selector:
    app: mongo
# input.py
import pymongo
from pymongo import MongoClient
 
client = MongoClient('mongodb://test-worker-1:30800/')
db = client.kube
tests = db.kube
 
tests.insert_one({'test': 'I got data'})
# output.py
import pymongo
from pymongo import MongoClient
 
client = MongoClient('mongodb://test-worker-1:30800/')
db = client.kube
tests = db.kube
 
try :
    print(tests.find({})[0]['test'])
except :
    print('data not found')

“replicas: 3″을 통해 pod 3개를 띄운 상태입니다. Service를 통해서 MongoDB에 data를 저장할 경우 endpoint 중 하나에 임의로 저장이 되게 됩니다. 이후 data를 읽어들일 때 data가 저장된 pod이 선택되지 않는다면 저장된 data를 읽을 수 없게 됩니다.

유사한 상황으로, web server를 Kubernetes에 띄우고 server가 session key를 통해서 client와 통신하는 상태에서, session key를 저장하는 공간을 database가 아닌 memory에 담을 시 동일한 현상이 발생할 수 있습니다.

위 문제의 완벽한 해결책은 아니지만, client ip를 토대로 동일한 pod에 요청을 보내는 것이 가능합니다. Service의 sessionAffinity: None을 ClientIP로 변경 시에, 동일한 pod으로 요청이 가도록 하는 방법입니다.

접근방법

새로운 service가 생성될 때, pod에 해당 service의 정보를 매번 등록하는 것은 번거롭기 때문에 Kubernetes에서는 ENV, DNS를 통해서 pod이 service 정보를 알게합니다. Pod에서 env | grep -i service를 통해 확인할 수 있습니다. DNS의 경우에는 (service명).(service가 속한 namespace).(설정가능한 부분)으로 service가 등록이 되어 있으며, 해당 pod이 동일 namespace에 속해 있다면 단순히 curl (service 명)을 통해서 요청이 가능함.

내부 pod을 endpoint로 지정 경우 말고, 외부에 있는 host를 pod으로 지정하는 방법도 있습니다. 앞서 설명한 ExternalName을 사용하는 것입니다.

apiVersion: v1
kind: Service
metadata:
  name: nginx-external
spec:
  type: ExternalName
  externalName: test01.dakao.io
  ports:
  - port: 80

NodePort란 cluster내의 node에 특정 service에 mapping된 port를 열어놓는 방식입니다. 이 때 node로 들어온 요청은 kube-proxy가 iptables에 등록한 내용에 의해 routing됩니다.

심화

kube-proxy가 등록하는 것에 대한 log는 docker logs k8s_kube-proxy_kube-proxy-(해당 서버에서 찾아야함)를 통해 확인할 수 있으며, 새로운 service(kubia)를 생성 시에, 아래와 같이 보여줍니다.

I0430 14:14:38.582251       1 service.go:309] Adding new service port "default/kubia:" at 10.231.55.33:80/TCP

iptables에서 iptables -t nat -L -n | grep -i kubia, iptables -t nat -S | grep -i kubia, 등을 통해서 좀 더 상세한 분석이 가능합니다.

구조

Kubernetes에서는 NodePort, Port, TargetPort, ConainerPort등이 존재하는데, 아래의 순서로 요청이 이루어집니다.

Pod에 지정된 containerPort는 ‘해당 pod의 TargetPort로 요청이 오면 containerPort로 요청을 보내주세요’의 의미입니다. containerPort 번호로 열려있는 container가 존재하지 않으면 요청은 실패하게 됩니다. 추가로 주의할 점은 TargetPort를 지정하지 않을 시 Port에 할당된 번호가 자동으로 TargetPort로 할당 된다는 것입니다.