自己紹介
2024年3月6日~29日の約1ヶ月間、CyberAgent group Infrastructure Unit(CIU)にCA Tech JOB(インターンシップ)で参加しました、加藤 滉大です。インターンシップでは、Knative Eventingによるイベント駆動型アプリケーションの調査や、ジョブ基盤のPoC実装を行いました。
はじめに
CA Tech JOBに参加する以前、2023年6月にインフラエンジニア向け クラウド技術体験型インターンシップ ~仮想マシン編~という2日間の短期間のインターンシップに参加しました。CPUやメモリの仮想化、インタフェースの仮想化など様々なことを勉強し、実際に仮想マシンを動かしていたサーバから、サーバの動作中にSAS HDDを引き抜くといった体験をさせていただきました。その中で、パブリッククラウドをりようするのではなく、サーバ機材やネットワークの選定から、一貫してフロントエンドまでの全層を扱えるCIUという部署に心惹かれ、インターンシップに申し込みをしました。
この記事では、実際に私がインターンシップ期間中に行った調査や実装の成果についてを紹介します。
Knative Eventing 調査
まず最初のタスクは、Knative Eventingの調査として以下のようなタスクを行いました。
– Knative EventingとRabbitMQとの連係
– AWS Lambdaのような使い方
– Cron機能の使用
– Eventingのみでのジョブ基盤の作成
Knative EventingとRabbitMQとの連係
当初調査を始めた3月上旬には、eventing-rabbitmqが非推奨化されていました。
https://github.com/knative-extensions/eventing-rabbitmq
https://github.com/knative/eventing/issues/7670
https://groups.google.com/g/knative-dev/c/6QzXvVodSeo
このタスクは比較的大きめのタスクだったはずだったのですが、非推奨化に伴い連係不可能と判断しました。
しかし、3月15日にeventing-rabbitmqの非推奨化が解除されました。これに気づいたのは、成果発表用の資料を作っている最中で、RabbitMQとの連係の調査を行う時間が確保できませんでした。
Lambdaのような使い方
Knative FunctionsとKnative Eventingを組み合わせることで可能です。
Functionをデプロイすると、Open Container Initiative (OCI)形式のコンテナイメージが自動で生成され、ServingのServiceに追加される形となるため、スケールを機能させることや、リビジョンによるトラヒック分離を使うこともできます。
イベント駆動型アプリケーションとしての利用は、EventingのSink先をデプロイ済みのFunctionのURLに指定することで可能となります。また、WebAPIとしての利用は、デプロイ済みのFunctionのURLにアクセスすることで可能です。
Lambdaのような使い方をするに当たってFunctionsを検証していたところ、Go, RustとSpring BootについてはApple Siliconに非対応でした。その他のNodeとTypeScript、Python、Quarkusについては、 -b s2iオプションをつけることで、実行/ビルド/デプロイが可能になります。
Cron機能
定期実行(Cron)はKnative EventingのPingSourceを使用することで可能です。
このPingSourceでは、Eventing v13.0からscheduleでQuartz Scheduler書式に対応し、秒フィールドがサポートされています#7349
具体的な使用方法として、以下は10分ごとにSinkに対して{“message”: “Hello world!”}を送信する例です。
bash kn source ping create {pingsource-name} \ --namespace {namespace} \ --schedule "*/10 * * * * *" \ --data '{"message": "Hello world!"}' \ --sink {sink-name}
encodingオプションにbase64を設定することで、dataにbase64形式のデータを使用することができます。
Eventingのみでジョブ基盤
ジョブ基盤の起動トリガ部分はEventingでの作成、アプリケーション部分はServing等での作成が必要となります。例えば、単純な定期実行バッチジョブはPingSourceとServingやFunctionsを組み合わせることで作成が可能です。そのため、Eventingのみでジョブ基盤を作成することは不可能です。
ジョブ基盤のPoC実装
Knative Eventingの調査がeventing-rabbitmqの非推奨化により、想定よりかなり早く終わってしまったため追加のタスクとして、ジョブ基盤のPoC実装やAPIの実装を行いました。概形としてはGoogle Cloud Run jobsのようなものの作成となります。
通常、例えばDeploymentを作成する際には、apps/v1.Deploymentを使用しますが、ジョブ基盤の作成にあたっては既存のリソースではなく新しくカスタムリソースを作成する必要があります。これは主に、実際にジョブを実行する際には、batch/v1.Jobだけではジョブ基盤を作成するのに不十分なため、それを制御するためのコントローラというものが必要になるためです。カスタムリソースの定義(Custom Resource Definitions)やカスタムコントローラの作成にはKubebuilderを使用します。
CRD
Google Cloud Run jobsでは、主にJob・Execution・Taskのカスタムリソースでジョブ基盤が構成されています。それぞれ、Jobは実行されるジョブのテンプレートの役割を、Executionは一回の実行を表していて並列数や実行数を管理する役割が与えられていて、子としてTaskを呼ばれるbatch/v1.Jobを持っています。
Job・Execution・Taskの具体的なカスタムリソースの定義は以下のようになります(一部省略)。
go //+kubebuilder:object:root=true type Job struct { Spec *JobSpec `json:"spec,omitempty"` } type JobSpec struct { Template ExecutionTemplateSpec `json:"template"` } type ExecutionTemplateSpec struct { Spec ExecutionSpec `json:"spec"` } type ExecutionSpec struct { Parallelism *int32 `json:"parallelism,omitempty"` TaskCount *int32 `json:"taskCount,omitempty"` Template TaskTemplateSpec `json:"template"` } type TaskTemplateSpec struct { Spec TaskSpec `json:"spec"` } type TaskSpec struct { // +kubebuilder:validation:MaxItems=1 // +kubebuilder:validation:MinItems=1 Containers []corev1.Container `json:"containers"` Volumes []corev1.Volume `json:"volumes,omitempty"` MaxRetries *int32 `json:"maxRetries,omitempty"` TimeoutSeconds *int64 `json:"timeoutSeconds,omitempty"` }
TaskSpecに実際にbatch/v1.Jobに渡すジョブを記載しそれをTaskTemplateSpecで包みExecutionSpecへと渡しています。ExecutionSpecでは並列数と実行完了数の定義を行います。そのExecutionSpecをExecutionTemplateSpecとJobSpecで包み、JobのSpecとします。このような実装とすることで、Executionごとに並列数や実行完了数を上書きして変更することが容易になります。
カスタムコントローラ
次にカスタムリソースで定義された状態に遷移させるカスタムコントローラを作成します。カスタムコントローラでは、Executionの作成時にTask(batch/v1.Job)の作成や実行状態の管理を行います。実装するカスタムコントローラの責務は、Executionの作成削除に合わせてTask(batch/v1.Job)の操作を行うことです。カスタムコントローラの本質はリコンサイル(reconcile) というリソースの変更を検知し、望まれた状態と現実のリソースの状態を一致させることにあります。
例えば Kubernetes でコンテナを複数台管理する場合によく利用する Deployment リソースの .spec.replicas が 3 で定義されている場合、Deployment コントローラのリコンサイル処理で ReplicaSet の .spec.replicasを 3 に変更しなくてはいけません。
Executionは自身のSpecにJobからのSpecを持ちますので、それを参照してTaskの作成を行います。
また、OwnerReferenceを付与することで、Executionを削除した際に子のTask(batch.v1/Job)の削除を自動で同時に行えるようになります。
実際のコードから一部抜粋したものが以下となります。
go taskSpec := execution.Spec.Template.Spec job := batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Namespace: execution.Namespace, Labels: label, GenerateName: execution.Name + "-", }, Spec: batchv1.JobSpec{ ActiveDeadlineSeconds: taskSpec.TimeoutSeconds, BackoffLimit: taskSpec.MaxRetries, Parallelism: execution.Spec.Parallelism, Completions: execution.Spec.TaskCount, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: label}, Spec: corev1.PodSpec{ Containers: taskSpec.Containers, Volumes: taskSpec.Volumes, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } err := ctrl.SetControllerReference(&execution, &job, r.Scheme) if err != nil { return err } err = r.Create(ctx, &job)
実際に、Taskを管理するCRD/コントローラを作成した際のデモの動画が以下となります。
gRPC
ここまででは、Executionを起動するのにいちいちyamlを書いて…ということになるので、追加のタスクとしてgRPCでAPIを作成しました。
controller-runtimeを使用し、JobのCreate, Get, List, Delete, Run, Update、 ExecutionのGet, List, Deleteを行えるようにしました。
例えば、CreateJob, RunJob, GetJobを行った際の実行結果が以下の通りです
感想
インターンシップ期間にはCIUの皆様をはじめ、人事の方やランチ会などで関わった方々など、多くの方にお力添えいただけたことで成長に繋げられました。本当にありがとうございました。
あっという間の1ヶ月間でしたが、実際に出社することや開発を体験することで得られることが多々ありました。例えば、業界の第一線で活躍されている方から、実際のプロダクトや実装の考えを伺えたのはインターンシップならではです。また、他部署の方々とお話をさせていただける機会があり、開発による技術力の会得のみならず、サイバーエージェントという会社の風土を知ることにもなり、それら就業時間外を含めて非常に実りの多い時間を過ごすことができました。
実は、私はKubernetesのCRDやカスタムコントローラ、Goを今までに殆ど書いたことがなく、インターンシップが始まった当初は完全に場違いなところに来てしまったのでは無いかと不安でいっぱいでしたが、サポートいただいたトレーナさんやチームの皆様のお陰があり技術を習得でき、実装まで行うことができました。普段の個人開発では、Android以外のKotlinに固執しそれ以外何も受け入れないといった姿勢でしたが、別の言語・分野への逃げ道の無い挑戦で、意外となんでもいけるなと自身の開発力に自信がついたと感じています。
最後とはなりますが、自身と同じような境遇を持つコードが書ける学部1・2年生にとって、この記事がインターンシップを通じた未知なる挑戦の一助となることを願って筆を置かせていただきます。