2주차 - 다양한 유형의 앱 배포하기

월간-CS | 쿠버네티스 비기너 클래스
이민석's avatar
Jul 24, 2024
2주차 - 다양한 유형의 앱 배포하기

지금까지

아래 과정을 수행하였습니다.

  1. 1주차 - Docker & EKS 기초

Pod, ReplicaSet, Deployment, Service

이미 수차례에 걸쳐서 배운 내용을 다시 복습하였습니다.

  1. Pod : K8s에서 어플리케이션 컨테이너를 실행하는 가장 작은 단위

  2. ReplicaSet : 지정된 replicaset 숫자만큼 Pods의 수량이 유지도록하는 오브젝트

  3. Deployment : ReplicaSet 배포와 버전관리를 수행해주는 오브젝트

  4. Service : Pod들 사이의 네트워크 통신을 관리 + 외부 트래픽을 Pod로 라우팅하는 K8s 오브젝트

실습 자료

Pod, ReplicaSet, Deployment, Service를 생성하는 실습을 진행하였습니다.

추가 공부 - Tips

강의에 나오지 않는 부분들에 대해서 추가로 공부를 진행하였습니다.

  1. Graceful Pod Shutdown with PreStop Hooks

  2. Debugging Pods with Ephemeral Containers

  3. Horizontal Pod Autoscaling Based on Custom Metrics

  4. Using Init Containers for Setup Scripts

  5. Node Affinity for Workload-Specific Scheduling

  6. Taints and Tolerations for Pod Isolation

  7. Pod Priority and Preemption for Critical Workloads

  8. ConfigMaps and Secrets for Dynamic Configuration

  9. Kubectl Debug for Direct Container Debugging

  10. Effiecient Resource Management with Requests and Limtis

이 중 회사(Carrieverse)에서 도입하면 좋을 것들은 다음과 같았습니다.

  1. Graceful Pod Shutdown with PreStop Hooks
    Server Application의 Transaction 완료 보장

  2. Horizontal Pod Autoscaling Based on Custom Metrics
    Server Application의 Scalability 보장

  3. Using Init Containers for Setup Scripts

    Server Application의 Environment Validation 보장

  4. Node Affinity for Workload-Specific Scheduling
    Server Application 별로 별개의 Node 할당 보장

  5. Pod Priority and Preemption for Critical Workloads

    Server Application과 Batch Application 간의 우선순위 지정

Graceful Pod Shutdown with PreStop Hooks

PreStop 훅을 사용하면 파드가 종료되기 전, 특정 명령이 실행 가능합니다.

이 기능으로 아래의 기능을 구현할 수 있습니다.

  1. 애플리케이션 정상 종료 보장

  1. 필요한 상태를 저장

  2. 데이터 손상을 방지

  3. 원활한 재시작 보장

아래 예제를 보면 30초 동안 상태가 유지된 이후 nginx가 종료되도록 하고 있습니다. 이를 활용하면 K8s Service Object와의 연결이 끊긴 Pods가 작업 중인 트랜잭션 처리의 완료를 보장할 수 있습니다.


apiVersion: v1
kind: Pod
metadata:
  name: graceful-shutdown-example

spec:
  container:
  - name : sample-container
    image: nginx
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 30 && nginx -s quit"]

서비스 연속성이 중요하며, 배포/확장/파드 재활용 중에 서비스 다운타임이 없거나 최소화되어야 하는 환경에서 PreStop Hook을 사용할 수 있습니다.

.spec.terminationGracePeriodSeconds 옵션과 Prestop Hooks와 충돌할 수 있습니다.

[Bad Case]
preStop 으로 명시된 시간보다 .spec.terminationGracePeriodSeconds가 길면 문제가 발생합니다.

  1. .spec.terminationGracePeriodSeconds: 30

  2. preStop: /bin/sh -c sleep 60

Debugging Pods with Ephemeral Containers

임시 컨테이너(Ephemeral Containers)를 사용하면 Pods 사양을 변경하지 않고 파드에 디버그 컨테이너(Debug Container)를 연결할 수 있습니다.

서비스를 중단할 여유가 없는 프로덕션 환경에서 실시간 문제를 디버깅하는데 사용할 수 있습니다.

다음 명령어를 사용하세요.

kubectl alpha debug \
   --it <POD_NAME>  \
   --image=busybox  \
   --target=<CONTAINER_NAME>

위 명령어는 기존 파드에 busybox 컨테이너를 추가하여 실행 중인 상태를 변경하지 않고도 명령을 실행하고 파드 환경을 검사할 수 있습니다.

  1. 표준 로그와 매트릭이 충분한 정보를 제공하지 않는 경우

  2. 프로덕션 문제를 심층적으로 분석할 수 있는 경우

단 임시 컨테이너는 파드의 리소스와 민감한 데이터에 엑세스할 수 있으므로 심각한 보안 취약점으로 작용할 수 있습니다. 따라서 권한이 있는 사용자 만 임시 컨테이너 배포가 가능하도록 해야 합니다.

Horizontal Pod Autoscaling Based on Custom Metrics

HPA를 이용하면 CPU / MEM Usage 뿐만 아니라 커스텀 매트릭을 이용해서 파드의 숫자를 늘리고 줄일 수 있습니다.

아래 예시는 <Your_Custom_Metric>의 평균값을 기준으로 애플리케이션 규모를 조정합니다.

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: custom-metric-hpa

spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: <Application>
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Pods
    pods:
      metric:
        name: <Your_Custom_Metric>
      target:
        type: AeverageValue
        averageValue: 10

일반적인 어플리케이션은 CPU / MEM 등으로 스케일링을 하는 것이 일반적일 수 있습니다. 하지만 특수한 비즈니스 요구사항에 따라 미세 조정된 스케일리 동작이 필요한 경우, 사용자 정의 매트릭이 필요할 수 있습니다.

단, 이런 사용자 정의 매트릭을 사용하기 위해서는 Prometheus나 Metric Server 등의 매트릭 서버와의 통합이 필요합니다.

Using Init Containers for Setup Scripts

어플리케이션 컨테이너보다 먼저 설정되어야 하는 설정파일 및 코드들을 Init Container를 이용해서 사용할 수 있습니다.

myapp-container가 실행되기 전, myservice가 필요하다면 다음과 같이 Init Container를 사용할 수 있습니다.

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
spec:
  containers:
  - name: myapp-container
    image: myapp
  initContainers:
  - name: init-myservice
    image: busybox
    command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']

개인적으로 매우 유용한 기능이라고 생각합니다.
코드 레벨의 구현 방식에 따라 RDS, Redis, Redshift 등이 연결되지 않았음에도 서버가 켜지는 경우가 있습니다.

이 경우, 실제 동작하는 서버가 동작하지 않는 서버로 롤아웃될 가능성이 있습니다. 따라서 K8s 엔지니어는 InitContainer를 적극활용하여 RDS, Reids, Redshift 등과의 네트워크 연결 / 앤드포인트 / 유저네임 / 비밀번호 상태 검사를 진행하는 것이 좋아보입니다.

Node Affinity for Workload-Specific Scheduling

Pod Nod Afinity는 특정 노드에만 Pods를 스케줄링하기 위한 제약조건을 설정하는 방법입니다. 사용자가 정의한 규칙에 따라 Pod가 특정 라벨을 가진 노드에만 실행되도록 합니다.

NodeAfinity는 크게 필수 조건(Mandatory)과 선호 조건(Perfereed)로 나뉩니다.

  • 필수 조건 : .spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution

  • 선호 조건 : .spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution

아래 예시를 통해서 필수 조건에 대한 예시를 볼 수 있습니다.

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  containers:
  - name: with-node-affinity
    image: nginx
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd

유사한 기능으로는 Taints 와 Toleration이 존재합니다.

Taints and Tolerations for Pod Isolation

Taints와 Toleration을 사용하면 노드가 특정 파드를 거부하도록 설정할 수 있습니다.

Taints가 걸려있는 노드는 기본적으로 모든 파드를 거부합니다.
Taints는 오직 적합한 Toleration이 설정된 파드만 받아들이게 되어 있습니다.

Pod Priority and Preemption for Critical Workloads

때로 절대로 중단되면 안되는 중요한 워크로드가 존재할 수 있습니다.
이 경우 PriorityClass를 이용해서 높은 우선순위를 할당할 수 있습니다.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: false

---

apiVersion: v1
kind: Pod
metadata:
  name: high-priority-pod

spec:
  containers:
  - name: high-priority
    image: nginx
  priorityClassName: high-priority

ConfigMaps and Secrets for Dynamic Configuration

Pods에 데이터를 주입하기 위해서는 ConfigMap 혹은 Secret을 사용할 수 있습니다.
각종 환경설정을 외부에서 주입할 수 있으므로 어플리케이션을 더 유연하게 만들 수 있습니다.

  • ConfigMap은 중요하지 않는 데이터에 중요합니다.

  • Secret은 중요한 데이터에 적합합니다.

apiVersion: v1
kind: Config
metadata:
  name: app-config
data:
  config.json: |
    {
      "key": "value",
      "databaseURL": "https://mydatabase.example.com"
    }
---
apiVersion: v1
kind: Config
metadata:
  name: myapp-pod
spec:
  containers:
    - name: myapp-container
      image: myapp

  volumeMounts:
    - name: config-volume
      configMap:
        name: app-config

회사에서 Secrets만 사용하고 있어서 발생하는 문제점이 참 많았습니다.
RSD Endpoint / Username / Password / Salt / JWT Secrets와 같이 중요하고 변경되지 않는 값도 있지만, ExpiredTime 같이 정책 측면에서 자주 변경되는 값도 있었습니다.

SSM Parameter Store & Secret Manager와 연동한 Secrets을 도입하기 전,
자주 변경되는 Secrets values-dev.yaml 파일의 관리가 어려웠습니다.

하지만 자주 변경되는 ExpiredTime 등은 Config.values-dev.yaml 파일로 Git/SVN에 올리는 것이 더 좋은 해결책이었던 것 같습니다.

Kubectl Debug for Direct Container Debugging

kubectl debug를 활용하면 이미 존재하는 파드에 대한 복제본을 만들고 이를 디버그 버전으로 사용할 수 있습니다.

kubectl debug pod/myapp-pod    \
   -it --copy-to=myapp-debug   \
   --container=myapp-container \
   --image=busybox

앞서 임시 컨테이너(Ephemeral Containers)과 비교하면 다음과 같습니다.

  • 공통점

    • 라이브 환경에서 어플리케이션 실행 상태에 영향을 주지 않고 문제를 디버깅

    • 민감한 데이터에 엑세스할 수 있음

  • 차이점

    • kubectl debug는 전체 클러스터 리소스 할당에 영향을 줌

Efficient Resource Management with Requests and Limits

K8s는 각 파드의 각 컨테이너에 대해서 CPU 및 메모리 요청과 제한을 지정할 수 있습니다.

  • 요청 : 컨테이너가 지정된 양의 리소스를 받도록 보장

  • 제한 : 컨테이너가 할당된 양을 초과하여 사용하지 않도록 보장

이는 리소스 할당을 효율적으로 관리하고 단일 어플리케이션이 클러스터 리소스를 독점을 방지하는 데에 도움이 됩니다.

특히 다음과 같은 서비스에서 CPU, MEM 과사용으로 서버 다운이 되는 것을 방지할 수 있어 보입니다.

  • 임시 컨테이너(Ephemeral Containers)나 쿠버네티스 디버그(K8s Debug) 등으로 라이브 환경 디버깅 중 잘못 입력한 스크립트로 CPU, MEM 사용량 폭증

  • 신규 Init Container & Container 배포 과정 중 잘못된 스크립트로 CPU, MEM 사용량 폭증

다음 예시를 통해서 요청(Requests) 및 제한(Limits)을 살펴볼 수 있습니다.

apiVersion: v1
kind: Pod
metadata:
  name: resource-demo

spec:
  containers:
  - name: demo-container
    image: nginx
    resources:
      requests:             # 요청
        memory: "64Mi"
        cpu: "250m"
      limits:               # 제한
        memory: "128Mi"
        cpu: "500m"

이러한 .spec은 파드가 특정한 양의 CPU & MEM을 요청하여 최적화 성능에 필요한 리소스를 확보하며, 동시에 지정된 한도를 초과하지 않도록 합니다.

  1. 모든 컨테이너에 요청 및 제한을 적용하여 예측 가능한 어플리케이션 성능을 보장

  2. 클러스터에서 실행 중인 애플리케이션 간의 리소스 경합 방지

이 경우 다음과 같은 주의 사항이 있습니다.

  1. .spec.containers[*].resources.limits를 너무 낮게 설정하면 클러스터가 요청된 리소스를 제공할 수 없는 경우, 파드가 종료되거나 스케줄링 되지 않을 수 있습니다.

  2. .spec.containers[*].resources.limits를 너무 높게 설정하면 클러스터 리소스를 비효율적으로 사용할 수 있습니다.

따라서 어플리케이션 성능을 모니터링하고 필요에 따라서 요청과 제한을 조정할 필요가 있습니다.

Custom Resource Definitions (CRDs) for Extending Kubernetes

CRDs를 사용하여 K8s에 존재하지 않는 새로운 타입의 오브젝트를 생성할 수 있습니다. 이 오브젝트는 k8s api / kubelet 등을 통해서 통신할 수 있습니다.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crontabs.stable.example.com
spec:
  group: stable.example.com
  versions:
    - name: v1
      served: true
      storage: true
  scope: Namespaced
  names:
    plural: crontabs
    singular: crontab
    kind: CronTab
    shortNames:
    - ct

단, 잘못 생성한 CRDs는 클러스터 전체 성능에 영향을 줄 수 있습니다.
따라서 항상 충분한 테스트를 하고 이를 도입해야 합니다.

EKS Storage

기본 Manifest

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.co
volumeBindingMode: WaitForFirstCosumer

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-mysql-pv-claim

spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ebs-cs
  resources:
    requests:
      storage: 4Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: usermanagement-decreation-script

data:
  mysql_usermgmt.sql: |-
    DROP DATABASE IF EXISTS usermgmt;
    CREATE DATABASE usermgmt;

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql

spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:5.6
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: dbpassword11

          ports:
            - containerPort: 3306
              name: mysl

          volumeMounts:
            - name: mysql-persistent-storage
              mountPath: /var/lib/mysql
            - name: usermanagement-decreation-script
              mounthPath: /docker-entrypoint-initdb.d

      volumes:
        - name: mysql-persistent-storage
          persistentVolumeClaim: ebs-mysql-pv-claim
        - name: usermanagement-decreation-script
          configMap:
            name: usermanagement-decreation-script
---
apiVersion: apps/v1
kind: Service
metadata:
  name: mysql
spec:
  selector:
    app: mysql
  ports:
    - port: 3306
  clusterIP: None # Pod IP


---
# APPS
apiVersion: apps/v1
kind: Deployment
metadata:
  name: usermgmt-microservice
  labels:
    app: usermgmt-restapp

spec:
  replicas: 1
  selector:
    matchLabels:
      app: usermgmt-restapp
  template:
    metadata:
      labels:
        app: usermgmt-restapp
    spec:
      containers:
        - name: usermgmt-restapp
          image: 
          ports:
           - containerPort: 8095
          env:
           - name: DB_HOST_NAME
             value: "mysql
           - name: DB_PORT
             value: "3306"
           - name: DB_NAME
             value: "usermgmt"
           - name: DB_USERNAME
             value: "root"
           - name: DB_PASSOWRD
             value: "dbpassword1!"
---
apiVersion: v1
kind: Service
metadata:
  name: usermgmt-restapp-service
  labels:
    app: usermgmt-restapp
spec:
  type: NodePort
  selector:
    app: usermgmt-restapp
  ports:
   - port: 8095
     targetPort: 8095
     nodePort: 31231

참고자료

Share article

Unchaptered