使用版本化文档的示例站点使用werf动态组装和部署Docker映像

我们已经讨论了我们的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 )。

总计,该网站具有以下版本:

  1. 根(默认情况下打开)
  2. 每个发行版的每个活动更新通道(例如werf.io/v1.0-beta )。

要在一般情况下生成特定版本的站点,只需在切换到所需版本的Git标记后,在werf存储库的/docs目录中运行相应的命令( jekyll build ),即可使用Jekyll工具对其进行编译。

仅保留以下内容:

  • 实用程序本身(werf)用于组装;
  • CI / CD流程基于GitLab CI;
  • 当然,所有这些都可以在Kubernetes中使用。

任务


现在,我们在制定任务时要考虑到所有描述的细节:

  1. 在任何更新渠道上更改werf版本之后, 应自动更新站点上的文档
  2. 为了进行开发,您有时需要能够预览站点的初步版本

必须从相应的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-docwerf-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://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 }} 。 但是您需要了解,从工件导入时,需要引用相同的名称。

描述工件时, 会使用类似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作业namename参数)的组成部分。 因此,当更改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.nameenvironment.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中可以找到本文的完整清单:


结果


  1. 我们得到了一个合理的构建结构:每个版本一个工件。
  2. 程序集是通用的,发布新版本的werf时不需要手动更改:站点上的文档会自动更新。
  3. 针对不同的轮廓收集了两个图像。
  4. 它工作很快,因为 最大限度地利用了缓存-当发布新版本的werf或调用GitHub钩子进行审阅提交时,仅会重建具有修改版本的相应工件。
  5. 无需考虑删除未使用的映像:werf策略清理将维护Docker Registry中的顺序。

结论


  • 使用werf可以使程序集快速运行,这要归功于程序集本身的缓存和使用外部存储库时的缓存。
  • 使用外部Git存储库无需每次都完全克隆存储库,也无需使用棘手的优化逻辑重新发明轮子。werf使用缓存并且仅克隆一次,然后fetch仅在必要时使用它。
  • 在程序集配置文件中使用Go模板的werf.yaml功能使您能够描述程序集,其结果取决于外部数据。
  • 在werf中使用挂载可显着加快工件的收集速度,这是由于高速缓存是所有管道所共有的。
  • werf使清洁变得容易,对于动态构建尤其如此。

聚苯乙烯


另请参阅我们的博客:

Source: https://habr.com/ru/post/zh-CN478690/


All Articles