Buildah入门

Buildah

buildah是Redhat旗下的一款专门构建工具,与Docker相比,优势在于:

  • 环境并不需要daemon(守护进程)
  • 不需要Dockerfile但也可以兼容Dockerfile
  • 可以将容器目录挂载到本地,通过直接操作本地目录来操作容器内容
  • 可以创建更小的镜像

安装

构建机器需要安装buildah,参见https://github.com/containers/buildah/blob/main/install.md

使用

Dockerfile方式

buildah兼容Dockerfile,原先使用Dockerfile的项目并不需要什么迁移成本。对于新项目,推荐以下构建方式。

添加layers配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.focustech.naja.web.mvc.NajaMvcServerMain</mainClass>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/layers.xml</configuration>
</layers>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
添加layers.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-2.6.xsd">
<application>
<!-- <application>标签中的内容定义了所有应用代码内容应该被如何分配到各个layer中,遵循Ant-style -->
<!-- <into>标签定义标签内容需要被添加到指定layer中 -->
<into layer="spring-boot-loader">
<include>org/springframework/boot/loader/**</include>
</into>
<into layer="application" />
</application>
<!-- <dependencies>标签中的内容定义了所有dependencies应该被如何分配到各个layer中,遵循group:artifact[:version]模式 -->
<dependencies>
<into layer="application">
<includeModuleDependencies />
</into>
<into layer="snapshot-dependencies">
<!-- 对应maven坐标 group:artifact[:version],如果需要限定SNAPSHOT则加载最后 -->
<include>*:*:*SNAPSHOT</include>
</into>
<into layer="dependencies" />
</dependencies>
<!-- layerOrder中需要包含<application>与<dependencies>中出现过的所有layer属性值 -->
<!-- layerOrder中layer出现的顺序很重要,出现变化概率越低的在最上面,而越高的则在最下面 -->
<layerOrder>
<layer>dependencies</layer>
<layer>spring-boot-loader</layer>
<layer>snapshot-dependencies</layer>
<layer>application</layer>
</layerOrder>
</layers>
添加multi-stage Dockerfile

细节配置可以根据项目需要进行调整。主要关注第二个FROM中的环境变量配置,暴露的端口,以及最后的ENTRYPOINT(CMD),根据需要修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM openjdk:8-jdk-alpine as builder

WORKDIR app
ARG JAR_FILE=./target/*.jar
COPY ${JAR_FILE} /app/app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar

FROM openjdk:8-jdk-alpine

LABEL maintainer="xxx"

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ENV JAVA_OPTS="-Xms128m -Xmx256m"

WORKDIR app

COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/application/ ./

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]

bash脚本方式

如果采用bash方式,则不适用分层构建(multi-stage),原因是Dockerfile每句指令将生成新Layer,可以作为缓存,而bash则是流水线式,不支持缓存。

构建语句与Dockerfile类似,具体语法可参见https://github.com/containers/buildah/tree/main/docs,下面是基本示例。

新建buildah.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 获取基础容器
container=$(buildah from openjdk:8-jdk-alpine)
buildah config --label maintainer="xxx" $containter
# 设置容器内容
buildah run $container -- /bin/sh -c 'mkdir /tmp/logs'
buildah config --workingdir app $container
buildah copy $container './target/*.jar' '/app/app.jar'
# 设置暴露端口
buildah config --port 8080 $container
# 初始指令
buildah config --entrypoint ["java","-jar","app.jar"]
# 提交本地容器
buildah commit $container buildah-springboot-demo:v1
buildah rm $container
对multi-stage的支持

如果原先项目是分层打包的(maven配置了layers为true),则可以采用以下bash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 抽取多层layers
builder_ctx=$(buildah from openjdk:8-jdk-alpine)
builder_mnt=$(buildah mount $builder_ctx)
buildah config --label maintainer="xxx" $builder_ctx
buildah config --workingdir app $builder_ctx
JAR_FILE='./target/*.jar'
buildah copy $builder_ctx $JAR_FILE '/app/app.jar'
buildah run $builder_ctx -- /bin/sh -c 'java -Djarmode=layertools -jar app.jar extract && rm app.jar'

# 创建容器,设置环境信息,并从builder容器拷贝layers文件信息
application_ctx=$(buildah from openjdk:8-jdk-alpine)
application_mnt=$(buildah mount $application_ctx)
buildah config --workingdir app $application_ctx
buildah config --env TZ=Asia/Shanghai $application_ctx
buildah run $application_ctx -- /bin/sh -c 'ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone'
buildah config --env JAVA_OPTS='-Xms128m -Xmx256m' $application_ctx

cp -rf $builder_mnt/app/dependencies/ $application_mnt/app
cp -rf $builder_mnt/app/snapshot-dependencies/ $application_mnt/app
cp -rf $builder_mnt/app/spring-boot-loader/ $application_mnt/app
cp -rf $builder_mnt/app/application/ $application_mnt/app
# 设置暴露的端口
buildah config --port 8080 $application_ctx

buildah config --entrypoint ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]
# 删除临时容器以及挂载目录
buildah unmount $builder_ctx $application_ctx
buildah commit $application_ctx buildah/layer-springboot-demo:v2
buildah rm $builder_ctx $application_ctx

镜像推送

写了个打包推送的脚本package-oci.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
echo "--------- start ---------"

# env
#JAVA_HOME=$JAVA_HOME
#maven_home=$MAVEN_HOME
#export MAVEN_HOME=${maven_home}
#export PATH=$MAVEN_HOME/bin:$PATH
IS_DEBUG=false
REMOTE_REGISTRY=${REMOTE_IMAGE_REGISTRY:-docker://127.0.0.1:5000}

[ $IS_DEBUG = "true" ] && echo original parameters=[$@]

# project path for being packaged
PROJECT_HOME="."
# to store log files and other temp cache
TMP_DIR=""
# bash script log path
LOG_FILE=""
# which strategy will be used to build the image
PACKAGE_STRATEGY="Dockerfile" # or "buildah"
# image info. like nginx:v1
IMAGE_INFO=""
# build file path, often be a Dockerfile or a bash script
BUILD_FILE=""
# do you need to push the image to the remote repository
PUSH_FLAG=false
# do you need to run `mvn clean package` before build image
PACKAGE_FLAG=true
# do you need to build image
BUILD_FLAG=true
# username:password
CREDS=""

#
logPrint() {
message="$1"
echo "$(date +'%F %T') ${message}" >>$LOG_FILE
}

show_help() {
echo "执行打包&发布镜像"
echo "./package-opt.sh [options]..."
echo "-p, --project-home 项目根目录,如/home/projects/springboot-demo"
echo "-i, --image-info 镜像信息,包含镜像名以及版本,格式同docker,[imageName:version],如nginx:v1"
echo "-s, --package-strategy 打包策略接受参数[ Dockerfile(default) | buildah ],是使用Dockerfile的方式,还是使用buildah的方式。如果是Dockerfile方式,项目根目录下需要有Dockerfile文件,如果是buildah方式,则项目根目录下需要有buildah.sh文件"
echo "-b, --build-file 打包执行文件路径,Dockerfile或者buildah.sh,如果值为Dockerfile,将默认使用Dockerfile打包策略,否则使用buildah打包策略"
echo " --push-flag 是否发布到远程仓库,true or false(default)"
echo " --package-flag 是否执行maven打包,true(default) or false"
echo " --build-flag 是否执行本地镜像构建,true(default) or false"
echo "-r --remote-registry 远程仓库地址,例如:docker://docker.vemic.com/fas-oss。默认为环境变量REMOTE_IMAGE_REGISTRY的值。如果未设置该环境变量,则默认docker://127.0.0.1:5000。"
echo " --creds [username[:password]] 远程仓库用户名密码。--creds=username:password"
echo "-h, --help 帮助选项。"
}

# read args
ARGS_CMD=$(getopt -o hp:i:s:b:r: --long help,project-home:,image-info:,package-strategy:,build-file:,push-flag:,package-flag:,build-flag:,creds:,remote-registry -n $(basename $0) -- "$@")

# echo ARGS_CMD=[$ARGS]
[ $? -ne 0 ] && exit 1
# echo formatted parameters=[$@]

# dispatch formatted arguments to position arguments($1,$2,...)
eval set -- "$ARGS_CMD"
while [ -n "$1" ]; do
case "$1" in
-h | --help)
show_help
shift 1
exit 1
;;
-p | --project-home)
PROJECT_HOME=$2
if [ ! -d $PROJECT_HOME ]; then
echo "project home not exist! target: $PROJECT_HOME"
shift 2
exit 1
fi
shift 2
;;
-i | --image-info)
IMAGE_INFO=$2
[ -z "$IMAGE_INFO" ] && echo "please set image info (like: -i nginx:v1)" && exit
shift 2
;;
-b | --build-file)
BUILD_FILE=$2
if [ ! -f $BUILD_FILE ]; then
echo "build file not exist! file path: $BUILD_FILE"
exit 1
elif [[ "$(basename $BUILD_FILE)" = "Dockerfile" ]]; then
PACKAGE_STRATEGY="Dockerfile"
else
PACKAGE_STRATEGY="buildah"
fi
shift 2
;;
-s | --package-strategy)
strategy=$2
if [[ $strategy != "Dockerfile" && $strategy != "buildah" ]]; then
echo "package strategy requires arg 'Dockerfile' or 'buildah'" && exit 1
fi
PACKAGE_STRATEGY=$2
shift 2
;;
--push-flag)
if [[ $2 != "true" && $2 != "false" ]]; then
echo "--push-flag must be true or false!" && exit 1
fi
PUSH_FLAG=$2
shift 2
;;
--package-flag)
if [[ $2 != "true" && $2 != "false" ]]; then
echo "--package-flag must be true or false!" && exit 1
fi
PACKAGE_FLAG=$2
shift 2
;;
--build-flag)
if [[ $2 != "true" && $2 != "false" ]]; then
echo "--build-flag must be true or false!" && exit 1
fi
BUILD_FLAG=$2
shift 2
;;
-r | --remote-registry)
if [[ ! "$2" =~ ^docker://.*|^docker-daemon:*|^oci:* ]]; then
echo "-r|--remote-registry is empty! please set correct address. see --help for more info." && exit 1
fi
REMOTE_REGISTRY=$2
shift 2
;;
--creds)
if [[ $2 = "" ]]; then
echo "--creds is empty. please set --creds=username:password" && exit 1
fi
CREDS=$2
shift 2
;;
\:)
echo "argument value missing! $1"
show_help && exit 1
;;
\?)
echo "unknown argument! $1"
show_help
exit 1
;;
--)
shift
break
;;
*)
echo "error arg! $1"
show_help
exit 1
;;
esac
done

# make preparations
make_preparations() {
echo "make_pre"
TMP_DIR="/tmp/oci-package/$(date +%Y%m%d)"
mkdir -p $TMP_DIR
LOG_FILE="${TMP_DIR}/$(date +%H%M%S).log"
touch $LOG_FILE
}

# mvn operations
do_compile() {
echo "maven compile work start"
if [ -f "$PROJECT_HOME pom.xml" ]; then
echo "pom.xml does not exist in the specified directory $PROJECT_HOME" && exit 1
fi
echo "target project home is : $PROJECT_HOME"
cd $PROJECT_HOME
mvn clean package -Dmaven.test.skip=true >>$LOG_FILE 2>&1
if [ $? -ne 0 ]; then
echo "mvn clean package failed! Please see $LOG_FILE" && exit 1
else
echo "mvn packaged successfully!"
fi
}

push_image() {
echo "start push image..."
cmd="buildah push --tls-verify=false"
[ -n "$CREDS" ] && cmd="${cmd} --creds=$CREDS"
if [[ "$REMOTE_REGISTRY" =~ ^docker://.* ]]; then
cmd="${cmd} $IMAGE_INFO $REMOTE_REGISTRY/$IMAGE_INFO"
elif [[ "$REMOTE_REGISTRY" =~ ^docker-daemon:* ]]; then
cmd="${cmd} $IMAGE_INFO docker-daemon:$IMAGE_INFO"
elif [[ "$REMOTE_REGISTRY" =~ ^oci:* ]]; then
cmd="${cmd} $IMAGE_INFO $REMOTE_REGISTRY:$IMAGE_INFO"
else
echo "REMOTE_IMAGE_REGISTRY invalid, please start with 'docker://' or 'docker-daemon:' or 'oci:'. see --help for more info."
fi
eval $cmd
if [ $? -eq 0 ]; then
echo "push image successfully!"
else
echo "push image to $REMOTE_REGISTRY/$IMAGE_INFO failed!" && exit 1
fi
}

package_oci_image_for_docker() {
cd $PROJECT_HOME
if [ -f "$PROJECT_HOME Dockerfile" ]; then
echo "Dockerfile does not exist in the specified directory $PROJECT_HOME"
exit 1
else
cmd='buildah bud --layers '${BUILD_FILE:+"-f $BUILD_FILE"}' -t $IMAGE_INFO .'
eval $cmd
[ $? -ne 0 ] && echo "build with Dockerfile failed!" && exit 1
fi
}

package_oci_image() {
cd $PROJECT_HOME
if [ -f "$PROJECT_HOME buildah.sh" ]; then
echo "buildah.sh does not exist in the specified directory $PROJECT_HOME"
exit 1
else
source ${BUILD_FILE:-buildah.sh} >$LOG_FILE 2>$LOG_FILE
[ $? -ne 0 ] && echo "build with buildah failed!" && exit 1
fi
}

# execute package
do_oci_package() {
echo "begin oci package,PACKAGE_STRATEGY=$PACKAGE_STRATEGY"
if [ $PACKAGE_STRATEGY = "Dockerfile" ]; then
package_oci_image_for_docker
elif [ $PACKAGE_STRATEGY = "buildah" ]; then
package_oci_image
else
echo "unknown package strategy: $PACKAGE_STRATEGY" && exit 1
fi
}

[[ -z "$IMAGE_INFO" ]] && echo "please set image info (like: -i nginx:v1)" && exit
make_preparations
[ $PACKAGE_FLAG = "true" ] && do_compile
[ $BUILD_FLAG = "true" ] && do_oci_package
[ $PUSH_FLAG = "true" ] && push_image

echo "--------- done! ---------"

构建机器执行package-oci.sh

配置环境变量REMOTE_IMAGE_REGISTRY为远程仓库地址,默认使用http://127.0.0.1:5000

执行./package-oci.sh -p /path/to/project -i demo:v1。即可推送oci镜像到指定远程仓库。

详细配置如下:

1
2
3
4
5
6
-p, --project-home                      项目根目录,如/home/projects/springboot-demo,默认为当前目录
-i, --image-info 镜像信息,包含镜像名以及版本,格式同docker,[imageName:version],如nginx:v1
-s, --package-strategy 打包策略接受参数[ Dockerfile(default) | buildah ],是使用Dockerfile的方式,还是使用buildah的方式。如果是Dockerfile方式,项目根目录下需要有Dockerfile文件,如果是buildah方式,则项目根目录下需要有buildah.sh文件
-b, --build-file 打包执行文件路径,Dockerfile或者buildah.sh,如果值为Dockerfile,将默认使用Dockerfile打包策略,否则使用buildah打包策略
--push-flag 是否发布到远程仓库,true or false(default)
-h, --help 帮助选项。