Definition文件

一个singularity definition(def)文件是一个蓝本描述怎么build一个容器,和Dockerfile类似。 这个文件指定了基础容器,以及要在基础容器上安装的软件,要设置的环境变量,要从host上加到容器中的文件,和容器的metadata。

概述

一个singlarity definition文件可以分为两部分:

  1. Header: Header部分描述了容器依赖的基础容器。

  2. Sections: 文件剩余部分包含多个sections(有时也叫做scriptlets或者blobs)。 每个section以%开始,后面跟上这个section的名字,section是可选的。 当build容器的时候, 使用 /bin/sh 执行每个section。当容器运行的时候,runscript默认的也是 /bin/sh,可以接受 /bin/sh 的参数。

更过def文件的例子,请看这里。 关于Dockerfile和Singularity definition文件的比较, 请参考这里

Sections

definition文件包含多个sections,不同的section添加不同的内容到容器或者在build容器的时候执行命令。 在build容器过程中,任何一条命令执行失败,build过程将停止。

下面是一个例子,其用到了所有的section类型,后面我们将一个个讨论这些不同的section。 当然一个def文件并不需要每种类型的section都包括。 另外,如果def文件中有多个同名的section,在build过程中后面section的内容会追加到前面的section中。

Bootstrap: library
From: ubuntu:18.04
Stage: build

%setup
    touch /file1
    touch ${SINGULARITY_ROOTFS}/file2

%files
    /file1
    /file1 /opt

%environment
    export LISTEN_PORT=12345
    export LC_ALL=C

%post
    apt-get update && apt-get install -y netcat
    NOW=`date`
    echo "export NOW=\"${NOW}\"" >> $SINGULARITY_ENVIRONMENT

%runscript
    echo "Container was created $NOW"
    echo "Arguments received: $*"
    exec echo "$@"

%startscript
    nc -lp $LISTEN_PORT

%test
    grep -q NAME=\"Ubuntu\" /etc/os-release
    if [ $? -eq 0 ]; then
        echo "Container base is Ubuntu as expected."
    else
        echo "Container base is not Ubuntu."
    fi

%labels
    Author d@sylabs.io
    Version v0.0.1

%help
    This is a demo container used to illustrate a def file that uses all
    supported sections.

Section在def文件中的位置不重要。

%setup

build容器过程中,在 %setup section的命令会首先执行,并且这些命令是在host上执行,而不是容器内部执行。 你可以通过环境变量``$SINGULARITY_ROOTFS``获取容器的文件系统。

Note

在build容器的时候,由于 %setup section下的命令是在容器外执行,因此要小心使用,不要对host造成破坏。

上面的例子中:

%setup
    touch /file1
    touch ${SINGULARITY_ROOTFS}/file2

host 根目录下创建 file1,这个文件我们在 %files section会用到。在 container 内的根目录下创建 file2

后面的 %files section提供了安全的方法拷贝host上的文件到container中, 由于使用 %setup 有可能会对host造成破坏,因此 %setup 不建议使用。

%files

%files section 允许你拷贝host上的文件到容器中:

%files [from <stage>]
    <source> [<destination>]
    ...

每行是一个 <source><destination> 对。 <source> 可以是:

  1. host上的一个路径

  2. 前面阶段已经build出的文件的路径

<destination> 是container内的路径。如果没有提供 <destination>,那么 <destination><source> 内容一样。

%files
    /file1
    /file1 /opt

第一行将拷贝host的根目录下的 file1 到container中的根目录下。 第一行将拷贝host的根目录下的 file1 到container中的/opt目录下。

definition文件中其它阶段生成的文件也可以拷贝到当前阶段中使用。

%files from stage_name
  /root/hello /bin/hello

不同的时,host文件拷贝到container中,没有生成软链接,而其它阶段的文件拷贝到当前阶段,实际上是生成一个软链接。

%files 会在 %post 之前执行。

%app*

某些情况下,如果对每个app都build一个容器,由于这些app的依赖基本都相同,因此这些容器之间是冗余的。 因此基于 Standard Container Integration Format (SCI-F), singularity支持在一个容器中安装多个apps,更多关于app的信息,请查看 这里

%post

在这一section,你可以从网络下载一些工具,像 gitwget, 你可以使用这些工具下载软件并安装这些软件,当然你也可以创建和修改文件等。

%post
    apt-get update && apt-get install -y netcat
    NOW=`date`
    echo "export NOW=\"${NOW}\"" >> $SINGULARITY_ENVIRONMENT

%post 使用Ubuntu包管理器 apt 更新系统和安装软件 netcat ( %startscript 将会用到这个软件)。

你不能在 %environment 中设置$SINGULARITY_ENVIRONMENT的值,将文本写入$SINGULARITY_ENVIRONMENT, 最终实际上会写入容器中的文件 /.singularity.d/env/91-environment.sh,然后在运行的时候自动source这个文件。

%test

在build容器时候,最后会运行 %test 下面的脚本来测试进行容器,容器build好以后,你可以使用 test 命令运行 %test 下面的脚本。

%test
    grep -q NAME=\"Ubuntu\" /etc/os-release
    if [ $? -eq 0 ]; then
        echo "Container base is Ubuntu as expected."
    else
        echo "Container base is not Ubuntu."
    fi

这段%test用来check基础OS是否为Ubuntu。 如果在build的时候不希望运行%test,可以使用 --notest 的选项。 容器build好以后,可以使用test命令运行%test下的脚本。

$ sudo singularity build --notest my_container.sif my_container.def

$ singularity test my_container.sif
Container base is Ubuntu as expected.

%environment

%environment 可以设置容器运行时的环境变量。 需要注意的是,这里定义的环境变量在build阶段不能使用。 在build阶段如果要使用环境变量,需要在 %post 里面定义和使用:

  • build阶段: %environment 下面的环境变量会被写到容器的metadata文件夹下的一个文件中,build的时候这个文件不会被source使用。

  • 运行阶段: metadata文件夹下的文件被source,可以使用定义的环境变量。

%environment
    export LISTEN_PORT=12345
    export LC_ALL=C

%startscript 将会用到 $LISTEN_PORT 环境变量, $LC_ALL 会被很多程序(通常是perl程序)用到。容器build好以后,你可以验证环境变量是否生效。

$ singularity exec my_container.sif env | grep -E 'LISTEN_PORT|LC_ALL'
LISTEN_PORT=12345
LC_ALL=C

你也可以在 %post 里面设置环境变量。

build容器的时候, %environment 下的内容写到容器中的文件 /.singularity.d/env/90-environment.sh 中。 %post 中如果通过命令将内容写入 $SINGULARITY_ENVIRONMENT, 那么这些内容实际上是写入了文件 /.singularity.d/env/91-environment.sh 中。

运行容器的时候, 在 /.singularity.d/env 下面的文件会按照顺序被source。 91-environment.sh会在90-environment.sh后被source, 因此这个例子中 %post 中写入的变量会优先于 %environment 中设置的变量。

关于singularity环境变量和metadata,请参考 Environment and Metadata

%startscript

%startscript 下的内容在build的时候会写入容器中的一个文件,在运行 instance start 命令的时候会执行这个文件。

%startscript
    nc -lp $LISTEN_PORT

这里netcat程序侦听TCP端口,这个端口是在 %environment 中设置的。

$ singularity instance start my_container.sif instance1
INFO:    instance started successfully

$ lsof | grep LISTEN
nc        19061               vagrant    3u     IPv4             107409      0t0        TCP *:12345 (LISTEN)

$ singularity instance stop instance1
Stopping instance1 instance of /home/vagrant/my_container.sif (PID=19035)

%runscript

%runscript 下的内容在build的时候会写入容器中的一个文件, 在运行 singularity run 命令或者直接运行容器的时候会执行这个文件。 当容器运行的时候,容器后面跟的参数会传给这个文件,这意味着在这个文件中你可以处理传递过来的参数。

%runscript
    echo "Container was created $NOW"
    echo "Arguments received: $*"
    exec echo "$@"

上述运行脚本中,首先会打出容器创建的时间 $NOW (这个变量是在 %post 中设置的)。 接着将传递给这个容器的所有参数作为一个字符串打印出来,然后讲所有参数以字符串数组方式传递给echo。 exec 用来生成一个新的echo进程来代替进入容器时的进程(shell进程),shell进程会结束,echo进程在容器中运行。

$ ./my_container.sif
Container was created Thu Dec  6 20:01:56 UTC 2018
Arguments received:

$ ./my_container.sif this that and the other
Container was created Thu Dec  6 20:01:56 UTC 2018
Arguments received: this that and the other
this that and the other

%labels

The %labels 用来添加metadata到容器文件 /.singularity.d/labels.json 中。 其格式是”名字 值”。

%labels
    Author d@sylabs.io
    Version v0.0.1
    MyLabel Hello World

添加label,只需要在lables下面每行的第一个空格后面加上”名字 值”就可以。

上面例子中, 第一个label是 Author,其值为 d@sylabs.io。 第二个label是 Version,其值为 v0.0.1。 最后一个label是 MyLabel,其值为 Hello World

你可以用inspect命令来查看容器的所有label。

$ singularity inspect my_container.sif

{
  "Author": "d@sylabs.io",
  "Version": "v0.0.1",
  "MyLabel": "Hello World",
  "org.label-schema.build-date": "Thursday_6_December_2018_20:1:56_UTC",
  "org.label-schema.schema-version": "1.0",
  "org.label-schema.usage": "/.singularity.d/runscript.help",
  "org.label-schema.usage.singularity.deffile.bootstrap": "library",
  "org.label-schema.usage.singularity.deffile.from": "ubuntu:18.04",
  "org.label-schema.usage.singularity.runscript.help": "/.singularity.d/runscript.help",
  "org.label-schema.usage.singularity.version": "3.0.1"
}

上面有些label是build的过程中自动生成的。关于label和metadata的更多信息,请参考 这里

%help

%help 下的内容在build的时候会写入容器中的一个metadata文件。 run-help 命令能查看 %help 的内容。

%help
    This is a demo container used to illustrate a def file that uses all
    supported sections.

容器build好以后,使用下面命令查看%help的内容:

$ singularity run-help my_container.sif
    This is a demo container used to illustrate a def file that uses all
    supported sections.

多阶段Build

Singularity从3.2开始支持多阶段build, 比如一个阶段先在一个环境中编译出一个二进制文件,在最终阶段中可以拷贝这个二进制文件到最终容器,这样就减小了容器的大小。

Bootstrap: docker
From: golang:1.12.3-alpine3.9
Stage: devel

%post
  # prep environment
  export PATH="/go/bin:/usr/local/go/bin:$PATH"
  export HOME="/root"
  cd /root

  # insert source code, could also be copied from host with %files
  cat << EOF > hello.go
  package main
  import "fmt"

  func main() {
    fmt.Printf("Hello World!\n")
  }
EOF

  go build -o hello hello.go


# Install binary into final image
Bootstrap: library
From: alpine:3.9
Stage: final

# install binary from stage one
%files from devel
  /root/hello /bin/hello

每个阶段的名字可以随意命名。每个阶段内的执行和单阶段执行相同。声明靠后的阶段可以拷贝前面的阶段的文件到当前阶段, 但是声明靠前的阶段不能拷贝声明靠后文件。因此 final 阶段能拷贝 devel 阶段的文件,而 devel 阶段不能拷贝 final 阶段的文件。

Apps

%app* 是相对独立的section。一个def文件可以有多个app,app之间没有顺序的关系。

The following runscript demonstrates how to build 2 different apps into the same container using SCI-F modules:

Bootstrap: docker
From: ubuntu

%environment
    GLOBAL=variables
    AVAILABLE="to all apps"

##############################
# foo
##############################

%apprun foo
    exec echo "RUNNING FOO"

%applabels foo
   BESTAPP FOO

%appinstall foo
   touch foo.exec

%appenv foo
    SOFTWARE=foo
    export SOFTWARE

%apphelp foo
    This is the help for foo.

%appfiles foo
   foo.txt

##############################
# bar
##############################

%apphelp bar
    This is the help for bar.

%applabels bar
   BESTAPP BAR

%appinstall bar
    touch bar.exec

%appenv bar
    SOFTWARE=bar
    export SOFTWARE

对一个app来说,%appinstall%post 等价, %appenv%environment 等价。

使用app:

% singularity run --app foo my_container.sif
RUNNING FOO

两个app中都定义了 $SOFTWARE。你可以执行下面的命令查看当前app能用的环境变量。

$ singularity exec --app foo my_container.sif env | grep SOFTWARE
SOFTWARE=foo

$ singularity exec --app bar my_container.sif env | grep SOFTWARE
SOFTWARE=bar

build容器的一些建议

  1. 在容器中安装程序和创建文件的时候,这些程序和文件放在系统目录下,不要放在 /home, /tmp 等有可能被mount覆盖的目录。

  2. 添加容器的描述和说明,如果你的runscript不能提供帮助说明,在 %help 或者 %apphelp 写上帮助说明, 一个好的容器应该告诉用户怎么使用它。

  3. 如果你需要添加环境变量,添加到 %environment%appenv 中。

  4. 在容器中创建的文件,其所有者应该是系统中用户id小于500的用户。

  5. 确保一些敏感文件像 /etc/passwd, /etc/group/etc/shadow 不包含密码。

  6. 用def文件Build容器,不要使用sandbox这种黑盒子的方式。