Kubernetes Deep Dive - (4). Pods and Containers
Pods / Containers in k8s
Manage Application Configuration in k8s
일반적으로 Application의 Config는 Dynamical하게 관리되는 경우가 많음.
k8s의 경우 runtime 시점에서 config을 자유롭게 변경해서 적용할 수 있도록 지원하고 있다.
- Non-sensitive : 토큰이나 auth key 등 confidential을 제외한 데이터들
- ConfigMap과 pods / containers는 M:N 관계. 숫자나 범위 제한 없이 자유롭게 매핑이 가능하다.
- secret 파일 생성에 쓰인 username.txt와 password.txt는 secret을 생성한 뒤에는 서버에서 삭제해도 된다. Secret 객체가 encoded username / password 값을 가지고 있기 때문.
Container에는 Env 환경변수 형태로 적용할 수 있다. 위 사진에서
- 컨테이너에서 사용할 환경변수의 이름은 SPECIAL_LEVEL_KEY
- configMap object의 이름은 name 필드 (special-config)
- configMap에서 읽어들일 key-value pair에서 key값: special-how.
즉 runtime에서 special-config라는 이름의 ConfigMap 객체를 참조한 뒤, special-how라는 key값을 읽어들인다. 해당 key값에 대응되는 value가 SPECIAL_LEVEL_KEY에 저장된다.
Hands-On: ConfigMap, Secret
configMap.yaml 파일을 생성하고,
kubectl apply -f configMap.yaml
로 객체 생성kubectl get configmaps
로 생성한 객체를 확인한다.kubectl describe configmap <이름>
으로 특정 configMap의 상세내용을 확인할 수 있다.
apiVersion: v1
kind: ConfigMap
metadata:
name: player-pro-demo # ConfigMap 이름.
data:
# property-like keys; each key maps to a simple value
# key-value 형태.
player_lives: "5"
properties_file_name: "user-interface.properties"
# file-like keys
base.properties: | # pipe는 multiple key를 정의하기 위한 형태.
enemy.types=aliens,monsters
player.maximum-lives=10
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
user-interface.properties가 key인 경우 multiple values, 마찬가지로 base.properties에도 multiple values가 할당되어 있는 걸 볼 수 있다.
Secret의 형식은 대략 아래와 같다.
apiVersion: v1
kind: Secret
metadata:
name: example-secret
type: Opaque
stringData:
username: YWRtaW4=
password: YWRtaW5wYXNzd29yZA==
- Secret에도 여러 가지 Type이 있음.
- 예시에서는 username, password를 string으로 직접 입력했지만, 이 방법은 그리 권장하지 않음. 여기에는 base64로 인코딩된 값이 들어가는 게 좋다.
echo -n 'admin' | base64
-> YWRtaW4=echo -n 'adminpassword' | base64
-> YWRtaW5wYXNzd29yZA==
kubectl apply -f secret.yaml
로 등록하고,kubectl get secrets
로 등록된 객체 확인 가능.
secret의 경우 describe로 상세정보를 확인하려 해도, 데이터를 정확히 공개하지 않는다.
configMap과 Secret을 Env 환경변수로 쓰기 위한 yaml 파일. pod를 실행한다.
apiVersion: v1
kind: Pod
metadata:
name: configmap-env-demo
spec:
containers:
- name: configmap-demo
image: alpine
command: ["sleep", "3600"]
env:
# Define the environment variable
- name: PLAYER_LIVES # 변수명
valueFrom: # 이 환경변수는 무엇을 참조할 것인지
configMapKeyRef:
name: player-pro-demo # The ConfigMap this value comes from.
key: player_lives # The key to fetch.
- name: PROPERTIES_FILE_NAME
valueFrom:
configMapKeyRef:
name: player-pro-demo
key: properties_file_name
- name: SECRET_USERNAME
valueFrom:
secretKeyRef:
name: example-secret
key: username
- name: SECRET_PASSWORD
valueFrom:
secretKeyRef:
name: example-secret
key: password
Application Config using Mount Volumes
configMap과 같은 오브젝트를 사용하는 대신, Volume에 직접 파일을 두어 사용할 수도 있다.
apiVersion: v1
kind: Pod
metadata:
name: configmap-vol-demo
spec:
containers:
- name: configmap-vol-demo
image: alpine
command: ["sleep", "3600"]
# volumes에서 정의한 객체가 mount될 path를 정의.
volumeMounts:
- name: player-map
mountPath: /etc/config/configMap
- name: player-secret
mountPath: /etc/config/secret
volumes:
# You set volumes at the Pod level, then mount them into containers inside that Pod
- name: player-map
configMap:
# Provide the name of the ConfigMap you want to mount.
# configMap 오브젝트의 이름 등록.
name: player-pro-demo
- name: player-secret
# secret을 등록하려면 secret의 이름을 등록한다.
secret:
secretName: example-secret
이 pod를 kubectl apply -f 로 생성하고, 해당 pod로 들어가면
kubectl exec configmap-vol-demo -it -- sh
/etc/config/configMap 항목에 파일 형태로 저장되어 있는 걸 확인할 수 있다.
Manage Application Configuration Posix ConfigMap
configMap-env-demo처럼 매번 pod -> container의 환경변수처럼 사용할 수 있지만, container의 환경변수 형태로 선언하는 대신 다른 방법으로 config를 정의해 활용할 수 있다. "posix configMap".
apiVersion: v1
kind: ConfigMap
metadata:
name: player-posix-demo
data:
PLAYER_LIVES: "5"
PROPERTIES_FILE_NAME: "user-interface.properties"
BASE_PROPERTIES: "Template1"
USER_INTERFACE_PROPERTIES: "Dark"
- 환경변수의 key-value 조합은 반드시 1:1이어야 한다.
- key값은 대문자이며, separate pair는 반드시 언더바 (_)로 지정되어야 한다.
- 이렇게 정의하면, configMap을 생성하고 등록할 때 컨테이너의 환경변수로 자동 등록해준다. key값은 환경변수의 이름, value값은 해당 환경변수의 값이 된다.
apiVersion: v1
kind: Pod
metadata:
name: configmap-posix-demo
spec:
containers:
- name: configmap-posix
image: anshuldevops/kubernetes-web:1.10.6
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: player-posix-demo
posix configMap을 적용하기 위한 컨테이너. 환경변수를 envFrom으로 정의하고, configMapRef로 configMap 객체의 이름을 지정하면 된다. 환경변수가 제대로 지정되었는지 확인하려면, configmap-posix-demo 컨테이너로 들어가서 printenv
명령어를 sh에 입력하면 됨.
ConfigMap & Secret from Files
sudo apt-get update
apt install apache2-utils
htpasswd -c .htpasswd user
# ht 연결을 위한 username = user, password는 명령어 실행하면 입력창이 뜬다. 이 세션에서 사용하기 위해 계속 기억해야 하는 비밀번호.
# 명령어 실행 결과로 .htpasswd 파일이 생성된다. 들어가보면 username: encrypted_password 형태로 key-value pair가 들어 있다.
# 이렇게 만들어진 파일을 secret으로 사용할 수 있음. nginx-htpasswd라는 이름의 secret을 만들었지만, 이름은 커스터마이즈 가능
kubectl create secret generic nginx-htpasswd --from-file .htpasswd
kubectl describe screts nginx-htpasswd
# kubectl로 secret 파일을 만들었다면, 생성에 사용되었던 파일은 삭제하는 게 좋다.
nginx.conf 파일을 토대로 configMap을 생성하는 방법
- nginx.conf 파일
user nginx worker_processes auto
error_log /var/log/nginx/error.log notice
pid /var/run/nginx.pid
events
{
worker_connections 1024
}
http
{
server
{
listen 80
server_name localhost
location /
{
root /usr/share/nginx/html
index index.html index.htm
}
auth_basic "Secure Site" # basic auth
auth_basic_user_file conf/.htpasswd # auth에 쓰이는 파일의 경로
}
}
이 conf 파일을 토대로 configMap을 생성하려면
`kubectl create configmap nginx-config-file --from-file nginx.conf`
```yml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: nginx-container
image: 'nginx:1.19.1'
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config-volume # configMap
mountPath: /etc/nginx # nginx config 파일의 디폴트 경로
- name: htpasswd-volume # secret
mountPath: /etc/nginx/conf
volumes:
- name: nginx-config-volume
configMap:
name: nginx-config-file
- name: htpasswd-volume
secret:
secretName: nginx-htpasswd
이제 nginx-pod yaml을 토대로 kubectl apply -f nginx-pod.yaml
로 pod를 올리면 된다.
Manage Container Resources in k8s
k8s에서 컨테이너를 생성하고 관리하는 것 중 '리소스' 관련한 내용.
- 특정 user가 container를 생성 요청하고 할당받을 때, 얼마나 resource 요청할 수 있는지 limit을 정할 수 있음.
- 리소스가 부족한 노드의 경우 kube scheduler에서 스케줄링 대상에서 빼버림. 즉 허용 가능한 리소스보다 더 많은 양이 필요한 경우 아예 컨테이너를 가동하지 않는다.
cf. 리소스를 얼마나 요청할지 결정하는 건 container이다.
또한, container에서 limit 제한을 걸어둔다 해도 정확히 그만큼만 사용할 수 있는 것도 아님.
kube scheduler에서 스케줄링에 참고하기 위해 사용하는 용도라고 보면 된다. 실제 사용량과 동일하지는 않음. 스케줄러는 container가 제시한 리소스를 참고로 해서, 실제로 그 정도 리소스 여유분이 있을 경우 컨테이너에게 제공하는 역할.
- 만약 500 CPU unit을 요청했다 -> 1/2 vCPU 사용.
- Limit의 경우, container의 리소스 사용량 제한을 위해 존재하며
- Runtime 시점에서 결정된다. 실제 cpu와 메모리 사용량은 runtime 시점에서부터 측정 가능하기 때문.
limit에 도달했을 경우 처리 방식은 조금씩 다르다. 위의 예시에서
- cpu 한도에 도달했을 경우: 프로세스 쓰로틀링이 진행되지만 컨테이너를 종료하지는 않는다.
- memory 한도에 도달했을 경우: 초과 시 kill container -> restart policy를 토대로 container를 재시작한다.
먼저, 같은 양의 resource를 요구하는 두 개의 pod를 만든다.
apiVersion: v1
kind: Pod
metadata:
name: frontend-1
spec:
containers:
- name: app
image: alpine
command: ["sleep", "3600"]
resources:
requests:
memory: "64Mi"
cpu: "250m"
---
apiVersion: v1
kind: Pod
metadata:
name: frontend-2
spec:
containers:
- name: app
image: alpine
command: ["sleep", "3600"]
resources:
requests:
memory: "64Mi"
cpu: "250m"
---
apiVersion: v1
kind: Pod
metadata:
name: frontend-3
spec:
containers:
- name: app
image: alpine
command: ["sleep", "3600"]
resources:
requests: # 요청량
memory: "64Mi"
cpu: "750m"
---
apiVersion: v1
kind: Pod
metadata:
name: frontend-4
spec:
containers:
- name: app
image: alpine
command: ["sleep", "3600"]
resources:
requests:
memory: "64Mi"
cpu: "750m"
cluster에서 top
입력 후 1
을 누르면, 해당 클러스터의 리소스 현황을 볼 수 있다.
- 현재 cpu 사용가능한 양은 1vCPU 정도 (1000 unit) 라고 한다.
- 3과 4번 포트의 경우 request cpu양을 의도적으로 크게 만든 뒤 kubectl apply를 실행하면 아래와 같은 에러가 발생한다.
리소스가 부족해서 컨테이너를 실행할 수 없다는 내용. 정확히는, 처음에 250m으로 실행해서 네 개 전부 정상적으로 실행되었지만, 750으로 변경한 pod 요청을 반영할 수 없다는 내용의 에러다.
kubectl delete -f 파일명.yaml
로 pods를 지워준다. 위 파일에서는 4개의 pod를 한 파일에 정의했기 때문에 4개 pod 전부 지워진다.
- 리소스 요청을 감당할 수 없는 pod의 경우 스케줄러가 아예 관리대상에서 빼 버린다. 그래서 750m을 요청한 Pod의 경우 pending 상태를 유지하게 된다.
- cpu 요청량을 수용할 수 있을 정도의 resource 여유분이 확보되면, 해당 Pod이 자동으로 실행된다.
apiVersion: v1
kind: Pod
metadata:
name: frontend-limit
spec:
containers:
- name: app
image: alpine
command: ["sleep", "3600"]
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
이번엔 limit이 정의되어 있는 pod. 만약 limit을 초과하는 cpu가 할당될 경우 쓰로틀링이 실행되며, memory가 한도를 초과할 경우 컨테이너를 삭제 후 재시작한다.
Monitor k8s Resources.
k8s는 active Monitoring을 사용함. container의 상태를 확인하고, failure 발생 시 자동으로 restart하는 기능을 지원하고 있음
Health Check을 제공하는 세 가지 Probe가 존재함.
Liveness probe
- 컨테이너의 상태를 확인함
- 기본적으로는 컨테이너가 실행중이지 않을 경우 down으로 판단함.
- down으로 판단된 컨테이너는 재시작하지만, 재시작은 restart policy를 따른다.
- 컨테이너 내에서 cmd를 주기적으로 실행하기 / HTTP health check 두 가지가 가능함.
- initialDelaySeconds : 프로세스 처음 시작 후 몇 초 뒤부터 health check를 진행할 것인가. 프로세스가 처음 뜨고 완전히 실행하기까지 시간이 필요한 걸 감안해서 선택.
- periodSeconds : interval time.
Startup Probe
- 애플리케이션에 따라 startUp time이 길거나 예측할 수 없는 경우가 있다. 이 경우 Liveness probe의 initialDelay만으로 status를 확인하기 쉽지 않음.
- 컨테이너가 처음 시작할 때 startup probe가 실행되고, startup probe가 success 상태가 되어야만 Liveness probe가 실행되도록 하는 일종의 트리거 역할이라고 보면 됨.
failureThreshold 디폴트 값은 1.
위 예시의 경우 애플리케이션의 status가 fail로 판정되기까지 최대 5분 (300초)은 startup Probe가 확인할 수 있다는 뜻이다.
Readiness Probe
- 트래픽을 받을 준비가 되어 있는지 확인하는 용도의 probe.
- 애플리케이션이 백엔드 / 프론트엔드 등으로 나뉘어 있을 경우.. end to end의 health check를 담당한다고 보면 됨.
Liveness와 readiness는 병렬로 실행할 수 있다. Liveness는 컨테이너의 health check이고, readiness는 애플리케이션의 health check이기 때문.
Liveness probe 실습파일.
apiVersion: v1
kind: Pod
metadata:
name: liveness-probe
spec:
containers:
- name: liveness
image: k8s.gcr.io/busybox
args:
- /bin/sh
- -c # command
- touch /tmp/healthcheck; sleep 60; rm -rf /tmp/healthcheck; sleep 600
livenessProbe:
exec:
command: # 해당 파일이 존재하는지 아닌지 체크.
- cat
- /tmp/healthcheck
initialDelaySeconds: 5
periodSeconds: 5
pod를 실행하면
- touch /tmp/healthcheck 로 파일 생성
- initDelaySecond 이후 cat /tmp/healthcheck로 파일 존재여부 체크
- 60초 뒤 파일이 삭제됨
- 삭제된 이후부터는 liveness가 fail이 됨.
kubectl describe liveness-probe
로 확인한 결과는 아래와 같다.
처음에는 성공적으로 Create / Start까지 진행되었지만,
Liveness probe failed가 발생한 후 restart를 자동으로 진행하는 걸 볼 수 있다.
http api로 liveness를 확인하려면
apiVersion: v1
kind: Pod
metadata:
name: liveness-probe-http
spec:
containers:
- name: liveness-nginx
image: k8s.gcr.io/nginx
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 3
kubectl describe liveness-probe-http
로 pod의 IP값을 확인하고, 해당 ip로 curl -u <ip주소>
접근 시 정상적으로 접근되는 걸 확인할 수 있다.
Startup Probe에 필요한 yaml 코드는 liveness와 거의 유사하다.
apiVersion: v1
kind: Pod
metadata:
name: startup-probe-http
spec:
containers:
- name: startup-nginx
image: k8s.gcr.io/nginx
startupProbe:
httpGet:
path: /
port: 80
failureThreshold: 30
periodSeconds: 10
위 코드의 startupProbe 세팅대로라면 failure 판정 / restart까지 최대 5분이 필요하다. failureThreshold 30 * periodSeconds 10을 곱하면 300초가 되기 때문.
readiness Probe 예시설정.
apiVersion: v1
kind: Pod
metadata:
name: hc-probe
spec:
containers:
- name: nginx
image: k8s.gcr.io/nginx
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 3
readinessProbe:
httpGet:
path: /
port: 9090 # 의도적으로 fail 발생하게 포트설정
initialDelaySeconds: 3
periodSeconds: 3
Status는 Ready이지만, Running 상태로 확정되지는 않는다. Readiness Probe에서 실패하기 때문.
Pods restart Policy
기본적으로 제공해주는 restart Policy는 크게 셋. always, onFailure, Never
- always의 단점: 성공적으로 프로그램이 exit되었을 때에도 container를 삭제하고 재시작한다.
- 프로그램이 에러를 내고 종료하던, 성공적으로 실행이 끝나던 상관없이 container를 재시작하게 됨.
- 따라서 '항상 실행되어야만 하는' 프로그램일 경우 적합하며, 특정 작업을 처리한 뒤 종료해야 하는 애플리케이션 컨테이너에는 적절하지 않다.
apiVersion: v1
kind: Pod
metadata:
name: restart-always-pod
spec:
restartPolicy: Always
containers:
- name: app
image: alpine
command: ["sleep", "20"]
# 20초 뒤 컨테이너의 프로그램이 성공적으로 종료됨.
- onFailure: container 에러 등으로 failure발생 시에만 restart.
apiVersion: v1
kind: Pod
metadata:
name: onfailure-always-pod
spec:
restartPolicy: OnFailure
containers:
- name: app
image: alpine
command: ["sleep", "20"]
# 20초 뒤 성공적으로 프로그램이 종료되기 때문에 container restart하지 않음.
# kubectl get pods -o wide로 pod상태를 확인하면 status: completed 상태가 된다.
# 만약 failure상태가 될 경우 status: CrashLoopBackOff가 되며, restart 횟수가 올라간 뒤 컨테이너를 재실행한다.
- Never: 한 번만 실행되는 게 보장되어야 하는 애플리케이션의 경우 적절함.
apiVersion: v1
kind: Pod
metadata:
name: never-always-pod
spec:
restartPolicy: Never
containers:
- name: app
image: alpine
command: ["sh", "-c", "sleep 20;", "Dummy Command"]
# dummy command라는 명령어는 없으므로, 20초 뒤 프로그램은 failed된다.
# 하지만 status는 completed.
Multi Container Pods
하나의 Pod 안에 여러 개의 Containers를 생성할 수 있다. (Multiple Containers in a Single Pod.)
같은 pod 내의 컨테이너끼리는 Network, Volume을 공유함.
apiVersion: v1
kind: Pod
metadata:
name: two-containers
spec:
restartPolicy: OnFailure
containers:
- name: nginx-container
image: nginx
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
- name: debian-container
image: debian
volumeMounts:
- name: shared-data
mountPath: /pod-data
command: ["/bin/sh"]
args: ["-c", "echo Hello from the Secondary container > /pod-data/index.html"]
volumes:
- name: shared-data
emptyDir: {}
# 두 번째 컨테이너의 경우 command 명령어 실행을 끝내면 정상적으로 종료됨. (onFailure)
두 개가 shared volumne (shared-data) 을 공유하고 있기 때문에, debian controller의 명령어로 nginx html 파일 이름이 변경된 것을 확인할 수 있다.
Container Initialization in k8s
- App Container 실행 전에 먼저 실행되는 특수한 목적의 컨테이너.
- 애플리케이션을 띄우기 전에 설정이 필요할 경우 보통 사용한다.
- mainfest에서 사전에 정의한 순서대로 init Containers가 실행된다
- 모든 init container가 정상적으로 completed 된 후에야 app container를 실행한다.
- 따라서, 정의하기에 따라 App Container의 startup을 block / delay하는 등의 작업이 가능하다.
- 보통 DB나 기타 App dependency를 init container에서 먼저 실행시킨 뒤 App을 실행하게 하는 등의 용도로 사용
apiVersion: v1
kind: Pod
metadata:
name: application-pod
spec:
containers:
- name: myapp-container # App Container.
image: busybox:1.28
command: ["sh", "-c", "echo The app is running! && sleep 3600"]
initContainers: # initContainers 정의
- name: init-myservice
image: busybox:1.28
command:
[
"sh",
"-c",
"until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 5; done",
]
- name: init-mydb
image: busybox:1.28
command:
[
"sh",
"-c",
"until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 5; done",
]
nslookup - DNS 주소 조회
- app container 실행에 필요한 container dependency가 세팅될 때까지 대기한다.
- myservice와 mydb라는 이름의 service가 먼저 실행될 때까지 대기 상태가 됨.
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
name: mydb
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9377