iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0
Kubernetes

當 Quarkus 想要騎乘駱駝並用8腳章魚掌控舵手 系列 第 14

Quarkus 實現 Hot-reload 之 Downward API 與 RBAC

  • 分享至 

  • xImage
  •  

先來談談 Apache Camel,Apache Camel 是一個基於已知Enterprise Integration Patterns的多功能開源整合框架。

Apache Camel 是一個小型函式庫,具有最少的依賴性,可以輕鬆整合到任何 Java 應用程式中。本次的範例將會使用

  • file-watch
  • Kubernetes

等進行整合。直接獻上程式碼

第一部分是 watch file,主要目的是監聽某個目錄或檔案來取得其是否有異動。對於 Apache Camel 是有路由概念,而基本的定義是使用 URI 方式。URI 中 events 定義當只有被監聽的檔案或目錄只要是 CREATE 或 MODIFY 的動作都將視為異動,且並使用 useFileHashing 參數來說明使用哈希的方式。最後定義了 .to("direct:restart-replica") 表示當這個異動被監聽後,他會將這訊息往 restart-replica 這個目的地進行下一個步驟。

@ApplicationScoped
public class FileWatchRouter extends RouteBuilder {
    @Inject
    WatchFileConfig watchFileConfig;

    @Override
    public void configure() throws Exception {
        fromF("file-watch://%s?autoCreate=false&events=CREATE,MODIFY&recursive=false&useFileHashing=true", watchFileConfig.dir().path())
            .routeId("Watch File")
            .routeGroup("hotreload")
            .log("File event: ${header.CamelFileEventType} occurred on file ${header.CamelFileName} at ${header.CamelFileLastModified}")
            .to("direct:restart-replica");
    }
    
}

自定義連線至 Kubernetes 配置。


@Singleton
public class KubernetesClientProducer {

    @Inject
    KubernetesConfig kubernetesConfig;
   
    @Produces
    public KubernetesClient kubernetesClient() {
        Config config = new ConfigBuilder()
            .withMasterUrl(kubernetesConfig.kubernetes().apiServer())
            .withOauthToken(kubernetesConfig.kubernetes().token())
            .withNamespace(kubernetesConfig.kubernetes().namespace())
            .withTrustCerts(kubernetesConfig.kubernetes().trustCerts())
        .build();
        
        return new KubernetesClientBuilder().withConfig(config).build();
    }
}

下一個是 restart-replica,而這個路由是負責獲取 Kubernetes 上 Deployment 資源。其會使用令牌 (token) 與 Kubernetes API 進行請求。最後會再把訊息路由到 triggerrestart-replica 這個路由其實沒什麼特別重要。

@ApplicationScoped
public class KubernetesRouter extends RouteBuilder {
        @Inject
        Config config;

        @Inject
        KubernetesConfig kubernetesConfig;

        @Inject
        KubernetesClientProducer kubernetesClientProducer;

        @Override
        public void configure() throws Exception {
                kubernetesClientProducer.kubernetesClient();
                from("direct:restart-replica")
                                .routeId("restart-replica")
                                .routeGroup("hotreload")
                                .setHeader(KubernetesConstants.KUBERNETES_OPERATION, constant("getDeployment"))
                                .setHeader(KubernetesConstants.KUBERNETES_NAMESPACE_NAME,
                                                constant(config.getValue("k8s.kubernetes.namespace",
                                                                String.class)))
                                .setHeader(KubernetesConstants.KUBERNETES_DEPLOYMENT_NAME,
                                                constant(kubernetesConfig.kubernetes().deployment().name()))
                                .toF("kubernetes-deployments:///?kubernetesClient=#kubernetesClient&namespace=%s",
                                config.getValue("k8s.kubernetes.namespace", String.class))
                                .log(LoggingLevel.INFO, """
                                                upgrade replica: %s
                                                """.formatted("${header.CamelKubernetesDeploymentName}"))
                                .to("direct:trigger");
        }

}

下面是 trigger 路由,搭配 @Consume 註解,這邊比較重要的是執行 rollout restart 的動作。而這邊會傳遞 restart-replica 路由訊息,這邊使用 Exchange 來獲取。

    @Consume("direct:trigger")
    public void getAllNodeStatus(Exchange exchange) {
        var deployName = (String)exchange.getIn().getHeader(KubernetesConstants.KUBERNETES_DEPLOYMENT_NAME);
        var namespace = (String)exchange.getIn().getHeader(KubernetesConstants.KUBERNETES_NAMESPACE_NAME);
        kubernetesClient.apps().deployments().inNamespace(namespace).withName(deployName).rolling().restart();
        log.info("rollout deployment");
    }

到這邊整題流程是下面,當按只要被監控到異動最後會觸發 Deployment 資源的重新啟動。

File <----file-watch-----> restart-replica -----> trigger -----> rollout restart Deployment

在程式碼中,會需要與 Kubernetes API 進行交互,而我們應用程式是一個 Deployment 資源,此時則需要透過 ServiceAccount 資源的令牌來存取。而該令牌需要且最小權限則需要透過 RBAC 進行定義。因此定義以下注意重啟屬於 patch

定義 ServiceAccount 此資源綁定至 Deployment 是沒有令牌的需要透過類型為 kubernetes.io/service-account-token 的 Secret 資源進行綁定。或搭配 automountServiceAccountToken 進行掛載至 Pod 中。如果地端驗證可用 kubernetes.io/service-account-token 方式比較方便。

中間定義了 ClusterRole 其用來限制對 Deployment 的操作。最後透過 RoleBindingClusterRoleServiceAccount 綁再一起。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: ithomerbaclab
  namespace: default
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ithomerbaclab-role
rules:
- apiGroups:
  - 'apps'
  resources:
  - 'deployments'
  verbs:
  - 'update'
  - 'get'
  - 'patch'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ithomerbaclab-role-binding
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: ithomerbaclab-role
subjects:
- kind: ServiceAccount
  name: ithomerbaclab
  namespace: default
---
apiVersion: v1
kind: Secret
metadata:
  name: ithomerbaclab-token
  namespace: default
  annotations:
    kubernetes.io/service-account.name: ithomerbaclab
type: kubernetes.io/service-account-token

最後定義此 Quarkus 應用程式的部署資源。屆時 Deployment 會掛載 ithomerbaclabconfigMap 資源。該資源是應用程式的配置檔,預設上 configMap 資源異動對於 Deployment 資源來說是會更新,但應用程式行為不會,因此需要透過上面定義的程式碼來實現熱重載(hot-reload),也就是重新啟動服務。

---
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    app.quarkus.io/quarkus-version: 3.14.1
    app.quarkus.io/build-timestamp: 2024-09-01 - 06:26:50 +0000
  labels:
    app.kubernetes.io/name: ithomerbaclab
    app.kubernetes.io/version: 1.0.0-SNAPSHOT
    app.kubernetes.io/managed-by: quarkus
  name: ithomerbaclab
  namespace: default
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 8080
  selector:
    app.kubernetes.io/name: ithomerbaclab
    app.kubernetes.io/version: 1.0.0-SNAPSHOT
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    app.quarkus.io/quarkus-version: 3.14.1
    app.quarkus.io/build-timestamp: 2024-09-01 - 06:26:50 +0000
  labels:
    app.kubernetes.io/name: ithomerbaclab
    app.kubernetes.io/version: 1.0.0-SNAPSHOT
    app.kubernetes.io/managed-by: quarkus
  name: ithomerbaclab
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/version: 1.0.0-SNAPSHOT
      app.kubernetes.io/name: ithomerbaclab
  template:
    metadata:
      annotations:
        app.quarkus.io/quarkus-version: 3.14.1
        app.quarkus.io/build-timestamp: 2024-09-01 - 06:26:50 +0000
      labels:
        app.kubernetes.io/managed-by: quarkus
        app.kubernetes.io/version: 1.0.0-SNAPSHOT
        app.kubernetes.io/name: ithomerbaclab
      namespace: default
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 2000
      hostAliases:
      - ip: "172.25.150.200"
        hostnames:
        - "ithome.cch.com"
      containers:
        - env:
            - name: KUBERNETES_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: DEPLOYMENT_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.labels['app.kubernetes.io/name']
          image: registry.hub.docker.com/cch0124/ithomerbaclab:day12.1
          imagePullPolicy: Always
          securityContext:
            privileged: false
            allowPrivilegeEscalation: false
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /q/health/live
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 5
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 10
          name: ithomerbaclab
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /q/health/ready
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 5
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 10
          startupProbe:
            failureThreshold: 3
            httpGet:
              path: /q/health/started
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 5
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 10
          volumeMounts:
          - name: config-volume
            mountPath: /home/jboss/config
      volumes:
      - name: config-volume
        configMap:
          name: ithomerbaclab
      serviceAccountName: ithomerbaclab
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: ithomerbaclab
  namespace: default
data:
  application.yaml: |
    watch:
      dir:
        path: /home/jboss/config
    greeting:
      message: "ithome-Day14"
    k8s: 
      kubernetes:
        deployment:
          name: ${DEPLOYMENT_NAME}
        namespace: ${KUBERNETES_NAMESPACE}
        trust-certs: true
        api-server: "https://kubernetes.default.svc"
        token: ...
    quarkus:
      config:
        locations: /home/jboss/config/application.yaml

當定義好後,透過 kubectl apply 部署。

Quarkus 應用程式要重啟需要知道 namespace、Deployment 資源的名稱。這邊使用 Downward API 方式來獲取名稱,並使用環境變數方式讓應用程式容器可以讀取。但能映射的欄位基本上也是有限,並非全部資源都能。關鍵欄位是 fieldRef,指定了環境變數的值來源,表示從 Pod 的某個字段中獲取值。另外是 fieldPath 指定了要獲取值的具體字段路徑,metadata.namespace 表示從 Pod 的元數據(metadata)中獲取 namespace 的值,這個 namespace 就是 Pod 所屬的 namespace 名稱。

從上面的 ithomerbaclab ConfigMap 資源,來看 quarkus.kubernetes-client.namespacek8s.kubernetes.deployment.name 都會分別參照以下的環境變數。這樣是不是超級靈活。

        - env:
            - name: KUBERNETES_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: DEPLOYMENT_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.labels['app.kubernetes.io/name']

今天到這邊,一天寫一篇累死。明天會進行觸發與驗證。


上一篇
Kubernetes 的 Role-Based Access Control
下一篇
Quarkus 實現 Hot-reload 之 Downward API 與 RBAC - 續
系列文
當 Quarkus 想要騎乘駱駝並用8腳章魚掌控舵手 31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言