1. 程式人生 > >使用 Jenkins + Ansible 實現 Spring Boot 自動化部署101

使用 Jenkins + Ansible 實現 Spring Boot 自動化部署101

本文首發於:Jenkins 中文社群

本文要點:

  1. 設計一條 Spring Boot 最基本的流水線:包括構建、製品上傳、部署。
  2. 使用 Docker 容器執行構建邏輯。
  3. 自動化整個實驗環境:包括 Jenkins 的配置,Jenkins agent 的配置等。

1. 程式碼倉庫安排

本次實驗涉及以下多個程式碼倉庫:

% tree -L 1
├── 1-cd-platform # 實驗環境相關程式碼
├── 1-env-conf # 環境配置程式碼-實現配置獨立
└── 1-springboot # Spring Boot 應用的程式碼及其部署程式碼


1-springboot 的目錄結構如下:

% cd 1-springboot
% tree -L 1 
├── Jenkinsfile # 流水線程式碼
├── README.md
├── deploy # 部署程式碼
├── pom.xml 
└── src # 業務程式碼


所有程式碼,均放在 GitHub:https://github.com/cd-in-practice

2. 實驗環境準備

筆者使用 Docker Compose + Vagrant 進行實驗。環境包括以下幾個系統:

  • Jenkins * 1 Jenkins master,全自動安裝外掛、預設使用者名稱密碼:admin/admin。
  • Jenkins agent * 2 Jenkins agent 執行在 Docker 容器中,共啟動兩個。
  • Artifactory * 1 一個商業版的製品庫。筆者申請了一個 30 天的商業版。

使用 Vagrant 是為了啟動虛擬機器,用於部署 Spring Boot 應用。如果你的開發機器無法使用 Vagrant,使用 VirtualBox 也可以達到同樣的效果。但是有一點需要注意,那就是網路。如果在虛擬機器中要訪問 Docker 容器內提供的服務,需要在 DNS 上或者 hosts 上做相應的調整。所有的虛擬機器的映象使用 Centos7。

另,接下來筆者的所有教程都將使用 Artifactory 作為製品庫。在此申明,筆者沒有收 JFrog——研發 Artifactory 產品的公司——任何廣告費。

筆者只是想試用商業產品,以便了解商業產品是如何應對製品管理問題的。

啟動 Artifactory 後,需要新增 “Virtual Repository” 及 “Local Repository”。具體請檢視 Artifactory 的官方文件。如果你當前使用的是 Nexus,參考本教程,做一些調整,問題也不大。

如果想使用已有製品庫,可以修改 1-cd-platform 倉庫中的 settings-docker.xml 檔案,指向自己的製品庫。

實驗環境近期的總體結構圖如下:

實驗環境近期的總體結構

之所以說是“近期的”,是因為上圖與本篇介紹的結構有小差異。本篇文章還沒有介紹 Nginx 與 Springboot 配置共用,但是總體不影響讀者理解。

3. Springboot 應用流水線介紹

Springboot 流水線有兩個階段:

  1. 構建並上傳製品
  2. 部署應用

流水線的所有邏輯都寫在 Jenkinsfile 檔案。接下來,分別介紹這兩個階段。

3.1 構建並上傳製品

此階段核心程式碼:

docker.image('jenkins-docker-maven:3.6.1-jdk8')
.inside("--network 1-cd-platform_cd-in-practice -v $HOME/.m2:/root/.m2") {
    sh """
      mvn versions:set -DnewVersion=${APP_VERSION}
      mvn clean test package
      mvn deploy
    """
}


它首先啟動一個裝有 Maven 的容器,然後在容器內執行編譯、單元測試、釋出製品的操作。

mvn versions:set -DnewVersion=${APP_VERSION} 的作用是更改 pom.xml 檔案中的版本。這樣就可以實現每次提交對應一個版本的效果。

3.2 部署應用

注意: 這部分需要一些 Ansible 的知識。

首先看部署指令碼的入口 1-springboot/deploy/playbook.yaml

---
- hosts: "springboot"
  become: yes
  roles:
    - {"role": "ansible-role-java", "java_home": "{{JAVA_HOME}}"}
    - springboot


先安裝 JDK,再安裝 Spring Boot。JDK 的安裝,使用了現成 Ansible role: https://github.com/geerlingguy/ansible-role-java

重點在 Spring Boot 部署的核心邏輯。它主要包含以下幾部分:

  1. 建立應用目錄。
  2. 從製品庫下載指定版本的製品。
  3. 生成 Systemd service 檔案(實現服務化)。
  4. 啟動服務。

以上步驟實現在 1-springboot/deploy/roles/springboot 中。

流水線的部署階段的核心程式碼如下:

docker.image('williamyeh/ansible:centos7').inside("--network 1-cd-platform_cd-in-practice") {

  checkout([$class: 'GitSCM', branches: [[name: "master"]], doGenerateSubmoduleConfigurations: false,
            extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: "env-conf"]], submoduleCfg: [],
            userRemoteConfigs: [[url: "https://github.com/cd-in-practice/1-env-conf.git"]]])

  sh "ls -al"

  sh """
    ansible-playbook --syntax-check deploy/playbook.yaml -i env-conf/dev
    ansible-playbook deploy/playbook.yaml -i env-conf/dev --extra-vars '{"app_version": "${APP_VERSION}"}'
  """
}


它首先將配置變數倉庫的程式碼 clone 下來,然後對 playbook 進行語法上的檢查,最後執行 ansible-playbook 命令進行部署。--extra-vars 引數的 app_version 用於指定將要部署的應用的版本。

3.3 實現簡易指定版本部署

1-springboot/Jenkinsfile 中實現了簡易的指定版本部署。核心程式碼如下:

  1. 流水線接受引數
parameters { string(name: 'SPECIFIC_APP_VERSION', 
defaultValue: '', description: '') }


  1. 如果指定了版本,則跳過構建階段,直接執行部署階段
stage("build and upload"){
      // 如果不指定部署版本,則執行構建
      when {
        expression{ return params.SPECIFIC_APP_VERSION == "" }
      }
      // 構建並上傳製品的邏輯
      steps{...}
}


之所以說是“簡易”,是因為部署時只指定了製品的版本,並沒有指定的部署邏輯和配置的版本。這三者的版本要同步,部署才真正做到準確。

4. 配置管理

所有的配置項都放在 1-env-conf 倉庫中。Ansible 執行部署時會讀取此倉庫的配置。

將配置放在 Git 倉庫中有兩個好處:

  1. 配置版本化。
  2. 任何配置的更改都可以被審查。

有好處並不代表沒有成本。那就是開發人員必須開始關心軟體的配置(筆者發現不少開發者忽視配置項管理的重要性。)。

本文重點不在配置管理,後面會有文章重點介紹。

5. 實驗環境詳細介紹

事實上,整個實驗,工作量大的地方有兩處:一是 Spring Boot 流水線本身的設計;二是整個實驗環境的自動化。讀者朋友之所以能一兩條簡單的命令就能啟動整個實驗環境,是因為筆者做了很多自動化的工作。筆者認為有必要在本篇介紹這些工作。接下來的文章將不再詳細介紹。

5.1 解決流水線中啟動的 Docker 容器無法訪問 http://artifactory

流水線中,我們需要將製品上傳到 artifactory(settings.xml 配置的倉庫地址是 http://artifactory:8081),但是發現無法解析 host。這是因為流水線中的 Docker 容器所在網路與 Docker compose 建立的網路不同。所以,解決辦法就是讓流水線中的 Docker 容器加入到 Docker compose 的網路。

具體解決辦法就是在啟動容器時,加入引數:--network 1-cd-platform_cd-in-practice

5.2 Jenkins 初次啟動初始化

在沒有做任何設定的情況啟動 Jenkins,會出現一個配置嚮導。這個過程必須是手工的。筆者希望這一步也是自動化的。Jenkins 啟動時會執行 init.groovy.d/目錄下的 Groovy 指令碼。

5.3 虛擬機器中如何能訪問到 http://artifactory

http://artifactory 部署在 Docker 容器中。Spring Boot 應用的製品要部署到虛擬機器中,需要從 http://artifactory 中拉取製品,也就是要在虛擬機器裡訪問容器裡提供的服務。虛擬機器與容器之間的網路是不通的。那怎麼辦呢?筆者的解決方案是使用宿主機的 IP 做中轉。具體做法就是在虛擬機器中加一條 host 記錄:

machine.vm.provision "shell" do |s|
    s.inline = "echo '192.168.52.1 artifactory' >> /etc/hosts"
end


以上是使用了 Vagrant 的 provision 技術,在執行命令 vagrant up 啟動虛擬機器時,就自動執行那段內聯 shell。192.168.52.1 是虛擬宿主機的 IP。所以,虛擬機器裡訪問 http://artifactory:8081 時,實際上訪問的是 http://192.168.52.1:8081

網路結構可以總結為下圖:

後記

目前遺留問題:

  1. 部署時製品版本、配置版本、部署程式碼版本沒有同步。
  2. Springboot 的配置是寫死在製品中的,沒有實現製品與配置項的分離。

這些遺留問題在後期會逐個解決。就像現實一樣,經常需要面對各種遺留專案的遺留問題。

附錄

  1. 使用 Jenkins + Ansible 實現自動化部署 Nginx:https://jenkins-zh.cn/wechat/articles/2019/04/2019-04-25-jenkins-ansible-nginx/
  2. 簡單易懂 Ansible 系列 —— 解決了什麼:https://showme.codes/2017-06-12/ansible-introduce/