こんにちは、ACS事業部の谷合です。
弊社ではPlatform Engineeringに注目しており、その延長線上で私もKubernetesクラスタ(以降K8s)を宣言的に自動払い出しできるツールの調査を行っています。
本Cluster APIを触ってみるシリーズでは、前回まではAKS上にVanilaなKubenetesやAKSを立ててみました。
今回は以下のチュートリアルに従って、Providerを実装してみましょう。 cluster-api.sigs.k8s.io
なお、チュートリアルは若干古く、記載通りにやっても動かないので、書き換えたりツールやパッケージのバージョンアップなどもしています。
私の実装したコードは以下のリポジトリに置いています。
github.com
Providerって?
以下の種類があり、互いに強調しながら、Clusterの払い出しを行います。 これら、ProviderはOperatorパターンを使用し、CustomControllerの集合体として提供されます。
Infrastracture
クラスタノードの実行に必要な前提条件をすべて提供します。
例えば、VM、ネットワーク、ロードバランサー、ファイアウォールルールなどです。
主要なクラウドサービス向けのプロバイダーが公開されています。
Bootstrap
ノードのブートストラップに使用されるブートストラップデータを生成します。
kubeadmなどを使ったBootstrap手段が提供されます。
Control plane
Control planeを作成します。
以下のリソースをインスタンス化して作成します
- etcd
- Kubernetes APIサーバー
- Kubernetes コントローラーマネージャー
- Kubernetes スケジューラ
- クラウドコントローラーマネージャー
- クラスタDNS(CoreDNSなど)
- サービスプロキシ(例:kube-proxy)
実装してみよう
実装Provider概要
今回実装するProviderはClusterリソースのみとし、Machineリソースなどは実装しません。
Provider内からMailgunというEメールの送受信および追跡を行うメール配信サービスのAPIを叩き、Clusterリソースデプロイ時に指定したドメインから
メールを送信します。また、送信先はProvoderデプロイ時に指定します。
前提条件
今回は以下のバージョンのツールで実装します。
事前にインストールしておきましょう。- Kubebuilder ("v3.10.0")
- Golang ("v1.19.9")
- Kubernetes ("v1.26.3")
- make ("v4.3")
- gcc ("v11.3.0")
- Kubebuilder ("v3.10.0")
Cluster APIの最新版v1.4.2を利用する場合、apimachinery、clinet-go、controller-runtimeは以下のバージョンを利用する必要があります。
- k8s.io/apimachinery ("v0.26.5")
- k8s.io/client-go ("v0.26.5")
- sigs.k8s.io/controller-runtime ("v0.14.6")
- k8s.io/apimachinery ("v0.26.5")
Mailgunのアカウント登録が済んでいる必要があります。 ドメイン名を作成する必要はありません。今回はSandboxのドメインを使用します。
Cluster APIリソースデプロイ
Cluster APIリリースページからコンパイル済みのマニフェストを使用するか、clusterctl initを実行するか、cluster-apiをクローンしてkustomizeを使用してそのマニフェストを適用することが可能です。
git clone https://github.com/kubernetes-sigs/cluster-api.git cd cluster-api make envsubst kustomize build config/default | envsubst | kubectl apply -f -
Projectテンプレート作成
以下コマンドでProjectを作成します。
repoには、go modulesのmodule名を指定します。
GitHubにリポジトリを作る場合はgithub.com/<user_name>/cluster-api-provider-mailgun
を指定してください。
mkdir cluster-api-provider-mailgun cd cluster-api-provider-mailgun kubebuilder init --domain cluster.x-k8s.io --repo github.com/<user_name>/cluster-api-provider-mailgun
APIテンプレート作成
以下コマンドでAPIを作成します。
なお、groupやversionなどは変更しないでください。
現在のCluster APIのAPIバージョンと合わせる必要があるためです。
kubebuilder create api --group infrastructure --version v1beta1 --kind MailgunCluster
コード解説
API実装
api/v1beta1/mailguncluster_types.go
に以下コードを加えていきます。
以下のようにSpecとStatusを構造体として実装します。
type Priority string const ( // PriorityUrgent means do this right away PriorityUrgent = Priority("Urgent") // PriorityUrgent means do this immediately PriorityExtremelyUrgent = Priority("ExtremelyUrgent") // PriorityBusinessCritical means you absolutely need to do this now PriorityBusinessCritical = Priority("BusinessCritical") ) // MailgunClusterSpec defines the desired state of MailgunCluster type MailgunClusterSpec struct { // Priority is how quickly you need this cluster Priority Priority `json:"priority"` // Request is where you ask extra nicely Request string `json:"request"` // Requester is the email of the person sending the request Requester string `json:"requester"` } // MailgunClusterStatus defines the observed state of MailgunCluster type MailgunClusterStatus struct { // MessageID is set to the message ID from Mailgun when our message has been sent MessageID *string `json:"response"` }
上記構造体は、以下の構造体から参照されます。
この構造体がProviderの本体となります。
// MailgunCluster is the Schema for the mailgunclusters API type MailgunCluster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MailgunClusterSpec `json:"spec,omitempty"` Status MailgunClusterStatus `json:"status,omitempty"` }
Controller実装
internal/controller/mailguncluster_controller.go
に以下コードを加えていきます。
Reconcile Loopと呼ばれる冪等性を保つ仕組みがあります。
宣言的に記載したCluster定義が、この処理にて保たれることになります。
Reconcile Loopはポインタレシーバを持つメソッドとして定義されていますので、以下のように構造体を定義します。
この構造体への値受け渡しは、後ほど実装します。
// MailgunClusterReconciler reconciles a MailgunCluster object type MailgunClusterReconciler struct { client.Client Mailgun mailgun.Mailgun Recipient string }
次に以下のMarkerを定義します。このMarkerに従ってkubebuilderがRoleのManifestを定義してくれます。
MailgunClustersリソースとclustersリソースに適切な権限を振ります。
//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters/status,verbs=get;update;patch //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters/finalizers,verbs=update //+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch
いよいよ本ProviderのMailgunClusterリソースを管理するControllerのReconcile Loopを実装します。
こちらが全体像となりますが、以降で細かく解説します。
// For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.6/pkg/reconcile func (r *MailgunClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) var mgCluster infrav1betav1.MailgunCluster if err := r.Get(ctx, req.NamespacedName, &mgCluster); err != nil { // import apierrors "k8s.io/apimachinery/pkg/api/errors" if apierrors.IsNotFound(err) { return ctrl.Result{}, nil } logger.Error(err, "couldn't get obj") return ctrl.Result{}, err } if mgCluster.Status.MessageID != nil { // We already sent a message, so skip reconciliation return ctrl.Result{}, nil } subject := fmt.Sprintf("[%s] New Cluster requested", mgCluster.Spec.Priority) body := fmt.Sprintf("Hello! One cluster please.\n\n%s\n", mgCluster.Spec.Request) requester := fmt.Sprintf("Cluster API Sandbox Provider <mailgun@%s>", mgCluster.Spec.Requester) msg := r.Mailgun.NewMessage(requester, subject, body, r.Recipient) _, msgID, err := r.Mailgun.Send(ctx, msg) if err != nil { logger.Error(err, fmt.Sprintf("couldn't send message %q", msgID)) return ctrl.Result{}, err } helper, err := patch.NewHelper(&mgCluster, r.Client) if err != nil { return ctrl.Result{}, err } mgCluster.Status.MessageID = &msgID if err := helper.Patch(ctx, &mgCluster); err != nil { logger.Error(err, fmt.Sprintf("couldn't patch cluster %q", mgCluster.Name)) return ctrl.Result{}, err } logger.Info(fmt.Sprintf("Message ID %s", *mgCluster.Status.MessageID)) return ctrl.Result{}, nil }
Reconcile Loopは定期的およびイベントごとに実行され、冪等性を保つように処理がされます。
そこで、毎回処理対象のリソースの状態を取得する必要があります。
それを以下のコードで実現します。まず、API実装
の項で実装したMailgunCluster構造体を型として変数を定義し、Get関数でリソースを取得します。
その際にエラーが変えれば、そのエラーがリソースが存在しないというエラーなのか否かをapierrors.IsNotFound関数で判断し、存在しないのであれば次回Reconcile Loop時に処理を引き継げるよう何もしないでreturnします。もしリソースが存在するが別の何らかのエラーなのであれば、そのエラーをreturnします。
var mgCluster infrav1betav1.MailgunCluster if err := r.Get(ctx, req.NamespacedName, &mgCluster); err != nil { if apierrors.IsNotFound(err) { return ctrl.Result{}, nil } logger.Error(err, "couldn't get obj") return ctrl.Result{}, err }
このMailgunリソース一つにつき、1回のみメールを送信します。
.Status.MessageIDを確認し、値があれば何もせずにreturnします。
if mgCluster.Status.MessageID != nil { // We already sent a message, so skip reconciliation return ctrl.Result{}, nil }
次にメッセージを以下のコードで生成します。 Mailgunクライアントは後ほど生成方法を説明します。
subject := fmt.Sprintf("[%s] New Cluster requested", mgCluster.Spec.Priority) body := fmt.Sprintf("Hello! One cluster please.\n\n%s\n", mgCluster.Spec.Request) requester := fmt.Sprintf("Cluster API Sandbox Provider <mailgun@%s>", mgCluster.Spec.Requester) msg := r.Mailgun.NewMessage(requester, subject, body, r.Recipient)
上記で生成したメッセージを送信します。
なお、生成したメッセージに送信元、送信先が含まれているので、メールの送信が可能となるいうわけです。
_, msgID, err := r.Mailgun.Send(ctx, msg) if err != nil { logger.Error(err, fmt.Sprintf("couldn't send message %q", msgID)) return ctrl.Result{}, err }
最後に.Status.MessageIDにメッセージIDを入れ、リソースを更新してReconcile処理を終了します。
helper, err := patch.NewHelper(&mgCluster, r.Client) if err != nil { return ctrl.Result{}, err } mgCluster.Status.MessageID = &msgID if err := helper.Patch(ctx, &mgCluster); err != nil { logger.Error(err, fmt.Sprintf("couldn't patch cluster %q", mgCluster.Name)) return ctrl.Result{}, err } logger.Info(fmt.Sprintf("Message ID %s", *mgCluster.Status.MessageID))
main処理実装
cmd/main.go
に以下コードを加えていきます。
main処理は重要な箇所のみ解説します。
まず、Mailgunのドメイン、API Key、送信先を環境変数として受け取って、Mailgunインスタンスを生成します。
なお、環境変数はconfigmapやsecretリソースから値を受け取ってOperatorコンテナ内に定義されます。
domain := os.Getenv("MAILGUN_DOMAIN") if domain == "" { setupLog.Info("missing required env MAILGUN_DOMAIN") os.Exit(1) } apiKey := os.Getenv("MAILGUN_API_KEY") if apiKey == "" { setupLog.Info("missing required env MAILGUN_API_KEY") os.Exit(1) } recipient := os.Getenv("MAIL_RECIPIENT") if recipient == "" { setupLog.Info("missing required env MAIL_RECIPIENT") os.Exit(1) } mg := mailgun.NewMailgun(domain, apiKey)
Controller実装
の項にて、Reconcile LoopのレシーバMailgunClusterReconciler構造体を定義しましたが、main処理の以下コードにて値を渡し、Contollerを起動します。
if err = (&controller.MailgunClusterReconciler{ Client: mgr.GetClient(), Mailgun: mg, Recipient: recipient, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MailgunCluster") os.Exit(1) }
Kubernetesリソース定義
ProviderはOperatorパターンで実装されるので、今回はkubebuilderを使っています。
kubebuilderはkustomizeを使い、リソースをデプロイするため、それ用にリソース定義を行います。
config/manager/configuration.yaml
というファイルを作成し、以下のsecret, configmapリソース定義を行います。
apiVersion: v1 kind: Secret metadata: name: mailgun-config namespace: system type: Opaque stringData: api_key: ${MAILGUN_API_KEY} --- apiVersion: v1 kind: ConfigMap metadata: name: mailgun-config namespace: system data: mailgun_domain: ${MAILGUN_DOMAIN} mail_recipient: ${MAIL_RECIPIENT}
次に、config/default/manager_config_patch.yaml
ファイルを作成し、以下の定義をします。
apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system spec: template: spec: containers: - name: manager env: - name: MAILGUN_API_KEY valueFrom: secretKeyRef: name: cluster-api-provider-mailgun-mailgun-config key: api_key - name: MAILGUN_DOMAIN valueFrom: configMapKeyRef: name: cluster-api-provider-mailgun-mailgun-config key: mailgun_domain - name: MAIL_RECIPIENT valueFrom: configMapKeyRef: name: cluster-api-provider-mailgun-mailgun-config key: mail_recipient
更に以下2種類のkustomization.yamlを編集します。
config/default/kustomization.yaml
patchesStrategicMerge: - manager_auth_proxy_patch.yaml - manager_config_patch.yaml
config/manager/kustomization.yaml
resources: - manager.yaml - configuration.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller newName: capiacr.azurecr.io/cluster-api-provider-mailgun newTag: v1
これにて実装は完了です。
お疲れさまでした。
動作確認
まず以下の環境変数を定義します。
この環境変数を定義することで、Providerデプロイ時に、先ほど定義したsecret, configmapリソースにデータが挿入されます。
export MAILGUN_DOMAIN="<Mailgun sandboxドメイン>" export MAILGUN_API_KEY="<Mailgun Private API Key>" export MAIL_RECIPIENT="<メール送信先>"
次に以下のコマンドでimageをbuildし、pushを行います。
make docker-build docker-push IMG=<Your Registry>/cluster-api-provider-mailgun:tag
更に以下コマンドを実行することで、Providerがデプロイされます。
make deploy IMG=<some-registry>/cluster-api-provider-mailgun:tag
ClusterおよびMailgunClusterリソースを以下のように定義し、デプロイを行います。
cat << EOT > config/samples/infrastructure_v1beta1_mailguncluster.yaml apiVersion: cluster.x-k8s.io/v1beta1 kind: Cluster metadata: name: hello-mailgun namespace: cluster-api-provider-mailgun-system spec: clusterNetwork: pods: cidrBlocks: ["192.168.0.0/16"] infrastructureRef: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: MailgunCluster name: hello-mailgun --- apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: MailgunCluster metadata: name: hello-mailgun namespace: cluster-api-provider-mailgun-system labels: app.kubernetes.io/name: mailguncluster app.kubernetes.io/instance: mailguncluster-sample app.kubernetes.io/part-of: cluster-api-provider-mailgun app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: cluster-api-provider-mailgun spec: priority: "ExtremelyUrgent" request: "Please make me a cluster, with sugar on top?" requester: "<Your Domain>" EOT
上記リソースをデプロイしたことで、以下のようなログからメールが送信されたことが確認できるはずです。
$ kubectl -n cluster-api-provider-mailgun-system logs cluster-api-provider-mailgun-controller-manager-6978b7b7bckxgcs manager -f 2023-05-24T06:38:25Z INFO controller-runtime.metrics Metrics server is starting to listen {"addr": "127.0.0.1:8080"} 2023-05-24T06:38:25Z INFO setup starting manager I0524 06:38:25.481839 1 leaderelection.go:248] attempting to acquire leader lease cluster-api-provider-mailgun-system/872ab3b9.cluster.x-k8s.io... 2023-05-24T06:38:25Z INFO Starting server {"kind": "health probe", "addr": "[::]:8081"} 2023-05-24T06:38:25Z INFO Starting server {"path": "/metrics", "kind": "metrics", "addr": "127.0.0.1:8080"} I0524 06:38:25.498879 1 leaderelection.go:258] successfully acquired lease cluster-api-provider-mailgun-system/872ab3b9.cluster.x-k8s.io 2023-05-24T06:38:25Z DEBUG events cluster-api-provider-mailgun-controller-manager-6978b7b7bckxgcs_8889e716-be00-4156-bdcb-badd381b25ab became leader {"type": "Normal", "object": {"kind":"Lease","namespace":"cluster-api-provider-mailgun-system","name":"872ab3b9.cluster.x-k8s.io","uid":"01083d43-1bed-47f4-ad64-491c46c7421e","apiVersion":"coordination.k8s.io/v1","resourceVersion":"902119"}, "reason": "LeaderElection"} 2023-05-24T06:38:25Z INFO Starting EventSource {"controller": "mailgunmachine", "controllerGroup": "infrastructure.cluster.x-k8s.io", "controllerKind": "MailgunMachine", "source": "kind source: *v1beta1.MailgunMachine"} 2023-05-24T06:38:25Z INFO Starting Controller {"controller": "mailgunmachine", "controllerGroup": "infrastructure.cluster.x-k8s.io", "controllerKind": "MailgunMachine"} 2023-05-24T06:38:25Z INFO Starting EventSource {"controller": "mailguncluster", "controllerGroup": "infrastructure.cluster.x-k8s.io", "controllerKind": "MailgunCluster", "source": "kind source: *v1beta1.MailgunCluster"} 2023-05-24T06:38:25Z INFO Starting Controller {"controller": "mailguncluster", "controllerGroup": "infrastructure.cluster.x-k8s.io", "controllerKind": "MailgunCluster"} 2023-05-24T06:38:25Z INFO Starting workers {"controller": "mailguncluster", "controllerGroup": "infrastructure.cluster.x-k8s.io", "controllerKind": "MailgunCluster", "worker count": 1} 2023-05-24T06:38:25Z INFO Starting workers {"controller": "mailgunmachine", "controllerGroup": "infrastructure.cluster.x-k8s.io", "controllerKind": "MailgunMachine", "worker count": 1} 2023-05-24T06:41:03Z INFO Message ID <20230524064102.219bc47d39e055b9@sandboxaXXXXXXXXXXXXXXXXXXXXXXXXXX.mailgun.org> {"controller": "mailguncluster", "controllerGroup": "infrastructure.cluster.x-k8s.io", "controllerKind": "MailgunCluster", "MailgunCluster": {"name":"hello-mailgun","namespace":"cluster-api-provider-mailgun-system"}, "namespace": "cluster-api-provider-mailgun-system", "name": "hello-mailgun", "reconcileID": "57506493-0f49-457f-b140-9d7a640fb9de"}
また、以下のようにMailgunのUI上でもメール送信が確認できます。
さいごに
コード実装という今までにないスタイルの記事を提供できたかと思います。
Cluster APIのProviderを使うことで自由にクラスタ定義が実装できますし、自社に合わせた形でクラスタの払い出しも可能となります。
ハードルは高いですが是非チャレンジしてみてください。
次回VPSやクラウドを使って独自にProviderを実装してみたいと思いますので、お楽しみに!
ACS事業部のご紹介
私達ACS事業部はAzure・AKSなどのクラウドネイティブ技術を活用した内製化のご支援をしております。
www.ap-com.co.jp
また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。
www.ap-com.co.jp