使用程式碼倉庫管理 GitLab CI 變數

  • 2019 年 10 月 10 日
  • 筆記

本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或重新修改使用,但需要註明來源。 署名 4.0 國際 (CC BY 4.0)

本文作者: 蘇洋

創建時間: 2019年07月27日 統計字數: 6560字 閱讀時間: 14分鐘閱讀 本文鏈接: https://soulteary.com/2019/07/27/use-the-code-repository-to-manage-gitlab-ci-variables.html

使用程式碼倉庫管理 GitLab CI 變數

隨著越來越多的項目用上了自動化構建,我們不得不在項目中一遍遍的配置持續集成中使用的環境變數,十幾個項目規模還好說,但是項目成百上千後,維護不同項目/不同項目分組變數的工作量也變的大了起來。

在大公司中,如果有團隊維護基礎技術設施,我們可以使用類似可配置的構建平台/應用配置中心等方案來解決這個問題。但是這類方案對於中小規模的團隊或者個人開發者來說卻不是那麼友好、甚至可以說投入成本過高。

本文將介紹如何使用程式碼倉庫管理項目/項目組變數,低成本解決項目在CI/CD過程中環境變數維護的問題。

寫在前面

使用程式碼倉庫管理應用文件配置你一定聽說過或者用過,但是使用程式碼倉庫管理環境變數,你或許就不一定用過了。

在聊具體方案之前,我們先了解下這兩種配置的異同。它們的共同點是,都儲存了項目構建/運行所需要的必要資訊。那麼他們主要的不同點是什麼呢?

  • 項目 CI/CD 變數:存放於 GitLab 項目/項目組設置頁面中變數配置中的欄位、在 CI/CD 過程中使用。
  • 項目配置文件:使用某種具體格式書寫,存放於項目倉庫某個位置,例如: ./config/app.json 或者 ./app.ini等。在項目運行後使用。

簡單來說就是:存放位置不同、使用時機不同。

我們都知道顯式聲明(Explicit declaration)對於維護性的利好,那麼如果我們能夠把變數也使用配置的方式來管理維護,問題就解決啦,比如像下面這樣使用:

  1. 讀取存放在文件中的變數資訊
  2. 解析每一條配置
  3. 寫入 GitLab CI 變數配置

依賴條件

官方文檔 中有提到 Group-levelVariablesAPI,可以對項目組的變數進行「CRUD」。(操作 Project-level Variables 同理)

使用有項目訪問許可權的帳號,打開 https://gitlab.domain.com/profile/personal_access_tokens ,勾選 APIread_repository 許可權,然後生成一枚類似 x6oeuvvfsoultearyZ2o 的 Access Token。

有了這枚 Token ,我們就能模擬用戶對 GitLab 進行變數配置操作了。

編寫程式

相比較官方實例中的 Bash 語句, Node.js 等高級語言編寫的腳本能在完成相同事情的時候,行數更短,比如下面的60來行程式可以解決這個問題:根據配置文件中定義的變數內容,設置多個項目或項目組、以及指定ID的項目或者項目組的變數配置。

const https = require('https');  const axios = require('axios');  const instance = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false }) });    /**   * settings @see `setting.example.json`   */  const settings = require('./settings.json');  const { baseHost, token, groupIds, projectIds, groupVars, projectVars } = settings;  const options = { headers: { 'PRIVATE-TOKEN': token } };    function combine(varList) {      if (!varList) return {};      const { privateVars, publicVars } = varList;      return Object.keys(publicVars).map((label) => { return { label, value: publicVars[label], protected: false } })          .concat(Object.keys(privateVars).map((label) => { return { label, value: privateVars[label], protected: true } }))          .reduce((r, i) => { r[i.label] = i; return r; }, {});  }    function update(itemIds, varsData, type) {      type = type.slice(-1) === 's' ? type.slice(0, -1) : type;      const apiType = `${type}s`;      itemIds.forEach(async (itemId) => {          try {              var { data: variablesExists } = await instance.get(`${baseHost}/${apiType}/${itemId}/variables`, options);          } catch (error) {              return console.log(error);          }            const variablesKeyExists = variablesExists.map((variable) => variable.key);          const newVariableKeys = Object.keys(varsData).filter((key) => !variablesKeyExists.includes(key));            variablesKeyExists.forEach(async (key) => {              if (!varsData[key]) return;              const { value, protected } = varsData[key];              try {                  await instance.put(`${baseHost}/${apiType}/${itemId}/variables/${key}`, `value=${value}&protected=${protected}`, options);              } catch (error) {                  return console.log(error);              }              console.log(`Update #${itemId} ${type}: [${key}]`);          });            newVariableKeys.forEach(async (key) => {              if (!varsData[key]) return;              const { value, protected } = varsData[key];              try {                  await instance.post(`${baseHost}/${apiType}/${itemId}/variables`, `key=${key}&value=${value}&protected=${protected}`, options);              } catch (error) {                  return console.log(error);              }              console.log(`Create #${itemId} ${apiType}: [${key}]`);          });            if (settings[`${type}Vars:${itemId}`]) {              const itemData = combine(settings[`${type}Vars:${itemId}`]);              delete settings[`${type}Vars:${itemId}`];              update([itemId], itemData, type);          }      });  }    const groupsVarsList = combine(groupVars);  const projectVarsList = combine(projectVars);    update(groupIds, groupsVarsList, 'group');  update(projectIds, projectVarsList, 'project');

想要使用這段腳本,需要創建一個配置文件 setting.json,將上面獲取的 Token、你的 GitLab 倉庫地址,以及你想配置的項目或項目組 id 放進去,一個相對完整的例子是下面這樣。

{      "baseHost": "https://gitlab.lab.com/api/v4",      "token": "H7hHmdCnryy7UCeFB7tH",      "groupIds": [          5      ],      "groupVars": {          "publicVars": {              "VAR_PUB": 1024          },          "privateVars": {              "VAR_HIDE": 1024,              "VAR_HIDE2": 1024          }      },      "projectIds": [          774,          775      ],      "projectVars": {          "publicVars": {              "VAR_PUB": 1024          },          "privateVars": {              "VAR_HIDE": 1024,              "VAR_HIDE2": 1024          }      },      "projectVars:775": {          "publicVars": {              "VAR_775_PUB": 2048          },          "privateVars": {              "VAR_775_HIDE": 2048,              "VAR_775_HIDE2": 2048          }      }  }

這裡除了 baseHosttoken 欄位外,其他的配置都是選配的,包括二級欄位 publicVarsprivateVars,所以如果你只是想配置兩個項目組,的公開變數,配置會簡短不少。

{      "baseHost": "https://gitlab.lab.com/api/v4",      "token": "H7hHmdCnryy7UCeFB7tH",      "groupIds": [          5, 6      ],      "groupVars": {          "publicVars": {              "VAR_PUB": 1024          }      }  }

考慮到不是每個同學都熟悉 JavaScript,我這裡把它封裝成了容器鏡像。

構建工具容器鏡像

鏡像文件十分簡單,基於 Node 官方鏡像,10 行以內指令解決問題。

FROM node:12.7.0-alpine    LABEL MAINTAINER="soulteary"    COPY ./src /app    WORKDIR /app    RUN npm i --production    VOLUME [ "/app/settings.json" ]    ENTRYPOINT [ "npm", "start" ]

將上面的內容保存為 Dockerfile ,接著使用 docker build 構建一個我們使用的工具鏡像:

docker build -t soulteary/gitlab-variable-helper .

當然,你也可以直接使用我在 DockerHub 上提供的公開鏡像:soulteary/gitlab-variable-helper 。

如何使用

在準備好你的配置文件 settings.json 後,你可以在本地環境或者伺服器、或是 GitLab Runner 中執行這個工具。

執行方法除了安裝好 Node.js 後執行 node.npm start 外,還可以選擇使用容器來執行:

docker run --rm -v `pwd`/settings.json:/app/settings.json soulteary/gitlab-variable-helper:1.0.0

當然,我更推薦的是使用 compose 文件進行容器執行,因為看起來會更加的清晰。

version: '3'    services:      updater:      image: docker.lab.com/gitlab-group-variable-update:1.0.0      volumes:       - ./config:/app/config

將上面的文件保存為 docker-compose.yml 後,我們可以再編寫一個 .gitlab-ci.yml ,讓變數配置變的「自動」起來:

stages:    - deploy    update:    stage: deploy    script:      - docker-compose down && docker-compose up      # 或者      # - docker run --rm -v `pwd`/settings.json:/app/settings.json soulteary/gitlab-variable-helper:1.0.0

如果你CI配置正確,每當你調整 settings.json內容,並使用 git push 將內容提交到 GitLab 後,都將會看到類似下面的日誌輸出。

docker-compose down && docker-compose up  Creating gitlab-group-update-test_updater_1 ... done  Attaching to gitlab-group-update-test_updater_1  updater_1  |  updater_1  | > [email protected] start /app  updater_1  | > node .  updater_1  |  updater_1  | Create #774 project: [VAR_PUB]  updater_1  | Update #775 project: [VAR_HIDE2]  updater_1  | Create #5 group: [VAR_PUB]  updater_1  | Update #5 group: [VAR_HIDE]  updater_1  | Create #775 project: [VAR_HIDE]  updater_1  | Create #774 project: [VAR_HIDE]  updater_1  | Update #5 group: [VAR_HIDE2]  updater_1  | Update #775 project: [VAR_PUB]  updater_1  | Update #774 project: [VAR_HIDE2]  updater_1  | Update #775 project: [VAR_775_HIDE2]  updater_1  | Update #775 project: [VAR_775_PUB]  updater_1  | Update #775 project: [VAR_775_HIDE]  gitlab-group-update-test_updater_1 exited with code 0

打開項目/項目組頁面,可以看到變數已經被成功設置完畢了。

完整項目,我已經提交到 GitHub 了:https://github.com/soulteary/gitlab-variable-helper,感興趣的同學可以自取。

最後

懶是程式設計師的美德,為了能變懶去折騰也是。

—EOF