アットランタイム

Dockerfile の STOPSIGNAL のハンドリング

はじめに

Dockerfile の STOPSIGNAL について、コンテナランタイムがどのようにハンドリングするのかを見ていく。

STOPSIGNAL

Dockerfile の STOPSIGNAL は、ランタイムによるコンテナ停止時に PID 1 のプロセスに送信するシグナルを制御するディレクティブである。 未指定の場合、SIGTERM シグナルが送信される。 このシグナルはコンテナを graceful にシャットダウンのために使用するため、一般のアプリケーションであれば SIGTERM で問題ないと思われる。 一方で、例えば nginx の graceful シャットダウンでは、SIGTERM ではなく、SIGQUIT であることが知られている。

Image Config

Dockerfile の STOPSIGNAL ディレクティブはビルド時に OCI Image Spec の Config の StopSignal にマップされる。 そのため、OCI Image Spec の SoptSignal を適切にハンドルするランタイムであれば Docker でなくともコンテナの停止時に指定したシグナルが送信されることが期待される。

https://github.com/opencontainers/image-spec/blob/v1.1.0/config.md

StopSignal string, OPTIONAL

The field contains the system call signal that will be sent to the container to exit. The signal can be a signal name in the format SIGNAME, for instance SIGKILL or SIGRTMIN+3.

containerd の実装

contaienrd のクライアント実装の ctr では kill コマンドの signal 引数が未指定の場合、GetStopSignal にてシグナルを取得する。

https://github.com/containerd/containerd/blob/v2.0.1/cmd/ctr/commands/tasks/kill.go#L123C4-L123C7

var killCommand = &cli.Command{
	Name:      "kill",
(snip)
	Action: func(cliContext *cli.Context) error {
		id := cliContext.Args().First()
(snip)
		} else {
			sig, err = containerd.GetStopSignal(ctx, container, sig)
			if err != nil {
				return err
			}
		}
		task, err := container.Task(ctx, nil)
		if err != nil {
			return err
		}
		err = RemoveCniNetworkIfExist(ctx, container)
		if err != nil {
			return err
		}
		return task.Kill(ctx, sig, opts...)

GetStopSignal はラベルから取得する。

https://github.com/containerd/containerd/blob/v2.0.1/client/signals.go#L37

// GetStopSignal retrieves the container stop signal, specified by the
// well-known containerd label (StopSignalLabel)
func GetStopSignal(ctx context.Context, container Container, defaultSignal syscall.Signal) (syscall.Signal, error) {
	labels, err := container.Labels(ctx)
	if err != nil {
		return -1, err
	}

	if stopSignal, ok := labels[StopSignalLabel]; ok {
		return signal.ParseSignal(stopSignal)
	}

	return defaultSignal, nil
}

ラベルは、WithImageStopSignal を呼び出したときに設定される。 シグナルの取得は GetOCIStopSignal が担っていると言える。

https://github.com/containerd/containerd/blob/v2.0.1/client/container_opts.go#L163

// WithImageStopSignal sets a well-known containerd label (StopSignalLabel)
// on the container for storing the stop signal specified in the OCI image
// config
func WithImageStopSignal(image Image, defaultSignal string) NewContainerOpts {
	return func(ctx context.Context, _ *Client, c *containers.Container) error {
		if c.Labels == nil {
			c.Labels = make(map[string]string)
		}
		stopSignal, err := GetOCIStopSignal(ctx, image, defaultSignal)
		if err != nil {
			return err
		}
		c.Labels[StopSignalLabel] = stopSignal
		return nil
	}
}

GetOCIStopSignal は Image Config の JSON をパースして StopSignal プロパティより読み出している。 なお、image.Config() は、ocispec.Descriptor を返すので、desc をキーとして boltdb より JSON を読み出す。

// GetOCIStopSignal retrieves the stop signal specified in the OCI image config
func GetOCIStopSignal(ctx context.Context, image Image, defaultSignal string) (string, error) {
	_, err := signal.ParseSignal(defaultSignal)
	if err != nil {
		return "", err
	}
	ic, err := image.Config(ctx)
	if err != nil {
		return "", err
	}
	if !images.IsConfigType(ic.MediaType) {
		return "", fmt.Errorf("unknown image config media type %s", ic.MediaType)
	}

	var (
		ociimage v1.Image
		config   v1.ImageConfig
	)
	p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
	if err != nil {
		return "", err
	}

	if err = json.Unmarshal(p, &ociimage); err != nil {
		return "", err
	}
	config = ociimage.Config

	if config.StopSignal == "" {
		return defaultSignal, nil
	}

	return config.StopSignal, nil
}

ctr では、コンテナの作成時に rootfs が指定されていなければ WithImageStopSignal が呼ばれるため、STOPSIGNAL が尊重される。

https://github.com/containerd/containerd/blob/v2.0.1/cmd/ctr/commands/run/run_unix.go#L195

func NewContainer(ctx context.Context, client *containerd.Client, cliContext *cli.Context) (containerd.Container, error) {
	var (
		id     string
		config = cliContext.IsSet("config")
	)
(snip)
		if cliContext.Bool("rootfs") {
			rootfs, err := filepath.Abs(ref)
			if err != nil {
				return nil, err
			}
			opts = append(opts, oci.WithRootFSPath(rootfs))
			cOpts = append(cOpts, containerd.WithContainerLabels(commands.LabelArgs(cliContext.StringSlice("label"))))
		} else {
(snip)
			cOpts = append(cOpts, containerd.WithImageStopSignal(image, "SIGTERM"))
		}
(snip)
	cOpts = append(cOpts, spec)

	// oci.WithImageConfig (WithUsername, WithUserID) depends on access to rootfs for resolving via
	// the /etc/{passwd,group} files. So cOpts needs to have precedence over opts.
	return client.NewContainer(ctx, id, cOpts...)
}

上記より containerd を使用する場合、クライアントが WithImageStopSignal を使用してコンテナを作成すれば良い。