首頁

目前文章總數:222 篇

  

最後更新:2026年 03月 07日

0023. Jenkins 如何在限制中完成自動化 : 動態讀取 Git JSON 機器清單的 Jenkins 佈署方案

日期:2026年 03月 14日

標籤: Jenkins Continuous Integration(CI) Ubuntu Linux Docker Docker-Compose Container

摘要:Jenkins


應用所需:1. jenkins 主機(本篇用Windows作業系統示範)
     2. 佈署站點的Linux機器(本篇用Centos 7示範)
解決問題:在受限環境下,不能安裝 Ansiable 等 Jenkins 插件時,如何用原生的 .Json 檔管理機器配置 + Pipeline腳本,實現自動化部署
相關參考:Jenkins × Ubuntu:用 SSH 與 SSH Agent 安全操控遠端主機的兩種實戰配置
基本介紹:本篇分為 4 大部分。
第一部分:環境實務上限制說明
第二部分:環境準備
第三部分:Pipeline配置
第四部分:Demo 實際部署






第一部分:環境實務上限制說明

Step 1:問題說明 - 安全性高的環境

以 Jenkins 為例,「不能隨意安裝插件」通常不是技術問題,而是風險控管策略。
尤其在 金融業、遊戲業、區塊鏈或大型 SaaS 公司更常見,主要考量以下:


1. 資安風險

Jenkins Plugin 特性 第三方開源、由不同團隊維護、更新頻率不一
風險點 插件可能有 CVE 漏洞
  插件可能偷偷對外連線
  插件可能可以讀取 Credentials


2. 供應鏈攻擊

企業經營考量 插件作者帳號被入侵
  惡意版本被上傳
  插件被植入後門


3. 版本相依與穩定性風險

Jenkins plugin 之間有依賴樹 包含升級核心套件、升級其他 plugin、造成 pipeline 失效、造成既有 job 全掛
影響舉例 某團隊裝了一個 plugin 隔天整個 Jenkins master 無法啟動


4. 權限與憑證安全問題

Jenkins plugin 具有 讀 Credentials、操作 Secret、讀 Git token、存取 Kubernetes config
影響舉例 整個 production 環境憑證可能被竊取。


公司不讓隨便裝 Jenkins plugin 的核心原因 : Jenkins 通常是公司生產環境的最高權限入口

Step 2:管理的意圖

通常是以下幾個部門在限制:

1. 降低公司整體風險 攻擊 = 整家公司被攻擊 ; 所以相關部門寧願慢一點,也不要出事。
2. 避免環境失控 中央控管可以避免 : 不一致環境 , 不可重現 , 升級地獄
3. 建立標準化 CI/CD 平台 透過標準化才能 : 可預測 , 可回滾 , 可審計


因此安全性的控管是有必要,需要運維團隊對 DevOps 有一定程度的理解,才能對 Jenkins 權限的安全性 & 效率 達到平衡

Step 3:本篇目的

在受限環境下,不能安裝 Ansiable 等 Jenkins 插件時,如何 取得 .Json 檔案管理機器配置 + Pipeline腳本,實現自動化部署
對於機器的配置,需要登入的帳號、密碼,若寫在 Pipeline 腳本中會難以維護 & 擴充
本篇的做法 : 將機器配置的資訊放在 .git 中,如何從 Pipeline 腳本中取得後,進行正確的部署

Step 4:本篇目的 - 圖示說明

原本機器配置資訊都在 Pipeline 腳本中寫死

最終的調整 : 會先將機器位置資訊從 .git 中取出,在用 pipeline 將 Json 轉成正確的機器位置




第二部分:環境準備

Step 1:確保有 Jenkins Server

為了說明,準備一個全新的 Jenkins Server 環境,除了官方建議的預設套件外,不安裝其他擴充插件
Windows Desktop Docker:

容器啟動:


Step 2:配置 SSh Publish位置

在實際生產環境中,公司部門會自己管理 Jenkins 中的 CredentialsSystem
此篇範例先將 Jenkins 中的 System 內的 SSH Servers 設定

Name:MyTargetLinux
Hostname:部署主機位置
Username:部署主機登入 Username
Remote Directory:根目錄




Step 3:集中管理機器配置檔

我們新增了 machine.json 在Git 來源

1. mahcine.json 機器資訊,集中管理 (示意,可以將帳號密碼集中管理,主要取決於部門管理)
2. new.groovy Hardcode 機器資訊,調整前的 pipeline 檔案
3. old.groovy 將機器資訊從 Git 取得後,調整後的 pipeline 檔案


Json 檔案的內容如下,未來有多台機器,可依照所需添加

{ 
  "MachineGroups": [ 
    ["MyTargetLinux"] 
  ]
}






第三部分:Pipeline配置

Step 1:Hardcode 的 Piepline(調整前)

我們新增了 old.groovy 在Git 來源
※部署用的程式代碼
※程式代碼對應相關參考:
Jenkins × Ubuntu:用 SSH 與 SSH Agent 安全操控遠端主機的兩種實戰配置

pipeline {
  agent any
  
  // 環境變數 【實務上依照自己的機器配置替換】
  environment {       		
        PROJECT_NAME = "GetDockerContainerEnvironmentParameterExample"// 專案名稱
		PROJECT_NAME_FOR_DOCKER = "getdockercontainerenvironmentparameterexample"// DockerName 強制小寫
		GIT_SOURCE_REPOSITORY = "https://github.com/gotoa1234/MyBlogExample.git"// 專案來源
		TARGET_MACHINE_IP = "172.29.155.94"// 對應的部署機器IP
		TARGET_MACHINE_CREDENTIAL = "MyTargetLinux"// 對應部署機器的SSH Server Name
  
        // 設定檔 Git 倉庫資訊
        CONFIG_GIT_URL = "https://github.com/gotoa1234/JenkinsCICDConfigure.git"
        CONFIG_FILE_PATH = "2026_03_14_GitGetPublishSetting/machine.json"
  }  
  
  // 定義單一建置時可異動參數 【實務上依照自己的機器配置替換參數】
  parameters {
        string(name: 'GIT_HASH_TAG', defaultValue: '', description: '指定發布的GIT Hash 標籤(雜湊版號),預設 head 表示更新最新代碼')
        string(name: 'ASPNETCORE_ENVIRONMENT', defaultValue: 'Development', description: 'ASP NETCORE 環境變數')
        string(name: 'DOTNET_ENVIRONMENT', defaultValue: 'Development', description: 'DOTNET NETCORE 環境變數')
        string(name: 'security_key', defaultValue: 'ProductionKey', description: 'IT 管理金鑰')
  }
  
  stages {
      
    // step 1. start
    stage('Checkout') {
       steps {
            checkout([$class: 'GitSCM', 
                branches: [[name: "remotes/origin/main"]],
                userRemoteConfigs: [[url: "${env.GIT_SOURCE_REPOSITORY}"]]
            ])

            sh """
                git pull origin main
            """

            sh """
                git checkout ${params.GIT_HASH_TAG}
            """
        }
     }
    // step 1. end

    // step 2. start
    stage('2-Publish Main Host') {
	  steps {
	      sshPublisher(publishers: 
	             [sshPublisherDesc(configName: "${env.TARGET_MACHINE_CREDENTIAL}", 
	                                transfers: [
	                                    sshTransfer(cleanRemote: true, 
	                                                   excludes: '', 
	                                                execCommand: '', 
	                                                execTimeout: 120000, 
	                                                    flatten: false, 
	                                              makeEmptyDirs: false, 
	                                          noDefaultExcludes: false, 
	                                           patternSeparator: '[, ]+', 
	                                            remoteDirectory: "var\\dockerbuildimage\\${PROJECT_NAME}\\", 
	                                         remoteDirectorySDF: false, 
	                                               removePrefix: "${PROJECT_NAME}", 
	                                                sourceFiles: "${PROJECT_NAME}\\**")],
	                     usePromotionTimestamp: false, 
	                   useWorkspaceInPromotion: false, 
	                                   verbose: false)
	             ])
	  }
    }
    // step 2. end

   // step 3. start
    stage('3-Build') {
	  steps {
         sshPublisher(
            failOnError: true,
            publishers: [sshPublisherDesc(
            configName: "${env.TARGET_MACHINE_CREDENTIAL}", 
            transfers: [sshTransfer(
                excludes: '', 
                execCommand: "cd /var/dockerbuildimage/${env.PROJECT_NAME} && \
                              dotnet publish ${PROJECT_NAME}.csproj -c Release -o publish --disable-build-servers", 
                execTimeout: 120000, 
                patternSeparator: '[, ]+')], 
            verbose: false)])
	  }
    }   
    // step 3. end
    
	// step 4. start
    stage('4-Publish DockerFile') {
	  steps {
	      sshPublisher(publishers: 
		         [sshPublisherDesc(configName: "${env.TARGET_MACHINE_CREDENTIAL}", 
				                    transfers: [
									    sshTransfer(cleanRemote: false, 
										               excludes: '', 
													execCommand: '', 
													execTimeout: 120000, 
													    flatten: false, 
												  makeEmptyDirs: false, 
											  noDefaultExcludes: false, 
											   patternSeparator: '[, ]+', 
											    remoteDirectory: "var\\dockerbuildimage\\${PROJECT_NAME}", 
											 remoteDirectorySDF: false, 
											       removePrefix: "${PROJECT_NAME}", 
												    sourceFiles: "${PROJECT_NAME}\\Dockerfile")], 
					   usePromotionTimestamp: false, 
					 useWorkspaceInPromotion: false, 
					                 verbose: false)
				])
	  }
    }
    // step 4. end
	
	// step 5. start
    stage('5-Build Image Remotely') {
      steps {
	    sh """
		   echo cd /var/dockerbuildimage/${env.PROJECT_NAME} 
		   echo docker build --no-cache -t ${env.PROJECT_NAME_FOR_DOCKER} .
		   echo docker tag ${env.PROJECT_NAME_FOR_DOCKER}:latest ${env.PROJECT_NAME_FOR_DOCKER}:hash_${params.GIT_HASH_TAG}
		   """
	  
        sshPublisher(
            failOnError: true,
            publishers: [sshPublisherDesc(
            configName: "${env.TARGET_MACHINE_CREDENTIAL}", 
            transfers: [sshTransfer(
                excludes: '', 
                execCommand: "cd /var/dockerbuildimage/${env.PROJECT_NAME} && \
                              docker build --no-cache -t ${env.PROJECT_NAME_FOR_DOCKER} . && \
                              docker tag ${env.PROJECT_NAME_FOR_DOCKER}:latest ${env.PROJECT_NAME_FOR_DOCKER}:hash_${params.GIT_HASH_TAG}", 
                execTimeout: 120000, 
                patternSeparator: '[, ]+')], 
            verbose: true)])
      }
    }
	// step 5. end
	
	// step 6. start
    stage('6-ReConstruct Container') {
      steps {

        sshPublisher(
            failOnError: true,
            publishers: [sshPublisherDesc(
            configName: "${env.TARGET_MACHINE_CREDENTIAL}", 
            transfers: [sshTransfer(
                excludes: '', 
                execCommand: """
                    sudo docker stop ${env.PROJECT_NAME_FOR_DOCKER} || true && \\
                    sudo docker rm ${env.PROJECT_NAME_FOR_DOCKER} || true && \\
                    sudo docker run -d \\
                      --name ${env.PROJECT_NAME_FOR_DOCKER} \\
                      -e ASPNETCORE_ENVIRONMENT=${params.ASPNETCORE_ENVIRONMENT} \\
                      -e DOTNET_ENVIRONMENT=${params.DOTNET_ENVIRONMENT} \\
                      -e security_key=${params.security_key} \\
                      -p 8090:8080 -p 8190:8081 \\
                      --mount type=bind,source=/var/dockervolumes/${env.PROJECT_NAME}/appsettings.json,target=/app/appsettings.json \\
                      --mount type=bind,source=/var/dockervolumes/${env.PROJECT_NAME}/appsettings.Development.json,target=/app/appsettings.Development.json \\
                      ${env.PROJECT_NAME_FOR_DOCKER}:latest
                """,
                execTimeout: 120000, 
                patternSeparator: '[, ]+')], 
            verbose: true)])
      }
    }   
	// step 6. end
	
	// step 7. start
    stage('7-Image Purne') {
      steps {
         sshPublisher(
             failOnError: true,
             publishers: [sshPublisherDesc(
             configName: "${env.TARGET_MACHINE_CREDENTIAL}", 
             transfers: [sshTransfer(
                 excludes: '', 
                 execCommand: "docker image prune -f", 
                 execTimeout: 120000, 
                 patternSeparator: '[, ]+')], 
             verbose: false)])
      }
    }
	// step 7. end
  }
}



Step 2:Git 取得機器位置

我們新增了 new.groovy 在Git 來源
與 old.groovy 的差異在於從 git 檔案中取得配置,並且解析 Json
目前示意 如何解析,未來實際應用,需要擴增多台機器時仍依需求而調整腳本


  environment {       		
	    //... 其餘重複略
		
        // 設定檔 Git 倉庫資訊
        CONFIG_GIT_URL = "https://github.com/gotoa1234/JenkinsCICDConfigure.git"
        CONFIG_FILE_PATH = "2026_03_14_GitGetPublishSetting/machine.json"

		//... 其餘重複略
  }  
  

//... 略
    // step 1. end

	// step Extention. start
    stage('1-Load Config from Git') {
         steps {
             script {                 
                 dir('pipeline-config') {
                     checkout([
                         $class: 'GitSCM',
                         branches: [[name: '*/main']],
                         userRemoteConfigs: [[
                             url: "${CONFIG_GIT_URL}"
                         ]]
                     ])
                     echo "Deployed Image Tag: ${CONFIG_FILE_PATH}"
                     // 讀取 JSON 
                     def jsonContent = readFile("${CONFIG_FILE_PATH}")                     
                     def jsonSlurper = new groovy.json.JsonSlurper()
                     def configJson = jsonSlurper.parseText(jsonContent)
                     
                     echo "Deployed Image Tag: ${configJson}"
                     // 取得機器 - 要對應 Jenkins 的 Setting 裡面的配置名稱
                     env.CONFIG_JSON = groovy.json.JsonOutput.toJson(configJson)
					 def config = jsonSlurper.parseText(env.CONFIG_JSON)
                     env.MachineGroups = config.MachineGroups 
					 echo "config.MachineGroups  => ${config.MachineGroups}"
                 }
             }
         }
    }
	
    stage('Deploy to Both Servers in Parallel') {
        steps {
            script {                                               
                    // 物件替換為 Json - 在單詞邊界加上雙引號
                    def machinesStr = env.MachineGroups
                    def myjson = machinesStr.replaceAll(/([\w-]+)/, '"$1"')                 
                    def targetMachineGroups = new groovy.json.JsonSlurper().parseText(myjson)   
                    
    
                    // 逐組處理
                    for (int groupIdx = 0; groupIdx < targetMachineGroups.size(); groupIdx++) {
                        def servers = targetMachineGroups[groupIdx]
						// 目前結構下只有一筆,未來要擴充機器更新 .json 檔數量,並調整成多筆
                        env.TARGET_MACHINE_CREDENTIAL = servers[0]

                    }                     
            }
        }
    } 
    // step Extention. end
    
	// step 2. start
//... 略	





第四部分:Demo 實際部署

Step 1:建立 Pipeline Job

建立 Pipeline Job ,並且使用 new.groovy 腳本




Step 2:部署完成

部署完畢後,如配置正確,目標部署機器可正常訪問,應可成功

檢查部署機器的 Container 容器,也在 Running 中
集中管理機器資訊在 .json 檔案中,並且不安裝任何插件