지금까지
아래 과정을 수행하였습니다.
Pod, ReplicaSet, Deployment, Service
이미 수차례에 걸쳐서 배운 내용을 다시 복습하였습니다.
Pod : K8s에서 어플리케이션 컨테이너를 실행하는 가장 작은 단위
ReplicaSet : 지정된
replicaset
숫자만큼 Pods의 수량이 유지도록하는 오브젝트Deployment : ReplicaSet 배포와 버전관리를 수행해주는 오브젝트
Service : Pod들 사이의 네트워크 통신을 관리 + 외부 트래픽을 Pod로 라우팅하는 K8s 오브젝트
실습 자료
Pod, ReplicaSet, Deployment, Service를 생성하는 실습을 진행하였습니다.
추가 공부 - Tips
강의에 나오지 않는 부분들에 대해서 추가로 공부를 진행하였습니다.
Graceful Pod Shutdown with PreStop Hooks
Debugging Pods with Ephemeral Containers
Horizontal Pod Autoscaling Based on Custom Metrics
Using Init Containers for Setup Scripts
Node Affinity for Workload-Specific Scheduling
Taints and Tolerations for Pod Isolation
Pod Priority and Preemption for Critical Workloads
ConfigMaps and Secrets for Dynamic Configuration
Kubectl Debug for Direct Container Debugging
Effiecient Resource Management with Requests and Limtis
이 중 회사(Carrieverse)에서 도입하면 좋을 것들은 다음과 같았습니다.
Graceful Pod Shutdown with PreStop Hooks
Server Application의 Transaction 완료 보장Horizontal Pod Autoscaling Based on Custom Metrics
Server Application의 Scalability 보장Using Init Containers for Setup Scripts
Server Application의 Environment Validation 보장
Node Affinity for Workload-Specific Scheduling
Server Application 별로 별개의 Node 할당 보장Pod Priority and Preemption for Critical Workloads
Server Application과 Batch Application 간의 우선순위 지정
Graceful Pod Shutdown with PreStop Hooks
PreStop 훅을 사용하면 파드가 종료되기 전, 특정 명령이 실행 가능합니다.
이 기능으로 아래의 기능을 구현할 수 있습니다.
애플리케이션 정상 종료 보장
필요한 상태를 저장
데이터 손상을 방지
원활한 재시작 보장
아래 예제를 보면 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가 길면 문제가 발생합니다.
.spec.terminationGracePeriodSeconds
: 30preStop:
/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 컨테이너를 추가하여 실행 중인 상태를 변경하지 않고도 명령을 실행하고 파드 환경을 검사할 수 있습니다.
표준 로그와 매트릭이 충분한 정보를 제공하지 않는 경우
프로덕션 문제를 심층적으로 분석할 수 있는 경우
단 임시 컨테이너는 파드의 리소스와 민감한 데이터에 엑세스할 수 있으므로 심각한 보안 취약점으로 작용할 수 있습니다. 따라서 권한이 있는 사용자
만 임시 컨테이너 배포가 가능하도록 해야 합니다.
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을 요청하여 최적화 성능에 필요한 리소스를 확보하며, 동시에 지정된 한도를 초과하지 않도록 합니다.
모든 컨테이너에 요청 및 제한을 적용하여 예측 가능한 어플리케이션 성능을 보장
클러스터에서 실행 중인 애플리케이션 간의 리소스 경합 방지
이 경우 다음과 같은 주의 사항이 있습니다.
.spec.containers[*].resources.limits
를 너무 낮게 설정하면 클러스터가 요청된 리소스를 제공할 수 없는 경우, 파드가 종료되거나 스케줄링 되지 않을 수 있습니다..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