이전 포스트
https://decompression.tistory.com/12
컨테이너 런타임 만들기 : Phase 1
컨테이너의 동작을 이해하기 위해 컨테이너 런타임을 만들어본다. 세상에 컨테이너 런타임이라고 알려진 소스들은 너무 많다.containerd, cri-o, kata container, runc, crun, podman 등등.. 그러나 이들은 같
decompression.tistory.com
전 글에서는 동작하는 컨테이너 데모를 만들어보았다.
이제 살을 붙여갈 차례인데 어떤 것부터 수행할지 고민하다가
역시 cli를 먼저 구현하려고 한다.
golang 에코시스템에는 spf13/cobra, urfave/cli 등 좋은 라이브러리들이 많지만
여기서는 urfave/cli를 사용하겠다.
구현해야 할 명령어의 목록은 이렇다. 이는 OCI Runtime Spec을 바탕으로 한다.
- State
- Create
- Start
- Kill
- Delete
더불어 create과정에서 컨테이너가 실행할 init까지 합쳐 총 6개의 커맨드를 구현한다.
물론 내부 로직들은 차차 구현해 나가도록 하고, 구조만 잡아보자.
먼저 cmd 디렉터리를 만들어 커맨드들을 몰아넣었다.

각 파일들의 내용은 별 거 없다. 모두 아래와 같은 간단한 스텁이다.
package cmd
import (
"context"
"github.com/urfave/cli/v3"
)
var DeleteCommand = &cli.Command{
Name: "delete",
Action: func(ctx context.Context, command *cli.Command) error {
return nil
},
}
이후 main.go에 커맨드들을 추가해 준다.
rootCmd := &cli.Command{
Commands: []*cli.Command{
cmd.CreateCommand,
cmd.DeleteCommand,
cmd.InitCommand,
cmd.KillCommand,
cmd.StartCommand,
cmd.StateCommand,
},
}
이제 원래의 동작을 현재의 구조에 맞게 옮길 것인데
진행하며 현재의 컨테이너 시작과 생성로직을 스펙에 맞게 분리해주어야 한다.
이는 어떻게 동작해야 할까?
OCI Runtime Spec의 Operation을 보면 create와 start을 보자.
Create
create <container-id> <path-to-bundle>
This operation MUST generate an error if it is not provided a path to the bundle and the container ID to associate with the container. If the ID provided is not unique across all containers within the scope of the runtime, or is not valid in any other way, the implementation MUST generate an error and a new container MUST NOT be created. This operation MUST create a new container.
All of the properties configured in config.json except for process MUST be applied. process.args MUST NOT be applied until triggered by the start operation. The remaining process properties MAY be applied by this operation. If the runtime cannot apply a property as specified in the configuration, it MUST generate an error and a new container MUST NOT be created.
The runtime MAY validate config.json against this spec, either generically or with respect to the local system capabilities, before creating the container (step 2). Runtime callers who are interested in pre-create validation can run bundle-validation tools before invoking the create operation.
Any changes made to the config.json file after this operation will not have an effect on the container.
Start
start <container-id>
This operation MUST generate an error if it is not provided the container ID. Attempting to start a container that is not created MUST have no effect on the container and MUST generate an error. This operation MUST run the user-specified program as specified by process. This operation MUST generate an error if process was not set.
process.args는 start작업이 실행되기 전까지 적용되어서는 안 된다.
process.args는 문자열 배열로, IEEE 표준 execvp의 argv와 유사한 의미를 가진다.
또한 start 명령어는 process에 지정된 사용자 정의 프로그램을 실행해야 한다
즉, create과정에서 cgroup이나 기타 설정등.. 컨테이너에 대한 전반적인 init작업을 통해
사용자가 정의한 프로세스를 실행(process.args) 하기 전 단계까지를 수행한다.
이를 구현하기 위한 여러 방법이 있겠지만, 여기서는 시그널을 이용하겠다.
프로세스는 POSIX 표준에 의해 SIGSTOP으로 프로세스를 멈추고, SIGCONT를 이용해 시작할 것이다.
runc의 경우 이러한 시그널 방식이 아닌 FIFO Pipe 방식을 택한다. 이유야 여럿이지만 주요하게는
한창 이러한 처리가 개발되던 go ~1.6 버전에서 시그널 처리에 있어 레이스 컨디션이 발생하는 문제가 있었기 때문이다.
현재는 원자성을 지키도록 해결된 상태임으로 그대로 사용한다.
func Init() {
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGCONT)
<-ch
fmt.Printf("Running: %v\n", os.Args[2:])
cgroup.SetupCgroups()
Must(syscall.Sethostname([]byte("container")))
const rootfs = "/root/ubuntufs"
Must(syscall.Chroot(rootfs))
Must(os.Chdir("/"))
Must(syscall.Mount("proc", "proc", "proc", 0, ""))
Must(syscall.Mount("tmpfs", "mytemp", "tmpfs", 0, ""))
defer Must(syscall.Unmount("proc", 0))
defer Must(syscall.Unmount("mytemp", 0))
if len(os.Args) < 3 {
log.Fatal("Usage: containeruntime")
}
syscall.Exec(os.Args[2], os.Args[3:], os.Environ())
}
이제 container를 create 하여 SIGCONT 대기상태를 만든 이후
[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime create /bin/bash
Running: [/bin/bash]
그리고 다른 터미널에서 프로세스를 향해 SIGCONT 시그널을 날려준다.
이는 kill 명령어를 통해 수행 가능하다.
[hwyoon@rocky9 containeruntime]$ sudo kill -SIGCONT 7244
그러면 멈춰있던 Init 프로세스가 동작하며 원래와 같은 동작을 수행한다.
[hwyoon@rocky9 containeruntime]# sudo ./containeruntime create /bin/bash
Running: [/bin/bash]
Running: [/bin/bash]
root@container:/# ls
bin dev home lib32 libx32 mnt opt root sbin sys usr
boot etc lib lib64 media mytemp proc run srv tmp var
이를 start명령어로 빼기 위해서는 컨테이너의 상태(state)를 저장해야 한다.
이러한 상태 기반 동작은 다음 포스트에서 더 구현해 보자.
물론 현재의 SIGSTOP, SIGCONT구조등은 create와 start가 같은 터미널 세션에서 일어난다고
가정하기에 표준적인 컨테이너 런타임에 적용하기 결함이 많다.
때문에 이는 포스트가 진행되며 조금씩 변경될 예정이다.
이외에도 main.go에 몰려있던 로직을 container와 linux패키지로 분리했다.
해당 포스트에서 다루지 않는 변경사항은 PR을 참조하자.
https://github.com/yoonhyunwoo/containeruntime/pull/1
feat: implement OCI runtime CLI structure with create/start separation by yoonhyunwoo · Pull Request #1 · yoonhyunwoo/containe
OCI Runtime Spec에 맞는 CLI 구조를 작성했다.
github.com
참고자료
https://github.com/golang/go/issues/14571
https://github.com/opencontainers/runc/pull/886
https://cli.urfave.org/v3/getting-started/
https://github.com/opencontainers/runtime-spec/blob/main/runtime.md
'개발노트' 카테고리의 다른 글
| 컨테이너 런타임 만들기 : Phase 3 (1) | 2025.08.06 |
|---|---|
| 컨테이너 런타임 만들기 : Phase 1 (0) | 2025.07.24 |
| Kubelet 서비스 어카운트 Token Jitter 최적화 (1) | 2025.07.22 |