無知

갈 길이 먼 공부 일기

기술 공부/쿠버네티스

쿠버네티스 (8) | 파드 메타데이터

moozii 2022. 3. 1. 14:18
앞으로의 스터디 내용은 <Kubernetes in Action>을 기반으로 진행합니다.
자세한 내용은, 해당 책을 확인해주세요! 
http://www.yes24.com/Product/Goods/89607047 

 

주요 내용은 아래와 같다

  • 특정 파드와 컨테이너 메타데이터를 컨테이너로 전달하는 방법
  • 컨테이너 내에서 실행 중인 애플리케이션이 쿠버네티스 API 서버와 통신해 클러스터 리소스 정보 얻기
  • 컨테이너에 정보를 전달하기 위한 Downward API
  • 쿠버네티스 REST API
  • 인증과 서버 검증을 kubectl proxy에 맡기기
  • 앰배서더 컨테이너 패턴
  • 쿠버네티스 클라이언트 라이브러리

 

Downward API

파드의 IP, 호스트 노드 이름, 파드 자체 이름 등 실행 시점까지 알려지지 않은 데이터, 파드 레이블, 어노테이션 등 이미 설정된 데이터 등 일부 데이터의 경우 동일한 정보를 반복적으로 설정하지 않도록 Downward API를 사용하는 것이 필요하다. 환경변수 또는 파일 형태로 Downward API가 파드와 환경 메타 데이터를 전달할 수 있다. 환경변수나 파일 내에 입력해야 할 파드 스펙, 상태값이 채워지도록 하는 방식이다. 

 

  1. Downward API 볼륨의 환경 변수 및 파일을 초기화하는 데에 사용하는 파드 매니페스트가 API 서버에 존재한다.
    1. 해당 매니페스트에는 메타데이터 및 상태값이 보관된다.
  2. 해당 파드 매니페스트가 파드 내의 컨테이너 환경 변수 혹은 파드의 downward API 볼륨을 통해 애플리케이션 프로세스에 설정 내용을 전달할 수 있다.

 

메타데이터

Downward API 사용 시 아래에 나열된 파드 메타데이터를 프로세스에 노출시킬 수 있다. 

  • 파드 이름
  • 파드 IP 주소
  • 파드 소속 네임스페이스
  • 파드 실행 노드, 서비스어카운트 이름
  • 컨테이너별 CPU와 메모리 요청, 제한
  • 파드 레이블
  • 파드 어노테이션

서비스 어카운트란, 파드와 API 서버가 통신할 때 인증하는 계정이다.

CPU와 메모리의 요청 및 제한이란, 컨테이너에 보장되는 CPU와 메모리의 양, 그리고 컨테이너가 얻을 수 있는 최대량을 의미한다.

 

환경 변수 또는 다운워드 API 볼륨으로 컨테이너에 전달하는 상기 항목들 중, "파드 레이블과 어노테이션" 2가지 항목은 환경변수로는 전달이 어렵고, 볼륨으로만 노출된다. 

 

$ cat downward-api-env.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: downward
spec:
  containers:
  - name: main
    image: busybox
    command: ["sleep", "9999999"]
    resources:
      requests:
        cpu: 15m
        memory: 100Ki
      limits:
        cpu: 100m
        memory: 4Mi
    env:
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name
    - name: POD_NAMESPACE
      valueFrom:
        fieldRef:
          fieldPath: metadata.namespace
    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP
    - name: NODE_NAME
      valueFrom:
        fieldRef:
          fieldPath: spec.nodeName
    - name: SERVICE_ACCOUNT
      valueFrom:
        fieldRef:
          fieldPath: spec.serviceAccountName
    - name: CONTAINER_CPU_REQUEST_MILLICORES
      valueFrom:
        resourceFieldRef:
          resource: requests.cpu
          divisor: 1m
    - name: CONTAINER_MEMORY_LIMIT_KIBIBYTES
      valueFrom:
        resourceFieldRef:
          resource: limits.memory
          divisor: 1Ki

 

특정 값을 설정하는 대신 fieldRef를 통해 파드 매니페스트의 metadata.name을 참조하여 환경변수를 정의한다. 

이때, 컨테이너 CPU 및 메모리 요청/제한 사항은 resourceFieldRef으로 참조를 한다는 특징이 있다.

리소스필드의 경우와 같이 자원 제한 또는 요청을 노출하는 환경변수는 필요한 단위를 지정하기 위해 divisor, 제수를 정의한다. 예시 속 1m은 1밀리코어로 1/1000 CPU코어를 의미한다. 메모리와 관련한 단위로는, 예시 속의 Mi는 메비바이트, Ki는 키비바이트를 뜻한다.

즉, CONTAINER_CPU_REQUEST_MILLICORES는 resource.requests.cpu를 파드 매니페스트에서 찾아 15m임을 확인하고 이를 divisor 1m으로 나누기 때문에 결과적으로 15로 설정되는 것이고, CONTAINER_MEMORY_LIMIT_KIBIBYTES는 resource.limits.memory인 4Mi를, divisor 1Ki로 나눈 4096으로 설정된다.

메비바이트(영어: Mebibyte, MiB) 또는 메가 이진 바이트(Mega binary byte)는 정보위나 컴퓨터 저장장치의 단위이다. 1 메가 이진 바이트 = 220 바이트 = 1,048,576 바이트 = 1,024 키비바이트 메가 이진 바이트와 비슷한 말인 메가바이트 (MB)는 상황(이진 접두어 참조)에 따라서 106 바이트 = 1,000,000 바이트로 사용된다. 두 용어는 비슷하지만 착오를 일으키는 경우가 잦다. 예를 들면 파일의 크기나 저장 장치의 빈공간의 크기를 정확하게 파악해야 할 경우가 있다. 이 혼란은 마이크로소프트의 윈도 운영 체제가 1000 킬로바이트로 똑같이 세 번째 숫자로 우리에게 크기를 보고하기 때문에 더욱 가중된다.
https://ko.wikipedia.org/wiki/%EB%A9%94%EB%B9%84%EB%B0%94%EC%9D%B4%ED%8A%B8 

 

제수, divisor의 일반적인 용례는 다음과 같다.

  • CPU 제한 및 요청
    • 1 = 코어 1개
    • 1m = 1 밀리코어
  • 메모리 제한 및 요청
    • 1 = 1byte
    • 1k = 1 kilobyte
    • 1Ki = 1 Kibibyte
    • 1M = 1 Megabyte
    • 1Mi = 1Mibibyte

 

실습을 통해 해당 매니페스트를 기반으로 파드를 생성하고 환경변수를 조회하고자 했으나,
파드의 컨테이너 생성에 문제가 지속적으로 발생했다. 

$ kubectl create -f downward-api-env.yaml 
pod/downward created

$ kubectl exec downward -- env
error: unable to upgrade connection: container not found ("main")

$ kubectl get po
NAME       READY   STATUS                 RESTARTS   AGE
downward   0/1     CreateContainerError   0          50s

$ kubectl describe po downward 
Name:         downward
Namespace:    default
(중략)
Events:
  Type     Reason                  Age                     From               Message
  ----     ------                  ----                    ----               -------
  Normal   Scheduled               2m54s                   default-scheduler  Successfully assigned default/downward to minikube
  Warning  FailedCreatePodSandBox  2m49s (x2 over 2m52s)   kubelet            Failed to create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox container for pod "downward": Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:393: copying bootstrap data to pipe caused: write init-p: broken pipe: unknown
  Warning  FailedCreatePodSandBox  2m44s                   kubelet            Failed to create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox container for pod "downward": Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:722: waiting for init preliminary setup caused: read init-p: connection reset by peer: unknown
  Warning  FailedCreatePodSandBox  2m42s (x10 over 2m54s)  kubelet            Failed to create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox container for pod "downward": Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: container init was OOM-killed (memory limit too low?): unknown
  Normal   SandboxChanged          2m42s (x12 over 2m53s)  kubelet            Pod sandbox changed, it will be killed and re-created.

$ uname -a
Darwin (중략).local 21.3.0 Darwin Kernel Version 21.3.0: Wed Jan  5 21:37:58 PST 2022; root:xnu-8019.80.24~20/RELEASE_X86_64 x86_64

$ docker version
Client:
 Cloud integration: v1.0.22
 Version:           20.10.11
 API version:       1.41
 Go version:        go1.16.10
 Git commit:        dea9396
 Built:             Thu Nov 18 00:36:09 2021
 OS/Arch:           darwin/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.11
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.9
  Git commit:       847da18
  Built:            Thu Nov 18 00:35:39 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.12
  GitCommit:        7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc:
  Version:          1.0.2
  GitCommit:        v1.0.2-0-g52b36a2
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

도커와 커널의 버전을 조회한 이유는 아래의 참고자료들을 통해 확인한 사항 때문이다. 도커와 커널의 버전이 맞지 않아 커널 업그레이드를 진행하거나 도커를 다운그레이드시키는 방식으로 해당 오류를 해결하고자 하는 사람들이 많았다. 하지만 이는 당시 2019년에 나온 도커 상의 버전 문제일 확률이 높았고, 리눅스 커널 상의 이야기가 많았는데 맥 기반 미니쿠베 환경에서 실습을 진행하는 나에게는 별도의 원인이 있는듯 보여 다른 자료의 조언을 참고했다. 

 

[버전 관련 해결안을 제시한 이슈 사이트]
https://meta.discourse.org/t/docker-copying-bootstrap-data-to-pipe-caused-write-init-p-broken-pipe/108947/7
https://github.com/docker/for-linux/issues/834
https://stackoverflow.com/questions/57914696/oci-runtime-create-failed-copying-bootstrap-data-to-pipe-caused-write-init-p
https://medium.com/@dirk.avery/docker-error-response-from-daemon-1d46235ff61d
https://talk.plesk.com/threads/cant-start-docker-containers.351485/ 
https://blog.csdn.net/xdshust/article/details/90268206
[쿠버네티스 상의 메모리 리밋 설정이 과하게 낮을 경우 발생한 문제라는 제안을 확인]
https://stackoverflow.com/questions/57914696/oci-runtime-create-failed-copying-bootstrap-data-to-pipe-caused-write-init-p

 

그래서 파드 매니페스트의 메모리 부분만을 수정한 새로운 매니페스트를 생성하고, 그를 기반으로 파드를 생성하니 아무런 무리 없이 실습이 진행됨을 확인하였다. 

$ vim downward-api-env-2.yaml
$ cat downward-api-env-2.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: downward
spec:
  containers:
  - name: main
    image: busybox
    command: ["sleep", "9999999"]
    resources:
      requests:
        cpu: 15m
        memory: 100Ki
      limits:
        cpu: 100m
        memory: 100Mi
    env:
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name
    - name: POD_NAMESPACE
      valueFrom:
        fieldRef:
          fieldPath: metadata.namespace
    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP
    - name: NODE_NAME
      valueFrom:
        fieldRef:
          fieldPath: spec.nodeName
    - name: SERVICE_ACCOUNT
      valueFrom:
        fieldRef:
          fieldPath: spec.serviceAccountName
    - name: CONTAINER_CPU_REQUEST_MILLICORES
      valueFrom:
        resourceFieldRef:
          resource: requests.cpu
          divisor: 1m
    - name: CONTAINER_MEMORY_LIMIT_KIBIBYTES
      valueFrom:
        resourceFieldRef:
          resource: limits.memory
          divisor: 1Ki

$ kubectl create -f downward-api-env-2.yaml
pod/downward created

$ kubectl get po
NAME       READY   STATUS    RESTARTS   AGE
downward   1/1     Running   0          7s

$ kubectl exec downward -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=downward
SERVICE_ACCOUNT=default
CONTAINER_CPU_REQUEST_MILLICORES=15
CONTAINER_MEMORY_LIMIT_KIBIBYTES=102400
POD_NAME=downward
POD_NAMESPACE=default
POD_IP=172.17.0.5
NODE_NAME=minikube
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
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_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
HOME=/root

결과적으로 환경변수 조회에 성공했다.

 

downwardAPI 볼륨에 파일로 메타데이터 전달하기

파드 레이블과 어노테이션은 환경변수로 노출할 수 없다는 특징이 있다고 앞서 언급한 바가 있는만큼, 볼륨을 통한 메타데이터 노출 방법에 대한 숙지도 필수적이다. 메타데이터를 프로세스에 노출하기 위한 메타데이터 필드 명시부터 진행하자. 

 

$ cat downward-api-volume.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: downward
  labels:
    foo: bar
  annotations:
    key1: value1
    key2: |
      multi
      line
      value
spec:
  containers:
  - name: main
    image: busybox
    command: ["sleep", "9999999"]
    resources:
      requests:
        cpu: 15m
        memory: 100Ki
      limits:
        cpu: 100m
        memory: 4Mi
    volumeMounts:
    - name: downward
      mountPath: /etc/downward
  volumes:
  - name: downward
    downwardAPI:
      items:
      - path: "podName"
        fieldRef:
          fieldPath: metadata.name
      - path: "podNamespace"
        fieldRef:
          fieldPath: metadata.namespace
      - path: "labels"
        fieldRef:
          fieldPath: metadata.labels
      - path: "annotations"
        fieldRef:
          fieldPath: metadata.annotations
      - path: "containerCpuRequestMilliCores"
        resourceFieldRef:
          containerName: main
          resource: requests.cpu
          divisor: 1m
      - path: "containerMemoryLimitBytes"
        resourceFieldRef:
          containerName: main
          resource: limits.memory
          divisor: 1

환경변수를 사용하는 대신 컨테이너의 etc/downward와 같이 volumeMount로 마운트 위치로 지정된 곳에 볼륨을 마운트하여 메타데이터를 지정하는 방식이다. 각 메타데이터는 path 항목을 통해 이름지어진 각각의 디렉토리 아래로 들어가 저장된다. 파드의 레이블을 예를 들자면, /etc/downward/labels 파일에 기록된다는 의미이다. 파드 매니페스트의 volumes.downwardAPI.items 항목들로 메타데이터를 저장하여 볼륨으로 올리고 노출시키는 것이다.

 

$ kubectl delete po downward 
pod "downward" deleted

$ kubectl create -f downward-api-volume.yaml
pod/downward created

$ kubectl get po              
NAME       READY   STATUS                 RESTARTS   AGE
downward   0/1     CreateContainerError   0          2m7s

$ vim downward-api-volume-2.yaml
$ cat downward-api-volume-2.yaml 
apiVersion: v1
kind: Pod
(중략)
spec:
  containers:
  - name: main
    image: busybox
    command: ["sleep", "9999999"]
    resources:
      requests:
        cpu: 15m
        memory: 100Ki
      limits:
        cpu: 100m
        memory: 100Mi
    volumeMounts:
    - name: downward
      mountPath: /etc/downward
  (후략)

$ kubectl delete po downward 
pod "downward" deleted

$ kubectl create -f downward-api-volume-2.yaml
pod/downward created

$ kubectl get po
NAME       READY   STATUS    RESTARTS   AGE
downward   1/1     Running   0          14s

$ kubectl exec downward -- ls -lL /etc/downward 
total 24
-rw-r--r--    1 root     root           134 Feb 28 05:56 annotations
-rw-r--r--    1 root     root             2 Feb 28 05:56 containerCpuRequestMilliCores
-rw-r--r--    1 root     root             9 Feb 28 05:56 containerMemoryLimitBytes
-rw-r--r--    1 root     root             9 Feb 28 05:56 labels
-rw-r--r--    1 root     root             8 Feb 28 05:56 podName
-rw-r--r--    1 root     root             7 Feb 28 05:56 podNamespace

$ kubectl exec downward -- cat /etc/downward/labels
foo="bar"%                                                                      

$ kubectl exec downward -- cat /etc/downward/annotations
key1="value1"
key2="multi\nline\nvalue\n"
kubernetes.io/config.seen="2022-02-28T05:56:19.287956173Z"
kubernetes.io/config.source="api"%

실습 코드를 통해 확인할 수 있듯, 과거에 발생한 문제와 동일한 해결방식을 채택해 무사히 볼륨 내 파일 조회에 성공했다. 

조회 명령에 대한 옵션은 다음 출처로 확인 가능했으며, -l은 상세 정보를 출력하는 것이고, -L은 심볼릭링크 정보 출력 시 원본 파일 정보를 출력하도록 하는 옵션이다. (확인 출처 : https://big-sun.tistory.com/27 )

 

실습을 통해 지난 환경변수 활용 예제에서 확인하지 못한 파드 레이블 및 어노테이션도 확인이 가능했다. 키=밸류 값으로 각 줄마다 기록된 것을 확인할 수 있었다. 

 

레이블 및 어노테이션 업데이트

파드 레이블이나 어노테이션은 생성 이후 실행 과정에서도 변경 및 수정이 가능하다는 특성이 있다. 따라서 볼륨으로 노출한 메타데이터도 지속적으로 업데이트해 데이터 최신화를 진행해주어야 하는데, 해당 업데이트는 쿠버네티스가 대신 수행한다. 변경이 어려운 환경변숫값 대신 볼륨을 사용하는 이유가 이 때문이다. 

 

컨테이너 수준 메타데이터 노출 시 컨테이너 이름을 지정해야

앞선 매니페스트를 확인해도 알 수 있지만, resourceFieldRef를 사용해 컨테이너의 리소스 제한이나 요청을 다루는 경우, 다른 항목들과 달리 컨테이너 이름을 지정해야 한다. 컨테이너 수준의 메타데이터를 노출하기 위함이므로 참조할 컨테이너를 설정해야 한다. 컨테이너가 하나인 파드여도 해당한다.

 

컨테이너 수준의 리소스에게 별도로 참조 컨테이너 이름을 지정해야 하는 이유는, 볼륨이 파드 수준에서 정의됐기 때문이다. 

 

컨테이너 지정이라는 번거로움 대신, 서로 다른 컨테이너 간 리소스 필드 데이터 전달이 가능하다는 장점이 있다. 환경변수로는 그와 달리 컨테이너 자신의 리소스 제한 및 요청 전달만 가능하다.

 

DownwardAPI 사용 시기와 의의

Downward API는 애플리케이션과 쿠버네티스 사이의 독립성을 유지하는 데에 쓰인다. 환경변수의 특정 데이터를 활용하는 기존 애플리케이션을 처리할 때에도 유용하다. 하지만, Downward API로 사용 가능한 메타데이터는 제한적이고, 더 많은 정보가 필요하면 쿠버네티스 API 서버에서 직접 가져와야 한다. 

 

 

쿠버네티스 API 서버 활용

일부 데이터만 지원하는 downward API와 달리, 애플리케이션에서 클러스터에 정의된 다른 파드나 리소스에 관한 더 많은 정보가 필요할 경우 더 많은 데이터를 확인할 수 있는 쿠버네티스 API 서버를 활용해야만 한다. 애플리케이션이 서비스 외 다른 리소스의 정보가 필요하거나 최신 정보에 접근해야만 하는 경우에도 마찬가지다. 

 

쿠버네티스 API 서버와 통신하는 방법을 살펴보기 이전에 서버의 REST 엔드포인트부터 익혀보자. 

 

쿠버네티스 REST API 살펴보기

API 서버에 직접 접속하기 위한 URL을 얻어보자.

$ kubectl cluster-info 
Kubernetes control plane is running at https://192.168.59.100:8443
CoreDNS is running at https://192.168.59.100:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

https 기반의 서버이므로 서버 인증서 없이 직접 통신하기에는 무리가 있고, kubectl proxy 명령어를 활용해 서버와 통신을 진행한다.

 

https://kinvolk.io/blog/2019/02/abusing-kubernetes-api-server-proxying/

 

kubectl proxy 명령은, 프록시 서버를 실행해 로컬 컴퓨터에서 HTTP 연결을 수신하고, 이 연결의 인증을 관리하며 API 서버로 전달하므로, 요청마다 인증 토큰을 전달할 필요가 없다. 또한 각 요청마다 서버 인증서를 확인해 중간자가 아닌 실제 API 서버와 통신하는 것을 담보한다. 

 

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

--------------------------------------------------

$ curl localhost:8001
{
  "paths": [
    "/.well-known/openid-configuration",
    "/api",
    "/api/v1", 		// 대부분의 리소스 타입을 여기서 확인할 수 있다
    "/apis",
    "/apis/",
    "/apis/admissionregistration.k8s.io",
    "/apis/admissionregistration.k8s.io/v1",
    "/apis/apiextensions.k8s.io",
    "/apis/apiextensions.k8s.io/v1",
    "/apis/apiregistration.k8s.io",
    "/apis/apiregistration.k8s.io/v1",
    "/apis/apps",
    "/apis/apps/v1",
    "/apis/authentication.k8s.io",
    "/apis/authentication.k8s.io/v1",
    "/apis/authorization.k8s.io",
    "/apis/authorization.k8s.io/v1",
    "/apis/autoscaling",
    "/apis/autoscaling/v1",
    "/apis/autoscaling/v2",
    "/apis/autoscaling/v2beta1",
    "/apis/autoscaling/v2beta2",
    "/apis/batch",
    "/apis/batch/v1",
    "/apis/batch/v1beta1",
    "/apis/certificates.k8s.io",
    "/apis/certificates.k8s.io/v1",
    "/apis/coordination.k8s.io",
    "/apis/coordination.k8s.io/v1",
    "/apis/discovery.k8s.io",
    "/apis/discovery.k8s.io/v1",
    "/apis/discovery.k8s.io/v1beta1",
    "/apis/events.k8s.io",
    "/apis/events.k8s.io/v1",
    "/apis/events.k8s.io/v1beta1",
    "/apis/flowcontrol.apiserver.k8s.io",
    "/apis/flowcontrol.apiserver.k8s.io/v1beta1",
    "/apis/flowcontrol.apiserver.k8s.io/v1beta2",
    "/apis/networking.k8s.io",
    "/apis/networking.k8s.io/v1",
    "/apis/node.k8s.io",
    "/apis/node.k8s.io/v1",
    "/apis/node.k8s.io/v1beta1",
    "/apis/policy",
    "/apis/policy/v1",
    "/apis/policy/v1beta1",
    "/apis/rbac.authorization.k8s.io",
    "/apis/rbac.authorization.k8s.io/v1",
    "/apis/scheduling.k8s.io",
    "/apis/scheduling.k8s.io/v1",
    "/apis/storage.k8s.io",
    "/apis/storage.k8s.io/v1",
    "/apis/storage.k8s.io/v1beta1",
    "/healthz",
    "/healthz/autoregister-completion",
    "/healthz/etcd",
    "/healthz/log",
    "/healthz/ping",
    "/healthz/poststarthook/aggregator-reload-proxy-client-cert",
    "/healthz/poststarthook/apiservice-openapi-controller",
    "/healthz/poststarthook/apiservice-registration-controller",
    "/healthz/poststarthook/apiservice-status-available-controller",
    "/healthz/poststarthook/bootstrap-controller",
    "/healthz/poststarthook/crd-informer-synced",
    "/healthz/poststarthook/generic-apiserver-start-informers",
    "/healthz/poststarthook/kube-apiserver-autoregistration",
    "/healthz/poststarthook/priority-and-fairness-config-consumer",
    "/healthz/poststarthook/priority-and-fairness-config-producer",
    "/healthz/poststarthook/priority-and-fairness-filter",
    "/healthz/poststarthook/rbac/bootstrap-roles",
    "/healthz/poststarthook/scheduling/bootstrap-system-priority-classes",
    "/healthz/poststarthook/start-apiextensions-controllers",
    "/healthz/poststarthook/start-apiextensions-informers",
    "/healthz/poststarthook/start-cluster-authentication-info-controller",
    "/healthz/poststarthook/start-kube-aggregator-informers",
    "/healthz/poststarthook/start-kube-apiserver-admission-initializer",
    "/livez",
    "/livez/autoregister-completion",
    "/livez/etcd",
    "/livez/log",
    "/livez/ping",
    "/livez/poststarthook/aggregator-reload-proxy-client-cert",
    "/livez/poststarthook/apiservice-openapi-controller",
    "/livez/poststarthook/apiservice-registration-controller",
    "/livez/poststarthook/apiservice-status-available-controller",
    "/livez/poststarthook/bootstrap-controller",
    "/livez/poststarthook/crd-informer-synced",
    "/livez/poststarthook/generic-apiserver-start-informers",
    "/livez/poststarthook/kube-apiserver-autoregistration",
    "/livez/poststarthook/priority-and-fairness-config-consumer",
    "/livez/poststarthook/priority-and-fairness-config-producer",
    "/livez/poststarthook/priority-and-fairness-filter",
    "/livez/poststarthook/rbac/bootstrap-roles",
    "/livez/poststarthook/scheduling/bootstrap-system-priority-classes",
    "/livez/poststarthook/start-apiextensions-controllers",
    "/livez/poststarthook/start-apiextensions-informers",
    "/livez/poststarthook/start-cluster-authentication-info-controller",
    "/livez/poststarthook/start-kube-aggregator-informers",
    "/livez/poststarthook/start-kube-apiserver-admission-initializer",
    "/logs",
    "/metrics",
    "/openapi/v2",
    "/openid/v1/jwks",
    "/readyz",
    "/readyz/autoregister-completion",
    "/readyz/etcd",
    "/readyz/informer-sync",
    "/readyz/log",
    "/readyz/ping",
    "/readyz/poststarthook/aggregator-reload-proxy-client-cert",
    "/readyz/poststarthook/apiservice-openapi-controller",
    "/readyz/poststarthook/apiservice-registration-controller",
    "/readyz/poststarthook/apiservice-status-available-controller",
    "/readyz/poststarthook/bootstrap-controller",
    "/readyz/poststarthook/crd-informer-synced",
    "/readyz/poststarthook/generic-apiserver-start-informers",
    "/readyz/poststarthook/kube-apiserver-autoregistration",
    "/readyz/poststarthook/priority-and-fairness-config-consumer",
    "/readyz/poststarthook/priority-and-fairness-config-producer",
    "/readyz/poststarthook/priority-and-fairness-filter",
    "/readyz/poststarthook/rbac/bootstrap-roles",
    "/readyz/poststarthook/scheduling/bootstrap-system-priority-classes",
    "/readyz/poststarthook/start-apiextensions-controllers",
    "/readyz/poststarthook/start-apiextensions-informers",
    "/readyz/poststarthook/start-cluster-authentication-info-controller",
    "/readyz/poststarthook/start-kube-aggregator-informers",
    "/readyz/poststarthook/start-kube-apiserver-admission-initializer",
    "/readyz/shutdown",
    "/version"
  ]
}%

프록시에 요청을 다음과 같이 보내면, 프록시는 서버가 반환하는 모든 것을 그대로 반환한다. 

반환된 것은 위와 같은 경로 목록으로, 리소스 생성 시 리소스 정의에 지정한 API 그룹과 버전에 해당하는 경로들이다. 

초기 쿠버네티스에서는 이런 API 그룹 개념을 사용하지 않아 많이 쓰이는 리소스 유형들은 위의 경로 상의 특정 그룹에 속하지는 않지만, 그 이후 추가된 새로운 많은 수의 리소스는 다음 경로들에서 확인 가능하다. 

 

예를 들어, 배치 API 그룹, 잡 리소스의 REST 엔드포인트를 살펴보자. 

$ curl http://localhost:8001/apis/batch
{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "batch",
  "versions": [
    {
      "groupVersion": "batch/v1",
      "version": "v1"
    },
    {
      "groupVersion": "batch/v1beta1",
      "version": "v1beta1"
    }
  ],
  "preferredVersion": {
    "groupVersion": "batch/v1",
    "version": "v1"
  }
}%

$ curl http://localhost:8001/apis/batch/v1
{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "batch/v1", 	// 아래는 해당 그룹의 API 리소스 목록
  "resources": [				// 이 그룹의 모든 리소스 유형을 담는 배열
    {
      "name": "cronjobs",
      "singularName": "",
      "namespaced": true, 		// 네임스페이스 지정 필드가 true인 잡 리소스에 대한 설명
      "kind": "CronJob",
      "verbs": [ 				// 해당 리소스로 사용 가능한 동사들
        "create",
        "delete",
        "deletecollection",
        "get",
        "list",
        "patch",
        "update",
        "watch"
      ],
      "shortNames": [
        "cj"
      ],
      "categories": [
        "all"
      ],
      "storageVersionHash": "sd5LIXh4Fjs="
    },
    {
      "name": "cronjobs/status",
      "singularName": "",
      "namespaced": true,
      "kind": "CronJob",
      "verbs": [
        "get",
        "patch",
        "update"
      ]
    },
    {
      "name": "jobs",
      "singularName": "",
      "namespaced": true,
      "kind": "Job",
      "verbs": [
        "create",
        "delete",
        "deletecollection",
        "get",
        "list",
        "patch",
        "update",
        "watch"
      ],
      "categories": [
        "all"
      ],
      "storageVersionHash": "mudhfqk/qZY="
    },
    {
      "name": "jobs/status", 		// 리소스는 상태를 수정하기 위한 특수 REST 엔드포인트 보유
      "singularName": "",
      "namespaced": true,
      "kind": "Job",
      "verbs": [
        "get",
        "patch",
        "update"
      ]
    }
  ]
}%

서버는 batch/v1 API 그룹에서 리소스 유형 및 REST 엔드포인트 목록을 반환한다. 그중 하나가 잡 리소스이다.

API 서버는 리소스 이름 외에도 네임스페이스 지정 여부, 약칭, 동사 목록 등을 가진다. 

 

클러스터에 있는 모든 잡 인스턴스 나열하기

다음과 같이 요청을 수행해서 잡 리소스 확인이 가능하다. 

조회되는 리소스 내용은, 해당 리소스에 관한 전체 json 정의임을 참고하자.

$ curl http://localhost:8001/apis/batch/v1/jobs
{
  "kind": "JobList",
  "apiVersion": "batch/v1",
  "metadata": {
    "resourceVersion": "154369"
  },
  "items": [
    {
      "metadata": {
        "name": "ingress-nginx-admission-create",
        "namespace": "ingress-nginx",
        "uid": "b17891ff-51bf-43cb-807c-28572f17b56e",
        "resourceVersion": "54924",
        "generation": 1,
        "creationTimestamp": "2022-02-16T10:50:51Z",
        "labels": {
          "app.kubernetes.io/component": "admission-webhook",
          "app.kubernetes.io/instance": "ingress-nginx",
          "app.kubernetes.io/name": "ingress-nginx"
        },
        "annotations": {
          "batch.kubernetes.io/job-tracking": "",
          "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"batch/v1\",\"kind\":\"Job\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/component\":\"admission-webhook\",\"app.kubernetes.io/instance\":\"ingress-nginx\",\"app.kubernetes.io/name\":\"ingress-nginx\"},\"name\":\"ingress-nginx-admission-create\",\"namespace\":\"ingress-nginx\"},\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"app.kubernetes.io/component\":\"admission-webhook\",\"app.kubernetes.io/instance\":\"ingress-nginx\",\"app.kubernetes.io/name\":\"ingress-nginx\"},\"name\":\"ingress-nginx-admission-create\"},\"spec\":{\"containers\":[{\"args\":[\"create\",\"--host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc\",\"--namespace=$(POD_NAMESPACE)\",\"--secret-name=ingress-nginx-admission\"],\"env\":[{\"name\":\"POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}}],\"image\":\"k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660\",\"imagePullPolicy\":\"IfNotPresent\",\"name\":\"create\"}],\"restartPolicy\":\"OnFailure\",\"securityContext\":{\"runAsNonRoot\":true,\"runAsUser\":2000},\"serviceAccountName\":\"ingress-nginx-admission\"}}}}\n"
        },
        "managedFields": [
          {
            "manager": "kubectl-client-side-apply",
            "operation": "Update",
            "apiVersion": "batch/v1",
            "time": "2022-02-16T10:50:51Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:metadata": {
                "f:annotations": {
                  ".": {},
                  "f:kubectl.kubernetes.io/last-applied-configuration": {}
                },
                "f:labels": {
                  ".": {},
                  "f:app.kubernetes.io/component": {},
                  "f:app.kubernetes.io/instance": {},
                  "f:app.kubernetes.io/name": {}
                }
              },
              "f:spec": {
                "f:backoffLimit": {},
                "f:completionMode": {},
                "f:completions": {},
                "f:parallelism": {},
                "f:suspend": {},
                "f:template": {
                  "f:metadata": {
                    "f:labels": {
                      ".": {},
                      "f:app.kubernetes.io/component": {},
                      "f:app.kubernetes.io/instance": {},
                      "f:app.kubernetes.io/name": {}
                    },
                    "f:name": {}
                  },
                  "f:spec": {
                    "f:containers": {
                      "k:{\"name\":\"create\"}": {
                        ".": {},
                        "f:args": {},
                        "f:env": {
                          ".": {},
                          "k:{\"name\":\"POD_NAMESPACE\"}": {
                            ".": {},
                            "f:name": {},
                            "f:valueFrom": {
                              ".": {},
                              "f:fieldRef": {}
                            }
                          }
                        },
                        "f:image": {},
                        "f:imagePullPolicy": {},
                        "f:name": {},
                        "f:resources": {},
                        "f:terminationMessagePath": {},
                        "f:terminationMessagePolicy": {}
                      }
                    },
                    "f:dnsPolicy": {},
                    "f:restartPolicy": {},
                    "f:schedulerName": {},
                    "f:securityContext": {
                      ".": {},
                      "f:runAsNonRoot": {},
                      "f:runAsUser": {}
                    },
                    "f:serviceAccount": {},
                    "f:serviceAccountName": {},
                    "f:terminationGracePeriodSeconds": {}
                  }
                }
              }
            }
          },
          {
            "manager": "kube-controller-manager",
            "operation": "Update",
            "apiVersion": "batch/v1",
            "time": "2022-02-16T10:50:57Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:status": {
                "f:completionTime": {},
                "f:conditions": {},
                "f:startTime": {},
                "f:succeeded": {},
                "f:uncountedTerminatedPods": {}
              }
            },
            "subresource": "status"
          }
        ]
      },
      "spec": {
        "parallelism": 1,
        "completions": 1,
        "backoffLimit": 6,
        "selector": {
          "matchLabels": {
            "controller-uid": "b17891ff-51bf-43cb-807c-28572f17b56e"
          }
        },
        "template": {
          "metadata": {
            "name": "ingress-nginx-admission-create",
            "creationTimestamp": null,
            "labels": {
              "app.kubernetes.io/component": "admission-webhook",
              "app.kubernetes.io/instance": "ingress-nginx",
              "app.kubernetes.io/name": "ingress-nginx",
              "controller-uid": "b17891ff-51bf-43cb-807c-28572f17b56e",
              "job-name": "ingress-nginx-admission-create"
            }
          },
          "spec": {
            "containers": [
              {
                "name": "create",
                "image": "k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660",
                "args": [
                  "create",
                  "--host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc",
                  "--namespace=$(POD_NAMESPACE)",
                  "--secret-name=ingress-nginx-admission"
                ],
                "env": [
                  {
                    "name": "POD_NAMESPACE",
                    "valueFrom": {
                      "fieldRef": {
                        "apiVersion": "v1",
                        "fieldPath": "metadata.namespace"
                      }
                    }
                  }
                ],
                "resources": {},
                "terminationMessagePath": "/dev/termination-log",
                "terminationMessagePolicy": "File",
                "imagePullPolicy": "IfNotPresent"
              }
            ],
            "restartPolicy": "OnFailure",
            "terminationGracePeriodSeconds": 30,
            "dnsPolicy": "ClusterFirst",
            "serviceAccountName": "ingress-nginx-admission",
            "serviceAccount": "ingress-nginx-admission",
            "securityContext": {
              "runAsUser": 2000,
              "runAsNonRoot": true
            },
            "schedulerName": "default-scheduler"
          }
        },
        "completionMode": "NonIndexed",
        "suspend": false
      },
      "status": {
        "conditions": [
          {
            "type": "Complete",
            "status": "True",
            "lastProbeTime": "2022-02-16T10:50:57Z",
            "lastTransitionTime": "2022-02-16T10:50:57Z"
          }
        ],
        "startTime": "2022-02-16T10:50:51Z",
        "completionTime": "2022-02-16T10:50:57Z",
        "succeeded": 1,
        "uncountedTerminatedPods": {}
      }
    },
    {
      "metadata": {
        "name": "ingress-nginx-admission-patch",
        "namespace": "ingress-nginx",
        "uid": "34590b10-0665-45be-ba05-bdff155cd14d",
        "resourceVersion": "54934",
        "generation": 1,
        "creationTimestamp": "2022-02-16T10:50:51Z",
        "labels": {
          "app.kubernetes.io/component": "admission-webhook",
          "app.kubernetes.io/instance": "ingress-nginx",
          "app.kubernetes.io/name": "ingress-nginx"
        },
        "annotations": {
          "batch.kubernetes.io/job-tracking": "",
          "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"batch/v1\",\"kind\":\"Job\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/component\":\"admission-webhook\",\"app.kubernetes.io/instance\":\"ingress-nginx\",\"app.kubernetes.io/name\":\"ingress-nginx\"},\"name\":\"ingress-nginx-admission-patch\",\"namespace\":\"ingress-nginx\"},\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"app.kubernetes.io/component\":\"admission-webhook\",\"app.kubernetes.io/instance\":\"ingress-nginx\",\"app.kubernetes.io/name\":\"ingress-nginx\"},\"name\":\"ingress-nginx-admission-patch\"},\"spec\":{\"containers\":[{\"args\":[\"patch\",\"--webhook-name=ingress-nginx-admission\",\"--namespace=$(POD_NAMESPACE)\",\"--patch-mutating=false\",\"--secret-name=ingress-nginx-admission\",\"--patch-failure-policy=Fail\"],\"env\":[{\"name\":\"POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}}],\"image\":\"k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660\",\"imagePullPolicy\":\"IfNotPresent\",\"name\":\"patch\"}],\"restartPolicy\":\"OnFailure\",\"securityContext\":{\"runAsNonRoot\":true,\"runAsUser\":2000},\"serviceAccountName\":\"ingress-nginx-admission\"}}}}\n"
        },
        "managedFields": [
          {
            "manager": "kubectl-client-side-apply",
            "operation": "Update",
            "apiVersion": "batch/v1",
            "time": "2022-02-16T10:50:51Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:metadata": {
                "f:annotations": {
                  ".": {},
                  "f:kubectl.kubernetes.io/last-applied-configuration": {}
                },
                "f:labels": {
                  ".": {},
                  "f:app.kubernetes.io/component": {},
                  "f:app.kubernetes.io/instance": {},
                  "f:app.kubernetes.io/name": {}
                }
              },
              "f:spec": {
                "f:backoffLimit": {},
                "f:completionMode": {},
                "f:completions": {},
                "f:parallelism": {},
                "f:suspend": {},
                "f:template": {
                  "f:metadata": {
                    "f:labels": {
                      ".": {},
                      "f:app.kubernetes.io/component": {},
                      "f:app.kubernetes.io/instance": {},
                      "f:app.kubernetes.io/name": {}
                    },
                    "f:name": {}
                  },
                  "f:spec": {
                    "f:containers": {
                      "k:{\"name\":\"patch\"}": {
                        ".": {},
                        "f:args": {},
                        "f:env": {
                          ".": {},
                          "k:{\"name\":\"POD_NAMESPACE\"}": {
                            ".": {},
                            "f:name": {},
                            "f:valueFrom": {
                              ".": {},
                              "f:fieldRef": {}
                            }
                          }
                        },
                        "f:image": {},
                        "f:imagePullPolicy": {},
                        "f:name": {},
                        "f:resources": {},
                        "f:terminationMessagePath": {},
                        "f:terminationMessagePolicy": {}
                      }
                    },
                    "f:dnsPolicy": {},
                    "f:restartPolicy": {},
                    "f:schedulerName": {},
                    "f:securityContext": {
                      ".": {},
                      "f:runAsNonRoot": {},
                      "f:runAsUser": {}
                    },
                    "f:serviceAccount": {},
                    "f:serviceAccountName": {},
                    "f:terminationGracePeriodSeconds": {}
                  }
                }
              }
            }
          },
          {
            "manager": "kube-controller-manager",
            "operation": "Update",
            "apiVersion": "batch/v1",
            "time": "2022-02-16T10:50:59Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:status": {
                "f:completionTime": {},
                "f:conditions": {},
                "f:startTime": {},
                "f:succeeded": {},
                "f:uncountedTerminatedPods": {}
              }
            },
            "subresource": "status"
          }
        ]
      },
      "spec": {
        "parallelism": 1,
        "completions": 1,
        "backoffLimit": 6,
        "selector": {
          "matchLabels": {
            "controller-uid": "34590b10-0665-45be-ba05-bdff155cd14d"
          }
        },
        "template": {
          "metadata": {
            "name": "ingress-nginx-admission-patch",
            "creationTimestamp": null,
            "labels": {
              "app.kubernetes.io/component": "admission-webhook",
              "app.kubernetes.io/instance": "ingress-nginx",
              "app.kubernetes.io/name": "ingress-nginx",
              "controller-uid": "34590b10-0665-45be-ba05-bdff155cd14d",
              "job-name": "ingress-nginx-admission-patch"
            }
          },
          "spec": {
            "containers": [
              {
                "name": "patch",
                "image": "k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660",
                "args": [
                  "patch",
                  "--webhook-name=ingress-nginx-admission",
                  "--namespace=$(POD_NAMESPACE)",
                  "--patch-mutating=false",
                  "--secret-name=ingress-nginx-admission",
                  "--patch-failure-policy=Fail"
                ],
                "env": [
                  {
                    "name": "POD_NAMESPACE",
                    "valueFrom": {
                      "fieldRef": {
                        "apiVersion": "v1",
                        "fieldPath": "metadata.namespace"
                      }
                    }
                  }
                ],
                "resources": {},
                "terminationMessagePath": "/dev/termination-log",
                "terminationMessagePolicy": "File",
                "imagePullPolicy": "IfNotPresent"
              }
            ],
            "restartPolicy": "OnFailure",
            "terminationGracePeriodSeconds": 30,
            "dnsPolicy": "ClusterFirst",
            "serviceAccountName": "ingress-nginx-admission",
            "serviceAccount": "ingress-nginx-admission",
            "securityContext": {
              "runAsUser": 2000,
              "runAsNonRoot": true
            },
            "schedulerName": "default-scheduler"
          }
        },
        "completionMode": "NonIndexed",
        "suspend": false
      },
      "status": {
        "conditions": [
          {
            "type": "Complete",
            "status": "True",
            "lastProbeTime": "2022-02-16T10:50:58Z",
            "lastTransitionTime": "2022-02-16T10:50:58Z"
          }
        ],
        "startTime": "2022-02-16T10:50:51Z",
        "completionTime": "2022-02-16T10:50:58Z",
        "succeeded": 1,
        "uncountedTerminatedPods": {}
      }
    }
  ]
}%

 

아래와 같이 새로 잡 리소스를 생성하고 다시 확인하면 리소스 목록에 추가되어 있음을 확인할 수 있다.

$ cat my-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
spec:
  template:
    metadata:
      labels:
        app: batch-job
    spec:
      restartPolicy: OnFailure
      containers:
      - name: main
        image: luksa/batch-job
        
$ kubectl create -f my-job.yaml
job.batch/my-job created

$ curl http://localhost:8001/apis/batch/v1/jobs
{
  "kind": "JobList",
  "apiVersion": "batch/v1",
  "metadata": {
    "resourceVersion": "154492"
  },
  "items": [
    {
      "metadata": {
        "name": "my-job",
        "namespace": "default",
        "uid": "e29fd824-69c2-425f-90f8-e8cda98b51c9",
        "resourceVersion": "154479",
        "generation": 1,
        "creationTimestamp": "2022-02-28T08:17:50Z",
        "labels": {
          "app": "batch-job",
          "controller-uid": "e29fd824-69c2-425f-90f8-e8cda98b51c9",
          "job-name": "my-job"
        },
        "annotations": {
          "batch.kubernetes.io/job-tracking": ""
        },
        "managedFields": [
          {
            "manager": "kube-controller-manager",
            "operation": "Update",
            "apiVersion": "batch/v1",
            "time": "2022-02-28T08:17:50Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:status": {
                "f:active": {},
                "f:startTime": {},
                "f:uncountedTerminatedPods": {}
              }
            },
            "subresource": "status"
          },
          {
            "manager": "kubectl-create",
            "operation": "Update",
            "apiVersion": "batch/v1",
            "time": "2022-02-28T08:17:50Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:metadata": {
                "f:labels": {
                  ".": {},
                  "f:app": {}
                }
              },
              "f:spec": {
                "f:backoffLimit": {},
                "f:completionMode": {},
                "f:completions": {},
                "f:parallelism": {},
                "f:suspend": {},
                "f:template": {
                  "f:metadata": {
                    "f:labels": {
                      ".": {},
                      "f:app": {}
                    }
                  },
                  "f:spec": {
                    "f:containers": {
                      "k:{\"name\":\"main\"}": {
                        ".": {},
                        "f:image": {},
                        "f:imagePullPolicy": {},
                        "f:name": {},
                        "f:resources": {},
                        "f:terminationMessagePath": {},
                        "f:terminationMessagePolicy": {}
                      }
                    },
                    "f:dnsPolicy": {},
                    "f:restartPolicy": {},
                    "f:schedulerName": {},
                    "f:securityContext": {},
                    "f:terminationGracePeriodSeconds": {}
                  }
                }
              }
            }
          }
        ]
      },
      "spec": {
        "parallelism": 1,
        "completions": 1,
        "backoffLimit": 6,
        "selector": {
          "matchLabels": {
            "controller-uid": "e29fd824-69c2-425f-90f8-e8cda98b51c9"
          }
        },
        "template": {
          "metadata": {
            "creationTimestamp": null,
            "labels": {
              "app": "batch-job",
              "controller-uid": "e29fd824-69c2-425f-90f8-e8cda98b51c9",
              "job-name": "my-job"
            }
          },
          "spec": {
            "containers": [
              {
                "name": "main",
                "image": "luksa/batch-job",
                "resources": {},
                "terminationMessagePath": "/dev/termination-log",
                "terminationMessagePolicy": "File",
                "imagePullPolicy": "Always"
              }
            ],
            "restartPolicy": "OnFailure",
            "terminationGracePeriodSeconds": 30,
            "dnsPolicy": "ClusterFirst",
            "securityContext": {},
            "schedulerName": "default-scheduler"
          }
        },
        "completionMode": "NonIndexed",
        "suspend": false
      },
      "status": {
        "startTime": "2022-02-28T08:17:50Z",
        "active": 1,
        "uncountedTerminatedPods": {}
      }
    },
    (중략)
  ]
}%

 

특정 네임스페이스 내의 리소스 검색도 가능하다. 경로 상에 네임스페이스를 추가해서 요청을 넣으면 된다.

$ curl http://localhost:8001/apis/batch/v1/namespaces/default/jobs/my-job
{
  "kind": "Job",
  "apiVersion": "batch/v1",
  "metadata": {
    "name": "my-job",
    "namespace": "default",
    "uid": "e29fd824-69c2-425f-90f8-e8cda98b51c9",
    "resourceVersion": "154595",
    "generation": 1,
    "creationTimestamp": "2022-02-28T08:17:50Z",
    "labels": {
      "app": "batch-job",
      "controller-uid": "e29fd824-69c2-425f-90f8-e8cda98b51c9",
      "job-name": "my-job"
    },
    "annotations": {
      "batch.kubernetes.io/job-tracking": ""
    },
    "managedFields": [
      {
        "manager": "kubectl-create",
        "operation": "Update",
        "apiVersion": "batch/v1",
        "time": "2022-02-28T08:17:50Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {
          "f:metadata": {
            "f:labels": {
              ".": {},
              "f:app": {}
            }
          },
          "f:spec": {
            "f:backoffLimit": {},
            "f:completionMode": {},
            "f:completions": {},
            "f:parallelism": {},
            "f:suspend": {},
            "f:template": {
              "f:metadata": {
                "f:labels": {
                  ".": {},
                  "f:app": {}
                }
              },
              "f:spec": {
                "f:containers": {
                  "k:{\"name\":\"main\"}": {
                    ".": {},
                    "f:image": {},
                    "f:imagePullPolicy": {},
                    "f:name": {},
                    "f:resources": {},
                    "f:terminationMessagePath": {},
                    "f:terminationMessagePolicy": {}
                  }
                },
                "f:dnsPolicy": {},
                "f:restartPolicy": {},
                "f:schedulerName": {},
                "f:securityContext": {},
                "f:terminationGracePeriodSeconds": {}
              }
            }
          }
        }
      },
      {
        "manager": "kube-controller-manager",
        "operation": "Update",
        "apiVersion": "batch/v1",
        "time": "2022-02-28T08:19:54Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {
          "f:status": {
            "f:completionTime": {},
            "f:conditions": {},
            "f:startTime": {},
            "f:succeeded": {},
            "f:uncountedTerminatedPods": {}
          }
        },
        "subresource": "status"
      }
    ]
  },
  "spec": {
    "parallelism": 1,
    "completions": 1,
    "backoffLimit": 6,
    "selector": {
      "matchLabels": {
        "controller-uid": "e29fd824-69c2-425f-90f8-e8cda98b51c9"
      }
    },
    "template": {
      "metadata": {
        "creationTimestamp": null,
        "labels": {
          "app": "batch-job",
          "controller-uid": "e29fd824-69c2-425f-90f8-e8cda98b51c9",
          "job-name": "my-job"
        }
      },
      "spec": {
        "containers": [
          {
            "name": "main",
            "image": "luksa/batch-job",
            "resources": {},
            "terminationMessagePath": "/dev/termination-log",
            "terminationMessagePolicy": "File",
            "imagePullPolicy": "Always"
          }
        ],
        "restartPolicy": "OnFailure",
        "terminationGracePeriodSeconds": 30,
        "dnsPolicy": "ClusterFirst",
        "securityContext": {},
        "schedulerName": "default-scheduler"
      }
    },
    "completionMode": "NonIndexed",
    "suspend": false
  },
  "status": {
    "conditions": [
      {
        "type": "Complete",
        "status": "True",
        "lastProbeTime": "2022-02-28T08:19:54Z",
        "lastTransitionTime": "2022-02-28T08:19:54Z"
      }
    ],
    "startTime": "2022-02-28T08:17:50Z",
    "completionTime": "2022-02-28T08:19:54Z",
    "succeeded": 1,
    "uncountedTerminatedPods": {}
  }
}%

 

파드 내 API 서버와 통신

kubectl이 없는 일반 파드 내에서 통신하는 방법에 대해서도 알아보자. 파드 내부에서 API 서버와 통신을 하기 위해서는 3가지 조건을 만족해야 한다. 

  1. API 서버의 위치를 알아야 한다
  2. API 서버와 통신하고 있는 것이 맞는지 확인해야 한다 (진위 여부 확인)
  3. API 서버로 인증해서 권한을 얻어야 한다

아무 기능도 수행하지 않는 임의의 파드를 우선 생성해서 실습을 진행해보자. 그렇게 생성한 파드의 컨테이너 내 셸에서 명령을 실행하고 curl 요청을 통해 API 서버에 액세스를 하는 실습이다.

 

$ cat curl.yaml
apiVersion: v1
kind: Pod
metadata:
  name: curl
spec:
  containers:
  - name: main
    image: tutum/curl
    command: ["sleep", "9999999"]
    
$ kubectl create -f curl.yaml
pod/curl created

$ kubectl exec -it curl -- bash
error: unable to upgrade connection: container not found ("main")

$ kubectl get po
NAME           READY   STATUS         RESTARTS   AGE
curl           0/1     ErrImagePull   0          69s
downward       1/1     Running        0          158m
my-job-jhgc2   0/1     Completed      0          16m

$ kubectl delete po curl
pod "curl" deleted

$ vim curl-2.yaml

$ cat curl-2.yaml
apiVersion: v1
kind: Pod
metadata:
  name: curl
spec:
  containers:
  - name: main
    image: curlimages/curl
    command: ["sleep", "9999999"]

$ kubectl create -f curl-2.yaml
pod/curl created

$ kubectl get po
NAME           READY   STATUS      RESTARTS   AGE
curl           1/1     Running     0          13s
downward       1/1     Running     0          160m
my-job-jhgc2   0/1     Completed   0          19m

$ kubectl exec -it curl -- bash
OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "bash": executable file not found in $PATH: unknown
command terminated with exit code 126

$ kubectl exec -it curl -- sh

/ $ env | grep KUBERNETES_SERVICE
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_HOST=10.96.0.1

/ $ curl https://kubernetes
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

해당 실습 과정에서 발생한 문제는 다음과 같다. 

1. 아무것도 하지 않는 파드를 생성하기 위해 참고한 도커 이미지가 삭제되었다. 

https://github.com/docker/hub-feedback/issues/1672 

그래서 해당 이미지의 대안을 해당 이슈의 댓글에서 확인했다.

"Used curlimages/curl instead, on a recent manual test and it worked for me."

해당 이미지를 참고하는 새로운 명세 파일을 작성해 이를 기반으로 파드를 생성했다.

 

2. 1번 문제를 해결하고 해당 파드의 쉘을 접속하려 하니 실행이 되지 않는다.

해당 문제에 대한 원인을 파악하니, 우리가 변경해서 새로이 참고한 이미지를 포함해, 기반 이미지가 알파인 기반일 경우 bash 쉘을 사용할 수 없어 sh와 같은 다른 쉘을 활용해야 한다고 한다.

문제 확인 : https://gymcoding.github.io/2020/09/21/docker-error-1/

알파인 이미지 사용 여부 확인 : https://hub.docker.com/r/curlimages/curl

확인하니 알파인 기반이 맞았고 bash가 아닌 sh로 실행하니 정상적으로 수행되었다.

 

참고로 여기서 grep이란 입력 파일 내용 중 특정 문자열을 찾기 위해 사용하는 명령어입니다.

https://recipes4dev.tistory.com/157 

 

결과적으로 파드 쉘 안에서 쿠버네티스로 서버 접속을 시도했으나, 인증서 문제가 발생했다.

-k 옵션을 사용하면 간단히 해결된다고 하는데, API 서버를 수동으로 사용할 때 일반적으로 사용한다고 한다. 

해당 옵션의 의미는 https 사이트를 SSL certificate 검증없이 연결한다는 의미이기 때문이다.

https://kim-dragon.tistory.com/47

 

하지만 그외 보다 정석적인 방법으로, 연결하려는 서버가 인증됐음을 신뢰하고 바로 연결하기보다, 인증서를 curl로 검사해 인증서를 확인하는 방법을 배워보자. 애초에 서버 인증서 확인을 건너뛰면 중간자 공격과 같이 인증 토큰이 공격자에게 노출될 위험이 있기 때문이다. 

중간자 공격은 네트워크 통신을 조작해 통신 내용 도청 및 조작을 시도하는 기법으로, 중간자로 통신 사이를 개입하는 방식이라고 한다.

https://ko.wikipedia.org/wiki/%EC%A4%91%EA%B0%84%EC%9E%90_%EA%B3%B5%EA%B2%A9

 

서버 아이덴티티 검증

지난 7장에서 다룬 시크릿의 내용을 복기해보자.

$ ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt     namespace  token

시크릿 내에는 3가지 항목이 있는데, 그중 ca.crt 파일은 쿠버네티스 API 서버의 인증서에 서명하는 데 사용되는 인증기관인 CA의 인증서를 보유하고 있다. 즉, API 서버와 통신 중인지 검증하기 위해서 서버의 인증서가 CA로 서명됐는지 확인해야 한다. curl --cacert 옵션을 통해 CA 인증서를 지정해서 다시 접속해서 확인해보자. 

 

$ curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://k
ubernetes
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
  "reason": "Forbidden",
  "details": {},
  "code": 403
}

403 오류가 발생했다. 책에서 나온 Unauthorized 가 발생하지는 않았으나 오류가 발생하는 것은 예상된 일이다. 그 이후의 책 속 해결안을 적용했을 때에도 오류가 나오는지 확인해보자. Forbidden이어도 책의 사례와 같이 curl이 인증서 서명을 확인해 서버 ID를 확인한 것인지 확신이 들지 않기 때문이다. >> 다음 챕터 실습을 진행하면서 권한 문제임을 확인했다. 

$ export CURL_CA_BUNDLE=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
$ curl https://kubernetes
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
  "reason": "Forbidden",
  "details": {},
  "code": 403
}

일단 실행시마다 인증서 지정을 자동으로 진행하도록 설정을 해주었다.

 

API 서버로 인증

서버에서 인증을 통과해야 클러스터에 배포된 API 오브젝트를 읽고 업데이트 및 삭제가 가능하다. 인증을 위해 토큰이 필요한데 이는 시크릿으로 제공되므로 시크릿 볼륨의 토큰 파일에 저장된 바를 확인할 수 있다. 

$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
// TOKEN 환경변수에 토큰을 저장

$ curl -H "Authorization: Bearer $TOKEN" https://kubernetes
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "forbidden: User \"system:serviceaccount:default:default\" cannot get path \"/\"",
  "reason": "Forbidden",
  "details": {},
  "code": 403
}

$ curl -H 'Authorization: Bearer $TOKEN' https://kubernetes
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "Unauthorized",
  "reason": "Unauthorized",
  "code": 401
}

"를 '로 변경해서 진행했더니 Unauthorized로 오류가 변경되었는데 왜인지도 모르겠어서 찾아봤다.

* 작은따옴표로 감싸진 문자열은 변화 없이 그대로 출력
* 큰따옴표 안에 넣으면 변수가 실제 값으로 치환된 후 출력
https://hue9010.github.io/etc/quotes/

즉, 큰따옴표가 맞는 것이고, 인증은 통과했는데, https://kubernetes 가 forbidden이라는 의미이다.

 

이 forbidden 오류는 다음과 같은 문제임이 확인됐다.

https://stackoverflow.com/questions/47973570/kubernetes-log-user-systemserviceaccountdefaultdefault-cannot-get-services

 

Kubernetes log, User "system:serviceaccount:default:default" cannot get services in the namespace

Forbidden!Configured service account doesn't have access. Service account may have been revoked. User "system:serviceaccount:default:default" cannot get services in the namespace "mycomp-services-p...

stackoverflow.com

In the first error the issue is that serviceaccount default in default namespace can not get services because it does not have access to list/get services. So what you need to do is assign a role to that user using clusterrolebinding.

즉, RBAC, 역할 기반 액세스 제어가 활성화된 쿠버네티스 클러스터를 사용하고 있는 현재 환경 상, 서비스 어카운트가 API 서버에 액세스할 권한이 없었기 때문에 발생한 오류였다. 이를 위해 현재로서는 API 서버를 쿼리할 수 있는 권한을 부여해 우회해야 한다. 

$ kubectl create clusterrolebinding permissive-binding --clusterrole=cluster-admin --group=system:serviceaccounts
clusterrolebinding.rbac.authorization.k8s.io/permissive-binding created

$ kubectl exec -it curl -- sh

/ $ export CURL_CA_BUNDLE=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

/ $ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

/ $ curl -H "Authorization: Bearer $TOKEN" https://kubernetes
{
  "paths": [
    "/.well-known/openid-configuration",
    "/api",
    "/api/v1",
    "/apis",
    "/apis/",
    "/apis/admissionregistration.k8s.io",
    (중략)
    "/readyz/poststarthook/start-kube-apiserver-admission-initializer",
    "/readyz/shutdown",
    "/version"
  ]
}

다만 위와 같이 모든 서비스 어카운트, 모든 파드에 클러스터 관리자 권한을 부여하는 것은 매우 위험하므로 본 테스트용 실습에서만 진행했다. 어쨌든 권한을 부여하면 조회에 성공한다! 보다 나은 RBAC 우회는 12챕터 즈음 배우리라 기대한다.

 

권한을 부여하고 헤더 부분에 토큰을 전달하면, 서버는 토큰을 인증된 것으로 인식하고 응답을 반환하여 모든 리소스 탐색이 가능했다.

 

파드가 실행 중인 네임스페이스 얻기

이미 downward API로 네임스페이스를 파드에 전달하는 방법을 알아보기는 했지만, 시크릿 볼륨에도 네임스페이스 파일이 있다. 파드가 실행 중인 네임스페이스가 포함되어 있는 파일이기 때문에, 환경변수로 전달하지 않고 파일을 읽어서도 조회가 가능하다. 파일 내용을 NS라는 변수로 로드시켜 나열해보자.

$ NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

$ curl -H "Authorization: Bearer $TOKEN" https://kubernetes/api/v1/namespaces/$NS/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "160889"
  },
  "items": [
    {
      "metadata": {
        "name": "curl",
        "namespace": "default",
        "uid": "700a6159-fa4d-4aa9-869f-74f0a3f74d8c",
        "resourceVersion": "157819",
        "creationTimestamp": "2022-03-01T00:17:24Z",
        "managedFields": [
          {
            "manager": "kubectl-create",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-03-01T00:17:24Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:spec": {
                "f:containers": {
                  "k:{\"name\":\"main\"}": {
                    ".": {},
                    "f:command": {},
                    "f:image": {},
                    "f:imagePullPolicy": {},
                    "f:name": {},
                    "f:resources": {},
                    "f:terminationMessagePath": {},
                    "f:terminationMessagePolicy": {}
                  }
                },
                "f:dnsPolicy": {},
                "f:enableServiceLinks": {},
                "f:restartPolicy": {},
                "f:schedulerName": {},
                "f:securityContext": {},
                "f:terminationGracePeriodSeconds": {}
              }
            }
          },
          {
            "manager": "Go-http-client",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-03-01T00:17:28Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:status": {
                "f:conditions": {
                  "k:{\"type\":\"ContainersReady\"}": {
                    ".": {},
                    "f:lastProbeTime": {},
                    "f:lastTransitionTime": {},
                    "f:status": {},
                    "f:type": {}
                  },
                  "k:{\"type\":\"Initialized\"}": {
                    ".": {},
                    "f:lastProbeTime": {},
                    "f:lastTransitionTime": {},
                    "f:status": {},
                    "f:type": {}
                  },
                  "k:{\"type\":\"Ready\"}": {
                    ".": {},
                    "f:lastProbeTime": {},
                    "f:lastTransitionTime": {},
                    "f:status": {},
                    "f:type": {}
                  }
                },
                "f:containerStatuses": {},
                "f:hostIP": {},
                "f:phase": {},
                "f:podIP": {},
                "f:podIPs": {
                  ".": {},
                  "k:{\"ip\":\"172.17.0.7\"}": {
                    ".": {},
                    "f:ip": {}
                  }
                },
                "f:startTime": {}
              }
            },
            "subresource": "status"
          }
        ]
      },
      "spec": {
        "volumes": [
          {
            "name": "kube-api-access-57rbp",
            "projected": {
              "sources": [
                {
                  "serviceAccountToken": {
                    "expirationSeconds": 3607,
                    "path": "token"
                  }
                },
                {
                  "configMap": {
                    "name": "kube-root-ca.crt",
                    "items": [
                      {
                        "key": "ca.crt",
                        "path": "ca.crt"
                      }
                    ]
                  }
                },
                {
                  "downwardAPI": {
                    "items": [
                      {
                        "path": "namespace",
                        "fieldRef": {
                          "apiVersion": "v1",
                          "fieldPath": "metadata.namespace"
                        }
                      }
                    ]
                  }
                }
              ],
              "defaultMode": 420
            }
          }
        ],
        "containers": [
          {
            "name": "main",
            "image": "curlimages/curl",
            "command": [
              "sleep",
              "9999999"
            ],
            "resources": {},
            "volumeMounts": [
              {
                "name": "kube-api-access-57rbp",
                "readOnly": true,
                "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
              }
            ],
            "terminationMessagePath": "/dev/termination-log",
            "terminationMessagePolicy": "File",
            "imagePullPolicy": "Always"
          }
        ],
        "restartPolicy": "Always",
        "terminationGracePeriodSeconds": 30,
        "dnsPolicy": "ClusterFirst",
        "serviceAccountName": "default",
        "serviceAccount": "default",
        "nodeName": "minikube",
        "securityContext": {},
        "schedulerName": "default-scheduler",
        "tolerations": [
          {
            "key": "node.kubernetes.io/not-ready",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 300
          },
          {
            "key": "node.kubernetes.io/unreachable",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 300
          }
        ],
        "priority": 0,
        "enableServiceLinks": true,
        "preemptionPolicy": "PreemptLowerPriority"
      },
      "status": {
        "phase": "Running",
        "conditions": [
          {
            "type": "Initialized",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-03-01T00:17:24Z"
          },
          {
            "type": "Ready",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-03-01T00:17:28Z"
          },
          {
            "type": "ContainersReady",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-03-01T00:17:28Z"
          },
          {
            "type": "PodScheduled",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-03-01T00:17:24Z"
          }
        ],
        "hostIP": "192.168.59.100",
        "podIP": "172.17.0.7",
        "podIPs": [
          {
            "ip": "172.17.0.7"
          }
        ],
        "startTime": "2022-03-01T00:17:24Z",
        "containerStatuses": [
          {
            "name": "main",
            "state": {
              "running": {
                "startedAt": "2022-03-01T00:17:28Z"
              }
            },
            "lastState": {},
            "ready": true,
            "restartCount": 0,
            "image": "curlimages/curl:latest",
            "imageID": "docker-pullable://curlimages/curl@sha256:faaba66e89c87fd3fb51336857142ee6ce78fa8d8f023a5713d2bf4957f1aca8",
            "containerID": "docker://41d2fdb13240499a982e404435ad49938ff917fa018a96165f2966a54482b70a",
            "started": true
          }
        ],
        "qosClass": "BestEffort"
      }
    },
    {
      "metadata": {
        "name": "downward",
        "namespace": "default",
        "uid": "61a47eca-a635-48eb-9378-afa996dcbfc0",
        "resourceVersion": "157271",
        "creationTimestamp": "2022-02-28T05:56:19Z",
        "labels": {
          "foo": "bar"
        },
        "annotations": {
          "key1": "value1",
          "key2": "multi\nline\nvalue\n"
        },
        "managedFields": [
          {
            "manager": "kubectl-create",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-02-28T05:56:19Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:metadata": {
                "f:annotations": {
                  ".": {},
                  "f:key1": {},
                  "f:key2": {}
                },
                "f:labels": {
                  ".": {},
                  "f:foo": {}
                }
              },
              "f:spec": {
                "f:containers": {
                  "k:{\"name\":\"main\"}": {
                    ".": {},
                    "f:command": {},
                    "f:image": {},
                    "f:imagePullPolicy": {},
                    "f:name": {},
                    "f:resources": {
                      ".": {},
                      "f:limits": {
                        ".": {},
                        "f:cpu": {},
                        "f:memory": {}
                      },
                      "f:requests": {
                        ".": {},
                        "f:cpu": {},
                        "f:memory": {}
                      }
                    },
                    "f:terminationMessagePath": {},
                    "f:terminationMessagePolicy": {},
                    "f:volumeMounts": {
                      ".": {},
                      "k:{\"mountPath\":\"/etc/downward\"}": {
                        ".": {},
                        "f:mountPath": {},
                        "f:name": {}
                      }
                    }
                  }
                },
                "f:dnsPolicy": {},
                "f:enableServiceLinks": {},
                "f:restartPolicy": {},
                "f:schedulerName": {},
                "f:securityContext": {},
                "f:terminationGracePeriodSeconds": {},
                "f:volumes": {
                  ".": {},
                  "k:{\"name\":\"downward\"}": {
                    ".": {},
                    "f:downwardAPI": {
                      ".": {},
                      "f:defaultMode": {},
                      "f:items": {}
                    },
                    "f:name": {}
                  }
                }
              }
            }
          },
          {
            "manager": "Go-http-client",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-03-01T00:07:12Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {
              "f:status": {
                "f:conditions": {
                  "k:{\"type\":\"ContainersReady\"}": {
                    ".": {},
                    "f:lastProbeTime": {},
                    "f:lastTransitionTime": {},
                    "f:status": {},
                    "f:type": {}
                  },
                  "k:{\"type\":\"Initialized\"}": {
                    ".": {},
                    "f:lastProbeTime": {},
                    "f:lastTransitionTime": {},
                    "f:status": {},
                    "f:type": {}
                  },
                  "k:{\"type\":\"Ready\"}": {
                    ".": {},
                    "f:lastProbeTime": {},
                    "f:lastTransitionTime": {},
                    "f:status": {},
                    "f:type": {}
                  }
                },
                "f:containerStatuses": {},
                "f:hostIP": {},
                "f:phase": {},
                "f:podIP": {},
                "f:podIPs": {
                  ".": {},
                  "k:{\"ip\":\"172.17.0.2\"}": {
                    ".": {},
                    "f:ip": {}
                  }
                },
                "f:startTime": {}
              }
            },
            "subresource": "status"
          }
        ]
      },
      "spec": {
        "volumes": [
          {
            "name": "downward",
            "downwardAPI": {
              "items": [
                {
                  "path": "podName",
                  "fieldRef": {
                    "apiVersion": "v1",
                    "fieldPath": "metadata.name"
                  }
                },
                {
                  "path": "podNamespace",
                  "fieldRef": {
                    "apiVersion": "v1",
                    "fieldPath": "metadata.namespace"
                  }
                },
                {
                  "path": "labels",
                  "fieldRef": {
                    "apiVersion": "v1",
                    "fieldPath": "metadata.labels"
                  }
                },
                {
                  "path": "annotations",
                  "fieldRef": {
                    "apiVersion": "v1",
                    "fieldPath": "metadata.annotations"
                  }
                },
                {
                  "path": "containerCpuRequestMilliCores",
                  "resourceFieldRef": {
                    "containerName": "main",
                    "resource": "requests.cpu",
                    "divisor": "1m"
                  }
                },
                {
                  "path": "containerMemoryLimitBytes",
                  "resourceFieldRef": {
                    "containerName": "main",
                    "resource": "limits.memory",
                    "divisor": "1"
                  }
                }
              ],
              "defaultMode": 420
            }
          },
          {
            "name": "kube-api-access-6jz4c",
            "projected": {
              "sources": [
                {
                  "serviceAccountToken": {
                    "expirationSeconds": 3607,
                    "path": "token"
                  }
                },
                {
                  "configMap": {
                    "name": "kube-root-ca.crt",
                    "items": [
                      {
                        "key": "ca.crt",
                        "path": "ca.crt"
                      }
                    ]
                  }
                },
                {
                  "downwardAPI": {
                    "items": [
                      {
                        "path": "namespace",
                        "fieldRef": {
                          "apiVersion": "v1",
                          "fieldPath": "metadata.namespace"
                        }
                      }
                    ]
                  }
                }
              ],
              "defaultMode": 420
            }
          }
        ],
        "containers": [
          {
            "name": "main",
            "image": "busybox",
            "command": [
              "sleep",
              "9999999"
            ],
            "resources": {
              "limits": {
                "cpu": "100m",
                "memory": "100Mi"
              },
              "requests": {
                "cpu": "15m",
                "memory": "100Ki"
              }
            },
            "volumeMounts": [
              {
                "name": "downward",
                "mountPath": "/etc/downward"
              },
              {
                "name": "kube-api-access-6jz4c",
                "readOnly": true,
                "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
              }
            ],
            "terminationMessagePath": "/dev/termination-log",
            "terminationMessagePolicy": "File",
            "imagePullPolicy": "Always"
          }
        ],
        "restartPolicy": "Always",
        "terminationGracePeriodSeconds": 30,
        "dnsPolicy": "ClusterFirst",
        "serviceAccountName": "default",
        "serviceAccount": "default",
        "nodeName": "minikube",
        "securityContext": {},
        "schedulerName": "default-scheduler",
        "tolerations": [
          {
            "key": "node.kubernetes.io/not-ready",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 300
          },
          {
            "key": "node.kubernetes.io/unreachable",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 300
          }
        ],
        "priority": 0,
        "enableServiceLinks": true,
        "preemptionPolicy": "PreemptLowerPriority"
      },
      "status": {
        "phase": "Running",
        "conditions": [
          {
            "type": "Initialized",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-02-28T05:56:19Z"
          },
          {
            "type": "Ready",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-03-01T00:07:09Z"
          },
          {
            "type": "ContainersReady",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-03-01T00:07:09Z"
          },
          {
            "type": "PodScheduled",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-02-28T05:56:19Z"
          }
        ],
        "hostIP": "192.168.59.100",
        "podIP": "172.17.0.2",
        "podIPs": [
          {
            "ip": "172.17.0.2"
          }
        ],
        "startTime": "2022-02-28T05:56:19Z",
        "containerStatuses": [
          {
            "name": "main",
            "state": {
              "running": {
                "startedAt": "2022-03-01T00:07:08Z"
              }
            },
            "lastState": {},
            "ready": true,
            "restartCount": 1,
            "image": "busybox:latest",
            "imageID": "docker-pullable://busybox@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb",
            "containerID": "docker://1fe76cf1515005694fced68e2c1177db6744e9c6a64a5b413e2c3aa46e4b8798",
            "started": true
          }
        ],
        "qosClass": "Burstable"
      }
    }
  ]
}

동일한 네임스페이스 내 모든 파드가 나열되었다. 동일 방식으로 다른 오브젝트를 검색해 PUT, PATCH 등으로 업데이트를 진행할 수도 있다.

 

파드가 쿠버네티스와 통신하는 방법 정리하기

  • 애플리케이션은 서버 인증서가 인증기관으로부터 서명됐는지 검증하기 위해 인증서 파일 ca.crt 파일을 확인해야 한다
  • 토큰 파일 내용을 헤더에 Bearer 토큰으로 넣어 인증해야 한다
  • 네임스페이스 파일은 파드 네임스페이스 내 오브젝트의 CRUD 작업 수행 시 네임스페이스를 서버로 전달하는 데에 사용한다
CRUD : Create, Read, Update, Delete
= HTTP Method : POST, GET, PATCH/PUT, DELETE

 

앰배서더 컨테이너를 이용한 서버 통신 간소화

보안을 유지하면서 통신을 보다 간편하게 진행하기 위해, kubectl proxy와 같이 파드 내에서도 프록시로 요청을 보내 인증 및 검증 처리를 진행하도록 하는 방법을 알아보자.

 

앰배서더 컨테이너는, 서버와 직접 통신하는 대신 통신할 프록시를 실행하는 컨테이너이다.

메인 컨테이너 애플리케이션이 API 서버와 직접 통신하기보다, HTTPS가 아닌 HTTP로 앰배서더에 연결하면, 앰배서더 컨테이너 내 프록시가 서버에 대한 HTTPS 연결을 처리하도록 해 보안을 투명하게 관리한다. 시크릿 볼륨 내 디폴트 토큰 파일을 활용한다.

 

파드의 모든 컨테이너는 동일한 루프백 네트워크 인터페이스를 공유하므로 애플리케이션은 로컬호스트 포트로 프록시에 액세스할 수 있다.

 

앰배서더 컨테이너 패턴을 확인하기 위해 새로 파드를 생성할 수도 있지만, 이번에는 파드 내에 메인 컨테이너 외에 앰배서더 컨테이너를 포함하는 파드를 생성하는 실습을 진행한다.

 

$ cat curl-with-ambassador-2.yaml
apiVersion: v1
kind: Pod
metadata:
  name: curl-with-ambassador
spec:
  containers:
  - name: main
    image: curlimages/curl
    command: ["sleep", "9999999"]
  - name: ambassador
    image: luksa/kubectl-proxy:1.6.2
 
$ kubectl create -f curl-with-ambassador-2.yaml
pod/curl-with-ambassador created

$ kubectl exec -it curl-with-ambassador -c main -- sh
/ $ curl localhost:8001
{
  "paths": [
    "/.well-known/openid-configuration",
    "/api",
    "/api/v1",
    "/apis",
    "/apis/",
    "/apis/admissionregistration.k8s.io",
    "/apis/admissionregistration.k8s.io/v1",
    "/apis/apiextensions.k8s.io",
    "/apis/apiextensions.k8s.io/v1",
    "/apis/apiregistration.k8s.io",
    "/apis/apiregistration.k8s.io/v1",
    "/apis/apps",
    "/apis/apps/v1",
    (중략)
    "/readyz/shutdown",
    "/version"
  ]
}

메인 컨테이너에 명령어를 수행하기 위해 평소와 다른 -c main 옵션을 추가해서 실행했다. 

상기 실습에서는 이전 실습과 달리 굳이 인증 관련 처리를 할 필요가 없었는데, 앰배서더 컨테이너를 통해 프록시를 수행했기 때문이다.

 

정확히는, 

1. 메인 컨테이너에 curl 명령을 localhost:8001로 실행

2. 로컬 호스트의 8001번 포트를 통해 앰배서더 컨테이너를 접근

3. 앰배서더 컨테이너는 kubectl proxy를 실행 (이렇게 되도록 해당 이미지를 사용함)

4. 프록시가 인증 관련 처리를 대신 진행해 API 서버에 접근

위의 단계적인 과정을 거친다. 

 

 

클라이언트 라이브러리를 통한 통신

프록시 앰배서더 컨테이너를 통해 일반적인 라이브러리 기반으로 간단한 HTTP 요청을 수행함을 확인했다. 그 이상을 수행시키기 위해서는 쿠버네티스 API 클라이언트 라이브러리 중 하나를 사용해야 한다.

 

API Machinery SIG가 지원하는 클라이언트 라이브러리 종류는 아래와 같다.

해당 공식 라이브러리 2개 외에도 여러 라이브러리가 존재한다.

자세한 사항은 책을 참고하자.

 

Swagger, 라이브러리 제작 및 API 상호작용

 

그외로도 본인의 라이브러리를 직접 만들 수도 있다!

스웨거 API 프레임워크를 사용해 클라이언트 라이브러리 및 문서의 생성이 가능하다는 것이다. 자세한 사항은 아래 사이트를 참고하자.

https://swagger.io/

 

API Documentation & Design Tools for Teams | Swagger

 

swagger.io

 

그외로도 API 서버를 --enable-swagger-ui=true 옵션으로 실행해서 활성화시키면,

스웨거 UI를 통해서도 API 탐색 및 상호작용이 가능하다고 한다. 

minikube start --extra-config=apiserver.Features.Enable-SwaggerUI=true
// 위와 같이 클러스터 시작 시 해당 UI를 활성화시킨 뒤에 아래 주소를 브라우저에 입력해 접속
https://<apiserver>:<port>/swagger-ui

 

추가 참고자료

https://kubernetes.io/ko/docs/tasks/inject-data-application/

 

애플리케이션에 데이터 주입하기

워크로드를 실행하는 파드에 대한 구성과 기타 데이터를 지정한다.

kubernetes.io