使用版本化文件網站的範例,使用 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 }} 在階段 階段。它的工作原理如下:當為根版本建立工件時(變數 .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 -}}

因為循環將產生幾個工件(我們希望如此),有必要考慮它們之間的分隔符號 - 序列 --- (有關配置文件語法的更多信息,請參見 文件)。如同前面所定義的,當在循環中呼叫模板時,我們會傳遞版本參數、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 可以輕鬆配置清理,這在動態建置時尤其重要。

聚苯乙烯

另請閱讀我們的博客:

來源: www.habr.com

添加評論