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
を使用してコンテナを作成すれば良い。