APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

[Cluster API] Providerを実装してみよう

こんにちは、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")
  • 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")
  • 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

本記事の投稿者: 谷合純也
AKS/ACAをメインにインフラ系のご支援を担当しています。
junya0530さんの記事一覧 | Zenn