分享程式代碼相關筆記
目前文章總數:190 篇
最後更新:2025年 07月 26日
1. 開發簽入代碼 | : | 開發人員將代碼準備完成,依照 2. IT 人員提供的金鑰參數 |
2. 開發 & IT 協調 | : | 依照兩邊協調的結果,開發人員持續用相同的參數取得金鑰 (※本篇環境參數用 security_key ) |
3. IT 人員進行部署準備 | : | 依照協調的結果,IT 執行部署準備 (金鑰保管等) |
4. IT 使用金鑰部署執行 | : | 正式執行部署,IT 人員會知道金鑰 |
5. 回傳金鑰結果 | : | 部署完成後,開發人員的代碼依照與 IT 人員協定的方式,獲取生產金鑰 |
主要是解決不讓開發人員 -> 直接取得金鑰 ,而是透過 IT 維運人員進行保管
※這邊身分可以依照實務狀況替換
打開範例代碼後,架構基本分成以下:
1. Web 控制器 | : | 檢視控制器,用來驗證說明,並用 SHA 256 結合字串加密 |
2. Model 模型 | : | 呼叫 C# 的內建靜態方法,取得環境變數上的參數 |
3. launchSettings | : | 本機 Debug 模式時,預設內建的環境參數 |
4. DockerFile | : | 部署時預設使用的 DockerFile |
5. JenkinsPipeline | : | Jenkins 執行部署的腳本,Build Image 然後運行 Container |
檢視 Index 首頁,主要呼叫 ViewModel
並且將取得的 sercurity_key 做 SHA256 加密,我們假定字串都是 Account:Louis
※實務上這個字串就是會員資料庫的帳號,要結合金鑰做加密結果
public IActionResult Index()
{
var getEnviromentInfo = new ContainerEnvironmentModel();
getEnviromentInfo.SecurityKeyHashMAC = ComputeHMACSHA256("Account:Louis", getEnviromentInfo.SecurityKey);
return View(getEnviromentInfo);
}
/// <summary>
/// HMAC SHA 256 加密
/// </summary>
/// <param name="message">原始字串</param>
/// <param name="key">金鑰</param>
/// <returns></returns>
static string ComputeHMACSHA256(string message, string key)
{
byte[] keyBytes = Encoding.UTF8.GetBytes(key);
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
using (HMACSHA256 hmac = new HMACSHA256(keyBytes))
{
byte[] hashBytes = hmac.ComputeHash(messageBytes);
StringBuilder builder = new StringBuilder();
foreach (var b in hashBytes)
{
builder.Append(b.ToString("x2"));
}
return builder.ToString();
}
}
使用 C# 內建的靜態方法,呼叫環境變數上的參數 ASPNETCORE_ENVIRONMENT , DOTNET_ENVIRONMENT , security_key
其中 security_key 是 IT 人員會提供的一個接口,負責的開發工程師要取得此參數
namespace GetDockerContainerEnvironmentParameterExample.Models
{
public class ContainerEnvironmentModel
{
public string AspNetCoreEnvironment =>
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty;
public string DotNetEnvironment =>
Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty;
public string SecurityKey =>
Environment.GetEnvironmentVariable("security_key") ?? string.Empty;
public string SecurityKeyHashMAC { get; set; }
}
}
為了便於本機驗證,可以在 launchSettings.json 中配置對應的 Environment 參數,可以確保有抓到正確的資料
如下,為 http、https、IIS Express 執行 Debug 偵錯時,增加攜帶參數
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:56870",
"sslPort": 44326
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5246",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"security_key": "MyLocalKey"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7186;http://localhost:5246",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"security_key": "MyLocalKey"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"security_key": "MyLocalKey"
}
}
}
}
運行容器都需要 DockerFile 或 Docker-compose.yml ,提供一個內建的檔案
對於 IT 人員的生產部署,是可以用來參考,但 IT 人員可以自行決定定義參數,並與開發人員溝通可使用參數
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
ENV ASPNETCORE_ENVIRONMENT=DEV
ENV DOTNET_ENVIRONMENT=DEV
ENV security_key=DEVTestKey
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
COPY ./publish .
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone
ENTRYPOINT ["dotnet", "GetDockerContainerEnvironmentParameterExample.dll"]
Jenkins Pipeline 腳本定義了環境配置加上 7 個 Stage 步驟
※此腳本符合小型專案架構,若中、大型、請採用 K8S 並且有統一的 Hub 處理管理Image(DockerHub / Harbor)
1. 基本配置 | : | 部署的專案名稱、容器名稱、 Git 檔案來源、部署位置 |
2. Stage 1 - Checkout | : | 從版控上取得代碼,這裡是從 Github 上抓取範例代碼 |
3. Stage 2 - Building | : | 建置該代碼,使用 dotnet build ,確保專案是可編譯 |
4. Stage 3 - Publish Main Host | : | 將編譯後的檔案部署到遠端機器 |
5. Stage 4 - Publish DockerFile | : | 將 DockerFile 放在正確的位置,可以依照實務狀況,決定是否此步驟需要 |
6. Stage 5 - Build Image Remotely | : | 建立 Docker Image |
7. Stage 6 - ReConstruct Container | : | 重建容器,即使有相同的容器運行,也會安全的重建 (非零停機部署的實踐) |
8. Stage 7 - Image Purne | : | 清空不需要的 Image ,節省空間 |
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 = "192.168.51.93"// 對應的部署機器IP
TARGET_MACHINE_CREDENTIAL = "DeployMechineUbuntu"// 對應部署機器的SSH Server Name
}
// 定義單一建置時可異動參數 【實務上依照自己的機器配置替換參數】
parameters {
string(name: 'GIT_HASH_TAG', defaultValue: '', description: '指定發布的GIT Hash 標籤(雜湊版號),預設 head 表示更新最新代碼')
string(name: 'ASPNETCORE_ENVIRONMENT', defaultValue: 'ProductionASPCORE', description: 'ASP NETCORE 環境變數')
string(name: 'DOTNET_ENVIRONMENT', defaultValue: 'ProductionDotnet', 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('Building') {
steps {
script {
sh """
dotnet publish ${PROJECT_NAME}/${PROJECT_NAME}.csproj -c Release -o publish/${PROJECT_NAME} --disable-build-servers
"""
}
}
}
// step 2. end
// step 3. start
stage('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}\\publish",
remoteDirectorySDF: false,
removePrefix: "publish\\${PROJECT_NAME}",
sourceFiles: "publish\\${PROJECT_NAME}\\**")],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false)
])
}
}
// step 3. end
// step 4. start
stage('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('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: false)])
}
}
// step 5. end
// step 6. start
stage('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} && \
docker rm ${env.PROJECT_NAME_FOR_DOCKER} || true && \
docker run -e ASPNETCORE_ENVIRONMENT=${params.ASPNETCORE_ENVIRONMENT} -e DOTNET_ENVIRONMENT=${params.DOTNET_ENVIRONMENT} -e security_key=${params.security_key} --name ${env.PROJECT_NAME_FOR_DOCKER}_A -d -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: false)])
}
}
// step 6. end
// step 7. start
stage('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
}
}
於 Jenkins 上建立 1 個 Job ,模擬 IT 人員會進行的部署
對應範例代碼的 JenkinsPipeline 腳本,會出現以下環境參數, IT 人員建置時可決定參數添加何者
※要隱藏參數可參考第五部分
預期會順利部署
部署完成後,容器若順利執行,可以執行 Inspect 或 Portainer 檢視
部署的參數都在容器上
對於開發人員來說,代碼端只要與 IT 人員確認好參數,都可以正確取得到 環境參數
通常會將 security_key 進行 HMAC 加密使用
※這邊是示範可以直接顯示,實務上不可能讓 security_key 明文 顯示
IT 是一個 Team 通常並不會讓所有人都看到 security_key 的明文,因此保管上必須做處理
只讓最初的生產金鑰的 IT 人員知道真正的明文
為了解決明文的高風險問題,可先進入 Jenkins 管理中
選擇 Credentials
進入 Global
選擇 Add Credentials
產生 security_key 專屬的密文項目
將產生的 GUID 複製 (也可以自定義)
將 security_key 從 parameters 改為放在 environment 中
並且替換成以下
security_key = credentials("您的 security_key ID")
因為從 parmeter 改為 environment ,因此建構容器的地方也需同步調整
使用新腳本再次進行部署
這次建置不在出現 security_key 的選項,並且在 Pipeline 腳本中,也以 credential 的方式隱藏
順利部署後,除了建立金鑰的 IT 人員之外,很大程度減少明文被檢視
※代碼仍需走查,因為還是有機會從代碼端讀出此參數 / 也有可能從生產部署機器檢視此金鑰