1. 序章

Kubernetesでしばらく作業した後、ボイラープレートコードがたくさん含まれていることにすぐに気付くでしょう。 単純なサービスの場合でも、必要なすべての詳細を提供する必要があります。通常は、非常に冗長なYAMLドキュメントの形式を取ります。

また、特定の環境にデプロイされた複数のサービスを処理する場合、それらのYAMLドキュメントには多くの繰り返し要素が含まれる傾向があります。 たとえば、特定のConfigMapまたはいくつかのサイドカーコンテナをすべての展開に追加したい場合があります。

この記事では、DRYの原則に固執し、Kubernetesアドミッションコントローラーを使用してこの繰り返しコードをすべて回避する方法について説明します。

2. アドミッションコントローラーとは何ですか?

アドミッションコントローラーは、認証された後、実行される前にAPIリクエストを前処理するためにKubernetesによって使用されるメカニズムです。

APIサーバープロセス( kube-apiserver )には、API処理の特定の側面をそれぞれ担当する、いくつかの組み込みコントローラーがすでに付属しています。

AllwaysPullImage は良い例です。このアドミッションコントローラーはポッド作成要求を変更するため、通知された値に関係なく、イメージプルポリシーは「常に」になります。 Kubernetesドキュメントには、標準のアドミッションコントローラーの完全なリストが含まれています。

kubeapi-server プロセスの一部として実際に実行される組み込みコントローラーに加えて、Kubernetesは外部アドミッションコントローラーもサポートします。 この場合、アドミッションコントローラーはAPIサーバーからのリクエストを処理する単なるHTTPサービスです。

さらに、これらの外部アドミッションコントローラは動的に追加および削除できるため、ダイナミックアドミッションコントローラと呼ばれます。 これにより、次のような処理パイプラインが作成されます。

ここでは、着信APIリクエストが認証されると、永続化レイヤーに到達するまで、組み込みの各アドミッションコントローラーを通過することがわかります。

3. アドミッションコントローラーの種類

現在、アドミッションコントローラーには2つのタイプがあります。

  • アドミッションコントローラーの変更
  • 検証アドミッションコントローラー

それらの名前が示すように、主な違いは、それぞれが着信要求で実行する処理のタイプです。 ミューティングコントローラーは、リクエストをダウンストリームに渡す前に変更する場合がありますが、検証コントローラーはリクエストを検証することしかできません。

これらのタイプに関する重要なポイントは、APIサーバーがそれらを実行する順序です。つまり、変更するコントローラーが最初に来て、次に検証コントローラーが来ます。 これは理にかなっています。検証は、変更するコントローラーのいずれかによって変更された可能性のある最終的な要求があった場合にのみ発生するためです。

3.1. 入学審査のリクエスト

組み込みのアドミッションコントローラー(変更および検証)は、単純なHTTP要求/応答パターンを使用して外部のアドミッションコントローラーと通信します。

  • リクエスト:requestプロパティで処理するAPI呼び出しを含むAdmissionReviewJSONオブジェクト
  • 応答:responseプロパティに結果を含むAdmissionReviewJSONオブジェクト

リクエストの例を次に示します。

{
  "kind": "AdmissionReview",
  "apiVersion": "admission.k8s.io/v1",
  "request": {
    "uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
    "kind": {
      "group": "apps",
      "version": "v1",
      "kind": "Deployment"
    },
    "resource": {
      "group": "apps",
      "version": "v1",
      "resource": "deployments"
    },
    "requestKind": {
      "group": "apps",
      "version": "v1",
      "kind": "Deployment"
    },
    "requestResource": {
      "group": "apps",
      "version": "v1",
      "resource": "deployments"
    },
    "name": "test-deployment",
    "namespace": "test-namespace",
    "operation": "CREATE",
    "object": {
      "kind": "Deployment",
      ... deployment fields omitted
    },
    "oldObject": null,
    "dryRun": false,
    "options": {
      "kind": "CreateOptions",
      "apiVersion": "meta.k8s.io/v1"
    }
  }
}

利用可能なフィールドの中には、特に重要なものがあります。

  • operation :これは、このリクエストがリソースを作成、変更、または削除するかどうかを示します
  • オブジェクト:処理中のリソースの仕様の詳細。
  • oldObject:リソースを変更または削除する場合、このフィールドには既存のリソースが含まれます

期待される応答もAdmissionReview JSONオブジェクトであり、 response:の代わりにresponseフィールドがあります。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
    "allowed": true,
    "patchType": "JSONPatch",
    "patch": "W3sib3A ... Base64 patch data omitted"
  }
}

応答オブジェクトのフィールドを分析してみましょう。

  • uid :このフィールドの値は、着信requestフィールドに存在する対応するフィールドと一致する必要があります
  • 許可:レビューアクションの結果。 true は、API呼び出し処理が次のステップに進む可能性があることを意味します
  • patchType:アドミッションコントローラーの変更にのみ有効です。 AdmissionReviewリクエストによって返されるパッチタイプを示します
  • patch :着信オブジェクトに適用するパッチ。 次のセクションの詳細

3.2. パッチデータ

変化するアドミッションコントローラーからの応答に存在するパッチフィールドは、要求を続行する前に何を変更する必要があるかをAPIサーバーに通知します。 その値は、APIサーバーが着信API呼び出しの本文を変更するために使用する命令の配列を含むBase64エンコードJSONPatchオブジェクトです。

[
  {
    "op": "add",
    "path": "/spec/template/spec/volumes/-",
    "value":{
      "name": "migration-data",
      "emptyDir": {}
    }
  }
]

この例では、デプロイメント仕様のvolume配列にボリュームを追加する単一の命令があります。 パッチを処理する際の一般的な問題は、元のオブジェクトに要素がすでに存在しない限り、既存の配列に要素を追加する方法がないという事実です。 最も一般的なオブジェクト(デプロイなど)にはオプションの配列が含まれているため、これはKubernetesAPIオブジェクトを処理するときに特に厄介です。

たとえば、前の例は、着信デプロイメントにすでに少なくとも1つのボリュームがある場合にのみ有効です。 そうでない場合は、少し異なる命令を使用する必要があります。

[
  {
    "op": "add",
    "path": "/spec/template/spec/volumes",
    "value": [{
      "name": "migration-data",
      "emptyDir": {}
    }]
  }
]

ここでは、新しい volume フィールドを定義しました。このフィールドの値は、ボリューム定義を含む配列です。 以前は、これが既存の配列に追加していたものであるため、値はオブジェクトでした。

4. ユースケースの例:Wait-For-It

アドミッションコントローラーの予想される動作の基本を理解したので、簡単な例を書いてみましょう。 特にマイクロサービスアーキテクチャを使用している場合に、ランタイムの依存関係を管理する場合のKubernetesの一般的な問題。 たとえば、特定のマイクロサービスがデータベースへのアクセスを必要とする場合、前者がオフラインであるかどうかを開始しても意味がありません。

このような問題に対処するために、ポッドでinitContainerを使用して、メインコンテナを開始する前にこのチェックを行うことができます。 これを行う簡単な方法は、人気のある wait-for-it シェルスクリプトを使用することです。これは、 dockerimageとしても利用できます。

スクリプトは、hostnameおよびportパラメーターを受け取り、それに接続しようとします。 テストが成功すると、コンテナは成功したステータスコードで終了し、ポッドの初期化が続行されます。 それ以外の場合は失敗し、関連するコントローラーは定義されたポリシーに従って再試行を続けます。 この飛行前チェックを外部化することのすばらしい点は、関連するKubernetesサービスが失敗に気付くことです。 その結果、リクエストは送信されず、全体的な復元力が向上する可能性があります。

4.1. アドミッションコントローラーの場合

これは、wait-for-itinitコンテナが追加された一般的な展開です。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      initContainers:
      - name: wait-backend
        image: willwill/wait-for-it
        args:
        -www.google.com:80
      containers: 
      - name: nginx 
        image: nginx:1.14.2 
        ports: 
        - containerPort: 80

(少なくともこの単純な例では)それほど複雑ではありませんが、すべてのデプロイメントに関連するコードを追加することにはいくつかの欠点があります。 特に、依存関係のチェックを正確に行う方法を指定する負担をデプロイメントの作成者に課しています。 代わりに、より良いエクスペリエンスでは、テストする必要があるwhatを定義するだけで済みます。

アドミッションコントローラーを入力します。このユースケースに対処するために、リソース内の特定のアノテーションの存在を検索し、存在する場合はinitContainerを追加する変化するアドミッションコントローラーを記述します。 これは、注釈付きのデプロイメント仕様がどのようになるかを示しています。

apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: frontend 
  labels: 
    app: nginx 
  annotations:
    com.baeldung/wait-for-it: "www.google.com:80"
spec: 
  replicas: 1 
  selector: 
    matchLabels: 
      app: nginx 
  template: 
    metadata: 
      labels: 
        app: nginx 
    spec: 
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
          - containerPort: 80

ここでは、注釈 com.baeldung / wait-for-it を使用して、テストする必要のあるホストとポートを示しています。 ただし、重要なのは、テストの実行方法を教えてくれることではありません。理論的には、展開仕様を変更せずに、任意の方法でテストを変更できます。

それでは、実装に移りましょう。

4.2. プロジェクト構造

前に説明したように、外部アドミッションコントローラは単なるHTTPサービスです。 そのため、基本構造としてSpring Bootプロジェクトを作成します。 この例では、必要なのは Spring Web Reactive スターターだけですが、実際のアプリケーションでは、アクチュエータやなどの機能を追加することも役立つ場合があります。いくつかのCloudConfigの依存関係。

4.3. リクエストの処理

アドミッションリクエストのエントリポイントは、着信ペイロードの処理をサービスに委任する単純なSpringRESTコントローラーです。

@RestController
@RequiredArgsConstructor
public class AdmissionReviewController {

    private final AdmissionService admissionService;

    @PostMapping(path = "/mutate")
    public Mono<AdmissionReviewResponse> processAdmissionReviewRequest(@RequestBody Mono<ObjectNode> request) {
        return request.map((body) -> admissionService.processAdmission(body));
    }
}

ここでは、ObjectNodeを入力パラメーターとして使用しています。 これは、APIサーバーから送信された整形式のJSONを処理しようとすることを意味します。 この緩いアプローチの理由は、この記事の執筆時点では、このペイロードの公式スキーマがまだ公開されていないためです。 この場合、非構造化タイプを使用すると、追加の作業が必要になりますが、特定のKubernetes実装またはバージョンがスローすることを決定した追加フィールドを実装が少しうまく処理できるようになります。

また、リクエストオブジェクトはKubernetes APIで利用可能なリソースのいずれかである可能性があるため、ここに構造を追加しすぎるとそれほど役に立ちません。

4.4. 入場リクエストの変更

処理の要点は、AdmissionServiceクラスで発生します。 これは、単一のパブリックメソッドprocessAdmission。を使用してコントローラーに挿入される@Componentクラスです。このメソッドは、着信レビュー要求を処理し、適切な応答を返します。

完全なコードはオンラインで入手でき、基本的にはJSON操作の長いシーケンスで構成されています。 それらのほとんどは些細なことですが、いくつかの抜粋は説明に値します:

if (admissionControllerProperties.isDisabled()) {
    data = createSimpleAllowedReview(body);
} else if (annotations.isMissingNode()) {
    data = createSimpleAllowedReview(body);
} else {
    data = processAnnotations(body, annotations);
}

まず、なぜ「無効」プロパティを追加するのですか? そうですね、高度に制御された環境では、既存のデプロイメントの構成パラメーターを削除または更新するよりもはるかに簡単に変更できる場合があります @ConfigurationPropertiesメカニズムを使用してこのプロパティを設定しているため、実際の値はさまざまなソースから取得できます。

次に、欠落しているアノテーションをテストします。これは、デプロイメントを変更しないでおく必要があることの兆候として扱います。 このアプローチにより、この場合に必要な「オプトイン」動作が保証されます。

もう1つの興味深いスニペットは、 injectInitContainer()メソッドのJSONPatch生成ロジックからのものです。

JsonNode maybeInitContainers = originalSpec.path("initContainers");
ArrayNode initContainers = 
maybeInitContainers.isMissingNode() ?
  om.createArrayNode() : (ArrayNode) maybeInitContainers;
ArrayNode patchArray = om.createArrayNode();
ObjectNode addNode = patchArray.addObject();

addNode.put("op", "add");
addNode.put("path", "/spec/template/spec/initContainers");
ArrayNode values = addNode.putArray("values");
values.addAll(initContainers);

着信仕様にinitContainersフィールドが含まれる保証はないため、2つのケースを処理する必要があります。これらは欠落しているか存在している可能性があります。 欠落している場合は、 ObjectMapper インスタンス(上記のスニペットではom )を使用して、新しいArrayNodeを作成します。 それ以外の場合は、着信配列を使用します。

そうすることで、単一の「追加」パッチ命令を使用できます。 その名前にもかかわらず、その動作は、フィールドが作成されるか、既存のフィールドを同じ名前に置き換えるようなものです。 value フィールドは常に配列であり、(おそらく空の)元のinitContainers配列が含まれます。 最後のステップでは、実際のwait-for-itコンテナを追加します。

ObjectNode wfi = values.addObject();
wfi.put("name", "wait-for-it-" + UUID.randomUUID())
// ... additional container fields added (omitted)

コンテナ名はポッド内で一意である必要があるため、固定プレフィックスにランダムなUUIDを追加するだけです。 これにより、既存のコンテナとの名前の衝突を回避できます。

4.5. 展開

アドミッションコントローラーの使用を開始する最後のステップは、ターゲットのKubernetesクラスターにデプロイすることです。 予想どおり、これにはYAMLを作成するか、Terraformなどのツールを使用する必要があります。 いずれにせよ、これらは私たちが作成する必要のあるリソースです。

  • アドミッションコントローラーを実行するためのDeployment。 障害が発生すると新しいデプロイメントがブロックされる可能性があるため、このサービスの複数のレプリカをスピンすることをお勧めします
  • サービスは、APIサーバーからアドミッションコントローラーを実行している利用可能なポッドにリクエストをルーティングします
  • MutatingWebhookConfiguration リソースは、どのAPI呼び出しをサービスにルーティングする必要があるかを記述します。

たとえば、デプロイが作成または更新されるたびに、Kubernetesでアドミッションコントローラーを使用したいとします。 MutatingWebhookConfiguration ドキュメントには、次のようなrule定義が表示されます。

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: "wait-for-it.baeldung.com"
webhooks:
- name: "wait-for-it.baeldung.com"
  rules:
  - apiGroups:   ["*"]
    apiVersions: ["*"]
    operations:  ["CREATE","UPDATE"]
    resources:   ["deployments"]
  ... other fields omitted

サーバーに関する重要なポイント:Kubernetesでは、外部アドミッションコントローラーと通信するためにHTTPSが必要です。 これは、SpringBootサーバーに適切な証明書と秘密鍵を提供する必要があることを意味します。 これを行う1つの方法については、サンプルのアドミッションコントローラーの展開に使用されるTerraformスクリプトを確認してください。

また、簡単なヒント: ドキュメントのどこにも言及されていませんが、一部のKubernetes実装(例: GCP)ポート443を使用する必要があります 、したがって、SpringBoot HTTPSポートをデフォルト値(8443)から変更する必要があります。

4.6. テスト

デプロイメントアーティファクトの準備ができたら、最後に既存のクラスターでアドミッションコントローラーをテストします。 この例では、Terraformを使用してデプロイを実行しているため、applyを実行するだけです。

$ terraform apply -auto-approve

完了すると、kubectlを使用して展開とアドミッションコントローラーのステータスを確認できます。

$ kubectl get mutatingwebhookconfigurations
NAME                               WEBHOOKS   AGE
wait-for-it-admission-controller   1          58s
$ kubectl get deployments wait-for-it-admission-controller         
NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
wait-for-it-admission-controller   1/1     1            1           10m

それでは、アノテーションを含む単純なnginxデプロイメントを作成しましょう。

$ kubectl apply -f nginx.yaml
deployment.apps/frontend created

関連するログをチェックして、wait-for-itinitコンテナーが実際に挿入されたことを確認できます。

 $ kubectl logs --since=1h --all-containers deployment/frontend
wait-for-it.sh: waiting 15 seconds for www.google.com:80
wait-for-it.sh: www.google.com:80 is available after 0 seconds

念のため、デプロイのYAMLを確認しましょう。

$ kubectl get deployment/frontend -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    com.baeldung/wait-for-it: www.google.com:80
    deployment.kubernetes.io/revision: "1"
		... fields omitted
spec:
  ... fields omitted
  template:
	  ... metadata omitted
    spec:
      containers:
      - image: nginx:1.14.2
        name: nginx
				... some fields omitted
      initContainers:
      - args:
        - www.google.com:80
        image: willwill/wait-for-it
        imagePullPolicy: Always
        name: wait-for-it-b86c1ced-71cf-4607-b22b-acb33a548bb2
	... fields omitted
      ... fields omitted
status:
  ... status fields omitted

この出力は、アドミッションコントローラーがデプロイメントに追加したinitContainerを示しています。

5. 結論

この記事では、JavaでKubernetesアドミッションコントローラーを作成し、それを既存のクラスターにデプロイする方法について説明しました。

いつものように、例の完全なソースコードはGitHubにあります。