쿠버네티스 인 액션 - 애플리케이션에서 파드 메타데이터와 그 외의 리소스에 액세스하기 (7)

hyeokjin·2022년 9월 10일
0

kubernetes

목록 보기
7/9
post-thumbnail

이 장에서는 특정 파드와 컨테이너 메타데이터를 컨테이너로 전달하는 방법과 컨테이너 내에서 실행 중인 애플리케이션이 쿠버네티스 API 서버와 통신해 클러스터에 배포된 리소스 정보를 얻는 것이 얼마나 쉬운지, 더 나아가 이런 리소스를 생성하거나 수정하는 방법을 알아보자.

사용 가능한 메타데이터 이해

Downward API 를 사용하면 파드 자체의 메타데이터를 해당 파드 내에서 실행 중인 프로세스에 노출할 수 있다. 현재 다음 정보를 컨테이너에 전달할 수 있다.

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

환경변수로 메타데이터 노출하기

먼저 환경변수로 파드와 컨테이너의 메타데이터를 컨테이너에 전달하는 방법을 알아보자

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:		# 특정 값을 설정하는 대신 파드 매니페스트의 metadata.name을 참조한다.
        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:		# 컨테이너의 CPU/메모리 요청과 제한은 fieldRef 대신 resourceFieldRef를 사용해 참조한다.
        resourceFieldRef:
          resource: requests.cpu
          divisor: 1m	# 리소스 필드의 경우 필요한 단위의 값을 얻으려면 제수를 정의한다.
    - name: CONTAINER_MEMORY_LIMIT_KIBIBYTES
      valueFrom:
        resourceFieldRef:
          resource: limits.memory
          divisor: 1Ki

프로세스가 실행되면 파드 스펙에 정의한 모든 환경변수를 조회할 수 있다.
파드의 이름, IP와 네임스페이스는 각각 POD_NAME, POD_IP와 POD_NAMESPACE 환경변수로 노출된다. 컨테이너가 실행 중인 노드의 이름은 NODE_NAME 변수로 노출된다. 서비스 어카운트 이름은 SERVICE_ACCOUNT 환경변수로 사용 가능하다. 그리고 이 컨테이너에 요청된 CPU 양과 컨테이너가 사용할 수 있는 최대 메모리 양을 갖는 두개의 환경변수를 작성한다.

파드를 만든 후에는 kubectl exec를 사용해 다음과 같이 컨테이너에 있는 모든 환경변수를 볼 수 있다.

$ kubectl exec -it downward env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=downward
CONTAINER_MEMORY_LIMIT_KIBIBYTES=4096
POD_NAME=downward
POD_NAMESPACE=default
POD_IP=10.32.2.19
NODE_NAME=gke-kubia-default-pool-c2f41b01-3gd0
SERVICE_ACCOUNT=default
CONTAINER_CPU_REQUEST_MILLICORES=15
KUBERNETES_SERVICE_HOST=10.36.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.36.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.36.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.36.0.1
TERM=xterm
HOME=/root

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

환경변수로 대신 파일로 메타데이터를 노출하려는 경우 downwardAPI 볼륨을 정의해 컨테이너에 마운트할 수 있다. 환경변수로 파드의 레이블이나 어노테이션을 노출할 수 없기 때문에 볼륨을 사용한다.

downward-api-volume.yaml

apiVersion: v1
kind: Pod
metadata:
  name: downward	# 이 레이블과 어노테이션은 downwardAPI 볼륨으로 노출된다.
  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:		# downward볼륨은 /etc/downward 아래에 마운트 한다.
    - name: downward
      mountPath: /etc/downward
  volumes:
  - name: downward		# downwardAPI 볼륨은 downward라는 이름으로 정의한다.
    downwardAPI:
      items:
      - path: "podName"	# 파드의 이름(metadata.name)은 podName 파일에 기록된다
        fieldRef:
          fieldPath: metadata.name
      - path: "podNamespace"
        fieldRef:
          fieldPath: metadata.namespace
      - path: "labels"	# 파드의 레이블은 /etc/downward/labels 파일에 기록된다.
        fieldRef:
          fieldPath: metadata.labels
      - path: "annotations"	# 파드의 어노테이션을 /etc/downward/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

환경변수로 메타데이터를 전달하는 대신 downward 라는 볼륨을 정의하고 컨테이너의 /etc/downward 아래에 마운트한다. 이 볼륨에 포함된 파일들은 볼륨 스펙의 downwardAPI.items 속성 아래에 설정된다.

다음 downwardAPI 볼륨 내 파일을 살펴보자

$ kubectl exec downward -- ls -1L /etc/downward
annotations
containerCpuRequestMilliCores
containerMemoryLimitBytes
labels
podName
podNamespace

파드가 실행되는 동안 레이블과 어노테이션을 수정할 수 있다는 것을 기억할 것이다. 예상대로 레이블이나 어노테이션이 변경될 때 쿠버네티스가 이 값을
가지고 있는 파일을 업데이트해서 파드가 항상 최신 데이터를 볼 수 있도록 한다. 이는 또한 레이블과 어노테이션이 환경변수로 노출될 수 없는지도 설명한다. 환경변숫값은 나중에 업데이트할 수 없기 때문이다.

볼륨의 레이블과 어노테이션은 별도의 중에 키=값 형식으로 저장된다.

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

$ kubectl exec downward cat /etc/downward/anotations
key1="value1"
key2="multi\nline\nvalue\n"
kubernetes.io/config.seen="2022-09-11T13:59:56.695657201Z"
kubernetes.io/config.source="api"

Downward API 사용 시기 이해

환경변수의 특정 데이터를 활용하는 기존 애플리케이션을 처리할 때 유용하다. downward API를 사용하면 애플리케이션을 다시 짜거나 데이터를 가져와서 환경변수에 노출하는 셸 스크립트를 사용하지 않고도 데이터를 애플리케이션에 노출할 수 있다.

쿠버네티스 API 서버와 통신하기

downward API는 단지 파드 자체의 메타데이터와 모든 파드의 데이터 중 일부만 노출한다. 때때로 애플리케이션에서 클러스터에 정의된 다른 파드나 리소스에 관한 더 많은 정보가 필요할 수 있다.

쿠버네티스 REST API

쿠버네티스 API와 통신하는 애플리케이션을 개발할 계획이라면, API에 관해 먼저 알아보고 싶을 것이다.

API 서버에 직접 접속을 해 볼 수 있다.

$ kubectl cluster-info
Kubernetes control plane is running at https://34.105.79.56

서버는 HTTPS를 사용하고 인증이 필요하다. 인증을 직접 처리하는 대신 kubectl proxy 명령을 실행해 프록시로 서버에 통신할 수있다.

$ 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/v2beta1",
    "/apis/autoscaling/v2beta2",
    "/apis/batch",				# batch API 그룹과 이 그룹의 두 가지의 버전
    "/apis/batch/v1",
    "/apis/batch/v1beta1",
    "/apis/certificates.k8s.io",
    "/apis/certificates.k8s.io/v1",
    "/apis/cloud.google.com",
    "/apis/cloud.google.com/v1",
    "/apis/cloud.google.com/v1beta1",
    "/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/flowcontrol.apiserver.k8s.io",
    "/apis/flowcontrol.apiserver.k8s.io/v1beta1",
    "/apis/internal.autoscaling.gke.io",
    "/apis/internal.autoscaling.gke.io/v1alpha1",
    "/apis/metrics.k8s.io",
    "/apis/metrics.k8s.io/v1beta1",
    "/apis/migration.k8s.io",
    "/apis/migration.k8s.io/v1alpha1",
    "/apis/networking.gke.io",
    "/apis/networking.gke.io/v1",
    "/apis/networking.gke.io/v1beta1",
    "/apis/networking.gke.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/nodemanagement.gke.io",
    "/apis/nodemanagement.gke.io/v1alpha1",
    "/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/snapshot.storage.k8s.io",
    "/apis/snapshot.storage.k8s.io/v1",
    "/apis/snapshot.storage.k8s.io/v1beta1",
    "/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"
  ]
}

예제로 /apis/batch/vi 경로는 잡 리소스 API 그룹이다. /apis/batch 경로 뒤에 무엇이 있는지 살펴보자

$ curl http://localhost:8001/apis/batch
{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "batch",
  "versions": [				# 두 가지 버전을 갖는 batch API 그룹
    {
      "groupVersion": "batch/v1",
      "version": "v1"
    },
    {
      "groupVersion": "batch/v1beta1",
      "version": "v1beta1"
    }
  ],
  "preferredVersion": {				# 클라이언트는 v1beta1 대신 v1 버전을 사용해야한다.
    "groupVersion": "batch/v1",
    "version": "v1"
  }
}



$ curl http://localhost:8001/apis/batch/v1
{
  "kind": "APIResourceList",			# batch/v1 API 그룹내의 API 리소스 목록
  "apiVersion": "v1",
  "groupVersion": "batch/v1",
  "resources": [						# 이 그룹의 모든 리소스 유형을 담는 배열
    {
      "name": "cronjobs",
      "singularName": "",
      "namespaced": 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",		# 네임스페이스 지정필드가 true인 잡 리소스에 관한 설명
      "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"
      ]
    }
  ]
}

클러스터에서 잡 목록을 얻으려면 다음과 같이 /apis/batch/v1/jobs 경로에서 GET 요청을 수행한다.

$ curl http://localhost:8001/apis/batch/v1/jobs
{
  "kind": "JobList",
  "apiVersion": "batch/v1",
  "metadata": {
    "resourceVersion": "4295670"
  },
  "items": []
}

클러스터에 잡 리소스가 배포되어 있지 않다면 items 배열이 비어 있을 것이다. 아래의 파일을 배포하고 REST 엔드포인트를 다시 접속하면 items에 항목이 생긴것을 볼 수 있다.

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

해당 파드를 추가 후 다시 확인해본다.

$ curl http://localhost:8001/apis/batch/v1/jobs
{
  "kind": "JobList",
  "apiVersion": "batch/v1",
  "metadata": {
    "resourceVersion": "4296149"
  },
  "items": [
    {
      "metadata": {
        "name": "my-job",
        "namespace": "default",
        "uid": "b276371d-36ad-4945-84d9-70d446af811c",
        "resourceVersion": "4296037",
        "generation": 1,
        "creationTimestamp": "2022-09-11T14:14:31Z",
        "labels": {
          "app": "batch-job",
          "controller-uid": "b276371d-36ad-4945-84d9-70d446af811c",
          "job-name": "my-job"
        },
        "managedFields": [
          {
            "manager": "kube-controller-manager",
            "operation": "Update",
            "apiVersion": "batch/v1",
            "time": "2022-09-11T14:14:31Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {"f:status":{"f:active":{},"f:startTime":{}}},
            "subresource": "status"
          },
          {
            "manager": "kubectl-create",
            "operation": "Update",
            "apiVersion": "batch/v1",
            "time": "2022-09-11T14:14:31Z",
            "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": "b276371d-36ad-4945-84d9-70d446af811c"
          }
        },
        "template": {
          "metadata": {
            "creationTimestamp": null,
            "labels": {
              "app": "batch-job",
              "controller-uid": "b276371d-36ad-4945-84d9-70d446af811c",
              "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-09-11T14:14:31Z",
        "active": 1
      }
    }
  ]
}

파드 내에서 API 서버와 통신

kubectl proxy를 사용해 로컬 컴퓨터에서 API 서버와 통신하는 방법을 배웠다. 이제 kubectl 이 없는 파드 내에서 통신하는 방법을 알아본다.

  • API 서버의 위치를 찾아야 한다
  • API 서버와 통신하고 있는지 확인해야 한다.
  • API 서버로 인증해야 한다.

가장 먼저 API 서버와 통신할 파드를 실행한다. 아무것도 하지 않는 파드를 실행한 다음 컨테이너 셸에서 kubectl exec를 실행한다.

curl 바이너리가 포함된 컨테이너 이미지를 사용한다.

curl.yaml

apiVersion: v1
kind: Pod
metadata:
  name: curl
spec:
  containers:
  - name: main
    image: ibmcom/curl:4.2.0-build.2	# 컨테이너에서 curl을 사용해야 하기 때문에 ibmcom/curl:4.2.0-build.2 이미지를 사용했음
    command: ["sleep", "9999999"]	# 컨테이너가 계속 실행되도록 하려고 지연 시간이 길게 sleep 커맨드 실행한다.
$ kubectl exec -it curl bash
[curler@curl ~]$

이제 API 서버와 통신할 준비가 되었다.
다음으로 API 서버 주소를 찾자.

쿠버네티스의 API 서버의 IP 와 포트를 찾아야한다. kubernetes라는 서비스가 디폴트 네임스페이스에 자동으로 노출되고 API 서버를 가르키도록 구성되기 때문에 쉽다.

$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.36.0.1    <none>        443/TCP   4d1h

우리는 각 서비스에 관한 환경변수가 구성돼 있음을 알 고 있다.
API 서버의 IP 주소와 포트를 컨테이너 내부의 KUBERNETES_SERVICE_HOST 와 KUBERNETES_SERVICE_PORT 변수에서 모두 얻을 수 있다.

[curler@curl ~]$ env | grep KUBERNETES_SERVICE
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_HOST=10.36.0.1
KUBERNETES_SERVICE_PORT_HTTPS=443

또한 각 서비스마다 DNS 엔트리가 있다는 것을 기억할 것이다.
환경변수를 조회할 필요도 없이. 단순히 curl 에서 https://kubernetes를 가르키기만 하면 된다.

앞의 예제에 표시된 환경변수에 따르면 API 서버가 HTTPS의 기본 포트인 443에서 수신 대기 중이므로 HTTPS로 서버에 접속할 수 있다.

[curler@curl ~]$ curl https://kubernetes
curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). 
 ...
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.
 (인증 문제로 아직 접근 불가하다.)

-k 옵션을 사용하여 API 서버를 수동으로 사용가능하지만, 더 올바른 방법으로 접속을 해보겠다.
연결하려는 서버가 인증된 API 서버라는 것을 맹목적으로 신뢰하는 대신 인증서를 curl로 검사해 인증서를 확인한다.

이 전 챕터에 시크릿을 설명하면서 각 컨테이너의 서비스어카운트(/var/run/secrets/kubernetes.io/serviceaccount/)에 마운트되는 자동으로 생성된 default-token-xxx 이름의 시크릿을 살펴봤다.

시크릿에는 세 개의 항목이 있는데, 쿠버네티스 API 서버의 인증서에 서명하는 데 사용되는 인증 기관의 인증서를 보유한 ca.crt 파일에 집중해보자

API 서버와 통신 중인지 확인하려면 서버의 인증서가 CA로 서명됐는지 확인해야한다. curl을 --cacert 옵션과 같이 사용하면 CA 인증서를 지정할 수 있으므로 API 서버를 다시 접속한다.

[curler@curl ~]$ curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://kubernetes
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {

  },
  "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
  "reason": "Forbidden",
  "details": {

  },
  "code": 403
}
🧨 참고
// RBAC가 활성화된 쿠버네티스 클러스터를 사용하는 경우 API 서버에 액세스할 권한이 없을 수 있다. 다음 명령을 실행해 RBAC를 우회할 수 있다
// 프로덕션 클러스터에서는 절대 해서는 안되고, 테스트 목적으로만 사용한다.
$ kubectl create clusterrolebinding permissive-binding --clusterrole=cluster-admin --group=system:serviceaccounts

서버의 인증서를 신뢰할 수 있는 CA가 서명했기 떄문에 curl이 서버의 ID를 확인했다. 하지만 여전히 인증처리가 필요하다.

그 전에 먼저 CURL_CA_BUNDLE 환경변수를 설정해 curl를 접속할때 --cacert를 지정할 필요없이 편하게 접속하도록 설정해보자

[curler@curl ~]$ export CURL_CA_BUNDLE=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

이제 curl https://kubernetes 명령으로 --cacert를 사용하지 않고 API 서버에 접속할 수 있다.

이어서 서버에 인증을 통과하려면 인증 토큰이 필요하다 이 토큰은 시크릿 볼륨의 token 파일에 저장된다. 토큰을 사용해 API 서버에 액세스 한다. 먼저 토큰을 환경변수에 로드한다.

[curler@curl ~]$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

토큰이 TOKEN 환경변수에 저장된다. 그리고 다음과 같이 API 서버로 요청을 보낼 수 있다.

[curler@curl ~]$ curl -H "Authorization: Bearer $TOKEN" https://kubernetes
{
  "paths": [
    "/.well-known/openid-configuration",
    "/api",
    "/api/v1",
    "/apis",
    "/apis/",
...

Authorization HTTP 헤더 내부에 토큰을 전달하고 API 서버는 토큰을 인증된 것으로 인식하고 적절한 응답을 반환했다. 이제 클러스터의 모든 리소스를 탐색할 수 있다.

예를 들어 동일한 네임스페이스 내에 있는 모든 파드를 조회할 수 있다.

[curler@curl ~]$ NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

[curler@curl ~]$ curl -H "Authorization: Bearer $TOKEN" https://kubernetes/api/vi/namespaces/$NS/pods
{
  "kind": "PodList"
  "apiVersion": "v1"
...

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

파드 내에서 실행 중인 애플리케이션이 쿠버네티스 API에 적절히 액세스할 수 있는 방법을 정리해보자

  • 애플리케이션은 API 서버의 인증서가 인증 기관으로부터 서명됐는지를 검증해야하며, 인증 기관의 인증서는 ca.cart 파일에 있다.

  • 애플리케이션은 token 파일의 내용을 Authorization HTTP 헤더에 Bearer 토큰으로 넣어 전송해서 자신을 인증해야 한다.

  • namespace 파일은 파드의 네임스페이스 안에 있는 API 오브젝트의 CRUD 작업을 수행할 때 네임스페이스를 API 서버로 전달하는 데 사용해야 한다.

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

API 서버를 쿼리해야 하는 경우에, API 서버와 직접 통신하는 대신 메인 컨테이너의 애플리케이션은 HTTPS 대신 HTTP로 앰배서더에 연결하고 앰배서더 프록시가 API 서버에 대한 HTTPS 연결을 처리하도록해 보안을 투명하게 관리할 수 있다.

파드에서 단일 컨테이너를 실행하는 대신 이미 만들어놓은 도커 이미지인 다목적 kubectl-proxy 컨테이너 이미지를 기반으로 추가적인 앰배서더 컨테이너를 실행한다

curl-with-ambassador.yaml

apiVersion: v1
kind: Pod
metadata:
  name: curl-with-ambassador
spec:
  containers:
  - name: main
    image: ibmcom/curl:4.2.0-build.2
    command: ["sleep", "9999999"]
  - name: ambassador		# kubectl-proxy 이미지를 실행하는 앰배서더 컨테이너
    image: luksa/kubectl-proxy:1.6.2
🧨 참고
luksa/kubectl-proxy:1.6.2 이미지는 아래의 파일을 기준으로 빌드했다.

> Dockerfile
FROM alpine
RUN apk update && apk add curl && curl -L -O https://dl.k8s.io/v1.8.0/kubernetes-client-linux-amd64.tar.gz && tar zvxf kubernetes-client-linux-amd64.tar.gz kubernetes/client/bin/kubectl && mv kubernetes/client/bin/kubectl / && rm -rf kubernetes && rm -f kubernetes-client-linux-amd64.tar.gz
ADD kubectl-proxy.sh /kubectl-proxy.sh
ENTRYPOINT /kubectl-proxy.sh

> kubectl-proxy.sh
#!/bin/sh

API_SERVER="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT"
CA_CRT="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"

/kubectl proxy --server="$API_SERVER" --certificate-authority="$CA_CRT" --token="$TOKEN" --accept-paths='^.*'

파드에 두 개의 컨테이너가 있으며 main 컨테이너에서 bash를 실행시키려면 -c 옵션을 사용한다.

$ kubectl exec -it curl-with-ambassador -c main bash

curl localhost:8001
{
  "paths": [
    "/.well-known/openid-configuration",
    "/api",
    "/api/v1",
    "/apis",
    "/apis/",
...

성공적으로 출력이된다. 인증 토큰 및 서버 인증서를 처리가 필요없다.
외부 서비스에 연결하는 복잡성을 숨기고 메인 컨테이너에서 실행되는 애플리케이션을 단순화하기 위해 앰배서더 컨테이너를 사용하는 좋은 예시다.

클라이언트 라이브러리를 사용해 API 서버와 통신

만약 단순한 API 요청 이상을 수행하려면 쿠버네티스 API 클라이언트 라이브러리 중 하나를 사용하는 것이 좋다.
kubernetes API 클라이언트 라이브러리는 Golang 클라이언트, Python 외에도 여러 언어에 관한 사용자 제공 클라이언트 라이브러리가 있다.
라이브러리리는 일반적으로 HTTPS를 지원하고 인증을 관리하므로 앰배서더 컨테이너를 사용할 필요가 없다.

예시로 Fabric8 Java Client 를 사용한 쿠버네티스 상호작용을 살펴보자


package kubia;

import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodList;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;

import java.util.Arrays;

public class Fabric8ClientTest {

    public static void main(String[] args) throws Exception {
        KubernetesClient client = new DefaultKubernetesClient();
        
        // list pods in the default namespace
        PodList pods = client.pods().inNamespace("default").list();
        pods.getItems().stream().forEach(s -> System.out.println("Found pod: " + s.getMetadata().getName()));
        
        // create a pod
        System.out.println("Creating a pod");
        Pod pod = client.pods().inNamespace("default").createNew()
                .withNewMetadata()
                    .withName("my-programmatically-created-pod")
                .endMetadata()
                .withNewSpec()
                    .addNewContainer()
                        .withName("main")
                        .withImage("busybox")
                        .withCommand(Arrays.asList("sleep", "99999"))
                    .endContainer()
                .endSpec()
                .done();
        System.out.println("Created pod: " + pod);

        // edit the pod (add a label to it)
        client.pods().inNamespace("default").withName("my-programmatically-created-pod").edit()
                .editMetadata()
                    .addToLabels("foo", "bar")
                .endMetadata()
                .done();
        System.out.println("Added label foo=bar to pod");

        System.out.println("Waiting 1 minute before deleting pod...");
        Thread.sleep(60000);

        // delete the pod
        client.pods().inNamespace("default").withName("my-programmatically-created-pod").delete();
        System.out.println("Deleted the pod");
    }
}

Faabric8 클라이언트는 훌륭하고 유창한 도메인 특화 언어 DSL API를 제공하기 때문에 코드를 자체적으로 실행되도록 작성해야 한다. 또한 가독성이 좋고 이해하기 쉽다.

스웨거와 OpenAPI를 사용해 자신의 라이브러리 구축

선택한 프로그래밍 언어에 사용할 수 있는 클라이언트가 없는 경우 스웨거 API 프레임워크를 사용해 클라이언트 라이브러리와 문서를 생성할 수 있다.
쿠버네티스 API 서버는 /swaggerapi 에서 스웨거 API 정의를 공개하고 /swagger.json 에서 OpenAPI 스펙을 공개 한다. 또한 스웨거 API 정의를 공개하는 경우 REST API를 탐색하기 위한 웹 UI도 제공한다.
이 UI로 REST API를 더 나은 방식으로 탐색할 수 있다. 자세한 내용을 살펴보려면 http://swagger.io를 방문하면 되겠다.

profile
노옵스를향해

0개의 댓글