Pipeline流水線設計的最佳實踐

談到到DevOps,持續交付流水線是繞不開的一個話題,相對於其他實踐,通過流水線來實現快速高質量的交付價值是相對能快速見效的,特別對於開發測試人員,能夠獲得實實在在的收益。很多文章介紹流水線,不管是jenkins,gitlab-ci, 流水線,還是drone, github action 流水線, 文章都很多,但是不管什麼工具,流水線設計的思路是一致的。於此同時,在實踐過程中,發現大家對流水像有些誤區,不是一大堆流水線,就是一個流水線調一個超級複雜的腳本,各種硬編碼和環境依賴,所以希望通過這篇文章能夠給大家分享自己對於Pipeline流水線的設計心得體會。

概念

  1. 持續集成 (Continuous Integration,CI)

持續集成(CI)是在源代碼變更後自動檢測、拉取、構建和(在大多數情況下)進行單元測試的過程
對項目而言,持續集成(CI)的目標是確保開發人員新提交的變更是好的,不會發生break build; 並且最終的主幹分支一直處於可發佈的狀態,
對於開發人員而言,要求他們必須頻繁地向主幹提交代碼,相應也可以即時得到問題的反饋。實時獲取到相關錯誤的信息,以便快速地定位與解決問題
顯然這個過程可以大大地提高開發人員以及整個IT團隊的工作效率,避免陷入好幾天得不到好的“部署產出”,影響後續的測試和交付。
image.png

  1. 持續交付 (Continuous Delivery,CD)

持續交付在持續集成的基礎上,將集成後的代碼部署到更貼近真實運行環境的「預發佈環境」(production-like environments)中。交付給質量團隊或者用戶,以供評審。如果評審通過,代碼就進入生產階段 持續交付並不是指軟件每一個改動都要儘快部署到產品環境中,它指的是任何的代碼修改都可以在任何時候實時部署。
強調: 1、手動部署 2、有部署的能力,但不一定部署
image.png

  1. 持續部署 (Continuous Deployment, CD)

代碼通過評審之後,自動部署到生產環境中。持續部署是持續交付的最高階段。
強調 1、持續部署是自動的 2、持續部署是持續交付的最高階段 3、持續交付表示的是一種能力,持續部署則是一種方式
image.png

流水線的編排設計

參考: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html
這裏非常推薦以版本控制系統爲源的構建流水線設計,從每一位開發人員提交代碼即可對當前提交代碼進行檢查編譯構建,儘快將錯誤反饋給每位提交人員。
640.jpg
對於DevOps流水線,主要是由各類任務串聯起來,而對於任務本身又分爲兩張類型,一種是自動化任務,一種是人工執行任務。具體如下:

  1. 自動化任務:包括了代碼靜態檢查,構建,打包,部署,單元測試,環境遷移,自定義腳本運行等。
  2. 人工任務:人工任務主要包括了檢查審覈,打標籤基線,組件包製作等類似工作。

而通常我們看到的流水線基本都由上述兩類任務組合編排而成,一個流水線可以是完全自動化執行,也可以中間加入了人工干預節點,在人工干預處理後再繼續朝下執行。比如流水線中到了測試部署完成後,可以到測試環境人工驗證環節,只有人工驗證通過再流轉到遷移發佈到生產環境動作任務。
DevOps流水線實際上和我們原來經常談到的持續集成最佳實踐是相當類似的,較大的一個差異點就在於引入了容器化技術來實現自動化部署和應用託管。至於在DevOps實踐中,是否必須馬上將項目切換到微服務架構框架模式,反而不是必須得。

在整個DevOps流水線中,我們實際上強調個一個關鍵點在於“一套Docker鏡像文件+多套環境配置+多套構建版本標籤”做法。以確保我們最終構建和測試通過的版本就是我們部署到生產環境的版本。
構建操作只有一次,而後面到測試環境,到UAT環境,到生產環境,都屬於是鏡像的環境遷移和部署。而不涉及到需要再次重新打包的問題。這個是持續集成,也是DevOps的基本要求。

流水線任務的標準化/原子化

今天談DevOps流水線編排,主要是對流水線編排本身的靈活性進一步思考。

  • 構建操作:構建我們通常採用Maven進行自動化構建,構建完成輸出一個或多個Jar包或War包。

注意常規方式下構建完執行進行部署操作,部署操作一般就是將構建的結果拷貝到我們的測試環境服務器,同時對初始化腳本進行啓動等。而在DevOps下,該操作會變成兩個操作,即一個打包,一個部署。打包是將構建完成的內容製作爲鏡像,部署是將鏡像部署到具體的資源池和指定集羣。

  • 打包鏡像操作:實際上即基於構建完成的部署包來生成鏡像。該操作一般首先基於一個基礎鏡像文件基礎上進行,在基礎鏡像文件上拷貝和寫入具體的部署包文件,同時在啓動相應的初始化腳本。

那麼首先要考慮構建操作和打包操作如何松耦合開,打包操作簡單來就是就是一個鏡像製作,需要的是構建操作產生的輸出。我們可以對其輸出和需要拷貝的內容在構建的時候進行約定。而打包任務則是一個標準化的鏡像製作任務,我們需要考慮的僅僅是基於
1)基於哪個基礎鏡像
2)中間件容器默認目錄設置
3)初始化啓動命令。
即在實際的打包任務設計的時候,我們不會指定具體的部署包和部署文件,這個完全由編排的時候由上游輸入。

  • 部署操作:部署操作相當更加簡單,重點就是將鏡像部署到哪個資源池,哪個集羣節點,初始化的節點配置等。具體部署哪個鏡像不要指定,而是由上游任務節點輸入。

任務節點間松耦合設計的意義
這種松耦合設計才能夠使流水線編排更加靈活。比如我們在進行了構建打包後,我們希望同時講打包內容部署到開發環境和測試環境。那麼則是打包動作完成後需要對接兩個應用部署任務。這兩個部署任務都依託上面的打包結果進行自動化部署,可以並行進行。
對於測試環境部署完成後,我們需要進行測試人員手工驗證測試,如果測試通過,我們打標籤後希望能夠直接發佈到UAT環境。而這種操作我們也希望在一個流水線來設計和完成。這樣我們更加容易在持續集成看板上看到整個版本構建和遷移的完整過程。如果這是在一個大流水線裏面,那麼對於UAT環境部署任務就需要一直去追溯流水線上的最近的一個打包任務節點,同時取該任務節點產生的輸出來進行相應的環境部署操作。
在談DevOps的時候,一個重點就是和QA/QC的協同,因此在流水線編排的時候一定要考慮各類測試節點,包括靜態代碼檢查,自動化的單元測試,人工的測試驗證。同時最好基於持續集成實踐,能夠將測試過程和整個自動化構建過程緊密結合起來。
簡單來說,測試人員發現build1.0.0001版本4個bug並提交,那麼在下次自動化構建完成並單元測試通過後,測試人員能夠很清楚的看到哪些Bug已經修改並可以在新構建的版本進行驗證。只有這樣才能夠形成閉環,整個流水線作業才能夠更好的發揮協同作用。

流水線中蘊含的工程實踐

流水線除了任務步驟的編排,更重要的核心是最佳工程實踐的體現。過去傳統的思維,自動化就是寫個shell/python腳本批量執行,在DevOps/微服務時代,這一招太out了,每種工程實踐的背後都有需要解決的問題,通過在流水線設計中注入最佳的工程實踐,可以讓流水線的價值最大化,也讓流水線更高級不是嘛。

  1. 版本控制 - 解決的問題:需求和代碼的關係,版本變化的跟蹤
  2. 最優的分支策略 - 解決的問題:版本發佈和團隊協作,某些情況會和環境有關係
  3. 代碼靜態掃描 ** - 解決的問題: 開發規範和安全的問題**
  4. 80%以上的單元測試覆蓋率 ** - 解決的問題:代碼功能質量的問題,讓測試左移**
  5. 漏洞(Vulnerability)掃描 - 解決的問題:部署環境/產品安全的問題
  6. 開源工具掃描 ** - 解決的問題:解決供應鏈安全問題,別忘了log4j**
  7. 製品(Artifact)版本控制 - 解決的問題:製品的版本控制,製品的晉級,某些情況下環境的回滾
  8. 環境自動創建 - 解決的問題:解決的是構建/部署環境一致性的問題,開發測的好好的,測試一驗證怎麼不行啊,容器化/雲原生讓這個問題更好的解決
  9. 不可變服務器(Immutable Server )- 解決的問題: 可能不好理解,打個比方如果如果你的服務器掛了,或者某次配置更改了服務就起不來了,使用不可變基礎設施的主要好處是部署的簡單性、可靠性和一致性,服務器可以隨時替換上線
  10. 集成測試
  11. 性能測試
  12. 每次提交都觸發:構建、部署和自動化測試 ** - 解決的問題:快速失敗,避免下游時間的浪費**
  13. 自動化變更請求 ** - 解決問題:某些場景下通過狀態變更觸發某些動作**
  14. 零停機發布 - 解決的問題:滾動/藍綠/灰度發佈等,用戶無感知
  15. 功能開關 - 解決的問題: 主幹開發中,如果某個功能沒開放完,就通過on/off某個特性來讓穩定的功能上線;還有一個場景,比如某些面對消費者的廣告網站,想看看自己某個功能客戶是否細化,通過功能開關看看市場反饋,一般和A/B測試配合

基於場景設計流水線

是否需要一條完整的流水線?流水線是越多越好,還是越少越好?
建議按照場景來設計,一條流水線通喫所有流程是不現實的,搞了好多流水線(比如一個構建就一個流水線,一個複製操作就一個流水線)這些都是不可取的,維護成本巨大,得不償失。
image.png
流水線按照場景分類如下:

  • 端到端自動化流水線
    • 需求、代碼構建、測試、部署環境內嵌自動化能力,每次提交都觸發完整流水線
  • 提交階段流水線(個人級)、
  • 驗收階段流水線(團隊級)、
  • 部署階段流水線(部署/發佈)
  • 流水線自動化觸發,遞次自動化(製品)晉級;
  • 流水線任務按需串行、並行、特殊場景下跳過執行
  • 必要環節人工干預, e.g. 在手工測試、正式發佈等環節導入手工確認環節,流水線牽引流動

image.png

1)提交流水線

過程如下:

  • 提交即構建
  • 編譯單測打包代碼質量檢查
  • 構建錯誤第一時間通知提交人

以Jenkins實現爲例,
通過webhook觸發CI構建,首先配置Jenkins項目

  • 使用generic webhook方式觸發項目構建
  • 配置構建觸發器參數(獲取gitlab返回的數據,比如分支、用戶等信息)
  • 配置構建觸發器中的token(確保唯一,建議可以用項目名稱)
  • 配置觸發器中的請求過濾(merge_request,opend)

image.png
其次是Gitlab的配置

  • 項目-》集成-》新建webhook
  • 填寫webhook地址?token=projectName
  • MergeRequest操作觸發

image.png
剩下的就是編寫Jenkinsfile了,下面列出幾個關鍵點
1.獲取gitlab數據中的分支名稱,作爲本次構建的分支名稱。
2.獲取gitlab數據中的用戶郵箱,作爲構建失敗後通知對象。

2)MR流水線

過程如下:

  • codereview
  • 配置分支保護
  • 創建合併請求對將代碼審查結果在評論區展現
  • 由assignUser合併代碼

合併流水線設計:合併流水線的步驟其實跟提交流水線很類似,但是在代碼質量檢查的步驟中嚴格要求檢查質量閾的狀態,當質量閾狀態爲錯誤的時候,需要立即失敗並通知發起人。
第一次設計

  • 開發人員創建MR並指定AssignUser。
  • CI工具開始對MR中的源分支進行編譯構建打包代碼檢查。
  • 構建成功(代碼質量沒問題)在MR頁面評論提示信息。
  • 構建失敗在MR頁面評論失敗信息

第二次設計(藉助GitlabCI)- 優化點:加入MR構建失敗攔截,成功自動合併

  • 項目配置當流水線成功時才能merge。
  • 開發人員創建MR並指定AssignUser。
  • Jenkins開始對MR中的源分支的最後一次commit狀態改爲running。
  • 然後進行編譯構建打包代碼檢查。
  • 構建成功,更新最後一次commit的狀態爲 success。
  • 構建失敗,更新最後一次commit的狀態爲faild。

3)SQL發佈流水線

image.png
除了代碼有版本,其實SQL也有“版本的”,SQL腳本的版本對於產品的升級回滾至關重要。
一般對SQL的集成,會包含如下要素

  1. 構建環節,對SQL語法進行檢查,避免打進包里語法是錯的;某些情況下,多個開發會寫不同的增量腳本,最後發佈時候需要做腳本的合併
  2. SQL腳本的版本,某些情況下產品自身要用表來記錄自身業務腳本的版本,通過產品版本來判斷某些腳本是否應該被執行。

當然,也有其他數據庫版本管理工具,比如 flyway 和 liquibase;

  • Flyway是獨立於數據庫的應用、管理並跟蹤數據庫變更的數據庫版本管理工具。用通俗的話講,Flyway可以像Git管理不同人的代碼那樣,管理不同人的sql腳本,從而做到數據庫同步。
  • liquibase 只是在功能上和Flyway有差異

不管怎麼樣,它們底層的原理都是用另外的表記錄SQL腳本的版本,升級更新是比較版本差異,來決定是否執行。
python自帶的model模塊 python manage.py makemigrations 同樣在做類似 的事情
數據庫版本管理
image.png

流水線的關鍵元素

不管你用什麼CI/CD平臺,開源的Jenkins, GitLab CI, Teckton, Drone,還是商用的Azure,阿里雲效等,不管是代碼化,還是可視化,流水線包含的元素基本都差不多,下面通過不同的示例來說明這些元素的作用和含義。
參考:

Agent&Runner(執行代理)

image: "registry.example.com/my/image:latest" #gitlab-ci
pool:
  vmImage: ubuntu-latest  #auzure
agent { label 'linux' }  //jenkins

agent {
    docker {
       image 'maven:3-alpine'
       label 'Ubuntu'
       args '-v /root/.m2:/root/.m2'
    }
}

Parameter(參數變量)

  • **流水線級別參數 **(全局參數),範圍限於整個流水線運行時,可被整個流水線其他任務使用
    • 內置全局參數 - 一般稱爲built-in(預定義) variable, 有的平臺成爲環境變量
export CI_JOB_ID="50"
export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"
export CI_COMMIT_SHORT_SHA="1ecfd275"
export CI_COMMIT_REF_NAME="main"
export CI_REPOSITORY_URL="https://gitlab-ci-token:[masked]@example.com/gitlab-org/gitlab-foss.git"

1. BUILD_ID : 當前build的id
2. BUILD_NUM : 當前build的在pipeline中的build num
3. PIPELINE_NAME : pipeline 名稱
4. PIPELINE_ID: pipeline Id
5. GROUP: pipeline 所屬的group 名稱
6. TRIGGER_USER: 觸發build的user(event觸發的爲觸發gitlab event的user)
7. STAGE_NAME: 當前運行的stage的名稱
8. STAGE_DISPLAY_NAME : 當前運行的stage的顯示名稱
9. PIPELINE_URL : pipeline在ui中的網頁的鏈接
10. BUILD_URL: build 在ui的網頁鏈接
11. WORKSPACE: 當前stage運行的工作目錄,通常用作拼接絕對路徑
  • 非內置全局參數
 environment {         
        HARBOR_ACCESS_KEY = credentials('harbor-userpwd-pair')     
        SERVER_ACCESS_KEY = credentials('deploy-userpwd-pair')      
        GITLAB_API_TOKEN = credentials('gitlab_api_token_secret')       
    }
  • 外部參數 - 一般作爲運行時參數
variables:
  TEST_SUITE:
    description: "The test suite that will run. Valid options are: 'default', 'short', 'full'."
    value: "default"
  DEPLOY_ENVIRONMENT:
    description: "Select the deployment target. Valid options are: 'canary', 'staging', 'production', or a stable branch of your choice."
parameters([ 
        separator(name: "PROJECT_PARAMETERS", sectionHeader: "Project Parameters"),
        string(name: 'PROJECT_NAME', defaultValue: 'vue-app', description: '項目名稱') ,
        string(name: 'GIT_URL', defaultValue: '[email protected]:devopsing/vuejs-docker.git', description: 'Git倉庫URL') ,
])
  • 步驟任務參數 (局部參數) - 一般作爲某個插件任務的輸入參數,也可以使用上個任務的輸出作爲參數,範圍僅限於該任務內
  • 加密變量 - 對特殊變量進行加密處理
secrets:
  DATABASE_PASSWORD:
    vault: production/db/password@ops  # translates to secret `ops/data/production/db`, field `password`

Step(步驟)

參考: https://docs.drone.io/pipeline/overview/

---
kind: pipeline
type: docker
name: default

steps:
- name: backend
  image: golang
  commands:
  - go build
  - go test

- name: frontend
  image: node
  commands:
  - npm install
  - npm run test

...

Stage(階段)

一般用於對多個任務(step)進行分組歸類,便於管理

 stage('Pull code') {
            steps {
                echo 'Pull code...'
                script {
                    git branch: '${Branch_Or_Tags}', credentialsId: 'gitlab-private-key', url: '[email protected]:xxxx/platform-frontend.git'
                }
            }
}

Trigger(觸發器)

trigger:
- master
- releases/*
trigger_pipeline:
stage: deploy
script:
- 'curl --fail --request POST --form token=$MY_TRIGGER_TOKEN --form ref=main "https://gitlab.example.com/api/v4/projects/123456/trigger/pipeline"'
rules:
- if: $CI_COMMIT_TAG
environment: production
trigger:
  event:
  - promote
  target:
  - production

trigger:
   type: cron
   cron: '*/5 * * * *' #每5分鐘執行一次

製品歸檔&緩存 (artifacts&cache)

一般用於CI製品的歸檔,以及CI構建的緩存

archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
job:
  artifacts:
    name: "$CI_JOB_NAME"
    paths:
      - binaries/

cache: &global_cache
  key: $CI_COMMIT_REF_SLUG
  paths:
    - node_modules/
    - public/
    - vendor/
  policy: pull-push

集成憑證(Credentials)

參考:

主要用於CI/CD流水線對接外部工具,通過token/pwd/private key等方式連接外部服務。一般需要在界面做些提前配置,生成token 或者憑證ID,將ID在CI/CD yaml 或jenkinsfile中使用

withCredentials([usernamePassword(credentialsId: 'amazon', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
// available as an env variable, but will be masked if you try to print it out any which way
// note: single quotes prevent Groovy interpolation; expansion is by Bourne Shell, which is what you want
sh 'echo $PASSWORD'
// also available as a Groovy variable
echo USERNAME
// or inside double quotes for string interpolation
echo "username is $USERNAME"
}

Service(服務)

該元素應用於一些複雜的場景,比如需要一種外部(公共)服務爲流水線提供某種輸入或者結果。
您可以將相互依賴的服務用於複雜的作業,例如端到端測試,其中外部API需要與自己的數據庫通信。
例如,對於使用API的前端應用程序的端到端測試,並且API需要數據庫:

end-to-end-tests:
  image: node:latest
  services:
    - name: selenium/standalone-firefox:${FIREFOX_VERSION}
      alias: firefox
    - name: registry.gitlab.com/organization/private-api:latest
      alias: backend-api
    - postgres:14.3
  variables:
    FF_NETWORK_PER_BUILD: 1
    POSTGRES_PASSWORD: supersecretpassword
    BACKEND_POSTGRES_HOST: postgres
  script:
    - npm install
    - npm test

模板(Template)

參考: https://docs.drone.io/template/yaml/
某些平臺會使用“模板“的概念,其實就是複用的思想,通過加載固定模板實現一些快捷動作

kind: template
load: plugin.yaml
data:
  name: name
  image: image
  commands: commands

kind: pipeline
type: docker
name: default
steps:
   - name: {{ .input.name }}
     image: {{ .input.image }}
     commands:
        - {{ .input.commands }}

執行邏輯控制

參考: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html

image.png

stage('run-parallel') {
  steps {
    parallel(
      a: {
        echo "task 1"
      },
      b: {
        echo "task 2"
      }
    )
  }
}

stage('Build') {
            when {
                environment name: 'ACTION_TYPE', value: 'CI&CD'
            }
            steps {                
                buildDocker("vue")                           
            } 
}

image.png

stages:
  - build
  - test
  - deploy

image: alpine

build_a:
  stage: build
  script:
    - echo "This job builds something quickly."

build_b:
  stage: build
  script:
    - echo "This job builds something else slowly."

test_a:
  stage: test
  needs: [build_a]
  script:
    - echo "This test job will start as soon as build_a finishes."
    - echo "It will not wait for build_b, or other jobs in the build stage, to finish."

test_b:
  stage: test
  needs: [build_b]
  script:
    - echo "This test job will start as soon as build_b finishes."
    - echo "It will not wait for other jobs in the build stage to finish."

deploy_a:
  stage: deploy
  needs: [test_a]
  script:
    - echo "Since build_a and test_a run quickly, this deploy job can run much earlier."
    - echo "It does not need to wait for build_b or test_b."
  environment: production

deploy_b:
  stage: deploy
  needs: [test_b]
  script:
    - echo "Since build_b and test_b run slowly, this deploy job will run much later."
  environment: production

門禁審批

參考:https://learn.microsoft.com/en-us/azure/devops/pipelines/release/deploy-using-approvals?view=azure-devops

pipeline {
    agent any
    stages {
        stage('Example') {
            input {
                message "Should we continue?"
                ok "Yes, we should."
                submitter "alice,bob"
                parameters {
                    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
                }
            }
            steps {
                echo "Hello, ${PERSON}, nice to meet you."
            }
        }
    }
}
pool: 
   vmImage: ubuntu-latest

jobs:
- job: waitForValidation
  displayName: Wait for external validation  
  pool: server    
  timeoutInMinutes: 4320 # job times out in 3 days
  steps:   
   - task: ManualValidation@0
     timeoutInMinutes: 1440 # task times out in 1 day
     inputs:
         notifyUsers: |
            [email protected]
         instructions: 'Please validate the build configuration and resume'
         onTimeout: 'resume'

部署流水線分步驟實施

說了這麼多,如果從0開始寫流水線呢,可以按照下面的步驟,從“點”到“線”結合業務需要串起來,適合自己團隊協作開發節奏的流水線纔是最好的。

  1. 價值流進行建模並創建簡單的可工作流程
  2. 將構建和部署流程自動化
  3. 將單元測試和代碼分析自動化
  4. 將驗收測試自動化
  5. 將發佈自動化

image.png
注意的事項
開始寫流水線需要注意一下幾個方面,請考慮進去

  • 確定變量 - 哪些是你每次構建或者部署需要變化的,比如構建參數,代碼地址,分支名稱,安裝版本,部署機器IP等,控制變化的,這樣保證任務的可複製性,不要寫很多hardcode進去
  • 變量/命名的規範化,不要爲了一時之快,最後換個機器/換個項目,流水線就不能玩了,還要再改
  • 如果可以,最好是封裝標準動作成爲插件,甚至做成自研平臺服務化,讓更多團隊受益
  • 如果你還在用手動的方式配置流水線,請儘快切換到代碼方式,不管是jenkinsfile,還是yaml , 一切皆代碼 也是DevOps提倡的。

image.png
image.png

流水線案例

案例-1

image.png

案例-2

內部工具鏈平臺架構圖.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章