DevOps 잡다구리/Kubernetes Stuff

[Kubernetes] Pod 생성 과정에서 container runtime 들이 하고 있는 일들 -1 CRICTL part

WhiteGoblin 2024. 9. 26. 18:25
반응형

0. 서론

 

지난번 글에서는 CRI-O 에 관하여 구체적으로 들어가서 코드를 살펴보고 공식문서에서 컨테이너가 생성되는 과정을 번역했습니다.

 

해당 글을 제가 다시 읽어보니 뭔가 너무 원록적이고 바로 와닿지 않는거 같아서 조금 더 범위를 줄여서 설명하고자 이렇게 글을 작성하게 되었습니다. 

 

우선 저희는 아래 명령어에 익숙할것이라고 생각 되어 집니다. 

 

kubectl run my-busybox --image=busybox --command -- sleep 3600

 

[출처] chatgpt 에 "kubectl 로 pod 를 만드는 명령어 하나 예제로 들어줘 이미지는 busybox 를 사용할거야" 를 쳤을때 나오는 명령어입니다.

 

위 명령어를 사용 하면 한시간 정도 유지가 되는 BusyBox 라는 이미지를 가지고 있는 컨테이너가 생성 됩니다. 

 

하지만 저는 이것을 사용하지 않고 crictl 을 사용해서 하나하나 만드는 과정을 가지고자 합니다. 

 

우선 CRI-O 안에서는 요청에 따라서 많은 과정들이 있지만 사용자 관점에서는 사실 어떻게 진행되는지 모르는게 더 많다고 생각합니다. 

 

https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md

 

cri-tools/docs/crictl.md at master · kubernetes-sigs/cri-tools

CLI and validation tools for Kubelet Container Runtime Interface (CRI) . - kubernetes-sigs/cri-tools

github.com

 

위 예제에서 정보를 얻어서 진행을 해보겠습니다. 

 

** 우선 이 과정은 crictl, cri-o, runc 가 설치 되어 있다는 것을 가정하고 진행되고 있습니다. 이러한 것들이 설치가 되어 있지 않다면 
아래 링크들을 참고 해주시면 감사하겠습니다. 


crictl installation : https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md

cri-o installation : https://github.com/cri-o/cri-o/blob/main/install.md

runc installation : https://github.com/opencontainers/runc

 

GitHub - opencontainers/runc: CLI tool for spawning and running containers according to the OCI specification

CLI tool for spawning and running containers according to the OCI specification - opencontainers/runc

github.com

 

1. Busybox 가 실행되고 있는 container 생성되는 과정 

우선 제일 먼저 저희가 container runtime 으로 사용할 cri-o service 가 설치 되어 있는지 그리고 버전을 체크해보겠습니다. 

sudo crictl --runtime-endpoint unix:///var/run/crio/crio.sock version

 

저는 아래와 같은 출력이 나왔습니다. 

 

설치되어 있는게 확인 되었으니 이제 앞으로의 과정을 간략하게 정리 하자면

 

  • 필요한 컨테이너 이미지 pull 
  • 컨테이너를 실행시킬 Pod Sandbox 생성하기
  • pod sandbox 안에 컨테이너를 생성하기
  • 컨테이너 실행하기

이렇게 진행 되게 됩니다. 

 

1.1. 필요한 컨테이너 이미지 pull 

sudo crictl pull busybox

 

 

위 사진과 같이 busybox 이미지를 가지고 오게 됩니다. 따로 이미지 태그를 붙이지 않아서 latest 를 가지고 올거 같습니다. 

 

1.2. 컨테이너를 실행시킬 Pod Sandbox 생성하기

POD_ID=$(sudo crictl runp testdata/sandbox_config.json)

 

위와 같이 pod 가 생성 되는데 sandbox_config.json 안에 있는 설정들을 기반으로 만들어지게 됩니다. 

 

sandbox_config.json 은 https://github.com/kubernetes/cri-api/blob/c75ef5b/pkg/apis/runtime/v1/api.proto#L356 를 기준으로 작성 되는거 같습니다. 그래서 파일 내용을 보자면

 

{
    "metadata": {
      "name": "busybox-sandbox",
      "namespace": "default",
      "attempt": 1,
      "uid": "busybox-sandbox-test"
    },
    "log_directory": "/tmp",
    "linux": {
    }
  }

 

위와 같이 되어 있습니다. 

sudo crictl inspectp $POD_ID

 

위 명령어를 통해서 pod 에 대한 상세한 정보를 얻을 수 있습니다. 

 

1.3. pod sandbox 안에 컨테이너를 생성하기

 

이제 그 pod 안에서 container 를 생성해줍니다.

CONTAINER_ID=$(sudo crictl create $POD_ID testdata/container_config.json testdata/sandbox_config.json)

 

위 명령어를 통해서 Container 가 해당 Pod 에 생성된것을 확인할 수 있습니다. 

 

1.4. 컨테이너 실행하기

 

sudo crictl start $CONTAINER_ID

 

 

컨테이너를 실행하게 되면 위 그림과 같이 컨테이너 아이디가 출력됩니다. 

 

다시 한번 명령어로 확인하게 되면 

sudo crictl ps

 

 

state 가 running 으로 변경되었는지 나오게 됩니다. 

 

2. CRICTL  에서  Pod Sandbox 생성하기 위해 하는 역할

위 부분중 pod sandbox 를 만드는 과정에서 각각이 무엇을 하는지 확인하고자 합니다. 

 

2.1 cri runtime service instance 생성

// 
// cmd > crictl > sandbox.go > runPodCommand
// ... L64
Action: func(c *cli.Context) error {
		sandboxSpec := c.Args().First()
		if c.NArg() != 1 || sandboxSpec == "" {
			return cli.ShowSubcommandHelp(c)
		}

		runtimeClient, err := getRuntimeService(c, c.Duration("cancel-timeout"))
		if err != nil {
			return err
		}

		podSandboxConfig, err := loadPodSandboxConfig(sandboxSpec)
		if err != nil {
			return fmt.Errorf("load podSandboxConfig: %w", err)
		}

		// Test RuntimeServiceClient.RunPodSandbox
		podID, err := RunPodSandbox(runtimeClient, podSandboxConfig, c.String("runtime"))
		if err != nil {
			return fmt.Errorf("run pod sandbox: %w", err)
		}
		fmt.Println(podID)
		return nil
},
// ... L89

 

crictl runp 를 실행할 시 위와 같은 함수를 실행하게 됩니다. 

		runtimeClient, err := getRuntimeService(c, c.Duration("cancel-timeout"))
		if err != nil {
			return err
		}

 

위 부분에서 runtime service 를 가지고 와서 runtimeClilent 를 선언해주게 됩니다. 이후 이 client 는 CRI 를 통해서 cri-o 로 요청을 보내게 됩니다. 

 

2.2. pod sandbox config json 파일 decode

		podSandboxConfig, err := loadPodSandboxConfig(sandboxSpec)
		if err != nil {
			return fmt.Errorf("load podSandboxConfig: %w", err)
		}

위 코드는 아까 저희가 넘겨준 sandbox_config.json 을 decode 해서 값을 저장 해주게 됩니다. 

 

2.3. gRPC 서버 와 통신을 위한 request 생성

		podID, err := RunPodSandbox(runtimeClient, podSandboxConfig, c.String("runtime"))
		if err != nil {
			return fmt.Errorf("run pod sandbox: %w", err)
		}

 

마지막으로 request 생성 및 해당 request 를 client 를 통해 CRI-O 로 전송하게 됩니다.

 

RunPodSandbox 는

func RunPodSandbox(client internalapi.RuntimeService, config *pb.PodSandboxConfig, runtime string) (string, error) {
	request := &pb.RunPodSandboxRequest{
		Config:         config,
		RuntimeHandler: runtime,
	}
	logrus.Debugf("RunPodSandboxRequest: %v", request)
	r, err := client.RunPodSandbox(context.TODO(), config, runtime)
	logrus.Debugf("RunPodSandboxResponse: %v", r)
	if err != nil {
		return "", err
	}
	return r, nil
}

 

 

2.4 client 를 통해서 요청 전송

	r, err := client.RunPodSandbox(context.TODO(), config, runtime)

 

위 코드는 이제 kubernetes 에서 정의하고 있는 CRI 를 따라가고 있습니다. 
https://github.com/kubernetes/kubernetes/tree/v1.30.5/pkg/kubelet/cri/remote

// https://github.com/kubernetes/kubernetes/tree/v1.30.5/pkg/kubelet/cri/remote
func (r *remoteRuntimeService) RunPodSandbox(ctx context.Context, config *runtimeapi.PodSandboxConfig, runtimeHandler string) (string, error) {
	// Use 2 times longer timeout for sandbox operation (4 mins by default)
	// TODO: Make the pod sandbox timeout configurable.
	timeout := r.timeout * 2

	klog.V(10).InfoS("[RemoteRuntimeService] RunPodSandbox", "config", config, "runtimeHandler", runtimeHandler, "timeout", timeout)

	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()
  // cri-api
	resp, err := r.runtimeClient.RunPodSandbox(ctx, &runtimeapi.RunPodSandboxRequest{
		Config:         config,
		RuntimeHandler: runtimeHandler,
	})

	if err != nil {
		klog.ErrorS(err, "RunPodSandbox from runtime service failed")
		return "", err
	}

	podSandboxID := resp.PodSandboxId

	if podSandboxID == "" {
		errorMessage := fmt.Sprintf("PodSandboxId is not set for sandbox %q", config.Metadata)
		err := errors.New(errorMessage)
		klog.ErrorS(err, "RunPodSandbox failed")
		return "", err
	}

	klog.V(10).InfoS("[RemoteRuntimeService] RunPodSandbox Response", "podSandboxID", podSandboxID)

	return podSandboxID, nil
}

 

위 코드에서 주목할 것은 

	resp, err := r.runtimeClient.RunPodSandbox(ctx, &runtimeapi.RunPodSandboxRequest{
		Config:         config,
		RuntimeHandler: runtimeHandler,
	})

 

// k8s.io/cri-api/pkg/apis/runtime/v1/api.pb.go 
func (c *runtimeServiceClient) RunPodSandbox(ctx context.Context, in *RunPodSandboxRequest, opts ...grpc.CallOption) (*RunPodSandboxResponse, error) {
	out := new(RunPodSandboxResponse)
	// sends request 
	err := c.cc.Invoke(ctx, "/runtime.v1.RuntimeService/RunPodSandbox", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

 

위 부분으로 결국 config 와 알맞는 handler 쪽으로 요청을 보내게 됩니다. 

 

이렇게 진행되고 난 후 crictl 에서는 cri-o 에서 처리된 결과를 기다리게 됩니다. 

 

CRI-O 에서 처리 되는 부분은 사실 저번 글에서 유추할 수 있지만 여기에 더 쓰기에는 글이 길어지는거 같아서 기회가 되면 다음 편에서 추가적으로 작성할 수 있도록 하겠습니다. 

 

3. Conclusion

 

이처럼 crictl 을 보면서 알게 된것은 

 

1. cri 를 통해서 kubelet 뿐만이 아니라 다른 cmd line tool 로도 container runtime 에 요청을 보낼 수 있게 interface 가 정의 되어 있어서 따로 툴을 만들어서 이용할 수 있다

2. cri-api 들이 정해져 있어서 이러한 요청에 필요한 metadata 도 정해져 있다. 

3. crictl part에서는 결국 runtime service 가지고 오기, config decode, request 생성 및 전송 

 

위와 같은 3가지 사실을 정리 해볼 수 있을거 같습니다. 

 

다음에는 CRI-O 에서 Run Pod Sandbox 요청을 어떻게 처리하는지에 대해서 들고 오겠습니다. 

 

어느새 여름이 가고 가을이 왔습니다. 다들 환절기에 감기들지 않게 조심하시고 두서 없는 글 읽어 주셔서 감사합니다. 

 

 

 

반응형