我们已经不止一次讨论过我们的 GitOps 工具。
深入了解站点结构的细微差别:为所有版本生成通用菜单、包含有关版本信息的页面等。 - 我们不会。 相反,让我们重点关注动态组装的问题和功能,以及相关的 CI/CD 流程。
简介:网站如何运作
首先,werf 文档与其代码一起存储。 这提出了某些开发要求,这些要求通常超出了本文的范围,但至少可以说:
- 在未更新文档的情况下不应发布新的 werf 功能,相反,文档中的任何更改都意味着新版本 werf 的发布;
- 该项目的开发相当密集:一天可以发布多次新版本;
- 部署具有新版本文档的站点的任何手动操作至少都是乏味的;
- 该项目采用语义方法
版本控制 ,具有5个稳定通道。 发布过程涉及版本通过渠道的顺序传递,以提高稳定性:从 alpha 到坚如磐石; - 该网站有一个俄语版本,它与主要版本(即英语版本)同时“生存和发展”(即内容更新)。
为了向用户隐藏所有这些“内部厨房”,为他提供“正常工作”的东西,我们做了 单独的werf安装和更新工具 -
在网站的版本选择菜单中,每个频道都提供了werf的最新版本。 默认情况下,按地址
该网站总共提供以下版本:
- root(默认打开),
- 对于每个版本的每个活动更新通道(例如,
werf.io/v1.0-beta ).
要生成站点的特定版本,一般来说,使用以下命令编译它就足够了 /docs
werf存储库对应的命令(jekyll build
),切换到所需版本的Git标签后。
只需要补充一点:
- 实用程序本身(werf)用于组装;
- CI/CD流程是基于GitLab CI基础构建的;
- 当然,这一切都在 Kubernetes 中运行。
任务
现在让我们考虑所有描述的细节来制定任务:
- 在任何更新通道上更改 werf 版本后 网站上的文档应该自动更新.
- 为了发展,有时你需要能够 查看网站的预览版本.
在从相应的 Git 标签更改任何频道上的版本后,必须重新编译该站点,但在构建映像的过程中,我们将获得以下功能:
- 由于频道上的版本列表发生变化,因此只需重建版本发生变化的频道的文档即可。 毕竟,一切都重新重建,不太好。
- 发布渠道集可能会发生变化。 例如,在某个时间点,渠道上可能没有比早期访问 1.1 版本更稳定的版本,但随着时间的推移它们会出现 - 在这种情况下,您不应该手动更改程序集吗?
事实证明, 装配依赖于改变外部数据.
履行
选择一种方法
或者,您可以在 Kubernetes 中将每个所需版本作为单独的 pod 运行。 此选项意味着集群中的对象数量较多,该数量将随着稳定版 werf 版本数量的增加而增长。 而这反过来又意味着更复杂的维护:每个版本都有自己的 HTTP 服务器,并且负载很小。 当然,这也意味着更大的资源成本。
我们走的是同一条路 将所有必要的版本组装在一张图像中。 站点所有版本的编译静态数据都位于带有 NGINX 的容器中,相应 Deployment 的流量来自 NGINX Ingress。 简单的结构 - 无状态应用程序 - 允许您使用 Kubernetes 本身轻松扩展部署(取决于负载)。
更准确地说,我们正在收集两张图像:一张用于生产电路,第二张是用于开发电路的附加图像。 附加映像仅在开发电路上与主映像一起使用(启动),并包含审查提交中的站点版本,并且它们之间的路由是使用 Ingress 资源执行的。
werf 与 git 克隆和工件
如前所述,为了生成特定版本文档的站点静态信息,您需要通过切换到适当的存储库标签来构建。 您还可以通过在每次构建时克隆存储库,从列表中选择适当的标签来完成此操作。 然而,这是一个相当资源密集型的操作,而且需要编写不平凡的指令...另一个严重的缺点是,使用这种方法无法在汇编期间缓存某些内容。
这里werf实用程序本身来帮助我们,实现 智能缓存 并允许您使用 fetch
如果需要的话。 此外,当从存储库添加数据时,我们可以仅选择必要的目录(在我们的例子中,这是目录 docs
),这将显着减少添加的数据量。
由于 Jekyll 是一个专为编译静态数据而设计的工具,并且在最终映像中不需要,因此编译为合乎逻辑的
我们编写werf.yaml
因此,我们决定在单独的 werf 工件中编译每个版本。 然而我们 我们不知道组装过程中会有多少这样的工件,所以我们无法编写固定的构建配置(严格来说,我们仍然可以,但不会完全有效)。
werf 允许您使用 werf.yaml
),这使得它成为可能 即时生成配置 取决于外部数据(您需要什么!)。 在我们的案例中,外部数据是有关版本和发布的信息,在此基础上我们收集所需数量的工件,最终我们获得两个图像: 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
Go模板变量,从环境变量中为它们分配值:
{{ $_ := set . "WerfVersions" (cat (env "CHANNELS") (env "RELEASES") | splitList " ") }}
{{ $Root := . }}
{{ $_ := set . "WerfRootVersion" (env "ROOT_VERSION") }}
{{ $_ := set . "WerfReviewCommit" (env "REVIEW_SHA") }}
对于我们需要的所有情况(包括生成根版本以及开发电路的版本),用于编译站点静态版本的工件的描述通常是相同的。 因此,我们将使用该函数将其移动到一个单独的块中 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://github.com/flant/werf.git
to: /app/
owner: jekyll
group: jekyll
{{- if .Commit }}
commit: {{ .Commit }}
{{- else }}
tag: {{ .Version }}
{{- end }}
stageDependencies:
install: ['docs/Gemfile','docs/Gemfile.lock']
beforeSetup: '**/*'
includePaths: 'docs'
excludePaths: '**/*.sh'
{{- end }}
工件名称必须是唯一的。 例如,我们可以通过添加通道名称(变量的值)来实现这一点 .Channel
) 作为工件名称的后缀: artifact: doc-{{ .Channel }}
。 但您需要了解,从工件导入时,您将需要引用相同的名称。
描述工件时,使用以下 werf 特征: build_dir
允许您在管道运行之间保存 Jekyll 缓存,这 显着加快重新组装速度.
您可能还注意到该文件的用途 releases.yml
是一个 YAML 文件,其中包含请求的发布数据
这是使用条件语句实现的 if
Go 模板和设计 {{ $Root.Files.Get "releases.yml" | sha256sum }}
在阶段 .Channel
是 root
) 文件哈希值 releases.yml
影响整个阶段的签名,因为它是 Ansible 任务名称的一部分(参数 name
)。 因此,当改变 内容 文件 releases.yml
相应的工件将被重新组装。
另请注意使用外部存储库。 在一个人工制品的图像中 /docs
,并根据传递的参数,立即添加所需标签或评论提交的数据。
为了使用工件模板生成通道和发布的传输版本的工件的描述,我们在变量上组织了一个循环 .WerfVersions
в werf.yaml
:
{{ range .WerfVersions -}}
{{ $VersionsDict := splitn "%" 2 . -}}
{{ dict "Version" $VersionsDict._1 "Channel" $VersionsDict._0 "Root" $Root | include "doc_artifact" }}
---
{{ end -}}
因为循环将生成几个工件(我们希望如此),有必要考虑它们之间的分隔符 - 序列 ---
(有关配置文件语法的更多信息,请参见
类似地,但没有循环,我们将工件模板称为“特殊情况”:对于根版本,以及来自审查提交的版本:
{{ 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
.
工件已准备就绪 - 是时候开始导入了!
最终的镜像设计为在 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 -}}
附加映像与主映像一起在开发电路上启动,仅包含该站点的两个版本:来自审查提交的版本和该站点的根版本(有一般资产,如果您还记得的话) ,发布数据)。 因此,附加图像仅在导入部分(当然还有名称)与主图像不同:
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 }}
如上所述,仅当运行设置的环境变量时才会生成审查提交的工件 REVIEW_SHA
。 如果没有环境变量,有可能根本不生成 werf-dev 镜像 REVIEW_SHA
,但为了
装配准备就绪! 让我们继续讨论 CI/CD 和重要的细微差别。
GitLab CI 中的管道和动态构建的功能
运行构建时,我们需要设置使用的环境变量 werf.yaml
。 这不适用于 REVIEW_SHA 变量,我们将在从 GitHub 挂钩调用管道时设置该变量。
我们将在 Bash 脚本中生成必要的外部数据 generate_artifacts
,这将生成两个 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'
您可以使用此类脚本的输出,例如使用 Bash 函数 source
.
有趣的来了。 为了使应用程序的构建和部署都能正常工作,有必要确保 werf.yaml
是 相同 至少 在一条管道内。 如果不满足此条件,则 werf 在组装(例如部署)期间计算的阶段的签名将不同。 这将导致部署错误,因为...... 部署所需的映像将丢失。
换句话说,如果在站点映像的组装过程中有关发行版和版本的信息相同,而在部署时发布了新版本并且环境变量具有不同的值,则部署将失败并出现错误:毕竟新版本的神器还没有打造出来。
如果一代 werf.yaml
依赖于外部数据(例如,当前版本的列表,如我们的例子),那么这些数据的组成和值应该记录在管道内。 如果外部参数经常变化,这一点尤其重要。
我们会的 接收并记录外部数据 在 GitLab 管道的第一阶段 (预建)并以形式进一步传输 GitLab CI 神器。 这将允许您使用相同的配置运行和重新启动管道作业(构建、部署、清理) werf.yaml
.
舞台内容 预建 文件 .gitlab-ci.yml
:
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 管道阶段进行构建和部署:构建和部署。 我们使用 GitHub werf 存储库中的钩子启动管道本身(即,当 GitHub 上的存储库发生更改时)。 它们的数据可以在 部分的 GitLab 项目属性中找到 CI/CD 设置 -> 管道触发器,然后在GitHub中创建对应的Webhook(设置 -> Webhook).
构建阶段将如下所示:
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 会将两个工件从构建阶段添加到构建阶段 预建,因此我们使用构造函数导出带有准备好的输入数据的变量 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 注册表会不断增长。 因此,配置基于策略的自动镜像清理势在必行。 这很容易做到。
要实施您将需要:
- 添加清洁步骤
.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 注册表中映像的令牌登录到 Docker 注册表(自动颁发的 GitLab CI 任务令牌没有权限)拥有这样的权利)。 令牌必须提前在GitLab中创建,并且其值必须在环境变量中指定 WERF_IMAGES_CLEANUP_PASSWORD
项目 (CI/CD 设置 -> 变量).
添加具有所需计划的清洁任务是在 持续集成/持续交付 ->
附表.
就是这样:Docker 注册表中的项目将不再不断地从未使用的映像中增长。
在实践部分结束时,让我提醒您,本文中的完整列表可在
导致
- 我们收到了一个逻辑组装结构:每个版本一个工件。
- 该组件是通用的,在发布新版本的 werf 时不需要手动更改:网站上的文档会自动更新。
- 组合两个图像以获得不同的轮廓。
- 它见效很快,因为尽可能多地使用缓存 - 当发布新版本的 werf 或调用 GitHub hook 进行审查提交时,仅重建具有更改版本的相应工件。
- 无需考虑删除未使用的镜像:根据 werf 策略进行清理将使 Docker 注册表保持有序。
发现
- 由于程序集本身的缓存以及使用外部存储库时的缓存,使用 werf 允许程序集快速工作。
- 使用外部 Git 存储库消除了每次克隆整个存储库或使用棘手的优化逻辑重新发明轮子的需要。 werf 使用缓存并仅进行一次克隆,然后使用
fetch
并且仅在必要时。 - 能够在构建配置文件中使用 Go 模板
werf.yaml
允许您描述其结果取决于外部数据的程序集。 - 在 werf 中使用 mount 可以显着加快工件的收集速度 - 由于缓存是所有管道所共有的。
- werf 可以轻松配置清理,这在动态构建时尤其重要。
PS
另请阅读我们的博客:
- «
在向 Kubernetes 交付新应用程序版本时运行命令 “; - «
使用 werf 和 GitLab CI 构建和部署相同类型的微服务 “; - «
使用 werf 推出复杂的 Helm 图表 “; - «
介绍 werf 1.0 stable:GitOps 与它有什么关系、状态和计划 “。
来源: habr.com