GoとKubernetesを用いたバッチ開発のすゝめ

Voicyアドベントカレンダー 2日目の記事です。

こんにちは株式会社Voicyでバックエンドエンジニアをしているごろうと申します。

普段の発信は会社のエンジニアメンバーでやっているvoi-chordというVoicyチャンネルで行っていることが多いので、ぜひそちらもお聞きください!

voicy.jp

今回のAdvent Calenderは調べてみるとあまり文献の多くなかったGoとKubernetesバッチの開発について書いていきます。

主にKubernetesやGoをサービスで利用している方には参考になる内容になっているかと思いますので、ぜひご覧ください!

kubernetesでのバッチ開発のすゝめ

弊社では、APIなどのアプリケーションはほぼ全てGo言語で書かれており、Kubernetes(EKS)上で管理されています。 そのため定期的に実行するバッチに関しても同じように管理したい!と思いGoとkubernetesで定期実行などのバッチ処理を作りました。

そして、実際にやってみると以下のようなメリットがありました。

  • 他アプリケーションと同じくk8sで管理できる
  • リソースの管理をAPIなどのアプリケーションとまとめて可能
  • Datadogなど外部ツールで監視対象増やすことなくJobの実行状況、Podの状態が監視できる
  • JobやCronjobの実行をYAMLで管理できる→GitOpsしている
  • kubernetesの充実した関連ツールを利用できる
  • アプリケーションと同じ言語で書けるので楽かつ共通処理はそのまま利用できるので開発効率が上がる

使用するGoライブラリ

GoのCLIツールを作るライブラリはいくつかあります。

今回は社内でよく使用されるCLIツールであるcobraをご紹介したいと思います。

cobraはGoの開発チームメンバーでもあるspf13さんによって作られたもので、スター数も2万4千を超えています。 github.com

cobraの使い方

CLIの作成方法は以下のような形になります。

rootCmdを定義した後に、実際の処理をするsubcmdをAddしていく形でいろいろな種類のバッチを作成することが可能です。

また、subcmdは別パッケージで定義することでバッチの種類が増えても管理しやすくなるのでおすすめです。

オプションフラグに関してもboolやstringなど様々な型を設定することが可能です。

   ctx := context.Background()

    rootCmd := &cobra.Command{Use: "batch"}

    rootCmd.AddCommand(subcmd.HogeHogeCommand(ctx))
    rootCmd.AddCommand(subcmd.FooBarCommand(ctx))


type hogehogeOptions struct {
    Notice bool
    Dryrun bool
}

func (opt *hogehogeOptions) Run(ctx context.Context, w io.Writer) error {
    //ここに必要な処理を書く
    return nil
}

func HogeHogeCommand(ctx context.Context) *cobra.Command {
    opt := &approveMonthlySubscribeOptions{}
    cmd := &cobra.Command{
        Use:   "hogehoge",
        Short: "処理の説明",
        Run: func(cmd *cobra.Command, args []string) {
            if err := opt.Run(ctx, cmd.OutOrStdout()); err != nil {
                panic(err)
            }
        },
    }
    cmd.Flags().BoolVarP(&opt.Notice, "notice", "", opt.Notice, "notice")
    cmd.Flags().BoolVarP(&opt.Dryrun, "dryrun", "", opt.Dryrun, "dryrun")
    return cmd
}

ここでは一部しか利用していませんが、Command構造体は様々な要素が入っており、よりリッチで使いやすいコマンドにすることも可能です。エイリアスなどは便利そうですね。 https://github.com/spf13/cobra/blob/9e1d6f1c2aa8df64b6a6ba39e92517f68580d653/command.go#L38

あとはDockerfileを作ってあげて、ビルドした後にrootCmdとして定義したbatchコマンドを実行すれば定義してSubcmdを実行できる状態になります。

CMD [ "..../batch" ]

Kubernetesでバッチを実行する方法

kubernetesでバッチを定期実行する方法としてはCronJobを利用します

CronJobは、Cronと同じフォーマットでいつジョブを実行するかを指定します。指定した時間になるとCronJobはJobを生成し、そのJobはPodを生成することで、Podが任意のジョブを実行する仕組みです。

定義は以下のようになります。

「args」において実際に動かしたいコマンド名を書くことにより、バッチの実行が可能になります。オプションがある場合はそこに続けて記載する形になります。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: name
  namespace: namespace
spec:
  schedule: "00 18 * * * "
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hogehoge
            image: <image名>
            args:
            - /app/batch
            - hogehoge
          restartPolicy: Never

YAML定義でいくつかポイントになってくる設定があるので解説します。

schedule

Cronと全く同じフォーマットが使われます。 例えば「00 18 * * *」とすれば毎日18:00に起動します。(kube-controller-managerのタイムゾーンを変更しない限りはUTCになるので注意)

毎時0分に起動したい場合は「0 * * * 」毎分実行したい場合は「/1 * * * *」のように記載します。

restartPolicy

Jobの失敗時の挙動になります。

OnFailure:再作成される

Never:再作成されない

concurrencyPolicy

並行稼働のためのポリシーです。1分毎など短い間隔で実行するJobの場合デフォルトのままだと並行稼働する可能性があるので注意が必要です。

Allow:並列稼働を許容する

Forbid:並列稼働を許容しない。

Replace:前回の起動時刻に起動したJobがRunning状態である場合、そのJobを停止して新たなJobを生成する

startingDeadlineSeconds

何らかの理由でscheduleに指定されて時刻になってもJobが起動出来なかった場合でも、何秒後までなら起動しても良いかを指定することができる

cronjobについてもっと詳しく知りたい方は公式ドキュメントなどが非常に参考になるため、是非ご覧ください

kubernetes.io

バッチ開発のTips

最後にバッチを作成する上で気にすべきTipsについて記載します。

オプションを活用する

    cmd.Flags().BoolVarP(&opt.Notice, "notice", "", opt.Notice, "notice")
    cmd.Flags().BoolVarP(&opt.Dryrun, "dryrun", "", opt.Dryrun, "dryrun")

上で例で定義したものにも2つのフラグをオプションとして定義しましたが、バッチの動作確認をする際などに利用できるオプションを追加すると非常に便利です。

例えばメールなどの通知の送信やデータ更新を伴うバッチにおいて処理の確認などをしたい場合にnoticeフラグをつけると通知がされない、dryrunフラグをつけるとデータの更新がされず処理内容のログが出るだけにするなどをアプリケーション側で制御できるようにすると、動作確認が簡単にできるようになり、非常に便利です。

Jobごとの責務を軽くする

例えば大量のデータを一気に処理する場合には、1つのバッチの処理する量を減らし、複数のJobを実行する形にする。処理が複数の段階にわかれるものに関しては段階毎にJobを実行するなど、一つのJob毎の責務を分離することでリトライ設計や失敗時のリカバリがしやすく管理のしやすいものになると思います。

バッチ失敗時のリカバリを用意しておく

定期的に実行するcronjobに関しては、時には失敗することもあるかと思います。 そrestartPolicyにOnFailureが設定されていればJob自体が失敗した場合には再実行されますが、Jobdは成功しているが処理が想定通りに動いていないなどが発生し得ます。

その際にcronjobをjobとして実行するコマンドがあるので、直接cronjobからJobを実行する以下のようなコマンドを実行すると良いでしょう。

kubectl create job job-name --from=cronjob/cronjob-name -n namespace

もしくはアプリケーション側で冪等性が担保されているリトライの仕組みを入れておくなどいくつか対策やリカバリ方法を考えておくと失敗しづらく、仮に失敗した場合でも素早く対応できるかと思います。

まとめ

最後までお読みいただきありがとうございました。 読んだ方の参考に少しでもなれば幸いです。 次回は同じくバックエンドエンジニアの @miyukiaizawaさんです! お楽しみに!