DevOps平台

  • 2019 年 10 月 3 日
  • 筆記

DevOps定義(來自維基百科): DevOps(Development和Operations的組合詞)是一種重視「軟件開發人員(Dev)」和「IT運維技術人員(Ops)」之間溝通合作的文化、運動或慣例。透過自動化「軟件交付」和「架構變更」的流程,來使得構建、測試、發佈軟件能夠更加地快捷、頻繁和可靠。

公司技術部目前幾百人左右吧,但是整個技術棧還是比較落後的,尤其是DevOps、容器這一塊,需要將全線打通,當時進來也主要是負責DevOps這一塊的工作,應該說也是沒怎麼做好,其中也走了不少彎路,下面主要是自己踩過的坑吧。

一、自由風格的軟件項目

主要還是基於jenkins裏面構建一個自由風格的軟件項目,當時參考的是阿里的codepipeline,就是對jenkins封裝一層,包括創建job、立即構建、獲取構建進度等都進行封裝,並將需要的東西進行存庫,沒有想到碼代碼的時候,一堆的坑,比如: 1.連續點擊立即構建,jenkins是不按順序返回的,(分佈式鎖解決) 2.跨域調用,csrf,這個還好,不過容易把jenkins搞的無法登錄(注意配置,具體可以點擊這裡) 3.創建job的時候只支持xml格式,還要轉換一下,超級坑(xstream強行轉換) 4.docker構建的時候,需要掛載宿主機的docker(想過用遠程的,但效率不高) 5.數據庫與jenkins的job一致性問題,任務創建失敗,批量刪除太慢(目前沒想好怎麼解決) 6.由於使用了數據庫,需要檢測job是否構建完成,為了自定義參數,我們自寫了個通知插件,將構建狀態返回到kafka,然後管理平台在進行消息處理。

完成了以上的東西,不過由於太過於簡單,導致只能進行單條線的CICD,而且CI僅僅實現了打包,沒有將CD的過程一同串行起來。簡單來說就是,用戶點擊了構建只是能夠打出一個鏡像,但是如果要部署到kubernetes,還是需要在應用裏手動更換一下鏡像版本。總體而言,這個版本的jenkins我們使用的還是單點的,不足以支撐構建量比較大的情況,甚至如果當前服務掛了,斷網了,整一塊的構建功能都不能用。

<project>      <actions/>      <description>xxx</description>      <properties>          <hudson.model.ParametersDefinitionProperty>              <parameterDefinitions>                  <hudson.model.TextParameterDefinition>                      <name>buildParam</name>                      <defaultValue>v1</defaultValue>                  </hudson.model.TextParameterDefinition>                  <hudson.model.TextParameterDefinition>                      <name>codeBranch</name>                      <defaultValue>master</defaultValue>                  </hudson.model.TextParameterDefinition>              </parameterDefinitions>          </hudson.model.ParametersDefinitionProperty>      </properties>      <scm class="hudson.plugins.git.GitSCM">          <configVersion>2</configVersion>          <userRemoteConfigs>              <hudson.plugins.git.UserRemoteConfig>                  <url>http://xxxxx.git</url>                  <credentialsId>002367566a4eb4bb016a4eb723550054</credentialsId>              </hudson.plugins.git.UserRemoteConfig>          </userRemoteConfigs>          <branches>              <hudson.plugins.git.BranchSpec>                  <name>${codeBranch}</name>              </hudson.plugins.git.BranchSpec>          </branches>          <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>          <extensions/>      </scm>      <builders>          <hudson.tasks.Shell>              <command>ls</command>          </hudson.tasks.Shell>          <hudson.tasks.Maven>              <targets>clean package install -Dmaven.test.skip=true</targets>              <mavenName>mvn3.5.4</mavenName>          </hudson.tasks.Maven>          <com.cloudbees.dockerpublish.DockerBuilder>              <server>                  <uri>unix:///var/run/docker.sock</uri>              </server>              <registry>                  <url>http://xxxx</url>              </registry>              <repoName>xxx/xx</repoName>              <forcePull>true</forcePull>              <dockerfilePath>Dockerfile</dockerfilePath>              <repoTag>${buildParam}</repoTag>              <skipTagLatest>true</skipTagLatest>          </com.cloudbees.dockerpublish.DockerBuilder>      </builders>      <publishers>          <com.xxxx.notifications.Notifier/>      </publishers>  </project>

二、優化之後的CICD

上面的過程也仍然沒有沒住DevOps的流程,人工干預的東西依舊很多,由於上級急需產出,所以只能將就着繼續下去。我們將構建、部署每個當做一個小塊,一個CICD的過程可以選擇構建、部署,花了很大的精力,完成了串行化的別樣的CICD。 以下圖為例,整個流程的底層為:paas平台-jenkins-kakfa-管理平台(選擇cicd的下一步)-kafka-cicd組件調用管理平台觸發構建-jenkins-kafka-管理平台(選擇cicd的下一步)-kafka-cicd組件調用管理平台觸發部署。

目前實現了串行化的CICD構建部署,之後考慮實現多個CICD並行,並且一個CICD能夠調用另一個CICD,實際運行中,出現了一大堆問題。由於經過的組件太多,一次cicd的運行報錯,卻很難排查到問題出現的原因,業務方的投訴也開始慢慢多了起來,只能說勸導他們不要用這個功能。

沒有CICD,就無法幫助公司上容器雲,無法合理的利用容器雲的特性,更無法走上雲原生的道路。於是,我們決定另謀出路。

三、調研期

由於之前的CICD問題太多,特別是經過的組件太多了,導致出現問題的時候無法正常排查,為了能夠更加穩定可靠,還是決定了要更換一下底層。 我們重新審視了下pipeline,覺得這才是正確的做法,可惜不知道如果做成一個產品樣子的東西,用戶方Dockerfile都不怎麼會寫,你讓他寫一個Jenkinsfile?不合理!在此之外,我們看到了serverless jenkins、谷歌的tekton。 GitLab-CICD Gitlab中自帶了cicd的工具,需要配置一下runner,然後配置一下.gitlab-ci.yml寫一下程序的cicd過程即可,構建鏡像的時候我們使用的是kaniko,整個gitlab的cicd在我們公司小項目中大範圍使用,但是學習成本過高,尤其是引入了kaniko之後,還是尋找一個產品化的CICD方案。

分佈式構建jenkins x 首先要解決的是多個構建同時運行的問題,很久之前就調研過jenkins x,它必須要使用在kubernetes上,由於當時官方文檔不全,而且我們的DevOps項目處於初始期,所有沒有使用。jenkins的master slave結構就不多說了。jenkins x應該說是個全家桶,包含了helm倉庫、nexus倉庫、docker registry等,代碼是jenkins-x-image

serverless jenkins 好像跟谷歌的tekton相關,用了下,沒調通,只能用於GitHub。感覺還不如直接使用tekton。

阿里云云效 提供了圖形化配置DevOps流程,支持定時觸發,可惜沒有跟gitlab觸髮結合,如果需要個公司級的DevOps,需要將公司的jira、gitlab、jenkins等集合起來,但是圖形化jenkins pipeline是個特別好的參考方向,可以結合阿里云云效來做一個自己的DevOps產品。

微軟Pipeline 微軟也是提供了DevOps解決方案的,也是提供了yaml格式的寫法,即:在右邊填寫完之後會轉化成yaml。如果想把DevOps打造成一款產品,這樣的設計顯然不是最好的。

谷歌tekton kubernetes的官方cicd,目前已用於kubernetes的release發版過程,目前也僅僅是與GitHub相結合,gitlab無法使用,全過程可使用yaml文件來創建,跑起來就是類似kubernetes的job一樣,用完即銷毀,可惜目前比較新,依舊處於alpha版本,無法用於生產。有興趣可以參考下:Knative 初體驗:CICD 極速入門

四、產品化後的DevOps平台

在調研DockOne以及各個產商的DevOps產品時,發現,真的只有阿里雲的雲效才是真正比較完美的DevOps產品,用戶不需要知道pipeline的語法,也不需要掌握kubernetes的相關知識,甚至不用寫yaml文件,對於開發、測試來說簡直就是神一樣的存在了。雲效對小公司(創業公司)免費,但是有一定的量之後,就要開始收費了。在調研了一番雲效的東西之後,發現雲效也是基於jenkins x改造的,不過阿里畢竟人多,雖然能約莫看出是pipeline的語法,但是阿里徹底改造成了能夠使用yaml來與後台交互。 下面是以阿里雲的雲效界面以及配合jenkins的pipeline語法來講解:

4.1 Java代碼掃描

PMD是一款可拓展的靜態代碼分析器它不僅可以對代碼分析器,它不僅可以對代碼風格進行檢查,還可以檢查設計、多線程、性能等方面的問題。阿里雲的是簡單的集成了一下而已,對於我們來說,底層使用了sonar來接入,所有的代碼掃描結果都接入了sonar。

stage('Clone') {      steps{          git branch: 'master', credentialsId: 'xxxx', url: "xxx"      }  }  stage('check') {      steps{          container('maven') {              echo "mvn pmd:pmd"          }      }  }

4.2 Java單元測試

Java的單元測試一般用的是Junit,在阿里雲中,使用了surefire插件,用來在maven構建生命周期的test phase執行一個應用的單元測試。它會產生兩種不同形式的測試結果報告。我這裡就簡單的過一下,使用"mvn test"命令來代替。

  stage('Clone') {      steps{          echo "1.Clone Stage"          git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"      }  }  stage('test') {      steps{          container('maven') {              sh "mvn test"          }      }  }

4.3 Java構建並上傳鏡像

鏡像的構建比較想使用kaniko,嘗試找了不少方法,到最後還是只能使用dind(docker in docker),掛載宿主機的docker來進行構建,如果能有其他方案,希望能提醒下。目前jenkins x使用的是dind,掛載的時候需要配置一下config.json,然後掛載到容器的/root/.docker目錄,才能在容器中使用docker。

為什麼不推薦dind:掛載了宿主機的docker,就可以使用docker ps查看正在運行的容器,也就意味着可以使用docker stop、docker rm來控制宿主機的容器,雖然kubernetes會重新調度起來,但是這一段的重啟時間極大的影響業務。

  stage('下載代碼') {      steps{          echo "1.Clone Stage"          git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"          script {              build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()          }      }  }  stage('打包並構建鏡像') {      steps{          container('maven') {              echo "3.Build Docker Image Stage"              sh "mvn clean install -Dmaven.test.skip=true"              sh "docker build -f xxx/Dockerfile -t xxxxxx:${build_tag} ."              sh "docker push xxxxxx:${build_tag}"          }      }  }  

4.4 部署到阿里雲k8s

CD過程有點困難,由於我們的kubernetes平台是圖形化的,類似於阿里雲,用戶根本不需要自己寫deployment,只需要在圖形化界面做一下操作即可部署。對於CD過程來說,如果應用存在的話,就可以直接替換掉鏡像版本即可,如果沒有應用,就提供個簡單的界面讓用戶新建應用。當然,在容器最初推行的時候,對於用戶來說,一下子需要接受docker、kubernetes、helm等概念是十分困難的,不能一個一個幫他們寫deployment這些yaml文件,只能用helm創建一個通用的spring boot或者其他的模板,然後讓業務方修改自己的配置,每次構建的時候只需要替換鏡像即可。

def tmp = sh (      returnStdout: true,      script: "kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print $1}'"  )  //如果是第一次,則使用helm模板創建,創建完後需要去epaas修改pod的配置  if(tmp.equals('')){      sh "helm init --client-only"      sh """helm repo add mychartmuseum http://xxxxxx                          --username myuser                          --password=mypass"""      sh """helm install --set name=${JOB_NAME}                          --set namespace=${namespace}                          --set deployment.image=${image}                          --set deployment.imagePullSecrets=${harborProject}                          --name ${namespace}-${JOB_NAME}                          mychartmuseum/soa-template"""  }else{      println "已經存在,替換鏡像"      //epaas中一個pod的容器名稱需要帶上"-0"來區分      sh "kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}"  }  

4.5 整體流程

代碼掃描,單元測試,構建鏡像三個並行運行,等三個完成之後,在進行部署

pipeline:

pipeline {      agent {          label "jenkins-maven"      }      stages{          stage('代碼掃描,單元測試,鏡像構建'){              parallel {                  stage('並行任務一') {                      agent {                          label "jenkins-maven"                      }                      stages('Java代碼掃描') {                          stage('Clone') {                              steps{                                  git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"                              }                          }                          stage('check') {                              steps{                                  container('maven') {                                      echo "$BUILD_NUMBER"                                  }                              }                          }                      }                  }                  stage('並行任務二') {                      agent {                          label "jenkins-maven"                      }                      stages('Java單元測試') {                          stage('Clone') {                              steps{                                  echo "1.Clone Stage"                                  git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"                              }                          }                          stage('test') {                              steps{                                  container('maven') {                                      echo "3.Build Docker Image Stage"                                      sh "mvn -v"                                  }                              }                          }                      }                  }                  stage('並行任務三') {                      agent {                          label "jenkins-maven"                      }                      stages('java構建鏡像') {                          stage('Clone') {                              steps{                                  echo "1.Clone Stage"                                  git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"                                  script {                                      build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()                                  }                              }                          }                          stage('Build') {                              steps{                                  container('maven') {                                      echo "3.Build Docker Image Stage"                                      sh "mvn clean install -Dmaven.test.skip=true"                                      sh "docker build -f epaas-portal/Dockerfile -t hub.gcloud.lab/rongqiyun/epaas:${build_tag} ."                                      sh "docker push hub.gcloud.lab/rongqiyun/epaas:${build_tag}"                                  }                              }                          }                      }                  }              }          }          stage('部署'){              stages('部署到容器雲') {                  stage('check') {                      steps{                          container('maven') {                              script{                                  if (deploy_app == "true"){                                      def tmp = sh (                                          returnStdout: true,                                          script: "kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print $1}'"                                      )                                      //如果是第一次,則使用helm模板創建,創建完後需要去epaas修改pod的配置                                      if(tmp.equals('')){                                          sh "helm init --client-only"                                          sh """helm repo add mychartmuseum http://xxxxxx                                                              --username myuser                                                              --password=mypass"""                                          sh """helm install --set name=${JOB_NAME}                                                              --set namespace=${namespace}                                                              --set deployment.image=${image}                                                              --set deployment.imagePullSecrets=${harborProject}                                                              --name ${namespace}-${JOB_NAME}                                                              mychartmuseum/soa-template"""                                      }else{                                          println "已經存在,替換鏡像"                                          //epaas中一個pod的容器名稱需要帶上"-0"來區分                                          sh "kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}"                                      }                                  }else{                                      println "用戶選擇不部署代碼"                                  }                              }                          }                      }                  }              }          }      }  }

在jenkins x中查看:

4.4 日誌

jenkins blue ocean步驟日誌:

雲效中的日誌:

4.5 定時觸發

    triggers {          cron('H H * * *') //每天      }

五、其他

5.1 Gitlab觸發

pipeline中除了有對於時間的trigger,還支持了gitlab的觸發,需要各種配置,不過如果真的對於gitlab的cicd有要求,直接使用gitlab-ci會更好,我們同時也對gitlab進行了runner的配置來支持gitlab的cicd。gitlab的cicd也提供了構建完後即銷毀的過程。

六、總結

功能最強大的過程莫過於自己使用pipeline腳本實現,選取最適合自己的,但是對於一個公司來說,如果要求業務方來掌握這些,特別是IT流動性大的時候,既需要重新培訓,同個問題又會被問多遍,所以,只能將DevOps實現成一個圖形化的東西,方便,簡單,相對來說功能還算強大。

DevOps最難的可能都不是以上這些,關鍵是讓用戶接受,容器雲最初推行時,公司原本傳統的很多發版方式都需要進行改變,有些業務方不願意改,或者有些代碼把持久化的東西存到了代碼中而不是分佈式存儲里,甚至有些用戶方都不願意維護老代碼,看都不想看然後想上容器,一個公司在做技術架構的時候,過於混亂到最後填坑要麼需要耗費太多精力甚至大換血。

最後,DevOps是雲原生的必經之路!!!

文章同步:
博客園:https://www.cnblogs.com/w1570631036/p/11524673.html
個人網站:http://www.wenzhihuai.com/getblogdetail.html?blogid=663
gitbook:https://gitbook.wenzhihuai.com/devops/devops-ping-tai