KubebuilderでKubernetes ServiceのipFamilyPolicyを常にPreferDualStackにする

# はじめに
これは[KMC Advent Calendar 2023](https://adventar.org/calendars/8840)の20日目の記事です。
昨日はpollenJPさんの[【Rust製構成管理ツール】JetPorch とは【次世代Ansible？】](https://qiita.com/pollenjp/items/aa54c7b236e40faae776) でした。Rust製は気になりますね。マネージドノードへの要件がないことに魅力を感じました。(yamlなのはそうなんだ......と思いもしましたが)

これは[kubeadm+flannelでkubernetesクラスタのIPv6環境構築](https://qiita.com/segre5458/items/3d6bf9a10b83ddedfa1b)の続きです。
前回KubernetesクラスタでIPv6環境を構築しました。しかし、問題点として作成するすべてのサービスが元は
```yaml
spec:
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
```
になっているため、
```yaml
spec:
  ipFamilies:
  - IPv4
  - IPv6
  ipFamilyPolicy: PreferDualStack
```
を付け加える必要がありました。
このような作業を毎回やるのは現実的ではありません。そのため、今回はKubebuilderを使用して、サービスを作成すると`spec.ipFamilies`と`spec.ipFamilyPolicy`を動的に書き換えてくれるMutatingAdmissionWebhookを作成しました。

作成したKubebuilderは[segre5458/k8s-ipv6-webhook](https://github.com/segre5458/k8s-ipv6-webhook)にあります。

# MutatingAdmissionWebhookとは
MutatingAdmissionWebhookはAdmission Controllerのプラグインの一つです。

詳しくは[A Guide to Kubernetes Admission Controllers](https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/)や[Admission Webhookを作って遊んで、その仕組みを理解しよう（説明編）](https://blog.mosuke.tech/entry/2022/05/15/admission-webhook-1/)などを参照してください。

ここではAdmission Webhookのみの解説を行います。
KubernetesのAdmission WebhookにはMutatingAdmissionWebhookとValidatingAdmissionWebhookがあります。
MutatingAdmissionWebhookはリソース作成時にリソースに対する変更を実装します。例としてPodの名前の変更やAnnotationの書き換えなどを行うことができます。KubebuilderではMutatingAdmissionWebhookはDefaultingAdmissionWebhookという名称になっています。
一方、ValidatingAdmissionWebhookはリソース作成/変更/削除時にリソースの検証を行うことができます。

以下は[A Guide to Kubernetes Admission Controllers](https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/)で提示されている図です。この図から分かる通りMutatingAdmissionWebhookはシリアルに実行され、ValidatingAdmissionWebhooknはその後にパラレルに実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3245951/c46079b9-3fd4-3d9e-d790-600327cfb461.png)

# Kubebuilderとは
Kubebuilderは、カスタムリソース定義（CRD）を使用してKubernetes APIを構築するためのフレームワークです。また、CRDの作成以外にもすでに存在するリソースへのWebhookを作成することもできます。今回はServiceの作成時に呼び出されるWebhookを作成するため、これを使用します。

Kubebuilderを理解するには[つくって学ぶKubebuilder](https://zoetrope.github.io/kubebuilder-training/)を一通り行うことをおすすめします。
今回はこの本に従ってKubebuilderを作成していきます。

なお、MutatingAdmissionWebhookのみを作成する場合はKubebuilderを利用しなくても作成することもできますが、Kubebuilderは通常必要な以下の実装を行ってくれます。

- Webhook サーバの作成
- サーバがマネージャーに追加されていることの確認
- webhook用のハンドラの作成
- 各ハンドラのサーバーへの追加

MutatingAdmissionWebhookのみを作成した同様の実装はすでに存在するのでそちらを利用してもいいでしょう。
https://github.com/tibordp/prefer-dual-stack-webhook

# Webhookの作成
それではKubebuilderを利用してWebhookを作成していきましょう。

## Kubebuilerのインストール
[Quick Start](https://book.kubebuilder.io/quick-start.html)にしたがって
- kubebuilder
- go
- docker
- kubectl
のインストールを行ってください

## 雛形の生成

まずはプロジェクトの雛形を作成します。

```bash
mkdir k8s-ipv6-webhook
cd k8s-ipv6-webhook
kubebuilder init --domain webhook.marokiki.net --repo github.com/segre5458/k8s-ipv6-webhook
```

- `--domain` : domainで指定した値がCRDのグループ名となります。そのため、すべてのAPIグループ名が`<group>.webhook.marokiki.net`となります。唯一の値を指定してください
- `--repo` : repoにはgo modulesのmodule名を指定します。GitHubを用いる場合はGitHubのリポジトリを指定すると良いでしょう

続いて、apiの雛形を生成します。

```bash
kubebuilder create api --group core --kind Service --resource=false --controller=false --version v1
```

- `--group` : 今回は`Service.Spec.IPFamilyPolicy`を書き換えることを目的としているため、利用するパッケージは[k8s.io/api/core/v1](https://github.com/kubernetes/api/blob/v0.29.0/core/v1/types.go#L4910)となります。よってgroupにはcoreを指定します
- `--kind` : 先程と同様の理由でkindにはserviceを指定します。独自のカスタムコントローラを作成する場合は独自の値を指定します
- `--resouce`, `--controller` : カスタムリソースとカスタムコントローラを作成する場合は`true`を指定します。今回は作成しないためfalseとしておきます
- `--version` : API Versionを指定します。 先程指定した`domain`と`group`に基づき、APIは`<group>.<domain>/<version>`となりますが、今回は既存のグループを指定しているため、`k8s.io/api/core/v1`に作成されます

続いてwebhookの雛形を生成します。

```bash
kubebuilder create webhook --group core --version v1 --kind Service --defaulting --programmatic-validation
```

`--defaulting`と`--programmatic-validation`を指定することでDefaultingWebhookとValidationWebhookの雛形が生成されます。

以上で雛形の生成は以上になります。現在のディレクトリ階層は
```
.
|-- Dockerfile
|-- Makefile
|-- PROJECT
|-- README.md
|-- api
|   `-- v1
|       |-- service_webhook.go
|       `-- webhook_suite_test.go
|-- bin
|   `-- controller-gen
|-- cmd
|   `-- main.go
|-- config
|   |-- certmanager
|   |   |-- certificate.yaml
|   |   |-- kustomization.yaml
|   |   `-- kustomizeconfig.yaml
|   |-- crd
|   |   `-- patches
|   |       |-- cainjection_in_services.yaml
|   |       `-- webhook_in_services.yaml
|   |-- default
|   |   |-- kustomization.yaml
|   |   |-- manager_auth_proxy_patch.yaml
|   |   |-- manager_config_patch.yaml
|   |   |-- manager_webhook_patch.yaml
|   |   `-- webhookcainjection_patch.yaml
|   |-- manager
|   |   |-- kustomization.yaml
|   |   `-- manager.yaml
|   |-- prometheus
|   |   |-- kustomization.yaml
|   |   `-- monitor.yaml
|   |-- rbac
|   |   |-- auth_proxy_client_clusterrole.yaml
|   |   |-- auth_proxy_role.yaml
|   |   |-- auth_proxy_role_binding.yaml
|   |   |-- auth_proxy_service.yaml
|   |   |-- kustomization.yaml
|   |   |-- leader_election_role.yaml
|   |   |-- leader_election_role_binding.yaml
|   |   |-- role.yaml
|   |   |-- role_binding.yaml
|   |   `-- service_account.yaml
|   `-- webhook
|       |-- kustomization.yaml
|       |-- kustomizeconfig.yaml
|       `-- service.yaml
|-- go.mod
|-- go.sum
`-- hack
    `-- boilerplate.go.txt

```
となっています。以上の雛形はKubeBuilderVersion 3.13.0 で生成したものになります。バージョンによって雛形は異なる可能性が高いです。

## プロジェクト作成
では雛形からWebhookを作成していきます。

まず、`internal/`は生成されていないため、`Dockerfile`内の`COPY internal/controller/ internal/controller/`の記述を削除します。
```diff_docker:Dockerfile
# Build the manager binary
FROM golang:1.20 as builder
ARG TARGETOS
ARG TARGETARCH

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

# Copy the go source
COPY cmd/main.go cmd/main.go
COPY api/ api/
- COPY internal/controller/ internal/controller/
+ # COPY internal/controller/ internal/controller/

# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]
```

続いて`config/default/kustomization.yaml`を編集します。
ここでは使用するリソースを指定します。
- `#- ../certmanager`のコメントアウトを消す
- `#- path: webhookcainjection_patch.yaml`のコメントアウトを消す
- `replace`以下のコメントアウトを全て消す
```diff_yaml:config/default/kustomization.yaml
    # Adds namespace to all resources.
    namespace: k8s-ipv6-webhook-system
    
    # Value of this field is prepended to the
    # names of all resources, e.g. a deployment named
    # "wordpress" becomes "alices-wordpress".
    # Note that it should also match with the prefix (text before '-') of the namespace
    # field above.
    namePrefix: k8s-ipv6-webhook-
    
    # Labels to add to all resources and selectors.
    #labels:
    #- includeSelectors: true
    #  pairs:
    #    someName: someValue
    
    resources:
    #- ../crd
    - ../rbac
    - ../manager
    # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
    # crd/kustomization.yaml
    - ../webhook
    # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
-   #- ../certmanager
+   - ../certmanager
    # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
    #- ../prometheus
    
    patches:
    # Protect the /metrics endpoint by putting it behind auth.
    # If you want your controller-manager to expose the /metrics
    # endpoint w/o any authn/z, please comment the following line.
    - path: manager_auth_proxy_patch.yaml
    
    # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
    # crd/kustomization.yaml
    - path: manager_webhook_patch.yaml
    
    # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
    # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
    # 'CERTMANAGER' needs to be enabled to use ca injection
-   #- path: webhookcainjection_patch.yaml
+   - path: webhookcainjection_patch.yaml
    # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
    # Uncomment the following replacements to add the cert-manager CA injection annotations
-  #replacements:
-  #  - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs
-  #      kind: Certificate
-  #      group: cert-manager.io
-  #      version: v1
-  #      name: serving-cert # this name should match the one in certificate.yaml
-  #      fieldPath: .metadata.namespace # namespace of the certificate CR
-  #    targets:
-  #      - select:
-  #          kind: ValidatingWebhookConfiguration
-  #        fieldPaths:
-  #          - .metadata.annotations.[cert-manager.io/inject-ca-from]
-  #        options:
-  #          delimiter: '/'
-  #          index: 0
-  #          create: true
-  #      - select:
-  #          kind: MutatingWebhookConfiguration
-  #        fieldPaths:
-  #          - .metadata.annotations.[cert-manager.io/inject-ca-from]
-  #        options:
-  #          delimiter: '/'
-  #          index: 0
-  #          create: true
-  #      - select:
-  #          kind: CustomResourceDefinition
-  #        fieldPaths:
-  #          - .metadata.annotations.[cert-manager.io/inject-ca-from]
-  #        options:
-  #          delimiter: '/'
-  #          index: 0
-  #          create: true
-  #  - source:
-  #      kind: Certificate
-  #      group: cert-manager.io
-  #      version: v1
-  #      name: serving-cert # this name should match the one in certificate.yaml
-  #      fieldPath: .metadata.name
-  #    targets:
-  #      - select:
-  #          kind: ValidatingWebhookConfiguration
-  #        fieldPaths:
-  #          - .metadata.annotations.[cert-manager.io/inject-ca-from]
-  #        options:
-  #          delimiter: '/'
-  #          index: 1
-  #          create: true
-  #      - select:
-  #          kind: MutatingWebhookConfiguration
-  #        fieldPaths:
-  #          - .metadata.annotations.[cert-manager.io/inject-ca-from]
-  #        options:
-  #          delimiter: '/'
-  #          index: 1
-  #          create: true
-  #      - select:
-  #          kind: CustomResourceDefinition
-  #        fieldPaths:
-  #          - .metadata.annotations.[cert-manager.io/inject-ca-from]
-  #        options:
-  #          delimiter: '/'
-  #          index: 1
-  #          create: true
-  #  - source: # Add cert-manager annotation to the webhook Service
-  #      kind: Service
-  #      version: v1
-  #      name: webhook-service
-  #      fieldPath: .metadata.name # namespace of the service
-  #    targets:
-  #      - select:
-  #          kind: Certificate
-  #          group: cert-manager.io
-  #          version: v1
-  #        fieldPaths:
-  #          - .spec.dnsNames.0
-  #          - .spec.dnsNames.1
-  #        options:
-  #          delimiter: '.'
-  #          index: 0
-  #          create: true
-  #  - source:
-  #      kind: Service
-  #      version: v1
-  #      name: webhook-service
-  #      fieldPath: .metadata.namespace # namespace of the service
-  #    targets:
-  #      - select:
-  #          kind: Certificate
-  #          group: cert-manager.io
-  #          version: v1
-  #        fieldPaths:
-  #          - .spec.dnsNames.0
-  #          - .spec.dnsNames.1
-  #        options:
-  #          delimiter: '.'
-  #          index: 1
-  #          create: true
+  replacements:
+  - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs
+      kind: Certificate
+      group: cert-manager.io
+      version: v1
+      name: serving-cert # this name should match the one in certificate.yaml
+      fieldPath: .metadata.namespace # namespace of the certificate CR
+    targets:
+      - select:
+          kind: ValidatingWebhookConfiguration
+        fieldPaths:
+          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+        options:
+          delimiter: '/'
+          index: 0
+          create: true
+      - select:
+          kind: MutatingWebhookConfiguration
+        fieldPaths:
+          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+        options:
+          delimiter: '/'
+          index: 0
+          create: true
+      - select:
+          kind: CustomResourceDefinition
+        fieldPaths:
+          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+        options:
+          delimiter: '/'
+          index: 0
+          create: true
+  - source:
+      kind: Certificate
+      group: cert-manager.io
+      version: v1
+      name: serving-cert # this name should match the one in certificate.yaml
+      fieldPath: .metadata.name
+    targets:
+      - select:
+          kind: ValidatingWebhookConfiguration
+        fieldPaths:
+          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+        options:
+          delimiter: '/'
+          index: 1
+          create: true
+      - select:
+          kind: MutatingWebhookConfiguration
+        fieldPaths:
+          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+        options:
+          delimiter: '/'
+          index: 1
+          create: true
+      - select:
+          kind: CustomResourceDefinition
+        fieldPaths:
+          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+        options:
+          delimiter: '/'
+          index: 1
+          create: true
+  - source: # Add cert-manager annotation to the webhook Service
+      kind: Service
+      version: v1
+      name: webhook-service
+      fieldPath: .metadata.name # namespace of the service
+    targets:
+      - select:
+          kind: Certificate
+          group: cert-manager.io
+          version: v1
+        fieldPaths:
+          - .spec.dnsNames.0
+          - .spec.dnsNames.1
+        options:
+          delimiter: '.'
+          index: 0
+          create: true
+  - source:
+      kind: Service
+      version: v1
+      name: webhook-service
+      fieldPath: .metadata.namespace # namespace of the service
+    targets:
+      - select:
+          kind: Certificate
+          group: cert-manager.io
+          version: v1
+        fieldPaths:
+          - .spec.dnsNames.0
+          - .spec.dnsNames.1
+        options:
+          delimiter: '.'
+          index: 1
+          create: true
```

次に、`webhook/manifests.yaml`を作成します。
初期状態では、`webhook/kustomization.yaml`に`manifests.yaml`が含まれているにも関わらず生成されていません。
```yaml:webhook/kustomization.yaml
resources:
- manifests.yaml
- service.yaml

configurations:
- kustomizeconfig.yaml
```
`webhook/manifests.yaml`の内容は以下のとおりです。
```yaml:webhook/manifests.yaml
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate--v1-service
  failurePolicy: Fail
  name: mutate.service.webhook.marokiki.net
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - services
  sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate--v1-service
  failurePolicy: Fail
  name: validate.service.webhook.marokiki.net
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - services
  sideEffects: None
```

また、`manager/kustomization.yaml`を以下のように変更し、Doceker imageの設定を行います。
```
resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
  newName: segre5458/k8s-ipv6-webhook
  newTag: latest
```

以上でWebhookを動かす設定ができます。

続いて`cmd/main.go`を編集してWebhookの実装を作成していきます。
上述した通り、`k8s.io/api/core/v1`を利用するので`import`に
```go
corev1 "k8s.io/api/core/v1"
v1 "github.com/segre5458/k8s-ipv6-webhook/api/v1"
```
を追加します。
また、`SetupWebhookWithManager`はカスタムコントローラ用の関数であるため変更します。
```diff_go:cmd/main.go
/*
Copyright 2023.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"flag"
	"os"

	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
	// to ensure that exec-entrypoint and run can make use of them.
	_ "k8s.io/client-go/plugin/pkg/client/auth"

	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+	corev1 "k8s.io/api/core/v1"
+	v1 "github.com/segre5458/k8s-ipv6-webhook/api/v1"
	//+kubebuilder:scaffold:imports
)

var (
	scheme   = runtime.NewScheme()
	setupLog = ctrl.Log.WithName("setup")
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))

	//+kubebuilder:scaffold:scheme
}

func main() {
	var metricsAddr string
	var enableLeaderElection bool
	var probeAddr string
	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
		"Enable leader election for controller manager. "+
			"Enabling this will ensure there is only one active controller manager.")
	opts := zap.Options{
		Development: true,
	}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()

	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:                 scheme,
		Metrics:                metricsserver.Options{BindAddress: metricsAddr},
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		LeaderElectionID:       "9c406f55.webhook.marokiki.net",
		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
		// when the Manager ends. This requires the binary to immediately end when the
		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
		// speeds up voluntary leader transitions as the new leader don't have to wait
		// LeaseDuration time first.
		//
		// In the default scaffold provided, the program ends immediately after
		// the manager stops, so would be fine to enable this option. However,
		// if you are doing or is intended to do any operation such as perform cleanups
		// after the manager stops then its usage might be unsafe.
		// LeaderElectionReleaseOnCancel: true,
	})
	if err != nil {
		setupLog.Error(err, "unable to start manager")
		os.Exit(1)
	}

-	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
-		if err = (&corev1.Service{}).SetupWebhookWithManager(mgr); err != nil {
-			setupLog.Error(err, "unable to create webhook", "webhook", "Service")
-			os.Exit(1)
-		}
-	}
+	ctrl.NewWebhookManagedBy(mgr).
+		WithDefaulter(&v1.ServiceNetwork{}).
+		WithValidator(&v1.ServiceNetwork{}).
+		For(&corev1.Service{}).
+		Complete()
	//+kubebuilder:scaffold:builder

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		setupLog.Error(err, "unable to set up health check")
		os.Exit(1)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		setupLog.Error(err, "unable to set up ready check")
		os.Exit(1)
	}

	setupLog.Info("starting manager")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "problem running manager")
		os.Exit(1)
	}
}
```

最後に、`api/v1/service_webhook.go`を編集してWebhookを作成します。
- `SetupWebhookWithManager`は`cmd/main.go`に記述したため削除します
- `ServiceNetwork`という新しい構造体を定義し、`admission.CustomDefaulter`と`admission.CustomValidater`にこれを指定し、この構造体にメソッドを実装していきます
- `//+kubebuilder`はkubebuilderマーカーであり、webhook マニフェストを生成するため使用されています。このマーカーはwebhook マニフェストを生成します。そのため、一部変更しています。詳しくは[document](https://book.kubebuilder.io/reference/markers/webhook)を参照して下さい


```diff_go:api/v1/service_webhook.go
    /*
    Copyright 2023.
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    */
    
    package v1
    
    import (
+       "context"
    	"k8s.io/apimachinery/pkg/runtime"
    	ctrl "sigs.k8s.io/controller-runtime"
    	logf "sigs.k8s.io/controller-runtime/pkg/log"
    	"sigs.k8s.io/controller-runtime/pkg/webhook"
    	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+       corev1 "k8s.io/api/core/v1"
    )
    
    // log is for logging in this package.
    var servicelog = logf.Log.WithName("service-resource")
    
-   // SetupWebhookWithManager will setup the manager to manage the webhooks
-   func (r *Service) SetupWebhookWithManager(mgr ctrl.Manager) error {
-   	return ctrl.NewWebhookManagedBy(mgr).
-   		For(r).
-  		Complete()
-   }
    
    // TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
    
-   //+kubebuilder:webhook:path=/mutate-core-v1-service,mutating=true,failurePolicy=fail,sideEffects=None,groups=core,resources=services,verbs=create;update,versions=v1,name=mservice.kb.io,admissionReviewVersions=v1
+   //+kubebuilder:webhook:path=/mutate--v1-service,mutating=true,failurePolicy=fail,sideEffects=None,groups=core,resources=services,verbs=create;update,versions=v1,name=mutate.service.webhook.marokiki.net,admissionReviewVersions=v1
    
+   type ServiceNetwork struct{}

-   var _ webhook.Defaulter = &Service{}
+   var _ admission.CustomDefaulter = &ServiceNetwork{}
    
    // Default implements webhook.Defaulter so a webhook will be registered for the type
-   func (r *Service) Default() {
-   	servicelog.Info("default", "name", r.Name)
-   
-   	// TODO(user): fill in your defaulting logic.
-   }
+   func (*ServiceNetwork) Default(ctx context.Context, obj runtime.Object) error {
+    	service := obj.(*corev1.Service)
+    	servicelog.Info("default", "name", service.Name)
+    
+    	if service.Spec.IPFamilyPolicy == nil {
+    		service.Spec.IPFamilyPolicy = new(corev1.IPFamilyPolicy)
+    	}
+    	*service.Spec.IPFamilyPolicy = corev1.IPFamilyPolicyPreferDualStack
+   	*&service.Spec.IPFamilies = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}
+   
+    	return nil
+   }
    
    // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
-   //+kubebuilder:webhook:path=/validate-core-v1-service,mutating=false,failurePolicy=fail,sideEffects=None,groups=core,resources=services,verbs=create;update,versions=v1,name=vservice.kb.io,admissionReviewVersions=v1
+   //+kubebuilder:webhook:path=/validate--v1-service,mutating=false,failurePolicy=fail,sideEffects=None,groups=core,resources=services,verbs=create;update,versions=v1,name=validate.service.webhook.marokiki.net,admissionReviewVersions=v1
    
-   var _ webhook.Validator = &Service{}
+   var _ admission.CustomValidator = &ServiceNetwork{}   
    // ValidateCreate implements webhook.Validator so a webhook will be registered for the type
-   func (r *Service) ValidateCreate() (admission.Warnings, error) {
-    	servicelog.Info("validate create", "name", r.Name)
-    
-    	// TODO(user): fill in your validation logic upon object creation.
-    	return nil, nil
-   }
+   func (*ServiceNetwork) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+    	service := obj.(*corev1.Service)
+    	servicelog.Info("validate create", "name", service.Name)
+    
+    	return nil, nil
+    }
    
    // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
-   func (r *Service) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
-    	servicelog.Info("validate update", "name", r.Name)
-    
-    	// TODO(user): fill in your validation logic upon object update.
-    	return nil, nil
-   }
+   func (*ServiceNetwork) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
+    	service := newObj.(*corev1.Service)
+    	servicelog.Info("validate update", "name", service.Name)
+   
+    	return nil, nil
+   }
    
    // ValidateDelete implements webhook.Validator so a webhook will be registered for the type
-    func (r *Service) ValidateDelete() (admission.Warnings, error) {
-    	servicelog.Info("validate delete", "name", r.Name)
-    
-    	// TODO(user): fill in your validation logic upon object deletion.
-    	return nil, nil
-   }
+   func (*ServiceNetwork) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+    	return nil, nil
+   }
```

DefalutingWebhookの実装は`func (*ServiceNetwork) Default(ctx context.Context, obj runtime.Object) error`に記述します。今回は`IPFamilyPolicy`を`PreferDualStack`し、`IPFamilies`に`IPv4`と`IPv6`の両方を指定しています。なお、`IPFamilies`の指定の順番には意味があります。[過去の記事](https://qiita.com/segre5458/items/3d6bf9a10b83ddedfa1b#metallb)を参照してください。
`service.Spec.IPFamilyPolicy`がnilの場合は`new`しています。記述は[Implementing defaulting/validating webhooks](https://book.kubebuilder.io/cronjob-tutorial/webhook-implementation)を参考にしています。
```go
func (*ServiceNetwork) Default(ctx context.Context, obj runtime.Object) error {
	service := obj.(*corev1.Service)
	servicelog.Info("default", "name", service.Name)

	if service.Spec.IPFamilyPolicy == nil {
		service.Spec.IPFamilyPolicy = new(corev1.IPFamilyPolicy)
	}
	*service.Spec.IPFamilyPolicy = corev1.IPFamilyPolicyPreferDualStack
	*&service.Spec.IPFamilies = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}

	return nil
}
```
ValidationWebhookの記述は`ValidateCreate`、`ValidateUpdate`、`ValidateDelete`に記述します。それぞれ、作成、更新、削除時に実行されます。今回は使用しないため重要な変更は行っていません。

以上でWebhookの作成は終了です。

## Docker Build
それでは作成したWebhookをBuildしてみましょう。
```bash
sudo make docker-build IMG=segre5458/k8s-ipv6-webhook:latest
```

ghcr.ioに作成したimageをpushします。
```bash
sudo docker login ghcr.io -u segre5458
sudo docker tag f44f9df79682 ghcr.io/segre5458/k8s-ipv6-webhook/webhook:latest
sudo docker push ghcr.io/segre5458/k8s-ipv6-webhook/webhook:latest
```

## Kubernetesで使用する
Configをapplyすることで作成したWebhookを使用できます。
```
kustomize build config/default | kubectl apply -f -
```
適当なServiceを作成すると`IPFamilyPolicy`と`IPFamilies`が書き換わっているのが確認できると思います。


# おわりに
以上で快適なk8s IPv6環境を作成することができました！
Kubebuilderを使うとHTTPSサーバを自分で作らなくてもいいといった利点がある一方で、基本的にカスタムリソースを作成するための雛形が生成されるため、Webhookだけを作成したいとなると変更しなければならない箇所が多くやや大変でした。
Kubebuilderにも入門できたので今度はCRDの作成もしてみたいです。

明日のAdvent Calenderの記事はACamusさんです。

# 参考資料
- [つくって学ぶKubebuilder](https://zoetrope.github.io/kubebuilder-training/)
- [Kubebuilder で Core Resource の Admission Webhook を作る](https://tech.griphone.co.jp/2021/12/12/kubebuilder-coreresource-webhook/)
- [The Kubebuilder Book](https://book.kubebuilder.io)