스타트업 인턴의 쿠버네티스 도전기

Hyemi Noh
21 min readAug 27, 2020

--

안녕하세요. Datarize에서 7,8월 2개월 동안 인턴 개발자로 일했던 노혜미입니다. Datarize에서 쿠버네티스를 사용해보면서 해봤던 태스크, 느낀 점등 쿠버네티스 사용 경험을 공유해드리려고 합니다.

너란 쿠버네티스 참 어렵다…

들어가기 전에

이 글은 도커 및 약간의 인프라 관련 경험을 가졌지만, 클러스터 운영 경험이 없는 주니어 개발자가 2개월 동안 쿠버네티스를 처음 공부하면서 써봤던 후기를 작성한 글입니다. 쿠버네티스 자체에 대한 심화된 내용을 원하시면 다른 글을 참고해주세요.

쿠버네티스 도입 계기

이전 구조의 제약

Datarize는 쇼핑몰과 같은 특정 고객사 사이트에 방문하는 유저들에게 개인화된 커뮤니케이션을 제공하기 위해 각 고객사 별로 별도 서버군을 구성해 제공하고 있었습니다. 이는 고객사 별로 철저히 독립된 환경을 제공할 수 있다는 장점이 있습니다. 하지만 사업이 확대되면서 다수의 신규 고객사를 받기 위해서는 새로운 서버 세팅, 고객사의 규모에 따른 유연한 스케일 업/다운같은 작업들이 지금보다 훨씬 용이해져야 했습니다.

쿠버네티스로 커버

쿠버네티스를 on premise로 직접 설치하면 위보다 더 큰 공수가 들 수 있습니다.. 😓 하지만 쿠버네티스는 여러 클라우드 플랫폼(AWS, GCP…)에서 관리형 서비스를 제공하고 있습니다. 관리형 서비스를 이용하면 별도의 셋팅없이 클릭 몇 번으로 쉽게 쿠버네티스 클러스터를 구축할 수 있습니다.

컨테이너 운용툴로 간단하게 쓰일 수 있는 것에는 도커 컴포즈도 있습니다. 하지만 도커 컴포즈와 쿠버네티스에는 큰 차이가 있습니다. 일반적으로 도커 컴포즈의 경우 단일 노드(머신)에서 작동하고 쿠버네티스는 여러 노드로 이루어진 클러스터에서 작동합니다. 좀 더 구체적으로 말하면 워커들을 관리하는 관리자 노드(들)와 관리자의 명령을 받는 워커 노드들로 구성돼있습니다. 따라서 클러스터에서 컨테이너 개수를 늘리고 줄일 수 있기 때문에 확장성이 증가합니다.

또한 구글에서 만들었고 에코 시스템도 다양하기 때문에 구현하고 싶은 게 있다면 잘 찾아서 구현하면 되는(?) 플랫폼입니다. 사용자가 직접 필요한 것을 만들기 어려운 시스템일수록 여러 지원이 있다는 건 아주 큰 장점이라고 생각합니다. 너무 길어질까봐 다 적지 못한 다른 강력한 점들은 여기를 참고하시길 바랍니다.

첫 걸음 내디기

쿠버네티스를 처음 접하고 든 생각은 ‘뭐가 이렇게 복잡하고 어려울까…’ 였습니다. (물론 지금도 같습니다..) 그 만큼 서비스 운영 경험이 거의 없는 초심자로서는 시작조차 어려웠습니다. 그래서 먼저 공부해본 사람으로서 어떻게 시작하는 게 좋을지 말씀드려보고자 합니다. 만약 어느정도 지식이 있으신 분들은 해당 섹션을 넘기셔도 좋습니다.

kubernetes tutorial 이라고 구글에 치면 사이트부터 동영상까지 정-말 많습니다. 하지만 ‘시간이 없다. 다 됐고 일단 빠르게 감만 잡고 싶다’ 면 저는 다음의 과정을 추천드립니다. 기본 전제는 쿠버네티스를 만든 구글이 제일 정확히 알고 있기 때문에 최대한 공식 문서를 참고하는 것입니다. 그리고 지금 이해가 안 되는 건 일단 넘기는 것입니다. 어차피 나중에 다시 봐야합니다..

  1. 아무것도 모르는 상태에서 공식 문서를 읽는 다면 더 큰 거부감이 느껴질 수 있으므로 해당 블로그 글을 통해 컨셉을 파악하고 잘 모르겠는 부분은 일단 느낌(?)만 가지고 넘깁니다. 아키텍처는 공식 문서의 쿠버네티스 컴포넌트를 보시는 것도 좋습니다. 몇몇 어려운 용어가 있는 거 빼고는 사전 지식 없이도 읽을 만합니다. (좀 더 심화된 내용을 원하시면 맨 아래 레퍼런스에서 언급하는 Kubernetes in Action의 chapter 11을 읽어보시면 좋습니다)
  2. 공식 문서 쿠버네티스 기초 학습의 대화형 튜토리얼을 통해 직접 여러 명령어를 쳐보면서 실습합니다. 간략하게 핵심 개념에 대해서도 설명해줍니다. 좋았던 점은 브라우저 상에서 제공되는 커맨드라인으로 튜토리얼을 진행하기 때문에 로컬에 아무것도 설치하지 않고도 명령어를 수행해 볼 수 있습니다.
  3. 튜토리얼을 모두 수행했을 때 가장 난해했던 것은 service라는 오브젝트였습니다. 뭔지도 잘 모르겠는데 구분까지 돼있었습니다. 여기에 ingress라는 것까지 등장하면 정말 혼란스럽습니다.. 처음 이해를 하는 데 있어서는 공식 문서보다는 여기를 참고하시는 게 더 좋습니다. 작성해야 하는 yaml 파일에 port가 여러 개가 나와 헷갈리는데 port에 대한 것은 여기를 참고하시면 됩니다.
  4. 실제로는 커맨드라인을 통해서 쿠버네티스 오브젝트를 만들기보다는 yaml 파일을 이용하게 됩니다. 처음 yaml 파일을 참고하실 때는 최대한 단순하게 pod, service 정도로 이루어진 yaml 파일을 보는 게 낫습니다. (절대 처음부터 volume이 나오는 yaml 파일은 보려고 시도 조차 하지마세요..) 비록 ingress 튜토리얼이기는 하지만 여기서는 간단한 yaml 파일 구성을 볼 수 있습니다.

첫 걸음 내디기에 나온 내용을 모두 따라해보신다면 감을 잡으시기에는 충분할 겁니다 :)

해본 것

이 섹션에서는 제가 어떤 태스크들을 해봤는지와 이를 통해 알게된 점을 말씀드리려고 합니다. 간단한 토이 프로젝트를 해보거나 특정한 주제에 대해서 탐구해봤습니다.

토이 프로젝트

어떤 요구 사항이 있었고 충족시키기 위해 어떻게 구현을 했으며 이로 인해 얻을 수 있는 좋은 점이 무엇인지 말씀드리겠습니다. 본격적으로 살펴보기 전에 토이 프로젝트이기에 알 수 없는 부작용들이 많이 있을 수 있다는 점을 명심해주시길 바랍니다.

1. Deploying app servers with ingress and helm

서버 별로 서로 다른 데이터를 캐쉬하고 있어야 하기 때문에 특정 패스에 대한 요청은 특정 service로 라우팅을 해줘야했습니다. 예를 들면, 클라이언트가 api.com/a/... 과 같이 요청을 보낸다면 a에 해당하는 pod에 요청을 보내기 위해 a에 해당하는 service로 요청을 보내야 했습니다.

그래서 아래와 같이 패스를 기준으로 특정 service로 라우팅을 하기 위해 ingress를 사용했습니다. 그리고 똑같은 구조로 이루어진 pod와 service가 패스만큼 필요하고 새로운 service가 추가될 때마다 기존 ingress에 추가돼야했습니다. yaml 파일의 불필요한 중복을 줄이기 위해 helm을 사용했습니다. ingress는 쿠버네티스 object 중 하나로 라우팅에 대한 규칙을 정의해 놓는 곳입니다. helm은 yaml 파일의 중복을 줄일 수 있게 해주고 남이 만든 yaml을 패키지처럼 사용할 수 있게 해주는 쿠버네티스 버전 패키지 매니저입니다.

apiVersion: apps/v1
kind: Deployment # 쉽게 보면 pod에 복제본 개수 등 이것 저것이 추가된 object
metadata:
name: server-deploy-{{ .Values.ID }}
template:
metadata:
labels:
app: server-{{ .Values.ID }}
...
---
apiVersion: v1
kind: Service
metadata:
name: server-svc-{{ .Values.ID }}
spec:
selector:
app: server-{{ .Values.ID }}
...

각 deployment와 service는 모두 똑같고 name만 다릅니다. 따라서 helm을 이용해 특정 yaml 파일에서 ID라는 값을 읽어와 넣어주면 하나의 yaml 파일로 서로 다른 name을 가진 deployment와 service를 찍어낼 수 있습니다. 이렇게 함으로써 불필요한 중복을 줄이고 한 번의 수정만으로도 여러 개의 yaml 파일을 수정한 것과 동일한 효과를 얻을 수 있습니다. 또한 필요하다면 각각의 deployment와 service 별로 replica(복제본) 개수를 다르게 하는 등 개별 관리가 가능해집니다.

2. workflow

특정 시간마다 수행돼야 하는 job에 유저가 파라미터를 넘겨줄 수 있어야 했고 job을 구성하는 step들이 순차적으로 수행돼야 했습니다. 쿠버네티스 object 중 CronJob 이라는 것을 이용하면 특정 시간마다 job은 수행할 수 있지만 dependency는 설정할 수 없었습니다. 그래서 다른 방법을 찾아보던 중 쿠버네티스 네이티브 워크플로우 엔진인 Argo라는 것을 발견해 사용했습니다.

공식 문서에 나온대로 quick start를 통해 클러스터에 필요한 object들을 배포하고 argo CLI를 설치해 workflow 수행을 위한 준비작업을 했습니다. 그리고 아래와 같이 샘플 yaml 파일을 작성해줍니다. generate step으로 학습에 필요한 데이터를 만들고 train step에서 해당 데이터를 이용해 학습 및 예측을 하는 job입니다.

apiVersion: argoproj.io/v1alpha1
# CronWorkflow로 cronjob이 가능하지만 당시 CLI version(v2.10.0)에서는
# 파라미터를 넘기는 옵션을 지원해주지 않아 Workflow 사용
kind: Workflow
metadata:
generateName: gen-train-wf-
spec:
entrypoint: entrypoint
arguments:
parameters:
- name: id
value: Empty

templates:
- name: entrypoint
steps:
# generate, train순서로 수행
- - name: generate
template: generate-template
- - name: train
template: train-template
# 아래에서 위에서 명시한 template 정의
...
$ argo submit gen-train.yaml -p id=a -n argo
Name: gen-train-wf-ppj56
Namespace: argo
ServiceAccount: default
Status: Pending
Created: Mon Aug 24 17:45:00 +0900 (now)
Parameters:
id: {0 a}
$ argo logs gen-train-wf-ppj56 -n argo
gen-train-wf-ppj56-200761619: 2020-08-24T08:45:04.974127977Z ['sample_a.csv']
gen-train-wf-ppj56-2264280755: 2020-08-24T08:45:12.581827179Z
id: a, f1-score: 0.5

CLI를 통해 유저가 파라미터를 넘겨줄 수 있고 job 내의 step들도 순차적으로 수행되는 것을 알 수 있습니다. 단순히 순차적으로 수행되는 것이 아니라 이전 step이 실패한다면 다음 step은 실행되지 않고 job은 실패하게 됩니다. 따라서 무의미한 리소스 소모를 막아줍니다.

본질적으로는 쿠버네티스를 이용해 workflow를 관리했을 때의 이점은 workflow만 별도로 다른 구성을 할 필요 없이 통합된 구조를 가져가는 것이라고 생각합니다. 즉, workflow가 다른 것들과 동떨어져 있는 게 아니라 같이 쿠버네티스 클러스터 안에서 관리되는 object 중 하나인 것입니다. 그렇다면 workflow 역시 쿠버네티스로 모니터링을 할 수 있는 대상이 됩니다.

3. Nginx + Logstash

Nginx는 web server이고 Logstash는 로그를 프로세싱한 뒤 elasticsearch같은 저장소에 로그를 보내는 프로그램입니다. Nginx의 access log를 실시간에 가깝게 사용하기 위해 라인 단위로 처리하고 저장소에 적재해야 하는 요구 사항이 있었습니다. 그래서 아래와 같이 하나의 pod 내에 Nginx 컨테이너와 Logstash 컨테이너를 넣는 구조를 설계했습니다.

apiVersion: apps/v1
kind: Deployment
...
template:
spec:
containers:
- name: nginx
image: <private nginx image>
volumeMounts:
# 공유 볼륨
- name: temp-volume
mountPath: /var/log/nginx
- name: logstash
image: dtr.kr.ncr.ntruss.com/logstash
# 공유 볼륨
volumeMounts:
- name: temp-volume
mountPath: /var/log/nginx
...

Nginx 컨테이너와 logstash 컨테이너는 하나의 pod에 같이 있으므로 볼륨을 공유할 수 있습니다. 동시에 다른 pod와는 분리된 볼륨을 가질 수 있게 됩니다. 그래서 하나의 node내에서 특정 컨테이너끼리 볼륨을 공유하면서도 볼륨의 충돌이 일어나지 않게 합니다.

여러 컨테이너를 pod라는 하나의 그룹으로 묶어 네트워크, 스토리지를 공유하기 때문에 공유는 공유대로 분리는 분리대로 할 수 있는 큰 장점을 가지는 것 같습니다.

특정 주제 탐구

아래 주제 모두 pod와 관련된 것들입니다. 쿠버네티스에서 서버를 띄우려고 할 때 안정적인 서빙을 위해서라면 꼭 필요하다고 생각해 조사해봤습니다.

1. probe

일정 시간마다 컨테이너에 특정 커맨드를 수행해 성공 여부를 검사해, 컨테이너를 죽이거나 외부에서 접근하지 못하도록 막는 장치입니다. probe에는 liveness probe, readiness(레디니스) probe, startup probe 세 가지 종류가 있습니다.

startup probe는 특수한 경우에 필요한 것 같아 생략하고 liveness probe와 readiness probe에 대해 탐구해봤습니다. liveness probe는 컨테이너가 잘 살아있는지를 확인하는 것입니다. 그리고 readiness probe는 컨테이너가 요청을 잘 처리할 수 있는지 확인하는 것입니다. probe를 하는 방법에는 exec, tcpSocket, httpGet 세 가지가 있습니다. 저는 명령어로 확인하는 exec와 특정 path에 GET 요청을 보내는 httpGet을 이용했습니다.

‘pod가 서버이면 어차피 *로드 밸런서가 *헬스 체크를 해주는 게 아닌가? liveness probe가 왜 필요해?’ 라고 의문이 드실 수 있습니다. 하지만 로드 밸런서의 헬스 체크 리퀘스트는 랜덤하게 pod 하나에만 가게 됩니다. 따라서 모든 pod 즉, 서버의 헬스를 체크할 수 없습니다. 모든 pod의 헬스 체크를 보장하기 위해 liveness probe가 필요합니다.

  • 로드 밸런서: 여러 서버의 앞에 붙어 리퀘스트를 골고루 서버들에게 분배해주는 장치
  • 헬스 체크: 서버가 잘 살아있는지 주기적으로 요청을 보내 확인하는 액션

그러면 readiness probe는 왜 필요할까요? 만약 서버가 요청을 받았을 때 디비에 접근해야 된다고 해봅시다. 갑자기 디비에 접근할 수 없게 되더라도 이 pod가 잘 살아있다면 liveness probe만으로는 문제를 진단할 수 없습니다. 이럴 때 필요한 것이 readiness probe입니다. readiness probe를 실패한다면 더 이상 해당 pod에 요청이 가지 않도록 만듭니다.

apiVersion: v1
kind: Pod
spec:
containers:
- name: server
...
livenessProbe:
httpGet:
path: /healthcheck
port: 5000
periodSeconds: 5
readinessProbe:
exec:
command:
- cat
- <some file>
periodSeconds: 10

별도로 스크립트를 만들거나 하지 않고 pod의 spec에 각각의 probe를 간단히 명시해주기만 하면 됩니다. 위의 파일대로라면 5초에 한 번 씩 5000 포트의 /healthcheck 패스에 GET 요청을 보내 실패한다면 컨테이너를 죽이고 재시작할 것입니다. 그리고 10초에 한 번 씩 cat으로 특정 파일을 읽고 실패한다면 service에서 pod를 unregister해서 외부에서의 요청이 해당 pod로 들어오지 못하도록 조치를 취합니다.

몇 가지 주의할 점은 readiness probe로 할 것을 liveness probe로 해서는 안 됩니다. 또한 두 가지 probe는 디펜던시를 가지는 게 아니므로 liveness probe가 성공하고 readiness probe가 수행되는 그런 것은 아닙니다.

2. preStop hook

명칭에서 알 수 있듯이 컨테이너가 종료되기 직전에 호출되는 훅”입니다. 종류는 Exec와 HTTP 두 가지가 있습니다. 저는 Exec를 사용해 특정 커맨드를 수행 하도록 했습니다.

preStop hook을 이용해 graceful shutdown을 보장하고 다른 액션보다 서비스에서 pod를 unregister하는 것을 가장 먼저 하도록 보장합니다. 이는 결국 Continuous Delivery를 잘 유지해 유저에게 중단 없이 지속적으로 서비스를 제공하기 위함입니다.

좀 더 자세히 설명해보겠습니다. graceful shutdown은 종료 신호를 보내자마자 프로세스를 강제 종료하는 것이 아니라 종료 신호를 보냈을 때 프로세스가 하던 작업을 마무리할 시간을 주는 것입니다. 예를 들면 Nginx같은 웹서버라면 자신이 처리하던 요청을 급하게라도 마무리를 할 수 있을 것입니다. 기본적으로 도커와 쿠버네티스는 종료 신호로 SIGTERM이라는 시그널을 보내는데 *Nginx의 경우 SIGTERM을 fast shutdown(강제 종료)로 받아들인다고 합니다. 따라서 preStop hook에 graceful shutdown으로 받아들이는 신호를 명시적으로 적어주면 됩니다.

  • 여기를 보시면 Nginx 컨트리뷰터가 고치겠다고 했는데 된 건지 안 된 건지 잘 모르겠습니다.

먼저 unregister를 하는 걸 보장한다는 건 무슨 얘기일까요? 컨테이너의 이미지를 바꾸는 것처럼 특정 update를 진행하기 위해서는 기존 pod를 죽이고 새로운 pod를 띄워야 합니다. 이때 외부의 요청을 처리하는 pod 즉, service에 등록된 pod라면 pod가 죽는 것이외에도 service에서 pod를 등록해제해야합니다. 해당 pod로 요청이 가지 않게 만드는 겁니다.

하지만 쿠버네티스의 경우 병렬적으로(parallel) 작동하기 때문에 pod가 죽고 unregister가 될 수 있습니다. 그렇다면 죽은 pod에 요청이 갈 수도 있기 때문에 해당 요청은 제대로 처리될 수 없습니다. 그래서 preStop hook에 sleep을 걸고 shutdown을 해줘 등록해제가 먼저 일어나도록 트릭을 걸어줍니다.

apiVersion: apps/v1
kind: Deployment
...
spec:
# pod template
template:
...
spec:
containers:
- name: nginx
...
lifecycle:
preStop:
exec:
command: [
"sh", "-c",
"sleep 30 && /usr/sbin/nginx -s quit",
]
# 해당 시간 내에 죽지 않으면 강제로 죽이므로 위의 명령어가 제대로
# 수행되기 위해서는 디폴트 30초 보다 긴 60초가 필요함
terminationGracePeriodSeconds: 60

probe처럼 pod spec에 명시를 해주면 됩니다. 위의 파일대로라면 30초 동안 pod가 자는 동안 service의 pod unregister가 완료될 것입니다. 그리고 quit 명령어로 graceful shutdown을 시도합니다.

하지만 preStop hook을 이용하더라도 많은 개발자들이 고통을 받고 있습니다.. 😇 저 또한 fortio라는 로드 테스팅 툴을 이용했을 때 모두 200 상태인 리스폰스를 받았지만 Closing dead socket 이라는 에러가 났습니다. 물론 preStop hook을 쓰지 않을 때보다 에러가 적게 나는 것을 확인할 수 있었습니다. 따라서 좀 더 많은 조사와 실험이 필요하다고 생각했습니다.

3. resource limit

pod가 사용할 수 있는 리소스를 제한하는 것입니다. 리소스의 지표는 CPU, memory, requests 등등 매우 다양합니다. 별도의 구성없이 기본적으로 쓸 수 있는 지표는 CPU와 memory입니다.

리소스를 제한해야 하는 이유는 하나의 컨테이너가 node의 모든 리소스를 독차지 하지 않도록 하기 위해서입니다. 만약 아무 제한이 없다면 서서히 node의 자원을 차지하다가 부족해지면 그때서야 죽게 될 것입니다. node 하나 당 컨테이너가 한 개만 뜨도록 구성돼 있다면 큰 문제가 없을 수도 있을 것입니다.

하지만 쿠버네티스처럼 여러 node로 이루어진 클러스터에서 여러 pod 즉, 컨테이너를 운영하도록 만드는 툴에게 이런 양상은 분명 운영 측면에서 부정적인 효과를 줄 것입니다. node간에 컨테이너가 적절히 분배되지 않을 것이기 때문입니다.

apiVersion: v1
kind: Pod
spec:
containers:
- name: cache-villain
...
resources:
# 평균 사용량인 requests도 명시 가능
limits:
memory: "1Gi"

위와 같이 명시한다면 컨테이너가 1GiB의 memory 사용량을 넘을 경우 statusOOMKilled로 변한 뒤 다시 시작됩니다. 따라서 개별 pod가 굶주리지 않고(starvation) 리소스를 충분히 사용할 수 있는 확률을 높여줍니다.

물론 이는 철저한 모니터링을 통해 컨테이너가 리소스를 얼만큼 사용하는지 정확히 파악하고 있을 때 그 진가를 발휘하는 것 같습니다. 또한 node의 스펙에 따라서 의도치 않은 방향으로 작동할 가능성도 있습니다. 하지만 안정적인 운영을 위해서 개별 pod의 리소스 사용량을 파악해 적절한 제한을 거는 것은 필요한 일 같습니다.

마무리

사용자가 명시적으로 선언한 것들을 실제로 그렇게 되도록 만드는 게 쿠버네티스의 핵심원리라고 생각합니다. 예를 들면 위에서 살펴봤던 예제에서 ‘이 컨테이너는 메모리를 이 만큼만 쓰게 제한해줘’라고 하면, 그만큼만 쓰도록 제한하고 지키지 않을 경우 가차없이 죽이고 다시 띄워버립니다.. 사용자가 파일에 선언만 하면 그걸 그대로 실행해주는 아주 멋진 친구입니다 😎

처음 접했을 때 느낀 것은 ‘어렵다..’ 입니다. 그냥 어렵습니다. 지금도 어렵습니다. 많은 것들을 강력하게 지원하기 위해 쿠버네티스 자체에서 이것저것 구현된 것도 많고 관련 플러그인(?)들도 많습니다. 또한 현재에도 활발하게 개발되고 있기 때문에 변화에 맞춰 계속 따라가야 합니다. 쿠버네티스의 주된 태도는 ‘우리가 제공은 해주는데 알아서 하렴 ㅎㅎ’ 이기 때문에 따로 클러스터에 구성을 해줘야 할 것도 많습니다. 특히 모니터링과 로깅은 필수적이지만 제대로 하려면 따로 설치를 해줘야 합니다.

공식 문서에서도 ‘이 정도는 알지?’하고 브레이크 없이 바로 어려운 것들을 설명하기도 해서 문서를 읽어도 여전히 이해 못 할 때가 많았습니다. 그리고 알아가면 알아 갈수록 알아야 할 게 더 많아지는 신기한(?) 현상을 경험해 볼 수 있습니다. 그래서 클러스터 환경에 익숙하지 않으시다면 (네트워크를 잘 모르신다면..) 프로덕션 레벨로 구축하기 위해 많은 고생을 하셔야 될 것 같습니다.. 😇

그럼에도 현재 대규모의 서비스를 운영하는 곳에서는 거의 쿠버네티스를 사용하는 추세인 것 같습니다. 심지어 서비스를 운영하지 않는 회사에서도 쿠버네티스를 사용하는 사례를 듣기도 했습니다.

Datarize에서도 쿠버네티스를 ‘잘’ 이용하면 다양한 규모의 많은 고객사들에 유연하게 대응할 수 있다고 생각합니다. 이 글은 단순히 쿠버네티스 사용 후기이지만… 쿠버네티스 첫걸음을 고민하고 계시는 저 같은 초심자 분들께 도움이 됐으면 하는 마음에서 작성해봤습니다. 긴 글 읽어주셔서 감사합니다 :)

번외: 참고하면 좋은 레퍼런스들

제가 쿠버네티스 공부를 하면서 찾아본 레퍼런스들 중 몇 가지를 추천드리고 싶습니다.

--

--

Hyemi Noh
Hyemi Noh

No responses yet