我们已经讨论了我们的werf GitOps工具
不止一次 ,但是这次我们想与项目文档
werf.io (其俄语版本为
ru.werf.io )分享构建站点的经验。 这是一个常规的静态站点,但是其组装很有趣,因为它是使用动态数量的工件构建的。

进入网站结构的细微差别:为所有版本,包含发行信息的页面生成通用菜单。 -我们不会。 相反,我们专注于动态程序集的问题和功能,而只关注随附的CI / CD流程。
简介:网站的布置方式
首先,werf文档及其代码一起存储。 这提出了某些开发要求,这些要求通常超出了本文的范围,但是至少我们可以这样说:
- 在不更新文档的情况下,不应发布新的werf函数,相反,文档中的任何更改都意味着将发布新版本的werf;
- 该项目的开发相当密集:新版本可能一天要发布几次;
- 使用新版本的文档对网站进行的任何手动部署至少都是乏味的;
- 该项目采用语义版本控制的方法,具有5个稳定通道。 释放过程涉及版本通过通道的顺序传递,以增加稳定性:从alpha到坚如磐石;
- 该网站有俄语版本,与主要版本(即英语版本)并行“实时发展”(即其内容已更新)。
为了向用户隐藏所有这些“内部厨房”,并为他提供“正常工作”,我们制作了一个
单独的werf安装和更新工具 -这是
multiwerf 。 只需指出您可以使用的发行版号和稳定版频道,multiwerf就会检查该频道上是否有新版本,并在必要时进行下载。
网站上的版本选择菜单中提供了每个通道中werf的最新版本。 默认情况下,最新版本的最稳定通道的版本在
werf.io/documentation中打开-搜索引擎也会对其进行索引。 该通道的文档可在单独的地址获得(例如,针对beta版本1.0的
werf.io/v1.0-beta/documentation )。
总计,该网站具有以下版本:
- 根(默认情况下打开)
- 每个发行版的每个活动更新通道(例如werf.io/v1.0-beta )。
要在一般情况下生成特定版本的站点,只需在切换到所需版本的Git标记后,在werf存储库的
/docs
目录中运行相应的命令(
jekyll build
),即可使用
Jekyll工具对其进行编译。
仅保留以下内容:
- 实用程序本身(werf)用于组装;
- CI / CD流程基于GitLab CI;
- 当然,所有这些都可以在Kubernetes中使用。
任务
现在,我们在制定任务时要考虑到所有描述的细节:
- 在任何更新渠道上更改werf版本之后, 应自动更新站点上的文档 。
- 为了进行开发,您有时需要能够预览站点的初步版本 。
必须从相应的Git标签更改任何通道上的版本后,才可以重新编译站点,但是在构建映像的过程中,我们将获得以下功能:
- 随着通道上版本列表的更改,仅需要重新组装版本已更改的通道的文档。 毕竟,重新组装一切并不是一件很漂亮的事情。
- 发布渠道的集合可能会有所不同。 例如,在某个时间点上,通道上的版本可能不会比早期访问的1.1版本更稳定,但是随着时间的流逝它们会出现-在这种情况下不手动更改组件吗?
事实证明,
程序集依赖于更改外部数据 。
实作
方法的选择
或者,您可以在Kubernetes中使用单独的pod运行每个所需的版本。 此选项意味着群集中的对象数量将增加,并且稳定的werf版本数量将增加。 这反过来又意味着更复杂的服务:每个版本都有自己的HTTP服务器,并且负载较小。 当然,这需要较高的资源成本。
我们沿着将
所有必要版本组装到一张图像的路径走了。 站点的所有版本的已编译静态信息均包含在具有NGINX的容器中,并且到达相应部署的流量通过NGINX Ingress进行。 一个简单的结构-一个无状态的应用程序-使得使用Kubernetes本身很容易扩展部署(取决于负载)。
更准确地说,我们收集了两个图像:一个用于生产电路,另一个用于开发电路。 仅在开发电路上与主映像一起使用(启动)附加映像,该映像包含来自审阅提交的站点版本,并且它们之间的路由使用Ingress资源执行。
werf vs git克隆和工件
如前所述,为了为特定版本的文档生成站点静态信息,您需要通过切换到相应的存储库标签进行构建。 通过在组装期间每次克隆存储库,从列表中选择适当的标签,也可以做到这一点。 但是,这是一个相当消耗资源的操作,而且,需要编写不平凡的指令...另一个严重的缺点-使用这种方法,无法在汇编过程中缓存某些内容。
werf实用程序在此为我们提供了帮助,该实用程序实现了
智能缓存并允许使用
外部存储库 。 使用werf从存储库添加代码将大大加快构建速度,因为 werf本质上
只进行一次存储库克隆,然后
仅在必要时进行
fetch
。 另外,从存储库添加数据时,我们只能选择必要的目录(在本例中为
docs
目录),这将大大减少添加的数据量。
由于Jekyll是一种用于编译静态函数的工具,因此在最终映像中不需要此工具,因此在
werf工件中进行编译并将
仅将编译结果导入最终映像是合乎逻辑的。
编写werf.yaml
因此,我们决定将在单独的werf工件中编译每个版本。 但是,我们
不知道在组装过程中会有多少工件 ,因此我们无法编写固定的组装配置(严格来说,我们仍然可以,但不会完全有效)。
werf允许您在配置文件(
werf.yaml
)中使用
Go-templates ,这使得可以根据外部数据(需要!)动态
生成配置 。 在我们的案例中,外部数据是有关版本和发行版的信息,在此基础上,我们收集了必要数量的工件,因此,我们获得了两个图像:
werf-doc
和
werf-dev
,它们在不同的路径上运行。
外部数据通过环境变量传递。 这是它们的组成:
- RELEASES-包含发行版列表和对应的werf当前版本的行,以列表的形式,以空格分隔,格式为
<_>%<_>
。 示例: 1.0%v1.0.4-beta.20
CHANNELS
-包含频道列表和对应的werf当前版本的行,其形式为列表,格式为<>%<_>
。 示例: 1.0-beta%v1.0.4-beta.20 1.0-alpha%v1.0.5-alpha.22
ROOT_VERSION
默认情况下在站点上显示的werf版本的版本(不一定总是需要显示最高版本号的文档)。 示例: v1.0.4-beta.20
REVIEW_SHA
您需要从中收集测试循环版本的审阅提交的哈希值。
这些变量将在管道GitLab CI中填充,并在下面进行详细描述。
首先,为方便起见,我们
werf.yaml
为环境变量分配值来在
werf.yaml
定义Go-template变量:
{{ $_ := set . "WerfVersions" (cat (env "CHANNELS") (env "RELEASES") | splitList " ") }} {{ $Root := . }} {{ $_ := set . "WerfRootVersion" (env "ROOT_VERSION") }} {{ $_ := set . "WerfReviewCommit" (env "REVIEW_SHA") }}
对于我们需要的所有情况(包括根版本的生成以及dev电路的版本),用于编译站点版本的静态信息的工件的描述通常是相同的。 因此,我们将使用
define
函数将其放置在单独的块中-以便随后通过
include
进行重用。 我们将以下参数传递给模板:
Version
-生成的版本(标签名称);Channel
为其生成工件的更新通道的名称;Commit
-如果为审核提交生成了工件,则提交哈希;- 上下文。
工件模板说明 {{- define "doc_artifact" -}} {{- $Root := index . "Root" -}} artifact: doc-{{ .Channel }} from: jekyll/builder:3 mount: - from: build_dir to: /usr/local/bundle ansible: install: - shell: | export PATH=/usr/jekyll/bin/:$PATH - name: "Install Dependencies" shell: bundle install args: executable: /bin/bash chdir: /app/docs beforeSetup: {{- if .Commit }} - shell: echo "Review SHA - {{ .Commit }}." {{- end }} {{- if eq .Channel "root" }} - name: "releases.yml HASH: {{ $Root.Files.Get "releases.yml" | sha256sum }}" copy: content: | {{ $Root.Files.Get "releases.yml" | indent 8 }} dest: /app/docs/_data/releases.yml {{- else }} - file: path: /app/docs/_data/releases.yml state: touch {{- end }} - file: path: "{{`{{ item }}`}}" state: directory mode: 0777 with_items: - /app/main_site/ - /app/ru_site/ - file: dest: /app/docs/pages_ru/cli state: link src: /app/docs/pages/cli - shell: | echo -e "werfVersion: {{ .Version }}\nwerfChannel: {{ .Channel }}" > /tmp/_config_additional.yml export PATH=/usr/jekyll/bin/:$PATH {{- if and (ne .Version "review") (ne .Channel "root") }} {{- $_ := set . "BaseURL" ( printf "v%s" .Channel ) }} {{- else if ne .Channel "root" }} {{- $_ := set . "BaseURL" .Channel }} {{- end }} jekyll build -s /app/docs -d /app/_main_site/{{ if .BaseURL }} --baseurl /{{ .BaseURL }}{{ end }} --config /app/docs/_config.yml,/tmp/_config_additional.yml jekyll build -s /app/docs -d /app/_ru_site/{{ if .BaseURL }} --baseurl /{{ .BaseURL }}{{ end }} --config /app/docs/_config.yml,/app/docs/_config_ru.yml,/tmp/_config_additional.yml args: executable: /bin/bash chdir: /app/docs git: - url: https:
工件的名称必须唯一。 例如,我们可以通过将通道名称(
.Channel
变量的值)作为后缀添加到工件名称中来实现此目标:
artifact: doc-{{ .Channel }}
。 但是您需要了解,从工件导入时,需要引用相同的名称。
描述工件时,
会使用类似
mount的werf功能。 通过使用
build_dir
服务目录进行挂载,可以在管道启动之间保存Jekyll缓存,这
大大加快了重建速度 。
您可能还注意到了
releases.yml
文件的使用-这是YAML文件,其中包含从
github.com请求的发布数据(通过执行管道获得的工件)。 编译站点时需要它,但是在本文的上下文中,我们感兴趣的事实是,
只有一个工件 (站点版本根
工件 )依赖于其状态(在其他工件中则不需要)。
这是使用条件操作符(用于
{{ $Root.Files.Get "releases.yml" | sha256sum }}
go模板)和
{{ $Root.Files.Get "releases.yml" | sha256sum }}
在舞台
上 。 它的工作方式如下:为根版本(
.Channel
变量为
root
)组装工件时,
releases.yml
文件的哈希会影响整个阶段的签名,因为它是Ansible作业
name
(
name
参数)的组成部分。 因此,当更改
releases.yml
文件的
内容时 ,将重建相应的工件。
还应注意使用外部存储库。 仅将
/docs
目录从
werf存储库添加到工件
的映像 ,并且根据传递的参数,立即添加必要标签或审阅提交的数据。
为了使用工件模板来生成通道和发行版的已传输版本的工件描述,我们在
werf.yaml
的变量
.WerfVersions
上组织了一个循环:
{{ range .WerfVersions -}} {{ $VersionsDict := splitn "%" 2 . -}} {{ dict "Version" $VersionsDict._1 "Channel" $VersionsDict._0 "Root" $Root | include "doc_artifact" }} --- {{ end -}}
因为 循环将生成多个工件(我们希望如此),有必要考虑它们之间的分隔符-序列
---
(有关配置文件的语法的更多信息,请参见
文档 )。 如前所述,当您循环调用模板时,我们传递版本参数,URL和根上下文。
同样,但已经没有循环,我们将工件模板称为“特殊情况”:用于根版本以及来自审阅提交的版本:
{{ dict "Version" .WerfRootVersion "Channel" "root" "Root" $Root | include "doc_artifact" }} --- {{- if .WerfReviewCommit }} {{ dict "Version" "review" "Channel" "review" "Commit" .WerfReviewCommit "Root" $Root | include "doc_artifact" }} {{- end }}
请注意,仅当
.WerfReviewCommit
了
.WerfReviewCommit
变量时,才会收集审阅提交的工件。
工件已准备就绪,该导入了!
最终的映像设计为在Kubernetes中运行,是常规的NGINX,在其中添加了
nginx.conf
服务器配置文件和来自工件的静态信息。 除了网站根版本的工件外,我们还需要在变量
.WerfVersions
上重复循环,以导入通道和发布版本的工件,并遵守我们先前采用的工件命名规则。 由于每个工件都以两种语言存储站点的版本,因此我们将它们导入到配置提供的位置。
最终werf-doc图像的描述 image: werf-doc from: nginx:stable-alpine ansible: setup: - name: "Setup /etc/nginx/nginx.conf" copy: content: | {{ .Files.Get ".werf/nginx.conf" | indent 8 }} dest: /etc/nginx/nginx.conf - file: path: "{{`{{ item }}`}}" state: directory mode: 0777 with_items: - /app/main_site/assets - /app/ru_site/assets import: - artifact: doc-root add: /app/_main_site to: /app/main_site before: setup - artifact: doc-root add: /app/_ru_site to: /app/ru_site before: setup {{ range .WerfVersions -}} {{ $VersionsDict := splitn "%" 2 . -}} {{ $Channel := $VersionsDict._0 -}} {{ $Version := $VersionsDict._1 -}} - artifact: doc-{{ $Channel }} add: /app/_main_site to: /app/main_site/v{{ $Channel }} before: setup {{ end -}} {{ range .WerfVersions -}} {{ $VersionsDict := splitn "%" 2 . -}} {{ $Channel := $VersionsDict._0 -}} {{ $Version := $VersionsDict._1 -}} - artifact: doc-{{ $Channel }} add: /app/_ru_site to: /app/ru_site/v{{ $Channel }} before: setup {{ end -}}
附加映像与主映像一起在dev电路上启动,仅包含站点的两个版本:来自审阅提交的版本和站点的根版本(有常规资产,并且,如果您还记得,还可以发布数据)。 因此,来自主图像的附加图像只会在导入部分有所不同(当然,名称也是如此):
image: werf-dev ... import: - artifact: doc-root add: /app/_main_site to: /app/main_site before: setup - artifact: doc-root add: /app/_ru_site to: /app/ru_site before: setup {{- if .WerfReviewCommit }} - artifact: doc-review add: /app/_main_site to: /app/main_site/review before: setup - artifact: doc-review add: /app/_ru_site to: /app/ru_site/review before: setup {{- end }}
如上所述,仅当werf以环境变量
REVIEW_SHA
开头时,才会生成审阅提交的工件。 如果没有
REVIEW_SHA
环境
REVIEW_SHA
,则可能根本不生成werf-dev映像,但是为了使
基于 werf-dev的Docker映像
策略清理适用于werf-dev映像,我们将其仅与根版本工件一起收集(无论如何,已组装),以简化管道的结构。
组装就绪! 我们传递给CI / CD和重要的细微差别。
GitLab CI中的管道和动态装配的功能
启动程序集时,我们需要设置
werf.yaml
使用的环境变量。 这不适用于REVIEW_SHA变量,当从GitHub挂钩调用管道时,我们将设置该变量。
我们将在
generate_artifacts
Bash脚本中生成必要的外部数据,该脚本将生成两个管道GitLab工件:
- 带有发布数据的
releases.yml
文件, - 包含要导出的环境变量的文件
common_envs.sh
。
您可以在
示例存储库中找到
generate_artifacts
文件的内容。 获取数据不是本文的主题,但是
common_envs.sh
文件对我们很重要,因为 werf的工作取决于此。 其内容的示例:
export RELEASES='1.0%v1.0.6-4' export CHANNELS='1.0-alpha%v1.0.7-1 1.0-beta%v1.0.7-1 1.0-ea%v1.0.6-4 1.0-stable%v1.0.6-4 1.0-rock-solid%v1.0.6-4' export ROOT_VERSION='v1.0.6-4'
您可以使用此类脚本的输出,例如,使用
source
Bash函数。
现在是有趣的部分。 为了使构建和部署应用程序都能正常运行,必须使
werf.yaml
对于至少
一个管道 werf.yaml
相同 。 如果不满足此条件,则werf在组装期间(例如,部署期间)计算的阶段的签名将不同。 这将导致部署错误,因为 部署所需的映像将不存在。
换句话说,如果在站点映像的组装期间有关发行版和版本的信息是一个,并且在发行时发行了新版本且环境变量具有不同的值,则部署将失败并显示错误:尚未收集新版本的工件。
如果
werf.yaml
的生成取决于外部数据(例如,在我们的示例中为当前版本的列表),则应在管道内记录此类数据的组成和值。 如果外部参数经常更改,这一点尤其重要。
我们将在GitLab(
预构建 )的流水线的第一阶段
接收和捕获外部数据 ,并将其作为
GitLab CI工件进一步传输。 这将允许您使用
werf.yaml
的相同配置来启动和重新启动管道任务(构建,部署,清理)。
.gitlab -
ci.yml文件的
Prebuild阶段的内容:
Prebuild: stage: prebuild script: - bash ./generate_artifacts 1> common_envs.sh - cat ./common_envs.sh artifacts: paths: - releases.yml - common_envs.sh expire_in: 2 week
通过在工件中捕获外部数据,您可以使用标准的GitLab CI管道阶段来构建和部署:构建和部署。 我们通过gerHub存储库werf的钩子启动管道(即在GitHub上更改存储库时)。 可以在
CI / CD设置->管道触发器部分的GitLab项目的属性中获取它们的数据,然后在GitHub中创建相应的Webhook(
设置-> Webhooks )。
构建阶段将如下所示:
Build: stage: build script: - type multiwerf && . $(multiwerf use 1.0 alpha --as-file) - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose) - source common_envs.sh - werf build-and-publish --stages-storage :local except: refs: - schedules dependencies: - Prebuild
GitLab将在
Prebuild阶段向构建阶段添加两个工件,因此我们使用
source common_envs.sh
导出具有准备好的输入的变量。 除了按计划启动管道外,我们在所有情况下都开始组装阶段。 根据时间表,将启动管道进行清洁-在这种情况下,我们不需要建造。
在部署阶段,我们描述两个任务-使用YAML模板分别部署到生产和开发电路:
.base_deploy: &base_deploy stage: deploy script: - type multiwerf && . $(multiwerf use 1.0 alpha --as-file) - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose) - source common_envs.sh - werf deploy --stages-storage :local dependencies: - Prebuild except: refs: - schedules Deploy to Production: <<: *base_deploy variables: WERF_KUBE_CONTEXT: prod environment: name: production url: werf.io only: refs: - master except: variables: - $REVIEW_SHA refs: - schedules Deploy to Test: <<: *base_deploy variables: WERF_KUBE_CONTEXT: dev environment: name: test url: werf.test.flant.com except: refs: - schedules only: variables: - $REVIEW_SHA
本质上,任务的区别仅在于指示werf应在其上执行部署的群集的上下文(
WERF_KUBE_CONTEXT
)并设置轮廓的环境变量(
environment.name
和
environment.url
),然后在Helm图表模板中使用它们。 不会提供模板的内容,因为 这个主题没有什么有趣的,但是您可以在
文章存储库中找到它们。
最后一点
由于werf版本发布的频率很高,因此通常会收集新映像,并且Docker Registry将不断增长。 因此,有必要配置按策略自动清除图像。 这很容易做到。
为了实现,您将需要:
- 在
.gitlab-ci.yml
添加一个纯化步骤; - 添加定期清理任务;
- 使用写访问令牌设置环境变量。
将清理阶段添加到
.gitlab-ci.yml
:
Cleanup: stage: cleanup script: - type multiwerf && . $(multiwerf use 1.0 alpha --as-file) - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose) - source common_envs.sh - docker login -u nobody -p ${WERF_IMAGES_CLEANUP_PASSWORD} ${WERF_IMAGES_REPO} - werf cleanup --stages-storage :local only: refs: - schedules
几乎我们所有人都已经看到了更高的级别-仅在清洁时,您需要首先使用具有删除Docker Registry中映像的权限的令牌登录Docker Registry(自动发行的GitLab CI任务令牌没有此类权限)。 令牌必须事先输入到GitLab中,并且其值必须在项目的环境变量
WERF_IMAGES_CLEANUP_PASSWORD
指定
(CI / CD设置->变量) 。
在
CI / CD中完成具有必要时间表的清洁任务
->
时间表就是这样:Docker Registry中的项目将不再因未使用的映像而不断增长。
在实践部分的结尾,我记得在
Git中可以找到本文的完整清单:
结果
- 我们得到了一个合理的构建结构:每个版本一个工件。
- 程序集是通用的,发布新版本的werf时不需要手动更改:站点上的文档会自动更新。
- 针对不同的轮廓收集了两个图像。
- 它工作很快,因为 最大限度地利用了缓存-当发布新版本的werf或调用GitHub钩子进行审阅提交时,仅会重建具有修改版本的相应工件。
- 无需考虑删除未使用的映像:werf策略清理将维护Docker Registry中的顺序。
结论
- 使用werf可以使程序集快速运行,这要归功于程序集本身的缓存和使用外部存储库时的缓存。
- 使用外部Git存储库无需每次都完全克隆存储库,也无需使用棘手的优化逻辑重新发明轮子。werf使用缓存并且仅克隆一次,然后
fetch
仅在必要时使用它。 - 在程序集配置文件中使用Go模板的
werf.yaml
功能使您能够描述程序集,其结果取决于外部数据。 - 在werf中使用挂载可显着加快工件的收集速度,这是由于高速缓存是所有管道所共有的。
- werf使清洁变得容易,对于动态构建尤其如此。
聚苯乙烯
另请参阅我们的博客: