1 - 서비스와 애플리케이션 연결하기

컨테이너 연결을 위한 쿠버네티스 모델

지속적으로 실행중이고, 복제된 애플리케이션을 가지고 있다면 네트워크에 노출할 수 있다.

쿠버네티스는 파드가 배치된 호스트와는 무관하게 다른 파드와 통신할 수 있다고 가정한다. 쿠버네티스는 모든 파드에게 자체 클러스터-프라이빗 IP 주소를 제공하기 때문에 파드간에 명시적으로 링크를 만들거나 컨테이너 포트를 호스트 포트에 매핑할 필요가 없다. 이것은 파드 내의 컨테이너는 모두 로컬호스트(localhost)에서 서로의 포트에 도달할 수 있으며 클러스터의 모든 파드는 NAT 없이 서로를 볼 수 있다는 의미이다. 이 문서의 나머지 부분에서는 이러한 네트워킹 모델에서 신뢰할 수 있는 서비스를 실행하는 방법에 대해 자세히 설명할 것이다.

이 튜토리얼은 간단한 nginx 서버를 사용하여 개념을 시연한다.

파드를 클러스터에 노출하기

이 작업은 이전 예시에서 수행해 보았지만, 네트워킹 관점을 중점에 두고 다시 한번 수행해 보자. nginx 파드를 생성하고, 해당 파드에 컨테이너 포트 사양이 있는 것을 참고한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

이렇게 하면 클러스터의 모든 노드에서 접근할 수 있다. 파드를 실행 중인 노드를 확인한다.

kubectl apply -f ./run-my-nginx.yaml
kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-3800858182-jr4a2   1/1       Running   0          13s       10.244.3.4    kubernetes-minion-905m
my-nginx-3800858182-kna2y   1/1       Running   0          13s       10.244.2.5    kubernetes-minion-ljyd

파드의 IP를 확인한다.

kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
    POD_IP
    [map[ip:10.244.3.4]]
    [map[ip:10.244.2.5]]

이제 클러스터의 모든 노드로 ssh 접속하거나 curl과 같은 도구를 사용하여 두 IP 주소에 질의를 전송할 수 있을 것이다. 컨테이너는 노드의 포트 80을 사용하지 않으며 , 트래픽을 파드로 라우팅하는 특별한 NAT 규칙도 없다는 것을 참고한다. 이것은 동일한 containerPort를 사용하여 동일한 노드에서 여러 nginx 파드를 실행하는 것이 가능하고, 또한 서비스에 할당된 IP 주소를 사용하여 클러스터의 다른 파드나 노드에서 접근할 수 있다는 의미이다. 호스트 노드의 특정 포트를 배후(backing) 파드로 포워드하고 싶다면, 가능은 하지만 네트워킹 모델을 사용하면 그렇게 할 필요가 없어야 한다.

만약 궁금하다면 쿠버네티스 네트워킹 모델을 자세히 읽어본다.

서비스 생성하기

이제 우리에게는 플랫(flat)하고 클러스터 전역에 걸치는 주소 공간에서 실행되고 있는 nginx 파드가 있다. 이론적으로는 이러한 파드와 직접 대화할 수 있지만, 노드가 죽으면 어떻게 되는가? 파드가 함께 죽으면 디플로이먼트에서 다른 IP를 가진 새로운 파드를 생성한다. 이 문제를 해결하는 것이 바로 서비스이다.

쿠버네티스 서비스는 클러스터 어딘가에서 실행되는 논리적인 파드 집합을 정의하고 추상화함으로써 모두 동일한 기능을 제공한다. 생성시 각 서비스에는 고유한 IP 주소(clusterIP라고도 한다)가 할당된다. 이 주소는 서비스의 수명과 연관되어 있으며, 서비스가 활성화 되어 있는 동안에는 변경되지 않는다. 파드는 서비스와 통신하도록 구성할 수 있으며, 서비스와의 통신은 서비스의 맴버 중 일부 파드에 자동적으로 로드-밸런싱 된다.

kubectl expose 를 사용해서 2개의 nginx 레플리카에 대한 서비스를 생성할 수 있다.

kubectl expose deployment/my-nginx
service/my-nginx exposed

이것은 다음 yaml 파일을 kubectl apply -f 로 실행한 것과 동일하다.

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

이 사양은 run: my-nginx 레이블이 부착된 모든 파드에 TCP 포트 80을 대상으로 하는 서비스를 만들고 추상화된 서비스 포트에 노출시킨다 (targetPort 는 컨테이너가 트래픽을 수신하는 포트, port 는 추상화된 서비스 포트로 다른 파드들이 서비스에 접속하기위해 사용하는 모든 포트일 수 있다). 서비스의 API 오브젝트를 보고 서비스 정의에서 지원되는 필드 목록을 확인한다. 서비스를 확인한다.

kubectl get svc my-nginx
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
my-nginx   ClusterIP   10.0.162.149   <none>        80/TCP    21s

앞에서 언급한 바와 같이, 서비스 밑에는 여러 파드들이 있다. 이 파드들은 엔드포인트슬라이스(EndpointSlice)를 통해 노출된다. 해당 서비스의 셀렉터는 지속적으로 평가되고, 그 결과는 레이블을 사용하여 서비스에 연결된 엔드포인트슬라이스로 POST된다. 파드가 죽으면, 해당 파드를 엔드포인트로 갖는 엔드포인트슬라이스에서 자동으로 제거된다. 신규 파드가 특정 서비스의 셀렉터에 매치되면 해당 서비스를 위한 엔드포인트슬라이스에 자동으로 추가된다. 엔드포인트를 확인해 보면, IP가 첫 번째 단계에서 생성된 파드의 IP와 동일하다는 것을 알 수 있다.

kubectl describe svc my-nginx
Name:                my-nginx
Namespace:           default
Labels:              run=my-nginx
Annotations:         <none>
Selector:            run=my-nginx
Type:                ClusterIP
IP:                  10.0.162.149
Port:                <unset> 80/TCP
Endpoints:           10.244.2.5:80,10.244.3.4:80
Session Affinity:    None
Events:              <none>
kubectl get ep my-nginx
NAME       ENDPOINTS                     AGE
my-nginx   10.244.2.5:80,10.244.3.4:80   1m

이제 클러스터의 모든 노드에서 <CLUSTER-IP>:<PORT> 로 nginx 서비스를 curl을 할 수 있을 것이다. 서비스 IP는 완전히 가상이므로 외부에서는 절대로 연결되지 않음에 참고한다. 만약 이것이 어떻게 작동하는지 궁금하다면 서비스 프록시에 대해 더 읽어본다.

서비스에 접근하기

쿠버네티스는 서비스를 찾는 두 가지 기본 모드인 환경 변수와 DNS를 지원한다. 전자는 기본적으로 작동하지만 후자는 CoreDNS 클러스터 애드온이 필요하다.

환경 변수

파드가 노드에서 실행될 때 kubelet은 각기 활성화된 서비스에 대해 일련의 환경 변수 집합을 추가한다. 이것은 순서 문제를 야기한다. 이유를 확인하려면 실행 중인 nginx 파드 환경을 점검해야 한다(실제 사용자의 파드 이름은 다를 것이다).

kubectl exec my-nginx-3800858182-jr4a2 -- printenv | grep SERVICE
KUBERNETES_SERVICE_HOST=10.0.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443

서비스에 대한 언급이 없다는 것에 참고해야 한다. 이것은 서비스 이전에 레플리카를 생성했기 때문이다. 이 작업을 수행할 때 또 다른 단점은 스케줄러가 두 파드를 모두 동일한 머신에 배치할 수도 있다는 것이며, 이로 인해 전체 서비스가 중단될 수 있다. 두개의 파드를 죽이고 디플로이먼트가 파드를 재생성하기를 기다리는 것으로 이를 정상화 할 수 있다. 이번에는 서비스가 레플리카들 에 존재한다. 이렇게 하면 올바른 환경 변수뿐만 아니라 파드의 스케줄러-수준의 서비스 분배(모든 노드에 동일한 용량이 제공되는 경우)가 된다.

kubectl scale deployment my-nginx --replicas=0; kubectl scale deployment my-nginx --replicas=2;

kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE     IP            NODE
my-nginx-3800858182-e9ihh   1/1       Running   0          5s      10.244.2.7    kubernetes-minion-ljyd
my-nginx-3800858182-j4rm4   1/1       Running   0          5s      10.244.3.8    kubernetes-minion-905m

파드가 죽고 재생성되었기 때문에 다른 이름을 가지는 것을 알 수 있다.

kubectl exec my-nginx-3800858182-e9ihh -- printenv | grep SERVICE
KUBERNETES_SERVICE_PORT=443
MY_NGINX_SERVICE_HOST=10.0.162.149
KUBERNETES_SERVICE_HOST=10.0.0.1
MY_NGINX_SERVICE_PORT=80
KUBERNETES_SERVICE_PORT_HTTPS=443

DNS

쿠버네티스는 DNS 클러스터 애드온 서비스를 제공하며 dns 이름을 다른 서비스에 자동으로 할당한다. 다음 명령어로 이것이 클러스터에서 실행 중인지 확인할 수 있다.

kubectl get services kube-dns --namespace=kube-system
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kube-dns   ClusterIP   10.0.0.10    <none>        53/UDP,53/TCP   8m

이 섹션의 나머지 부분에서는 수명이 긴 IP의 서비스(my-nginx)와 이 IP 에 이름을 할당한 DNS 서버가 있다고 가정한다. 여기서는 CoreDNS 클러스터 애드온(애플리케이션 이름 kube-dns)을 사용하므로, 표준 방법(예: gethostbyname())을 사용해서 클러스터의 모든 파드에서 서비스와 통신할 수 있다. 만약 CoreDNS가 실행 중이 아니라면 CoreDNS README 또는 CoreDNS 설치를 참조해서 활성화 할 수 있다. 이것을 테스트하기 위해 다른 curl 애플리케이션을 실행한다.

kubectl run curl --image=radial/busyboxplus:curl -i --tty
Waiting for pod default/curl-131556218-9fnch to be running, status is Pending, pod ready: false
Hit enter for command prompt

이제, nslookup my-nginx 를 입력하고 실행한다:

[ root@curl-131556218-9fnch:/ ]$ nslookup my-nginx
Server:    10.0.0.10
Address 1: 10.0.0.10

Name:      my-nginx
Address 1: 10.0.162.149

서비스 보안

지금까지는 클러스터 내부에서만 ngnix 서버에 엑세스 해왔다. 서비스를 인터넷에 공개하기 전에 통신 채널이 안전한지 확인해야 한다. 이를 위해선 다음이 필요하다.

  • https에 대한 자체 서명한 인증서 (신원 인증서를 가지고 있지 않은 경우)
  • 인증서를 사용하도록 구성된 nginx 서버
  • 파드에 접근할 수 있는 인증서를 만드는 시크릿

nginx https 예제에서 이 모든 것을 얻을 수 있다. 이를 위해서는 도구를 설치해야 한다. 만약 설치하지 않으려면 나중에 수동으로 단계를 수행한다. 한마디로:

make keys KEY=/tmp/nginx.key CERT=/tmp/nginx.crt
kubectl create secret tls nginxsecret --key /tmp/nginx.key --cert /tmp/nginx.crt
secret/nginxsecret created
kubectl get secrets
NAME                  TYPE                                  DATA      AGE
nginxsecret           kubernetes.io/tls                     2         1m

그리고 또한 컨피그맵:

kubectl create configmap nginxconfigmap --from-file=default.conf
configmap/nginxconfigmap created
kubectl get configmaps
NAME             DATA   AGE
nginxconfigmap   1      114s

다음은 make를 실행하는데 문제가 있는 경우에 수행해야 하는 수동 단계이다(예시로 windows).

# Create a public private key pair
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /d/tmp/nginx.key -out /d/tmp/nginx.crt -subj "/CN=my-nginx/O=my-nginx"
# Convert the keys to base64 encoding
cat /d/tmp/nginx.crt | base64
cat /d/tmp/nginx.key | base64

이전 명령의 출력을 사용해서 다음과 같이 yaml 파일을 생성한다. base64로 인코딩된 값은 모두 한 줄에 있어야 한다.

apiVersion: "v1"
kind: "Secret"
metadata:
  name: "nginxsecret"
  namespace: "default"
type: kubernetes.io/tls
data:
  tls.crt: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURIekNDQWdlZ0F3SUJBZ0lKQUp5M3lQK0pzMlpJTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ1l4RVRBUEJnTlYKQkFNVENHNW5hVzU0YzNaak1SRXdEd1lEVlFRS0V3aHVaMmx1ZUhOMll6QWVGdzB4TnpFd01qWXdOekEzTVRKYQpGdzB4T0RFd01qWXdOekEzTVRKYU1DWXhFVEFQQmdOVkJBTVRDRzVuYVc1NGMzWmpNUkV3RHdZRFZRUUtFd2h1CloybHVlSE4yWXpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBSjFxSU1SOVdWM0IKMlZIQlRMRmtobDRONXljMEJxYUhIQktMSnJMcy8vdzZhU3hRS29GbHlJSU94NGUrMlN5ajBFcndCLzlYTnBwbQppeW1CL3JkRldkOXg5UWhBQUxCZkVaTmNiV3NsTVFVcnhBZW50VWt1dk1vLzgvMHRpbGhjc3paenJEYVJ4NEo5Ci82UVRtVVI3a0ZTWUpOWTVQZkR3cGc3dlVvaDZmZ1Voam92VG42eHNVR0M2QURVODBpNXFlZWhNeVI1N2lmU2YKNHZpaXdIY3hnL3lZR1JBRS9mRTRqakxCdmdONjc2SU90S01rZXV3R0ljNDFhd05tNnNTSzRqYUNGeGpYSnZaZQp2by9kTlEybHhHWCtKT2l3SEhXbXNhdGp4WTRaNVk3R1ZoK0QrWnYvcW1mMFgvbVY0Rmo1NzV3ajFMWVBocWtsCmdhSXZYRyt4U1FVQ0F3RUFBYU5RTUU0d0hRWURWUjBPQkJZRUZPNG9OWkI3YXc1OUlsYkROMzhIYkduYnhFVjcKTUI4R0ExVWRJd1FZTUJhQUZPNG9OWkI3YXc1OUlsYkROMzhIYkduYnhFVjdNQXdHQTFVZEV3UUZNQU1CQWY4dwpEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRVhTMW9FU0lFaXdyMDhWcVA0K2NwTHI3TW5FMTducDBvMm14alFvCjRGb0RvRjdRZnZqeE04Tzd2TjB0clcxb2pGSW0vWDE4ZnZaL3k4ZzVaWG40Vm8zc3hKVmRBcStNZC9jTStzUGEKNmJjTkNUekZqeFpUV0UrKzE5NS9zb2dmOUZ3VDVDK3U2Q3B5N0M3MTZvUXRUakViV05VdEt4cXI0Nk1OZWNCMApwRFhWZmdWQTRadkR4NFo3S2RiZDY5eXM3OVFHYmg5ZW1PZ05NZFlsSUswSGt0ejF5WU4vbVpmK3FqTkJqbWZjCkNnMnlwbGQ0Wi8rUUNQZjl3SkoybFIrY2FnT0R4elBWcGxNSEcybzgvTHFDdnh6elZPUDUxeXdLZEtxaUMwSVEKQ0I5T2wwWW5scE9UNEh1b2hSUzBPOStlMm9KdFZsNUIyczRpbDlhZ3RTVXFxUlU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
  tls.key: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2RhaURFZlZsZHdkbFIKd1V5eFpJWmVEZWNuTkFhbWh4d1NpeWF5N1AvOE9ta3NVQ3FCWmNpQ0RzZUh2dGtzbzlCSzhBZi9WemFhWm9zcApnZjYzUlZuZmNmVUlRQUN3WHhHVFhHMXJKVEVGSzhRSHA3VkpMcnpLUC9QOUxZcFlYTE0yYzZ3MmtjZUNmZitrCkU1bEVlNUJVbUNUV09UM3c4S1lPNzFLSWVuNEZJWTZMMDUrc2JGQmd1Z0ExUE5JdWFubm9UTWtlZTRuMG4rTDQKb3NCM01ZUDhtQmtRQlAzeE9JNHl3YjREZXUraURyU2pKSHJzQmlIT05Xc0RadXJFaXVJMmdoY1kxeWIyWHI2UAozVFVOcGNSbC9pVG9zQngxcHJHclk4V09HZVdPeGxZZmcvbWIvNnBuOUYvNWxlQlkrZStjSTlTMkQ0YXBKWUdpCkwxeHZzVWtGQWdNQkFBRUNnZ0VBZFhCK0xkbk8ySElOTGo5bWRsb25IUGlHWWVzZ294RGQwci9hQ1Zkank4dlEKTjIwL3FQWkUxek1yall6Ry9kVGhTMmMwc0QxaTBXSjdwR1lGb0xtdXlWTjltY0FXUTM5SjM0VHZaU2FFSWZWNgo5TE1jUHhNTmFsNjRLMFRVbUFQZytGam9QSFlhUUxLOERLOUtnNXNrSE5pOWNzMlY5ckd6VWlVZWtBL0RBUlBTClI3L2ZjUFBacDRuRWVBZmI3WTk1R1llb1p5V21SU3VKdlNyblBESGtUdW1vVlVWdkxMRHRzaG9reUxiTWVtN3oKMmJzVmpwSW1GTHJqbGtmQXlpNHg0WjJrV3YyMFRrdWtsZU1jaVlMbjk4QWxiRi9DSmRLM3QraTRoMTVlR2ZQegpoTnh3bk9QdlVTaDR2Q0o3c2Q5TmtEUGJvS2JneVVHOXBYamZhRGR2UVFLQmdRRFFLM01nUkhkQ1pKNVFqZWFKClFGdXF4cHdnNzhZTjQyL1NwenlUYmtGcVFoQWtyczJxWGx1MDZBRzhrZzIzQkswaHkzaE9zSGgxcXRVK3NHZVAKOWRERHBsUWV0ODZsY2FlR3hoc0V0L1R6cEdtNGFKSm5oNzVVaTVGZk9QTDhPTm1FZ3MxMVRhUldhNzZxelRyMgphRlpjQ2pWV1g0YnRSTHVwSkgrMjZnY0FhUUtCZ1FEQmxVSUUzTnNVOFBBZEYvL25sQVB5VWs1T3lDdWc3dmVyClUycXlrdXFzYnBkSi9hODViT1JhM05IVmpVM25uRGpHVHBWaE9JeXg5TEFrc2RwZEFjVmxvcG9HODhXYk9lMTAKMUdqbnkySmdDK3JVWUZiRGtpUGx1K09IYnRnOXFYcGJMSHBzUVpsMGhucDBYSFNYVm9CMUliQndnMGEyOFVadApCbFBtWmc2d1BRS0JnRHVIUVV2SDZHYTNDVUsxNFdmOFhIcFFnMU16M2VvWTBPQm5iSDRvZUZKZmcraEppSXlnCm9RN3hqWldVR3BIc3AyblRtcHErQWlSNzdyRVhsdlhtOElVU2FsbkNiRGlKY01Pc29RdFBZNS9NczJMRm5LQTQKaENmL0pWb2FtZm1nZEN0ZGtFMXNINE9MR2lJVHdEbTRpb0dWZGIwMllnbzFyb2htNUpLMUI3MkpBb0dBUW01UQpHNDhXOTVhL0w1eSt5dCsyZ3YvUHM2VnBvMjZlTzRNQ3lJazJVem9ZWE9IYnNkODJkaC8xT2sybGdHZlI2K3VuCnc1YytZUXRSTHlhQmd3MUtpbGhFZDBKTWU3cGpUSVpnQWJ0LzVPbnlDak9OVXN2aDJjS2lrQ1Z2dTZsZlBjNkQKckliT2ZIaHhxV0RZK2Q1TGN1YSt2NzJ0RkxhenJsSlBsRzlOZHhrQ2dZRUF5elIzT3UyMDNRVVV6bUlCRkwzZAp4Wm5XZ0JLSEo3TnNxcGFWb2RjL0d5aGVycjFDZzE2MmJaSjJDV2RsZkI0VEdtUjZZdmxTZEFOOFRwUWhFbUtKCnFBLzVzdHdxNWd0WGVLOVJmMWxXK29xNThRNTBxMmk1NVdUTThoSDZhTjlaMTltZ0FGdE5VdGNqQUx2dFYxdEYKWSs4WFJkSHJaRnBIWll2NWkwVW1VbGc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"

이제 파일을 사용해서 시크릿을 생성한다.

kubectl apply -f nginxsecrets.yaml
kubectl get secrets
NAME                  TYPE                                  DATA      AGE
nginxsecret           kubernetes.io/tls                     2         1m

이제 nginx 레플리카를 수정하여 암호화된 인증서를 사용한 https 서버와 서비스를 실행하고, 두 포트(80과 443)를 노출한다.

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    protocol: TCP
    name: https
  selector:
    run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      - name: configmap-volume
        configMap:
          name: nginxconfigmap
      containers:
      - name: nginxhttps
        image: bprashanth/nginxhttps:1.0
        ports:
        - containerPort: 443
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
        - mountPath: /etc/nginx/conf.d
          name: configmap-volume

nginx-secure-app의 매니페스트에 대한 주목할만한 점:

  • 이것은 동일한 파일에 디플로이먼트와 서비스의 사양을 모두 포함하고 있다.
  • nginx 서버 는 포트 80에서 HTTP 트래픽을 443에서 HTTPS 트래픽 서비스를 제공하고, nginx 서비스는 두 포트를 모두 노출한다.
  • 각 컨테이너는 /etc/nginx/ssl 에 마운트된 볼륨을 통해 키에 접근할 수 있다. 이것은 nginx 서버가 시작되기 전에 설정된 것이다.
kubectl delete deployments,svc my-nginx; kubectl create -f ./nginx-secure-app.yaml

이 시점에서 모든 노드에서 nginx 서버에 연결할 수 있다.

kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
    POD_IP
    [map[ip:10.244.3.5]]
node $ curl -k https://10.244.3.5
...
<h1>Welcome to nginx!</h1>

마지막 단계에서 curl에 -k 파라미터를 제공한 방법에 참고한다. 이는 인증서 생성시 nginx를 실행하는 파드에 대해 아무것도 모르기 때문에 curl에 CName 불일치를 무시하도록 지시해야하기 때문이다. 서비스를 생성해서 인증서에 사용된 CName을 서비스 조회시 파드에서 사용된 실제 DNS 이름과 연결했다. 파드에서 이것을 테스트 해보자(단순히 동일한 시크릿이 재사용되고 있으며, 파드는 서비스에 접근하기위해 nginx.crt만 필요하다).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: curl-deployment
spec:
  selector:
    matchLabels:
      app: curlpod
  replicas: 1
  template:
    metadata:
      labels:
        app: curlpod
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: curlpod
        command:
        - sh
        - -c
        - while true; do sleep 1; done
        image: radial/busyboxplus:curl
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
kubectl apply -f ./curlpod.yaml
kubectl get pods -l app=curlpod
NAME                               READY     STATUS    RESTARTS   AGE
curl-deployment-1515033274-1410r   1/1       Running   0          1m
kubectl exec curl-deployment-1515033274-1410r -- curl https://my-nginx --cacert /etc/nginx/ssl/tls.crt
...
<title>Welcome to nginx!</title>
...

서비스 노출하기

애플리케이션의 일부인 경우 원한다면 외부 IP 주소에 서비스를 노출할 수 있다. 쿠버네티스는 이를 수행하는 2가지 방법인 NodePorts와 LoadBalancers를지원한다. 마지막 섹션에서 생성된 서비스는 이미 NodePort 를 사용했기에 노드에 공용 IP가 있는경우 nginx HTTPS 레플리카가 인터넷 트래픽을 처리할 준비가 되어 있다.

kubectl get svc my-nginx -o yaml | grep nodePort -C 5
  uid: 07191fb3-f61a-11e5-8ae5-42010af00002
spec:
  clusterIP: 10.0.162.149
  ports:
  - name: http
    nodePort: 31704
    port: 8080
    protocol: TCP
    targetPort: 80
  - name: https
    nodePort: 32453
    port: 443
    protocol: TCP
    targetPort: 443
  selector:
    run: my-nginx
kubectl get nodes -o yaml | grep ExternalIP -C 1
    - address: 104.197.41.11
      type: ExternalIP
    allocatable:
--
    - address: 23.251.152.56
      type: ExternalIP
    allocatable:
...

$ curl https://<EXTERNAL-IP>:<NODE-PORT> -k
...
<h1>Welcome to nginx!</h1>

이제 클라우드 로드 밸런서를 사용하도록 서비스를 재생성한다. my-nginx 서비스의 TypeNodePort 에서 LoadBalancer 로 변경한다.

kubectl edit svc my-nginx
kubectl get svc my-nginx
NAME       TYPE           CLUSTER-IP     EXTERNAL-IP        PORT(S)               AGE
my-nginx   LoadBalancer   10.0.162.149   xx.xxx.xxx.xxx     8080:30163/TCP        21s
curl https://<EXTERNAL-IP> -k
...
<title>Welcome to nginx!</title>

EXTERNAL-IP 의 IP 주소는 공용 인터넷에서 이용할 수 있는 주소이다. CLUSTER-IP 는 클러스터/프라이빗 클라우드 네트워크 내에서만 사용할 수 있다.

AWS에서는 LoadBalancer 유형은 IP가 아닌 (긴)호스트네임을 사용하는 ELB를 생성한다는 점을 참고한다. 이것은 일반적인 kubectl get svc 의 출력에 맞추기에는 매우 길기 때문에 실제로 이를 보려면 kubectl describe service my-nginx 를 수행해야 한다. 다음과 같은 것을 보게 된다.

kubectl describe service my-nginx
...
LoadBalancer Ingress:   a320587ffd19711e5a37606cf4a74574-1142138393.us-east-1.elb.amazonaws.com
...

다음 내용

2 - 소스 IP 주소 이용하기

쿠버네티스 클러스터에서 실행 중인 애플리케이션은 서비스 추상화를 통해서 서로를, 그리고 외부 세계를 찾고 통신한다. 이 문서는 다른 종류의 서비스로 전송된 패킷의 소스 IP에 어떤 일이 벌어지는지와 이 동작을 필요에 따라 어떻게 전환할 수 있는지 설명한다.

시작하기 전에

용어

이 문서는 다음 용어를 사용한다.

NAT
네트워크 주소 변환
소스 NAT
패킷 상의 소스 IP 주소를 변경하는 것. 이 페이지에서는 일반적으로 노드 IP 주소로의 변경을 의미함.
대상 NAT
패킷 상의 대상 IP 주소를 변경하는 것. 이 페이지에서는 일반적으로 파드 IP 주소로의 변경을 의미함.
VIP
쿠버네티스의 모든 서비스에 할당되어 있는 것과 같은, 가상 IP 주소.
Kube-proxy
모든 노드에서 서비스 VIP 관리를 조율하는 네트워크 데몬.

전제 조건

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

이 예시는 HTTP 헤더로 수신한 요청의 소스 IP 주소를 회신하는 작은 nginx 웹 서버를 이용한다. 다음과 같이 생성할 수 있다.

kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.4

출력은 다음과 같다.

deployment.apps/source-ip-app created

목적

  • 간단한 애플리케이션을 다양한 서비스 종류로 노출하기
  • 각 서비스 유형에 따른 소스 IP NAT 의 동작 이해하기
  • 소스 IP 주소 보존에 관한 절충 사항 이해

Type=ClusterIP 인 서비스에서 소스 IP

iptables 모드 (기본값)에서 kube-proxy를 운영하는 경우 클러스터 내에서 클러스터IP로 패킷을 보내면 소스 NAT를 통과하지 않는다. kube-proxy가 실행중인 노드에서 http://localhost:10249/proxyMode 를 입력해서 kube-proxy 모드를 조회할 수 있다.

kubectl get nodes

출력은 다음과 유사하다.

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

한 노드의 프록시 모드를 확인한다. (kube-proxy는 포트 10249에서 수신대기한다.)

# 질의 할 노드의 쉘에서 이것을 실행한다.
curl localhost:10249/proxyMode

출력은 다음과 같다.

iptables

소스 IP 애플리케이션을 통해 서비스를 생성하여 소스 IP 주소 보존 여부를 테스트할 수 있다.

kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

출력은 다음과 같다.

service/clusterip exposed
kubectl get svc clusterip

출력은 다음과 같다.

NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
clusterip    ClusterIP   10.0.170.92   <none>        80/TCP    51s

그리고 동일한 클러스터의 파드에서 클러스터IP를 치면:

kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm

출력은 다음과 같다.

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

그런 다음 해당 파드 내에서 명령을 실행할 수 있다.

# "kubectl run" 으로 터미널 내에서 이것을 실행한다.
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

그런 다음 wget 을 사용해서 로컬 웹 서버에 쿼리한다.

# "10.0.170.92"를 "clusterip"라는 이름의 서비스의 IPv4 주소로 변경한다.
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

client_address 는 클라이언트 파드와 서버 파드가 같은 노드 또는 다른 노드에 있는지 여부에 관계없이 항상 클라이언트 파드의 IP 주소이다.

Type=NodePort 인 서비스에서 소스 IP

Type=NodePort인 서비스로 보내진 패킷은 소스 NAT가 기본으로 적용된다. NodePort 서비스를 생성하여 이것을 테스트할 수 있다.

kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

출력은 다음과 같다.

service/nodeport exposed
NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)
NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')

클라우드 공급자 상에서 실행한다면, 위에 보고된 nodes:nodeport를 위한 방화벽 규칙을 열어주어야 한다. 이제 위에 노드 포트로 할당받은 포트를 통해 클러스터 외부에서 서비스에 도달할 수 있다.

for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

출력은 다음과 유사하다.

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

명심할 것은 정확한 클라이언트 IP 주소가 아니고, 클러스터 내부 IP 주소이다. 왜 이런 일이 발생했는지 설명한다.

  • 클라이언트는 node2:nodePort로 패킷을 보낸다.
  • node2는 소스 IP 주소(SNAT)를 패킷 상에서 자신의 IP 주소로 교체한다.
  • noee2는 대상 IP를 패킷 상에서 파드의 IP로 교체한다.
  • 패킷은 node 1로 라우팅 된 다음 엔드포인트로 라우팅 된다.
  • 파드의 응답은 node2로 다시 라우팅된다.
  • 파드의 응답은 클라이언트로 다시 전송된다.

이를 그림으로 표현하면 다음과 같다.

source IP nodeport figure 01

그림. Source IP Type=NodePort using SNAT

이를 피하기 위해 쿠버네티스는 클라이언트 소스 IP 주소를 보존하는 기능이 있다. service.spec.externalTrafficPolicy 의 값을 Local 로 하면 오직 로컬 엔드포인트로만 프록시 요청하고 다른 노드로 트래픽 전달하지 않는다. 이 방법은 원본 소스 IP 주소를 보존한다. 만약 로컬 엔드 포인트가 없다면, 그 노드로 보내진 패킷은 버려지므로 패킷 처리 규칙에서 정확한 소스 IP 임을 신뢰할 수 있으므로, 패킷을 엔드포인트까지 전달할 수 있다.

다음과 같이 service.spec.externalTrafficPolicy 필드를 설정하자.

kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

출력은 다음과 같다.

service/nodeport patched

이제 다시 테스트를 실행해보자.

for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

출력은 다음과 유사하다.

client_address=104.132.1.79

엔드포인트 파드가 실행 중인 노드에서 올바른 클라이언트 IP 주소인 딱 한 종류의 응답만 수신한다.

어떻게 이렇게 되었는가:

  • 클라이언트는 패킷을 엔드포인트가 없는 node2:nodePort 보낸다.
  • 패킷은 버려진다.
  • 클라이언트는 패킷을 엔드포인트를 가진 node1:nodePort 보낸다.
  • node1은 패킷을 올바른 소스 IP 주소로 엔드포인트로 라우팅 한다.

이를 시각적으로 표현하면 다음과 같다.

source IP nodeport figure 02

그림. Source IP Type=NodePort preserves client source IP address

Type=LoadBalancer 인 서비스에서 소스 IP

Type=LoadBalancer인 서비스로 보낸 패킷은 소스 NAT를 기본으로 하는데, Ready 상태로 모든 스케줄된 모든 쿠버네티스 노드는 로드 밸런싱 트래픽에 적합하다. 따라서 엔드포인트가 없는 노드에 패킷이 도착하면 시스템은 엔드포인트를 포함한 노드에 프록시를 수행하고 패킷 상에서 노드의 IP 주소로 소스 IP 주소를 변경한다 (이전 섹션에서 기술한 것처럼).

로드밸런서를 통해 source-ip-app을 노출하여 테스트할 수 있다.

kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

출력은 다음과 같다.

service/loadbalancer exposed

서비스의 IP 주소를 출력한다.

kubectl get svc loadbalancer

다음과 유사하게 출력된다.

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)   AGE
loadbalancer   LoadBalancer   10.0.65.118   203.0.113.140     80/TCP    5m

다음으로 이 서비스의 외부 IP에 요청을 전송한다.

curl 203.0.113.140

다음과 유사하게 출력된다.

CLIENT VALUES:
client_address=10.240.0.5
...

그러나 구글 클라우드 엔진/GCE 에서 실행 중이라면 동일한 service.spec.externalTrafficPolicy 필드를 Local로 설정하면 서비스 엔드포인트가 없는 노드는 고의로 헬스 체크에 실패하여 강제로 로드밸런싱 트래픽을 받을 수 있는 노드 목록에서 자신을 스스로 제거한다.

이를 그림으로 표현하면 다음과 같다.

Source IP with externalTrafficPolicy

이것은 어노테이션을 설정하여 테스트할 수 있다.

kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

쿠버네티스에 의해 service.spec.healthCheckNodePort 필드가 즉각적으로 할당되는 것을 봐야 한다.

kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

출력은 다음과 유사하다.

  healthCheckNodePort: 32122

service.spec.healthCheckNodePort 필드는 /healthz에서 헬스 체크를 제공하는 모든 노드의 포트를 가리킨다. 이것을 테스트할 수 있다.

kubectl get pod -o wide -l app=source-ip-app

출력은 다음과 유사하다.

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

다양한 노드에서 /healthz 엔드포인트를 가져오려면 curl 을 사용한다.

# 선택한 노드에서 로컬로 이것을 실행한다.
curl localhost:32122/healthz
1 Service Endpoints found

다른 노드에서는 다른 결과를 얻을 수 있다.

# 선택한 노드에서 로컬로 이것을 실행한다.
curl localhost:32122/healthz
No Service Endpoints Found

컨트롤 플레인에서 실행중인 컨트롤러는 클라우드 로드 밸런서를 할당한다. 또한 같은 컨트롤러는 각 노드에서 포트/경로(port/path)를 가르키는 HTTP 상태 확인도 할당한다. 엔드포인트가 없는 2개의 노드가 상태 확인에 실패할 때까지 약 10초간 대기한 다음, curl 을 사용해서 로드밸런서의 IPv4 주소를 쿼리한다.

curl 203.0.113.140

출력은 다음과 유사하다.

CLIENT VALUES:
client_address=198.51.100.79
...

크로스-플랫폼 지원

일부 클라우드 공급자만 Type=LoadBalancer 를 사용하는 서비스를 통해 소스 IP 보존을 지원한다. 실행 중인 클라우드 공급자에서 몇 가지 다른 방법으로 로드밸런서를 요청한다.

  1. 클라이언트 연결을 종료하고 새 연결을 여는 프록시를 이용한다. 이 경우 소스 IP 주소는 클라이언트 IP 주소가 아니고 항상 클라우드 로드밸런서의 IP 주소이다.

  2. 로드밸런서의 VIP에 전달된 클라이언트가 보낸 요청을 중간 프록시가 아닌 클라이언트 소스 IP 주소가 있는 노드로 끝나는 패킷 전달자를 이용한다.

첫 번째 범주의 로드밸런서는 진짜 클라이언트 IP를 통신하기 위해 HTTP Forwarded 또는 X-FORWARDED-FOR 헤더 또는 프록시 프로토콜과 같은 로드밸런서와 백엔드 간에 합의된 프로토콜을 사용해야 한다. 두 번째 범주의 로드밸런서는 서비스의 service.spec.healthCheckNodePort 필드의 저장된 포트를 가르키는 HTTP 헬스 체크를 생성하여 위에서 설명한 기능을 활용할 수 있다.

정리하기

서비스를 삭제한다.

kubectl delete svc -l app=source-ip-app

디플로이먼트, 레플리카셋 그리고 파드를 삭제한다.

kubectl delete deployment source-ip-app

다음 내용