使用 Java Operator SDK 建立 Kubernetes Operator
一、簡介
在本教程中,我們將介紹 Kubernetes Operator 的概念以及如何使用 Java Operator SDK 實作它們。為了說明這一點,我們將實作一個操作符,該操作符可以簡化將 OWASP 的Dependency-Track應用程式實例部署到叢集的任務。
2.什麼是 Kubernetes Operator?
用 Kubernetes 的話來說,Operator 是一個軟體元件,通常部署在叢集中,用於管理一組資源的生命週期。它擴展了本機控制器集(例如副本集和作業控制器),以將複雜或相互關聯的元件作為單一託管單元進行管理。
讓我們來看看使用運算符的一些常見用例:
- 將應用程式部署到叢集時實施最佳實踐
- 追蹤並從意外刪除/更改應用程式使用的資源中恢復
- 自動執行與應用程式相關的內務任務,例如定期備份和清理
- 自動配置叢集外資源-例如儲存桶和證書
- 整體改善應用程式開發人員與 Kubernetes 互動時的體驗
- 透過允許使用者僅管理應用程式級資源而不是低階資源(例如 Pod 和部署)來提高整體安全性
- 將特定於應用程式的資源(也稱為自訂資源定義)公開為 Kubernetes 資源
最後一個用例非常有趣。它允許解決方案提供者利用圍繞常規 Kubernetes 資源的現有實踐來管理特定於應用程式的資源。主要好處是任何採用此應用程式的人都可以使用現有的基礎設施即程式碼工具。
為了讓我們了解不同類型的可用運算符,我們可以查看OperatorHub.io網站。在那裡,我們將找到流行資料庫的操作員、API 管理器、開發工具等。
3. 算子和CRD
自訂資源定義,簡稱 CRD,是 Kubernetes 的擴充機制,它允許我們在叢集中儲存結構化資料。與該平台上的幾乎所有內容一樣,CRD 定義本身也是一種資源。
此元定義描述給定 CRD 實例的範圍(基於命名空間的或全域的)以及用於驗證 CRD 實例的架構。註冊後,使用者可以像本地實例一樣建立 CRD 實例。叢集管理員還可以將 CRD 作為角色定義的一部分,從而僅向授權使用者和應用程式授予存取權限。
現在,在其本身上註冊 CRD 並不意味著 Kubernetes 會以任何方式使用它。對於 Kubernetes 而言,CRD 執行個體只是其內部資料庫中的一個項目。由於標準 Kubernetes 本機控制器都不知道如何處理它,因此什麼也不會發生。
這就是操作員的控制器部分發揮作用的地方。部署後,它將監視與相應自訂資源相關的事件並做出回應。
在這裡, act
部分是重要的。該術語受到控制理論的啟發,可以總結為下圖:
4. 實現一個算子
讓我們回顧一下建立 Operator 需要完成的主要任務:
- 定義我們將透過操作員管理的目標資源的模型
- 建立一個 CRD 以擷取部署這些資源所需的參數
- 建立一個控制器來監視叢集中與已註冊 CRD 相關的事件
在本教程中,我們將為OWASP旗艦專案Dependency-Track實作一個運算子。該應用程式允許用戶追蹤整個組織使用的庫中的漏洞,從而允許軟體安全專業人員評估和解決發現的任何問題。
Dependency-Track 的Docker 發行版由兩個元件組成:API 和前端服務,每個元件都有自己的映像。將它們部署到 Kubernetes 叢集時,常見的做法是將每個鏡像包裝在一個Deployment中,以管理執行這些鏡像的Pod 。
然而,這還不是全部。我們還需要一些額外的資源來實現完整的解決方案:
此外,我們還需要正確設定liveness/readiness探針、資源限制和其他普通使用者不應該關心的細節。
讓我們看看如何使用 Operator 來簡化此任務。
5. 定義模型
我們的操作員將專注於運行依賴追蹤系統所需的最少資源。幸運的是,所提供的圖像具有合理的預設值,因此我們只需要一個資訊:用於存取應用程式的外部 URL。
這暫時保留了資料庫和儲存設置,但是一旦我們掌握了正確的基礎知識,添加這些功能就很簡單了。
但是,我們將為定制留出一些餘地。特別是,允許使用者覆蓋用於部署的映像和版本非常方便,因為它們正在不斷發展。
讓我們看一下 Dependency-Track 安裝的圖表,其中顯示了其所有元件:
所需的模型參數為:
- 將在其中建立資源的 Kubernetes 命名空間
- 用於安裝和衍生每個元件名稱的名稱
- 與 Ingress 資源一起使用的主機名
- 新增至 Ingress 的可選額外註釋。我們需要這些,因為一些雲端提供者(例如 AWS)需要它們才能正常運作。
6. 控制器專案設定
下一步是手動定義 CRD 模式,但由於我們使用的是 Java Operator SDK,因此這將得到處理。相反,讓我們轉向控制器專案本身。
我們將從標準 Spring Boot 3 WebFlux 應用程式開始並新增所需的依賴項:
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-spring-boot-starter</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-spring-boot-starter-test</artifactId>
<version>5.4.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>crd-generator-apt</artifactId>
<version>6.9.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.77</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.77</version>
</dependency>
這些相依性的最新版本可在 Maven Central 上找到:
-
[operator-framework-spring-boot-starter](https://mvnrepository.com/artifact/io.javaoperatorsdk/operator-framework-spring-boot-starter)
-
[operator-framework-spring-boot-starter-test](https://mvnrepository.com/artifact/io.javaoperatorsdk/operator-framework-spring-boot-starter-test)
-
[crd-generator-apt](https://mvnrepository.com/artifact/io.fabric8/crd-generator-apt)
-
[bcprov-jdk18on](https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on)
-
[bcpkix-jdk18on](https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk18on)
前兩個分別需要實作和測試運算子。 crd-generator-apt
是從附註解的類別產生 CRD 定義的註解處理器。最後, bouncycastle函式庫需要支援現代加密標準。
請注意新增到測試啟動器中的排除項。我們刪除了log4j
依賴項,因為它與logback
衝突。
7. 實施主要資源
主資源類別代表使用者將部署到叢集中的 CRD 。它使用@Group
和@Version
註釋進行標識,以便 CRD 註釋處理器可以在編譯時產生適當的 CRD 定義:
@Group("com.baeldung")
@Version("v1")
public class DeptrackResource extends CustomResource<DeptrackSpec, DeptrackStatus> implements Namespaced {
@JsonIgnore
public String getFrontendServiceName() {
return this.getMetadata().getName() + "-" + DeptrackFrontendServiceResource.COMPONENT;
}
@JsonIgnore
public String getApiServerServiceName() {
return this.getMetadata().getName() + "-" + DeptrackApiServerServiceResource.COMPONENT;
}
}
在這裡,我們利用 SDK 的類別CustomResource
來實作我們的DeptrackResource
。除了基底類別之外,我們還使用Namespaced
,這是一個標記接口,它通知註釋處理器我們的 CRD 實例將部署到 Kubernetes 命名空間。
我們只為該類別添加了兩個輔助方法,稍後我們將使用它們來衍生前端和 API 服務的名稱。在這種情況下,我們需要@JsonIgnore
註釋,以避免在對 Kubernetes 的 API 呼叫中序列化/反序列化實例 CRD 實例時出現問題。
8. 規格和狀態等級
CustomResource
類別需要兩個模板參數:
- 具有我們模型支援的參數的規範類
- 包含有關係統動態資訊的狀態類
在我們的例子中,我們只有幾個參數,所以這個規範非常簡單:
public class DeptrackSpec {
private String apiServerImage = "dependencytrack/apiserver";
private String apiServerVersion = "";
private String frontendImage = "dependencytrack/frontend";
private String frontendVersion = "";
private String ingressHostname;
private Map<String, String> ingressAnnotations;
// ... getters/setters omitted
}
至於狀態類,我們只需擴充ObservedGenerationAwareStatus
:
public class DeptrackStatus extends ObservedGenerationAwareStatus {
}
使用這種方法,SDK 將在每次更新時自動增加observedGeneration
狀態欄位。這是控制器用來追蹤資源變化的常見做法。
9. 調節器
接下來,我們需要建立一個Reconciler
類,負責管理 Dependency-Track 系統的整體狀態。我們的類別必須實作此接口,該接口將資源類別作為參數:
@ControllerConfiguration(dependents = {
@Dependent(name = DeptrackApiServerDeploymentResource.COMPONENT, type = DeptrackApiServerDeploymentResource.class),
@Dependent(name = DeptrackFrontendDeploymentResource.COMPONENT, type = DeptrackFrontendDeploymentResource.class),
@Dependent(name = DeptrackApiServerServiceResource.COMPONENT, type = DeptrackApiServerServiceResource.class),
@Dependent(name = DeptrackFrontendServiceResource.COMPONENT, type = DeptrackFrontendServiceResource.class),
@Dependent(type = DeptrackIngressResource.class)
})
@Component
public class DeptrackOperatorReconciler implements Reconciler<DeptrackResource> {
@Override
public UpdateControl<DeptrackResource> reconcile(DeptrackResource resource, Context<DeptrackResource> context) throws Exception {
return UpdateControl.noUpdate();
}
}
這裡的關鍵點是@ControllerConfiguration
註解。其dependents
屬性列出了其生命週期將連結到主資源的各個資源。
對於部署和服務,除了資源type
之外,我們還需要指定name
屬性來區分它們。至於 Ingress,不需要名稱,因為每個部署的 Dependency-Track 資源只有一個名稱。
請注意,我們還添加了@Component
註釋。我們需要這個,以便操作員的自動配置邏輯檢測協調器並將其添加到其內部註冊表中。
10. 依賴資源類
對於我們希望透過 CRD 部署在叢集中建立的每個資源,我們需要實作一個KubernetesDependentResource
類別。這些類別必須使用@KubernetesDependent
進行註釋,並負責管理這些資源的生命週期以回應主要資源的變更。
SDK 提供了CRUDKubernetesDependentResource
實用程式類,可以大幅簡化此任務。我們只需要重寫desired()
方法,它會傳回依賴資源的期望狀態的描述:
@KubernetesDependent(resourceDiscriminator = DeptrackApiServerDeploymentResource.Discriminator.class)
public class DeptrackApiServerDeploymentResource extends CRUDKubernetesDependentResource<Deployment, DeptrackResource> {
public static final String COMPONENT = "api-server";
private Deployment template;
public DeptrackApiServerDeploymentResource() {
super(Deployment.class);
this.template = BuilderHelper.loadTemplate(Deployment.class, "templates/api-server-deployment.yaml");
}
@Override
protected Deployment desired(DeptrackResource primary, Context<DeptrackResource> context) {
ObjectMeta meta = fromPrimary(primary, COMPONENT)
.build();
return new DeploymentBuilder(template)
.withMetadata(meta)
.withSpec(buildSpec(primary, meta))
.build();
}
private DeploymentSpec buildSpec(DeptrackResource primary, ObjectMeta primaryMeta) {
return new DeploymentSpecBuilder()
.withSelector(buildSelector(primaryMeta.getLabels()))
.withReplicas(1)
.withTemplate(buildPodTemplate(primary,primaryMeta))
.build();
}
private LabelSelector buildSelector(Map<String, String> labels) {
return new LabelSelectorBuilder()
.addToMatchLabels(labels)
.build();
}
private PodTemplateSpec buildPodTemplate(DeptrackResource primary, ObjectMeta primaryMeta) {
return new PodTemplateSpecBuilder()
.withMetadata(primaryMeta)
.withSpec(buildPodSpec(primary))
.build();
}
private PodSpec buildPodSpec(DeptrackResource primary) {
String imageVersion = StringUtils.hasText(primary.getSpec().getApiServerVersion()) ?
":" + primary.getSpec().getApiServerVersion().trim() : "";
String imageName = StringUtils.hasText(primary.getSpec().getApiServerImage()) ?
primary.getSpec().getApiServerImage().trim() : Constants.DEFAULT_API_SERVER_IMAGE;
return new PodSpecBuilder(template.getSpec().getTemplate().getSpec())
.editContainer(0)
.withImage(imageName + imageVersion)
.and()
.build();
}
}
在本例中,我們使用可用的建構器類別建立Deployment
。資料本身部分來自從傳遞給方法的主要資源中提取的元資料以及在初始化時讀取的模板。這種方法允許我們使用已經經過戰鬥驗證的現有部署作為模板,並僅修改真正需要的內容。
最後,我們需要指定一個Discriminator
類,運算子引擎在處理來自多個同類來源的事件時使用它來定位正確的資源類別。在這裡,我們將使用基於框架中可用的ResourceIDMatcherDiscriminator
實用程式類別的實作:
class Discriminator extends ResourceIDMatcherDiscriminator<Deployment, DeptrackResource> {
public Discriminator() {
super(COMPONENT, (p) -> new ResourceID(
p.getMetadata().getName() + "-" + COMPONENT,
p.getMetadata().getNamespace()));
}
}
此實用程式類別需要事件來源名稱和映射函數。後者採用主資源實例並傳回關聯元件的資源識別碼(命名空間+名稱)。
由於所有資源類別共享相同的基本結構,因此我們不會在這裡重現它們。相反,我們建議檢查原始程式碼以了解每個資源是如何建構的。
11. 本地測試
由於控制器只是一個常規的 Spring 應用程序,因此我們可以使用常規測試框架為我們的應用程式建立單元和整合測試。
Java Operator SDK 還提供了方便的模擬 Kubernetes 實現,有助於簡單的測試案例。要在測試類別中使用此模擬實現,我們將@EnableMockOperator
與標準@SpringBootTest
一起使用:
@SpringBootTest
@EnableMockOperator(crdPaths = "classpath:META-INF/fabric8/deptrackresources.com.baeldung-v1.yml")
class ApplicationUnitTest {
@Autowired
KubernetesClient client;
@Test
void whenContextLoaded_thenCrdRegistered() {
assertThat(
client
.apiextensions()
.v1()
.customResourceDefinitions()
.withName("deptrackresources.com.baeldung")
.get())
.isNotNull();
}
}
crdPath
屬性包含註解處理器建立 CRD 定義 YAML 檔案的位置。在測試初始化過程中,mock Kubernetes 服務會自動註冊它,以便我們可以建立 CRD 實例並檢查是否正確建立了預期的資源。
SDK的測試基礎架構也配置了一個Kubernetes
客戶端,我們可以用它來模擬部署並檢查是否正確建立了預期的資源。請注意,不需要工作的 Kubernetes 叢集!
12. 打包和部署
為了打包我們的控制器項目,我們可以使用Dockerfile
,或者更好的是 Spring Boot 的build-image
目標。我們推薦後者,因為它確保映像遵循有關安全性和層組織的推薦最佳實踐。
將映像發佈到本機或遠端註冊表後,我們必須建立一個 YAML 清單以將控制器部署到現有叢集中。
此清單包含管理控制器和支援資源的部署本身:
- CRD 定義
- 控制器「駐留」的命名空間
- 列出控制器所使用的所有 API 的叢集角色
- 服務帳戶
- 將角色連結到帳戶的集群角色綁定
產生的清單可在我們的 GitHub 儲存庫中找到。
13.CRD部署測試
為了完成我們的教程,讓我們建立一個簡單的 Dependency-Track CRD 清單並部署它。我們將使用專用名稱空間(“測試”)並公開它。
在我們的測試中,我們使用偵聽 IP 位址 172.31.42.16 的本機 Kubernetes,因此我們將使用deptrack.172.31.42.16.nip.io
作為主機名稱。 NIP.IO是一項 DNS 服務,它將*.1.2.3.4.nip.io
形式的任何主機名稱解析為 IP 位址1.2.3.4
,因此我們不需要設定任何 DNS 項目。
讓我們來看看部署清單:
apiVersion: com.baeldung/v1
kind: DeptrackResource
metadata:
namespace: test
name: deptrack1
labels:
project: tutorials
annotations:
author: Baeldung
spec:
ingressHostname: deptrack.172.31.42.16.nip.io
現在,讓我們使用kubectl
部署它:
$ kubectl apply -f k8s/test-resource.yaml
deptrackresource.com.baeldung/deptrack1 created
我們可以取得控制器日誌來查看它對 CRD 創建的反應並創建了依賴資源:
$ kubectl get --namespace test deployments
NAME READY UP-TO-DATE AVAILABLE AGE
deptrack1-api-server 0/1 1 0 62s
deptrack1-frontend 1/1 1 1 62s
$ kubectl get --namespace test services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
deptrack1-frontend-service ClusterIP 10.43.122.76 <none> 8080/TCP 2m17s
$ kubectl get --namespace test ingresses
NAME CLASS HOSTS ADDRESS PORTS AGE
deptrack1-ingress traefik deptrack.172.31.42.16.nip.io 172.31.42.16 80 2m53s
正如預期的那樣,測試命名空間現在有兩個部署、兩個服務和一個入口。如果我們打開瀏覽器並指向 https://deptrack.172.31.42.16.nip.io,我們將看到應用程式的登入頁面。這表示該解決方案已正確部署。
為了完成測試,讓我們刪除 CRD:
$ kubectl delete --namespace test deptrackresource/deptrack1
deptrackresource.com.baeldung "deptrack1" deleted
由於 Kubernetes 知道哪些資源連結到 CRD,因此它們也將被刪除:
$ kubectl get --namespace test deployments
No resources found in test namespace.
14. 結論
在本教學中,我們展示如何使用 Java Operator SDK 實作基本的 Kubernetes Operator 。儘管所需的樣板程式碼數量很多,但實作起來很簡單。
此外,SDK 還負責處理狀態協調的大部分繁重工作,讓開發人員需要定義處理複雜部署的最佳方法。
與往常一樣,所有程式碼都可以在 GitHub 上取得。