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

我们已经不止一次讨论过我们的 GitOps 工具。 韦尔夫,这次我们想分享一下我们在组装网站和项目本身的文档方面的经验—— werf.io (其俄语版本是 en.werf.io)。 这是一个普通的静态站点,但其组装很有趣,因为它是使用动态数量的工件构建的。

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

深入了解站点结构的细微差别:为所有版本生成通用菜单、包含有关版本信息的页面等。 - 我们不会。 相反,让我们重点关注动态组装的问题和功能,以及相关的 CI/CD 流程。

简介:网站如何运作

首先,werf 文档与其代码一起存储。 这提出了某些开发要求,这些要求通常超出了本文的范围,但至少可以说:

  • 在未更新文档的情况下不应发布新的 werf 功能,相反,文档中的任何更改都意味着新版本 werf 的发布;
  • 该项目的开发相当密集:一天可以发布多次新版本;
  • 部署具有新版本文档的站点的任何手动操作至少都是乏味的;
  • 该项目采用语义方法 版本控制,具有5个稳定通道。 发布过程涉及版本通过渠道的顺序传递,以提高稳定性:从 alpha 到坚如磐石;
  • 该网站有一个俄语版本,它与主要版本(即英语版本)同时“生存和发展”(即内容更新)。

为了向用户隐藏所有这些“内部厨房”,为他提供“正常工作”的东西,我们做了 单独的werf安装和更新工具 - 多工器。 您只需要指定版本号和您准备使用的稳定性通道,multiwerf 将检查通道上是否有新版本,并在必要时下载。

在网站的版本选择菜单中,每个频道都提供了werf的最新版本。 默认情况下,按地址 werf.io/文档 最新版本最稳定的渠道版本打开 - 它也被搜索引擎索引。 该通道的文档可在不同的地址获取(例如, werf.io/v1.0-beta/文档 对于测试版 1.0)。

该网站总共提供以下版本:

  1. root(默认打开),
  2. 对于每个版本的每个活动更新通道(例如, werf.io/v1.0-beta).

要生成站点的特定版本,一般来说,使用以下命令编译它就足够了 杰奇通过在目录中运行 /docs werf存储库对应的命令(jekyll build),切换到所需版本的Git标签后。

只需要补充一点:

  • 实用程序本身(werf)用于组装;
  • CI/CD流程是基于GitLab CI基础构建的;
  • 当然,这一切都在 Kubernetes 中运行。

任务

现在让我们考虑所有描述的细节来制定任务:

  1. 在任何更新通道上更改 werf 版本后 网站上的文档应该自动更新.
  2. 为了发展,有时你需要能够 查看网站的预览版本.

在从相应的 Git 标签更改任何频道上的版本后,必须重新编译该站点,但在构建映像的过程中,我们将获得以下功能:

  • 由于频道上的版本列表发生变化,因此只需重建版本发生变化的频道的文档即可。 毕竟,一切都重新重建,不太好。
  • 发布渠道集可能会发生变化。 例如,在某个时间点,渠道上可能没有比早期访问 1.1 版本更稳定的版本,但随着时间的推移它们会出现 - 在这种情况下,您不应该手动更改程序集吗?

事实证明, 装配依赖于改变外部数据.

履行

选择一种方法

或者,您可以在 Kubernetes 中将每个所需版本作为单独的 pod 运行。 此选项意味着集群中的对象数量较多,该数量将随着稳定版 werf 版本数量的增加而增长。 而这反过来又意味着更复杂的维护:每个版本都有自己的 HTTP 服务器,并且负载很小。 当然,这也意味着更大的资源成本。

我们走的是同一条路 将所有必要的版本组装在一张图像中。 站点所有版本的编译静态数据都位于带有 NGINX 的容器中,相应 Deployment 的流量来自 NGINX Ingress。 简单的结构 - 无状态应用程序 - 允许您使用 Kubernetes 本身轻松扩展部署(取决于负载)。

更准确地说,我们正在收集两张图像:一张用于生产电路,第二张是用于开发电路的附加图像。 附加映像仅在开发电路上与主映像一起使用(启动),并包含审查提交中的站点版本,并且它们之间的路由是使用 Ingress 资源执行的。

werf 与 git 克隆和工件

如前所述,为了生成特定版本文档的站点静态信息,您需要通过切换到适当的存储库标签来构建。 您还可以通过在每次构建时克隆存储库,从列表中选择适当的标签来完成此操作。 然而,这是一个相当资源密集型的操作,而且需要编写不平凡的指令...另一个严重的缺点是,使用这种方法无法在汇编期间缓存某些内容。

这里werf实用程序本身来帮助我们,实现 智能缓存 并允许您使用 外部存储库。 使用 werf 从存储库添加代码将显着加快构建速度,因为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 文件,其中包含请求的发布数据 github.com (执行管道时获得的工件)。 编译网站时需要它,但在本文的上下文中,我们对此很感兴趣,因为它取决于其状态 仅重新组装一件工件 — 站点根版本的工件(其他工件不需要)。

这是使用条件语句实现的 if Go 模板和设计 {{ $Root.Files.Get "releases.yml" | sha256sum }} 在阶段 阶段。 它的工作原理如下:当为根版本构建工件时(变量 .Channelroot) 文件哈希值 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 -}}

因为循环将生成几个工件(我们希望如此),有必要考虑它们之间的分隔符 - 序列 --- (有关配置文件语法的更多信息,请参见 文件资料)。 正如前面所定义的,当在循环中调用模板时,我们会传递版本参数、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.

工件已准备就绪 - 是时候开始导入了!

最终的镜像设计为在 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,但为了 按政策清洁 werf 中的 Docker 镜像适用于 werf-dev 镜像,我们将仅使用根版本工件构建它(无论如何它已经构建),以简化管道结构。

装配准备就绪! 让我们继续讨论 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 注册表中的项目将不再不断地从未使用的映像中增长。

在实践部分结束时,让我提醒您,本文中的完整列表可在 混帐:

导致

  1. 我们收到了一个逻辑组装结构:每个版本一个工件。
  2. 该组件是通用的,在发布新版本的 werf 时不需要手动更改:网站上的文档会自动更新。
  3. 组合两个图像以获得不同的轮廓。
  4. 它见效很快,因为尽可能多地使用缓存 - 当发布新版本的 werf 或调用 GitHub hook 进行审查提交时,仅重建具有更改版本的相应工件。
  5. 无需考虑删除未使用的镜像:根据 werf 策略进行清理将使 Docker 注册表保持有序。

发现

  • 由于程序集本身的缓存以及使用外部存储库时的缓存,使用 werf 允许程序集快速工作。
  • 使用外部 Git 存储库消除了每次克隆整个存储库或使用棘手的优化逻辑重新发明轮子的需要。 werf 使用缓存并仅进行一次克隆,然后使用 fetch 并且仅在必要时。
  • 能够在构建配置文件中使用 Go 模板 werf.yaml 允许您描述其结果取决于外部数据的程序集。
  • 在 werf 中使用 mount 可以显着加快工件的收集速度 - 由于缓存是所有管道所共有的。
  • werf 可以轻松配置清理,这在动态构建时尤其重要。

PS

另请阅读我们的博客:

来源: habr.com

添加评论