はじめに
こんにちは! UZABASE SPEEDA SRE teamの生賀です。
最近あった嬉しかったことは、自分が翻訳した日本語がkubernetesのCronJob - Kubernetesページに反映されていたことです。
閑話休題、弊社SPEEDAサービスでは大量のバッチジョブがHinemosを起点としてVM上で動作しています。 SREチームではこのようなジョブ群を徐々にサーバから切り離して、コンテナライズを進めています。
そんな大量にあるジョブですが、環境変数だけが異なっているものも多数あり、実行環境 x 環境変数と環境が異なると掛け算式に増えていきます。 これをkubernetesのCronJobでyamlハードコーディングすると容易に1000行を超えてしまい、管理上のコストも含め現実的ではありません。1
そこで、kubernetesパッケージマネージャーを使用することにしました。その選択肢としてはHelmやKustomizeなどがあります。 これらを利用することで環境毎に異なる設定値のリソースが作成できたり、複雑な依存関係を持つkubernetesリソース群をChartという一つのフォーマットにまとめることができます。
今回、要件を満たすエントリが見当たらなかった為、利用事例として投稿させていだきます。
Kustomizeの場合
まずはKustomizeで同様の利用を想定した際の構成を見てみましょう。
下記ディレクトリ構成は公式のKustomizeのGithubの、サンプルディレクトリ構成から拝借しました。
Kustomizeのサンプルディレクトリ構成
~/someApp ├── base │ ├── deployment.yaml │ ├── kustomization.yaml │ └── service.yaml └── overlays ├── development │ ├── cpu_count.yaml │ ├── kustomization.yaml │ └── replica_count.yaml └── production ├── cpu_count.yaml ├── kustomization.yaml └── replica_count.yaml
staging, develop, production環境といった環境差分をoverlaysで表現するのはKustomizeの方がいいかもしれませんが、Kustomizeの基本的なユースケースに照らし合わせてジョブを作成するとなると、次の例ようにCronJob毎にディレクトリを切る必要があり、管理コストが嵩んでしまいます。
Kustomizeで多数CronJobを作成する
例えば24個のAジョブと24個のBジョブ計48個作成する場合、overlays下に48個のディレクトリ構成ができてしまいます。以下がサンプルになります。
~/someApp ├── base │ ├── cronjob.yaml │ └── kustomization.yaml └── overlays ├── 0101-job-a │ ├── category.yaml │ ├── kustomization.yaml │ └── table.yaml ├── 0102-job-a │ ├── category.yaml │ ├── kustomization.yaml │ └── table.yaml ├── 0103-job-a │ ├── category.yaml │ ├── kustomization.yaml │ └── table.yaml │ ︙ # 増えるだけ用意しないといけなくなる └── 0224-job-b ├── caterory.yaml ├── kustomization.yaml └── table.yaml
バッチジョブが20を超えてくるとなるとディレクトリで分割するのは現実的に厳しいと思います。
参考:Kustomize で CronJob を同一テンプレートからスケジュール毎に生成する
以上の理由から、KustomizeではなくHelmを利用することを決めました。
Helmの場合
Helmではアクションと呼ばれる制御構造によってリストをループ処理することができます。したがって、今回のケースではこちらを採用することにしました。
Helm, Tillerのバージョンはv2.14.3を使用しています。
今回の要件として、
- 同一のテンプレートをベースとして、
- DBのテーブル毎に含まれる、
- 複数の地域情報を取り出し処理ができる2
バッチジョブを作成する必要がありました。
これを実現するためには先述したループ処理を行う必要があります。
実現方法としては「ループ処理をネストすれば可能」というのが答えなのですが、少しだけハマりどころがあったので、それも合わせてお話できればと思います。
通常のCronJobをループ処理する場合、values.yaml
に回したい変数のリストを作成し、range
を入れればループ処理が可能です。
単一のリストを利用してCronJobをループ処理する
まずはスケジュール毎にCronJobを作成したい場合を想定してみましょう。
以下の実行例ではスケジュール毎にスケジュールのインデックスをecho
で出力するという設定をyamlで行います。動作確認したい場合はhelm test
を利用しましょう。
test-schedule-cj.yaml
{{- range $index, $schedule := .Values.global.schedules }} --- # 複数作成の為に必須 apiVersion: batch/v1beta1 kind: CronJob metadata: name: test-{{ $index | add1 }} # CronJob毎に一意になるような名前をつける必要がある namespace: {{ $namespace }} labels: chart: "{{ $chartName }}-{{ $chartVersion }}" release: "{{ $releaseName }}" heritage: "{{ $releaseService }}" spec: schedule: {{ $schedule }} suspend: false successfulJobsHistoryLimit: 1 failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: serviceAccount: {{ $serviceAccount }} containers: - image: "{{ $imageRepository }}:{{ $imageTag }}" name: test # テストで検証するのであれば、busyboxイメージでechoを出力します。 args: - /bin/sh - -c - date; echo Index count is $(INDEX). env: name: INDEX value: {{ $index | add1 }} restartPolicy: OnFailure {{- end }}
因みにyamlの可読性の為、最初に以下のような代入を行っています。(以降、省略)
test-schedule-cj.yaml
# global {{- $namespace := .Values.global.namespace }} {{- $serviceAccount := .Values.global.serviceAccount }} {{- $imageRepository := .Values.global.imageRepository }} {{- $imageTag := .Values.globa.imageTag }} # chart {{- $chartName := .Chart.Name }} {{- $chartVersion := .Chart.Version }} {{- $releaseName := .Release.Name }} {{- $releaseService := .Release.Service }}
values.yamlの設定は以下のようになります。
vaules.yaml
global: # kubernetes namespace: test-ns serviceAccount: test-sa # image imageRepository: busybox imageTag: latest schedules: [ '0 0 * * *', '10 0 * * *', '20 0 * * *', '30 0 * * *' ]
特に難しい部分はなく、range
アクションで作成したいCronJobを囲むだけです。
ただ一つ注意しないといけない点として、CronJobの名前は一意になるようにしなければいけません。
その為、全ての名前が一意になるように命名規則を考えてつけましょう。
基本的にはループで回す変数名をつけるようにすれば大丈夫だと思います。3
作成後、以下のhelmコマンドを実行します。
$ helm upgrade --install job01 .
これでテーブルの数だけジョブを回すことができるChartのリリースができますね。
複数のリストを利用してCronJobをループ処理する
一つの変数の条件でループ処理ができたので、複数の変数のリストを使用してループする場合を考えてみましょう。 スケジュールのループ処理内にテーブルのループ処理をネストするだけで作成できます。 記述例としては以下のようになります。
test-schedule-table-cj.yaml
{{- range $index, $schedule := .Values.global.schedules }} {{- range $table := .Values.global.tables }} --- apiVersion: batch/v1beta1 kind: CronJob metadata: name: test-{{ $index | add1 }} namespace: {{ $namespace }} labels: chart: "{{ $chartName }}-{{ $chartVersion }}" release: "{{ $releaseName }}" heritage: "{{ $releaseService }}" spec: schedule: {{ $schedule }} suspend: false successfulJobsHistoryLimit: 1 failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: serviceAccount: {{ $serviceAccount }} containers: - image: "{{ $imageRepository }}:{{ $imageTag }}" name: test args: - /bin/sh - -c - date; echo Index count is $(INDEX). Table name is $(TABLE_NAME). env: name: INDEX value: {{ $index | add1 }} name: TABLE_NAME value: {{ $tablel.name }} restartPolicy: OnFailure {{- end }} {{- end }}
values.yaml
global: # kubernetes namespace: test-ns serviceAccount: test-sa # image imageRepository: busybox imageTag: latest # schedule info schedules: [ '0 0 * * *', '10 0 * * *', '20 0 * * *', '30 0 * * *' ] # table info tables: - name: XxxTest category: walk - name: YyyTest category: walk - name: ZzzTest category: run
しかし、例のようにスケジュール×テーブルという変数でジョブを回そうとすると失敗してしまいます。 以下が失敗の際に出力されるエラーになります。
UPGRADE FAILED Error: render error in "test-loop/templates/est-schedule-table-cj.yaml": template: test-loop/templates/est-schedule-table-cj.yaml:15:28: executing "test-loop/templates/test-schedule-table-cj.yaml" at <.Values.global.tables>: can't evaluate field Values in type interface {} Error: UPGRADE FAILED: render error in "test-loop/templates/est-schedule-table-cj.yaml": template: test-loop/templates/est-schedule-table-cj.yaml:15:28: executing "test-loop/templates/est-schedule-table-cj.yaml" at <.Values.global.tables>: can't evaluate field Values in type interface {}
失敗の原因は.
のカレントスコープが「.Values.global.schedules」を向いているためで、.Values.global.region
が習得できません。
回避策としては、ループの処理の際に、「常にグローバルスコープを持つ$
を使用する」ことでこのエラーを回避できます。
したがって、以下のdiff
の変更点のように最初の記述でループしたい変数を別の変数に代入することでスコープを変えないままネストしたループ処理ができるようになります。
また、CronJobのリソース名も一意にするため、テーブルの名前を更新しておきましょう。
test-schedule-table-cj.yaml
+ {{- $regions := .Values.global.tables }} - {{- range $index, $schedule := .Values.global.schedules }} - {{- range $table := .Values.global.tables }} + {{- range $index, $schedule := .Values.global.schedules }} + {{- range $table := $tables }} - name: test-{{ $index | add1 }} + name: test-{{ $index | add1 }}-{{ $table.name }} # 名前を一意にするため
しかし、これでもまだ十分ではありません。あくまでスケジュールのindexがほしいのではなく、スケジュール(cron)毎に習得される地域の変数をCronJobのmetadataや環境変数に代入したいのです。
スケジュールの時間毎に、異なる地域のバッチジョブを実行する
golangのSprig libraryを利用してリストを取得するようにしています。
このような形にしたのは、複数のバッチジョブを後述するsubchartに記述する際、values.yaml
をDRYにするためです。
全てのリストを1つづつ取得する関数がなかったので、次のような形で再現しています。
test-region-table-cj.yaml
{{- $regions := .Values.global.tables }} {{- range $index, $schedule := $schedules }} {{- range $table := $tables }} # スケジュール毎に地域のリストを取得する {{- $region := slice $regions $index ( $index | add1 ) | first }} --- apiVersion: batch/v1beta1 kind: CronJob metadata: name: test-{{ $region }}-{{ $table.name }} # indexではなく地域別で名前をつけている namespace: {{ $namespace }} labels: chart: "{{ $chartName }}-{{ $chartVersion }}" release: "{{ $releaseName }}" heritage: "{{ $releaseService }}" spec: schedule: {{ $schedule }} suspend: false successfulJobsHistoryLimit: 1 failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: serviceAccount: {{ $serviceAccount }} containers: - image: "{{ $imageRepository }}:{{ $imageTag }}" name: {{ $name }} args: - /bin/sh - -c - date; echo This chart has region $(REGION) and table $(TABLE_NAME). env: - name: TABLE_NAME value: "{{ $table.name }}" - name: TABLE_CATEGORY value: "{{ $table.category }}" - name: REGION value: {{ $region | quote }} restartPolicy: OnFailure {{- end }} {{- end }}
values.yaml
にはglobal.regions
を、schedule
のリスト長と同一のリストを加えましょう。4
values.yaml
+ regions: [japan, us, uk, china]
これで、地域別のDBのテーブルの情報が取得できるようになりましたね。
サブチャートを作成して、一連のワークフローを一つのChartで再現する
実際には上記のようなバッチジョブが後続に存在しているので、これらをひとまとまりにして扱う必要があります。 最後に、これらバッチジョブをひまとまりにして処理する方法を学びましょう。
複数のjob(Dockerイメージが別)を扱うためにsubchartを採用しました。 ServiceAccount関連のリソースやNetworkPolicyやNamespaceのようなグローバルなリソースは大本のChartにまとめ、各ジョブをsubchartに入れるという形を取ります。
values.yaml
の項目で、globalの変数と、そうでないsubchart毎のローカル変数を使いわけることで実現できます。
以下にサンプルのディレクトリ構成を挙げます。
. ├── Chart.yaml ├── charts │ ├── 0101-job │ │ ├── Chart.yaml │ │ └── templates │ │ ├── _helpers.tpl │ │ ├── xxx-cronjob.yaml │ │ └── tests │ │ └── test-xxx.yaml │ ├── 0102-job │ │ ├── Chart.yaml │ │ └── templates │ │ ├── _helpers.tpl │ │ ├── yyy-cronjob.yaml │ │ └── tests │ │ └── test-yyy.yaml │ └── 0103-job │ ├── Chart.yaml │ └── templates │ ├── _helpers.tpl │ └── zzz-cronjob.yaml │ └── tests │ └── test-zzz.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── namespace.yaml │ ├── networkpolicy.yaml │ ├── secrets │ │ ︙ │ └── serviceaccount.yaml └── values.yaml
個別のyaml設定の注意点
各サブチャート毎に分化されたschedules
はグローバルの設定ではなく、個別のジョブ毎のローカルの設定になるため、各subchart内では、.Values.global.schedules
から.Values.schedules
のようにグローバルのスコープをローカルのスコープに変更しておきましょう。
+ {{- $category := .Values.schedules }} - {{- $tables := .Values.global.schedules }}
フラグで実行するジョブバッチを選択する
後でsubhart毎にテストをするには、個別のsubchartだけを実行する必要があります。
そのため、enabled
を各subchartに記述します。
以下のように各CronJobリソースを条件式{{- if .Values.enabled }}
で挟んで、
test-subchart-cj.yaml
{{- if .Values.enabled }} {{- range $index, $schedule := $schedules }} {{- range $table := $tables }} --- apiVersion: batch/v1beta1 ︙ {{- end}} {{- end}} {{- end}}
values.yaml
に以下の設定を入れます。
values.yaml
︙ 0101-job: enabled: true # 全てtrueにしておく imageRepository: xxx ︙
実行時に不必要なジョブに対してfalseのフラグを立てることで個別実行が実現できます。
$ helm upgrade --install jobs --set 0101-job.enabled=true --set 0102-job.enabled=false --set 0103-job.enabled=false .
上記は0101-jobだけがChartだけがデプロイされる例になります。
テストに関して1つ注意点があります。 テストを行う毎はテストの実行ジョブの順番が変わるのでご注意下さい。5
完成したvalues.yaml
最終的なvalues.yamlは次のようになります。
values.yaml
global: # kubernetes namespace: test-ns serviceAccount: test-sa # table info tables: - name: XxxTest category: walk - name: YyyTest category: walk - name: ZzzTest category: run regions: [japan, us, uk, china] # test config tests: tables: - name: XxxTest category: walk regions: [japan, china] 0101-job: enabled: true imageRepository: xxx imageTag: 1.0.0 schedules: [ '0 0 * * *', '10 0 * * *', '20 0 * * *', '30 0 * * *' ] 0102-job: enabled: true imageRepository: yyy imageTag: 1.0.0 schedule: '0 1 * * *' 0103-job: enabled: true imageRepository: zzz imageTag: 0.1.0 schedules: [ '0 2 * * *', '10 2 * * *', '20 2 * * *', '30 2 * * *' ]
おわりに
今回ループ処理化したCronJob Aが成功したらCronJob Bを実行すると言ったワークフローは単純なスケジュール(cron)でHelm Chart化しました。
その他ハマりどころとしてはvalues.yaml
のsubchart名と一致しないChart名、ディレクトリ名になっているとチャートがデプロイされないという事例がありました。案外、見落とします。
名前を変更した際には確認するようにしましょう。
最後に注意点で、今回のエントリでは実運用で想定するようなTLSの暗号化通信やSecurityContext、Compute Resourcesなど省略しているので、それぞれの環境に合わせて設定していただればと思います。
以上!
仲間募集
ユーザベースのSPEEDA SREチームは、No Challenge, No SRE, No SPEEDA を掲げて業務に取り組んでいます。
「挑戦しなければ、SREではないし、SREがなければ、SPEEDAもない」という意識で、日々ユーザベースのMissionである、「経済情報で、世界をかえる」の実現に向けて邁進しています。
少しでも興味を持ってくださった方はこちらまで!