概述 Buildpacks 是云原生的应用镜像构建工具,它主要的作用是将应用源代码打包成符合OCI标准的镜像,供给各种云环境使用。
核心组件 Buildpacks 可以分为几个部分,参考 Componets 。
Lifecycle Lifecycle 是 Buildpacks 最重要的组件,它将由应用源代码到镜像的构建步骤抽象出来,完成了对整个过程的编排,并最终产出应用镜像。
它所编排的步骤为(按顺序):
Stack Stack 的功能是提供基础运行环境,它包含两种镜像:
build ,为 Lifecycle 的 Build 阶段提供运行环境
run ,为业务应用提供运行环境
Buildpack Buildpack 可以理解为任务单一的构建单元,每一个 buildpack 都具备以下三个元素:
buildpack.toml ,描述了 buildpack 的元数据,内容定义格式参考 buildpacktoml
bin/detect ,Lifecycle 中 Detect 阶段的实现
bin/build ,Lifecycle 中 Build 阶段的实现
它的工作原理可以简单理解为:bin/detect 将会判断是否可以进入该 buildpack ,如果可以进入该 buildpack ,那么 Lifecycle 就会指导流程进入 Build 阶段同时触发 bin/build 执行。
每个 buildpack 完成后都会产出一份清单,用于表明自己产出了什么内容(可为空),且如果要完成后续的 buildpack 过程,还需要什么内容(可为空)。参考 Build Plan 。
同时在 Build 阶段,基于复用构建逻辑的原则,buildpack 的作者可以设置不通过的 layer (layer 内保存了所关联的构建过程的结果,如文件、运行时等内容),并设置 build 、launch 、cache 三种属性(Layer Content Metadata )。Lifecycle 将根据这三个参数的组合来决定是否复用一个 layer ,规则参考 Layer Types 。
Builder Builder 描述了一个完整的构建编排逻辑。它本质上是将 Stack 及若干个 Buildpack 组合在一起,形成一个业务逻辑明确的构建工具。
图片来自 Buildpacks.io 官网
Buildpacks 在 OpenFunction 中的作用 在 OpenFunction 中,Buildpacks 主要用于 build 阶段,配合 Tekton 完成函数从源代码到镜像的制作。
制作过程由 buildpacks.yaml 描述,分为三个步骤:
prepare ,准备环境变量
create ,使用 /cnb/lifecycle/creator(即 Lifecycle 中的 Create 阶段的实现)进行源代码的构建,规范标准参考 creator 。
results ,输出镜像的摘要
OpenFunction 还需要基于 Buildpacks 的原理制作适用于 serverless(FaaS)场景的 builder 。
这个 builder 需要具备以下特性:
封装函数源代码,使之能被多种常见的事件源触发(function framework)
快速构建镜像
适用于在线或离线构建场景
常见的开源 builder 产品:
gcr.io/buildpacks/builder:v1
paketobuildpacks/builder(包含 base 、 full 、 tiny 三个类型)
基于 gcp/buildpacks 自定义 builder Google 提供了一套适用于buildpacks的工具, GoogleCloudPlatform/buildpacks ,用于识别业务代码语言,然后使用对应的语言构建工具打包业务代码。
参考 本地构建gcp/buildpacks/builder镜像 使用 GoogleCloudPlatform/buildpacks 制作原生的 gcp/builder 。
相比 paketobuildpacks/builder ,gcp/buildpacks 提供了更丰富的构建时上下文以及现成的 function framework 模板,基于 gcp/buildpacks 自定义更契合 OpenFunction 的 builder 在实现上将会更容易一些。
当前 OpenFunction 的 builder 项目在这里:https://github.com/OpenFunction/builder
stack 以 python3.9 的函数构建为例,需要先准备一个部署有 python3.9 runtime 的 stack ,stack 本质为一个基础运行环境,在制作过程中加入 LABEL io.buildpacks.stack.id 即可以指定 stack 的标识。
可以参考 Create a stack 制作一个简单的 stack,stack 的规范标准请参考 Stacks
原生 gcp/buildpacks 将下载语言 runtime 的步骤放置在 buildpack 中,即在探测到语言类型后再下载安装对应的语言 runtime
本例中选择将语言 runtime 安装在 stack 中,以减少函数构建过程中的时间(python、node、golang 语言函数的构建时间在原有的80~90s基础上缩短20s左右)
❓ 需要探讨该部分机制的合理性
以下为基于 gcp/buildpacks 自定义的 python3.9 stack :
完整的 stack 定义请参考 py39/stack
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 FROM gcr.io/gcp-runtimes/ubuntu_18_0_4ARG cnb_uid=1000 ARG cnb_gid=1000 ARG stack_id="google" RUN apt-get update && apt-get install -y --no-install-recommends \ libexpat1 \ libffi6 \ libmpdec2 \ libicu60 \ libc++1-9 \ tzdata \ && apt-get clean && rm -rf /var/lib/apt/lists/* LABEL io.buildpacks.stack.id=${stack_id} RUN groupadd cnb --gid ${cnb_gid} && \ useradd --uid ${cnb_uid} --gid ${cnb_gid} -m -s /bin/bash cnb RUN apt-get update && apt-get install -y --no-install-recommends \ wget \ build-essential \ libffi-dev \ zlib1g-dev \ libssl-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ cd /opt && wget https://www.python.org/ftp/python/3.9.5/Python-3.9.5.tgz && tar xzf Python-3.9.5.tgz && cd Python-3.9.5 && \ sed -i '214,217s/^#//' Modules/Setup && /bin/bash -c "./configure && make -j8 && make -j8 install" && \ ln -sf /usr/local /bin/python3.9 /usr/bin/python3 && python3 -m pip install --upgrade pip setuptools wheel && \ cd /opt && rm -rf Python-3.9.5 ENV CNB_USER_ID=${cnb_uid}ENV CNB_GROUP_ID=${cnb_gid}ENV CNB_STACK_ID=${stack_id}
buildpack 参考其中标识为 google.python.functions-framework 的 buildpack.toml:
api 表示 buildpack 遵循的格式标准
[buildpack] 中定义了一些元数据,用于和其他资源关联
[[stacks]] 用于描述 buildpack 可以运行在哪些 stack 上
可以参考 Create a buildpack 制作一个简单的 buildpack
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 api = "0.2" [buildpack] id = "google.python.functions-framework" version = "0.9.4" name = "Python - Functions Framework" [[stacks]] id = "google" [[stacks]] id = "google.python37" [[stacks]] id = "google.python38" [[stacks]] id = "google.python39"
gcp 提供了 bazel 工具将相关代码目录打包成 tgz 文件,对于 buildpack 来说,就是封装了 /bin/detect 和 /bin/build 两个功能。google.python.functions-framework 这个 buildpack 的打包配置参考 BUILD.bazel 。
基于这个机制,只需要在 main.go 中完成 detectFn 和 buildFn 两个函数的内容即可。
builder builder 的工作内容描述:
图片来自 Buildpacks.io 官网
builder 的定义文件 builder.toml 内容如下:
[[buildpacks]] 表示引入的 buildpack ,此处 gcp 使用 bazel 将相关代码目录打包成了 tgz 文件
[[order]]|[[order.group]] 表示 buildpack 的执行顺序
[stack] 表示引用的 stack 内容
[lifecycle] 表示使用的 lifecycle 模块的版本
可以参考 Create a builder 制作一个简单的 builder
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 [[buildpacks]] id = "google.config.entrypoint" uri = "entrypoint.tgz" [[buildpacks]] id = "google.python.pip" uri = "python/pip.tgz" [[buildpacks]] id = "google.python.functions-framework" uri = "python/functions_framework.tgz" [[buildpacks]] id = "google.python.missing-entrypoint" uri = "python/missing_entrypoint.tgz" [[buildpacks]] id = "google.utils.label" uri = "label.tgz" [[order]] [[order.group]] id = "google.python.functions-framework" [[order.group]] id = "google.python.pip" optional = true [[order.group]] id = "google.config.entrypoint" optional = true [[order.group]] id = "google.utils.label" [stack] id = "google" build-image = "openfunctiondev/buildpacks-py37-build:v1" run-image = "openfunctiondev/buildpacks-py37-run:v1" [lifecycle] version = "0.11.1"
运行日志 以下为一个 builder 运行时的日志信息:
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 ===> DETECTING 3 of 4 buildpacks participating google.python.functions-framework 0.9.4 google.python.pip 0.9.2 google.utils.label 0.0.1 ===> ANALYZING Restoring metadata for "google.python.pip:pip" from app image ===> RESTORING Removing "google.python.pip:pip", not in cache ===> BUILDING === Python - Functions Framework (google.python.functions-framework@0.9.4) === -------------------------------------------------------------------------------- Running "python3 -m compileall -f -q ." Done "python3 -m compileall -f -q ." (63.312851ms) Handling functions with dependency on functions-framework. === Python - pip (google.python.pip@0.9.2) === Installing application dependencies. -------------------------------------------------------------------------------- Running "python3 -m pip install --requirement requirements.txt --upgrade --upgrade-strategy only-if-needed --no-warn-script-location --no-warn-conflicts --force-reinstall --no-compile --user (PIP_CACHE_DIR=/layers/google.python.pip/pipcache PIP_DISABLE_PIP_VERSION_CHECK=1)" Collecting functions-framework==1.4.3 Downloading functions_framework-1.4.3-py3-none-any.whl (21 kB) Collecting watchdog>=0.10.0 Downloading watchdog-2.1.1-py3-none-manylinux2014_x86_64.whl (74 kB) Collecting click<8.0,>=7.0 Downloading click-7.1.2-py2.py3-none-any.whl (82 kB) Collecting gunicorn<21.0,>=19.2.0 Downloading gunicorn-20.1.0-py3-none-any.whl (79 kB) Collecting flask<2.0,>=1.0 Downloading Flask-1.1.3-py2.py3-none-any.whl (94 kB) Collecting Werkzeug<2.0,>=0.15 Downloading Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB) Collecting Jinja2<3.0,>=2.10.1 Downloading Jinja2-2.11.3-py2.py3-none-any.whl (125 kB) Collecting itsdangerous<2.0,>=0.24 Downloading itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB) Collecting setuptools>=3.0 Downloading setuptools-56.2.0-py3-none-any.whl (785 kB) Collecting MarkupSafe>=0.23 Downloading MarkupSafe-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl (31 kB) Installing collected packages: MarkupSafe, Werkzeug, setuptools, Jinja2, itsdangerous, click, watchdog, gunicorn, flask, functions-framework Successfully installed Jinja2-2.11.3 MarkupSafe-2.0.0 Werkzeug-1.0.1 click-7.1.2 flask-1.1.3 functions-framework-1.4.3 gunicorn-20.1.0 itsdangerous-1.1.0 setuptools-56.2.0 watchdog-2.1.1 Done "python3 -m pip install --requirement requirements.txt --upgr..." (1m5.686905222s) -------------------------------------------------------------------------------- Running "python3 -m compileall --invalidation-mode unchecked-hash -qq /layers/google.python.pip/pip" Done "python3 -m compileall --invalidation-mode unchecked-hash -qq..." (788.702099ms) Checking for incompatible dependencies. -------------------------------------------------------------------------------- Running "python3 -m pip check" No broken requirements found. Done "python3 -m pip check" (535.823132ms) === Utils - Label Image (google.utils.label@0.0.1) === ===> EXPORTING Reusing layers from image 'index.docker.io/zephyrfish/python-hello-func@sha256:e5550f9bb775872b95b23bd1030d0bdc99d24ed8b94383341638bea6a84acb19' Reusing layer 'google.python.functions-framework:functions-framework' Adding layer 'google.python.pip:pip' Adding 1/1 app layer(s) Reusing layer 'launcher' Reusing layer 'config' Reusing layer 'process-types' Adding label 'io.buildpacks.lifecycle.metadata' Adding label 'io.buildpacks.build.metadata' Adding label 'io.buildpacks.project.metadata' Setting default process type 'web' Saving zephyrfish/python-hello-func:latest... *** Images (sha256:f34b0428549a6f61ceaf751520f55fc3b2b7dbe9fd795f25318a83d487bb6c33): zephyrfish/python-hello-func:latest Adding cache layer 'google.python.pip:pip' Adding cache layer 'google.python.pip:pipcache'
function framework FaaS 的目的在于降低用户使用 serverless 框架的成本,为了让用户专注于业务函数本身,就需要有一个 framework 将用户纯粹的业务函数转换为符合 serverless 框架标准的 main 函数。
例如以下是一个输入函数,这个函数显然无法直接运行在 Knative 等 serverless 框架下(可以参考 Knative 的 Hello World Sample ):
1 2 3 4 5 6 7 8 9 10 package myfunctionimport ( "fmt" "net/http" ) func HelloWorld (w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello, world" ) }
经过 google.go.functions-framework 转换后的 main.go:
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 package mainimport ( "log" "os" "net/http" userfunction "example.com/myfunction" "github.com/GoogleCloudPlatform/functions-framework-go/funcframework" ) func register (fn interface {}) error { if fnHTTP, ok := fn.(func (http.ResponseWriter, *http.Request) ); ok { funcframework.RegisterHTTPFunction("/" , fnHTTP) } else { funcframework.RegisterEventFunction("/" , fn) } return nil } func main () { if err := register(userfunction.Handler); err != nil { log.Fatalf("Function failed to register: %v\n" , err) } http.HandleFunc("/robots.txt" , http.NotFound) http.HandleFunc("/favicon.ico" , http.NotFound) port := os.Getenv("PORT" ) if port == "" { port = "8080" } if err := funcframework.Start(port); err != nil { log.Fatalf("Function failed to start: %v\n" , err) } }
这样一来,函数(被嵌入框架后)就可以在 serverless 场景下被调用了。