Table of Contents
本文是对个人项目 Containers Hooks Toolkit 的详细介绍。
Overview

container-hooks-toolkit
用于向容器的配置文件(config.json
)中插入自定义的oci hooks
,以实现在容器的不同生命周期进行细粒度的操作,组件包括:
container-hooks-runtime
container-hooks-ctk
container-hooks
container-hooks-runtime
container-hooks-runtime
是对主机上安装的runc
的轻量级包装器,通过将指定的oci hooks
注入容器的运行时规范,然后调用主机本地的runc
,并传递修改后的带有钩子设置的容器运行时规范。runc
在启动容器时,会自动运行注入的oci hooks
。
container-hooks-runtime
的配置文件包含了container-hooks-runtime
的配置选项,路径为/etc/container-hooks/config.toml
,支持对其修改从而定义container-hooks-runtime
的日志文件路径、日志级别以及底层运行时:
[container-hooks-runtime]debug = "/etc/container-hooks/container-hooks-runtime.log"log-level = "info"runtimes = ["runc", "docker-runc"]
container-hooks-runtime
日志记录了容器生命周期的相关记录,默认路径为/etc/container-hooks/container-hooks-runtime.log
。
container-hooks-ctk
container-hooks-ctk
是一个命令行工具,主要用于配置各容器运行时支持container hooks runtime
,以向符合OCI
规范的容器中插入OCI Hooks
。
container-hooks-runtime
的完成介绍和用法见README of container-hooks-ctk。
container-hooks-ctk
所有操作均需要root
权限。
container-hooks-ctk [global options] command [command options] [arguments...]
container-hooks-ctk
支持的主要命令(每个命令又包含若干子命令)包括:
runtime
:runtime
命令用于配置各容器运行时支持/移除container-hooks-runtime
。config
:config
命令用于生成container hooks toolkit
的配置文件。install
:install
命令用于将container hooks toolkit
复制到/usr/bin
目录。
container-hooks-ctk
生成的默认配置文件路径为/etc/container-hooks/config.toml
,默认内容如下:
[container-hooks]# 插入的oci hooks文件的路径path = "/etc/container-hooks/hooks.json"
[container-hooks-ctk]# container-hooks-ctk工具的路径path = "/usr/bin/container-hooks-ctk"
[container-hooks-runtime]# container hooks runtime日志文件路径debug = "/etc/container-hooks/container-hooks-runtime.log"# 日志文件记录级别log-level = "info"# 默认底层容器运行时runtimes = ["runc", "docker-runc"]
container-hooks
container-hooks
是一个空程序,仅用于判断当前容器是否已经添加自定义hooks
,避免重复添加。
Usecase
container-toolkit
支持docker
,containerd
和cri-o
,简单用例介绍如下。
1.下载
git clone https://github.com/peng-yq/container-hooks-toolkit.git
2.编译
make all
下面的所有操作均需要
root
权限
3.复制到/usr/bin
cd bin./container-hooks-ctk install --toolkit-root=$(pwd)
4.生成配置文件
container-hooks-ctk config
5.以docker
为例,进行配置
container-hooks-ctk runtime configure --runtime=docker --defaultsystemctl restart docker
配置完成后的docker
配置文件:
{ "default-runtime": "container-hooks-runtime", "registry-mirrors": [ "https://yxzrazem.mirror.aliyuncs.com" ], "runtimes": { "container-hooks-runtime": { "path": "/usr/bin/container-hooks-runtime", "runtimeArgs": [] } }}
6.编写自定义oci hooks
,格式如下,必须添加第一个prestart hook
中的container-hooks
用于避免重复添加定义hooks
,需要写入至/etc/container-hooks/hooks.json
文件中(此路径可在配置文件中修改,请注意json格式问题)
{ "prestart": [ { "path": "/usr/bin/container-hooks" }, { "path": "/etc/container-hooks/test.sh" } ]}
这里用一个简单的脚本文件做为实例,脚本内容如下,指向脚本会往/etc/container-hooks/test.txt
中追加Hello World
:
#!/bin/bash
echo "Hello World" >> /etc/container-hooks/test.txt
7.运行容器,执行自定义hook
sudo docker run hello-world

Container Runtime
Runtime
在中文博客中一般被翻译为运行时,比如会经常看到docker
、containerd
、cri-o
和runc
等都被称为runtime
。但实际上他们的定义是不一样的,OCI
规范中的原文介绍如下:
OCI
对容器的定义也很直接了当,这里也介绍一下。
container
An environment for executing processes with configurable isolation and resource limitations. For example, namespaces, resource limits, and mounts are all part of the container environment.
我们知道容器是一种轻量级的虚拟化技术,与宿主机共用操作系统内核。容器其实就是可配置隔离和资源限制的进程执行环境,通过namespace
技术(将系统资源:如网络接口、进程ID列表、挂载点虚拟化)来确保容器内的应用程序进程与系统上的其他进程相隔离,容器中的进程会感觉它们是在一个独立的系统中运行。并通过cgroup
技术控制和限制容器可以使用的物理资源(如 CPU 时间、内存用量等)的能力。
runtime
An implementation of this specification. It reads the configuration files from a bundle, uses that information to create a container, launches a process inside the container, and performs other lifecycle actions.
OCI
对runtime
的定义就是根据容器配置文件创建容器进程,控制容器生命周期的工具,即低级运行时,例如runc
和kata-container
。
runtime caller
An external program to execute a runtime, directly or indirectly.
Examples of direct callers include containerd, CRI-O, and Podman. Examples of indirect callers include Docker/Moby and Kubernetes.
Runtime callers often execute a runtime via runc-compatible command line interface, however, its interaction interface is currently out of the scope of the Open Container Initiative Runtime Specification.
中文博客对运行时的描述其实是模糊的,极其容易让人混淆。OCI
对此做了一个很好的定义,运行时 (runtime
)只是最底层的低级的直接和容器交互的程序;而高级的面向用户的,一般直接或间接调用运行时的程序则称为runtime caller
。runtime caller
也可根据runtime
的关系,再细分为直接和间接的,直接的例如containerd
、cri-o
等;间接的比如k8s
和docker
。一般来说用户直接接触的都是indirect runtime caller
。
Container Shim
在Overview中提到container-hooks-runtime
是shim
,那shim
是什么呢?
每一个Containerd
或Docker
容器(实际上Docker
也是调用Containerd
来创建容器,具体关系见下图)都有一个相应的 “shim
” 守护进程,这个守护进程会提供一个API
,Containerd
使用该API
来管理容器基本的生命周期(启动/停止),在容器中执行新的进程、调整TTY
的大小以及与特定平台相关的其他操作。shim
还有一个作用是向Containerd
报告容器的退出状态,在容器退出状态被Containerd
收集之前,shim
会一直存在。这一点和僵尸进程很像,僵尸进程在被父进程回收之前会一直存在,只不过僵尸进程不会占用资源,而shim
会占用资源。

Shim
将Containerd
进程从容器的生命周期中分离出来,具体的做法是runc
在创建和运行容器之后退出,并将shim
作为容器的父进程,即使Containerd
进程挂掉或者重启,也不会对容器造成任何影响。这样做的好处很明显,你可以高枕无忧地升级或者重启Containerd
,不会对运行中的容器产生任何影响。
Container Bundle
从runtime
的定义“It reads the configuration files from a bundle, uses that information to create a container, launches a process inside the container”中可以知道runtime
读取bundle
中的配置文件来创建容器。
Bundle
只涉及如何将容器及其配置数据存储在本地文件系统中,以便兼容OCI
规范的任何运行时加载。Bundle
包含加载和运行容器所需的全部信息:
config.json
:命名必须为config.json
,且必须在bundle
目录的根目录- 容器的根文件系统:在
config.json
中的root.path
字段指定
Bundle
目录中的内容才是容器必须,其目录本身非必须。
在实际调用过程中,containerd
等runtime caller
会将准备好的bundle
目录等参数传递给runc
等runtime
,由runc
来根据配置创建或控制容器的生命周期。
Config.json
容器的配置文件包含了构建标准容器的元数据,包括用户指定进程、运行环境和环境变量等。内容比较多,详细介绍可见博客。其中和本工具直接相关的为POSIX-platform Hooks
部分的内容。
RootFS
RootFS
是容器在启动时内部进程可见的文件系统,即容器的根目录。当我们运行docker exec
命令进入容器的时候看到的文件系统就是rootfs
。RootFS
通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类Unix
操作系统中的目录系统,如/dev
、/proc
、/bin
、/etc
、/lib
、/usr
、/tmp
及运行 容器所需的配置文件、工具等。
就像Linux
启动会先用只读模式挂载rootfs
,运行完完整性检查之后,再切换成读写模式一样。容器挂载rootfs
时,也会先挂载为只读模式,但是与Linux
做法不同的是,在挂载完只读的rootfs
之后,会利用联合挂载技术(Union Mount
)在已有的rootfs
上再挂一个读写层。容器在运行过程中文件系统发生的变化只会写到读写层,并通过whiteout
技术隐藏只读层中的旧版本文件。
关于
overlay
、联合挂载等技术的详细描述见手撕docker文件结构 —— overlayFS,image,container文件结构详解
How runc create, start and delete container?
以docker run ubuntu:latest
为例,containerd
传递给runc
会经历四个阶段:
create
start
delete
:因为基础的ubuntu
容器没有设置entrypoint
,因此容器启动后会马上退出delete –force
runc create
时的调用参数如下,准确来说应该是global options
。
–-root
指定了用于存储容器状态的根目录,这个根目录是tmpfs
(类Unix系统上暂存档存储空间的常见名称,通常以挂载文件系统方式实现,并将资料存储在易失性存储器即内存而非永久存储设备中),这里是/var/run/docker/runtime-runc/moby
。关于moby的介绍--log
指定了runc
日志文件。-–log-format
指定了runc
日志文件格式为json
。–-systemd-cgroup
开启systemd cgroup
支持,这也是容器资源隔离机制的关键。--bundle
指定的目录和runc
日志的目录一致。-–pid-file
指定了容器进程的pid
路径。
--root /var/run/docker/runtime-runc/moby --log/run/containerd/io.containerd.runtime.v2.task/moby/6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05/log.json --log-format json --systemd-cgroupcreate --bundle/run/containerd/io.containerd.runtime.v2.task/moby/6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05 --pid-file/run/containerd/io.containerd.runtime.v2.task/moby/6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05/init.pid6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05
日志输出的runc start
时的调用参数如下,相比于create
参数少了一些,基本参数保持一致,6dbd69e…
为容器id。
--root /var/run/docker/runtime-runc/moby --log/run/containerd/io.containerd.runtime.v2.task/moby/6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05/log.json --log-format json --systemd-cgroupstart 6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05
日志输出的runc delete
时的调用参数如下,和start
的参数一致。
--root /var/run/docker/runtime-runc/moby --log/run/containerd/io.containerd.runtime.v2.task/moby/6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05/log.json --log-format json --systemd-cgroupdelete 6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05
在runc delete
后,还有个runc delete –-force
的操作,参数如下。
--root /var/run/docker/runtime-runc/moby --log/run/containerd/io.containerd.runtime.v2.task/moby/6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05/log.json --log-format json delete --force6dbd69e6f9e0c7efe3026ae83624012fce6917e2f7e2c73958b8530d369fef05
Oci Hooks
什么是钩子呢?钩子主要有两个关键点,一个是导致钩子调用的事件(某个事件发生前或者发生后),一个是钩子的具体代码。因此,钩子实际上就是在某个时间点或事件点触发的一系列函数或代码。
容器中的钩子和容器的生命周期息息相关,钩子能使容器感知其生命周期内的事件,并且当相应的生命周期钩子被调用时运行指定的代码。OCI Runtime Spec对容器生命周期的描述如下(单纯从低级运行时创建容器开始,不包括镜像)。
Lifecycle
定义了容器从创建到退出之间的时间轴:
- 容器开始创建:通常为
OCI
规范运行时(runc
)调用create
命令 +bundle
+container id
- 容器运行时环境创建中: 根据容器的
config.json
中的配置进行创建,此时用户指定程序还未运行,这一步后所有对config.json
的更改均不会影响容器 prestart hooks
createRuntime hooks
createContainer hooks
- 容器启动:通常为
OCI
规范运行时(runc
)调用create
命令 +bundle
+container id
startContainer hooks
- 容器执行用户指定程序
poststart hooks
:任何poststart
钩子执行失败只会log a warning
,不影响其他生命周期(操作继续执行)就好像钩子成功执行一样- 容器进程退出:
error
、正常退出和运行时调用kill
命令均会导致 - 容器删除:通常为
OCI
规范运行时(runc
)调用delete
命令 +container id
- 容器摧毁:区别于容器删除,
3
、4
、5
、7
的钩子执行失败除了生成一个error
外,会直接跳到这一步。撤销第二步创建阶段执行的操作。 poststop hooks
:执行失败后的操作和poststart
一致
可以看到oci
定义的容器生命周期中,如果在容器的config.json
中定义了钩子,runc
必须执行钩子,并且时间节点在前的钩子执行成功后才能执行下一个钩子;若有一个钩子执行失败,则会报错并摧毁容器(在容器创建后执行的钩子失败,并不会删除容器,而是启动失败)。
OCI
规定单个钩子的字段如下:
path (string, REQUIRED)
:绝对路径args (array of strings, OPTIONAL)
env (array of strings, OPTIONAL)
timeout (int, OPTIONAL)
:终止钩子的秒数
Prestart
Prestart
钩子必须作为创建操作的一部分,在运行时环境创建完成后(根据 config.json
中的配置),但在执行 pivot_root
或任何同等操作之前调用。
即将废弃,被后面三个钩子所取代,注意后面三个钩子在较老版本的runc中可能不支持。
CreateRuntime Hooks
createRuntime
钩子必须作为创建操作的一部分,在运行时环境创建完成后(根据config.json
中的配置),但在执行 pivot_root
或任何同等操作之前调用。
在容器命名空间被创建后调用。
createRuntime
钩子的定义目前未作明确规定,钩子作者只能期望运行时创建挂载命名空间并执行挂载操作。运行时可能尚未执行其他操作,如cgroups
和SELinux/AppArmor
标签
CreateContainer Hooks
createContainer
钩子必须作为创建操作的一部分,在运行时环境创建完成后(根据 config.json
中的配置),但在执行 pivot_root
或任何同等操作之前调用。
在执行 pivot_root 操作之前,但在创建和设置挂载命名空间之后调用。
StartContainer Hooks
StartContainer
钩子作为启动操作的一部分,必须在执行用户指定的进程之前调用startContainer
挂钩。此钩子可用于在容器中执行某些操作,例如在容器进程生成之前在linux
上运行ldconfig
二进制文件。
Poststart
Poststart
钩子必须在用户指定的进程执行后、启动操作返回前调用。例如,此钩子可以通知用户容器进程已生成。
Poststop
Poststart
钩子必须在容器删除后、删除操作返回前调用。清理或调试函数就是此类钩子的例子。
summary
namespace
是指path
以及钩子必须在指定的namespace
中解析或调用,比如runtime
命名空间表示访问的钩子path
是宿主机上的路径;而container
为容器中的路径。
Name | Namespace | When |
---|---|---|
prestart (Deprecated) | runtime | After the start operation is called but before the user-specified program command is executed. |
createRuntime | runtime | During the create operation, after the runtime environment has been created and before the pivot root or any equivalent operation. |
createContainer | container | During the create operation, after the runtime environment has been created and before the pivot root or any equivalent operation. |
startContainer | container | After the start operation is called but before the user-specified program command is executed. |
poststart | runtime | After the user-specified process is executed but before the start operation returns. |
poststop | runtime | After the container is deleted but before the delete operation returns. |
How Container Hooks Toolkit Works
有了前面的铺垫,最后对container hooks toolkit
是如何工作的进行介绍:
- 当我们执行
docker run
命令创建并启动一个新的容器时,这个命令会被发送到Docker
守护进程 (dockerd
)。 - 接收到来自
Docker
客户端(docker cli
)的命令后,dockerd
解析这些命令。dockerd
根据参数检查本地是否存在指定的镜像,如果不存在,它会从配置的Docker
镜像仓库(默认是Docker Hub
)下载镜像。dockerd
创建一个容器配置,并将其传递给容器引擎 (containerd
)。 containerd
接收来自dockerd
的请求,并负责进一步处理这些请求,如创建、启动、停止容器等。containerd
会生成一个容器规范(如OCI
规范,也就是前面提到的config.json
),并将其传递给较低级别的容器运行时,这里是我们配置的container-hooks-runtime
。container-hooks-runtime
对传递来的参数进行解析,如果是create/start
,就解析出/etc/container-hooks/hooks.json
中钩子并插入到容器的bundle
路径下的config.json
中,并传递给runc
。- 如果是非
create/start
命令,container-hooks-runtime
就直接将参数传递给runc
。 runc
根据传递而来的OCI
规范设置必要的命名空间、控制组、根文件系统等,如果设置了容器钩子,则在特定的生命周期节点运行,然后启动容器的入口点进程(PID 1
)。
但在执行docker run
命令时,对runc
先传递create
,再传递start
,这样不就添加了两次自定义钩子吗?是的,但我们提供了container-hooks
这个空程序,用于避免重复添加自定义钩子,但container-hooks-runtime
解析到容器的配置文件config.json
中的prestart
钩子中存在container-hooks
,则不会再次添加自定义钩子。
Customized
提供一些更加定制化的思路:
案例1:在容器启动前自动对容器进行签名验证和完整性校验
此时直接编写hooks
至/etc/container-hooks/hooks.json
就行不通了,因为我们无法提前预知每个容器的启动镜像信息。可以对项目进行二次开发,不采用读取文件中的钩子的形式,而是直接在代码中进行插入并根据容器的配置进行参数调整。
需要修改的代码部分:
/internel/runtime
/internel/modifier