Azure Batchを使ってコンテナでバッチを動かそう

# はじめに

コンテナにロジックを閉じ込め任意のインスタンスで動かすことができるAWS Batchが便利で使っていたのですが、今の案件はAzureでしたのでAzure Batchを使ってみました。これもコンセプトはAWSと同じです。
Azureは相変わらずドキュメントやQiitaなどにもあまり記事がないので少しでもお役に立てれば幸いです。

ちなみに今回JavaSDKで実装していますが、画面上からバッチの定義を行ったり他の言語のSDKを使った場合も同じ設定値に設定することで動かすことができますので参考になるかと思います。

# Azure Batch

AzureBatchでは任意のプログラム（Shellも）を、指定された大きさのインスタンスで動かすことが可能なサービスです。
AWS Batchのようにコンテナ内のプログラムを動かすことが可能です。ちなみにAWS（※）ではCloudWatchEventsを使ってスケジュールさせていましたが、AzureBatchではスケジュール機能が組み込まれています。

※ AWSの場合は、CloudWatchEvents -> Lambda -> AWS Batchって感じで動かす必要があります

# 前準備

コンテナで動かすプログラムはこんな感じです。
ちゃんと指定されたインスタンスで動いているかどうか確認するためにOSなどの情報を出してみましょう。

ちなみにJavaでOSの情報をだすには[https://github.com/oshi/oshi](https://github.com/oshi/oshi)が便利ですよ。

```java
public class BatchMain {

    public static void main(String[] args) {

        System.out.println("execute main method.");

        System.out.println("Environment Variables");
        System.getenv().forEach((k,v) -> System.out.println(k + ":" + v));

        var si = new SystemInfo();
        System.out.println("OS: " + si.getOperatingSystem());

        var hardware = si.getHardware();
        System.out.println("Processor: " + hardware.getProcessor());

        var memory = hardware.getMemory();
        System.out.println("Memory: " + memory.getAvailable() + " / " + memory.getTotal());

        System.exit(0);
    }
}
```

さてこれをAzure Container Registryにpushしていきます。いつも通りjibを使ってpushします。
gradle.propertiesにUSERNAMEとPASSWORDを忘れずに書いておいてください。

```gradle
jib {
    to {
        image = 'xxxxx.azurecr.io/batch:0.0.1-SNAPSHOT'
        auth {
            username = "${USERNAME}"
            password = "${PASSWORD}"
        }
    }
}
```

```shell
gradlew jib
```

# Azure Batchのアカウントを作る

「batch accounts」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/4dfb095d-f1a9-6d5d-161a-a5df0415e56d.png)


batch アカウントを作りましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/1e57797d-300f-c350-139e-a8635eb729b8.png)

batch account, url, primary access keyをコピーしておきます。これはプログラムで指定するときに利用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/f720ed2d-3f77-10b0-42b8-840a8a805ed1.png)



# プールを作る

Azure Batchでは実行環境の定義をPoolで行います。プールの設定はジョブごとに作る必要はなく環境が変わらないのであれば一つ大丈夫です。AWSでいうとComputing Environmentかな。

サポートされる仮想マシンイメージ(Linux)は[こちら](https://docs.microsoft.com/ja-jp/azure/batch/batch-docker-container-workloads)に記載がありますがcentos or ubuntuのみになります。
自分で用意することももちろん可能です。

指定する値はAzureの管理コンソールから探したほうが簡単でしょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/58c1fd1f-2c88-2d17-85d7-e286d9a995d6.png)



```java

public class Pool {

    public static final String POOL_ID = "pool_1";

    public static void main(String[] args) throws IOException {
        BatchClient client = BatchClient.open(new BatchSharedKeyCredentials(
                "https://xxxxx.japaneast.batch.azure.com",
                "xxxx",
                "xxxxxx"));

        ContainerRegistry containerRegistry = new ContainerRegistry()
                .withRegistryServer("xxxxxx.azurecr.io")
                .withUserName("xxxxxx")
                .withPassword("xxxxxxxxx");

        ContainerConfiguration containerConfiguration = new ContainerConfiguration();
        containerConfiguration.withContainerRegistries(Arrays.asList(containerRegistry));
        containerConfiguration.withContainerImageNames(Arrays.asList("xxxx.azurecr.io/batch:0.0.1-SNAPSHOT"));

        ImageReference imageReference = new ImageReference();
        imageReference.withPublisher("microsoft-azure-batch");
        imageReference.withOffer("ubuntu-server-container");
        imageReference.withSku("16-04-lts");
        imageReference.withVersion("latest");

        // VM Configuration
        VirtualMachineConfiguration virtualMachineConfiguration = new VirtualMachineConfiguration();
        virtualMachineConfiguration.withImageReference(imageReference);
        virtualMachineConfiguration.withContainerConfiguration(containerConfiguration);
        virtualMachineConfiguration.withNodeAgentSKUId("batch.node.ubuntu 16.04");

        // Create Pool
        client.poolOperations().createPool(POOL_ID, "standard_d1_v2", virtualMachineConfiguration, 1);
    }

}
```

# シンプルに動かす

Poolで定義した情報をもとにJOB、Taskを動かしてみます。
まずはJOBを作ります。

```java

public class SimpleJob {

    public static final String POOL_ID = "pool_1";

    public static final String JOB_ID = "job_1";

    public static void main(String[] args) throws IOException {
        BatchClient client = BatchClient.open(new BatchSharedKeyCredentials(
                "https://xxxxx.japaneast.batch.azure.com",
                "xxxx",
                "xxxxxx"));

        PoolInformation poolInformation = new PoolInformation();
        poolInformation.withPoolId(POOL_ID);

        // CreateJob
        client.jobOperations().createJob(JOB_ID, poolInformation);
    }
}
```

JOB内でタスクを動かします。

```java
public class SimpleTask {

    public static void main(String[] args) throws IOException, InterruptedException {

        BatchClient client = BatchClient.open(new BatchSharedKeyCredentials(
                "https://xxxxx.japaneast.batch.azure.com",
                "xxxx",
                "xxxxxx"));

        TaskAddParameter parameter = new TaskAddParameter();
        parameter.withId("task1")
                .withUserIdentity(new UserIdentity().withAutoUser(new AutoUserSpecification().withElevationLevel(ElevationLevel.ADMIN).withScope(AutoUserScope.TASK)))
                .withContainerSettings(new TaskContainerSettings().withImageName("xxxx.azurecr.io/batch:0.0.1-SNAPSHOT")
                        .withContainerRunOptions("--rm"))
                .withConstraints(new TaskConstraints().withMaxTaskRetryCount(-1)).withCommandLine("");
        client.taskOperations().createTask(ScheduleJob.JOB_ID, parameter);

        long timeout = 300 * 1000;
        long startTime = System.currentTimeMillis();

        while (System.currentTimeMillis() - startTime <= timeout) {
            CloudTask task = client.taskOperations().getTask(ScheduleJob.JOB_ID, parameter.id());

            if (task.state() != TaskState.COMPLETED) {
                Thread.sleep(1000);
            }
        }
    }
}
```

これを実行するとAzureの管理画面上で確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/7fb697e8-2c71-4ee6-adf3-36b38e8eaadf.png)

動いたJOBをクリックすると、Taskの画面が表示され出力された内容は画面上から見ることができます。

# 指定された時間にJOBを実行する

JOBの時間をwithDoNotRunUntilで指定します。JOBが実行されると動くタスクも合わせて定義します。

```java

public class ScheduleJob {

    public static final String POOL_ID = "pool_1";

    public static final String JOB_ID = "job_5";

    public static void main(String[] args) throws IOException {
        BatchClient client = BatchClient.open(new BatchSharedKeyCredentials(
                "https://xxxxx.japaneast.batch.azure.com",
                "xxxx",
                "xxxxxx"));

        // Create Schedule SimpleJob
        DateTime scheduleDateTime = new DateTime(2019,8,22,4,30, DateTimeZone.UTC);
        Schedule schedule = new Schedule().withDoNotRunUntil(scheduleDateTime);

        // pool
        PoolInformation poolInformation = new PoolInformation();
        poolInformation.withPoolId(POOL_ID);

        // container
        JobManagerTask task = new JobManagerTask();
        task.withId("scheduletask")
                .withUserIdentity(new UserIdentity().withAutoUser(new AutoUserSpecification().withElevationLevel(ElevationLevel.ADMIN).withScope(AutoUserScope.TASK)))
                .withContainerSettings(new TaskContainerSettings().withImageName("xxxxx.azurecr.io/batch:0.0.1-SNAPSHOT")
                        .withContainerRunOptions("--rm"))
                .withCommandLine("");

        JobSpecification jobSpecification = new JobSpecification().withPoolInfo(poolInformation).withJobManagerTask(task);

        client.jobScheduleOperations().createJobSchedule(JOB_ID,schedule, jobSpecification);
    }
}
```

# 指定された時間から繰り返し動かす（Cron）

初回に動く時刻をDoNotRunUntilで指定し、RecurrenceIntervalで繰り返し動かす時間を指定します。
DoNotRunUntilを指定しない場合は、JOBを定義された時間から繰り返し動かすことになります。

```java
public class CronScheduleJob {

    public static final String POOL_ID = "pool_1";

    public static final String JOB_ID = "job_7";

    public static void main(String[] args) throws IOException {
        BatchClient client = BatchClient.open(new BatchSharedKeyCredentials(
                "https://xxxxx.japaneast.batch.azure.com",
                "xxxx",
                "xxxxxx"));

        // Create Schedule SimpleJob
        DateTime startDateTime = new DateTime(2019,8,22,4,45, DateTimeZone.UTC);
        Period period = new Period(0,1,0,0);
        Schedule schedule = new Schedule().withRecurrenceInterval(period).withDoNotRunUntil(startDateTime);

        // pool
        PoolInformation poolInformation = new PoolInformation();
        poolInformation.withPoolId(POOL_ID);

        // container
        JobManagerTask task = new JobManagerTask();
        task.withId("cronscheduletask")
                .withUserIdentity(new UserIdentity().withAutoUser(new AutoUserSpecification().withElevationLevel(ElevationLevel.ADMIN).withScope(AutoUserScope.TASK)))
                .withContainerSettings(new TaskContainerSettings().withImageName("xxxxx.azurecr.io/batch:0.0.1-SNAPSHOT")
                        .withContainerRunOptions("--rm"))
                .withCommandLine("");

        JobSpecification jobSpecification = new JobSpecification().withPoolInfo(poolInformation).withJobManagerTask(task);

        client.jobScheduleOperations().createJobSchedule(JOB_ID,schedule, jobSpecification);
    }
}

```

# 参考

* [Azure Batchのドキュメント](https://docs.microsoft.com/ja-jp/azure/batch/)
* [Introducing Linux support on Azure Batch](https://blogs.technet.microsoft.com/windowshpc/2016/03/29/introducing-linux-support-on-azure-batch/)