Service로 클러스터 안의 애플리케이션에 접근하기

Service로 클러스터 안의 애플리케이션에 접근하기 (Connect Applications with Services)

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

이제 지속적으로 실행되고 복제된 애플리케이션이 있으니 네트워크에 노출할 수 있어요.

쿠버네티스는 파드가 어떤 호스트에 있든 다른 파드와 통신할 수 있다고 가정합니다. 쿠버네티스는 모든 파드에 자신만의 클러스터 전용 IP 주소를 제공하므로, 파드 사이에 링크를 명시적으로 만들거나 컨테이너 포트를 호스트 포트에 매핑할 필요가 없습니다. 이는 파드 안의 컨테이너가 모두 localhost의 서로 다른 포트에 도달할 수 있고, 클러스터의 모든 파드가 NAT 없이 서로를 볼 수 있음을 의미합니다. 이 문서의 나머지 부분은 그러한 네트워킹 모델에서 어떻게 신뢰할 수 있는 서비스를 실행할 수 있는지 자세히 설명해요.

이 튜토리얼은 개념을 시연하기 위해 간단한 nginx 웹 서버를 사용합니다.

출처: 문서

본문

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

이전 예시에서 했지만, 네트워킹 관점에 초점을 맞춰 다시 한번 해봅시다. nginx 파드를 만들고, 컨테이너 포트 명세가 있음을 확인하세요:

이것은 클러스터의 어떤 노드에서든 접근 가능하게 만듭니다. 파드가 실행 중인 노드를 확인한다:

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 주소로 클러스터의 다른 파드나 노드에서 접근할 수 있습니다. 호스트 노드의 특정 포트를 백킹 파드로 전달하도록 구성하고 싶다면 할 수도 있지만, 네트워킹 모델상 그렇게 할 필요는 없습니다.

궁금하다면 쿠버네티스 네트워킹 모델에 대해 더 읽을 수 있습니다.

Service 만들기

그래서 평면적이고 클러스터 전체의 주소 공간에서 nginx를 실행하는 파드가 있습니다. 이론적으로는 이 파드에 직접 말할 수 있지만, 노드가 죽으면 어떻게 될까요? 파드도 함께 죽고, Deployment 안의 ReplicaSet이 다른 IP로 새 파드를 만들 것입니다. 이것이 Service가 해결하는 문제입니다.

쿠버네티스 Service는 클러스터 어딘가에서 실행 중이고 모두 같은 기능을 제공하는 논리적 파드 집합을 정의하는 추상화입니다. 생성되면 각 Service에 고유한 IP 주소(clusterIP라고도 함)가 할당됩니다. 이 주소는 Service의 수명에 묶여 있으며 Service가 살아있는 동안 변하지 않습니다. 파드는 Service에 말하도록 구성될 수 있고, Service에 대한 통신은 Service의 구성원인 어떤 파드에 자동으로 로드 밸런싱된다는 것을 알 수 있습니다.

kubectl expose로 2개의 nginx replica에 대한 Service를 만들 수 있습니다:

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

이것은 다음 yaml의 kubectl apply -f와 같습니다:

이 명세는 run: my-nginx 라벨이 있는 어떤 파드의 TCP 80 포트를 대상으로 하는 Service를 만들고, 추상화된 Service 포트(targetPort: 컨테이너가 트래픽을 받는 포트, port: 다른 파드가 Service에 접근하는 데 사용하는 어떤 포트든 될 수 있는 추상화된 Service 포트)에 노출합니다. 서비스 정의에서 지원되는 필드 목록을 보려면 Service API 객체를 보세요.

Service를 확인한다:

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

앞서 언급했듯이 Service는 파드 그룹에 의해 뒷받침됩니다. 이러한 파드는 EndpointSlice를 통해 노출됩니다. Service의 선택자는 지속적으로 평가되며 결과는 Service에 연결된 EndpointSlice에 POST됩니다. 파드가 죽으면 엔드포인트로 포함된 EndpointSlice에서 자동으로 제거됩니다. Service의 선택자와 일치하는 새 파드는 그 Service의 EndpointSlice에 자동으로 추가됩니다.

엔드포인트를 확인하고, IP가 첫 단계에서 만든 파드와 같다는 점에 주의하세요:

kubectl describe svc my-nginx
Name:                my-nginx
Namespace:           default
Labels:              run=my-nginx
Annotations:         <none>
Selector:            run=my-nginx
Type:                ClusterIP
IP Family Policy:    SingleStack
IP Families:         IPv4
IP:                  10.0.162.149
IPs:                 10.0.162.149
Port:                <unset> 80/TCP
TargetPort:          80/TCP
Endpoints:           10.244.2.5:80,10.244.3.4:80
Session Affinity:    None
Events:              <none>
kubectl get endpointslices -l kubernetes.io/service-name=my-nginx
NAME             ADDRESSTYPE   PORTS   ENDPOINTS               AGE
my-nginx-7vzhx   IPv4          80      10.244.2.5,10.244.3.4   21s

이제 클러스터의 어떤 노드에서든 <CLUSTER-IP>:<PORT>로 nginx Service에 curl할 수 있어야 합니다. Service IP는 완전히 가상이며 실제 선(wire)에 닿지 않는다는 점에 주의하세요. 이것이 어떻게 동작하는지 궁금하다면 service proxy에 대해 더 읽을 수 있습니다.

Service에 접근하기

쿠버네티스는 Service를 찾는 2가지 기본 모드를 지원합니다 — 환경 변수와 DNS입니다. 전자는 기본적으로 동작하고 후자는 CoreDNS cluster addon이 필요합니다.

서비스 환경 변수가 바람직하지 않다면(예상 프로그램과 충돌 가능성, 처리할 변수가 너무 많음, DNS만 사용 등) pod spec에서 enableServiceLinks 플래그를 false로 설정해 이 모드를 비활성화할 수 있습니다.

환경 변수

파드가 노드에서 실행되면 kubelet이 각 활성 Service에 대한 환경 변수 집합을 추가합니다. 이는 순서 문제를 도입합니다. 이유를 보려면 실행 중인 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

Service에 대한 언급이 없는 점에 주의하세요. replica를 Service보다 먼저 만들었기 때문입니다. 이렇게 하는 또 다른 단점은 스케줄러가 두 파드를 같은 머신에 두어, 그 머신이 죽으면 전체 Service가 다운될 수 있다는 것입니다. 2개의 파드를 죽이고 Deployment가 다시 만들 때까지 기다리면 올바른 방법으로 할 수 있습니다. 이번에는 replica보다 먼저 Service가 존재합니다. 이렇게 하면 (모든 노드의 용량이 같다면) 스케줄러 수준의 Service 확산과 올바른 환경 변수를 얻을 수 있습니다:

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

쿠버네티스는 다른 Service에 dns 이름을 자동으로 할당하는 DNS 클러스터 애드온 Service를 제공합니다. 클러스터에서 실행 중인지 확인할 수 있어요:

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)를 가진 Service와 그 IP에 이름을 할당한 DNS 서버가 있다고 가정합니다. 여기서는 CoreDNS 클러스터 애드온(애플리케이션 이름 kube-dns)을 사용하므로 표준 방법(예: gethostbyname())으로 클러스터의 어떤 파드에서든 Service에 말할 수 있습니다. CoreDNS가 실행 중이지 않다면 CoreDNS README 또는 CoreDNS 설치를 참조해 활성화할 수 있습니다.

이것을 테스트하기 위해 또 다른 curl 애플리케이션을 실행해 봅시다:

kubectl run curl --image=radial/busyboxplus:curl -i --tty --rm
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

Service 보안 유지하기

지금까지는 클러스터 안에서만 nginx 서버에 접근했습니다. Service를 인터넷에 노출하기 전에 통신 채널이 안전한지 확인하고 싶을 것입니다. 이를 위해 다음이 필요합니다:

  • https용 자체 서명 인증서 (이미 신원 인증서가 없다면)
  • 인증서를 사용하도록 구성된 nginx 서버
  • 인증서를 파드가 접근할 수 있게 만드는 secret

이 모든 것은 nginx https example에서 얻을 수 있습니다. 그러려면 go와 make 도구가 설치되어 있어야 합니다. 그것들을 설치하고 싶지 않다면 나중에 수동 단계를 따르세요. 간단히 말해:

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

그리고 configmap도:

kubectl create configmap nginxconfigmap --from-file=default.conf

default.conf의 예시는 Kubernetes examples 프로젝트 저장소에서 찾을 수 있습니다.

configmap/nginxconfigmap created
kubectl get configmaps
NAME             DATA   AGE
nginxconfigmap   1      114s

다음 명령으로 nginxconfigmap ConfigMap의 세부 사항을 볼 수 있습니다:

kubectl describe configmap  nginxconfigmap

출력은 다음과 비슷합니다:

Name:         nginxconfigmap
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
default.conf:
----
server {
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;

        listen 443 ssl;

        root /usr/share/nginx/html;
        index index.html;

        server_name localhost;
        ssl_certificate /etc/nginx/ssl/tls.crt;
        ssl_certificate_key /etc/nginx/ssl/tls.key;

        location / {
                try_files $uri $uri/ =404;
        }
}

BinaryData
====

Events:  <none>

다음은 make 실행에 문제가 있을(예: windows) 경우 따를 수동 단계입니다:

# Create a public private key pair
openssl req -x509 -noenc -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 | tr -d '\n'
cat /d/tmp/nginx.key | base64 | tr -d '\n'

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

apiVersion: "v1"
kind: "Secret"
metadata:
  name: "nginxsecret"
  namespace: "default"
type: kubernetes.io/tls
data:
 # NOTE: Replace the following values with your own base64-encoded certificate and key.
  tls.crt: "REPLACE_WITH_BASE64_CERT" 
  tls.key: "REPLACE_WITH_BASE64_KEY"

이제 파일을 사용해 시크릿을 만든다:

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

이제 시크릿 안의 인증서를 사용해 https 서버를 시작하도록 nginx replica를 수정하고, Service가 두 포트(80과 443)를 노출하도록 수정한다:

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

  • 같은 파일에 Deployment와 Service 명세가 모두 포함되어 있습니다.
  • nginx 서버는 80 포트에서 HTTP 트래픽을, 443에서 HTTPS 트래픽을 제공하며, nginx Service는 두 포트를 모두 노출합니다.
  • 각 컨테이너는 /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 불일치를 무시하도록 지시해야 합니다. Service를 만들어 인증서에 사용된 CName을 Service 조회 중 파드가 사용하는 실제 DNS 이름과 연결했습니다. 파드에서 이것을 테스트해 봅시다(단순화를 위해 같은 secret을 재사용하며, 파드는 Service에 접근하기 위해 nginx.crt만 필요합니다):

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>
...

Service 노출하기

애플리케이션의 일부에서는 Service를 외부 IP 주소에 노출하고 싶을 수 있습니다. 쿠버네티스는 NodePort와 LoadBalancer 두 가지 방법을 지원합니다. 마지막 섹션에서 만든 Service는 이미 NodePort를 사용하므로, 노드에 공용 IP가 있다면 nginx HTTPS replica가 인터넷에서 트래픽을 제공할 준비가 되어 있습니다.

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>

이제 Service를 다시 만들어 클라우드 로드 밸런서를 사용해 봅시다. my-nginx Service의 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에서는 type LoadBalancer가 ELB를 만들며, 이는 IP가 아닌 (긴) 호스트네임을 사용합니다. 너무 길어서 표준 kubectl get svc 출력에 맞지 않으므로, 보려면 kubectl describe service my-nginx를 실행해야 합니다. 다음과 같은 것을 볼 수 있습니다:

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

더 알아보기 (Learn more)