1.准备我们所需要的环境(docker 和pack 第二条命令可直接安装成功)
[root@yaqu002 ~]# docker -v
Docker version 20.10.14, build a224086
[root@yaqu002 ~]# (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.24.1/pack-v0.24.1-linux.tgz" | sudo tar -C /usr/local/bin/ --no-same-owner -xzv pack)
设置本地环境
首先,我们将创建一个示例 Ruby 应用程序,您可以在开发 buildpack 时使用它:
mkdir ruby-sample-app
在当前目录中创建一个名为的文件,其ruby-sample-app/app.rb内容如下:
require 'sinatra'
set :bind, '0.0.0.0'
set :port, 8080
get '/' do
'Hello World!'
end
然后,创建一个名为的文件ruby-sample-app/Gemfile,其内容如下:
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "sinatra"
最后,通过执行以下命令确保您的本地 Docker 守护程序正在运行:
docker version
如果您看到类似于以下的输出,那么您就可以开始了!否则,启动 Docker 并再次检查。
Client: Docker Engine - Community
Version: 20.10.9
API version: 1.41
Go version: go1.16.8
Git commit: c2ea9bc
Built: Mon Oct 4 16:08:29 2021
OS/Arch: linux/amd64
Context: default
Experimental: true
Server: Docker Engine - Community
Engine:
Version: 20.10.9
API version: 1.41 (minimum version 1.12)
Go version: go1.16.8
Git commit: 79ea9d3
Built: Mon Oct 4 16:06:34 2021
OS/Arch: linux/amd64
Experimental: false
云原生 Buildpack 的构建块
现在我们将设置 buildpack 脚手架。
让我们创建 buildpack 所在的目录:
使用 Pack CLI
该buildpack new 命令将创建一个以 buildpack ID 命名的目录。
例子:
pack buildpack new examples/ruby \
--api 0.7 \
--path ruby-buildpack \
--version 0.0.1 \
--stacks io.buildpacks.samples.stacks.bionic
此命令将创建ruby-buildpack包含buildpack.toml, bin/build, bin/detect文件的目录。
附加参数
-a, --api生成的 buildpack 的 Buildpack API 兼容性
-h, --help“新”的帮助
--path文件系统上生成工件的位置。
--stacks此 buildpack 将兼容的堆栈。按顺序对每个堆栈重复,或按逗号分隔列表提供一次
-V, --versionbuildpack.toml 中的 buildpack 版本
buildpack.toml
您将ruby-buildpack/buildpack.toml在您的 buildpack 目录中描述我们的 buildpack。
# Buildpack API version
api = "0.7"
# Buildpack ID and metadata
[buildpack]
id = "examples/ruby"
version = "0.0.1"
# Stacks that the buildpack will work with
[[stacks]]
id = "io.buildpacks.samples.stacks.bionic"
您会注意到文件中的两个特定字段:buildpack.id和stack.id. buildpack ID 是您在创建 buildpack 组、构建器等时引用 buildpack 的方式。堆栈 ID 是运行 buildpack 的根文件系统。此示例可以在两个不同堆栈之一上运行,均基于 Ubuntu Bionic。
detect和build
接下来,我们将介绍detect和build脚本。这些文件是bin在你的 buildpack 目录中创建的。
现在更新您的ruby-buildpack/bin/detect文件并复制以下内容:
#!/usr/bin/env bash
set -eo pipefail
exit 1
还要更新您的ruby-buildpack/bin/build文件并复制以下内容:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
exit 1
这两个文件是可执行文件detect和build脚本。您现在可以使用此 buildpack。
使用你的 buildpackpack
为了测试您的 buildpack,您需要使用packCLI 针对您的示例 Ruby 应用程序运行 buildpack。
通过运行以下命令设置您的默认构建器:
pack config default-builder cnbs/sample-builder:bionic
告诉 pack 信任我们的默认构建器:
pack config trusted-builders add cnbs/sample-builder:bionic
然后运行以下pack命令:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
该pack build命令将您的 Ruby 示例应用程序作为--path参数,并将您的 buildpack 作为--buildpack参数。
运行该命令后,您应该会看到它未能检测到,因为detect当前编写的脚本只是为了出错。
===> DETECTING
err: examples/[email protected] (1)
ERROR: No buildpack groups passed detection.
ERROR: failed to detect: buildpack(s) failed with err
检测您的应用程序
接下来,您将要实际检测您正在构建的应用程序是 Ruby 应用程序。为此,您需要检查Gemfile.
exit 1用detect以下检查替换脚本:
if [[ ! -f Gemfile ]]; then
exit 100
fi
您的ruby-buildpack/bin/detect脚本应如下所示:
#!/usr/bin/env bash
set -eo pipefail
if [[ ! -f Gemfile ]]; then
exit 100
fi
接下来,使用更新的 buildpack 重建您的应用程序:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
您应该看到以下输出:
===> ANALYZING
Previous image with name "test-ruby-app" not found
===> DETECTING
examples/ruby 0.0.1
===> RESTORING
===> BUILDING
---> Ruby Buildpack
ERROR: failed to build: exit status 1
ERROR: failed to build: executing lifecycle
请注意,detect现在通过了,因为Gemfile在 Ruby 应用程序中有一个有效的ruby-sample-app,但现在build失败了,因为它当前被写入错误输出。
您还会注意到ANALYZING现在出现在构建输出中。此步骤是 buildpack 生命周期的一部分,用于查看是否有任何先前的镜像构建具有 buildpack 可以重用的层。稍后我们将更详细地讨论这个主题。
构建您的应用程序
现在我们将更改您创建的构建步骤以安装应用程序依赖项。这将需要更新build脚本,以便它执行以下步骤:
为 Ruby 运行时创建一个层
下载 Ruby 运行时并将其安装到层
安装 Bundler(Ruby 依赖管理器)
使用 Bundler 安装依赖项
通过这样做,您将学习如何使用 buildpack 创建任意层,以及如何读取应用程序的内容以执行下载依赖项等操作。
让我们从更改 开始,ruby-buildpack/bin/build以便它为 Ruby 创建一个层。
创建图层
Buildpack 层由 Buildpack 执行环境提供给我们的 buildpack的层目录内的目录表示。要创建一个代表 Ruby 运行时的新层目录,请将build脚本更改为如下所示:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
layersdir=$1
rubylayer="$layersdir"/ruby
mkdir -p "$rubylayer"
该rubylayer目录是作为构建脚本的第一个位置参数提供的目录的子目录(层目录),这是我们将存储 Ruby 运行时的地方。
接下来,我们将下载 Ruby 运行时并将其安装到层目录中。将以下代码添加到build脚本末尾:
echo "---> Downloading and extracting Ruby"
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-2.5.1.tgz
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
此代码使用该wget工具从给定的 URL 下载 Ruby 二进制文件,并将其解压缩到rubylayer目录中。
创建层的最后一步是编写一个包含层元数据的 TOML 文件。TOML 文件的名称必须与图层的名称匹配(在本例中为ruby.toml)。如果没有这个文件,Buildpack 生命周期将忽略层目录。launch对于 Ruby 层,我们需要通过将密钥设置为 来确保它在启动映像中可用true。将以下代码添加到构建脚本中:
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
安装依赖
接下来,我们将使用您安装的 Ruby 运行时来下载应用程序的依赖项。首先,我们需要通过将 Ruby 可执行文件放在 Path 中来使我们的脚本可以使用它。将以下代码添加到build脚本末尾:
export PATH="$rubylayer"/bin:$PATH
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
现在我们可以安装 Bundler,一个 Ruby 的依赖管理器,然后运行bundle install命令。将以下代码附加到脚本中:
echo "---> Installing bundler"
gem install bundler --no-ri --no-rdoc
echo "---> Installing gems"
bundle install
现在 Buildpack 已准备好进行测试。
运行构建
您的完整ruby-buildpack/bin/build脚本应如下所示:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
# 1. GET ARGS
layersdir=$1
# 2. CREATE THE LAYER DIRECTORY
rubylayer="$layersdir"/ruby
mkdir -p "$rubylayer"
# 3. DOWNLOAD RUBY
echo "---> Downloading and extracting Ruby"
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-2.5.1.tgz
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
# 4. MAKE RUBY AVAILABLE DURING LAUNCH
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
# 5. MAKE RUBY AVAILABLE TO THIS SCRIPT
export PATH="$rubylayer"/bin:$PATH
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
# 6. INSTALL BUNDLER
echo "---> Installing bundler"
gem install bundler --no-ri --no-rdoc
# 7. INSTALL GEMS
echo "---> Installing gems"
bundle install
再次构建您的应用程序:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
您将看到以下输出:
===> DETECTING
...
===> ANALYZING
...
===> RESTORING
===> BUILDING
---> Ruby Buildpack
---> Downloading and extracting Ruby
---> Installing bundler
...
---> Installing gems
...
===> EXPORTING
...
Successfully built image 'test-ruby-app'
在您的 Docker 守护程序中创建了一个名为的新映像test-ruby-app,其中包含 Ruby 运行时的层。但是,您的应用映像还不能运行。我们将在下一节中使应用程序映像可运行。
使您的应用程序可运行
要使您的应用程序可运行,必须设置默认启动命令。您需要将以下内容添加到build脚本的末尾:
# ...
# Set default start command
cat > "$layersdir/launch.toml" << EOL
[[processes]]
type = "web"
command = "bundle exec ruby app.rb"
default = true
EOL
# ...
您的完整ruby-buildpack/bin/build脚本现在应如下所示:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
# 1. GET ARGS
layersdir=$1
# 2. CREATE THE LAYER DIRECTORY
rubylayer="$layersdir"/ruby
mkdir -p "$rubylayer"
# 3. DOWNLOAD RUBY
echo "---> Downloading and extracting Ruby"
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-2.5.1.tgz
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
# 4. MAKE RUBY AVAILABLE DURING LAUNCH
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
# 5. MAKE RUBY AVAILABLE TO THIS SCRIPT
export PATH="$rubylayer"/bin:$PATH
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
# 6. INSTALL BUNDLER
echo "---> Installing bundler"
gem install bundler --no-ri --no-rdoc
# 7. INSTALL GEMS
echo "---> Installing gems"
bundle install
# ========== ADDED ===========
# 8. SET DEFAULT START COMMAND
cat > "$layersdir/launch.toml" << EOL
[[processes]]
type = "web"
command = "bundle exec ruby app.rb"
default = true
EOL
然后使用更新的 buildpack 重建您的应用程序:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
然后,您应该能够运行新的 Ruby 应用程序:
docker run --rm -p 8080:8080 test-ruby-app
并查看服务器日志输出:
[2019-04-02 18:04:48] INFO WEBrick 1.4.2
[2019-04-02 18:04:48] INFO ruby 2.5.1 (2018-03-29) [x86_64-linux]
== Sinatra (v2.0.5) has taken the stage on 8080 for development with backup from WEBrick
[2019-04-02 18:04:48] INFO WEBrick::HTTPServer#start: pid=1 port=8080
通过在您最喜欢的浏览器中导航到localhost:8080来测试它!
我们可以将多种进程类型添加到单个应用程序中。我们将在下一节中这样做。
指定多个进程类型
buildpacks 的好处之一是它们是多进程的——一个映像可以为每个操作模式有多个入口点。让我们看看这是如何工作的。我们将扩展我们的应用程序以拥有一个工作进程。
让我们创建一个ruby-sample-app/worker.rb包含以下内容的工作文件:
for i in 0..5
puts "Running a worker task..."
end
构建我们的应用程序后,我们可以使用web进程(当前默认)或我们的新工作进程运行生成的图像。
为了能够运行工作进程,我们需要让我们的 buildpack 为工作进程定义一个“进程类型”。将定义流程的部分修改为:
# ...
cat > "$layersdir/launch.toml" << EOL
# our web process
[[processes]]
type = "web"
command = "bundle exec ruby app.rb"
default = true
# our worker process
[[processes]]
type = "worker"
command = "bundle exec ruby worker.rb"
EOL
# ...
您的完整ruby-buildpack/bin/build脚本现在应如下所示:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
# 1. GET ARGS
layersdir=$1
# 2. CREATE THE LAYER DIRECTORY
rubylayer="$layersdir"/ruby
mkdir -p "$rubylayer"
# 3. DOWNLOAD RUBY
echo "---> Downloading and extracting Ruby"
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-2.5.1.tgz
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
# 4. MAKE RUBY AVAILABLE DURING LAUNCH
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
# 5. MAKE RUBY AVAILABLE TO THIS SCRIPT
export PATH="$rubylayer"/bin:$PATH
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
# 6. INSTALL BUNDLER
echo "---> Installing bundler"
gem install bundler --no-ri --no-rdoc
# 7. INSTALL GEMS
echo "---> Installing gems"
bundle install
# ========== MODIFIED ===========
# 8. SET DEFAULT START COMMAND
cat > "$layersdir/launch.toml" << EOL
# our web process
[[processes]]
type = "web"
command = "bundle exec ruby app.rb"
default = true
# our worker process
[[processes]]
type = "worker"
command = "bundle exec ruby worker.rb"
EOL
现在,如果您使用更新的 buildpack 重建您的应用程序:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
然后,您应该能够运行新的 Ruby 工作进程:
docker run --rm --entrypoint worker test-ruby-app
并查看工作日志输出:
Running a worker task...
Running a worker task...
Running a worker task...
Running a worker task...
Running a worker task...
Running a worker task...
接下来,我们将看看如何通过利用缓存来改进我们的 buildpack。
通过缓存提高性能
我们可以通过缓存构建之间的依赖关系来提高性能,只在必要时重新下载。首先,让我们创建一个可缓存bundler层。
创建bundler图层
为此,请替换build脚本中的以下行:
echo "---> Installing gems"
bundle install
具有以下内容:
echo "---> Installing gems"
bundlerlayer="$layersdir/bundler"
mkdir -p "$bundlerlayer"
echo -e '[types]\ncache = true\nlaunch = true' > "$layersdir/bundler.toml"
bundle config set --local path "$bundlerlayer" && bundle install && bundle binstubs --all --path "$bundlerlayer/bin"
您的完整ruby-buildpack/bin/build脚本现在应如下所示:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
# 1. GET ARGS
layersdir=$1
# 2. CREATE THE LAYER DIRECTORY
rubylayer="$layersdir"/ruby
mkdir -p "$rubylayer"
# 3. DOWNLOAD RUBY
echo "---> Downloading and extracting Ruby"
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-2.5.1.tgz
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
# 4. MAKE RUBY AVAILABLE DURING LAUNCH
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
# 5. MAKE RUBY AVAILABLE TO THIS SCRIPT
export PATH="$rubylayer"/bin:$PATH
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
# 6. INSTALL BUNDLER
echo "---> Installing bundler"
gem install bundler --no-ri --no-rdoc
# ======= MODIFIED =======
# 7. INSTALL GEMS
echo "---> Installing gems"
bundlerlayer="$layersdir/bundler"
mkdir -p "$bundlerlayer"
echo -e '[types]\ncache = true\nlaunch = true' > "$layersdir/bundler.toml"
bundle config set --local path "$bundlerlayer" && bundle install && bundle binstubs --all --path "$bundlerlayer/bin"
# 8. SET DEFAULT START COMMAND
cat > "$layersdir/launch.toml" << EOL
# our web process
[[processes]]
type = "web"
command = "bundle exec ruby app.rb"
default = true
# our worker process
[[processes]]
type = "worker"
command = "bundle exec ruby worker.rb"
EOL
现在当我们运行时:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
在此阶段,您将看到类似于以下内容EXPORTING:
Adding layer 'examples/ruby:bundler'
缓存依赖项
现在,让我们实现缓存逻辑。我们首先需要创建一个ruby-sample-app/Gemfile.lock包含以下内容的文件:
通常你会在bundle install本地运行来生成这个文件,但为了简单起见,我们将ruby-sample-app/Gemfile.lock手动创建。
GEM
remote: https://rubygems.org/
specs:
mustermann (1.0.3)
rack (2.0.7)
rack-protection (2.0.7)
rack
sinatra (2.0.7)
mustermann (~> 1.0)
rack (~> 2.0)
rack-protection (= 2.0.7)
tilt (~> 2.0)
tilt (2.0.9)
PLATFORMS
ruby
DEPENDENCIES
sinatra
BUNDLED WITH
2.0.2
替换上一步中的 gem 安装逻辑:
# ...
echo "---> Installing gems"
bundlerlayer="$layersdir/bundler"
mkdir -p "$bundlerlayer"
echo -e '[types]\ncache = true\nlaunch = true' > "$layersdir/bundler.toml"
bundle config set --local path "$bundlerlayer" && bundle install && bundle binstubs --all --path "$bundlerlayer/bin"
# ...
使用下面的新逻辑检查是否有任何宝石已更改。这只是为先前创建一个校验Gemfile.lock和并将其与当前的校验和进行比较Gemfile.lock。如果它们相同,则重复使用这些宝石。如果不是,则安装新的 gem。
我们现在将额外的元数据写入我们bundler.toml的表单cache = true和launch = true. 这指导生命周期缓存我们的 gem 并在启动我们的应用程序时提供它们。使用cache = true生命周期可以保留现有的 gem,以便构建时间很快,即使是微小的Gemfile.lock变化。
请注意,有时您可能希望从以前的构建中清除缓存层,在这种情况下,您应该始终确保在继续构建之前删除层的内容。在下面的情况下,这可以在命令rm -rf "$bundlerlayer"/*之后使用简单的来完成。mkdir -p "$bundlerlayer"
# Compares previous Gemfile.lock checksum to the current Gemfile.lock
bundlerlayer="$layersdir/bundler"
local_bundler_checksum=$((sha256sum Gemfile.lock || echo 'DOES_NOT_EXIST') | cut -d ' ' -f 1)
remote_bundler_checksum=$(cat "$layersdir/bundler.toml" | yj -t | jq -r .metadata.checksum 2>/dev/null || echo 'DOES_NOT_EXIST')
# Always set the types table so that we re-use the appropriate layers
echo -e '[types]\ncache = true\nlaunch = true' >> "$layersdir/bundler.toml"
if [[ -f Gemfile.lock && $local_bundler_checksum == $remote_bundler_checksum ]] ; then
# Determine if no gem dependencies have changed, so it can reuse existing gems without running bundle install
echo "---> Reusing gems"
bundle config --local path "$bundlerlayer" >/dev/null
bundle config --local bin "$bundlerlayer/bin" >/dev/null
else
# Determine if there has been a gem dependency change and install new gems to the bundler layer; re-using existing and un-changed gems
echo "---> Installing gems"
mkdir -p "$bundlerlayer"
cat > "$layersdir/bundler.toml" << EOL
[metadata]
checksum = "$local_bundler_checksum"
EOL
bundle config set --local path "$bundlerlayer" && bundle install && bundle binstubs --all --path "$bundlerlayer/bin"
fi
您的完整ruby-buildpack/bin/build脚本现在将如下所示:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
# 1. GET ARGS
layersdir=$1
# 2. CREATE THE LAYER DIRECTORY
rubylayer="$layersdir"/ruby
mkdir -p "$rubylayer"
# 3. DOWNLOAD RUBY
echo "---> Downloading and extracting Ruby"
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-2.5.1.tgz
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
# 4. MAKE RUBY AVAILABLE DURING LAUNCH
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
# 5. MAKE RUBY AVAILABLE TO THIS SCRIPT
export PATH="$rubylayer"/bin:$PATH
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
# 6. INSTALL BUNDLER
echo "---> Installing bundler"
gem install bundler --no-ri --no-rdoc
# ======= MODIFIED =======
# 7. INSTALL GEMS
# Compares previous Gemfile.lock checksum to the current Gemfile.lock
bundlerlayer="$layersdir/bundler"
local_bundler_checksum=$((sha256sum Gemfile.lock || echo 'DOES_NOT_EXIST') | cut -d ' ' -f 1)
remote_bundler_checksum=$(cat "$layersdir/bundler.toml" | yj -t | jq -r .metadata.checksum 2>/dev/null || echo 'DOES_NOT_EXIST')
# Always set the types table so that we re-use the appropriate layers
echo -e '[types]\ncache = true\nlaunch = true' >> "$layersdir/bundler.toml"
if [[ -f Gemfile.lock && $local_bundler_checksum == $remote_bundler_checksum ]] ; then
# Determine if no gem dependencies have changed, so it can reuse existing gems without running bundle install
echo "---> Reusing gems"
bundle config --local path "$bundlerlayer" >/dev/null
bundle config --local bin "$bundlerlayer/bin" >/dev/null
else
# Determine if there has been a gem dependency change and install new gems to the bundler layer; re-using existing and un-changed gems
echo "---> Installing gems"
mkdir -p "$bundlerlayer"
cat >> "$layersdir/bundler.toml" << EOL
[metadata]
checksum = "$local_bundler_checksum"
EOL
bundle config set --local path "$bundlerlayer" && bundle install && bundle binstubs --all --path "$bundlerlayer/bin"
fi
# 8. SET DEFAULT START COMMAND
cat > "$layersdir/launch.toml" << EOL
# our web process
[[processes]]
type = "web"
command = "bundle exec ruby app.rb"
default = true
# our worker process
[[processes]]
type = "worker"
command = "bundle exec ruby worker.rb"
EOL
现在,当您构建应用程序时:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
它将下载宝石:
===> BUILDING
---> Ruby Buildpack
---> Downloading and extracting Ruby
---> Installing bundler
...
---> Installing gems
如果您再次构建应用程序:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
您将在此阶段看到新的缓存逻辑在工作BUILDING:
===> BUILDING
---> Ruby Buildpack
---> Downloading and extracting Ruby
---> Installing bundler
...
---> Reusing gems
接下来,让我们看看 buildpack 用户如何能够为 buildpack 提供配置。
使您的 buildpack 可配置
很可能并非所有 Ruby 应用程序都希望使用相同版本的 Ruby。让我们使 Ruby 版本可配置。
选择 Ruby 版本
我们将允许 buildpack 用户通过其应用程序中的文件定义所需的 Ruby 版本.ruby-version。我们将首先更新detect脚本以检查此文件。然后,我们将记录我们可以使用的依赖项provide(Ruby),以及应用程序将require在Build Plan文档中记录的特定依赖项,生命周期用于确定 buildpack 是否会提供应用程序所需的一切。
更新ruby-buildpack/bin/detect为如下所示:
#!/usr/bin/env bash
set -eo pipefail
if [[ ! -f Gemfile ]]; then
exit 100
fi
# ======= ADDED =======
plan=$2
version=2.5.1
if [[ -f .ruby-version ]]; then
version=$(cat .ruby-version | tr -d '[:space:]')
fi
echo "provides = [{ name = \"ruby\" }]" > "$plan"
echo "requires = [{ name = \"ruby\", metadata = { version = \"$version\" } }]" >> "$plan"
# ======= /ADDED =======
然后您需要更新您的build脚本以在构建计划中查找记录的 Ruby 版本:
您的ruby-buildpack/bin/build脚本应如下所示:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
# ======= MODIFIED =======
# 1. GET ARGS
layersdir=$1
plan=$3
# 2. CREATE THE LAYER DIRECTORY
rubylayer="$layersdir"/ruby
mkdir -p "$rubylayer"
# ======= MODIFIED =======
# 3. DOWNLOAD RUBY
ruby_version=$(cat "$plan" | yj -t | jq -r '.entries[] | select(.name == "ruby") | .metadata.version')
echo "---> Downloading and extracting Ruby $ruby_version"
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-$ruby_version.tgz
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
# 4. MAKE RUBY AVAILABLE DURING LAUNCH
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
# 5. MAKE RUBY AVAILABLE TO THIS SCRIPT
export PATH="$rubylayer"/bin:$PATH
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
# 6. INSTALL BUNDLER
echo "---> Installing bundler"
gem install bundler --no-ri --no-rdoc
# 7. INSTALL GEMS
# Compares previous Gemfile.lock checksum to the current Gemfile.lock
bundlerlayer="$layersdir/bundler"
local_bundler_checksum=$((sha256sum Gemfile.lock || echo 'DOES_NOT_EXIST') | cut -d ' ' -f 1)
remote_bundler_checksum=$(cat "$layersdir/bundler.toml" | yj -t | jq -r .metadata.checksum 2>/dev/null || echo 'DOES_NOT_EXIST')
# Always set the types table so that we re-use the appropriate layers
echo -e '[types]\ncache = true\nlaunch = true' >> "$layersdir/bundler.toml"
if [[ -f Gemfile.lock && $local_bundler_checksum == $remote_bundler_checksum ]] ; then
# Determine if no gem dependencies have changed, so it can reuse existing gems without running bundle install
echo "---> Reusing gems"
bundle config --local path "$bundlerlayer" >/dev/null
bundle config --local bin "$bundlerlayer/bin" >/dev/null
else
# Determine if there has been a gem dependency change and install new gems to the bundler layer; re-using existing and un-changed gems
echo "---> Installing gems"
mkdir -p "$bundlerlayer"
cat >> "$layersdir/bundler.toml" << EOL
[metadata]
checksum = "$local_bundler_checksum"
EOL
bundle config set --local path "$bundlerlayer" && bundle install && bundle binstubs --all --path "$bundlerlayer/bin"
fi
# 8. SET DEFAULT START COMMAND
cat > "$layersdir/launch.toml" << EOL
# our web process
[[processes]]
type = "web"
command = "bundle exec ruby app.rb"
default = true
# our worker process
[[processes]]
type = "worker"
command = "bundle exec ruby worker.rb"
EOL
最后,创建一个ruby-sample-app/.ruby-version包含以下内容的文件:
2.5.0
现在当你运行时:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
您会注意到.ruby-version下载了应用程序文件中指定的 Ruby 版本。
===> BUILDING
---> Ruby Buildpack
---> Downloading and extracting Ruby 2.5.0
接下来,让我们看看 buildpacks 如何存储有关输出应用程序映像中提供的依赖项的信息以进行自省。
添加物料清单
buildpacks 的好处之一是它们还可以使用构建过程中的元数据填充应用程序映像,从而允许您审核应用程序映像以获取以下信息:
可用的进程类型以及与之关联的命令
应用映像所基于的运行映像
buildpacks 用于创建应用程序映像
和更多…!
pack您可以通过其inspect-image命令找到其中一些信息。物料清单信息将使用 提供pack sbom download。
pack inspect-image test-ruby-app
您应该看到以下内容:
Run Images:
cnbs/sample-stack-run:bionic
Buildpacks:
ID VERSION HOMEPAGE
examples/ruby 0.0.1 -
Processes:
TYPE SHELL COMMAND ARGS
web (default) bash bundle exec ruby app.rb
worker bash bundle exec ruby worker.rb
除了上述标准元数据之外,构建包还可以填充有关它们以Bill-of-Materials. 让我们看看我们如何使用它来填充有关ruby在输出应用程序图像中安装的版本的信息。
要将ruby版本添加到 的输出中pack download sbom,我们必须提供包含此信息的软件Bill-of-Materials( )。SBOM报告 SBOM 数据有三种“标准”方式。您需要选择在脚本末尾使用CycloneDX、SPDX或Syft更新ruby.sbom.(适合您的 SBOM 标准的扩展名,其中之一cdx.json,spdx.json或syft.json)build。关于选择哪种 SBOM 格式的讨论超出了本教程的范围,但我们会注意到,您选择使用的 SBOM 格式很可能是任何 SBOM 扫描仪的输出格式(例如:syft cli) 您可能会选择使用。在本例中,我们将使用 CycloneDX json 格式。
首先,注释buildpack.toml以指定它发出 CycloneDX:
# Buildpack API version
api = "0.7"
# Buildpack ID and metadata
[buildpack]
id = "examples/ruby"
version = "0.0.1"
sbom-formats = [ "application/vnd.cyclonedx+json" ]
# Stacks that the buildpack will work with
[[stacks]]
id = "io.buildpacks.samples.stacks.bionic"
然后,在我们的 buildpack 实现中,我们将生成必要的 SBOM 元数据:
# ...
# Append a Bill-of-Materials containing metadata about the provided ruby version
cat >> "$layersdir/ruby.sbom.cdx.json" << EOL
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"components": [
{
"type": "library",
"name": "ruby",
"version": "$ruby_version"
}
]
}
EOL
我们还可以为列出的每个依赖项添加一个 SBOM 条目Gemfile.lock。这里我们使用在数组中jq添加一条新记录:componentsbundler.sbom.cdx.json
crubybom="${layersdir}/ruby.sbom.cdx.json"
cat >> ${rubybom} << EOL
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"components": [
{
"type": "library",
"name": "ruby",
"version": "$ruby_version"
}
]
}
EOL
if [[ -f Gemfile.lock ]] ; then
for gem in $(gem dep -q | grep ^Gem | sed 's/^Gem //')
do
version=${gem##*-}
name=${gem%-${version}}
DEP=$(jq --arg name "${name}" --arg version "${version}" \
'.components[.components| length] |= . + {"type": "library", "name": $name, "version": $version}' \
"${rubybom}")
echo ${DEP} > "${rubybom}"
done
fi
您的ruby-buildpack/bin/build脚本应如下所示:
#!/usr/bin/env bash
set -eo pipefail
echo "---> Ruby Buildpack"
# 1. GET ARGS
layersdir=$1
plan=$3
# 2. CREATE THE LAYER DIRECTORY
rubylayer="$layersdir"/ruby
mkdir -p "$rubylayer"
# 3. DOWNLOAD RUBY
ruby_version=$(cat "$plan" | yj -t | jq -r '.entries[] | select(.name == "ruby") | .metadata.version')
echo "---> Downloading and extracting Ruby $ruby_version"
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-$ruby_version.tgz
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
# 4. MAKE RUBY AVAILABLE DURING LAUNCH
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
# 5. MAKE RUBY AVAILABLE TO THIS SCRIPT
export PATH="$rubylayer"/bin:$PATH
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
# 6. INSTALL BUNDLER
echo "---> Installing bundler"
gem install bundler --no-ri --no-rdoc
# 7. INSTALL GEMS
# Compares previous Gemfile.lock checksum to the current Gemfile.lock
bundlerlayer="$layersdir/bundler"
local_bundler_checksum=$((sha256sum Gemfile.lock || echo 'DOES_NOT_EXIST') | cut -d ' ' -f 1)
remote_bundler_checksum=$(cat "$layersdir/bundler.toml" | yj -t | jq -r .metadata.checksum 2>/dev/null || echo 'DOES_NOT_EXIST')
# Always set the types table so that we re-use the appropriate layers
echo -e '[types]\ncache = true\nlaunch = true' >> "$layersdir/bundler.toml"
if [[ -f Gemfile.lock && $local_bundler_checksum == $remote_bundler_checksum ]] ; then
# Determine if no gem dependencies have changed, so it can reuse existing gems without running bundle install
echo "---> Reusing gems"
bundle config --local path "$bundlerlayer" >/dev/null
bundle config --local bin "$bundlerlayer/bin" >/dev/null
else
# Determine if there has been a gem dependency change and install new gems to the bundler layer; re-using existing and un-changed gems
echo "---> Installing gems"
mkdir -p "$bundlerlayer"
cat >> "$layersdir/bundler.toml" << EOL
[metadata]
checksum = "$local_bundler_checksum"
EOL
bundle config set --local path "$bundlerlayer" && bundle install && bundle binstubs --all --path "$bundlerlayer/bin"
fi
# 8. SET DEFAULT START COMMAND
cat > "$layersdir/launch.toml" << EOL
# our web process
[[processes]]
type = "web"
command = "bundle exec ruby app.rb"
default = true
# our worker process
[[processes]]
type = "worker"
command = "bundle exec ruby worker.rb"
EOL
# ========== ADDED ===========
# 9. ADD A SBOM
rubybom="${layersdir}/ruby.sbom.cdx.json"
cat >> ${rubybom} << EOL
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"components": [
{
"type": "library",
"name": "ruby",
"version": "$ruby_version"
}
]
}
EOL
if [[ -f Gemfile.lock ]] ; then
for gem in $(gem dep -q | grep ^Gem | sed 's/^Gem //')
do
version=${gem##*-}
name=${gem%-${version}}
DEP=$(jq --arg name "${name}" --arg version "${version}" \
'.components[.components| length] |= . + {"type": "library", "name": $name, "version": $version}' \
"${rubybom}")
echo ${DEP} > "${rubybom}"
done
fi
然后使用更新的 buildpack 重建您的应用程序:
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
查看您的物料清单需要download从您的本地图像中提取(或提取)物料清单。此命令可能需要一些时间才能返回。
pack sbom download test-ruby-app
SBOM 信息现在已下载到本地文件系统:
cat layers/sbom/launch/examples_ruby/ruby/sbom.cdx.json | jq -M
您应该会发现包含的ruby版本2.5.0与预期的一样。
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"components": [
{
"type": "library",
"name": "ruby",
"version": "2.5.0"
},
...
]
}
恭喜!您已经创建了第一个可配置的 Cloud Native Buildpack,它使用检测、图像层和缓存来创建可自省且可运行的 OCI 图像。