微服务学习 - Buildpacks 工作原理

概述

Buildpacks 是云原生的应用镜像构建工具,它主要的作用是将应用源代码打包成符合OCI标准的镜像,供给各种云环境使用。

核心组件

Buildpacks 可以分为几个部分,参考 Componets

Lifecycle

Lifecycle 是 Buildpacks 最重要的组件,它将由应用源代码到镜像的构建步骤抽象出来,完成了对整个过程的编排,并最终产出应用镜像。

它所编排的步骤为(按顺序):

  • Detect ,判断是否可以使用该 buildpack ,规范标准参考 Phase #1: Detection
  • Analyze ,与 Restore 配合,为 Build 阶段提供文件、 layers 的缓存。需要在上一个 Build 阶段定义需要缓存的 layer,规范标准参考 Phase #2: Analysis
  • Restore ,与 Analyze 配合,为 Build 阶段提供文件、 layers 的缓存。需要在上一个 Build 阶段定义需要缓存的 layer,规范标准参考 Phase #2: Analysis
  • Build ,将源代码构建为容器中的可运行物,规范标准参考 Phase #3: Build
  • Export ,输出OCI镜像,规范标准参考 Phase #4: Export
  • Create , 一键执行上述5个步骤(按顺序)。
  • Launch ,负责启动镜像中的应用,规范标准参考 Launch
  • Rebase ,用于替换应用的 run 镜像。

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 组合在一起,形成一个业务逻辑明确的构建工具。

create-builder diagram

图片来自 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_4

ARG cnb_uid=1000
ARG cnb_gid=1000
ARG stack_id="google"

# Required by python/runtime: libexpat1, libffi6, libmpdecc2.
# Required by dotnet/runtime: libicu60
# Required by go/runtime: tzdata (Go may panic without /usr/share/zoneinfo)
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 的工作内容描述:

build diagram

图片来自 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 myfunction

import (
"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 main

import (
"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)
}

// Don't invoke the function for reserved URLs.
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 场景下被调用了。