
背景
ASP.NET Core默認(rèn)的配置文件定義在appsetings.json和appsettings.{Environment}.json文件中。
這里面有一個(gè)問(wèn)題就是,在使用容器部署時(shí),每次修改配置文件都需要重新構(gòu)建鏡像。當(dāng)然你也可能會(huì)說(shuō),我的配置文件很穩(wěn)定不需要修改,但你又如何確保配置文件中一些機(jī)密配置的安全問(wèn)題呢?比如暴露了你的遠(yuǎn)程數(shù)據(jù)庫(kù)的連接信息,哪天被員工不小心刪庫(kù)跑路了呢?
那接下來(lái)就來(lái)講解下如何在.NET Core 中正確使用ConfigMap。
ConfigMap/Secret
K8S中引入了ConfigMap/Secret來(lái)存儲(chǔ)配置數(shù)據(jù),分別用于存儲(chǔ)非敏感信息和敏感信息。其目的在于將應(yīng)用和配置解耦,以確保容器化應(yīng)用程序的可移植性。
創(chuàng)建 ConfigMap
玩耍K8S,請(qǐng)先自行準(zhǔn)備環(huán)境,Win10用戶可以參考我的上篇文章ASP.NET Core 借助 K8S 玩轉(zhuǎn)容器編排來(lái)準(zhǔn)備環(huán)境。
ConfigMap的創(chuàng)建很簡(jiǎn)單,一句命令就可以直接將appsettings.json文件轉(zhuǎn)換為ConfigMap。
PS:使用K8S一定要善用幫助命令,比如執(zhí)行kubectl create configmap -h,你就可以了解到多種創(chuàng)建ConfigMap的方式。
> kubectl create configmap -h
Create a configmap based on a file, directory, or specified literal value.
A single configmap may package one or more key/value pairs.
When creating a configmap based on a file, the key will default to the basename of the file, and the value will default
to the file content. If the basename is an invalid key, you may specify an alternate key.
When creating a configmap based on a directory, each file whose basename is a valid key in the directory will be
packaged into the configmap. Any directory entries except regular files are ignored (e.g. subdirectories, symlinks,
devices, pipes, etc).
Aliases:
configmap, cm
Examples:
# Create a new configmap named my-config based on folder bar
kubectl create configmap my-config --from-file=path/to/bar
# Create a new configmap named my-config with specified keys instead of file basenames on disk
kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt
# Create a new configmap named my-config with key1=config1 and key2=config2
kubectl create configmap my-config --from-literal=key1=config1 --from-literal=key2=config2
# Create a new configmap named my-config from the key=value pairs in the file
kubectl create configmap my-config --from-file=path/to/bar
# Create a new configmap named my-config from an env file
kubectl create configmap my-config --from-env-file=path/to/bar.env
其中我們可以看到可以通過(guò)指定--from-file來(lái)從指定文件創(chuàng)建。
kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt
Let's have a try!
1. 先行創(chuàng)建示例項(xiàng)目:dotnet new mvc -n K8S.NETCore.ConfigMap
2. 默認(rèn)包含兩個(gè)配置文件appsettings.json和appsettings.Development.json

3. 先來(lái)嘗試將appsettings.json轉(zhuǎn)換為ConfigMap:
> cd K8S.NETCore.ConfigMap
# 創(chuàng)建一個(gè)namespace,此步可選
> kubectl create namespace demo
namespace "demo" created
# -n變量指定configmap創(chuàng)建到哪個(gè)namespace下
> kubectl create configmap appsettings --from-file=appsettings.json=./appsettings.json -n demo
configmap "appsettings" created
# 查看剛剛創(chuàng)建的configmap,-o指定輸出的格式
> kubectl get configmap appsettings -n demo -o yaml
apiVersion: v1
data:
appsettings.json: "{\r\n \"Logging\": {\r\n \"LogLevel\": {\r\n \"Default\":
\"Warning\"\r\n }\r\n },\r\n \"AllowedHosts\": \"*\"\r\n}\r\n"
kind: ConfigMap
metadata:
creationTimestamp: null
name: appsettings
namespace: demo
從上面的輸出結(jié)果來(lái)看,其中包含了\r\n換行符,顯然不是我們想要的結(jié)果。猜測(cè)是因?yàn)閃indows和Linux系統(tǒng)換行符的差異導(dǎo)致的。先來(lái)插播下?lián)Q行符的知識(shí):
CR:Carriage Return,對(duì)應(yīng)ASCII中轉(zhuǎn)義字符\r,表示回車
LF:Linefeed,對(duì)應(yīng)ASCII中轉(zhuǎn)義字符\n,表示換行
CRLF:Carriage Return & Linefeed,\r\n,表示回車并換行
眾所周知,Windows操作系統(tǒng)采用兩個(gè)字符來(lái)進(jìn)行換行,即CRLF;Unix/Linux/Mac OS X操作系統(tǒng)采用單個(gè)字符LF來(lái)進(jìn)行換行;
所以解決方式就很簡(jiǎn)單,將換行符切換為L(zhǎng)inux系統(tǒng)的\n即可。操作方式很簡(jiǎn)單:
對(duì)于VS Code 只需要按圖下所示操作即可,點(diǎn)擊右下角的CRLF,選擇LF即可。

對(duì)于VS,如果VS打開(kāi)json文件有下面的提示,直接切換就好。沒(méi)有,可以安裝Line Endings Unifier擴(kuò)展來(lái)統(tǒng)一處理。

# 先刪除之前創(chuàng)建的configmap
> kubectl delete configmap appsettings -n demo
> kubectl create configmap appsettings --from-file=appsettings.json=./appsettings.json -n demo
configmap "appsettings" created
> kubectl get configmap appsettings -n demo -o yaml
apiVersion: v1
data:
appsettings.json: |
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
kind: ConfigMap
metadata:
creationTimestamp: null
name: appsettings
namespace: demo
現(xiàn)在ConfigMap的格式正常了。下面我們嘗試把appsettings.Development.json也合并到一個(gè)ConfigMap中。
> kubectl delete configmap appsettings -n demo
> kubectl create configmap appsettings --from-file=appsettings.json=./appsettings.json --from-file=appsettings.Development.json=./appsettings.Development.json -n demo
configmap "appsettings" created
> kubectl get configmap appsettings -n demo -o yaml
apiVersion: v1
data:
appsettings.Development.json: |
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
appsettings.json: |
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
kind: ConfigMap
metadata:
creationTimestamp: null
name: appsettings
namespace: demo
PS:
- 如果你的配置文件包含多余的空格,則生成的ConfigMap可能就會(huì)包含
\n字符,就像這樣:
appsettings.Development.json: "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Debug\",\n \"System\": \"Information\",\n \"Microsoft\": \"Information\"\n \ }\n }\n} \n"。解決辦法就是保存文件時(shí)記得格式化文件就好了,或者手動(dòng)刪除多余空格。- 創(chuàng)建ConfigMap的時(shí)候可以指定
--dry-run參數(shù)進(jìn)行試運(yùn)行,避免直接創(chuàng)建到服務(wù)器。- 從文件創(chuàng)建ConfigMap時(shí),可以不指定Key,默認(rèn)會(huì)以文件名為Key。
kubectl create configmap appsettings --from-file=./appsettings.json --from-file=./appsettings.Development.json -n demo --dry-run -o yaml
至此,完成了appsetting到configmap的切換。
應(yīng)用 ConfigMap
ConfigMap的應(yīng)用很簡(jiǎn)單,只需要將configmap掛載到容器內(nèi)的獨(dú)立目錄即可。
先來(lái)看一下借助VS幫生成的Dockerfile。
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS build
WORKDIR /src
COPY ["K8S.NETCore.ConfigMap.csproj", ""]
RUN dotnet restore "./K8S.NETCore.ConfigMap.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "K8S.NETCore.ConfigMap.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "K8S.NETCore.ConfigMap.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "K8S.NETCore.ConfigMap.dll"]
可以看出文件中定義的WORKDIR /app指定的工作目錄為/app,所以需要把ConfigMap掛載到/app目錄下。先執(zhí)行docker build -t k8s.netcore.configmap:dev . 構(gòu)建鏡像。
我們來(lái)新建一個(gè)configmap-deploy.yaml文件配置如下:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: k8s-configmap-demo
spec:
selector:
matchLabels:
app: k8s-configmap-demo
template:
metadata:
labels:
app: k8s-configmap-demo
spec:
containers:
- name: k8s-configmap-demo
image: k8s.netcore.configmap:dev
imagePullPolicy: IfNotPresent
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
volumeMounts:
- mountPath: /app/appsettings.json
name: test
readOnly: true
subPath: appsettings.json
- mountPath: /app/appsettings.Development.json
name: test
readOnly: true
subPath: appsettings.Development.json
volumes:
- configMap:
defaultMode: 420
name: appsettings
name: test
這里有必要解釋兩個(gè)參數(shù):
- volumes:-configMap:指定引用哪個(gè)ConfigMap
- volumeMounts:用來(lái)指定將ConfigMap中的配置掛載到容器的哪個(gè)路徑
- subPath:用來(lái)指定引用ConfigMap的哪個(gè)配置節(jié)點(diǎn)。
創(chuàng)建Deployment之前先修改下ConfigMap的配置,以方便確認(rèn)最終成功從ConfigMap掛載配置。將Logging:LogLevel:Default:節(jié)點(diǎn)的默認(rèn)值改為Error。
> kubectl edit configmap appsettings -n demo
configmap/appsettings edited
> kubectl get cm appsettings -n demo -o yaml
apiVersion: v1
data:
appsettings.Development.json: |-
{
"Logging": {
"LogLevel": {
"Default": "Error",
"System": "Information",
"Microsoft": "Information"
}
}
}
appsettings.json: |
{
"Logging": {
"LogLevel": {
"Default": "Error"
}
},
"AllowedHosts": "*"
}
kind: ConfigMap
metadata:
creationTimestamp: "2019-09-02T22:50:14Z"
name: appsettings
namespace: demo
resourceVersion: "445219"
selfLink: /api/v1/namespaces/demo/configmaps/appsettings
uid: 07048d5a-cdd4-11e9-ad6d-00155d3a3103
修改完畢后,執(zhí)行后續(xù)命令來(lái)創(chuàng)建Deployment,并驗(yàn)證。
# 創(chuàng)建deployment
> kubectl apply -f .\k8s-deploy.yaml -n demo
deployment.extensions/k8s-configmap-demo created
# 獲取創(chuàng)建的pod
> kubectl get pods -n demo
NAME READY STATUS RESTARTS AGE
k8s-configmap-demo-7cfbdfff67-xdrcx 1/1 Running 0 12s
# 進(jìn)入pod內(nèi)部
> kubectl exec -it k8s-configmap-demo-7cfbdfff67-xdrcx /bin/bash -n demo
root@k8s-configmap-demo-7cfbdfff67-xdrcx:/app# cat appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Error"
}
},
"AllowedHosts": "*"
}
root@k8s-configmap-demo-7cfbdfff67-xdrcx:/app# cat appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Error",
"System": "Information",
"Microsoft": "Information"
}
}
}
從以上輸出可以看出,默認(rèn)的配置項(xiàng)已被ConfigMap的配置覆蓋。
熱更新
以Volume方式掛載的ConfigMap支持熱更新(大概需要10s左右)。但一種情況例外,就是指定subPath的情況下,更新ConfigMap,容器中掛載的ConfigMap是不會(huì)自動(dòng)更新的。
A container using a ConfigMap as a subPath volume will not receive ConfigMap updates.
對(duì)于這種情況,也很好處理,將ConfigMap掛載到/app目錄下一個(gè)單獨(dú)目錄就好,比如掛載到/app/config目錄,然后修改配置文件的加載路徑即可。
hostBuilder.ConfigureAppConfiguration((context, builder) =>
{
builder.SetBasePath(Path.Join(AppContext.BaseDirectory, "config"))
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", true, true);
});
最后
本文就.NET Core如何應(yīng)用ConfigMap進(jìn)行了詳細(xì)的介紹。其中最關(guān)鍵在于appsettings.json到ConfigMap的轉(zhuǎn)換,以及掛載目錄的指定。希望對(duì)你有所幫助。
而至于Secret的應(yīng)用,原理相通了,關(guān)鍵在于Secret的生成,這里就交給你自己探索了。