Kubernetes 기본 오브젝트

정윤호·2023년 10월 19일
0

Kubernetes Object

쿠버네티스의 오브젝트는 쿠버네티스 시스템에서 영속성을 가지는 객체로 쿠버네티스 클러스터의 상태 관리를 위한 객체이다. 즉 쿠버네티스 시스템의 도메인을 모델링한 것이 쿠버네티스 오브젝트이다. 쿠버네티스 오브젝트의 가장 큰 특징은 생성 의도를 가지고 있다는 점인데, Desired State가 기술된 오브젝트를 생성하면 쿠버네티스 시스템이 해당 오브젝트의 Desired State를 보장하는 방식으로 지속적으로 동작하게 된다. 그렇기 때문에 쿠버네티스를 이용한 인프라 관리는 주로 yaml 파일을 이용한 선언형 프로그래밍 방법을 이용하게 된다.

쿠버네티스에 의해서 배포 및 관리되는 가장 기본적인 오브젝트로는 Pod, Service, Volume, Namespace 총 4가지가 있다.

  • Pod
  • Service
  • Volume
  • Namespace

그 외에도 다음과 같은 추가적인 오브젝트들이 있다.

  • ConfigMap
  • Secret
  • ResourceQuota
  • LimitRange

Pod

파드는 쿠버네티스에서 생성하고 관리할 수 있는 배포 가능한 작은 컴퓨팅 단위로 독립적인 공간과 IP를 갖는다. 하나의 파드는 1개 이상의 컨테이너를 갖고 있기 때문에 여러 기능을 묶어 하나의 목적으로 배포해서 사용할 수 있다.

파드가 생성될 때 고유의 IP 주소가 할당된다. 하지만 쿠버네티스 클러스터 내에서만 해당 IP를 통해 해당 파드로 접근이 가능하고 외부에서는 해당 IP로 접근할 수 없다. 만약 파드에 문제가 생기면 쿠버네티스 시스템이 이를 감지해서 파드를 삭제하고 재생성하게 되는데, 이때 파드의 IP 주소는 바뀌게 된다. 그렇기 때문에 파드의 IP는 가변적인 IP 주소이고 이러한 문제를 해결하기 위해 Service라는 오브젝트가 존재한다.

Container

파드 내에는 하나의 독립적인 서비스를 구동할 수 있는 컨테이너가 1개 이상 존재한다. 그리고 컨테이너들은 서비스들이 연결될 수 있도록 포트를 가지고 있다. 한 컨테이너가 포트를 하나 이상 가질 수 있지만 한 파드내에서 컨테이너들끼리 포트가 중복될 수는 없다. 파드 내의 컨테이너들은 파드라는 하나의 호스트로 묶여 있다고 보면 된다. 그렇기 때문에 losthost를 이용해서 한 컨테이너에서 다른 컨테이너로 접근이 가능하다.

Label

모든 오브젝트에는 레이블을 달 수 있는데 주로 파드에 레이블을 달아서 사용한다. 레이블은 목적에 따라서 오브젝트를 분류하고 분류된 오브젝트들을 따로 골라서 연결하기 위해 사용한다. 한 파드에는 여러 레이블을 달 수 있으며 key:value 형태로 구성한다.

예를 들어, 다음과 같이 Pod metadata의 labels에 key:value 형태로 레이블을 달아서 파드를 생성하고

apiVersion: v1
kind: Pod
metadata:
  name: pod-1
  labels:
    type: web
    lo: dev
spec:
  containers:
    - name: container
    image: tmkube/init

다음과 같이 Service의 selector에 해당 파드의 레이블을 입력해서 서비스를 생성하면

apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    type: web
  ports:
    - port: 8080

셀렉터를 통해 key:value로 해당 내용과 매칭되는 레이블이 있는 파드와 연결된다.

Node Schedule

파드는 클러스터에 존재하는 여러 노드 중에 한 노드에 할당되어 생성되게 되는데 이 때, 쿠버네티스 시스템의 스케줄러에 의해 노드를 자동으로 선택해주는 방법과 직접 노드를 선택하는 방법이 있다.

먼저 전자의 경우에는 쿠버네티스 시스템의 스케줄러 레벨에서 리소스가 여유로운 노드를 판단해, 해당 노드로 파드가 생성되도록 스케줄링해준다. 후자의 경우에는 파드를 생성할 때 nodeSelector을 이용해서 직접 노드를 선택할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-2
spec:
  nodeSelector:
    hostname: node1
  containers:
    - name: container
    image: tmkube/init

Service

앞서 언급했다시피, 파드는 언제든 장애 상황으로 인해 죽거나 재생성될 수 있는 유동적인 오브젝트이기 때문에 접속 정보가 고정되어있지 않다. 그렇기 때문에 파드 접속을 안정적으로 유지하기 위한 기능이 필요한데 이러한 기능을 제공해주는 오브젝트가 서비스이다. 서비스는 파드와는 달리 사용자가 직접 지우지 않는 한 삭제되거나 재생성되지 않으므로 고정된 접속 정보를 가지고 있으며 안정적이다. 서비스에는 크게 4가지 타입이 있다.

ClusterIP

ClusterIP는 쿠버네티스 클러스터 내에서만 접근할 수 있는 IP로, 서비스는 기본적으로 자신의 ClusterIP를 가지고 있고 서비스의 디폴트 타입이다. 클러스터 내에서만 접근할 수 있기 때문에 클러스터 내의 모든 오브젝트들이 접근 가능하지만 외부에서는 접근할 수 없다. 클러스터 내부에서는 <ClusterIP:ServicePort>로 접근 가능하다.

서비스는 기본적으로 여러 개 파드와 연결할 수 있는데 이렇게 여러 파드와 연결되면 서비스가 트래픽을 분산해서 파드에 전달해준다. 즉, selector를 이용해 로드 밸런싱이 가능하다.

다음과 같이 8080포트로 오픈된 컨테이너를 가지고 있는 파드를 생성하고

apiVersion: v1
kind: Pod
metadata:
  name: pod-1
  labels:
     app: pod
spec:
  containers:
  - name: container
    image: kubetm/app
    ports:
    - containerPort: 8080

다음과 같이 서비스를 생성할 수 있는데

apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
  type: ClusterIP

서비스 타입이 ClusterIP이고 9000포트로 들어오면 app: pod 레이블을 가지고 있는 파드의 8080포트로 연결된다는 의미이다.

NodePort

ClusterIP가 클러스터 내부에서만 접근 가능한 서비스였다면, NodePort는 외부에서의 접근이 가능한 서비스 유형이다.NodePort는 외부에서 노드 IP의 특정 포트({NodeIP}:{NodePort})로 들어오는 요청을 감지하여, 연결된 파드의 해당 포트로 트래픽을 전달한다. NodePort 타입으로 만들어도 서비스에는 기본적으로 ClusterIP가 할당되기 때문에 ClusterIP 타입의 기능들을 포함하고 있다. NodePort의 특징 중 하나는 특정 NodePort 타입의 서비스와 연결되어 있는 파드가 할당되어 있는 노드들에는 같은 서비스들이 자동으로 생성되어 NodeIP가 달라도 같은 NodePort로 해당 파드들에게 접근이 가능하다는 점이다. 즉, 어느 노드이건 간에 같은 NodePort로 접속하면 해당 NodePort로 열려있는 서비스에 연결되고 해당 서비스는 파드에 트래픽을 전달해준다. 만약 같은 노드 내의 파드들에만 접근 가능하게 하고 싶다면 externalTrafficPolicy 속성을 Local로 설정하면 된다. 그렇게 되면 특정 NodePort의 IP로 접근하는 트래픽은 해당 노드에 올려져있는 파드에만 전달된다.

NodePort는 30000~32767 사이에서 할당이 가능하며, 지정하지 않으면 해당 범위에서 자동으로 할당된다.

다음과 같이 NodePort 타입의 서비스를 생성할 수 있다.

apiVersion: v1
kind: Service
metadata:
  name: svc-2
spec:
  selector:
    app: pod # 서비스가 지정될 파드 레이블 정보
  ports:
  - port: 9000 # 서비스 포트 번호
    targetPort: 8080 # 노출하고자 하는 파드의 컨테이너 포트 번호
    nodePort: 30000 # 외부 사용자가 애플리케이션에 접근하기 위한 포트 번호(선택)
  type: NodePort
  externalTrafficPolicy: Local

LoadBalancer

LoadBalancer도 외부에서 접근 가능하도록 하는 서비스 타입으로 실제 서비스 환경에서는 대부분 LoadBalancer 타입을 사용한다.

Nodeport는 각 노드의 IP를 알아야 파드에 접근할 수 있는 반면에, LoadBalancer는 클라우드 플랫폼으로부터 도메인 이름과 IP를 할당받기 때문에 Nodeport보다 쉽게 파드에 접근할 수 있다. 또한 NodePort의 경우 노드의 IP 주소가 바뀌면, 이를 반영해야 하는 단점이 있다.

기본적으로 LoadBalancer도 ClusterIP를 가지고 있으며, ClusterIP와 NodePort 성격을 가지고 있다. LoadBalancer 타입의 경우 AWS, GCP 등과 같은 클라우드 플랫폼 환경에서만 사용 가능하며 가상 환경이나 온프레미스 환경에서는 사용이 어렵다. 온프레미스 환경에서 LoadBalancer 타입의 서비스를 사용하기 위해서는 MetalLB 등을 이용해서 직접 구축해야 한다.

https://metallb.universe.tf

다음과 같이 LoadBalancer 타입으로 서비스 생성이 가능하며

apiVersion: v1
kind: Service
metadata:
  name: svc-3
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
  type: LoadBalancer

클라우드 플랫폼을 통해 외부 IP를 할당받았다면 해당 서비스의 EXTERNAL-IP와 port 정보(<EXTERNAL-IP:port>)를 이용해 원하는 파드에 접근이 가능하다.

ExternalName

ExternalName은 외부 서비스를 쿠버네티스 내부에서 호출하고자할 때 사용할 수 있다.

다음과 같이 생성할 수 있다.

apiVersion: v1
kind: Service
metadata:
  name: svc-4
spec:
  type: ExternalName
  externalName: google.com

Volume

쿠버네티스 파드 내에서 기동되는 컨테이너는 고유한 파일 시스템을 가진다. 하지만 컨테이너가 재기동되면 이전 컨테이너에서 쓰여진 파일들은 유실된다. 볼륨은 이처럼 컨테이너가 종료되더라도 파일시스템을 유지시키고 파드 내 다른 컨테이너들과 파일을 공유하는 기능을 제공하는 오브젝트이다.

emptyDir

emptyDir은 컨테이너들끼리 데이터를 공유하기 위해서 볼륨을 사용하는 것이다. 최초 볼륨이 생성될 때에는 볼륨이 비어있기 때문에 emptyDir이라고 부른다. 볼륨은 파드 내에 파드 생성 시 생성되고 파드 삭제 시 없어진다. 파드에 문제가 생겨서 재생성되면 데이터가 없어지기 때문에 임시 데이터를 저장하는데에 사용되는 간단한 볼륨이다.

다음과 같이 파드 내 컨테이너들을 emptyDir 볼륨으로 마운트할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-1
spec:
  containers:
  - name: container1
    image: kubetm/init
    volumeMounts:
    - name: empty-dir
      mountPath: /mount1
  - name: container2
    image: kubetm/init
    volumeMounts:
    - name: empty-dir
      mountPath: /mount2
  volumes:
  - name : empty-dir
    emptyDir: {}

hostPath

hostPath는 노드 파일시스템을 파드의 경로로 마운트하는 데에 사용한다. emptyDir와 다른 점은 해당 path를 각 파드들이 마운트해서 공유하기 때문에 파드들이 삭제되어도 해당 노드에 있는 데이터는 사라지지 않는 특징이 있다는 점이다. 하지만 특정 파드가 삭제되었다고 해서 해당 파드가 해당 노드에 재생성된다는 보장이 없기 때문에 이전에 마운트했던 노드의 파일시스템에 접근하지 못하는 경우가 발생할 수 있다. 각 노드에는 해당 노드만을 위해 사용되는 시스템 파일이나 설정 파일 등이 있는데 이러한 파일들을 파드가 이용할 때 주로 hostPath를 사용한다.

다음과 같이 파드 내 컨테이너들을 hostPath 볼륨으로 마운트할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-2
spec:
  nodeSelector:
    kubernetes.io/hostname: k8s-node1
  containers:
  - name: container
    image: kubetm/init
    volumeMounts:
    - name: host-path
      mountPath: /mount1
  volumes:
  - name : host-path
    hostPath:
      path: /node-v
      type: DirectoryOrCreate

hostPath에 있는 path는 파드가 만들어지기 전에 만들어져 있어야 파드가 생성될 때 에러가 발생하지 않는다.

PVC/PV

파드에서 실행 중인 컨테이너화된 앱이 디스크에 데이터를 유지해야 하고, 파드가 삭제되고 재생성되었을 경우 다른 노드로 스케줄링되더라도 동일한 데이터를 사용해야 한다면 emptyDir과 hostPath로는 불가능한데, 이러한 경우에 사용하는 볼륨 방식이 PVC/PV이다. PVC/PV 방식은 User와 Admin로 역할을 구분해서 볼륨을 운영한다. 볼륨에는 로컬 볼륨도 있고 외부의 원격으로 사용되는 볼륨도 있는데 이에 대해 Admin이 PV를 정의한다. 그리고 User가 PVC를 생성한다. 그러면 쿠버네티스에서 PVC를 PV와 연결해준다. 그런 다음 파드를 생성할 때 PVC를 사용하면 해당 볼륨에 마운트된다. PVC가 PV를 연결할 때 쿠버네티스가 자동으로 연결해주는데, capacity와 accessModes를 보고 PVC의 요청 내용에 맞게 연결해준다.

다음과 같이 PV를 정의한다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-01
spec:
  capacity:
    storage: 2G
  accessModes:
  - ReadWriteOnce
  local:
    path: /node-v
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - {key: kubernetes.io/hostname, operator: In, values: [k8s-node1]}

그리고 PVC를 정의한다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-01
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1G
  storageClassName: ""

PVC를 사용해 파드를 생성하면 해당 볼륨에 마운트된다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-3
spec:
  containers:
  - name: container
    image: kubetm/init
    volumeMounts:
    - name: pvc-pv
      mountPath: /mount3
  volumes:
  - name : pvc-pv
    persistentVolumeClaim:
      claimName: pvc-01

Namespace

네임스페이스는 단일 클러스터에서 리소스를 구분해서 관리하는 논리적인 그룹 단위이다. 네임스페이스의 특징은 다음과 같다

  • 한 네임스페이스 안에 있는 같은 타입의 오브젝트의 이름은 유일해야 한다. -> 오브젝트들마다 별도의 유효 아이디가 존재하긴 하지만 한 네임스페이스 안에서는 같은 종류의 오브젝트라면 오브젝트의 이름 또한 유효 아이디의 역할을 한다.
  • 네임스페이스가 다르면 파드의 레이블을 서비스가 레이블 셀렉터로 가리키고 있더라도 연결되지 않는다. 즉, 레이블 셀렉터는 같은 네임스페이스 안에서만 작동한다.
  • 네임스페이스를 지우게 되면 그 안의 리소스들은 모두 지워지게 된다.

물론 네임스페이스의 여러 가지 특징들은 네임스페이스 기반 오브젝트(디플로이먼트, 서비스 등)에만 적용되며, 클러스터 범위의 오브젝트(스토리지 클래스, 노드, 퍼시스턴트 볼륨 등)는 적용되지 않는다.

다음과 같이 네임스페이스를 생성할 수 있으며

apiVersion: v1
kind: Namespace
metadata:
  name: nm-1

다음과 같이 파드와 서비스를 생성할 때 해당 네임스페이스 정보를 metadata에 입력해서 해당 네임스페이스 단위로 관리할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-1
  namespace: nm-1
  labels:
    app: pod
spec:
  containers:
  - name: container
    image: kubetm/app
    ports:
    - containerPort: 8080
apiVersion: v1
kind: Service
metadata:
  name: svc-1
  namespace: nm-1
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080

ResourceQuota와 LimitRange 두 오브젝트들로 네임스페이스 단위, 파드 단위의 자원 사용을 제한할 수 있다.

ResourceQuota

리소스 쿼터는 네임스페이스에 사용 가능한 자원에 제한을 걸어 다른 네임스페이스에 부정적인 영향을 끼치지 않도록 해주는 오브젝트이다.

각 파드는 클러스터 자원을 공유해서 필요한 자원을 사용한다. 그런데 한 네임스페이스에 있는 파드들이 클러스터에 있는 남은 모든 자원들을 사용해버리면 다른 네임스페이스에 있는 파드 입장에서는 더 이상 가용 자원이 없게 되어 자원이 필요할 때 문제가 발생하게 된다. 이러한 상황을 미연에 방지하기 위해 리소스 쿼터가 존재한다.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: rq-1
  namespace: nm-1
spec:
  hard:
    requests.memory: 3Gi
    limits.memory: 6Gi

LimitRange

리밋 레인지는 파드의 자원 사용량을 제한해서 한 네임스페이스에서 특정 파드가 과도하게 자원을 점유하는 것을 막아주는 오브젝트이다.

한 파드가 자원 사용량을 크게 가져가면 다른 파드들이 해당 네임스페이스에 더 이상 들어올 수 없게 된다. 이러한 상황을 미연에 방지하기 위해 리밋 레인지를 줘서 네임스페이스에 들어오는 파드의 크기를 제한할 수 있다. 이렇게 한 파드의 자원 사용량이 리밋 레인지보다 낮아야 해당 네임스페이스에 들어갈 수 있다. 아님 들어갈 수 없다. 이 두 오브젝트는 네임스페이스 뿐만 아니라 클러스터에도 달 수 있다. 자원에 대한 제한을 걸 수 있다.

apiVersion: v1
kind: LimitRange
metadata:
  name: lr-1
  namespace: nm-1
spec:
  limits:
  - type: Container
    min:
      memory: 0.1Gi
    max:
      memory: 0.4Gi
    maxLimitRequestRatio:
      memory: 3
    defaultRequest:
      memory: 0.1Gi
    default:
      memory: 0.2Gi

ConfigMap과 Secret

같은 애플리케이션도 개발 환경과 운영 환경 등 환경에 따라 변하는 설정값들이 있는데 이러한 값들 때문에 큰 용량의 이미지를 별도로 관리하는 것은 비효율적이다. 이처럼 환경에 따라 변하는 값들을 외부에서 설정할 수 있게 해주는 오브젝트가 있는데 ConfigMap과 Secret이 있다. 일반적인 값들은 ConfigMap으로 관리하고, Password, OAuth Token, SSH KEY와 같이 보안적인 관리가 필요한 값들은 Secret으로 관리한다. 두 오브젝트를 연결해서 파드를 생성하게 되면 해당 값들이 컨테이너의 환경 변수에 들어가게 된다. 두 오브젝트를 이용하게 되면 개발 환경과 운영 환경에서 ConfigMap과 Secret만 바꿔주면 같은 컨테이너 이미지를 사용하더라도 원하는 환경에 배포할 수 있다.

ConfigMap과 Secret을 사용하는 방법에는 크게 3가지 방법이 있다.

1. Env(Literal)

먼저 상수로 사용하는 방법이 있다. key:value로 구성된 ConfigMap과 Secret을 생성하면 두 오브젝트가 연결된 파드에서는 key를 이용해 해당 값들을 가져와서 컨테이너의 환경변수를 세팅할 수 있다. 이 방법의 경우 환경변수가 파드에 한번 적용되고 나면 ConfigMap이나 Secret의 데이터가 바뀌어도 파드의 설정값에는 영향이 없다.

간단한 예제를 보자면, ConfigMap을 만들고

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-dev
data:
  SSH: 'false'
  User: dev

키, 밸류는 모두 스트링 값이기 때문에 불리언값으로 넣고 싶다면 '' 쿼테이션을 달아서 넣어줘야 한다.

Secret을 생성한 다음에

apiVersion: v1
kind: Secret
metadata:
  name: sec-dev
data:
  Key: MTIzNA==

파드를 생성할 때 다음과 같이 래퍼런스를 달아주면 된다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-1
spec:
  containers:
  - name: container
    image: kubetm/init
    envFrom:
    - configMapRef:
        name: cm-dev
    - secretRef:
        name: sec-dev

2. Env(File)

두 번째로 파일을 환경변수로 넣는 방법이 있다. 이 때 파일 이름이 key, 파일 내용(혹은 Secret의 경우 Base64)이 value가 된다. 이 방법 또한 환경변수를 한번 주입하면 ConfigMap이나 Secret의 데이터가 바뀌어도 파드의 설정값에는 영향이 없다.

다음과 같이 두 오브젝트에 대한 파일을 생성하고

echo "Content" >> file-c.txt
kubectl create configmap cm-file --from-file=./file-c.txt

echo "Content" >> file-s.txt
kubectl create secret generic sec-file --from-file=./file-s.txt

파일 이름을 키로 참조하도록 하면 된다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-file
spec:
  containers:
  - name: container
    image: kubetm/init
    env:
    - name: file-c
      valueFrom:
        configMapKeyRef:
          name: cm-file
          key: file-c.txt
    - name: file-s
      valueFrom:
        secretKeyRef:
          name: sec-file
          key: file-s.txt

3. Volume Mount

마지막으로 파일을 마운팅하는 방법이 있다. 파일을 ConfigMap이나 Secret에 담는 것까지는 두 번째 방법과 같다. 그런 다음 마운트 패스를 정의하고 파일들을 해당 볼륨의 패스로 마운트를 하고 파드 생성 시에 ConfigMap나 Secret 정보를 입력하고 볼륨 정보를 입력하면 된다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-mount
spec:
  containers:
  - name: container
    image: kubetm/init
    volumeMounts:
    - name: file-volume
      mountPath: /mount
  volumes:
  - name: file-volume
    configMap:
      name: cm-file

볼륨 마운트 방식은 원본 파일을 참조하고 있기 때문에 환경변수 방식과는 달리 파일의 내용이 변하면 파드의 설정값들도 같이 변하게 된다.

Reference

profile
Grind Hard, Shine Hard

0개의 댓글