JavaからKubernetesのJobを実行してみる

## 概要
KubernetesのAPIを呼び出すJavaのラッパーライブラリが実は公式で用意されており、Javaアプリケーションから比較的簡単にKubernetesの操作を行うことができる。

- https://github.com/kubernetes-client/java

今回はJobを起動するREST APIをSpring Bootで作成してみる。

## 環境

- Java 8
- Spring Boot 1.5.10.RELEASE
- Kubernetes 1.8

## 今回のアプリの仕様

1. [公式ドキュメント](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/)にあるpiを算出するJobサンプルを実行する
1. Jobの定義はYAMLファイルから読み込む
    - client-javaはJSON(GSON)を想定しているので、YAML->JSON変換してバインドする 
1. Job名は一意になるように日付を付加する
1. 認証はサービスアカウントのkubernetes configを使う

## 実装

### プロジェクトの作成

```sh
curl -s https://start.spring.io/starter.tgz \
    -d baseDir=k8s-job-sample \
    -d type=gradle-project \
    -d javaVersion=1.8 \
    -d dependencies=web,lombok | tar -xzvf -
```

build.gradleに依存を追加

```gradle
compile('io.kubernetes:client-java:1.0.0-beta2')
```

### Job manifestを設置
```yaml:src/main/resources/job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    spec:
      containers:
      - name: pi
        image: perl
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(100)"]
      restartPolicy: Never
  backoffLimit: 4
```

### Bean定義

```java:DemoApplication.java
package com.example.demo;

import com.google.gson.Gson;
import io.kubernetes.client.ApiClient;
import io.kubernetes.client.apis.BatchV1Api;
import io.kubernetes.client.util.Config;
import java.io.IOException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public Gson gson() {
        return new Gson();
    }

    @Bean
    public Yaml yaml() {
        return new Yaml(new SafeConstructor());
    }

    @Bean
    public ApiClient apiClient() throws IOException {
        return Config.fromConfig("/path/to/k8s-config.yaml");
    }

    @Bean
    public BatchV1Api batchV1Api(ApiClient apiClient) {
        return new BatchV1Api(apiClient);
    }

}
```
※簡易化のため、Applicationクラスに記述

### Controller

```java:JobController.java
package com.example.demo;

import com.google.gson.Gson;
import io.kubernetes.client.ApiException;
import io.kubernetes.client.apis.BatchV1Api;
import io.kubernetes.client.models.V1Job;
import io.kubernetes.client.models.V1ObjectMeta;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ResourceLoader;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.Yaml;

@RestController
@RequestMapping("jobs")
@RequiredArgsConstructor
public class JobController {
    private final BatchV1Api batchApi;
    private final ResourceLoader resourceLoader;
    private final Gson gson;
    private final Yaml yaml;
    private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");

    @PostMapping
    public V1Job executeJob(@RequestParam(required = false, defaultValue = "default") String nameSpace)
            throws IOException, ApiException {

        V1Job job = loadJobFromYaml("job.yaml");

        V1ObjectMeta metaData = job.getMetadata();
        metaData.setName(String.format("%s-%s", metaData.getName(), dateTimeFormatter.format(LocalDateTime.now())));

        return batchApi.createNamespacedJob(nameSpace, job, null);
    }

    private V1Job loadJobFromYaml(String fileName) throws IOException {
        Map jobManifest = (Map) yaml.load(
                resourceLoader.getResource(ResourceLoader.CLASSPATH_URL_PREFIX + fileName).getInputStream());

        return gson.fromJson(gson.toJson(jobManifest), V1Job.class);
    }
}
```

## 動作確認

```
$ curl -X POST http://localhost:8080/jobs?nameSpace=develop
```