DevOps功能實現解析

概述

過去傳統的開發模式是開發團隊研發了產品,後期的部署運維交給單獨的運維團隊負責。這種開發模式常常會致使一些混亂的問題,好比,前期開發時因爲缺少後面測試和部署時的及時反饋,一些小問題沒有及時發現,致使後面錯誤累積,甚至積重難返,須要返工重作;也有可能前期開發時沒有出現任何問題,可是到後面部署運維時一些基礎環境變了,致使不少衝突產生,運維或開發團隊又須要在短期內解決該問題,耗時耗力,甚至可能拖延產品的上線日期。這種傳統的開發模式雖然有分工明確,各司其職的優勢,可是正由於如此,開發、測試和運維團隊之間嚴重脫節,缺少密切的合做,不少前期沒有發現的小問題會在後期部署運維時集中爆發,大大提升了開發的成本以及延長了產品的迭代週期。針對現代軟件愈來愈複雜,需求變化愈來愈快的趨勢,人們提出了DevOps(Development&Operations)開發模式,它不是一種工具集,而是一套方法論,主張開發、測試和運維團隊之間進行溝通、協做、集成和自動化,以綜合協做的工做方式改善整個團隊在交付軟件過程當中的速度和質量。java

若是說DevOps是一種開發理念,那麼CI/CD(持續集成、持續部署)管道就是其中的一種實踐方式,它表明着發佈流程自身的一個循環,從編寫代碼、構建鏡像、測試代碼、部署代碼到後面生產環境從新測試和部署等,這是一個持續的過程,反覆的過程。它使開發、測試和運維團隊等一開始就以綜合協做的方式綁定在一塊兒,解決了前期開發得不到及時的測試、部署反饋以及運維在後期纔開始介入等問題,避免了錯誤累積以及可以快速響應新的需求變化。經常使用的CI/CD工具備Jenkins、Drone和GitLab CI等等,因爲系統平臺使用的是Jenkins,咱們就以Jenkins爲例簡單介紹下其相關概念。Jenkins是一個用java語言編寫的開源工具,其CI/CD功能是經過一個叫pipeline的插件完成的。顧名思義,pipeline就是一套運行在Jenkins上的流水線框架,相似於工廠中的流水線做業。它將代碼編譯、腳本運行、鏡像構建、測試、部署等功能集成在一塊兒,爲了清楚地劃分不一樣的功能邏輯,一個pipeline被劃分紅了若干個Stage(稱之爲階段),每一個Stage表明一組相關的操做,好比:「build」,「Test」,「Deploy」等。其中每一個Stage內又劃分了多個Step(稱之爲步驟),它是最基本的操做單元,好比:建立目錄、構建鏡像、部署應用等等,由各種Jenkins插件提供具體功能。pipeline示意圖以下所示:nginx

pipeline

魔方雲DevOps實現

簡單地說,魔方雲是基於Kubernetes管理多種雲的雲平臺管理系統,內部功能的設計基本上均是經過Kubernetes(如下簡稱k8s)提供的自定義controller功能來實現的,其基本邏輯就是根據業務須要抽象出多個CRD(Custom Resource Definition,自定義資源對象),並編寫對應的controller來實現業務邏輯。git

爲了實現CI/CD功能,咱們抽象出了多個CRD,跟pipeline相關的3個CRD:github

  • pipeline:記錄pipeline運行時狀態、倉庫的認證信息、鉤子觸發配置以及項目代碼地址等信息。
  • pipelineExecution:每次pipeline運行時產生的執行實例(如下簡稱執行實例),運行完記錄結果信息;
  • pipelineSetting:整個項目下pipeline運行的設置信息,好比內存、CPU的限制,最大流水線並行運行個數等等。

跟源代碼倉庫(如下以gitlab爲例)相關的3個CRD:golang

  • sourceCodeProviderConfig:記錄代碼倉庫的application id 和secret受權信息;
  • sourceCodeCredential:記錄代碼倉庫的oauth認證信息;
  • sourceCodeRepository:記錄代碼倉庫的每一個具體的項目信息。

除了抽象出對應的CRD外,咱們還須要編寫對應的controller代碼實現對應的業務邏輯,好比當pipeline運行時,咱們須要產生pipeline執行實例,並實時同步其運行的狀態信息等等。文章下面會詳細介紹。web

pipeline功能步驟有不少種類型,包括運行腳本、構建發佈鏡像、發佈應用模板、部署YAML、部署應用等等。爲了提供這些功能,咱們採用Jenkins做爲底層的CI/CD工具,docker registry 做爲鏡像倉庫中心,minio做爲日誌存儲中心等等。這些服務是運行在pipeline所在項目的命名空間下。綜上,咱們設計的CI/CD系統功能的實現邏輯如圖所示:docker

pipeline-process

如上,當觸發pipeline執行邏輯時,會產生一個pipelineExecution crd,以記錄本次運行pipeline的狀態信息。當goroutine(syncState)發現有新的執行實例產生時,就會經過Jenkins引擎接口啓動Jenkins server端流水線做業的運行,Jenkins server 端收到信息後會啓動單獨的一個Jenkins slave pod進行流水線做業的響應。同時,goroutine(syncState)會不斷地經過引擎接口輪詢pipeline執行實例的運行狀況進而更新 pipelineExecution crd的狀態,好比,運行成功或失敗等等。當pipeline執行實例發生狀態變化時,就會觸發其對應的controller業務邏輯,進而經過Jenkins引擎接口與Jenkins server 通訊進行不一樣的操做,好比,暫停流水線的運行,運行完清除不須要的資源等等。當流水線做業發生狀態變化時,又會經過goroutine(syncState)更改pipeline執行實例的狀態,進而又觸發對應的controller業務代碼進行不一樣的業務邏輯處理,往復循環,直到流水線運行結束。這就是整個pipeline執行時的一個邏輯流程。咱們把整個pipeline模塊的實現劃分紅了3個部分:shell

  • crd:即上述提到的6個crd類型;
  • 基礎接口定義:即與Jenkins server通訊的客戶端,與代碼倉庫交互的客戶端等等基礎模塊;
  • controller:實現邏輯功能的業務代碼。

CRD

咱們首先介紹下3個與源代碼倉庫相關的crd。當咱們首次配置pipeline時會進行代碼倉庫受權設置,填寫的gitlab Application Id和secret等信息會被存入到sourceCodeProviderConfig這個crd中,下面摘取了一些主要的字段信息並添加了詳細註釋,敏感信息使用了‘*’代替。json

apiVersion: project.cubepaas.com/v3
metadata:
  name: gitlab
  namespace: p-qqxs7
clientId: 89d840b****** // Application Id
clientSecret: a69657b****** // secret
enabled: true
hostname: gitlab.******.cn // 代碼倉庫的地址
projectName: c-llqwv:p-qqxs7
redirectUrl: https://******/verify-auth
type: gitlabPipelineConfig // 倉庫類型

當配置完受權信息後,平臺就會與源代碼倉庫進行交互,獲取到倉庫的oauth認證信息並填充到sourceCodeCredential crd中,同時會獲取該倉庫下全部的項目代碼的信息並填充到sourceCodeRepository crd中。以下所示:api

apiVersion: project.cubepaas.com/v3
metadata:
  name: p-qqxs7-gitlab-******
spec:
  accessToken: 65b4e90****** // 訪問token
  displayName: ****** // 代碼倉庫的顯示暱稱
  gitLoginName: oauth2
  loginName: ****** // 代碼倉庫的登入用戶名
  projectName: c-llqwv:p-qqxs7
  sourceCodeType: gitlab // 代碼倉庫類型, gitlab github ...
apiVersion: project.cubepaas.com/v3
kind: SourceCodeRepository
metadata:
  labels:
    cubepaas.com/creator: linkcloud
  name: 89c129ff-f572-489e-98f7-3f111b3056f7
  namespace: user-lwckv
spec:
  defaultBranch: master // 默認代碼分支
  projectName: c-llqwv:p-qqxs7
  sourceCodeCredentialName: user-lwckv:p-qqxs7-gitlab-****** // 指向上述 sourceCodeCredential crd
  sourceCodeType: gitlab
  url: https://gitlab.netbank.cn/******/testpipeline.git // 項目的url地址

這樣就將倉庫的oauth認證信息以及倉庫的項目信息保存到了2個不一樣的crd中。

下面詳細看下與pipeline相關的3個crd結構信息。

sh-4.4# kubectl get crds | grep pipeline
pipelineexecutions.project.cubepaas.com                            2019-12-18T10:32:10Z
pipelines.project.cubepaas.com                                     2019-12-18T10:32:11Z
pipelinesettings.project.cubepaas.com                              2019-12-18T10:32:10Z

pipelinesetting crd 保存着整個項目下全部的pipeline的運行設置信息,好比CPU、內存資源限額,最多可同時運行多少個pipeline等等,不一樣功能的配置信息保存在多個crd下。

p-qqxs7     executor-cpu-limit          19d // cpu限制配置
p-qqxs7     executor-cpu-request        19d // cpu預留配置
p-qqxs7     executor-memory-limit       19d // 內存限制配置
p-qqxs7     executor-memory-request     19d // 內存預留配置
p-qqxs7     executor-quota              19d // 最多可同時運行多少個pipeline
p-qqxs7     registry-signing-duration   19d // 用於設置docker鏡像倉庫證書的有效時長

// 好比,看下executor-quota詳細信息
apiVersion: project.cubepaas.com/v3
metadata:
  name: executor-quota
  namespace: p-qqxs7
default: "2" // 默認最多可同時運行2個pipeline
projectName: c-llqwv:p-qqxs7
value: "3" // 自定義設置,最多可同時運行3個pipeline,沒有值會取上面默認值

下面是pipeline crd 的詳細字段信息,主要是保存了上次執行pipeline時的結果信息、倉庫的認證信息、鉤子信息以及項目代碼地址信息等。

apiVersion: project.cubepaas.com/v3
kind: Pipeline
metadata:
  name: p-2qz4b
  namespace: p-qqxs7
spec:
  projectName: c-llqwv:p-qqxs7
  repositoryUrl: https://gitlab.******.cn/******/testpipeline.git // 項目代碼地址
  sourceCodeCredentialName: user-lwckv:p-qqxs7-gitlab-****** // 指向對應的用戶認證信息(sourceCodeCredential crd,見下面 status ---> sourceCodeCredential 字段)
  triggerWebhookPush: true // 當push代碼時觸發該流水線執行
status:
  lastExecutionId: p-qqxs7:p-2qz4b-1 // 最新一次運行的執行實例對應的id
  lastRunState: Success // 最新一次運行的最後結果
  nextRun: 2 // 下次運行時執行實例對應的序號
  pipelineState: active // 該流水線處於激活狀態
  sourceCodeCredential: // 上述已介紹,此處再也不贅述
    apiVersion: project.cubepaas.com/v3
    kind: SourceCodeCredential
    metadata:
      name: p-qqxs7-gitlab-******
      namespace: user-lwckv
    spec:
      displayName: ******
      gitLoginName: oauth2
      loginName: ******
      projectName: c-llqwv:p-qqxs7
      sourceCodeType: gitlab
      userName: user-lwckv
    status: {}
  token: a8c70ff2-a87a-4ef5-b677-73fc6c65fe69 // 訪問token
  webhookId: "241" // 經過gitlab webhook觸發流水線執行邏輯

pipelineExecution crd 結構的字段信息:

apiVersion: project.cubepaas.com/v3
kind: PipelineExecution
metadata:
  name: p-2qz4b-1 // 執行實例的命名規則是:pipeline crd 的名字加序號,運行一次pipeline,序號加1
  namespace: p-qqxs7
spec:
  branch: master // 代碼倉庫分支
  commit: 6f67ae4****** // git commit id
  event: push // push代碼觸發流水線執行
  message: Update .cubepaas-devops.yml file
  pipelineConfig: // 如下是pipeline具體的stage和step的配置信息,每次運行時從代碼倉庫的配置文件(.cubepaas-devops.yml)拉取下來填充該結構
    notification: {}
    stages:
    - name: Clone // 克隆代碼
      steps:
      - sourceCodeConfig: {}
    - name: one // 構建發佈鏡像
      steps:
      - publishImageConfig:
          buildContext: .
          dockerfilePath: ./Dockerfile
          registry: 127.0.0.1:34844
          tag: test:001
    - name: two // 運行腳本
      steps:
      - runScriptConfig:
          image: golang:latest
          shellScript: go env
    timeout: 60 // 超時設置
  pipelineName: p-qqxs7:p-2qz4b
  projectName: c-llqwv:p-qqxs7
  ref: refs/heads/master // 拉取的是master分支
  repositoryUrl: https://gitlab.netbank.cn/******/testpipeline.git // 項目代碼地址
  run: 1 // 這次運行序號
  triggeredBy: webhook // 如何觸發
status: // 如下記錄着pipeline每一個stage和step的運行結果信息
  executionState: Success
  stages:
  - ended: 2020-01-07T08:06:36Z
    state: Success
    steps:
    - ended: 2020-01-07T08:06:36Z
      state: Success
  ...
  ...

以上就是實現CI/CD系統功能所抽象出的6個主要的CRD。

基礎接口定義

pipeline 涉及到的功能模塊不少,從源代碼倉庫拉取代碼、設置鉤子操做、倉庫觸發流水線操做以及與 Jenkins server 端通訊的 client 等等。爲了方便之後的擴展,關鍵數據結構都被定義成了接口,以下所示:

這是與Jenkins server端通訊的client接口定義。

type PipelineEngine interface {
    // 檢查jenkins運行條件是否知足
    PreCheck(execution *v3.PipelineExecution) (bool, error)
    // 運行pipeline
    RunPipelineExecution(execution *v3.PipelineExecution) error
    // 從新運行pipeline
    RerunExecution(execution *v3.PipelineExecution) error
    // 中止pipeline的運行
    StopExecution(execution *v3.PipelineExecution) error
    // 獲取詳細的日誌信息
    GetStepLog(execution *v3.PipelineExecution, stage int, step int) (string, error)
    // 同步運行狀態
    SyncExecution(execution *v3.PipelineExecution) (bool, error)
}

這是代碼倉庫觸發流水線操做的接口定義:

// 當代碼倉庫觸發流水線操做時,由該接口響應請求並啓動流水線執行
type Driver interface {
    Execute(req *http.Request) (int, error)
}

以 gitlab 爲例

func (g GitlabDriver) Execute(req *http.Request) (int, error) {
    ...

    event := req.Header.Get(GitlabWebhookHeader)
    if event != gitlabPushEvent && event != gitlabMREvent && event != gitlabTagEvent {
        return http.StatusUnprocessableEntity, fmt.Errorf("not trigger for event:%s", event)
    }

    ...

    // 根據不一樣的觸發條件獲取請求參數中對應的信息
    info := &model.BuildInfo{}
    if event == gitlabPushEvent {
        info, err = gitlabParsePushPayload(body)
        ...
    } else if event == gitlabMREvent {
        info, err = gitlabParseMergeRequestPayload(body)
        ...
    } else if event == gitlabTagEvent {
        info, err = gitlabParseTagPayload(body)
        ...
    }

    // 驗證並建立pipeline執行實例,開始流水線的運行
    return validateAndGeneratePipelineExecution(g.PipelineExecutions, g.SourceCodeCredentials, g.SourceCodeCredentialLister, info, pipeline)
}

除此以外,還有與代碼倉庫進行交互的客戶端(獲取代碼分支信息、克隆代碼、拉取pipeline配置文件等)、與minio server通訊的客戶端(保存、獲取日誌信息)等結構,再也不贅述。

controller

當觸發流水線執行邏輯時,會經過pipeline crd和代碼倉庫中的pipeline配置文件產生一個pipelineExecution crd,這時會觸發pipelineExecution對應的controller運行業務邏輯。以下所示:

func (l *Lifecycle) Create(obj *v3.PipelineExecution) (runtime.Object, error) {
    return l.Sync(obj)
}

func (l *Lifecycle) Updated(obj *v3.PipelineExecution) (runtime.Object, error) {
    return l.Sync(obj)
}

當建立或更新pipelineExecution時會調用Sync同步函數進行業務處理。下面只摘取重要的代碼邏輯,以下所示:

func (l *Lifecycle) Sync(obj *v3.PipelineExecution) (runtime.Object, error) {

    ...

    // 若是 pipeline 執行實例被暫停,則會中止流水線做業
    if obj.Status.ExecutionState == utils.StateAborted {
        if err := l.doStop(obj); err != nil {
            return obj, err
        }
    }

    // 若是 pipeline 執行實例運行完畢,則會清理流水線做業的一些資源
    // 好比,產生的Jenkins slave pod
    if obj.Labels != nil && obj.Labels[utils.PipelineFinishLabel] == "true" {
        return l.doFinish(obj)
    }

    // 若是 pipeline 執行實例正在運行中,則直接返回,無操做
    if v3.PipelineExecutionConditionInitialized.GetStatus(obj) != "" {
        return obj, nil
    }

    // 判斷流水線做業是否超出資源限額
    exceed, err := l.exceedQuota(obj)
    if err != nil {
        return obj, err
    }
    // 若是超出資源限額,則會設置當前 pipeline 執行實例爲阻塞狀態
    if exceed {
        obj.Status.ExecutionState = utils.StateQueueing
        obj.Labels[utils.PipelineFinishLabel] = ""

        if err := l.newExecutionUpdateLastRunState(obj); err != nil {
            return obj, err
        }

        return obj, nil
    } else if obj.Status.ExecutionState == utils.StateQueueing {
        obj.Status.ExecutionState = utils.StateWaiting
    }

    // 更新 pipeline 執行實例的狀態: 好比運行序號+1
    if err := l.newExecutionUpdateLastRunState(obj); err != nil {
        return obj, err
    }
    v3.PipelineExecutionConditionInitialized.CreateUnknownIfNotExists(obj)
    obj.Labels[utils.PipelineFinishLabel] = "false"

    // 在數據面部署pipeline功能所需資源
    if err := l.deploy(obj.Spec.ProjectName); err != nil {
        obj.Labels[utils.PipelineFinishLabel] = "true"
        obj.Status.ExecutionState = utils.StateFailed
        v3.PipelineExecutionConditionInitialized.False(obj)
        v3.PipelineExecutionConditionInitialized.ReasonAndMessageFromError(obj, err)
    }

    // 將 configMap 存儲的docker鏡像倉庫端口信息同步到pipeline執行實例中去.
    if err := l.markLocalRegistryPort(obj); err != nil {
        return obj, err
    }

    return obj, nil
}

其中,deploy函數的邏輯就是第一次運行時經過判斷數據面中是否存在pipeline的命名空間,若是存在就表明基礎資源已經配置完成,直接走reconcileRb函數,該函數的邏輯見下面;若是不存在,就會在數據面中初始化必要的基礎資源,好比:pipeline命名空間, Jenkins docker minio服務, 配置configMap, secret等等。

func (l *Lifecycle) deploy(projectName string) error {
    clusterID, projectID := ref.Parse(projectName)
    ns := getPipelineNamespace(clusterID, projectID)
    // 若是該pipeline的namespace已經有了,說明下面的資源部署已經完成了,則直接走reconcileRb流程
    // 不然走下面的資源部署流程
    if _, err := l.namespaceLister.Get("", ns.Name); err == nil {
        return l.reconcileRb(projectName)
    } else if !apierrors.IsNotFound(err) {
        return err
    }

    // 建立pipeline對應的命名空間,如p-qqxs7-pipeline
    if _, err := l.namespaces.Create(ns); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the pipeline namespace")
    }

    ...

    // 隨機產生一個token,用於配置下面的secret
    token, err := randomtoken.Generate()

    nsName := utils.GetPipelineCommonName(projectName)
    ns = getCommonPipelineNamespace()
    // 建立用於部署docker鏡像倉庫的代理服務的命名空間
    if _, err := l.namespaces.Create(ns); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the cattle-pipeline namespace")
    }

    // 在 pipeline namespace 內建立secret : pipeline-secret
    secret := getPipelineSecret(nsName, token)
    l.secrets.Create(secret); 

    ...

    // 獲取管理面項目的系統用戶token
    apikey, err := l.systemAccountManager.GetOrCreateProjectSystemToken(projectID)

    ...

    // 在 pipeline namespace 內建立secret: pipeline-api-key,用於數據面與管理面通訊的憑證
    secret = GetAPIKeySecret(nsName, apikey)
    l.secrets.Create(secret); 

    // 調諧 docker 鏡像倉庫的證書配置(在控制面中)
    if err := l.reconcileRegistryCASecret(clusterID); err != nil {
        return err
    }

    // 將控制面中的 docker 鏡像倉庫的證書配置同步到數據面中
    if err := l.reconcileRegistryCrtSecret(clusterID, projectID); err != nil {
        return err
    }

    // 在 pipeline namespace 內建立 serviceAccount : jenkins
    sa := getServiceAccount(nsName)
    if _, err := l.serviceAccounts.Create(sa); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating a pipeline service account")
    }

    ... 

    // 在 pipeline namespace 內建立 service: jenkins
    jenkinsService := getJenkinsService(nsName)
    if _, err := l.services.Create(jenkinsService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the jenkins service")
    }

    // 在 pipeline namespace 內建立 deployment: jenkins
    jenkinsDeployment := GetJenkinsDeployment(nsName)
    if _, err := l.deployments.Create(jenkinsDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the jenkins deployment")
    }

    // 在 pipeline namespace 內建立 service: docker-registry
    registryService := getRegistryService(nsName)
    if _, err := l.services.Create(registryService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the registry service")
    }

    // 在 pipeline namespace 內建立 deployment: docker-registry
    registryDeployment := GetRegistryDeployment(nsName)
    if _, err := l.deployments.Create(registryDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the registry deployment")
    }

    // 在 pipeline namespace 內建立 service: minio
    minioService := getMinioService(nsName)
    if _, err := l.services.Create(minioService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the minio service")
    }

    // 在 pipeline namespace 內建立 deployment: minio
    minioDeployment := GetMinioDeployment(nsName)
    if _, err := l.deployments.Create(minioDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the minio deployment")
    }

    // 調諧 configMap: proxy-mappings,用於配置docker鏡像倉庫代理服務的端口信息
    if err := l.reconcileProxyConfigMap(projectID); err != nil {
        return err
    }

    // 建立secret: devops-docker-registry,存儲訪問docker倉庫的認證信息
    if err := l.reconcileRegistryCredential(projectName, token); err != nil {
        return err
    }

    // 建立 daemonset: registry-proxy,每一個節點部署一套docker鏡像倉庫的nginx代理服務
    // 能夠在任意一個節點上經過不一樣的端口便可訪問到不一樣的docker鏡像倉庫
    nginxDaemonset := getProxyDaemonset()
    if _, err := l.daemonsets.Create(nginxDaemonset); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the nginx proxy")
    }

    return l.reconcileRb(projectName)
}

reconcileRb函數的功能就是遍歷全部namespace, 對其調諧rolebindings, 目的是讓 pipeline serviceAccount(jenkins) 對該project下的全部namespace具備所須要的操做權限,這樣Jenkins server纔可以在數據面中正常提供CI/CD基礎服務。

func (l *Lifecycle) reconcileRb(projectName string) error {

    ...

    var namespacesInProject []*corev1.Namespace
    for _, namespace := range namespaces {
        parts := strings.Split(namespace.Annotations[projectIDLabel], ":")
        if len(parts) == 2 && parts[1] == projectID {
            // 過濾出屬於該project下的全部namespace
            namespacesInProject = append(namespacesInProject, namespace)
        } else {
            // 對非該project下的namespace, 清除有關該 pipeline 的 rolebinding
            if err := l.roleBindings.DeleteNamespaced(namespace.Name, commonName, &metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
                return err
            }
        }
    }

    for _, namespace := range namespacesInProject {
        // 對屬於該project下的namespace, 建立 rolebinding: 對 jenkins serviceAccount 綁定角色
        // 即賦予 jenkins serviceAccount 對該project下的全部namespace所須要的操做權限
        rb := getRoleBindings(namespace.Name, commonName)
        if _, err := l.roleBindings.Create(rb); err != nil && !apierrors.IsAlreadyExists(err) {
            return errors.Wrapf(err, "Error create role binding")
        }
    }

    // 賦予 jenkins serviceAccount 在 cluster 內建立和修改 namespace 的權限
    // 當部署應用時能夠指定建立新的命名空間
    clusterRbs := []string{roleCreateNs, projectID + roleEditNsSuffix}
    for _, crbName := range clusterRbs {
        crb := getClusterRoleBindings(commonName, crbName)
        if _, err := l.clusterRoleBindings.Create(crb); err != nil && !apierrors.IsAlreadyExists(err) {
            return errors.Wrapf(err, "Error create cluster role binding")
        }
    }

    return nil
}

咱們經過kubernator ui查看下數據面k8s中部署的有關pipeline的資源,如圖所示:

deploy

pipeline-secret用於配置Jenkins、docker-registry、minio服務的auth信息,pipeline-api-key用於數據面與管理面通訊的憑證,registry-crt用於配置docker鏡像倉庫的證書。

goroutine(syncState)的代碼邏輯比較簡單,當產生新的pipeline執行實例時就會啓動Jenkins server端流水線做業的運行並實時同步其運行狀態到pipeline執行實例中。代碼邏輯以下:

func (s *ExecutionStateSyncer) syncState() {
    set := labels.Set(map[string]string{utils.PipelineFinishLabel: "false"})
    allExecutions, err := s.pipelineExecutionLister.List("", set.AsSelector())
    executions := []*v3.PipelineExecution{}
    // 遍歷該cluster下的 pipeline 執行實例
    for _, e := range allExecutions {
        if controller.ObjectInCluster(s.clusterName, e) {
            executions = append(executions, e)
        }
    }

    for _, execution := range executions {
        if v3.PipelineExecutionConditionInitialized.IsUnknown(execution) {
            // 檢查數據面k8s中 Jenkins pod 是否正常,正常則運行該 pipeline job
            s.checkAndRun(execution)
        } else if v3.PipelineExecutionConditionInitialized.IsTrue(execution) {
            e := execution.DeepCopy()
            // 若是已經啓動了,則同步運行狀態
            updated, err := s.pipelineEngine.SyncExecution(e)
            if updated {
                // 更新最新的狀態到 pipelineExecution crd 中
                s.updateExecutionAndLastRunState(e);
            }
        } else {
            // 更新最新的狀態到 pipelineExecution crd 中
            s.updateExecutionAndLastRunState(execution);
        }
    }

    logrus.Debugf("Sync pipeline execution state complete")
}

除此以外,系統爲了內部的docker鏡像倉庫的安全性,會啓動一個goroutine每隔一段時間就更新下docker鏡像倉庫的證書。同時爲了方便訪問docker鏡像倉庫,系統在數據面k8s中啓動了一個daemonset類型的代理服務,內部採用的是nginx代理,經過不一樣的端口指向不一樣的docker鏡像倉庫,這樣就能夠在任意一個節點經過不一樣的端口方便地訪問到不一樣的docker鏡像倉庫。經過kubectl在數據面中獲取到的configMap(proxy-mappings)信息以下所示:

apiVersion: v1
items:
- apiVersion: v1
  data:
    mappings.yaml: |
      mappings: '{"p-ljzgf":"34380","p-qqxs7":"34844"}'
  kind: ConfigMap
  metadata:
    labels:
      cubepaas.com/creator: linkcloud
    name: proxy-mappings
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

咱們能夠看到如今系統上有兩個pipeline的docke鏡像倉庫端口信息。咱們能夠經過不一樣的端口訪問到對應pipeline的docker鏡像倉庫。如:

michael@work1:~$ docker login 127.0.0.1:34844
Username: admin
Password: 
WARNING! Your password will be stored unencrypted in /home/michael/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

魔方雲DevOps使用

一般,流水線第一步是從代碼倉庫克隆代碼,而後纔是編譯、測試和部署等一系列功能的設置,所以咱們首先須要受權平臺可以訪問代碼倉庫。咱們以gitlab倉庫爲例,將配置好的Application Id、secret和倉庫地址填入到對應的文本框中,點擊受權,這樣咱們就完成了倉庫的受權配置。如圖所示:

shouquan

配置完成後,咱們就能夠激活pipeline功能了,咱們簡單地設置pipeline配置文件( .cubepaas-devops.yml)以下:

stages:
- name: one
  steps:
  - publishImageConfig:
      dockerfilePath: ./Dockerfile
      buildContext: .
      tag: test:001
      registry: 127.0.0.1:34844
- name: two
  steps:
  - runScriptConfig:
      image: golang:latest
      shellScript: go env
timeout: 60
notification: {}

如上,簡單地配置了兩個階段,一個是構建發佈鏡像,一個是運行腳本,你能夠按照本身項目的發佈流程配置好pipeline,最後點擊完成,咱們就能夠看到該pipeline自動運行了。pipeline的配置文件會推送到代碼倉庫中,這樣就能夠實現pipeline歷史版本的回溯,支持從代碼庫直接讀取pipeline配置文件,從而實現了Pipeline as Code的理念。點擊查看日誌,你能夠看到pipeline各個階段運行的詳細日誌信息。如圖所示。

log

【注意】首次運行pipeline時系統會從網絡下載Jenkins、docker、minio以及其餘pipeline-tools鏡像,若是長時間未運行,請查看網絡是否有問題。

相關文章
相關標籤/搜索