1. 배경
Kubeflow를 설치한 후 생성된 파드를 보면 다양한 컴포넌트들이 존재하는 것을 확인할 수 있습니다. Kubeflow 기반의 플랫폼을 개발하거나 정확하게 Kubeflow를 사용하기 위해서 각 컴포넌트들의 역할과 동작과정에 대한 이해가 필요하다고 느꼈습니다. 이를 위해 순차적으로 컴포넌트들에 대해 공부한 내용을 기록하려 합니다.
2. Notebook 생성 방법 및 구조
일반 사용자가 Kubeflow에서 노트북을 사용하는 방식은 간단합니다. Dashboard에서 노트북 Menu에서 생성하여 접속해 사용할 수 있습니다. 자신의 Namespace에서 할당받은 가용 Resource(Memory, Cpu)를 분할해 사용할 수 있습니다.
노트북이 생성되면 해당 네임스페이스에서 pod가 생성된 것을 확인할 수 있습니다. notebook이라는 CRD를 통해 생성되며 이는 Statefulset에서 확장된 것 을 확인 할 수 있습니다.
Notebook CRD는notebooks.kubeflow.org라는 이름으로 구성되어 있고 Describe 명령어를 통해 자세한 사항을 확인할 수 있습니다. CRD는 오브젝트의 정의만 하기 때문에 Kubernetes에서 오브젝트를 생성하고 재시작하는 등 유지하기 위해서(desired state를 유지) Controller가 필요합니다. 이러한 역할을 하는 controller가 notebook-controller-deployment
입니다. 실제 컨트롤러가 이러한 desired state를 지향하기 위해 동작하는 과정을 reconcile이라고 하는데 실제 Controller의 코드를 확인하면 reconcile function을 통해 Notebook의 동작 과정을 확인할 수 있습니다. (향후 코드 분석을 업데이트 예정)
func (r *NotebookReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
log := r.Log.WithValues("notebook", req.NamespacedName)
// TODO(yanniszark): Can we avoid reconciling Events and Notebook in the same queue?
event := &corev1.Event{}
var getEventErr error
getEventErr = r.Get(ctx, req.NamespacedName, event)
if getEventErr == nil {
log.Info("Found event for Notebook. Re-emitting...")
// Find the Notebook that corresponds to the triggered event
involvedNotebook := &v1beta1.Notebook{}
nbName, err := nbNameFromInvolvedObject(r.Client, &event.InvolvedObject)
if err != nil {
return ctrl.Result{}, err
}
involvedNotebookKey := types.NamespacedName{Name: nbName, Namespace: req.Namespace}
if err := r.Get(ctx, involvedNotebookKey, involvedNotebook); err != nil {
log.Error(err, "unable to fetch Notebook by looking at event")
return ctrl.Result{}, ignoreNotFound(err)
}
// re-emit the event in the Notebook CR
log.Info("Emitting Notebook Event.", "Event", event)
r.EventRecorder.Eventf(involvedNotebook, event.Type, event.Reason,
"Reissued from %s/%s: %s", strings.ToLower(event.InvolvedObject.Kind), event.InvolvedObject.Name, event.Message)
return ctrl.Result{}, nil
}
3. Notebook 생성 과정
위와 같은 구조를 가진 노트북은 jupyter-web-app-deployment을
통해 API 통신이 이루어지고 생성됩니다.
jupyter-web-app-deployment
내부에서 해당 내용을 확인할 수 있는데 flask로 이루어진 API 서버에서 통신하는 것을 확인 할 수 있습니다. notebook 생성 폼에서 받은 data를 파싱해 PVC를 생성하고 Notebook을 생성 하고 마운트 하는 과정입니다.
@bp.route("/api/namespaces/<namespace>/notebooks", methods=["POST"])
@decorators.request_is_json_type
@decorators.required_body_params("name")
def post_pvc(namespace):
body = request.get_json()
log.info("Got body: %s" % body)
notebook = helpers.load_param_yaml(
utils.NOTEBOOK_TEMPLATE_YAML,
name=body["name"],
namespace=namespace,
serviceAccount="default-editor",
)
defaults = utils.load_spawner_ui_config()
form.set_notebook_image(notebook, body, defaults)
form.set_notebook_image_pull_policy(notebook, body, defaults)
form.set_server_type(notebook, body, defaults)
form.set_notebook_cpu(notebook, body, defaults)
form.set_notebook_memory(notebook, body, defaults)
form.set_notebook_gpus(notebook, body, defaults)
form.set_notebook_tolerations(notebook, body, defaults)
form.set_notebook_affinity(notebook, body, defaults)
form.set_notebook_configurations(notebook, body, defaults)
form.set_notebook_shm(notebook, body, defaults)
# Notebook volumes
api_volumes = []
api_volumes.extend(form.get_form_value(body, defaults, "datavols",
"dataVolumes"))
workspace = form.get_form_value(body, defaults, "workspace",
"workspaceVolume", optional=True)
if workspace:
api_volumes.append(workspace)
# ensure that all objects can be created
api.create_notebook(notebook, namespace, dry_run=True)
for api_volume in api_volumes:
pvc = volumes.get_new_pvc(api_volume)
if pvc is None:
continue
api.create_pvc(pvc, namespace, dry_run=True)
# create the new PVCs and set the Notebook volumes and mounts
for api_volume in api_volumes:
pvc = volumes.get_new_pvc(api_volume)
if pvc is not None:
logging.info("Creating PVC: %s", pvc)
pvc = api.create_pvc(pvc, namespace)
v1_volume = volumes.get_pod_volume(api_volume, pvc)
mount = volumes.get_container_mount(api_volume, v1_volume["name"])
notebook = volumes.add_notebook_volume(notebook, v1_volume)
notebook = volumes.add_notebook_container_mount(notebook, mount)
log.info("Creating Notebook: %s", notebook)
api.create_notebook(notebook, namespace)
return api.success_response("message", "Notebook created successfully.")
api.create_notebook(notebook, namespace)
코드에서 notebook을 생성하는데 이는 Kubernetes Python client를 사용하는 것으로 확인 할 수 있습니다.
실제 노트북을 Dashboard에서 생성할 때 해당 API 주소로 데이터를 전송하는 것을 확인 할 수 있습니다.
4. Notebook 생성 권한 수정
Kubeflow을 통해 노트북 생성 시 Jovyan(UID:1000, GID:100) 으로 생성합니다.Jovyan은 해당 이미지 생성시 적용된 User입니다.
따라서 노트북 내부에서 파일을 생성 후 PV에서 확인하면 해당 서버의 1000:100으로 확인됩니다.
#ec2-user의 UID 1000
#GID 100 users
-rw-r--r--. 1 ec2-user users 10 4월 15 04:43 test.txt
저희는 노트북 생성 유저의 그룹을 100번이 아닌 특정 그룹으로 설정해야 하기 때문에 이를 수정할 필요가 있습니다. 이를 변경하는 방법은 여러 가지 있을 수 있지만 Kubernetes의 SecurityContext를 이용하는 방법으로 수정을 진행합니다. 이를 위해 jupyter-web-app-deployment의 코드를 수정합니다. kubeflow github를 클론 하여 코드를 수정하였습니다.
노트북을 생성할 때 Obejct를 생성하기 위해서 기본 yaml을 읽어 들이는데 이는 notebook_template.yaml
에 존재합니다. 해당 부분에서 SecurityContext를
추가해 파드 실행 시 User와 Group의 권한을 생성합니다.
수동으로 UID, GID를 설정하였기 때문에 이 상태로도 동작이 가능하지만 향후 GID를 동적으로 받을 수 있게 코드를 추가합니다. (이는 실제로 적용하지는 않았음)
jupyter/backend/apps/common/forms.py
에서 SecurityContext
를 설정하는 함수를 생성한 후 post.py
에서 생성한 함수를 set 합니다.
이렇게 변경한 코드를 적용하기 위해 이미지를 빌드합니다. Jupyter 폴더에 존재하는 Dockerfile의 위치가 맞지 않아 빌드가 실패합니다. 이를 위해 Dockerfile을 한 단계 위로 옮긴 후(crud-web-apps에서) 실행합니다.
이후 해당 이미지를 Nexus주소로 태그를 진행한 후 Push 합니다.
Kubernetes에서 해당 이미지를 적용하기 위해 jupyter-web-app-deployment
의 이미지를 변경합니다. nexus에서 이미지를 받아오기 때문에 imagePullSecret
을 추가해 줬습니다.
이후 새롭게 노트북 생성 후 파일을 생성 시 UID jovyan(1000), GID 1500으로 생성된 것을 확인할 수 있습니다.
'Kubernetes' 카테고리의 다른 글
쿠버네티스 멀티클러스터 관리하기(Feat. Teleport) (1) | 2023.10.12 |
---|---|
Kubernetes에서 학습용 Job 생성하기 (0) | 2023.10.12 |
Kubernetes Offline 환경 설치하기 (0) | 2023.10.12 |
Kubernetes CI/CD 구축(Jenkins,Nexus) (0) | 2023.10.12 |
Kubeflow V1.4 설치 및 초기 설정(User 추가, CORS, dex DB 분리) (0) | 2023.10.12 |