[Kubernetes] オブジェクトストレージを払い出すカスタムコントローラーを作ってみた
2022年10月17日 月曜日
CONTENTS
こんにちは、運用技術部運用システム開発課で社内システムを開発をしている豊崎です。
普段の業務内容としましては、サービス運用をしている社内の人たちに対して、システムを提供し、運用改善などを行っています。バリー君がマスコットキャラの自動架電システム(※) barry などを担当しています。
※サービス監視からのアラートを受けると、自動的に関係者へ電話をかけるシステム
今回は、そのようなシステム開発をしていて、”あったらいいなあ・・・😩”と思っていたものを作成し、業務改善に取り組んだ話をしていきたいと思います。
背景
上記でも触れましたが、私は普段、システム開発をしており、そのシステムを社内Kubernetes基盤であるIKEで運用しています。システムを運用していると、データのバックアップなど、オブジェクトストレージにデータを保存したい時が多々あると思います。今回もデータベースのバックアップをどこに保存しようか悩んでる時にアイディアが浮かんできました。Kubernetesのマニフェストをapplyするだけで、 オブジェクトストレージ(バケット)が割り当てられたら良いのに・・・と。
弊社では、IIJオブジェクトストレージサービスを提供しており、オブジェクトストレージを担当するプロの人たちが居ます。ここで、エンジニアならば、自ずと次のようなアイディアが浮かんでくるでしょう。サービス連携したら面白いんじゃないか・・・と。
このようなわくわく、興奮は冷めないうちに行動するのが吉です。しかし、私は、SRE部所属ではなく、IKEを利用する1人のユーザです。SRE部の方に、機能提案をして実装してもらうという解決方法もあると思いますが、自分で実装したいってのがエンジニアですよね・・・?たまたま、この時は、年度替わりでかつ、SRE部に所属したい人を募集をしていたため、このビッグウェーブに乗るしかないと思いました。私が所属する部署の課長とSRE部の課長は、話が分かる人のため、話はとんとん拍子で進み、無事、SRE部に所属することになりました 🙏
構成図
今回、IIJオブジェクトストレージサービスと連携するにあたり、以下のような構成でシステムを組みました。
構成要素
構成要素としましては、KubernetesクラスタであるIKEにkube-apiserver、etcd、object-storage-managerが配置されています。
kube-apiserver、etcdは、Kubernetesを構成する要素であり、ユーザからのリソース作成、更新、削除といった操作を受け付け、それらのデータをetcdと呼ばれるkey-valueストアへ保存します。
object-storage-managerは、今回、私が作成するプログラムであり、IIJオブジェクトストレージサービスの契約処理を行うcontract-controller、バケット操作を行うbucket-controllerを管理します。また、リソースのデフォルト値を設定、バリデーション処理を行うwebhookもこのmanagerによって管理されています。
ワークフロー
契約処理
ユーザは、IIJオブジェクトストレージサービスを契約するために、契約リソース(Contract)を作成します。(①)
(この際、kube-apiserverは、契約リソースをobject-storage-managerが管理しているwebhookへ渡します。webhookで実行されるバリデーションに問題がある場合は、ユーザへエラーを返します。)
適用した契約リソースは、kube-apiserverを経由して、object-storage-managerのcontract-controllerに渡されます。contract-controller は、IIJオブジェクトストレージサービスに対し、新規契約をリクエストし、レスポンスとして、契約情報(バケットを操作するための accessKeyIdとsecretAccessKey)を返します。(②)
契約情報は、一部(accessKeyId、secretAccessKey)を除き、契約リソースの.statusに保存されます。また、accessKeyIdとsecretAccessKeyは、KubernetesのSecretとして保存され、バケット作成の処理に利用されます。
バケット作成(更新)
ユーザは、①と②で作成した契約情報を用いて、バケットリソース(Bucket)を作成します。(③)
(契約リソースと同様に、ここでもwebhookのバリデーションが実行されます。)
この際、ユーザは、バケットに対し、バケットポリシーなども同時に設定することができます。適用したバケットリソースは、kube-apiserverを経由して、object-storage-managerのbucket-controllerに渡されます。bucket-controllerでは、ユーザが設定した情報(.spec)をIIJ オブジェクトストレージサービスを操作するAPIリクエストの形式に変換し、バケット操作(作成、更新、削除)を行います。その後、bucket-controllerは、バケットに関する情報(acl、policyなど)を取得し、バケットリソースの.statusに保存します。
以上の処理が完了しますと、ユーザは、IIJ オブジェクトストレージサービスが公開しているAPIを介して、オブジェクト操作をすることができるようになります。また、バケットの情報を変更したい場合は、変更したバケットリソースを適用することで、bucket-controllerがバケットの更新処理を行います。
使用技術
今回、object-storage-managerを作成するにあたって、次の技術を使用しました。
- Kubernetes
- kind
- ローカルマシンにKubernetesクラスタを構築する
- 簡単にクラスタ生成、破棄することができる
- kubebuilder
- カスタムコントローラーを開発するためのフレームワーク
- Goでプログラムを記述することができる
- mkcert
- webhookを開発環境で動かすため、証明書を作成する
リソース定義の設計
object-storage-managerは、契約リソース(Contract)、バケットリソース(Bucket)を管理します。
契約リソースは、ユーザがIIJ オブジェクトストレージサービスを利用するのに必要な契約情報を簡潔に記述できます。
(リソース定義としては、社内で使用している情報を指定し、それを適用するだけなので、ここでは省略させていただきます。)
Bucketリソースは、ユーザがバケット操作(作成、更新、削除)をする際に用いるリソースです。
バケットの名前、契約の名前のみが必須プロパティとなっており、後は、バケットリソースを削除した時の挙動を設定するプロパティとバケットのセキュリティ設定を行うプロパティです。(※ユーザのNamespaceに存在しない契約の名前を指定した場合、エラーになります。)
ほとんど、オブジェクトストレージサービスが提供しているAPIのラッパーであるため、だいぶシンプルな定義になっていると思います。
以下が、Bucketリソースの定義です。
apiVersion: dagrin.ike.iij.jp/v1alpha1 kind: Bucket metadata: name: <resource name> spec: bucketName: <your bucket name> contractName: <your contract name> # The following properties are optional bucketReclaimPolicy: Delete | Retain(default) cannedAcl: private | public-read | public-read-write grantRead: - value: <id | email | url(for anonymous)> type: id | emailAddress | url ... grantWrite: - value: <id | email | url(for anonymous)> type: id | emailAddress | url ... grantReadAcp: - value: <id | email | url(for anonymous)> type: id | emailAddress | url ... grantWriteAcp - value: <id | email | url(for anonymous)> type: id | emailAddress | url ... fullControll: - value: <id | email | url(for anonymous)> type: id | emailAddress | url ... policy: { "Version": "2008-10-17", "Id": "office-only", "Statement": { "Action": "dag:*", "Sid": "1", "Effect": "Deny", "Condition": { "NotIpAddress": { "iijgio:SourceIp": [ "...", "..." ], }, }, "Principal": { "IIJGIO": "*" }, "Resource": [ "grn:iijgio:dag:::<your bucket name>-bucket", "grn:iijgio:dag::<your bucket name>-bucket/*", ], }, }
開発
開発を行うにあたって、以下のプロセスで進めていきました。
- 契約API、オブジェクトストレージAPIのGoクライアントを生成
- OpenAPIで仕様書を作成し、OpenAPI Code Generatorで自動生成
- kubebuilderを使って、object-storage-managerを実装
- 契約リソース、コントローラーの実装
- バケットリソース、コントローラーの実装
- webhookでデフォルト値の設定、バリデーション機能の実装
- envtestを用いて、コントローラー、webhookの挙動を確認するテスト実装
- コードレビュー
- 一定期間、テスト環境で一連の動作を確認後、本番環境へデプロイ
以下は、コントローラー開発を終えたあとの所感です。
webhookを考慮したコントローラーの実装
リソース、コントローラーの実装を終えてから思ったことは、webhookの実装を考慮してから、コントローラーの実装に取り掛かるべきだったということです。バケットの名前などは、バケット作成後、変更させたくない値です。これらの処理は、webhookでバリデーションを行うべきなのですが、開発を始めた頃は、知識不足でコントローラー側で頑張って実装をしていました。同様にデフォルト値もコントローラー側で実装していたので、後々のテストの修正が少し面倒になりました。
コントローラーのテスト
言わなくても分かると思いますが、テストは絶対に書いた方が良いです。思ってもいないところでリソースに値が存在しなかったりする場合があるので、リソースの.statusの変更の遷移に沿って、テストを書いておくことをオススメします。毎回、手作業でテストするのも大変ですし、コントローラー開発は、デグレーションが発生しやすいと個人的に思っています。vscodeのような開発環境であれば、コードカバレッジ結果をコード上にハイライトすることができるので、ちゃんとコントローラーのコードが動いているか一目瞭然です。
開発環境でのwebhook
開発環境でwebhookを動かす際、証明書をインストールしないと動かないという仕様にハマりました。
(ユーザがリソースを作成、更新、削除した時、kube-apiserverは、webhookのエンドポイントにHTTPSリクエストを送信するため、証明書が必要です。)
kubebuilderの公式issueにワークアラウンドが述べられており、大変助かりました 🙏
# Project rootで実行 # 1. 証明書の作成 $ mkcert -install $ export CAROOT=.certs/webhook $ mkcert -cert-file=$CAROOT/tls.crt -key-file=$CAROOT/tls.key host.docker.internal 172.17.0.1 # 2. kube-apiserverにルートCAを設定をする # rootCAをbase64でエンコードして, config/local/webhook/manifests.yamlのcaBundleに設定する # linuxを利用している場合は、config/local/webhook/manifests.yamlのurlを切り替えてください # for mac $ cat $CAROOT/rootCA.pem | base64 # for linux $ cat $CAROOT/rootCA.pem | base64 -w 0 # config/webhook/manifests.yaml webhooks: - admissionReviewVersions: - v1 clientConfig: caBundle: <ここに設定する> url: https://host.docker.internal:9443/mutate-<your endpoint path> # for linux # url: https://172.17.0.1:9443/mutate-<your endpoint path> # 設定した情報を反映 $ kubectl apply -k config/local/webhook # 3. webhookに証明書を設定する (main.goに記述されているmanagerに証明書情報を設定する) # コマンドラインオプションなどで設定できるようにしておくと便利 if webhookCertDir != "" { whs := mgr.GetWebhookServer() whs.CertDir = webhookCertDir } $ make run
リソースの状態を.status.conditionsに定義する
こちらも最初は、.statusに自己流で管理するようにしてました。レビューする際にレビュアーの方に共有してもらった情報になります。
kubernetesのcommunityのリポジトリでも述べられている通り、リソースの状態を.spec.conditionsで定義しておくと、ツールや他のコントローラーから、それらのリソースの情報を収集しやすくなるようです。(例えば、kubectl wait --for=condition=Ready <kind>/<resource-name>
)
以下は、サンプルコードになります。
type BucketConditionType string const ( BucketConditionReady BucketConditionType = "Ready" ) type BucketCondition struct { Type BucketConditionType `json:"type"` Status corev1.ConditionStatus `json:"status"` Reason string `json:"reason,omitempty"` Message string `json:"message,omitempty"` LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` } type BucketStatus struct { ... Conditions []BucketCondition `json:"conditions,omitempty"` ... }
まとめ
いかがだったでしょうか?
今年度の始まりからシステムを構想し、本番環境へのデプロイまで比較的、早く行動ができたと思います。(やはり、わくわくするといった気持ちは、何よりもエンジニアの原動力になりますね🤗)
プロジェクトの稼働を調整してくれたPM方々には、大変感謝です。
今後もシステムを開発する際、このような “わくわくする” といった気持ちは大切にしていきたいと思います。
今回、この記事では触れませんでしたが、KubernetesにはContainer Object Storage Interface(COSI)というオブジェクトストレージのプロビジョニングとマネージメントに関するインターフェース仕様が提案されています。正式にはリリースされていないため、今回は、この仕様に沿って実装はしませんでした。COSIの今後の発展も楽しみですね。
さて、今回は、このくらいにしておくとします。
閲覧していただき、ありがとうございました👋