Kubeflow Componet 분석 #1 - Jupyter Notebook ( Notebook 권한 수정)

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 으로 생성된 것을 확인 할 수 있습니다.

좋은 웹페이지 즐겨찾기