이전 글 : https://decompression.tistory.com/14

 

컨테이너 런타임 만들기 : Phase 2

이전 포스트https://decompression.tistory.com/12 컨테이너 런타임 만들기 : Phase 1컨테이너의 동작을 이해하기 위해 컨테이너 런타임을 만들어본다. 세상에 컨테이너 런타임이라고 알려진 소스들은 너무

decompression.tistory.com

 

전 글에서는 OCI(Open Container Initiative) Runtime의
CLI(Commad Line Interface) 골자를 만들었다.

 

이번엔 OCI Runtime Spec의 State와 그를 기반으로 한
컨테이너 동작을 구현한다.

 

컨테이너 state의 예시는 다음과 같다.

{
    "ociVersion": "0.2.0",
    "id": "oci-container1",
    "status": "running",
    "pid": 4422,
    "bundle": "/containers/redis",
    "annotations": {
        "myKey": "myValue"
    }
}

 

어려울 건 없다. State가 가져야 할 값은 다음과 같다.

  • ociVersion : 현재 state가 준수하는 OCI Runtime Spec의 버전
  • id : 컨테이너의 ID, 호스트 내에서 고유해야 함
  • status : 컨테이너 런타임 상태 (creating, created, running, stopped)
  • pid : 컨테이너 프로세스 ID
  • bundle : 컨테이너 번들 디렉터리의 절대 경로
  • annotations : 컨테이너의 주석 목록

이전에 만들었던 container 패키지에 직접 구조체를 생성할 수도 있겠지만
OCI Runtime Spec은 specs-go 패키지에서 사용할 수 있는 구조체등을 제공한다,

 

specs-go가 제공하는 State구조체는 아래와 같다.

// State holds information about the runtime state of the container.
type State struct {
	// Version is the version of the specification that is supported.
	Version string `json:"ociVersion"`
	// ID is the container ID
	ID string `json:"id"`
	// Status is the runtime status of the container.
	Status ContainerState `json:"status"`
	// Pid is the process ID for the container process.
	Pid int `json:"pid,omitempty"`
	// Bundle is the path to the container's bundle directory.
	Bundle string `json:"bundle"`
	// Annotations are key values associated with the container.
	Annotations map[string]string `json:"annotations,omitempty"`
}

 

이러한 state를 어디서 관리할지는 자율적이다. 
이번 contaeruntime 프로젝트의 경우 /run/containeruntime에 저장한다.

 

/run 디렉터리는 런타임 변수 데이터를 저장하는 디렉터리로 시스템이 부팅된 이후부터의 데이터를 가진다.
containerd와 같은 고수준 런타임은 상태 저장형으로 동작하기에 /var/lib/containerd와 같은 영구적인 디렉터리를 사용하지만

저수준 런타임의 경우 어디까지나 런타임 스코프 내에서 동작해야 한다.

 

container패키지에 state관리를 위한 state.go파일을 만들어 관리용 함수 몇 개를 추가했다.

package container

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/opencontainers/runtime-spec/specs-go"
)

const (
	containeruntimeStateDir string = "/run/containeruntime"
)

func initStateDir() error {
	return os.MkdirAll(containeruntimeStateDir, 0755)
}

func getStatePath(containerId string) string {
	return filepath.Join(containeruntimeStateDir, containerId+".json")
}

func saveState(state *specs.State) error {
	statePath := getStatePath(state.ID)
	tempPath := statePath + ".tmp"

	f, err := os.Create(tempPath)
	if err != nil {
		return fmt.Errorf("Can not save state: %v", err)
	}
	defer f.Close()
	defer os.Remove(tempPath)

	if err := json.NewEncoder(f).Encode(state); err != nil {
		return fmt.Errorf("Can not parsing state : %v", err)
	}

	f.Close()
	return os.Rename(tempPath, statePath)
}

func loadState(containerId string) (*specs.State, error) {
	statePath := getStatePath(containerId)
	state := &specs.State{}

	f, err := os.Open(statePath)
	if err != nil {
		return nil, fmt.Errorf("Can not open state: %v", err)
	}
	defer f.Close()

	if err = json.NewDecoder(f).Decode(state); err != nil {
		return nil, fmt.Errorf("Can not parsing state : %v", err)
	}

	return state, nil
}

func deleteState(containerId string) error {
	statePath := getStatePath(containerId)
	if err := os.Remove(statePath); err != nil {
		return fmt.Errorf("Can not delete state: %v", err)
	}
	return nil
}

func listState(containerId string) ([]*specs.State, error) {
	var states []*specs.State
	files, err := os.ReadDir(containeruntimeStateDir)
	if err != nil {
		return nil, fmt.Errorf("Can not list state: %v", err)
	}

	for _, file := range files {
		if strings.HasSuffix(file.Name(), ".json") {
			state, err := loadState(file.Name())
			if err != nil {
				continue
			}
			states = append(states, state)
		}
	}

	return states, nil
}

func newContainerState(id, bundlePath string) (*specs.State, error) {
	state := &specs.State{
		Version:     specs.Version,
		ID:          id,
		Status:      specs.StateCreating,
		Pid:         0,
		Bundle:      bundlePath,
		Annotations: nil,
	}
	return state, nil
}

func setContainerPID(containerId string, pid int) error {
	state, err := loadState(containerId)
	if err != nil {
		return fmt.Errorf("Can set container pid: %v", err)
	}

	state.Pid = pid
	if err := saveState(state); err != nil {
		return fmt.Errorf("Can set container pid: %v", err)
	}

	return nil
}

 

 

이외에도 이전 포스트에서 사용한 SIGCON 시그널을 start에서 실행되도록 만든다.

func Start(containerId string) error {
	state, err := loadState(containerId)
	if err != nil {
		fmt.Printf("container : %v\n", err)
		return fmt.Errorf("container : %v", err)
	}
	state.Status = specs.StateRunning
	syscall.Kill(state.Pid, syscall.SIGCONT)
	saveState(state)
	return nil
}

 

 

이후 start 함수를 cli에서 호출할 수 있도록 한다.

cmd/start.go

package cmd

import (
    "context"
    "fmt"

    "github.com/urfave/cli/v3"
    "github.com/yoonhyunwoo/containeruntime/internal/container"
)

var StartCommand = &cli.Command{
    Name: "start",
    Action: func(ctx context.Context, command *cli.Command) error {
       if command.Args().Len() != 1 {
          return nil
       }

       containerId := command.Args().First()
       err := container.Start(containerId)
       if err != nil {
          fmt.Printf("Can not start conateinr %s", containerId)
       }
       return nil
    },
}

 

이제 컨테이너 state를 반환하는 state명령어를 만든다. 

출력을 위해 직렬화된 state는 아래와 같다. (state-schema.json)

{
    "description": "Open Container Runtime State Schema",
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "properties": {
        "ociVersion": {
            "$ref": "defs.json#/definitions/ociVersion"
        },
        "id": {
            "description": "the container's ID",
            "type": "string"
        },
        "status": {
            "type": "string",
            "enum": [
                "creating",
                "created",
                "running",
                "stopped"
            ]
        },
        "pid": {
            "type": "integer",
            "minimum": 0
        },
        "bundle": {
            "type": "string"
        },
        "annotations": {
            "$ref": "defs.json#/definitions/annotations"
        }
    },
    "required": [
        "ociVersion",
        "id",
        "status",
        "bundle"
    ]
}

 

이게 출력되면 아래와 같은 예시가 나온다.

{
    "ociVersion": "0.2.0",
    "id": "oci-container1",
    "status": "running",
    "pid": 4422,
    "bundle": "/containers/redis",
    "annotations": {
        "myKey": "myValue"
    }
}

 

이에 맞게 state구조체를 반환하는 State 함수를 만든다.

internal/container/container.go

func State(containerId string) (*specs.State, error) {
    state, err := loadState(containerId)
    if err != nil {
       fmt.Printf("container : %v\n", err)
    }
    return state, err
}

 

그리고 cmd/state.go에서 이를 받아 출력한다.

package cmd

import (
    "context"
    "encoding/json"
    "fmt"
    "os"

    "github.com/urfave/cli/v3"
    "github.com/yoonhyunwoo/containeruntime/internal/container"
)

var StateCommand = &cli.Command{
    Name: "state",
    Action: func(ctx context.Context, command *cli.Command) error {
       if command.Args().Len() != 1 {

          return nil
       }

       containerId := command.Args().First()
       containerState, err := container.State(containerId)
       if err != nil {
          fmt.Printf("Can not get conateinr %s", containerId)
       }

       containerStateBytes, _ := json.MarshalIndent(containerState, "", " ")
       os.Stdout.Write(containerStateBytes)
       return nil
    },
}

 

 

빌드 이후 실행해 보면 포맷에 맞게 state가 잘 떨어지는 모습을 볼 수 있다.

[hwyoon@rocky9 containeruntime]$ ./containeruntime state id
{
 "ociVersion": "1.2.1",
 "id": "id",
 "status": "created",
 "pid": 19977,
 "bundle": "/rootfs/ubuntu"
}

 

 

이외에도 cli기본 뼈대를 위해 나머지를 간단히 만든다.

 

kill에 대한 설명은 아래와 같다.
create, running상태인 컨테이너를 대상으로 지정된 시그널을 보내는 것이다.

Kill
kill <container-id> <signal>

This operation MUST generate an error if it is not provided the container ID. Attempting to send a signal to a container that is neither created nor running MUST have no effect on the container and MUST generate an error. This operation MUST send the specified signal to the container process.

 

간단히 kill 함수를 구현해 주었다.

internal/container/container.go

func Kill(containerId string, signal syscall.Signal) error {
    state, err := loadState(containerId)
    if err != nil {
       fmt.Printf("container : %v\n", err)
    }
    if state.Status == specs.StateRunning || state.Status == specs.StateCreated {
       return fmt.Errorf("You can send a signal only to containers in the running or created state.")
    }
    syscall.Kill(state.Pid, signal)
    return nil
}

 

이제 kill signal을 어떻게 받을 것 인지가 중요한데.

SIGTERM과 같이 human-readable 하게 보내지는 않는다. 

 

linux 시스템의 kill 커맨드와 같이 숫자로 받는다. (사실 시스템콜이 다 int 기반이다)

흔히 아래와 같은 명령어로 익숙할 것이다.

kill -9 <PID>

 

이런 번호를 아는 방법은 여러 가지 있겠지만

가장 편한 건 kill -l을 통해 조회할 수 있다.

[hwyoon@rocky9 containeruntime]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

 

go에서 이를 다루는 방법은 간단하다.

syscall 패키지에서 제공하는 Signal 객체로 캐스팅해 주면 된다.

cmd/kill.go

package cmd

import (
    "context"
    "fmt"
    "strconv"
    "syscall"

    "github.com/urfave/cli/v3"
    "github.com/yoonhyunwoo/containeruntime/internal/container"
)

var KillCommand = &cli.Command{
    Name:      "kill",
    Usage:     "This command sends a specific signal to the main process of a container.",
    ArgsUsage: "<containerid> <signal>",
    Action: func(ctx context.Context, command *cli.Command) error {

       if command.Args().Len() != 2 {
          return nil
       }

       containerId := command.Args().First()

       signalNumber, err := strconv.Atoi((command.Args().Get(1)))
       if err != nil {
          fmt.Printf("Invalid signal number: %s\n", command.Args().Get(1))
          return nil
       }

       signal := syscall.Signal(signalNumber)
       err = container.Kill(containerId, signal)
       if err != nil {
          fmt.Printf("Can not start conateinr %s", containerId)
       }
       return nil
    },
}

 

다음은 delete다. 이를 수행 시 SIGKILL을 통해 프로세스를 종료한다.

종료가 무사히 완료되면 컨테이너를 위해 생성했던 cgroups, state 등을 정리해야 한다.

 

관행적으로 프로세스의 생존유무는 signal 0으로 체크한다.

여기선 가볍게 5초 정도 기다리도록 했다.

internal/container/container.go

func Delete(containerId string) error {
    _ = Kill(containerId, syscall.SIGKILL)
    for range 5 {
       time.Sleep(1 * time.Second)
       if err := Kill(containerId, 0); err != nil {
          return deleteState(containerId)
       }
    }
    return errors.New("The container is still running")
}

 

또한 Cgroups도 정리해야 한다.

internal/linux/cgroup/v2.go

func CleanCgroups() error {
	cgroupRoot := "/sys/fs/cgroup"
	processCgroup := filepath.Join(cgroupRoot, "gamap-container", "processes")
	if _, err := os.Stat(processCgroup); err == nil {
		procsFile := filepath.Join(processCgroup, "cgroup.procs")
		err = os.WriteFile(procsFile, []byte(""), 0700)
		if err != nil {
			return errors.New("Error removing cgroup processes")
		}
		time.Sleep(100 * time.Millisecond)
		err = os.Remove(processCgroup)
		if err != nil {
			return errors.New("Error removing cgroup processes")
		}
	}

	return nil
}

 

 

이후 역시 이를 delete커맨드에서 사용하도록 만들어준다.

cmd/delete.go

package cmd

import (
	"context"
	"errors"

	"github.com/urfave/cli/v3"
	"github.com/yoonhyunwoo/containeruntime/internal/container"
	"github.com/yoonhyunwoo/containeruntime/internal/linux/cgroup"
)

var DeleteCommand = &cli.Command{
	Name:      "delete",
	Usage:     "This command deletes a container and its associated resources.",
	ArgsUsage: "<container-id>",
	Action: func(ctx context.Context, command *cli.Command) error {
		if command.Args().Len() != 1 {
			return errors.New("container-id is required")
		}

		containerId := command.Args().First()
		if err := container.Delete(containerId); err != nil {
			return err
		}

		return cgroup.CleanCgroups()
	},
}

 

[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime create
Running: []
[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime state id
{
 "ociVersion": "1.2.1",
 "id": "id",
 "status": "created",
 "pid": 21245,
 "bundle": "/rootfs/ubuntu"
}[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime delete id
[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime state id
container : Can not open state: open /run/containeruntime/id.json: no such file or directory
Can not get conateinr idnull[hwyoon@rocky9 containeruntime]$ 
[hwyoon@rocky9 containeruntime]$ ps -ef | grep -i 21245
hwyoon     21285   19793  0 22:52 pts/2    00:00:00 [rosetta] /usr/bin/grep grep --color=auto -i 21245
[hwyoon@rocky9 ~]$ ls /sys/fs/cgroup/gamap-container/processes
ls: cannot access '/sys/fs/cgroup/gamap-container/processes': No such file or directory

 

프로세스 및 state, cgroups까지 잘 지워진다.

 

만들면서 계속 빌드 및 테스트가 반복되고 있다.
어딘가에는 명시를 해두어야 할 것 같아서 Makefile을 만들었다.

BINARY_NAME=containeruntime
GOOS=linux

.PHONY: build setup-ubuntu lint

build:
    GOOS=$(GOOS) go build -o $(BINARY_NAME) .

setup-ubuntu:
    sudo docker create --name temp-ubuntu ubuntu:22.04
    sudo mkdir -p /root/ubuntufs
    sudo docker export temp-ubuntu -o /tmp/ubuntu.tar
    sudo tar -xf /tmp/ubuntu.tar -C /root/ubuntufs
    sudo rm /tmp/ubuntu.tar
    sudo docker rm temp-ubuntu

lint:
    go fmt ./...

 

복잡하게 만들 필요는 없을 것 같고

build, setup-ubuntu, lint정도만 만들었다.

 

지금까지의 플로우는 아래와 같다.

containeruntime의 컨테이너 라이프사이클

 

 

이외에 따로 작성하지 않은 변경사항은 PR(Pull Request)을 참고하자

https://github.com/yoonhyunwoo/containeruntime/pull/2/files

 

feat : Handles container lifecycle based on state by yoonhyunwoo · Pull Request #2 · yoonhyunwoo/containeruntime

State-based container lifecycle management Utilizing /run/containeruntime as the state directory

github.com

 

 

다음 포스트에선 에러 처리 및 config기반 처리를 수행하려고 한다.

 

참고자료

https://pkg.go.dev/github.com/opencontainers/runtime-spec/specs-go

https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html

https://github.com/opencontainers/runtime-spec/blob/main/schema/state-schema.json

https://www.linuxquestions.org/questions/linux-newbie-8/signal-0-in-linux-835606/

+ Recent posts