1. 程式人生 > >.NET Core 使用 K8S ConfigMap的正確姿勢

.NET Core 使用 K8S ConfigMap的正確姿勢

背景

ASP.NET Core預設的配置檔案定義在appsetings.jsonappsettings.{Environment}.json檔案中。
這裡面有一個問題就是,在使用容器部署時,每次修改配置檔案都需要重新構建映象。當然你也可能會說,我的配置檔案很穩定不需要修改,但你又如何確保配置檔案中一些機密配置的安全問題呢?比如暴露了你的遠端資料庫的連線資訊,哪天被員工不小心刪庫跑路了呢?
那接下來就來講解下如何在.NET Core 中正確使用ConfigMap。

ConfigMap/Secret

K8S中引入了ConfigMap/Secret來儲存配置資料,分別用於儲存非敏感資訊和敏感資訊。其目的在於將應用和配置解耦,以確保容器化應用程式的可移植性。

建立 ConfigMap

玩耍K8S,請先自行準備環境,Win10使用者可以參考我的上篇文章ASP.NET Core 藉助 K8S 玩轉容器編排來準備環境。

ConfigMap的建立很簡單,一句命令就可以直接將appsettings.json檔案轉換為ConfigMap。

PS:使用K8S一定要善用幫助命令,比如執行kubectl create configmap -h,你就可以瞭解到多種建立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

其中我們可以看到可以通過指定--from-file來從指定檔案建立。

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. 先行建立示例專案:dotnet new mvc -n K8S.NETCore.ConfigMap
2. 預設包含兩個配置檔案appsettings.jsonappsettings.Development.json

3. 先來嘗試將appsettings.json轉換為ConfigMap:

> cd K8S.NETCore.ConfigMap
# 建立一個namespace,此步可選
> kubectl create namespace demo 
namespace "demo" created
# -n變數指定configmap建立到哪個namespace下
> kubectl create configmap appsettings --from-file=appsettings.json=./appsettings.json -n demo
configmap "appsettings" created
# 檢視剛剛建立的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

從上面的輸出結果來看,其中包含了\r\n換行符,顯然不是我們想要的結果。猜測是因為Windows和Linux系統換行符的差異導致的。先來插播下換行符的知識:

CR:Carriage Return,對應ASCII中轉義字元\r,表示回車
LF:Linefeed,對應ASCII中轉義字元\n,表示換行
CRLF:Carriage Return & Linefeed,\r\n,表示回車並換行
眾所周知,Windows作業系統採用兩個字元來進行換行,即CRLF;Unix/Linux/Mac OS X作業系統採用單個字元LF來進行換行;

所以解決方式就很簡單,將換行符切換為Linux系統的\n即可。操作方式很簡單:
對於VS Code 只需要按圖下所示操作即可,點選右下角的CRLF,選擇LF即可。

對於VS,如果VS開啟json檔案有下面的提示,直接切換就好。沒有,可以安裝Line Endings Unifier擴充套件來統一處理。

# 先刪除之前建立的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

現在ConfigMap的格式正常了。下面我們嘗試把appsettings.Development.json也合併到一個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:

  1. 如果你的配置檔案包含多餘的空格,則生成的ConfigMap可能就會包含\n字元,就像這樣:
    appsettings.Development.json: "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Debug\",\n \"System\": \"Information\",\n \"Microsoft\": \"Information\"\n \ }\n }\n} \n"。解決辦法就是儲存檔案時記得格式化檔案就好了,或者手動刪除多餘空格。
  2. 建立ConfigMap的時候可以指定--dry-run引數進行試執行,避免直接建立到伺服器。
  3. 從檔案建立ConfigMap時,可以不指定Key,預設會以檔名為Key。
    kubectl create configmap appsettings --from-file=./appsettings.json --from-file=./appsettings.Development.json -n demo --dry-run -o yaml

至此,完成了appsetting到configmap的切換。

應用 ConfigMap

ConfigMap的應用很簡單,只需要將configmap掛載到容器內的獨立目錄即可。

先來看一下藉助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目錄下。先執行docker build -t k8s.netcore.configmap:dev . 構建映象。

我們來新建一個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        

這裡有必要解釋兩個引數:

  1. volumes:-configMap:指定引用哪個ConfigMap
  2. volumeMounts:用來指定將ConfigMap中的配置掛載到容器的哪個路徑
  3. subPath:用來指定引用ConfigMap的哪個配置節點。

建立Deployment之前先修改下ConfigMap的配置,以方便確認最終成功從ConfigMap掛載配置。將Logging:LogLevel:Default:節點的預設值改為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

修改完畢後,執行後續命令來建立Deployment,並驗證。

# 建立deployment
> kubectl apply -f .\k8s-deploy.yaml -n demo
deployment.extensions/k8s-configmap-demo created
# 獲取建立的pod
> kubectl get pods -n demo
NAME                                  READY   STATUS    RESTARTS   AGE
k8s-configmap-demo-7cfbdfff67-xdrcx   1/1     Running   0          12s
# 進入pod內部
> 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"
    }
  }
}

從以上輸出可以看出,預設的配置項已被ConfigMap的配置覆蓋。

熱更新

以Volume方式掛載的ConfigMap支援熱更新(大概需要10s左右)。但一種情況例外,就是指定subPath的情況下,更新ConfigMap,容器中掛載的ConfigMap是不會自動更新的。

 A container using a ConfigMap as a subPath volume will not receive ConfigMap updates.

對於這種情況,也很好處理,將ConfigMap掛載到/app目錄下一個單獨目錄就好,比如掛載到/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如何應用ConfigMap進行了詳細的介紹。其中最關鍵在於appsettings.json到ConfigMap的轉換,以及掛載目錄的指定。希望對你有所幫助。
而至於Secret的應用,原理相通了,關鍵在於Secret的生成,這裡就交給你自己探索了。