談到到DevOps,持續交付流水線是繞不開的一個話題,相對於其他實踐,通過流水線來實現快速高質量的交付價值是相對能快速見效的,特別對於開發測試人員,能夠獲得實實在在的收益。很多文章介紹流水線,不管是jenkins,gitlab-ci, 流水線,還是drone, github action 流水線, 文章都很多,但是不管什麼工具,流水線設計的思路是一致的。於此同時,在實踐過程中,發現大家對流水像有些誤區,不是一大堆流水線,就是一個流水線調一個超級複雜的指令碼,各種寫死和環境依賴,所以希望通過這篇文章能夠給大家分享自己對於Pipeline流水線的設計心得體會。
持續整合(CI)是在原始碼變更後自動檢測、拉取、構建和(在大多數情況下)進行單元測試的過程
對專案而言,持續整合(CI)的目標是確保開發人員新提交的變更是好的,不會發生break build; 並且最終的主幹分支一直處於可釋出的狀態,
對於開發人員而言,要求他們必須頻繁地向主幹提交程式碼,相應也可以即時得到問題的反饋。實時獲取到相關錯誤的資訊,以便快速地定位與解決問題
顯然這個過程可以大大地提高開發人員以及整個IT團隊的工作效率,避免陷入好幾天得不到好的「部署產出」,影響後續的測試和交付。
持續交付在持續整合的基礎上,將整合後的程式碼部署到更貼近真實執行環境的「預釋出環境」(production-like environments)中。交付給質量團隊或者使用者,以供評審。如果評審通過,程式碼就進入生產階段 持續交付並不是指軟體每一個改動都要儘快部署到產品環境中,它指的是任何的程式碼修改都可以在任何時候實時部署。
強調: 1、手動部署 2、有部署的能力,但不一定部署
程式碼通過評審之後,自動部署到生產環境中。持續部署是持續交付的最高階段。
強調 1、持續部署是自動的 2、持續部署是持續交付的最高階段 3、持續交付表示的是一種能力,持續部署則是一種方式
參考: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html
這裡非常推薦以版本控制系統為源的構建流水線設計,從每一位開發人員提交程式碼即可對當前提交程式碼進行檢查編譯構建,儘快將錯誤反饋給每位提交人員。
對於DevOps流水線,主要是由各類任務串聯起來,而對於任務本身又分為兩張型別,一種是自動化任務,一種是人工執行任務。具體如下:
而通常我們看到的流水線基本都由上述兩類任務組合編排而成,一個流水線可以是完全自動化執行,也可以中間加入了人工干預節點,在人工干預處理後再繼續朝下執行。比如流水線中到了測試部署完成後,可以到測試環境人工驗證環節,只有人工驗證通過再流轉到遷移釋出到生產環境動作任務。
DevOps流水線實際上和我們原來經常談到的持續整合最佳實踐是相當類似的,較大的一個差異點就在於引入了容器化技術來實現自動化部署和應用託管。至於在DevOps實踐中,是否必須馬上將專案切換到微服務架構框架模式,反而不是必須得。
在整個DevOps流水線中,我們實際上強調個一個關鍵點在於「一套Docker映象檔案+多套環境設定+多套構建版本標籤」做法。以確保我們最終構建和測試通過的版本就是我們部署到生產環境的版本。
構建操作只有一次,而後面到測試環境,到UAT環境,到生產環境,都屬於是映象的環境遷移和部署。而不涉及到需要再次重新打包的問題。這個是持續整合,也是DevOps的基本要求。
今天談DevOps流水線編排,主要是對流水線編排本身的靈活性進一步思考。
注意常規方式下構建完執行進行部署操作,部署操作一般就是將構建的結果拷貝到我們的測試環境伺服器,同時對初始化指令碼進行啟動等。而在DevOps下,該操作會變成兩個操作,即一個打包,一個部署。打包是將構建完成的內容製作為映象,部署是將映象部署到具體的資源池和指定叢集。
那麼首先要考慮構建操作和打包操作如何鬆耦合開,打包操作簡單來就是就是一個映象製作,需要的是構建操作產生的輸出。我們可以對其輸出和需要拷貝的內容在構建的時候進行約定。而打包任務則是一個標準化的映象製作任務,我們需要考慮的僅僅是基於
1)基於哪個基礎映象
2)中介軟體容器預設目錄設定
3)初始化啟動命令。
即在實際的打包任務設計的時候,我們不會指定具體的部署包和部署檔案,這個完全由編排的時候由上游輸入。
任務節點間鬆耦合設計的意義
這種鬆耦合設計才能夠使流水線編排更加靈活。比如我們在進行了構建打包後,我們希望同時講打包內容部署到開發環境和測試環境。那麼則是打包動作完成後需要對接兩個應用部署任務。這兩個部署任務都依託上面的打包結果進行自動化部署,可以並行進行。
對於測試環境部署完成後,我們需要進行測試人員手工驗證測試,如果測試通過,我們打標籤後希望能夠直接釋出到UAT環境。而這種操作我們也希望在一個流水線來設計和完成。這樣我們更加容易在持續整合看板上看到整個版本構建和遷移的完整過程。如果這是在一個大流水線裡面,那麼對於UAT環境部署任務就需要一直去追溯流水線上的最近的一個打包任務節點,同時取該任務節點產生的輸出來進行相應的環境部署操作。
在談DevOps的時候,一個重點就是和QA/QC的協同,因此在流水線編排的時候一定要考慮各類測試節點,包括靜態程式碼檢查,自動化的單元測試,人工的測試驗證。同時最好基於持續整合實踐,能夠將測試過程和整個自動化構建過程緊密結合起來。
簡單來說,測試人員發現build1.0.0001版本4個bug並提交,那麼在下次自動化構建完成並單元測試通過後,測試人員能夠很清楚的看到哪些Bug已經修改並可以在新構建的版本進行驗證。只有這樣才能夠形成閉環,整個流水線作業才能夠更好的發揮協同作用。
流水線除了任務步驟的編排,更重要的核心是最佳工程實踐的體現。過去傳統的思維,自動化就是寫個shell/python指令碼批次執行,在DevOps/微服務時代,這一招太out了,每種工程實踐的背後都有需要解決的問題,通過在流水線設計中注入最佳的工程實踐,可以讓流水線的價值最大化,也讓流水線更高階不是嘛。
是否需要一條完整的流水線?流水線是越多越好,還是越少越好?
建議按照場景來設計,一條流水線通吃所有流程是不現實的,搞了好多流水線(比如一個構建就一個流水線,一個複製操作就一個流水線)這些都是不可取的,維護成本巨大,得不償失。
流水線按照場景分類如下:
過程如下:
以Jenkins實現為例,
通過webhook觸發CI構建,首先設定Jenkins專案
其次是Gitlab的設定
剩下的就是編寫Jenkinsfile了,下面列出幾個關鍵點
1.獲取gitlab資料中的分支名稱,作為本次構建的分支名稱。
2.獲取gitlab資料中的使用者郵箱,作為構建失敗後通知物件。
過程如下:
合併流水線設計:合併流水線的步驟其實跟提交流水線很類似,但是在程式碼質量檢查的步驟中嚴格要求檢查質量閾的狀態,當質量閾狀態為錯誤的時候,需要立即失敗並通知發起人。
第一次設計
第二次設計(藉助GitlabCI)- 優化點:加入MR構建失敗攔截,成功自動合併
除了程式碼有版本,其實SQL也有「版本的」,SQL指令碼的版本對於產品的升級回滾至關重要。
一般對SQL的整合,會包含如下要素
當然,也有其他資料庫版本管理工具,比如 flyway 和 liquibase;
不管怎麼樣,它們底層的原理都是用另外的表記錄SQL指令碼的版本,升級更新是比較版本差異,來決定是否執行。
python自帶的model模組 python manage.py makemigrations 同樣在做類似 的事情
資料庫版本管理
不管你用什麼CI/CD平臺,開源的Jenkins, GitLab CI, Teckton, Drone,還是商用的Azure,阿里雲效等,不管是程式碼化,還是視覺化,流水線包含的元素基本都差不多,下面通過不同的範例來說明這些元素的作用和含義。
參考:
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'
}
}
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`
參考: 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
...
一般用於對多個任務(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:
- 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分鐘執行一次
一般用於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
參考:
主要用於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"
}
該元素應用於一些複雜的場景,比如需要一種外部(公共)服務為流水線提供某種輸入或者結果。
您可以將相互依賴的服務用於複雜的作業,例如端到端測試,其中外部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
參考: 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
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")
}
}
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
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開始寫流水線呢,可以按照下面的步驟,從「點」到「線」結合業務需要串起來,適合自己團隊共同作業開發節奏的流水線才是最好的。
注意的事項
開始寫流水線需要注意一下幾個方面,請考慮進去